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()
../_images/examples_13_ggmn_4_0.png
[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
../_images/examples_13_ggmn_7_1.png

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()
../_images/examples_13_ggmn_10_0.png

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]:
Make this Notebook Trusted to load map: File -> Trust Notebook