Reading GGMN observations
This notebook introduces how to use the hydropandas package to read, process and visualise groundwater level data from the Global Groundwater Monitoring Network (GGMN). GGMN is maintained by UN-IGRAC and aggregates in-situ groundwater level measurements from monitoring networks around the world. Data are retrieved through the public GGMN WFS and web interface.
[1]:
import contextily as ctx
import matplotlib.pyplot as plt
import hydropandas as hpd
from hydropandas.io import ggmn
# enabling logging so we can see what happens in the background
hpd.util.get_color_logger("INFO");
Read GGMN observations within an extent
Use hpd.read_ggmn to download groundwater level observations for all GGMN monitoring locations within a bounding box. The extent is [xmin, xmax, ymin, ymax] in the coordinate system given by crs. Use only_metadata=True for a fast first look without measurements.
[2]:
# read GGMN station metadata only (fast)
# extent: [xmin, xmax, ymin, ymax] in WGS84
extent = [5.1, 5.13, 52.0, 52.05]
oc_meta = hpd.read_ggmn(
extent=extent,
crs=4326,
only_metadata=True,
max_locations=50,
)
print(f"Found {len(oc_meta)} GGMN locations in the extent")
oc_meta
ggmn location: 100%|██████████| 9/9 [00:00<00:00, 4047.25it/s]
Found 9 GGMN locations in the extent
[2]:
| screen_bottom | tube_nr | y | screen_top | x | location | tube_top | unit | filename | source | ground_level | obs | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| name | ||||||||||||
| 700576 | NaN | 52.047123 | NaN | 5.120948 | NaN | m | GGMN | NaN | GroundwaterObs 700576 -----metadata------ scre... | |||
| 700513 | NaN | 52.031113 | NaN | 5.123593 | NaN | m | GGMN | NaN | GroundwaterObs 700513 -----metadata------ scre... | |||
| 700497 | NaN | 52.031113 | NaN | 5.123593 | NaN | m | GGMN | NaN | GroundwaterObs 700497 -----metadata------ scre... | |||
| 700291 | NaN | 52.047123 | NaN | 5.120948 | NaN | m | GGMN | NaN | GroundwaterObs 700291 -----metadata------ scre... | |||
| 696021 | NaN | 52.047123 | NaN | 5.120948 | NaN | m | GGMN | NaN | GroundwaterObs 696021 -----metadata------ scre... | |||
| 538948 | NaN | 52.031020 | NaN | 5.122428 | NaN | m | GGMN | NaN | GroundwaterObs 538948 -----metadata------ scre... | |||
| 525408 | NaN | 52.031020 | NaN | 5.122428 | NaN | m | GGMN | NaN | GroundwaterObs 525408 -----metadata------ scre... | |||
| 414858 | NaN | 52.009011 | NaN | 5.101124 | NaN | m | GGMN | NaN | GroundwaterObs 414858 -----metadata------ scre... | |||
| 414850 | NaN | 52.040078 | NaN | 5.102242 | NaN | m | GGMN | NaN | GroundwaterObs 414850 -----metadata------ scre... |
[3]:
# plot monitoring locations on a map
ax = oc_meta.to_gdf().plot(figsize=(8, 6), color="steelblue", markersize=60, zorder=2)
ctx.add_basemap(ax=ax, crs=4326, attribution=False)
for idx, row in oc_meta.iterrows():
ax.annotate(
text=idx,
xy=(row["x"], row["y"]),
fontsize=7,
ha="center",
va="bottom",
)
ax.set_title("GGMN monitoring locations")
plt.tight_layout()
[4]:
# download groundwater level measurements for the same extent
oc = hpd.read_ggmn(
extent=extent,
crs=4326,
tmin="2010-01-01",
tmax="2026-12-31",
keep_all_obs=True,
max_locations=10,
max_pages=10,
timeout=120,
)
oc
ggmn location: 100%|██████████| 9/9 [01:08<00:00, 7.56s/it]
[4]:
| screen_bottom | tube_nr | y | screen_top | x | location | tube_top | unit | filename | source | ground_level | obs | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| name | ||||||||||||
| 700576 | NaN | 52.047123 | NaN | 5.120948 | NaN | m | GGMN | NaN | GroundwaterObs 700576 -----metadata------ scre... | |||
| 700513 | NaN | 52.031113 | NaN | 5.123593 | NaN | m | GGMN | NaN | GroundwaterObs 700513 -----metadata------ scre... | |||
| 700497 | NaN | 52.031113 | NaN | 5.123593 | NaN | m | GGMN | NaN | GroundwaterObs 700497 -----metadata------ scre... | |||
| 700291 | NaN | 52.047123 | NaN | 5.120948 | NaN | m | GGMN | NaN | GroundwaterObs 700291 -----metadata------ scre... | |||
| 696021 | NaN | 52.047123 | NaN | 5.120948 | NaN | m | GGMN | NaN | GroundwaterObs 696021 -----metadata------ scre... | |||
| 538948 | NaN | 52.031020 | NaN | 5.122428 | NaN | m | GGMN | NaN | GroundwaterObs 538948 -----metadata------ scre... | |||
| 525408 | NaN | 52.031020 | NaN | 5.122428 | NaN | m | GGMN | NaN | GroundwaterObs 525408 -----metadata------ scre... | |||
| 414858 | NaN | 52.009011 | NaN | 5.101124 | NaN | m | GGMN | NaN | GroundwaterObs 414858 -----metadata------ scre... | |||
| 414850 | NaN | 52.040078 | NaN | 5.102242 | NaN | m | GGMN | NaN | GroundwaterObs 414850 -----metadata------ scre... |
Plot observations from a single monitoring well
[5]:
# select the first well that actually has measurements and plot the time series
o = None
for _, row in oc.iterrows():
if not row.obs.empty:
o = row.obs
break
if o is not None:
print(f"Well: {o.name} | unit: {o.unit}")
o["groundwater_level"].plot(
figsize=(12, 4),
marker=".",
linewidth=0.8,
ylabel=f"Groundwater level ({o.unit})",
title=f"GGMN groundwater level – {o.name}",
)
plt.tight_layout()
else:
print("No measurements found in the downloaded collection.")
Well: 700576 | unit: m
Retrieve measurements for a known record
If you know the GGMN record ID you can call ggmn.get_level_measurements directly to get a tidy DataFrame without creating a full ObsCollection.
[6]:
# fetch groundwater level data for a specific GGMN record
record_id = 695507 # known record in the Netherlands
df, unit = ggmn.get_level_measurements(
record_id=record_id,
parameter="Water level elevation a.m.s.l.",
max_pages=10,
timeout=120,
)
print(f"Retrieved {len(df)} measurements | unit: {unit}")
df.head(10)
Retrieved 100 measurements | unit: m
[6]:
| groundwater_level | |
|---|---|
| time | |
| 2025-01-07 | -6.405833 |
| 2025-01-08 | -6.447958 |
| 2025-01-09 | -6.476792 |
| 2025-01-10 | -6.615000 |
| 2025-01-11 | -6.716542 |
| 2025-01-12 | -6.834042 |
| 2025-01-13 | -6.849125 |
| 2025-01-14 | -6.780208 |
| 2025-01-15 | -6.779625 |
| 2025-01-16 | -6.808250 |
[7]:
# plot the retrieved time series
if not df.empty:
df["groundwater_level"].plot(
figsize=(12, 4),
marker=".",
linewidth=0.8,
ylabel=f"Groundwater level ({unit})",
title=f"GGMN record {record_id}",
)
plt.tight_layout()
Filter by parameter name
Some GGMN monitoring locations report multiple parameters (e.g. water level above sea level and depth below surface). Use the parameter argument to filter for a specific parameter string.
[8]:
# download only 'depth below surface' measurements
oc_depth = hpd.read_ggmn(
extent=extent,
crs=4326,
tmin="2010-01-01",
tmax="2026-12-31",
parameter="depth",
keep_all_obs=True,
max_locations=10,
max_pages=10,
timeout=120,
)
print(f"Observations with 'depth' parameter: {len(oc_depth)}")
oc_depth
ggmn location: 100%|██████████| 9/9 [01:06<00:00, 7.41s/it]
Observations with 'depth' parameter: 9
[8]:
| screen_bottom | tube_nr | y | screen_top | x | location | tube_top | unit | filename | source | ground_level | obs | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| name | ||||||||||||
| 700576 | NaN | 52.047123 | NaN | 5.120948 | NaN | m | GGMN | NaN | GroundwaterObs 700576 -----metadata------ scre... | |||
| 700513 | NaN | 52.031113 | NaN | 5.123593 | NaN | m | GGMN | NaN | GroundwaterObs 700513 -----metadata------ scre... | |||
| 700497 | NaN | 52.031113 | NaN | 5.123593 | NaN | m | GGMN | NaN | GroundwaterObs 700497 -----metadata------ scre... | |||
| 700291 | NaN | 52.047123 | NaN | 5.120948 | NaN | m | GGMN | NaN | GroundwaterObs 700291 -----metadata------ scre... | |||
| 696021 | NaN | 52.047123 | NaN | 5.120948 | NaN | m | GGMN | NaN | GroundwaterObs 696021 -----metadata------ scre... | |||
| 538948 | NaN | 52.031020 | NaN | 5.122428 | NaN | m | GGMN | NaN | GroundwaterObs 538948 -----metadata------ scre... | |||
| 525408 | NaN | 52.031020 | NaN | 5.122428 | NaN | m | GGMN | NaN | GroundwaterObs 525408 -----metadata------ scre... | |||
| 414858 | NaN | 52.009011 | NaN | 5.101124 | NaN | m | GGMN | NaN | GroundwaterObs 414858 -----metadata------ scre... | |||
| 414850 | NaN | 52.040078 | NaN | 5.102242 | NaN | m | GGMN | NaN | GroundwaterObs 414850 -----metadata------ scre... |
[9]:
# interactive map of downloaded monitoring locations
oc[["lat", "lon"]] = oc[["y", "x"]]
oc.plots.interactive_map(popup_width=300)
[9]: