Query observed climate data for Denmark from DMI's Climate Data API.
Four commands:
| Command | What it does |
|---|---|
rank |
One place, many times — the highest or lowest N values in a series |
scan |
Many places, one criterion — everywhere in an area crossing a threshold |
stations |
What do nearby stations measure? |
inspect |
What does the grid cell at this point carry? |
dmi-climate-values.py <command> [options]
dmi-climate-values.py <command> --help
Built to answer things like what were the ten wettest hours near Rinkenæs and which stations in Sønderjylland have recorded a skybrud since 1990.
pip install pandas requests pyprojPython ≥ 3.9. pyproj is optional but recommended — see Grid cell resolution below.
No API key. DMI removed authentication from opendataapi.dmi.dk on 2 December 2025;
registration and keys are no longer required for any endpoint. Calls that still send a
key work fine, since the key is ignored. Fair-use rate limiting still applies.
The host moved off the old dmigw.govcloud.dk domain. Tutorials and GitHub scripts
still pointing there are stale.
# what do the nearest stations measure?
dmi-climate-values.py stations --lat 54.9007 --lon 9.5333
# what does the grid cell here carry?
dmi-climate-values.py inspect --lat 54.9007 --lon 9.5333
# ten wettest hours
dmi-climate-values.py rank --lat 54.9007 --lon 9.5333 \
--parameter acc_precip --resolution hour
# every station within 50 km that has recorded a skybrud
dmi-climate-values.py scan --lat 54.9007 --lon 9.5333 \
--parameter max_precip_30m --resolution day --threshold 15 --start 1990-01-01Start with stations or inspect. Parameter availability varies by
station, by grid cell, and by resolution; guessing at IDs is the main source of empty
results.
Pulls the full series for one place and returns the extremes.
dmi-climate-values.py rank --parameter max_temp_w_date --resolution day
dmi-climate-values.py rank --parameter min_temp --resolution day --order min
dmi-climate-values.py rank --parameter acc_precip --resolution hour --decluster 24Station mode (default) searches /collections/station/items in a box around your
point, keeps stations reporting your parameter, and picks the nearest by great-circle
distance. It prints the five nearest first — distance, type, operating period — so you
can see what you're getting. This is what you want for extremes: a gauge measures a
point, so intensity isn't diluted by area averaging.
Grid mode uses the cell containing your point. Spatially complete, no gaps where there's no station, but 10 km is still an area mean so extremes land between ERA5 and a gauge.
Without --decluster, one storm can occupy five of ten slots as consecutive hours.
--decluster 24 keeps only the peak within each 24-hour window, giving ten distinct
events instead.
Usually right for precipitation. Usually wrong for temperature — a heatwave genuinely is several consecutive hot days, and suppressing them hides the duration that made it notable. Default is off; decide per parameter.
Instead of one place over time, finds every place in an area crossing a value.
--threshold is required.
# skybrud stations within 50 km
dmi-climate-values.py scan --lat 54.9007 --lon 9.5333 \
--parameter max_precip_30m --resolution day --threshold 15 --start 1990-01-01
# all of Denmark, saved to CSV
dmi-climate-values.py scan --bbox 7,54,16,58 \
--parameter max_precip_30m --resolution day --threshold 15 \
--start 2000-01-01 --out skybrud.csv
# grid cells rather than stations
dmi-climate-values.py scan --mode grid --radius 80 \
--parameter acc_precip --resolution day --threshold 40 --start 1990-01-01
# below a threshold
dmi-climate-values.py scan --parameter min_temp --resolution day \
--threshold -15 --order min --radius 100One row per place: name, coordinates, peak value, when the peak occurred, and how many times it crossed. Sorted by peak.
Extent. --radius (default 50 km) draws a box around --lat/--lon.
--bbox lon_min,lat_min,lon_max,lat_max overrides it; 7,54,16,58 covers Denmark.
Volume. The API cannot filter on value, so a scan pulls everything in the extent
and filters locally. It accumulates per place as it goes — peak, timestamp, count —
rather than holding the series, so a national multi-decade scan stays within memory.
Hourly scans chunk monthly, everything else yearly, with progress per chunk. Hourly
scans over five years trigger an upfront warning: national hourly is millions of
records and you want --resolution day.
Exceedance counts are not comparable across stations. A station active since 1961
shows more skybrud than one installed in 2010, and that's coverage, not climate. The
count is in the output so you can normalise — the station listing carries
operationFrom and operationTo if you want frequency per year rather than a tally.
| Flag | Default | Notes |
|---|---|---|
--lat, --lon |
Rinkenæs | WGS84 decimal degrees |
--parameter |
acc_precip |
DMI parameterId |
--resolution |
hour |
hour, day, month, year |
--order |
max |
min for cold records and other low-end extremes |
--mode |
station |
station or grid |
--grid-size |
10km |
10km or 20km; grid mode only |
--start, --end |
1990 → today | ISO dates |
--qc-only |
off | Keep only manually quality-controlled values |
--top |
10 |
How many rows to print |
--out |
— | CSV is written only if you pass this. Otherwise print only |
| Flag | Default | Notes |
|---|---|---|
--station-id |
— | Skip the nearest-station search |
--box |
0.6 |
Station search half-width, degrees (~65 km) |
--decluster |
0 |
Hours between events; 0 disables |
| Flag | Default | Notes |
|---|---|---|
--threshold |
required | The value to cross |
--radius |
50 |
Scan extent in km around the point |
--bbox |
— | Scan extent as lon_min,lat_min,lon_max,lat_max |
| Flag | Default | Notes |
|---|---|---|
--lat, --lon |
Rinkenæs | |
--box |
0.6 |
Search half-width, degrees |
--parameter |
— | Only stations reporting this parameter |
--limit |
5 |
Stations to print |
| Flag | Default | Notes |
|---|---|---|
--lat, --lon |
Rinkenæs | |
--grid-size |
10km |
10km or 20km |
Both rank and scan print to the terminal regardless. --out results.csv
additionally writes the full result set, which matters in scan where --top
truncates the printed table but the CSV holds every place.
The Danish kvadratnet is defined on ETRS89 / UTM32N (EPSG:25832), and cell IDs encode
the southwest corner in units of 10 km: 10km_<northing/1e4>_<easting/1e4>. So the
cell containing a point is computed, not searched for. pyproj does the projection.
This matters. An earlier version probed with a bounding box and limit=1, which
returns an arbitrary cell from the box rather than the containing one — at
54.9007, 9.5333 that produced 10km_607_52, a cell spanning 9.311–9.467 lon and
54.776–54.867 lat, which doesn't contain the point at all. The correct cell is
10km_608_53.
Without pyproj the script falls back to a widening box probe that picks the nearest
cell by centroid distance. Workable, but the computed route is exact.
The 20 km grid is not part of the official kvadratnet — DMI notes it only follows the
same naming convention. The 20 km computation here is inferred from the format example
in the docs; verify with inspect before relying on it. The 10 km computation
follows the published standard.
The summary line reports count, date span, and completeness against a gap-free record. Well under 100% means gaps, relocation, or an instrumentation change — check the operating period printed above it.
Values not marked manual in qcStatus haven't been through DMI's climatologist
review; these are flagged [not QC'd] by rank and * by scan. For
extreme-value work use --qc-only. An unreviewed spike is as likely to be a sensor
fault as a real event, and faults cluster at exactly the high end you're ranking.
Precipitation is not corrected for wind and temperature undercatch. Danish gauges typically lose 5–15% for rain, more for snow. Apply a catch correction before comparing against model output or bias-correcting.
Timestamps are UTC. Danish summer time is UTC+2, so a summer afternoon peak reads two
hours earlier than local clock time. The API accepts offset-aware datetimes if you
prefer local; the + must be URL-encoded as %2B.
max_precip_30m uses a different day definition from every other parameter, so its
daily windows don't align with the local-day boundaries used elsewhere.
| Parameter | Unit | Resolutions on grid | Note |
|---|---|---|---|
acc_precip |
mm | hour, day, month/year | Accumulated precipitation |
max_precip_30m |
mm | day, month/year | Max 30-min intensity in 24h — the skybrud metric |
no_days_acc_precip_10 |
days | day, month/year | Days with ≥ 10 mm |
max_temp_w_date |
°C | hour, day, month/year | Maximum temperature |
min_temp |
°C | hour, day, month/year | Minimum temperature |
max_wind_speed_3sec |
m/s | hour, day, month/year | Gust |
mean_temp |
°C | hour, day, month/year | |
drought_index |
— | day, month/year | |
pot_evaporation_makkink |
— | day, month/year |
max_precip_30m is the one to reach for on cloudburst questions: DMI's skybrud
definition is 15 mm in 30 minutes, and this parameter is that quantity directly.
Ranking hourly acc_precip smears the peak across the hour and understates it.
No server-side ranking or value filtering. sortorder sorts by time only. Both
commands therefore pull the series and filter locally. For multi-decade hourly requests
that's a few hundred thousand records — fine, but not instant.
Pagination caps. Max 300,000 objects per request, max 500,000 offset. Time-chunking
stays inside both. If a scan chunk hits the offset ceiling you get an explicit warning
that those results are incomplete, rather than silently short numbers. For very long
periods use DMI's bulk download service.
Manual gauges have no hourly data. Many Danish precipitation stations are read once
daily at 08:00 and carry day only. Hourly precipitation comes from automatic and
Pluvio stations. If the nearest station returns nothing at hour, check its type
before assuming the query is wrong.
Parameter coverage is uneven. Not every parameter exists at every resolution, or on
the grid. An empty result is more often a parameter/resolution mismatch than absent
data — the inspect and stations commands tell you which.
- DMI Klimaatlas — projected indicators (skybrud frequency, extreme return levels)
on a 1×1 km grid, per RCP scenario and time slice, via ArcGIS REST/WFS at
klimaatlas-dmidata.opendata.arcgis.com. Projections, not observations — no time series to extract. - SVK / Spildevandskomiteen regnrækker (Skrift 30/31) — the standard for Danish stormwater dimensioning, with sub-hourly gauge intensities. The quantity DMI's 15 mm/30 min definition is defined against.
- CERRA — 5.5 km regional reanalysis for Europe, 1984–2021. Better on convective extremes than ERA5, still not convection-permitting.
- ERA5 — right tool for long homogeneous records, trend and circulation analysis. Wrong tool for design storms.
DMI's free data is published under CC BY. Credit DMI, link the licence, and state whether you modified the data.
- API docs — https://www.dmi.dk/friedata/dokumentation/apis/climate-data-api-1
- Parameter list — https://www.dmi.dk/friedata/dokumentation/climate-data
- Station list — https://www.dmi.dk/friedata/dokumentation/data/climate-data-stations
- Swagger — https://opendataapi.dmi.dk/v2/climateData/swagger-ui/index.html
- Authentication change — https://www.dmi.dk/friedata/dokumentation/authentication
- Kvadratnet — https://da.wikipedia.org/wiki/Det_danske_Kvadratnet