FLOW: Participant Survey Data from Freshwater Citizen Science Project¶

Category: Water Quality · Size: 407 kB · Format: PDF, XLSX License: CC-BY-4.0 · Zenodo record · Data sheet on the CSDH

Participant survey data from the FLOW project (2021) monitoring and protecting freshwater streams, analysing how citizen science fosters knowledge and collective action.

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

What's in the dataset¶

The FLOW project (2021) ran a quasi-experiment on freshwater citizen science. Volunteers were surveyed several times and compared against people who did not take part. Everything lives in a single Excel sheet (Data_FLOW_survey.xlsx); the PDF is the codebook.

In [1]:
from pathlib import Path

DATA = Path('/srv/data/flow-freshwater')
for f in sorted(DATA.rglob('*')):
    if f.is_file():
        print(f"{f.relative_to(DATA)}  ({f.stat().st_size/1e3:,.0f} kB)")
Data_FLOW_survey.xlsx  (149 kB)
Dateset_Explanation.pdf  (258 kB)

Load the survey and understand its shape¶

The file is wide and longitudinal: one row per participant, and each psychological measure is repeated across survey waves, encoded as a suffix on the column name:

  • *.1 = baseline (before), *.2 = post (after the monitoring season), *.3 = follow-up (collected only for the treatment group).

Key columns:

  • group — Treatment (took part in FLOW stream monitoring) vs Control1 / Control2.
  • knowledge_score — an objective freshwater-knowledge quiz, scored 1-8.
  • attitude, awareness, behavioral_control, collective_efficacy, group_id, nature_rel, skills, interest — self-report constructs on a 1-5 Likert scale.
  • age, gender — demographics.
In [2]:
import pandas as pd
import numpy as np

df = pd.read_excel(DATA / 'Data_FLOW_survey.xlsx')
print('shape:', df.shape)
print('groups:', df['group'].value_counts().to_dict())
print('age: median %.0f (range %.0f-%.0f)' % (df.age.median(), df.age.min(), df.age.max()))
df[['group', 'age', 'gender',
    'knowledge_score.1', 'knowledge_score.2', 'knowledge_score.3',
    'attitude.1', 'attitude.2']].head()
shape: (555, 53)
groups: {'Control2': 250, 'Treatment': 213, 'Control1': 92}
age: median 41 (range 13-84)
Out[2]:
group age gender knowledge_score.1 knowledge_score.2 knowledge_score.3 attitude.1 attitude.2
0 Treatment 18.0 male 6.0 6.0 NaN 3.666667 3.500000
1 Treatment 16.0 female 1.0 3.0 NaN 5.000000 4.000000
2 Treatment 17.0 female 4.0 5.0 NaN 4.500000 4.500000
3 Treatment 47.0 female 8.0 8.0 8.0 5.000000 5.000000
4 Treatment 19.0 male 7.0 8.0 NaN 4.166667 4.666667

The core question: did the freshwater knowledge quiz improve?¶

knowledge_score is objective (a quiz, not an opinion), so it is the cleanest place to look for a learning effect. We compare the mean score before (.1) and after (.2) for each group. If citizen science teaches people, the Treatment group should jump while the controls stay flat.

In [3]:
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.container import BarContainer
sns.set_theme(style='whitegrid', context='talk')

waves = {'Baseline (.1)': 'knowledge_score.1', 'Post (.2)': 'knowledge_score.2'}
groups = ['Treatment', 'Control1', 'Control2']

means = pd.DataFrame({w: [df.loc[df.group == g, col].mean() for g in groups]
                      for w, col in waves.items()}, index=groups)
sems = pd.DataFrame({w: [df.loc[df.group == g, col].sem() for g in groups]
                     for w, col in waves.items()}, index=groups)
print(means.round(2))

ax = means.plot(kind='bar', yerr=sems, capsize=4, figsize=(9, 5.5),
                color=['#8bb8d0', '#1f6f8b'], edgecolor='black', rot=0)
ax.set_ylabel('Mean knowledge score (1-8)')
ax.set_title('Freshwater knowledge: before vs after participation')
ax.set_ylim(0, 8)
for c in ax.containers:
    if isinstance(c, BarContainer):
        ax.bar_label(c, fmt='%.1f', padding=3, fontsize=11)
plt.tight_layout(); plt.show()
           Baseline (.1)  Post (.2)
Treatment           4.28       6.38
Control1            4.09       4.41
Control2            2.27       2.42
No description has been provided for this image

The treatment group starts a bit ahead and then climbs sharply after the season, while the control groups barely move. To make that "engagement effect" explicit, we plot the gain (post minus baseline) for every construct, contrasting Treatment with the pooled controls.

In [4]:
constructs = ['knowledge_score', 'attitude', 'awareness', 'behavioral_control',
              'collective_efficacy', 'group_id', 'nature_rel', 'skills']

def gain(sub, c):
    return (sub[f'{c}.2'] - sub[f'{c}.1']).mean()

treat = df[df.group == 'Treatment']
ctrl = df[df.group.isin(['Control1', 'Control2'])]
delta = pd.DataFrame({'Treatment': [gain(treat, c) for c in constructs],
                      'Control': [gain(ctrl, c) for c in constructs]}, index=constructs)
delta = delta.sort_values('Treatment')
print(delta.round(2))

fig, ax = plt.subplots(figsize=(9, 6.5))
y = np.arange(len(delta))
ax.hlines(y, delta['Control'], delta['Treatment'], color='#c9ccd1', lw=3, zorder=1)
ax.scatter(delta['Control'], y, s=110, color='#b0b7bf', label='Control', zorder=2, edgecolor='black')
ax.scatter(delta['Treatment'], y, s=110, color='#1f6f8b', label='Treatment', zorder=2, edgecolor='black')
ax.axvline(0, color='black', lw=1)
ax.set_yticks(y); ax.set_yticklabels(delta.index)
ax.set_xlabel('Mean change from baseline to post (.2 - .1)')
ax.set_title('Where does taking part move the needle?')
ax.legend(loc='lower right')
plt.tight_layout(); plt.show()
                     Treatment  Control
collective_efficacy       0.04     0.05
nature_rel                0.06    -0.01
attitude                  0.08    -0.02
behavioral_control        0.13    -0.07
awareness                 0.16     0.12
group_id                  0.24      NaN
skills                    0.67     0.01
knowledge_score           2.11     0.21
No description has been provided for this image

Knowledge shows by far the largest treatment-vs-control gap; several attitudinal and collective-action measures also nudge upward for participants. Finally, the treatment group was surveyed a third time (follow-up), so we can check whether the knowledge gain sticks using a paired boxplot across the three waves for Treatment participants only.

In [5]:
long = (treat[['knowledge_score.1', 'knowledge_score.2', 'knowledge_score.3']]
        .rename(columns={'knowledge_score.1': 'Baseline',
                         'knowledge_score.2': 'Post',
                         'knowledge_score.3': 'Follow-up'})
        .melt(var_name='Wave', value_name='Knowledge score').dropna())

fig, ax = plt.subplots(figsize=(8.5, 5.5))
order = ['Baseline', 'Post', 'Follow-up']
sns.boxplot(data=long, x='Wave', y='Knowledge score', order=order,
            palette=['#8bb8d0', '#1f6f8b', '#134b5f'], ax=ax)
sns.stripplot(data=long, x='Wave', y='Knowledge score', order=order,
              color='black', alpha=0.15, size=3, ax=ax)
w_means = long.groupby('Wave')['Knowledge score'].mean().reindex(order)
ax.plot(range(len(order)), w_means.values, 'o-', color='crimson', lw=2, label='mean')
ax.set_ylim(0, 8.5); ax.set_title('Treatment group: does the knowledge gain last?')
ax.legend(); plt.tight_layout(); plt.show()

print('Treatment knowledge means:', w_means.round(2).to_dict())
/tmp/ipykernel_43599/3870522887.py:9: 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=long, x='Wave', y='Knowledge score', order=order,
No description has been provided for this image
Treatment knowledge means: {'Baseline': 4.28, 'Post': 6.38, 'Follow-up': 6.39}

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 FLOW: Participant Survey Data from Freshwater Citizen Science Project, license CC-BY-4.0. Notebook from the Citizen Science Data Hub (CSDH) — Fundación Ibercivis.

In [6]:
# ⚠️ 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 -- flow-freshwater.ipynb && echo "Restored. Now: File → Reload Notebook from Disk"