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.
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.
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
| 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?").
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)
| 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.
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()
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.
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)
| 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.
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,
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.ipynbnotebook). - 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.
# ⚠️ 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"