Explore a MARIS NetCDF file

How to decode groups, nomenclatures, and time into analysis-ready DataFrames

Why NetCDF4

MARIS collects marine radioactivity measurements from many providers. Each provider uses its own formats, units, and nomenclature. MARISCO curates each dataset into a standard form and encodes it as one NetCDF4 file. The file is self-contained. It carries the data, the description of every variable, and the nomenclatures needed to interpret the codes.

This how-to shows how to open a MARIS NetCDF file and explore it with netCDF4 and pandas. We use the HELCOM 2024 dataset, 100-HELCOM-MORS-2024.nc, as a working example. The same steps apply to any MARISCO output file.

A MARIS file has four parts. Global attributes describe the dataset: title, summary, publisher, license, geographic bounds, time coverage, … Groups hold the sample types. Variables carry the measurements, one array per column over the sample dimension. Enumeration types define the MARIS nomenclatures: the standard lists of nuclides, units, species, areas, and detection limit codes. The sections below show how to read each part.

Open the file

Start by opening the file and printing it. The printout lists the groups, the variables of each group, and the global attributes. It is the quickest way to get an overview of a MARIS file.

from netCDF4 import Dataset
from pathlib import Path

fname = Path('../../_data/output/100-HELCOM-MORS-2024.nc')
nc = Dataset(fname, 'r')
print(nc)
<class 'netCDF4.Dataset'>
root group (NETCDF4 data model, file format HDF5):
    id: 26VMZZ2Q
    title: Environmental database - Helsinki Commission Monitoring of Radioactive Substances
    summary: MORS Environment database has been used to collate data resulting from monitoring of environmental radioactivity in the Baltic Sea based on HELCOM Recommendation 26/3.

The database is structured according to HELCOM Guidelines on Monitoring of Radioactive Substances (https://www.helcom.fi/wp-content/uploads/2019/08/Guidelines-for-Monitoring-of-Radioactive-Substances.pdf), which specifies reporting format, database structure, data types and obligatory parameters used for reporting data under Recommendation 26/3.

The database is updated and quality assured annually by HELCOM MORS EG.
    keywords: oceanography, Earth Science > Oceans > Ocean Chemistry> Radionuclides, Earth Science > Human Dimensions > Environmental Impacts > Nuclear Radiation Exposure, Earth Science > Oceans > Ocean Chemistry > Ocean Tracers, Earth Science > Oceans > Marine Sediments, Earth Science > Oceans > Ocean Chemistry, Earth Science > Oceans > Sea Ice > Isotopes, Earth Science > Oceans > Water Quality > Ocean Contaminants, Earth Science > Biological Classification > Animals/Vertebrates > Fish, Earth Science > Biosphere > Ecosystems > Marine Ecosystems, Earth Science > Biological Classification > Animals/Invertebrates > Mollusks, Earth Science > Biological Classification > Animals/Invertebrates > Arthropods > Crustaceans, Earth Science > Biological Classification > Plants > Macroalgae (Seaweeds)
    history: TBD
    keywords_vocabulary: GCMD Science Keywords
    keywords_vocabulary_url: https://gcmd.earthdata.nasa.gov/static/kms/
    record: TBD
    featureType: TBD
    cdm_data_type: TBD
    Conventions: CF-1.10 ACDD-1.3
    publisher_name: Paul MCGINNITY, Iolanda OSVATH, Florence DESCROIX-COMANDUCCI
    publisher_email: p.mc-ginnity@iaea.org, i.osvath@iaea.org, F.Descroix-Comanducci@iaea.org
    publisher_url: https://maris.iaea.org
    publisher_institution: International Atomic Energy Agency - IAEA
    creator_name: [{"creatorType": "author", "name": "HELCOM MORS"}]
    institution: TBD
    metadata_link: TBD
    creator_email: TBD
    creator_url: TBD
    references: TBD
    license: Without prejudice to the applicable Terms and Conditions (https://nucleus.iaea.org/Pages/Others/Disclaimer.aspx), I hereby agree that any use of the data will contain appropriate acknowledgement of the data source(s) and the IAEA Marine Radioactivity Information System (MARIS).
    comment: TBD
    geospatial_lat_min: 54.006167
    geospatial_lon_min: 10.2917
    geospatial_lat_max: 60.3767
    geospatial_lon_max: 29.05
    geospatial_vertical_min: 0.0
    geospatial_vertical_max: 72.0
    geospatial_bounds: POLYGON ((10.2917 54.006167, 29.05 54.006167, 29.05 60.3767, 10.2917 60.3767, 10.2917 54.006167))
    geospatial_bounds_crs: EPSG:4326
    time_coverage_start: 1985-07-16T00:00:00
    time_coverage_end: 2023-06-11T00:00:00
    local_time_zone: TBD
    date_created: TBD
    date_modified: TBD
    publisher_postprocess_logs: Convert 'nuclide' column values to lowercase, strip spaces, and store in 'NUCLIDE' column., Remap values from 'NUCLIDE' to 'NUCLIDE' for groups: all., Parse HELCOM DATE (MM/DD/YY HH:MM:SS) with fallback to YEAR/MONTH/DAY., Encode time as seconds since epoch., Melt HELCOM dual-value sediment rows into separate rows per measurement type (Bq/kg, Bq/m²)., Sanitize measurement values by removing blanks and standardizing to use the `VALUE` column., Convert relative uncertainty (percent) to absolute (standard) uncertainty per group., Set the MARIS-standard UNIT column from per-sample-type conventions (column name, basis column, or melt result)., Map HELCOM `<` / detected-value conventions to MARIS detection-limit integer codes (2 for DL, 1 for detected)., Remap values from 'rubin' to 'SPECIES' for groups: BIOTA., Remap values from 'tissue' to 'BODY_PART' for groups: BIOTA., Remap values from 'SPECIES' to 'BIO_GROUP' for groups: BIOTA., Replace invalid HELCOM SEDI codes with -99 sentinel before nomenclature lookup., Remap values from 'sedi' to 'SED_TYPE' for groups: SEDIMENT., Remap values from 'filt' to 'FILT' for groups: SEAWATER., Assign internal sequential SMP_ID and preserve provider KEY as SMP_ID_PROVIDER., Rename HELCOM sdepth/tdepth columns to MARIS-standard SMP_DEPTH/TOT_DEPTH and cast as float., Add salinity (SAL) from HELCOM salin column where present., Add temperature (TEMP) from HELCOM ttemp column., Remap Sediment slice top and bottom to MARIS format., Map basis F to W (BIOTA)., Compute PERCENTWT = dw% / 100 (SEDIMENT)., Compute DRYWT / WETWT from weight + basis (BIOTA)., Parse lat/lon from decimal-degree or degree-minute columns, preferring decimal., Drop rows with invalid longitude & latitude values. Convert `,` separator to `.` separator., Add station to all DataFrames.
    dimensions(sizes): 
    variables(dimensions): 
    groups: biota, seawater, sediment

Global attributes

The root group stores dataset metadata as global attributes. Read them all at once as a dict, or fetch one by name. The geospatial bounds, time coverage, and publisher information all come from these attributes.

nc.__dict__
{'id': '26VMZZ2Q',
 'title': 'Environmental database - Helsinki Commission Monitoring of Radioactive Substances',
 'summary': 'MORS Environment database has been used to collate data resulting from monitoring of environmental radioactivity in the Baltic Sea based on HELCOM Recommendation 26/3.\n\nThe database is structured according to HELCOM Guidelines on Monitoring of Radioactive Substances (https://www.helcom.fi/wp-content/uploads/2019/08/Guidelines-for-Monitoring-of-Radioactive-Substances.pdf), which specifies reporting format, database structure, data types and obligatory parameters used for reporting data under Recommendation 26/3.\n\nThe database is updated and quality assured annually by HELCOM MORS EG.',
 'keywords': 'oceanography, Earth Science > Oceans > Ocean Chemistry> Radionuclides, Earth Science > Human Dimensions > Environmental Impacts > Nuclear Radiation Exposure, Earth Science > Oceans > Ocean Chemistry > Ocean Tracers, Earth Science > Oceans > Marine Sediments, Earth Science > Oceans > Ocean Chemistry, Earth Science > Oceans > Sea Ice > Isotopes, Earth Science > Oceans > Water Quality > Ocean Contaminants, Earth Science > Biological Classification > Animals/Vertebrates > Fish, Earth Science > Biosphere > Ecosystems > Marine Ecosystems, Earth Science > Biological Classification > Animals/Invertebrates > Mollusks, Earth Science > Biological Classification > Animals/Invertebrates > Arthropods > Crustaceans, Earth Science > Biological Classification > Plants > Macroalgae (Seaweeds)',
 'history': 'TBD',
 'keywords_vocabulary': 'GCMD Science Keywords',
 'keywords_vocabulary_url': 'https://gcmd.earthdata.nasa.gov/static/kms/',
 'record': 'TBD',
 'featureType': 'TBD',
 'cdm_data_type': 'TBD',
 'Conventions': 'CF-1.10 ACDD-1.3',
 'publisher_name': 'Paul MCGINNITY, Iolanda OSVATH, Florence DESCROIX-COMANDUCCI',
 'publisher_email': 'p.mc-ginnity@iaea.org, i.osvath@iaea.org, F.Descroix-Comanducci@iaea.org',
 'publisher_url': 'https://maris.iaea.org',
 'publisher_institution': 'International Atomic Energy Agency - IAEA',
 'creator_name': '[{"creatorType": "author", "name": "HELCOM MORS"}]',
 'institution': 'TBD',
 'metadata_link': 'TBD',
 'creator_email': 'TBD',
 'creator_url': 'TBD',
 'references': 'TBD',
 'license': 'Without prejudice to the applicable Terms and Conditions (https://nucleus.iaea.org/Pages/Others/Disclaimer.aspx), I hereby agree that any use of the data will contain appropriate acknowledgement of the data source(s) and the IAEA Marine Radioactivity Information System (MARIS).',
 'comment': 'TBD',
 'geospatial_lat_min': '54.006167',
 'geospatial_lon_min': '10.2917',
 'geospatial_lat_max': '60.3767',
 'geospatial_lon_max': '29.05',
 'geospatial_vertical_min': '0.0',
 'geospatial_vertical_max': '72.0',
 'geospatial_bounds': 'POLYGON ((10.2917 54.006167, 29.05 54.006167, 29.05 60.3767, 10.2917 60.3767, 10.2917 54.006167))',
 'geospatial_bounds_crs': 'EPSG:4326',
 'time_coverage_start': '1985-07-16T00:00:00',
 'time_coverage_end': '2023-06-11T00:00:00',
 'local_time_zone': 'TBD',
 'date_created': 'TBD',
 'date_modified': 'TBD',
 'publisher_postprocess_logs': "Convert 'nuclide' column values to lowercase, strip spaces, and store in 'NUCLIDE' column., Remap values from 'NUCLIDE' to 'NUCLIDE' for groups: all., Parse HELCOM DATE (MM/DD/YY HH:MM:SS) with fallback to YEAR/MONTH/DAY., Encode time as seconds since epoch., Melt HELCOM dual-value sediment rows into separate rows per measurement type (Bq/kg, Bq/m²)., Sanitize measurement values by removing blanks and standardizing to use the `VALUE` column., Convert relative uncertainty (percent) to absolute (standard) uncertainty per group., Set the MARIS-standard UNIT column from per-sample-type conventions (column name, basis column, or melt result)., Map HELCOM `<` / detected-value conventions to MARIS detection-limit integer codes (2 for DL, 1 for detected)., Remap values from 'rubin' to 'SPECIES' for groups: BIOTA., Remap values from 'tissue' to 'BODY_PART' for groups: BIOTA., Remap values from 'SPECIES' to 'BIO_GROUP' for groups: BIOTA., Replace invalid HELCOM SEDI codes with -99 sentinel before nomenclature lookup., Remap values from 'sedi' to 'SED_TYPE' for groups: SEDIMENT., Remap values from 'filt' to 'FILT' for groups: SEAWATER., Assign internal sequential SMP_ID and preserve provider KEY as SMP_ID_PROVIDER., Rename HELCOM sdepth/tdepth columns to MARIS-standard SMP_DEPTH/TOT_DEPTH and cast as float., Add salinity (SAL) from HELCOM salin column where present., Add temperature (TEMP) from HELCOM ttemp column., Remap Sediment slice top and bottom to MARIS format., Map basis F to W (BIOTA)., Compute PERCENTWT = dw% / 100 (SEDIMENT)., Compute DRYWT / WETWT from weight + basis (BIOTA)., Parse lat/lon from decimal-degree or degree-minute columns, preferring decimal., Drop rows with invalid longitude & latitude values. Convert `,` separator to `.` separator., Add station to all DataFrames."}
for att in nc.ncattrs(): 
    print(att, '=', nc.getncattr(att))
id = 26VMZZ2Q
title = Environmental database - Helsinki Commission Monitoring of Radioactive Substances
summary = MORS Environment database has been used to collate data resulting from monitoring of environmental radioactivity in the Baltic Sea based on HELCOM Recommendation 26/3.

The database is structured according to HELCOM Guidelines on Monitoring of Radioactive Substances (https://www.helcom.fi/wp-content/uploads/2019/08/Guidelines-for-Monitoring-of-Radioactive-Substances.pdf), which specifies reporting format, database structure, data types and obligatory parameters used for reporting data under Recommendation 26/3.

The database is updated and quality assured annually by HELCOM MORS EG.
keywords = oceanography, Earth Science > Oceans > Ocean Chemistry> Radionuclides, Earth Science > Human Dimensions > Environmental Impacts > Nuclear Radiation Exposure, Earth Science > Oceans > Ocean Chemistry > Ocean Tracers, Earth Science > Oceans > Marine Sediments, Earth Science > Oceans > Ocean Chemistry, Earth Science > Oceans > Sea Ice > Isotopes, Earth Science > Oceans > Water Quality > Ocean Contaminants, Earth Science > Biological Classification > Animals/Vertebrates > Fish, Earth Science > Biosphere > Ecosystems > Marine Ecosystems, Earth Science > Biological Classification > Animals/Invertebrates > Mollusks, Earth Science > Biological Classification > Animals/Invertebrates > Arthropods > Crustaceans, Earth Science > Biological Classification > Plants > Macroalgae (Seaweeds)
history = TBD
keywords_vocabulary = GCMD Science Keywords
keywords_vocabulary_url = https://gcmd.earthdata.nasa.gov/static/kms/
record = TBD
featureType = TBD
cdm_data_type = TBD
Conventions = CF-1.10 ACDD-1.3
publisher_name = Paul MCGINNITY, Iolanda OSVATH, Florence DESCROIX-COMANDUCCI
publisher_email = p.mc-ginnity@iaea.org, i.osvath@iaea.org, F.Descroix-Comanducci@iaea.org
publisher_url = https://maris.iaea.org
publisher_institution = International Atomic Energy Agency - IAEA
creator_name = [{"creatorType": "author", "name": "HELCOM MORS"}]
institution = TBD
metadata_link = TBD
creator_email = TBD
creator_url = TBD
references = TBD
license = Without prejudice to the applicable Terms and Conditions (https://nucleus.iaea.org/Pages/Others/Disclaimer.aspx), I hereby agree that any use of the data will contain appropriate acknowledgement of the data source(s) and the IAEA Marine Radioactivity Information System (MARIS).
comment = TBD
geospatial_lat_min = 54.006167
geospatial_lon_min = 10.2917
geospatial_lat_max = 60.3767
geospatial_lon_max = 29.05
geospatial_vertical_min = 0.0
geospatial_vertical_max = 72.0
geospatial_bounds = POLYGON ((10.2917 54.006167, 29.05 54.006167, 29.05 60.3767, 10.2917 60.3767, 10.2917 54.006167))
geospatial_bounds_crs = EPSG:4326
time_coverage_start = 1985-07-16T00:00:00
time_coverage_end = 2023-06-11T00:00:00
local_time_zone = TBD
date_created = TBD
date_modified = TBD
publisher_postprocess_logs = Convert 'nuclide' column values to lowercase, strip spaces, and store in 'NUCLIDE' column., Remap values from 'NUCLIDE' to 'NUCLIDE' for groups: all., Parse HELCOM DATE (MM/DD/YY HH:MM:SS) with fallback to YEAR/MONTH/DAY., Encode time as seconds since epoch., Melt HELCOM dual-value sediment rows into separate rows per measurement type (Bq/kg, Bq/m²)., Sanitize measurement values by removing blanks and standardizing to use the `VALUE` column., Convert relative uncertainty (percent) to absolute (standard) uncertainty per group., Set the MARIS-standard UNIT column from per-sample-type conventions (column name, basis column, or melt result)., Map HELCOM `<` / detected-value conventions to MARIS detection-limit integer codes (2 for DL, 1 for detected)., Remap values from 'rubin' to 'SPECIES' for groups: BIOTA., Remap values from 'tissue' to 'BODY_PART' for groups: BIOTA., Remap values from 'SPECIES' to 'BIO_GROUP' for groups: BIOTA., Replace invalid HELCOM SEDI codes with -99 sentinel before nomenclature lookup., Remap values from 'sedi' to 'SED_TYPE' for groups: SEDIMENT., Remap values from 'filt' to 'FILT' for groups: SEAWATER., Assign internal sequential SMP_ID and preserve provider KEY as SMP_ID_PROVIDER., Rename HELCOM sdepth/tdepth columns to MARIS-standard SMP_DEPTH/TOT_DEPTH and cast as float., Add salinity (SAL) from HELCOM salin column where present., Add temperature (TEMP) from HELCOM ttemp column., Remap Sediment slice top and bottom to MARIS format., Map basis F to W (BIOTA)., Compute PERCENTWT = dw% / 100 (SEDIMENT)., Compute DRYWT / WETWT from weight + basis (BIOTA)., Parse lat/lon from decimal-degree or degree-minute columns, preferring decimal., Drop rows with invalid longitude & latitude values. Convert `,` separator to `.` separator., Add station to all DataFrames.
nc.title
nc.getncattr('title')
'Environmental database - Helsinki Commission Monitoring of Radioactive Substances'

Groups

A MARIS file has one group per sample type present in the dataset. The schema defines four groups: seawater, biota, sediment, and suspended_matter. A provider dataset rarely has all four. HELCOM 2024 has three: biota, seawater, and sediment. Iterate nc.groups to see which groups a file contains.

list(nc.groups)
['biota', 'seawater', 'sediment']

Read a group into a DataFrame

Every variable in a group is a one-dimensional array over the sample dimension id. Reading all variables into a dict and building a DataFrame gives one row per sample. This is the representation we work with for analysis.

import pandas as pd

grp = nc.groups['biota']
d = {vn: grp.variables[vn][:] for vn in grp.variables if vn not in grp.dimensions}
df_biota = pd.DataFrame(d)
df_biota.head()
id_provider lon lat smp_depth time station nuclide value unit unc dl bio_group species body_part drywt wetwt percentwt
0 BBFFG1999001 13.720000 54.220001 0.0 929404800 BGBODD 4 841.000000 4 58.869999 1 11 96 54 NaN NaN 0.16920
1 BBFFG1987140 15.960000 54.700001 55.5 566697600 87/45 9 0.032600 5 0.000000 2 4 50 52 NaN NaN 0.31450
2 BSSSM2008013 12.074200 57.335201 0.0 1225670400 SWR25 2 17.600000 4 0.704000 1 11 96 54 NaN NaN 0.22000
3 BCLOR1992003 19.000000 54.583302 0.0 701740800 GD.BAY 31 1.900000 5 0.532000 1 4 192 52 75.900002 230.0 0.33000
4 BBFFG2006018 16.379999 55.080002 72.0 1165190400 BBHOL5 67 0.000032 5 NaN 2 4 99 52 409.701599 1680.0 0.24387

Variable attributes

Each variable carries its own attributes: a long name, a standard name from the CF vocabulary, and units where applicable. The table below is built entirely from the file. It tells a reader what every column means, with no external documentation. This is what makes a MARIS file self-documented.

meta = {}
for vn, v in grp.variables.items():
    meta[vn] = {att: v.getncattr(att) for att in v.ncattrs()}

pd.DataFrame(meta).T
long_name standard_name units axis time_origin time_zone abbreviation calendar
id Measurement ID NaN NaN NaN NaN NaN NaN NaN
id_provider Measurement ID as defined by data provider NaN NaN NaN NaN NaN NaN NaN
lon Measurement longitude longitude degrees_east NaN NaN NaN NaN NaN
lat Measurement latitude latitude degrees_north NaN NaN NaN NaN NaN
smp_depth Sample depth below seal level sample_depth_below_sea_floor m Z NaN NaN NaN NaN
time Time of measurement time seconds since 1970-01-01 00:00:00.0 T 1970-01-01 00:00:00 UTC Date/Time gregorian
station Station station NaN NaN NaN NaN NaN NaN
nuclide Nuclide nuclide NaN NaN NaN NaN NaN NaN
value Activity activity NaN NaN NaN NaN NaN NaN
unit Unit unit NaN NaN NaN NaN NaN NaN
unc Uncertainty uncertainty NaN NaN NaN NaN NaN NaN
dl Detection limit threshold detection_limit_threshold NaN NaN NaN NaN NaN NaN
bio_group Biota group biota_group_tbd NaN NaN NaN NaN NaN NaN
species Species species NaN NaN NaN NaN NaN NaN
body_part Body part body_part_tbd NaN NaN NaN NaN NaN NaN
drywt Dry weight of biota sample, expressed in grams. dry_weight_of_biota_sample NaN NaN NaN NaN NaN NaN
wetwt Wet weight of biota sample, expressed in grams. wet_weight_of_biota_sample NaN NaN NaN NaN NaN NaN
percentwt Dry weight as a percentage of fresh weight. percentage_weight_of_biota_sample NaN NaN NaN NaN NaN NaN

Decode the nomenclatures

Coded columns are NetCDF enumeration types. The mapping from code to name lives in the file, in the variable’s datatype. Invert the mapping and apply it to the column to replace codes with names.

nuclide_enum = grp.variables['nuclide'].datatype.enum_dict
code2name = {v: k for k, v in nuclide_enum.items()}
df_biota['nuclide'].map(code2name)
0      k40
1     co60
2      be7
3    cs134
4    pu238
5    cs134
6      k40
7     sr90
8    cs134
9      k40
Name: nuclide, dtype: str
for col in ['nuclide', 'unit', 'dl', 'bio_group', 'species', 'body_part']:
    enum = grp.variables[col].datatype.enum_dict
    df_biota[col + '_name'] = df_biota[col].map({v: k for k, v in enum.items()})

Decode the time

Time is stored as seconds since a reference date. The reference date and calendar live in the units attribute of the time variable. cftime decodes the column using that attribute, so the code works for any MARIS file, whatever reference date it uses.

from cftime import num2date

units = grp.variables['time'].units
df_biota['time'] = num2date(df_biota['time'].values, units=units, only_use_cftime_datetimes=False)
df_biota['time']
0   1999-06-15
1   1987-12-17
2   2008-11-03
3   1992-03-28
4   2006-12-04
5   1998-10-12
6   1995-09-12
7   1991-12-16
8   2011-03-03
9   1994-10-10
Name: time, dtype: datetime64[us]

Two details before analysis

Two details matter for analysis. The value variable has no units attribute because the unit is per measurement: the unit column of each row holds it. And dl is a code, not a number. dl_name says whether a value was detected or falls below the detection limit. Filter or flag it before plotting.