MARCSI: Inventory of Marine Citizen Science Initiatives (Global)¶

Category: Marine Biodiversity · Size: 1.4 MB · Format: CSV, JSON License: CC-BY-SA-4.0 (ShareAlike: derivative notebooks CC-BY-SA-4.0) · Zenodo record · Data sheet on the CSDH

Global inventory of marine citizen science initiatives, documenting past and present projects worldwide and assessing the FAIR compliance of the data they produce.

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

What's in the dataset¶

MARCSI is a hand-curated inventory of marine citizen-science initiatives worldwide. Each row is one project/initiative; the columns describe what it studies, where and when it runs, and crucially how open and FAIR the data it produces is.

There are two files: the data itself (.csv) and a companion CSVW schema (.json) that documents every column and, handily, tells us the CSV's delimiter and how each field is coded.

In [1]:
from pathlib import Path

DATA = Path('/srv/data/marcsi-marine')

for f in sorted(DATA.rglob('*')):
    if f.is_file():
        print(f"{f.relative_to(DATA)}  ({f.stat().st_size/1e6:,.2f} MB)")
MARCSI database_final.csv  (1.38 MB)
MARCSI database_final_csvw.json  (0.01 MB)

Loading it correctly¶

This CSV has two quirks that trip up a naive pd.read_csv:

  • the delimiter is a semicolon (;), not a comma;
  • it's encoded in Latin-1 (there are accented characters), not UTF-8.

Both facts are declared in the companion CSVW JSON, so we don't have to guess. We also strip the stray quote characters from the column names and trim whitespace from the categorical text (several categories otherwise appear twice, e.g. 'National' vs 'National ').

In [2]:
import pandas as pd

df = pd.read_csv(DATA / 'MARCSI database_final.csv', sep=';', encoding='latin-1')

# tidy up: column names arrive wrapped in single quotes; trim whitespace in text cells
df.columns = [c.strip().strip("'") for c in df.columns]
obj_cols = df.select_dtypes('object').columns
df[obj_cols] = df[obj_cols].apply(lambda s: s.str.strip())

print('rows, cols:', df.shape)
df[['Marine citizen science initiative title', 'Scientific topic (1)',
    'Geographical scale', 'Geographic location - country/countries',
    'Status', 'Open Access']].head()
rows, cols: (1267, 29)
Out[2]:
Marine citizen science initiative title Scientific topic (1) Geographical scale Geographic location - country/countries Status Open Access
0 Tangaroa Blue Foundation – Australian Marine D... Ecology (e.g. coastal ecology, the state of ce... National Australia active Yes (raw data is available)
1 Litter Intelligence Pollution (e.g. marine litter or the effect of... National New Zealand active Yes (raw data is available)
2 Citclops Environmental variables (e.g. water quality, t... Global Global completed Yes (raw data is available)
3 Marine Debris Tracker Pollution (e.g. marine litter or the effect of... Global Global active Yes (raw data is available)
4 Phytoplankton Monitoring Network (PMN) Environmental variables (e.g. water quality, t... National USA active Yes (raw data is available)

A first profile of the initiatives¶

Before any charts, a couple of value_counts tell us the shape of the collection: most projects are active, and they span every geographic scale from a single beach (Local) to the whole planet (Global).

In [3]:
print('Status of initiatives')
print(df['Status'].value_counts(dropna=False), '\n')

print('Geographical scale')
print(df['Geographical scale'].value_counts(dropna=False))
Status of initiatives
Status
active                 703
completed              343
Not found              182
NaN                     19
periodically active      9
abandoned                6
on hold                  4
upcoming                 1
Name: count, dtype: int64 

Geographical scale
Geographical scale
National          387
Regional          367
Local             218
Global            156
International     109
Not found          19
NaN                10
Not applicable      1
Name: count, dtype: int64

What do these projects study?¶

The Scientific topic (1) column classifies each initiative into one primary theme. The stored values are long and self-documenting (they include examples in parentheses), so we shorten them to the leading keyword for plotting.

In [4]:
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style='whitegrid')

# keep the keyword before the '(' — 'Single species (e.g. ...)' -> 'Single species'
topic = df['Scientific topic (1)'].str.split('(').str[0].str.strip()
topic_counts = topic.value_counts()

fig, ax = plt.subplots(figsize=(9, 5))
sns.barplot(x=topic_counts.values, y=topic_counts.index, color='#2c7fb8', ax=ax)
ax.set(xlabel='Number of initiatives', ylabel='',
       title='Primary scientific topic of marine citizen-science initiatives')
for i, v in enumerate(topic_counts.values):
    ax.text(v + 3, i, str(v), va='center')
plt.tight_layout()
plt.show()
No description has been provided for this image

Single-species monitoring (marine mammals, birds, seaweeds, invasive species…) and broad biodiversity mapping dominate, together with pollution (marine litter, oil spills). This matches the intuition that charismatic species and visible litter are the easiest things for the public to record.

Where in the world?¶

The Geographic location field is free text (some rows say Global, others name a country or even a US state). We take the top countries to see which nations host the most documented marine CS activity.

In [5]:
loc = df['Geographic location - country/countries'].replace({'Not found': None}).dropna()
# drop the 'Global' pseudo-location so real countries stand out
top = loc[loc != 'Global'].value_counts().head(15)

fig, ax = plt.subplots(figsize=(9, 6))
sns.barplot(x=top.values, y=top.index, color='#41ab5d', ax=ax)
ax.set(xlabel='Number of initiatives', ylabel='',
       title='Top 15 countries hosting marine citizen-science initiatives')
plt.tight_layout()
plt.show()

n_global = (loc == 'Global').sum()
print(f"(plus {n_global} initiatives tagged 'Global' — not tied to one country)")
No description has been provided for this image
(plus 151 initiatives tagged 'Global' — not tied to one country)

The English-speaking Atlantic and Pacific rim — the UK, Australia, Ireland, USA, New Zealand — leads the inventory, partly a real signal (strong volunteering traditions) and partly a survey bias (the inventory was compiled largely from English-language sources).

The core question: how FAIR is the data?¶

The inventory's scientific purpose is to assess whether the data these projects produce is FAIR — Findable, Accessible, Interoperable, Reusable. First, is the raw data even open at all?

In [6]:
oa = df['Open Access'].str.split('(').str[0].str.strip()
oa_counts = oa.value_counts()

colmap = {'Yes': '#238b45', 'Partially': '#fe9929', 'No': '#cb181d'}
bar_colors = [colmap.get(k.split(' ')[0].rstrip(','), '#999999') for k in oa_counts.index]

fig, ax = plt.subplots(figsize=(8, 4.5))
ax.barh(range(len(oa_counts)), oa_counts.values, color=bar_colors)
ax.set_yticks(range(len(oa_counts)))
ax.set_yticklabels(oa_counts.index)
ax.invert_yaxis()
ax.set(xlabel='Number of initiatives', title='Is the raw data openly available?')
for i, v in enumerate(oa_counts.values):
    ax.text(v + 4, i, str(v), va='center')
plt.tight_layout()
plt.show()

print((oa_counts / oa_counts.sum() * 100).round(1).astype(str) + ' %')
No description has been provided for this image
Open Access
No           58.9 %
Partially    23.0 %
Yes          18.1 %
Name: count, dtype: object

Only a minority of projects release raw data openly; most keep it closed or share it only partially (buried in a report or paper).

Now the four FAIR principles. These were only assessed for projects whose data is at least partly available, so we look at the share meeting each principle among the projects that were scored.

In [7]:
principles = ['Findable', 'Accessible', 'Interoperable', 'Reusable']

rows = []
for p in principles:
    s = df[p].dropna()
    yes = (s == 'Yes').sum()
    rows.append({'Principle': p, 'assessed': len(s), 'Yes': yes,
                 'pct_yes': 100 * yes / len(s)})
fair = pd.DataFrame(rows)
print(fair)

fig, ax = plt.subplots(figsize=(8, 4.5))
sns.barplot(data=fair, x='pct_yes', y='Principle', color='#6a51a3', ax=ax)
ax.set(xlabel='% of assessed initiatives meeting the principle', ylabel='',
       title='FAIR compliance of marine citizen-science data', xlim=(0, 100))
for i, v in enumerate(fair['pct_yes']):
    ax.text(v + 1, i, f'{v:.0f}%', va='center')
plt.tight_layout()
plt.show()
       Principle  assessed  Yes    pct_yes
0       Findable       690   13   1.884058
1     Accessible       690  240  34.782609
2  Interoperable       690   10   1.449275
3       Reusable       690   72  10.434783
No description has been provided for this image

The verdict is stark: even among projects whose data is available, it is often Accessible (you can reach it via a URL) but rarely Findable, Interoperable or Reusable — it lacks persistent identifiers, standard vocabularies and clear licences. In FAIR terms, marine citizen science has an access culture but not yet an infrastructure culture.

Bonus: growth over time¶

Finally, when did these initiatives start? The Start date field is messy free text ('2004', 'enero 2009', 'Not found'), but we can pull a 4-digit year out with a regex and watch the field grow.

In [8]:
year = pd.to_numeric(
    df['Start date'].str.extract(r'(19\d{2}|20\d{2})')[0], errors='coerce')
year = year[(year >= 1970) & (year <= 2024)]
per_year = year.value_counts().sort_index()

fig, ax = plt.subplots(figsize=(9, 4.5))
ax.bar(per_year.index, per_year.values, color='#2c7fb8', label='started that year')
ax2 = ax.twinx()
ax2.plot(per_year.index, per_year.cumsum(), color='#cb181d', lw=2, label='cumulative')
ax.set(xlabel='Start year', ylabel='Initiatives started that year',
       title='When marine citizen-science initiatives were founded')
ax2.set_ylabel('Cumulative number')
ax.legend(loc='upper left'); ax2.legend(loc='lower right')
plt.tight_layout()
plt.show()

print(f"Median start year: {int(year.median())}  ·  "
      f"{(year >= 2010).mean()*100:.0f}% started in 2010 or later")
No description has been provided for this image
Median start year: 2013  ·  63% started in 2010 or later

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 MARCSI: Inventory of Marine Citizen Science Initiatives (Global), license CC-BY-SA-4.0. Notebook from the Citizen Science Data Hub (CSDH) — Fundación Ibercivis.

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