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 (~/).

What's in the dataset¶

A single CSV catalogues ~4,950 citizen-science projects collected by the CS Track project. Each row is one project with its language, title, platform URL, and two machine-tagged fields: Research Areas and SDG (UN Sustainable Development Goals). Let's start by listing the file.

In [1]:
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.

In [2]:
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']
Out[2]:
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:

  • Language is usually a plain string (English) but sometimes arrives wrapped as a JSON-ish list (["English"]).
  • Research Areas and Sdg are 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.

In [3]:
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()
Out[3]:
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.

In [4]:
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.')
No description has been provided for this image
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.

In [5]:
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)')
No description has been provided for this image
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.

In [6]:
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())
No description has been provided for this image
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.ipynb notebook).
  • 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.

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 -- cs-track-database.ipynb && echo "Restored. Now: File → Reload Notebook from Disk"