load_data
def load_data()->dict:Fetch Fram Strait CSV records and return one combined SEAWATER DataFrame
This handler converts Fram Strait I-129, U-236, and U-238 seawater CSV records from Zenodo into MARIS-standard NetCDF4 data. RECORDS lists six source files spanning 2020 to 2025.
I-129 measurements come from ETH Zürich (Laboratory of Ion Beam Physics). U-236 and U-238 measurements come from ETH and from VERA (Vienna Environmental Research Accelerator facility, University of Vienna). Where VERA reported a value, the handler uses it and drops the matching ETH value. U-238 is converted from ppb to atoms/kg.
The handler reshapes each record’s wide value and uncertainty columns into one row per measurement, and combines all records into the SEAWATER group. Each output row keeps its source Cruise identifier and carries MARIS identifiers for nuclide, unit, and detection status. Laboratory reconciliation is in progress.
The Fram Strait records on Zenodo need to be homogenized before ingestion. The FS2020_21 record contains measurements from two laboratories: the Laboratory of Ion Beam Physics at ETH Zürich, Switzerland (ETH), and the Vienna Environmental Research Accelerator facility at the University of Vienna, Austria (VERA). The FS2022 through FS2025 records contain ETH measurements only. Their latitude and longitude columns use different names than FS2020_21, and their date columns are inconsistent even among themselves: some records report a single Date column, others report separate Year, Month, and Day columns.
Some samples were measured by both ETH and VERA. Measurement values and their uncertainties arrive in wide format: each column name encodes a laboratory, nuclide, and unit, such as ETH_unc_I129_at_l, and VERA’s columns carry no laboratory prefix. The handler prefixes VERA’s columns with VERA_ and the FS2022-2025 records’ columns with ETH_, so every record follows the same {lab}_[unc_]{nuclide}_{unit} naming before reshaping.
For a given sample and nuclide, MARIS reports a measurement from one laboratory. The data provider set this precedence: ETH measured I-129 in every case. For U-236 and U-238, the handler uses VERA’s value when VERA reported one, and ETH’s value otherwise. The handler drops the U-236/U-238 ratio columns that ETH alone reported, since MARIS derives that ratio from the other columns.
Fetch Fram Strait CSV records and return one combined SEAWATER DataFrame
META_COLS = ['Cruise', 'Station', 'Latitude_degN', 'Longitude_degE', 'Date',
'Niskin', 'Pressure_dbar', 'PracticalSalinity', 'Temperature_degC', 'Sample_ID']
ETH_COLS = ['ETH_I129_at_l', 'ETH_unc_I129_at_l', 'ETH_I129_at_kg', 'ETH_unc_I129_at_kg',
'ETH_U236_at_l', 'ETH_unc_U236_at_l', 'ETH_U236_at_kg', 'ETH_unc_U236_at_kg',
'ETH_U238_ppb', 'ETH_unc_U238_ppb']
VERA_COLS = ['VERA_U236_at_l', 'VERA_unc_U236_at_l', 'VERA_U236_at_kg', 'VERA_unc_U236_at_kg',
'VERA_U238_ppb', 'VERA_unc_U238_ppb']
RAW_COLS = META_COLS + ETH_COLS + VERA_COLSdef _normalize_2020_21(df: pd.DataFrame) -> pd.DataFrame:
"Align FS20/FS21 records (ETH + VERA labs) to the canonical Fram Strait raw schema"
df = df.drop(columns=[c for c in df.columns if 'U236_U238' in c])
df = df.rename(columns={
'Temperature_degC_ctd': 'Temperature_degC',
'Salinity_ctd': 'PracticalSalinity',
'Bottle': 'Niskin',
})
df = _prefix_lab_cols(df, 'ETH', ['I129_at_l', 'I129_at_kg'])
df = _prefix_lab_cols(df, 'VERA', ['U236_at_l', 'U236_at_kg', 'U238_ppb'])
for stem in ['U236_at_l', 'U236_at_kg', 'U238_ppb']:
df = _prefer_vera(df, stem)
df['Date'] = pd.to_datetime(df['Date'], format='%d/%m/%Y').dt.strftime('%Y-%m-%d')
return df[RAW_COLS]def _normalize_2022_25(df: pd.DataFrame) -> pd.DataFrame:
"Align FS22-25 records (ETH lab only) to the canonical Fram Strait raw schema"
df = df.rename(columns={'Latitude': 'Latitude_degN', 'Longitude': 'Longitude_degE'})
if 'Date' not in df.columns:
df['Date'] = pd.to_datetime(df[['Year', 'Month', 'Day']]).dt.strftime('%Y-%m-%d')
df = _prefix_lab_cols(df, 'ETH', ['I129_at_l', 'I129_at_kg'])
return df.reindex(columns=RAW_COLS)RECORDS = {
"FS2020_21": {
"url": "https://zenodo.org/records/19387002/files/FramStrait_2020_2021_radionuclides.csv?download=1",
"adapter": _normalize_2020_21
},
"FS2022_i129": {
"url": "https://zenodo.org/records/20425174/files/FS2022_i129.csv?download=1",
"adapter": _normalize_2022_25
},
"FS2022S_i129": {
"url": "https://zenodo.org/records/20628610/files/FS2022S_data_i129.csv?download=1",
"adapter": _normalize_2022_25
},
"FS2023_i129": {
"url": "https://zenodo.org/records/20448047/files/FS2023_i129.csv?download=1",
"adapter": _normalize_2022_25
},
"FS2024_i129": {
"url": "https://zenodo.org/records/20761705/files/FS2024_i129.csv?download=1",
"adapter": _normalize_2022_25
},
"FS2025_i129": {
"url": "https://zenodo.org/records/20761832/files/FS2025_i129.csv?download=1",
"adapter": _normalize_2022_25
},
}
status = 'Active'Index(['Cruise', 'Station', 'Latitude_degN', 'Longitude_degE', 'Date',
'Niskin', 'Pressure_dbar', 'PracticalSalinity', 'Temperature_degC',
'Sample_ID', 'ETH_I129_at_l', 'ETH_unc_I129_at_l', 'ETH_I129_at_kg',
'ETH_unc_I129_at_kg', 'ETH_U236_at_l', 'ETH_unc_U236_at_l',
'ETH_U236_at_kg', 'ETH_unc_U236_at_kg', 'ETH_U238_ppb',
'ETH_unc_U238_ppb', 'VERA_U236_at_l', 'VERA_unc_U236_at_l',
'VERA_U236_at_kg', 'VERA_unc_U236_at_kg', 'VERA_U238_ppb',
'VERA_unc_U238_ppb'],
dtype='str')
count mean min max
Station 877.0 2.052212e+02 1.000000e+00 4.150000e+02
Latitude_degN 877.0 7.886102e+01 7.867350e+01 8.040850e+01
Longitude_degE 877.0 -3.883666e+00 -1.699983e+01 8.005167e+00
Niskin 877.0 1.040023e+01 1.000000e+00 2.400000e+01
Pressure_dbar 877.0 1.859958e+02 1.992000e+00 2.703528e+03
PracticalSalinity 875.0 3.358706e+01 2.850500e+01 3.510550e+01
Temperature_degC 876.0 6.933839e-01 -1.815000e+00 9.699700e+00
Sample_ID 877.0 6.550969e+01 1.000000e+00 1.800000e+02
ETH_I129_at_l 872.0 3.298183e+09 1.424944e+08 7.422415e+09
ETH_unc_I129_at_l 872.0 1.120120e+08 2.905559e+06 5.696773e+08
ETH_I129_at_kg 345.0 3.389845e+09 1.390414e+08 7.092565e+09
ETH_unc_I129_at_kg 345.0 1.609641e+08 2.835150e+06 5.560286e+08
ETH_U236_at_l 17.0 1.289830e+07 1.110658e+07 1.974957e+07
ETH_unc_U236_at_l 17.0 5.531827e+05 3.803456e+05 8.944368e+05
ETH_U236_at_kg 17.0 1.254941e+07 1.080000e+07 1.921000e+07
ETH_unc_U236_at_kg 17.0 5.382353e+05 3.700000e+05 8.700000e+05
ETH_U238_ppb 17.0 3.306471e+00 2.970000e+00 3.490000e+00
ETH_unc_U238_ppb 17.0 6.647059e-02 5.000000e-02 1.100000e-01
VERA_U236_at_l 155.0 1.813813e+07 4.730000e+06 3.901000e+07
VERA_unc_U236_at_l 155.0 1.331226e+06 4.000000e+05 7.650000e+06
VERA_U236_at_kg 155.0 1.765394e+07 4.548323e+06 3.799334e+07
VERA_unc_U236_at_kg 155.0 1.295806e+06 3.846362e+05 7.450630e+06
VERA_U238_ppb 155.0 3.240774e+00 2.640000e+00 3.600000e+00
VERA_unc_U238_ppb 155.0 1.118065e-01 3.000000e-02 2.700000e-01
FS is seawater-only. RenameColsCB maps provider metadata columns to MARIS working names, ParseDateTimeCB converts the collection date to UTC TIME, and AddDepthCB computes sampling depth from the reported Pressure_dbar using the TEOS-10 equation of state via the gsw library.
Map Fram Strait provider columns to MARIS standard names
# Verify RenameColsCB maps provider columns to MARIS names
dfs_mock = {
"SEAWATER": pd.DataFrame({
"Station": [341],
"Latitude_degN": [78.832167],
"Longitude_degE": [-2.004667],
"PracticalSalinity": [34.9],
"Temperature_degC": [0.0538],
"Sample_ID": [1],
})
}
tfm = Transformer(dfs_mock, cbs=[RenameColsCB()])
tfm()
for col in ["STATION", "LAT", "LON", "SAL", "TEMP", "SMP_ID_PROVIDER"]:
test_eq(col in tfm.dfs["SEAWATER"].columns, True)
print("RenameColsCB: FS2025 columns mapped correctly. ✓")RenameColsCB: FS2025 columns mapped correctly. ✓
LAT LON STATION SAL TEMP SMP_ID_PROVIDER
0 78.833167 -0.003667 47 34.9311 1.7576 1
1 78.833167 -0.003667 47 34.9247 2.4914 2
Parse FS collection date into a UTC TIME value
# Verify ParseDateTimeCB parses Date into UTC TIME
dfs_mock = {"SEAWATER": pd.DataFrame({"Date": ["2025-07-30"]})}
tfm = Transformer(dfs_mock, cbs=[ParseDateTimeCB()])
tfm()
test_eq("TIME" in tfm.dfs["SEAWATER"].columns, True)
test_eq("Date" not in tfm.dfs["SEAWATER"].columns, True)
print(f"ParseDateTimeCB: TIME = {tfm.dfs['SEAWATER']['TIME'].iloc[0]}. ✓")ParseDateTimeCB: TIME = 2025-07-30 00:00:00+00:00. ✓
TIME
0 2020-08-27 00:00:00+00:00
1 2020-08-27 00:00:00+00:00
2 2020-08-27 00:00:00+00:00
| Cruise | STATION | LAT | LON | Niskin | Pressure_dbar | SAL | TEMP | SMP_ID_PROVIDER | ETH_I129_at_l | ... | ETH_unc_U236_at_kg | ETH_U238_ppb | ETH_unc_U238_ppb | VERA_U236_at_l | VERA_unc_U236_at_l | VERA_U236_at_kg | VERA_unc_U236_at_kg | VERA_U238_ppb | VERA_unc_U238_ppb | TIME | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | FS20 | 47 | 78.833167 | -0.003667 | 13 | 400.374 | 34.9311 | 1.7576 | 1 | 1.744666e+09 | ... | NaN | NaN | NaN | 13570000.0 | 1120000.0 | 13177053.06 | 1.087568e+06 | 3.35 | 0.15 | 2020-08-27 00:00:00+00:00 |
| 1 | FS20 | 47 | 78.833167 | -0.003667 | 15 | 200.500 | 34.9247 | 2.4914 | 2 | 2.015258e+09 | ... | NaN | NaN | NaN | 12520000.0 | 470000.0 | 12169354.84 | 4.568368e+05 | 3.11 | 0.06 | 2020-08-27 00:00:00+00:00 |
| 2 | FS20 | 47 | 78.833167 | -0.003667 | 16 | 150.565 | 34.9469 | 2.9641 | 3 | 2.142153e+09 | ... | NaN | NaN | NaN | 11910000.0 | 1220000.0 | 11579324.34 | 1.186127e+06 | 3.09 | 0.14 | 2020-08-27 00:00:00+00:00 |
| 3 | FS20 | 47 | 78.833167 | -0.003667 | 17 | 100.702 | 34.9915 | 4.0705 | 4 | 2.384657e+09 | ... | NaN | NaN | NaN | 13760000.0 | 620000.0 | 13381607.36 | 6.029503e+05 | 3.13 | 0.11 | 2020-08-27 00:00:00+00:00 |
| 4 | FS20 | 47 | 78.833167 | -0.003667 | 18 | 74.915 | 34.9148 | 4.1901 | 5 | 2.700046e+09 | ... | NaN | NaN | NaN | 14300000.0 | 490000.0 | 13909361.65 | 4.766145e+05 | 3.10 | 0.08 | 2020-08-27 00:00:00+00:00 |
5 rows × 26 columns
We use the Thermodynamic Equation Of Seawater - 2010 (TEOS-10) via the gsw Python package (gsw.z_from_p) to convert reported Pressure_dbar to sampling depth in metres. The conversion uses the reported latitude for the gravitational acceleration correction. We have rounded depths to one decimal place.
Compute sampling depth using Thermodynamic Equation of SeaWater 2010 (TEOS-10)
As already mentioned, Fram Strait data layout is wide. Each sample occupies one row, and each laboratory result sits in its own column, named for the laboratory, nuclide, and unit, with a twin column holding its uncertainty.
In MARIS, each variable (lab, unit, value, uncertainty) is a column, each measurement is a row.
MeltFramStraitCB performs that reshape. It matches every measurement column by name, melts the values and uncertainties into one row per measurement so that each value keeps its own uncertainty, and drops the rows where no value was reported.
Column headers in the Fram Strait dataset encode multiple pieces of information (lab, nuclide, unit) into a single string. This adds friction to data ingestion, since every new dataset with a different naming convention requires custom parsing logic. MARIS prefers a tidy data layout (Wickham 2014, doi:10.18637/jss.v059.i10 where each column holds a single variable, and metadata like nuclide, unit, and method are stored as separate columns, not baked into the header.
Reshape wide Fram Strait value and uncertainty columns into one row per measurement
dfs_mock = {
"SEAWATER": pd.DataFrame({
"STATION": [101, 102],
"LAT": [78.5, 79.0],
"LON": [1.0, 2.0],
"TIME": pd.to_datetime(["2025-07-30", "2025-07-31"], utc=True),
"ETH_I129_at_l": [1.2e9, 2.4e9],
"ETH_unc_I129_at_l": [1.0e8, 2.0e8],
"ETH_I129_at_kg": [3.4e9, np.nan],
"ETH_unc_I129_at_kg": [3.0e8, np.nan],
"VERA_U236_at_l": [1.5e7, np.nan],
"VERA_unc_U236_at_l": [1.2e6, np.nan],
})
}
tfm = Transformer(dfs_mock, cbs=[MeltFramStraitCB()])
tfm()
out = tfm.dfs["SEAWATER"].sort_values(["STATION", "LAB", "NUCLIDE", "UNIT"]).reset_index(drop=True)
print(out[["STATION", "LAB", "NUCLIDE", "UNIT", "VALUE", "UNC"]].to_string(index=False))
test_eq(len(out), 4)
test_eq(out["STATION"].tolist(), [101, 101, 101, 102])
test_eq(out["LAB"].tolist(), ["ETH", "ETH", "VERA", "ETH"])
test_eq(out["NUCLIDE"].tolist(), ["I129", "I129", "U236", "I129"])
test_eq(out["UNIT"].tolist(), ["at_kg", "at_l", "at_l", "at_l"])
test_eq(out["VALUE"].tolist(), [3.4e9, 1.2e9, 1.5e7, 2.4e9])
test_eq(out["UNC"].tolist(), [3.0e8, 1.0e8, 1.2e6, 2.0e8])
print("MeltFramStraitCB: wide results reshaped, two labs kept separate, missing values removed. ✓") STATION LAB NUCLIDE UNIT VALUE UNC
101 ETH I129 at_kg 3400000000.0 300000000.0
101 ETH I129 at_l 1200000000.0 100000000.0
101 VERA U236 at_l 15000000.0 1200000.0
102 ETH I129 at_l 2400000000.0 200000000.0
MeltFramStraitCB: wide results reshaped, two labs kept separate, missing values removed. ✓
The following example shows how MeltFramStraitCB converts the wide value and uncertainty columns for both laboratories into one row per reported measurement, keeps laboratory, nuclide, and unit as separate columns, and drops missing values.
Rows: 1733
LAB NUCLIDE UNIT
ETH I129 at_kg 345
at_l 872
U236 at_kg 17
at_l 17
U238 ppb 17
VERA U236 at_kg 155
at_l 155
U238 ppb 155
dtype: int64
Cruise LAB NUCLIDE UNIT VALUE UNC
0 FS20 ETH I129 at_kg 1.702534e+09 1.341982e+08
1 FS20 ETH I129 at_l 1.744666e+09 1.375192e+08
5 FS20 VERA U236 at_kg 1.317705e+07 1.087568e+06
6 FS20 VERA U236 at_l 1.357000e+07 1.120000e+06
7 FS20 VERA U238 ppb 3.350000e+00 1.500000e-01
8 FS20 ETH I129 at_kg 1.966604e+09 1.549447e+08
# Each output row carries a value and its uncertainty, so the non-null cell count
# across all measurement columns should be exactly twice the melted row count.
wide = dfs['SEAWATER']
meas = [c for c in wide.columns if MEAS_PAT.fullmatch(c)]
test_eq(len(out) * 2, wide[meas].notna().sum().sum())
print(f"{len(out)} measurements from {len(meas)} columns. ✓")1733 measurements from 16 columns. ✓
EXPECTED = {
('ETH', 'I129', 'at_l'): 872, ('ETH', 'I129', 'at_kg'): 345,
('ETH', 'U236', 'at_l'): 17, ('ETH', 'U236', 'at_kg'): 17,
('ETH', 'U238', 'ppb'): 17, ('VERA', 'U236', 'at_l'): 155,
('VERA', 'U236', 'at_kg'): 155, ('VERA', 'U238', 'ppb'): 155,
}
test_eq(out.groupby(['LAB', 'NUCLIDE', 'UNIT']).size().to_dict(), EXPECTED)Fram Strait reports U-238 in parts-per-billion (ppb, mass of U per mass of seawater), while MARIS requires atoms per kg. ConvertU238CB converts the VALUE column for U-238 rows using:
\[\text{atoms/kg} = C_{\text{ppb}} \times 10^{-9} \times 1000 \times \frac{N_A}{M_{238}}\]
where (N_A = 6.02214076 {23} {-1}) is Avogadro’s number and (M_{238} = 238.05 ^{-1}) is the molar mass of U-238, giving a conversion factor of (2.530 {15} {-1},^{-1}). Rows with other nuclides are left unchanged.
Convert U-238 VALUE from ppb to atoms/kg
# Verify ConvertU238CB scales VALUE and UNC for U-238 only
dfs_mock = {'SEAWATER': pd.DataFrame({
'NUCLIDE': ['I129', 'U238'],
'UNIT': ['at_kg', 'at_ppb'],
'VALUE': [1.0, 2.0],
'UNC': [0.1, 0.2],
})}
tfm = Transformer(dfs_mock, cbs=[ConvertU238CB()])
tfm()
out = tfm.dfs['SEAWATER']
test_eq(out.loc[out['NUCLIDE']=='U238', 'VALUE'].iloc[0], 2.0 * U238_PPB_TO_AT_KG)
test_eq(out.loc[out['NUCLIDE']=='U238', 'UNC'].iloc[0], 0.2 * U238_PPB_TO_AT_KG)
test_eq(out.loc[out['NUCLIDE']=='U238', 'UNIT'].iloc[0], 'at_kg')
print("ConvertU238CB: I-129 unchanged, U-238 scaled, UNIT updated. ✓")ConvertU238CB: I-129 unchanged, U-238 scaled, UNIT updated. ✓
U-238 rows: 172, mean VALUE = 8.21e+15 atoms/kg
The melt produces string columns: NUCLIDE (I129, U236, U238), UNIT (at_kg, at_l), and LAB columns. MARIS stores these as integer foreign-key IDs from the central nomenclatures.
RemapCB maps source column values through a lookup table to a target column.
# Verify RemapCB assigns correct NUCLIDE, UNIT, LAB, AREA IDs
dfs_mock = {'SEAWATER': pd.DataFrame({
'NUCLIDE': ['I129', 'U236'],
'LAB': ['ETH', 'VERA'],
'UNIT': ['at_kg', 'at_l'],
'VALUE': [1.0, 2.0],
'UNC': [0.1, 0.2],
})}
tfm = Transformer(dfs_mock, cbs=[
RemapCB(lut=NUCLIDE_LUT, col_remap='NUCLIDE', col_src='NUCLIDE'),
RemapCB(lut=UNIT_LUT, col_remap='UNIT', col_src='UNIT'),
RemapCB(lut=LAB_LUT, col_remap='LAB', col_src='LAB')
])
tfm()
out = tfm.dfs['SEAWATER']
test_eq(out['NUCLIDE'].tolist(), [28, 108])
test_eq(out['UNIT'].tolist(), [9, 12])
print("RemapCB: all nomenclatures mapped to correct MARIS IDs. ✓")RemapCB: all nomenclatures mapped to correct MARIS IDs. ✓
tfm = Transformer(dfs, cbs=[
RenameColsCB(),
ParseDateTimeCB(),
AddDepthCB(),
MeltFramStraitCB(),
ConvertU238CB(),
RemapCB(lut=NUCLIDE_LUT, col_remap='NUCLIDE', col_src='NUCLIDE'),
RemapCB(lut=UNIT_LUT, col_remap='UNIT', col_src='UNIT'),
RemapCB(lut=LAB_LUT, col_remap='LAB', col_src='LAB')
])
tfm()
out = tfm.dfs['SEAWATER']
print(f"Rows: {len(out)}")
print(out.groupby(['LAB', 'NUCLIDE', 'UNIT']).size())
print(out[['Cruise', 'LAB', 'NUCLIDE', 'UNIT', 'VALUE', 'UNC']].head(6).to_string())
print('Units: ', out.UNIT.unique())Rows: 1733
LAB NUCLIDE UNIT
345 28 9 345
12 872
64 9 17
108 9 17
12 17
504 64 9 155
108 9 155
12 155
dtype: int64
Cruise LAB NUCLIDE UNIT VALUE UNC
0 FS20 345 28 9 1.702534e+09 1.341982e+08
1 FS20 345 28 12 1.744666e+09 1.375192e+08
5 FS20 504 108 9 1.317705e+07 1.087568e+06
6 FS20 504 108 12 1.357000e+07 1.120000e+06
7 FS20 504 64 9 8.474763e+15 3.794670e+14
8 FS20 345 28 9 1.966604e+09 1.549447e+08
Units: [ 9 12]
The provider does not report a detection level, but MARIS requires this field. The available MARIS categories are:
{'Not applicable': -1,
'Not available': 0,
'Detected value': 1,
'Detection limit': 2,
'Not detected': 3,
'Derived': 4}
Assign missing Detection Limit column to MARIS ‘Detected value: 1’ category
SanitizeLonLatCB, EncodeTimeCB, and AddSampleIDCB need no FS-specific callback. SanitizeLonLatCB validates lat/lon ranges and corrects sign convention. EncodeTimeCB encodes TIME into the NetCDF numeric representation. AddSampleIDCB assigns sequential SMP_ID and preserves SMP_ID_PROVIDER. All three come from marisco.callbacks.
Cast STATION to string before encoding
FS’s Station column is pure numeric, so pandas infers int64. But STATION maps to a string-typed NetCDF variable, so FormatStationCB casts it to str as the last step before encoding. This callback is defined locally in this notebook only.
Cast STATION to str for the NetCDF4 string-typed station variable
# Verify FormatStationCB casts STATION to str
dfs_mock = {'SEAWATER': pd.DataFrame({'STATION': [341, 415]})}
tfm = Transformer(dfs_mock, cbs=[FormatStationCB()])
tfm()
out = tfm.dfs['SEAWATER']
test_eq(out['STATION'].tolist(), ['341', '415'])
test_eq(all(isinstance(v, str) for v in out['STATION']), True) # what the encoder's per-element NetCDF write actually needs
print("FormatStationCB: STATION cast to str. ✓")FormatStationCB: STATION cast to str. ✓
get_cbs returns the callbacks of the pipeline, in order. The cell below and encode both use it, so the pipeline is defined once. It returns new instances on each call, so no state carries over between runs.
Callbacks, in pipeline order, that turn raw Fram Strait data into MARIS-standard DataFrames
def get_cbs() -> list:
"Callbacks, in pipeline order, that turn raw Fram Strait data into MARIS-standard DataFrames"
return [
RenameColsCB(),
ParseDateTimeCB(),
AddDepthCB(),
MeltFramStraitCB(),
ConvertU238CB(),
RemapCB(lut=NUCLIDE_LUT, col_remap='NUCLIDE', col_src='NUCLIDE'),
RemapCB(lut=UNIT_LUT, col_remap='UNIT', col_src='UNIT'),
RemapCB(lut=LAB_LUT, col_remap='LAB', col_src='LAB'),
AddDetectionLimitCB(),
SanitizeLonLatCB(),
EncodeTimeCB(),
AddSampleIDCB(col_provider="SMP_ID_PROVIDER"),
FormatStationCB()
]Final shape: (1733, 18)
Columns: ['Cruise', 'STATION', 'LAT', 'LON', 'Niskin', 'Pressure_dbar', 'SAL', 'TEMP', 'SMP_ID_PROVIDER', 'TIME', 'SMP_DEPTH', 'LAB', 'NUCLIDE', 'UNIT', 'UNC', 'VALUE', 'DL', 'SMP_ID']
SMP_ID SMP_ID_PROVIDER NUCLIDE UNIT LAB
0 1 1 28 9 345
1 2 1 28 12 345
2 3 1 108 9 504
3 4 1 108 12 504
Final data summary (uppercase columns only):
LAT LON SAL TEMP TIME SMP_DEPTH LAB NUCLIDE UNIT UNC VALUE DL SMP_ID
count 1733.000000 1733.000000 1723.000000 1728.000000 1.733000e+03 1733.000000 1733.000000 1733.000000 1733.000000 1.733000e+03 1.733000e+03 1733.0 1733.000000
mean 78.846159 -3.794303 33.565289 0.622876 1.648324e+09 165.295788 387.663012 47.452972 10.807271 2.694743e+13 8.153272e+14 1.0 867.000000
std 0.139003 5.595021 1.598968 2.359036 5.075129e+07 277.559678 70.470930 31.956175 1.468615 9.420889e+13 2.462455e+15 0.0 500.418325
min 78.673500 -16.999833 28.504999 -1.815000 1.598486e+09 2.000000 345.000000 28.000000 9.000000 3.700000e+05 4.548323e+06 1.0 1.000000
25% 78.833000 -7.999333 32.363700 -1.419200 1.598918e+09 25.300000 345.000000 28.000000 9.000000 3.906112e+07 1.808362e+09 1.0 434.000000
50% 78.833333 -4.005833 34.243200 -0.106200 1.628208e+09 99.100000 345.000000 28.000000 12.000000 7.906752e+07 2.989910e+09 1.0 867.000000
75% 78.834167 -1.000000 34.902600 2.294700 1.663373e+09 197.800000 504.000000 64.000000 12.000000 1.896498e+08 4.289536e+09 1.0 1300.000000
max 80.408500 8.005167 35.105500 9.699700 1.755043e+09 2658.000000 504.000000 108.000000 12.000000 6.830406e+14 9.107208e+15 1.0 1733.000000
The encoder wraps the full pipeline and writes the standardised data to a NetCDF4 file. Global attributes are assembled via GlobAttrsFeeder with BboxCB, DepthRangeCB, TimeRangeCB, plus keywords and processing logs.
The resulting file contains spatial, depth, and time coverage derived from the transformed seawater data, together with FS keywords and the recorded processing steps.
FS_KEYWORDS = [
"Fram Strait","Greenland Sea","I-129","U-236","U-238","radionuclides","seawater","Arctic Ocean",
]
def get_attrs(tfm):
"Retrieve global attributes for Fram Strait"
return GlobAttrsFeeder(tfm.dfs, cbs=[
BboxCB(),
DepthRangeCB(),
TimeRangeCB(),
KeyValuePairCB("keywords", ", ".join(FS_KEYWORDS)),
KeyValuePairCB("publisher_postprocess_logs", ", ".join(tfm.logs)),
])()Fram Strait 2020-2025 I-129, U-236, U-238 seawater radionuclide data
def encode(
dest=None, # Output NetCDF file path
src=None, # Unused; Fram Strait fetches its data from RECORDS
**kwargs # Additional arguments
):
"Fram Strait 2020-2025 I-129, U-236, U-238 seawater radionuclide data"
dfs = load_data()
tfm = Transformer(dfs, cbs=get_cbs())
tfm()
encoder = NetCDFEncoder(tfm.dfs, dest_fname=dest,
global_attrs=get_attrs(tfm))
encoder.encode()Fram Strait NetCDF written.
[Path('../../_data/output/fram_strait_2020_2025_SEAWATER.csv')]
df = pd.read_csv("../../_data/output/fram_strait_2020_2025_SEAWATER.csv")
print(f"Shape: {df.shape}")
print(f"Columns: {df.columns.tolist()}")
print(f"\nNuclide IDs: {df.nuclide_id.unique()}")
print(f"Unit IDs: {df.unit_id.unique()}")
print(f"Sample type IDs: {df.samptype_id.unique()}")
print(f"\nStation range: {df.station.min()}–{df.station.max()}")
print(f"Date range: {df.begperiod.min()} to {df.begperiod.max()}")
print(f"Sample depth range: {df.sampdepth.min()} to {df.sampdepth.max()}m")Shape: (1733, 15)
Columns: ['detection', 'lab_id', 'latitude', 'longitude', 'nuclide_id', 'salinity', 'sampdepth', 'samplabcode', 'station', 'temperatur', 'begperiod', 'uncertaint', 'unit_id', 'activity', 'samptype_id']
Nuclide IDs: [ 28 108 64]
Unit IDs: [ 9 12]
Sample type IDs: [1]
Station range: 1–415
Date range: 2020-08-27 to 2025-08-13
Sample depth range: 2.0 to 2658.0m
detection lab_id latitude longitude nuclide_id salinity sampdepth \
0 = 345 78.83317 -0.003667 28 34.9311 395.8
1 = 345 78.83317 -0.003667 28 34.9311 395.8
2 = 504 78.83317 -0.003667 108 34.9311 395.8
3 = 504 78.83317 -0.003667 108 34.9311 395.8
4 = 504 78.83317 -0.003667 64 34.9311 395.8
samplabcode station temperatur begperiod uncertaint unit_id \
0 1 47 1.7576 2020-08-27 1.341982e+08 9
1 1 47 1.7576 2020-08-27 1.375192e+08 12
2 1 47 1.7576 2020-08-27 1.087568e+06 9
3 1 47 1.7576 2020-08-27 1.120000e+06 12
4 1 47 1.7576 2020-08-27 3.794670e+14 9
activity samptype_id
0 1.702534e+09 1
1 1.744666e+09 1
2 1.317705e+07 1
3 1.357000e+07 1
4 8.474763e+15 1