TESS Network Motivation Survey (Light Pollution Citizen Science)¶

Category: Light Pollution · Size: 5.4 MB · Format: CSV, HTML, JSON, R, TTL License: CC-BY-4.0 · Zenodo record · Data sheet on the CSDH

Motivation survey data from ~120 volunteers hosting TESS photometers to measure night-sky brightness and light pollution at a global scale.

The data is mounted read-only at /srv/data/tess-light-pollution/. Save anything you produce in your personal folder (~/).

What's really in this dataset¶

Despite the name motivation survey, the folder ships a full research package: an RO-Crate manifest, the survey ontology as Turtle (.ttl), the original R analysis script, and two CSVs that hold the actual numbers:

  • tess-network-results.csv — the raw survey, in long format: one row per answer (question x volunteer).
  • tess-network-analysis-results.csv — the authors' published summary (10 motivation values with means and correlations).

The survey is built on Schwartz's theory of basic human values: it measures ten motivational drivers (achievement, benevolence, universalism, power, ...) with two 1-5 questions each, and asks an overall "how motivated are you?" rating. Our goal: rebuild the volunteer motivation profile from the raw answers and see which values go hand-in-hand with staying motivated.

In [1]:
from pathlib import Path

DATA = Path('/srv/data/tess-light-pollution')
for f in sorted(DATA.rglob('*')):
    if f.is_file():
        print(f"{f.relative_to(DATA)}  ({f.stat().st_size/1e3:,.0f} kB)")
ro-crate-metadata.json  (16 kB)
ro-crate-preview.html  (48 kB)
tess-network-analysis-results.csv  (1 kB)
tess-network-analysis-script.R  (7 kB)
tess-network-procedure.ttl  (90 kB)
tess-network-results.csv  (534 kB)
tess-network-results.ttl  (2,310 kB)
tess-network-survey.ttl  (2,401 kB)

The raw survey (long format)¶

Each row is a single answer. The columns that matter for us: user (anonymised volunteer id), tag (which motivation value the question probes), question, questionType (star / slider / emoji / options ...), and value (the numeric answer, 1-5 on the rating questions). Let's load it and look at how the survey is organised.

In [2]:
import pandas as pd
import numpy as np

raw = pd.read_csv(DATA / 'tess-network-results.csv')

# Drop the developers' test accounts and blank users (people who never really started)
raw = raw[raw['user'].notna() & (raw['user'].astype(str).str.strip() != '')]
raw = raw[~raw['user'].isin(['testCefriel', 'testCefriel2'])]

print(f"{len(raw):,} answers from {raw['user'].nunique()} volunteers")
print('\nquestion formats:')
print(raw['questionType'].value_counts())
raw[['user', 'tag', 'questionType', 'question', 'value']].head()
2,915 answers from 90 volunteers

question formats:
questionType
options     1172
star         781
slider       357
emoji        261
checkbox     163
select        97
text          84
Name: count, dtype: int64
Out[2]:
user tag questionType question value
1 u242978450 global motivation star How much are you motivated in participating to... 5
2 u242978359 global motivation star How much are you motivated in participating to... 5
3 u242980556 global motivation star How much are you motivated in participating to... 5
4 u242980399 global motivation star How much are you motivated in participating to... 5
5 u242980552 global motivation star How much are you motivated in participating to... 5

Building each volunteer's motivation profile¶

The ten Schwartz values are each measured by two rating questions (on a 1-5 scale). Following the authors' method, for every volunteer we average their answers within each value to get a single 1-5 score per motivation. That turns the long table into a tidy volunteers x motivations matrix. We also pull each volunteer's overall motivation rating (question 1011, "How much are you motivated in participating to the TESS network?").

In [3]:
VALUES = ['achievement', 'belongingness', 'benevolence', 'conformity', 'hedonism',
          'power', 'routine', 'self-direction', 'stimulation', 'universalism']

# Keep only the 1-5 rating answers for the ten motivation values
motiv = raw[raw['tag'].isin(VALUES) & raw['value'].between(1, 5)]

# volunteers x motivations: average the 2 questions per value, per volunteer
profile = motiv.pivot_table(index='user', columns='tag', values='value', aggfunc='mean')

# Overall self-rated motivation (the target we will correlate against)
profile['global_motivation'] = (
    raw[raw['questionId'] == 1011].groupby('user')['value'].mean()
)
profile = profile[VALUES + ['global_motivation']]
print('shape (volunteers x variables):', profile.shape)
profile.round(2).head()
shape (volunteers x variables): (88, 11)
Out[3]:
tag achievement belongingness benevolence conformity hedonism power routine self-direction stimulation universalism global_motivation
user
u168016834 4.0 3.5 4.5 2.0 4.0 3.5 3.5 4.5 4.5 3.0 2.0
u242978352 4.5 4.0 4.5 3.0 4.5 2.5 2.0 5.0 5.0 4.5 5.0
u242978354 3.0 4.5 4.5 4.0 4.5 3.0 3.5 4.5 4.0 4.0 3.0
u242978357 4.0 NaN NaN NaN 4.0 NaN 2.5 3.0 4.0 NaN NaN
u242978358 4.0 4.0 5.0 3.0 5.0 2.0 3.5 4.5 4.5 5.0 5.0

1. What drives these volunteers?¶

Averaging each motivation across all volunteers shows the profile of the community. The bars are sorted, with error bars for the spread between people.

In [4]:
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style='whitegrid')

summary = profile[VALUES].agg(['mean', 'std']).T.sort_values('mean')

fig, ax = plt.subplots(figsize=(8, 5))
colors = sns.color_palette('viridis', len(summary))
ax.barh(summary.index, summary['mean'], xerr=summary['std'],
        color=colors, edgecolor='white', capsize=3)
ax.axvline(3, color='grey', ls='--', lw=1, label='scale midpoint (3)')
ax.set_xlim(1, 5)
ax.set_xlabel('mean rating  (1 = not at all, 5 = very much)')
ax.set_title('Volunteer motivation profile — TESS light-pollution network')
ax.legend(loc='lower right')
plt.tight_layout()
plt.show()
No description has been provided for this image

Reading it: the community is driven by intrinsic and altruistic values — self-direction (learning), benevolence and universalism (contributing to science and protecting the night sky) sit near the top. The extrinsic drivers — power (recognition) and conformity (social obligation) — sit well below the midpoint. These are people who show up for the cause, not for the reward.

2. Which values actually predict staying motivated?¶

A high average is not the same as being important. To find the levers for keeping volunteers engaged, we correlate each motivation value with the overall self-rated motivation (Pearson r). A larger positive r means volunteers who score high on that value also tend to be more motivated overall. This reproduces the published tess-network-analysis-results.csv.

In [5]:
from scipy import stats

rows = []
for v in VALUES:
    sub = profile[[v, 'global_motivation']].dropna()
    r, p = stats.pearsonr(sub[v], sub['global_motivation'])
    rows.append({'value': v, 'r': r, 'pvalue': p, 'n': len(sub)})
corr = pd.DataFrame(rows).sort_values('r')
corr['significant'] = corr['pvalue'] < 0.05

fig, ax = plt.subplots(figsize=(8, 5))
bar_colors = ['#2a9d8f' if s else '#c9c9c9' for s in corr['significant']]
ax.barh(corr['value'], corr['r'], color=bar_colors, edgecolor='white')
ax.set_xlabel('correlation with overall motivation  (Pearson r)')
ax.set_title('Which values go with a more motivated volunteer?')
handles = [plt.Rectangle((0, 0), 1, 1, color='#2a9d8f'),
           plt.Rectangle((0, 0), 1, 1, color='#c9c9c9')]
ax.legend(handles, ['significant (p < 0.05)', 'not significant'], loc='lower right')
plt.tight_layout()
plt.show()

corr.set_index('value').round(3)
No description has been provided for this image
Out[5]:
r pvalue n significant
value
power 0.093 0.409 81 False
conformity 0.125 0.265 81 False
routine 0.264 0.017 81 True
stimulation 0.348 0.001 81 True
achievement 0.420 0.000 81 True
self-direction 0.423 0.000 81 True
belongingness 0.453 0.000 81 True
hedonism 0.534 0.000 81 True
benevolence 0.545 0.000 81 True
universalism 0.681 0.000 81 True

Reading it: universalism, benevolence and hedonism correlate most strongly with overall motivation, while power and conformity are essentially unrelated (grey, not significant). The message for recruitment: appeals to protecting the night sky for everyone and contributing to real science resonate; offering status or leaning on social pressure does not.

3. Who are the volunteers, and does age change the picture?¶

Finally, a quick demographic read. The survey records age band (question 1086) and gender (1085). Let's see the age distribution and whether overall motivation varies across age groups.

In [6]:
AGE_ORDER = ['19-24', '25-34', '35-44', '45-54', '55-64', 'over 64']
age = raw[raw['questionId'] == 1086].groupby('user')['option'].first()
demo = profile.join(age.rename('age')).dropna(subset=['age'])

fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))

counts = demo['age'].value_counts().reindex(AGE_ORDER).fillna(0)
axes[0].bar(counts.index, counts.values, color=sns.color_palette('crest', len(AGE_ORDER)))
axes[0].set_title('Volunteers by age band')
axes[0].set_ylabel('number of volunteers')
axes[0].tick_params(axis='x', rotation=30)

sns.boxplot(data=demo, x='age', y='global_motivation', order=AGE_ORDER,
            palette='crest', ax=axes[1])
sns.stripplot(data=demo, x='age', y='global_motivation', order=AGE_ORDER,
              color='0.25', size=3, alpha=0.5, ax=axes[1])
axes[1].set_title('Overall motivation by age band')
axes[1].set_ylabel('overall motivation (1-5)')
axes[1].tick_params(axis='x', rotation=30)

plt.tight_layout()
plt.show()
/tmp/ipykernel_43476/1208765612.py:13: FutureWarning: 

Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.

  sns.boxplot(data=demo, x='age', y='global_motivation', order=AGE_ORDER,
No description has been provided for this image

Reading it: the network skews older — the 45-64 bands dominate, consistent with a community of committed amateur astronomers. Overall motivation is high and fairly flat across ages (medians near 4-5 everywhere), so there is no obvious young vs old motivation gap to exploit; the levers from chart 2 (the values) matter more than age. With only ~90 respondents, treat per-group differences as suggestive, not conclusive.

Your turn¶

This is just the starting point. Some ideas:

  • Check the dataset challenge on its CSDH data sheet.
  • Work on a copy: right-click the file → Duplicate (or Save Notebook As…). Your changes only live in your Hub space — they're never pushed to GitHub.
  • Edited this notebook and want the original back? Use the Restore cell below (or the restore.ipynb notebook).
  • Questions and results: on the platform forum.

Attribution: data from TESS Network Motivation Survey (Light Pollution Citizen Science), license CC-BY-4.0. Notebook from the Citizen Science Data Hub (CSDH) — Fundación Ibercivis.

In [7]:
# ⚠️ RESTORE: this DISCARDS YOUR CHANGES to this notebook and resets it to the original.
# 1. Uncomment the line below (remove the #)   2. Run this cell
# 3. Then: menu File → Reload Notebook from Disk

# !git -C ~/citizen-science-data fetch -q origin && git -C ~/citizen-science-data checkout origin/main -- tess-light-pollution.ipynb && echo "Restored. Now: File → Reload Notebook from Disk"