Boreholes

This notebook shows how to plot borehole data along with a groundwater measurement well.

Two different packages are used:

  • brodata, to obtain and visualise borehole data from the BRO database

  • geost to get Bodemkundig booronderzoek from the BRO database

[1]:
import brodata
import contextily as ctx
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from shapely.geometry import LineString, Point

import hydropandas as hpd
[2]:
# reading data from a groundwatermonitoring well (see notebook 01)
gw_bro = hpd.GroundwaterObs.from_bro("GMW000000041261", 1)

Borehole (Brodata)

[3]:
# select extent with a buffer around gw_bro
p = Point(gw_bro.x, gw_bro.y)
extent = np.array(p.buffer(1000).bounds).astype(int)[[0, 2, 1, 3]]

# get bro ids of borehole data
gdf = brodata.bhr.get_characteristics(extent=extent)
[4]:
# plot borehole data
f, axes = plt.subplots(ncols=2, figsize=(16, 8))

gdf["broid"] = gdf.index.str[-7:]
gdf["x"] = gdf.geometry.x
gdf = gdf.sort_values("x")
gdf["geometry"] = gdf.geometry
line = LineString(gdf["deliveredLocation"].values)
distance_along_line = pd.Series(
    [line.project(point) for point in gdf["geometry"]], gdf.index
)

# plot values
xticks = []
xticklabels = []
for bro_id in gdf.index:
    bhrgt = brodata.bhr.GeotechnicalBoreholeResearch.from_bro_id(bro_id)
    for x, bl in enumerate(bhrgt.descriptiveBoreholeLog):
        brodata.plot.bro_lithology_advanced(
            bl["layer"],
            x=distance_along_line[bro_id],
            z=bhrgt.offset,
            width=80,
            bro_id=bro_id,
            ax=axes[0],
        )
        xticks.append(distance_along_line[bro_id])
        xticklabels.append(bro_id[-7:])

axes[0].set_xlim(min(distance_along_line) - 80, max(distance_along_line) + 80)
axes[0].set_xticks(xticks)
axes[0].set_xticklabels(xticklabels, rotation=45, ha="right")
axes[0].set_ylim(3, gdf["offset"].astype(float).max() + 0.5)
axes[0].set_ylabel("z (m t.o.v. NAP)")
# plot map

# plot well
axes[1].scatter(gw_bro.x, gw_bro.y, color="black")
axes[1].annotate(gw_bro.name, (gw_bro.x + 25, gw_bro.y + 10))

# plot boreholes
legend_labels = gdf["broid"].tolist()
gdf.plot("broid", ax=axes[1], legend=True)

# plot line
gpd.GeoDataFrame(geometry=[line]).plot(ax=axes[1])

# settings
axes[1].set_xlim(extent[:2])
axes[1].set_ylim(extent[2:])

# basemap
ctx.add_basemap(axes[1], crs=28992)
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag determinationProcedure  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag determinationMethod  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag fractionDistribution  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag dispersionMethod  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag removedMaterial  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag equivalentMassDeterminationMethod  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag equivalentMass  not supported in GeotechnicalBoreholeResearch BHR000000348833
Tag basicParticleSizeDistribution  not supported in GeotechnicalBoreholeResearch BHR000000348833
SoilName sterkZandigSilt not supported (found at broId BHR000000348833). Please add sterkZandigSilt to lithology_properties.
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348807
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348807
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348866
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348866
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348629
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348629
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348741
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348741
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348867
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348867
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348683
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348683
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348675
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348675
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348869
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348869
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348801
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348801
Tag meanHighestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348684
Tag meanLowestGroundwaterLevel  not supported in GeotechnicalBoreholeResearch BHR000000348684
../_images/examples_10_borehole_5_1.png

Bodemkundig booronderzoek (geost)

The geost package can only be pip installed for python version 3.12 or higher. Install it using:

pip install geost

[5]:
!pip install geost
Collecting geost
  Downloading geost-0.5.1-py3-none-any.whl.metadata (14 kB)
Requirement already satisfied: geopandas in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geost) (1.1.4)
Requirement already satisfied: lxml in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geost) (6.1.3)
Requirement already satisfied: numpy in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geost) (2.5.3)
Requirement already satisfied: openpyxl in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geost) (3.1.5)
Requirement already satisfied: pandas in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geost) (3.0.5)
Requirement already satisfied: platformdirs in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geost) (4.11.8)
Requirement already satisfied: pooch in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geost) (1.9.0)
Requirement already satisfied: pyarrow in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geost) (25.0.1)
Collecting pydap (from geost)
  Downloading pydap-3.5.11-py3-none-any.whl.metadata (9.3 kB)
Requirement already satisfied: pyproj in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geost) (3.8.0)
Collecting pyvista (from geost)
  Downloading pyvista-0.49.0-py3-none-any.whl.metadata (14 kB)
Collecting rioxarray (from geost)
  Downloading rioxarray-0.23.0-py3-none-any.whl.metadata (5.3 kB)
Requirement already satisfied: shapely in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geost) (2.1.2)
Collecting xlrd (from geost)
  Downloading xlrd-2.0.2-py2.py3-none-any.whl.metadata (3.5 kB)
Requirement already satisfied: pyogrio>=0.7.2 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geopandas->geost) (0.13.0)
Requirement already satisfied: packaging in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from geopandas->geost) (26.3)
Requirement already satisfied: python-dateutil>=2.8.2 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from pandas->geost) (2.9.0.post0)
Requirement already satisfied: certifi in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from pyogrio>=0.7.2->geopandas->geost) (2026.7.22)
Requirement already satisfied: six>=1.5 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from python-dateutil>=2.8.2->pandas->geost) (1.17.0)
Requirement already satisfied: et-xmlfile in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from openpyxl->geost) (2.0.0)
Requirement already satisfied: requests>=2.19.0 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from pooch->geost) (2.34.2)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from requests>=2.19.0->pooch->geost) (3.5.1)
Requirement already satisfied: idna<4,>=2.5 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from requests>=2.19.0->pooch->geost) (3.19)
Requirement already satisfied: urllib3<3,>=1.26 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from requests>=2.19.0->pooch->geost) (2.7.0)
Collecting requests-cache (from pydap->geost)
  Downloading requests_cache-1.3.3-py3-none-any.whl.metadata (9.4 kB)
Requirement already satisfied: scipy in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from pydap->geost) (1.18.1)
Collecting webob>=1.8.10 (from pydap->geost)
  Downloading webob-1.8.11-py2.py3-none-any.whl.metadata (13 kB)
Requirement already satisfied: beautifulsoup4 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from pydap->geost) (4.15.0)
Requirement already satisfied: Jinja2 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from pydap->geost) (3.1.6)
Requirement already satisfied: soupsieve>=1.6.1 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from beautifulsoup4->pydap->geost) (2.9.2)
Requirement already satisfied: typing-extensions>=4.0.0 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from beautifulsoup4->pydap->geost) (4.16.0)
Requirement already satisfied: MarkupSafe>=2.0 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from Jinja2->pydap->geost) (3.0.3)
Collecting cyclopts>=4.0.0 (from pyvista->geost)
  Downloading cyclopts-4.25.2-py3-none-any.whl.metadata (12 kB)
Requirement already satisfied: matplotlib>=3.0.1 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from pyvista->geost) (3.11.2)
Requirement already satisfied: pillow in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from pyvista->geost) (12.3.0)
Collecting pyvista-validation==0.2.2 (from pyvista->geost)
  Downloading pyvista_validation-0.2.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (8.7 kB)
Collecting scooby>=0.5.1 (from pyvista->geost)
  Downloading scooby-0.12.0-py3-none-any.whl.metadata (16 kB)
Collecting vtk!=9.4.0 (from pyvista->geost)
  Downloading vtk-9.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (5.6 kB)
Requirement already satisfied: attrs>=23.1.0 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from cyclopts>=4.0.0->pyvista->geost) (26.1.0)
Collecting docstring-parser<4.0,>=0.15 (from cyclopts>=4.0.0->pyvista->geost)
  Downloading docstring_parser-0.18.0-py3-none-any.whl.metadata (3.5 kB)
Collecting rich-rst<3.0.0,>=1.3.1 (from cyclopts>=4.0.0->pyvista->geost)
  Downloading rich_rst-2.1.0-py3-none-any.whl.metadata (4.7 kB)
Requirement already satisfied: rich>=13.6.0 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from cyclopts>=4.0.0->pyvista->geost) (15.0.0)
Requirement already satisfied: pygments>=2.0.0 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from rich-rst<3.0.0,>=1.3.1->cyclopts>=4.0.0->pyvista->geost) (2.21.0)
Requirement already satisfied: contourpy>=1.0.1 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from matplotlib>=3.0.1->pyvista->geost) (1.4.0)
Requirement already satisfied: cycler>=0.10 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from matplotlib>=3.0.1->pyvista->geost) (0.12.1)
Requirement already satisfied: fonttools>=4.28.2 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from matplotlib>=3.0.1->pyvista->geost) (4.65.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from matplotlib>=3.0.1->pyvista->geost) (1.5.1)
Requirement already satisfied: pyparsing>=3 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from matplotlib>=3.0.1->pyvista->geost) (3.3.2)
Requirement already satisfied: markdown-it-py>=2.2.0 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from rich>=13.6.0->cyclopts>=4.0.0->pyvista->geost) (4.2.0)
Requirement already satisfied: mdurl~=0.1 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from markdown-it-py>=2.2.0->rich>=13.6.0->cyclopts>=4.0.0->pyvista->geost) (0.1.2)
Collecting cattrs>=22.2 (from requests-cache->pydap->geost)
  Downloading cattrs-26.2.0-py3-none-any.whl.metadata (8.5 kB)
Collecting url-normalize>=2.0 (from requests-cache->pydap->geost)
  Downloading url_normalize-3.0.0-py3-none-any.whl.metadata (7.5 kB)
Requirement already satisfied: rasterio>=1.4.3 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from rioxarray->geost) (1.5.1)
Requirement already satisfied: xarray>=2026.2 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from rioxarray->geost) (2026.7.0)
Requirement already satisfied: affine in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from rasterio>=1.4.3->rioxarray->geost) (3.0.1)
Requirement already satisfied: click!=8.2.*,>=4.0 in /home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages (from rasterio>=1.4.3->rioxarray->geost) (8.5.0)
Downloading geost-0.5.1-py3-none-any.whl (110 kB)
Downloading pydap-3.5.11-py3-none-any.whl (2.5 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.5/2.5 MB 84.8 MB/s  0:00:00
Downloading webob-1.8.11-py2.py3-none-any.whl (118 kB)
Downloading pyvista-0.49.0-py3-none-any.whl (2.7 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.7/2.7 MB 191.8 MB/s  0:00:00
Downloading pyvista_validation-0.2.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (261 kB)
Downloading vtk-9.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (139.7 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 139.7/139.7 MB 240.6 MB/s  0:00:00
Downloading cyclopts-4.25.2-py3-none-any.whl (242 kB)
Downloading docstring_parser-0.18.0-py3-none-any.whl (22 kB)
Downloading rich_rst-2.1.0-py3-none-any.whl (272 kB)
Downloading scooby-0.12.0-py3-none-any.whl (21 kB)
Downloading requests_cache-1.3.3-py3-none-any.whl (70 kB)
Downloading cattrs-26.2.0-py3-none-any.whl (74 kB)
Downloading url_normalize-3.0.0-py3-none-any.whl (16 kB)
Downloading rioxarray-0.23.0-py3-none-any.whl (72 kB)
Downloading xlrd-2.0.2-py2.py3-none-any.whl (96 kB)
Installing collected packages: xlrd, webob, url-normalize, scooby, pyvista-validation, docstring-parser, cattrs, requests-cache, vtk, rich-rst, pydap, rioxarray, cyclopts, pyvista, geost
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 15/15 [geost]
Successfully installed cattrs-26.2.0 cyclopts-4.25.2 docstring-parser-0.18.0 geost-0.5.1 pydap-3.5.11 pyvista-0.49.0 pyvista-validation-0.2.2 requests-cache-1.3.3 rich-rst-2.1.0 rioxarray-0.23.0 scooby-0.12.0 url-normalize-3.0.0 vtk-9.7.0 webob-1.8.11 xlrd-2.0.2
[6]:
import geost
/home/docs/checkouts/readthedocs.org/user_builds/hydropandas/envs/stable/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
[7]:
# get Bodemkundig booronderzoek (BHR-GT)
boreholes = geost.bro_api_read("BHR-GT", bbox=[int(i) for i in p.buffer(1000).bounds])
[8]:
# add colors based on column 'standard_name'
# specify colors
colors = brodata.plot.lithology_colors.copy()
colors_rgb = {}
for soil, color in colors.items():
    colors_rgb[soil] = tuple((np.array(color) * 255).astype(int))

colors_rgb["sterkZandigSilt"] = (243, 225, 100)
colors_rgb["siltigZandMetGrind"] = (220, 200, 80)
colors_rgb["siltigZand"] = (243, 225, 150)
colors_rgb["zwakGrindigZand"] = (231, 210, 70)

col = "geotechnicalSoilName"  # "standardSoilName"
boreholes.data["color"] = boreholes.data[col].map(colors_rgb)

# plot borehole data
f, axes = plt.subplots(figsize=(16, 8), ncols=2)
labels = []
for i, (xy, df) in enumerate(boreholes.data.groupby("x")):
    df["top"] = df["surface"] - df["top"]
    df["bottom"] = df["surface"] - df["bottom"]
    for _, row in df.iterrows():
        color = [c / 255 for c in row["color"]] + [1.0]
        if row[col] in labels:
            axes[0].fill_between(
                [i - 0.25, i + 0.25],
                [row["top"], row["top"]],
                [row["bottom"], row["bottom"]],
                color=color,
            )
        else:
            axes[0].fill_between(
                [i - 0.25, i + 0.25],
                [row["top"], row["top"]],
                [row["bottom"], row["bottom"]],
                color=color,
                label=row[col],
            )
            labels.append(row[col])
axes[0].set_xticks(np.arange(0, i + 1), boreholes.data.sort_values("x")["nr"].unique())
# Rotate and align the xtick labels
for label in axes[0].get_xticklabels():
    label.set_rotation(45)
    label.set_ha("right")

axes[0].set_ylabel("m NAP")  # assuming that the level is in m NAP
axes[0].set_xlim(-0.5, i + 1.5)
axes[0].legend()
axes[0].set_title(f"Bodemkundig booronderzoek close to {gw_bro.name}")
# plot map

# plot well
axes[1].scatter(gw_bro.x, gw_bro.y, label=gw_bro.name, color="black")

# plot boreholes
for i, dfnr in boreholes.data.groupby(["x", "y"]):
    axes[1].scatter(*i, label=dfnr["nr"].unique()[0])

# settings
axes[1].set_xlim(extent[:2])
axes[1].set_ylim(extent[2:])
axes[1].legend()

# basemap
ctx.add_basemap(axes[1], crs=28992)
../_images/examples_10_borehole_10_0.png