CS Track Database: Citizen Science Projects Catalog¶
Category: Citizen Science Metadata · Size: 2.4 MB · Format: CSV License: CC-BY-4.0 · Zenodo record · Data sheet on the CSDH
Comprehensive database of citizen science projects with titles, topic areas, languages, URLs and alignment with the UN Sustainable Development Goals.
The data is mounted read-only at /srv/data/cs-track-database/.
Save anything you produce in your personal folder (~/).
from pathlib import Path
DATA = Path('/srv/data/cs-track-database')
for f in sorted(DATA.rglob('*')):
if f.is_file():
print(f"{f.relative_to(DATA)} ({f.stat().st_size/1e6:,.1f} MB)")
CSTrack_database.csv (2.4 MB)
Load the data¶
The file is a small (2.4 MB) UTF-8 CSV, so we can read it whole. We peek at the columns and the first rows to see what we are dealing with.
import pandas as pd
df = pd.read_csv(DATA / 'CSTrack_database.csv')
print('shape:', df.shape)
print('columns:', list(df.columns))
df.head(3)
shape: (4949, 6) columns: ['Insert Date', 'Language', 'Title', 'URL Platform', 'Research Areas', 'Sdg']
| Insert Date | Language | Title | URL Platform | Research Areas | Sdg | |
|---|---|---|---|---|---|---|
| 0 | 2020-06-16 | German | Virenmonitoring | https://www.citizen-science.at/projekte/virenm... | ["Life Sciences & Biomedicine, Virology, 0.962... | [] |
| 1 | 2020-06-16 | German | Deutsch in Österreich | https://www.citizen-science.at/projekte/deutsc... | ["Social Sciences, Linguistics, 0.716406357773... | [] |
| 2 | 2020-06-16 | German | Citree | https://www.citizen-science.at/projekte/citree | ["Physical Sciences, Sustainability Science, 0... | ["SDG, SDG #11, 0.37277650533619905" "SDG, SDG... |
The data is semi-structured¶
Three columns need cleaning before we can count anything:
Languageis usually a plain string (English) but sometimes arrives wrapped as a JSON-ish list (["English"]).Research AreasandSdgare multi-valued: each cell holds several quoted tags separated by spaces, and every tag looks like"Domain, Sub-area, relevance-score"(tags are ordered by descending score, so the first is the project's best-matching topic). Empty cells are[].
We write small parsers using regular expressions to pull these apart.
import re
def clean_scalar(x):
"""Unwrap values like ["English"] -> English; leave plain strings as-is."""
if pd.isna(x):
return x
m = re.findall(r'"([^"]+)"', str(x))
return m[0] if m else str(x)
def tags(x):
"""Return the list of quoted tags inside a Research Areas / Sdg cell."""
if pd.isna(x):
return []
return re.findall(r'"([^"]+)"', str(x))
# Clean the language column
df['lang'] = df['Language'].map(clean_scalar)
# Primary (top-scoring) research domain and sub-area for each project
def primary_domain(x):
t = tags(x)
return t[0].split(',')[0].strip() if t else None
def primary_subarea(x):
t = tags(x)
if not t:
return None
parts = [p.strip() for p in t[0].split(',')]
return parts[1] if len(parts) >= 2 else None
df['domain'] = df['Research Areas'].map(primary_domain)
df['subarea'] = df['Research Areas'].map(primary_subarea)
# List of SDG numbers each project is tagged with
def sdg_numbers(x):
return [int(n) for n in re.findall(r'SDG #(\d+)', str(x))]
df['sdgs'] = df['Sdg'].map(sdg_numbers)
df[['lang', 'domain', 'subarea', 'sdgs']].head()
| lang | domain | subarea | sdgs | |
|---|---|---|---|---|
| 0 | German | Life Sciences & Biomedicine | Virology | [] |
| 1 | German | Social Sciences | Linguistics | [] |
| 2 | German | Physical Sciences | Sustainability Science | [11, 10, 9, 12, 13, 1, 8, 4, 2] |
| 3 | German | Technology | Remote Sensing | [1, 11] |
| 4 | German | Life Sciences & Biomedicine | Veterinary Sciences | [15] |
The citizen-science landscape by language¶
Which languages do these projects live in? Citizen science is often assumed to be English-first — let's check.
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style='whitegrid')
lang_counts = df['lang'].value_counts().head(12)
fig, ax = plt.subplots(figsize=(9, 5))
sns.barplot(x=lang_counts.values, y=lang_counts.index, color='#4C72B0', ax=ax)
ax.set(xlabel='Number of projects', ylabel='', title='Top 12 languages of citizen-science projects')
for i, v in enumerate(lang_counts.values):
ax.text(v + 10, i, str(v), va='center')
plt.tight_layout()
plt.show()
share = lang_counts.iloc[0] / len(df) * 100
print(f'English accounts for {share:.0f}% of all projects; {df["lang"].nunique()} languages in total.')
English accounts for 59% of all projects; 36 languages in total.
Which SDGs does citizen science serve?¶
The SDG field is multi-valued, so a single project can support several goals. We explode the list to count how many projects touch each of the 17 goals.
sdg_titles = {
1: 'No poverty', 2: 'Zero hunger', 3: 'Good health', 4: 'Quality education',
5: 'Gender equality', 6: 'Clean water', 7: 'Affordable energy', 8: 'Decent work',
9: 'Industry & innovation', 10: 'Reduced inequalities', 11: 'Sustainable cities',
12: 'Responsible consumption', 13: 'Climate action', 14: 'Life below water',
15: 'Life on land', 16: 'Peace & justice', 17: 'Partnerships',
}
sdg_exploded = df.explode('sdgs').dropna(subset=['sdgs'])
sdg_counts = sdg_exploded['sdgs'].astype(int).value_counts().sort_index()
labels = [f'{n}. {sdg_titles[n]}' for n in sdg_counts.index]
fig, ax = plt.subplots(figsize=(9, 6))
sns.barplot(x=sdg_counts.values, y=labels, color='#55A868', ax=ax)
ax.set(xlabel='Number of projects tagged', ylabel='', title='Citizen-science projects per UN Sustainable Development Goal')
plt.tight_layout()
plt.show()
n_tagged = (df['sdgs'].str.len() > 0).sum()
print(f'{n_tagged} of {len(df)} projects ({n_tagged/len(df)*100:.0f}%) carry at least one SDG tag.')
print('Most-served goal:', f'SDG {sdg_counts.idxmax()} - {sdg_titles[sdg_counts.idxmax()]}',
f'({sdg_counts.max()} projects)')
2766 of 4949 projects (56%) carry at least one SDG tag. Most-served goal: SDG 15 - Life on land (1329 projects)
Do research domains and SDGs line up the way we'd expect?¶
Now the interesting part. We cross-tabulate each project's primary research domain against the SDGs it is tagged with. A heatmap reveals whether, say, Life Sciences projects really cluster on the environmental goals — and where surprising links appear.
cross = sdg_exploded.dropna(subset=['domain']).copy()
cross['sdgs'] = cross['sdgs'].astype(int)
mat = pd.crosstab(cross['domain'], cross['sdgs'])
mat = mat.reindex(columns=range(1, 18), fill_value=0)
# Normalise each domain row to a percentage so domains of different sizes compare fairly
mat_pct = mat.div(mat.sum(axis=1), axis=0) * 100
fig, ax = plt.subplots(figsize=(11, 4.5))
sns.heatmap(mat_pct, cmap='rocket_r', annot=False, cbar_kws={'label': '% of domain\'s SDG tags'},
xticklabels=[f'{n}' for n in range(1, 18)], ax=ax)
ax.set(xlabel='SDG number', ylabel='Primary research domain',
title='Where each research domain concentrates its SDG effort')
plt.tight_layout()
plt.show()
# Top sub-areas overall, for context
print('Top 8 primary research sub-areas:')
print(df['subarea'].value_counts().head(8).to_string())
Top 8 primary research sub-areas: subarea Biodiversity & Conservation 682 Remote Sensing 393 Environmental Sciences & Ecology 240 Ornithology 233 Construction & Building Technology 210 Water Resources 165 Plant Sciences 150 Astronomy & Astrophysics 144
Working with data larger than memory¶
Your session has a 4 GB RAM limit, but you can analyse files of 10 GB or more without loading them whole:
- Read only the columns you need:
pd.read_csv(f, usecols=[...])/pd.read_parquet(f, columns=[...]). - Process in chunks and keep only the result:
total = 0 for chunk in pd.read_csv(file, chunksize=1_000_000): total += len(chunk)
- Query with SQL without loading anything — DuckDB (already installed) reads
CSV and Parquet straight from disk and only brings the result into memory:
import duckdb duckdb.sql("SELECT column, count(*) FROM '/srv/data/.../file.parquet' GROUP BY column").df()
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 CS Track Database: Citizen Science Projects Catalog, 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 -- cs-track-database.ipynb && echo "Restored. Now: File → Reload Notebook from Disk"