Sample data — demo dataset walkthrough
The sample_data/ directory at the root of the repository contains a
complete, ready-to-run example: hourly observations and model output for
50 European monitoring stations, four pollutants, covering the full year
2021. No additional data download is required.
Directory layout
sample_data/
├── startup50_FINAL.ini station metadata (coordinates, types, variables)
├── run_ModCsv_assessment.yaml run configuration — point fmm_assess here
└── CSV/
├── obs50/ observations — one CSV per station (50 files)
└── mod50_FH0/ model output — one CSV per station (50 files)
Each CSV file is named after the EEA station code (e.g. AT30104.csv)
and contains semicolon-separated hourly data:
year;month;day;hour;NO2;O3;PM25;PM10
2021;1;1;0;4.5;44.0;12.32;14.27
2021;1;1;1;5.0;41.5;13.77;15.77
2021;1;1;2;6.0;38.5;14.74;16.77
...
Missing observations are encoded either as -999 or as empty fields
(both are handled by the reader). The header row is fixed in this
exact order; only the four pollutant columns visible above are read by
the pipeline.
Dataset characteristics:
Property |
Value |
|---|---|
Period |
1 January – 31 December 2021 |
Stations |
50 (pan-European, diverse station types and areas) |
Pollutants |
NO₂, O₃, PM₂.₅, PM₁₀ |
Time resolution |
Hourly |
File format |
CSV (semicolon-delimited) |
The startup file — startup50_FINAL.ini
The startup file supplies station metadata to the pipeline. It has three sections.
[MODEL] — dataset-level attributes:
[MODEL]
;Year
;frequency
;Scale
2021
hour
urban
These three lines declare the reference year, the temporal resolution
(hour), and the default spatial scale.
[PARAMETERS] — pollutants present in the CSV files:
[PARAMETERS]
;Specie*type*measureunit
NO2;GAS;ugm-3
O3;GAS;ugm-3
PM25;PM;ugm-3
PM10;PM;ugm-3
Each line declares a pollutant, its physical type (GAS / PM), and
its measurement unit. The pollutant names here must match the column
headers in the CSV files.
[MONITORING] — one row per station, with a fixed 12-column header:
[MONITORING]
StationCode;StationName;Stationabbreviation;Altitude;Lon;Lat;GMTLag;Region;StationType;AreaType;Siting;listOfvariables
AD0945A;AD0945A;3;2515.00;1.716990;42.534900;GMT;AD;background;rural;Encamp;O3*;
AT2M611;AT2M611;22;470.00;14.379400;46.760600;GMT;AT;background;suburban;Carinthia;O3*;
AT30104;AT30104;32;265.00;14.519500;48.192200;GMT;AT;traffic;rural;LowerAustria;NO2*O3*PM25*PM10*;
...
Column |
Meaning |
|---|---|
|
EEA short code; must match the CSV filename ( |
|
Human-readable label used on plots |
|
Short numeric/alphanumeric ID |
|
Metres above sea level |
|
WGS-84 decimal degrees |
|
UTC offset (string; |
|
Country / sub-region label (free-text) |
|
|
|
|
|
Free-text local-context label |
|
|
A station is included for a given pollutant only if that pollutant
appears in its listOfvariables. The StationType × AreaType
combination drives the SI / MPI classification (UB, RB, UT,
industrial/traffic/background coherence cells, etc.).
An optional 13th column MeasurementType (fixed | indicative) can
be appended to a station’s row to override the run-level default for
that station. When absent or blank, the YAML measurement_type field
is used.
The run configuration — run_ModCsv_assessment.yaml
run_id: NO2
pollutant: NO2
year: 2021
aggregations:
- short
- long
uncertainty_def: aaqd
measurement_type: fixed
resolution_short: hourly
resolution_long: annual
min_data_capture: 0.75
min_stations: 10
include_station_types: [background, traffic, industrial]
include_area_types: [urban, suburban, rural]
inputs:
startup_ini: ./startup50_FINAL.ini
obs_dir: ./CSV/obs50
model_file: ./CSV/mod50_FH0
uncertainty: null
outputs:
dir: ./out_no2_aaqd_final
formats: [png, pdf]
dpi: 200
timeseries:
stations: [""]
nuts: []
show_obs: true
Key fields:
Field |
Meaning |
|---|---|
|
Free-text run identifier; appended to every output filename |
|
One of |
|
Reference year (used for time-axis labelling and seasonal slicing) |
|
Subset of |
|
|
|
|
|
Optional time-resolution disambiguators (e.g. |
|
75 % rule on the active aggregation |
|
Below this, the network 90th-percentile is unreliable (warning logged) |
|
Filters applied before any indicator is computed |
|
Path to the startup |
|
Either a directory of per-station CSVs or a single NetCDF file |
|
Same — directory or NetCDF; auto-detected |
|
|
|
Where CSVs, logs and figures are written |
|
Subset of |
|
Optional explicit list of station codes for time-series panels |
|
NUTS-region prefix filter (alternative to explicit list); panels show the mean across matching stations |
All paths in the YAML are resolved relative to the YAML’s parent
directory, so the example above can be run from anywhere as long as
the file structure under sample_data/ is preserved.
Running the example
From a Python shell:
from fmm_assess.pipeline import run_assessment
res = run_assessment("sample_data/run_ModCsv_assessment.yaml")
print(f"MQI_short p90 = {res.aggregate.MQI_short_p90:.3f} "
f"({'PASS' if res.aggregate.MQO_short_pass else 'FAIL'})")
print(f"MQI_long p90 = {res.aggregate.MQI_long_p90:.3f} "
f"({'PASS' if res.aggregate.MQO_long_pass else 'FAIL'})")
From the command line (after editing cli.py to point at the YAML):
python -m fmm_assess.cli
The pipeline typically finishes in well under a minute on the demo dataset.
Expected outputs
After a successful run, sample_data/out_no2_aaqd_final/ will contain:
out_no2_aaqd_final/
├── per_station_NO2.csv one row per station, all indicators
├── summary_NO2.csv pooled network-level scalars
├── summary_by_type_NO2.csv fixed / indicative split (AAQD Annex VI)
├── summary_temporal_coherence_NO2.csv 3×3 temporal-coherence grid
├── run.log timestamped run log
├── mplotl/ matplotlib PNG / PDF figures
│ ├── radar.{png,pdf}
│ ├── target.{png,pdf}
│ ├── ts_report.{png,pdf}
│ ├── scatter.{png,pdf}
│ ├── taylor.{png,pdf}
│ ├── scatter_dyneval.{png,pdf}
│ ├── bars.{png,pdf}
│ └── timeseries_*.{png,pdf}
└── pltly/ plotly interactive HTMLs
├── radar.html
├── target.html
└── ... (same set of eight diagrams)
The two backends produce bit-identical numerical content — same indicators, same dot positions, same pass/fail colouring. The difference is interactivity, not data.
Per-station CSV columns (per_station_<run_id>.csv):
station, station_type, area_type, measurement_type, n_valid_short, n_valid_long, mean_O, mean_M, sigma_O, sigma_M, R, BIAS, RMSE, CRMSE, O_bar, M_bar, D_short, D_long, MQI_short, MQI_long, TI_B, TI_R, TI_sigma, dWS_obs, dWS_mod, dWkWe_obs, dWkWe_mod, dDN_obs, dDN_mod, coh_seasonal, coh_week, coh_day, MPI_UT_UB, MPI_UB_RB.
The last five columns are the per-station §3.7.3 contributions (the
network-level 90th-percentile of these reproduces the corresponding
cell in summary_temporal_coherence_<run_id>.csv and the MPI_UT_UB
/ MPI_UB_RB scalars in summary_<run_id>.csv).
Trying other pollutants
The bundled run_ModCsv_assessment.yaml is configured for NO₂. To
assess O₃, PM₁₀ or PM₂.₅ instead, change the four lines at the top:
run_id: O3 # any free-text label — appears in output filenames
pollutant: O3
resolution_short: max daily 8hr mean # required for O3 short
resolution_long: seasonal # required for O3 long
For PM10 / PM25, set resolution_short: daily and resolution_long: annual.
See TECH_SPEC_fmm_assess.md §4 for the full
valid-value matrix per pollutant.
Customising the dataset
The demo dataset is intentionally small (50 stations, one model realisation, one year). The same pipeline scales to:
larger station networks (hundreds of stations) — the only constraint is that every station appearing in
[MONITORING]has matching CSV filenames underobs_dirandmodel_file;NetCDF inputs — replace
obs_dir/model_filewith paths to.cdf/.nc/.nc4files (DeltaTool format, one variable per station, with a globalParametersattribute listing the species on the V axis);mixed fixed + indicative networks — append a 13th
MeasurementTypecolumn to the relevant[MONITORING]rows.
For everything else, see the Full specification.