Using SGU wells#

This notebook demonstrates the use of the python package sgu-client and how to integrate with gwrefpy. The package is installed along with gwrefpy if recommened add-ons are included: pip install "gwrefpy[recommended]"

The Geological Survey of Sweden (SGU) serves an API for their groundwater monitoring network. A Python implementation of an API client has been developed in sgu-client which makes getting observed groundwater levels from SGU into Python a breeze.

Note

The sgu-client package is not affiliated with, supported or endorsed by SGU.

import gwrefpy as gr
from sgu_client import SGUClient
import warnings

warnings.filterwarnings("ignore")
gr.set_log_level("ERROR")
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 2
      1 import gwrefpy as gr
----> 2 from sgu_client import SGUClient
      3 import warnings
      5 warnings.filterwarnings("ignore")

ModuleNotFoundError: No module named 'sgu_client'

Getting observed groundwater levels#

This basic example will show how to fetch observed groundwater levels. For more options, please refer to these docs.

Below, we wrangle the data in two steps:

  1. remove the UTC timezone from the fetched timeseries to achieve compatability with the example model we will load later,

  2. resample to daily medians to align the frequency with that of the example model we will load later

with SGUClient() as client:
    lagga_levels = client.levels.observed.get_measurements_by_name("95_2").to_series()

lagga_levels.index = lagga_levels.index.tz_convert(None) 
lagga_levels = lagga_levels.resample("1D").median()

_ = lagga_levels.plot(figsize=(6, 2))
../_images/db1104870a0d449a9126285c0a804fb9ce66cb8e00efddb82a77e209e7e737cd.png

Inserting as a reference well in a Model#

For this basic demonstration, we will load a tutorial model and add our newly fetched SGU well as a reference well.

model = gr.Model("deviation_example.gwref")
model.wells_summary()
name well_type data_points start_date end_date mean_level latest_value latest_date latitude longitude elevation best_fit_ref_well best_rmse num_fits avg_rmse
0 19W110U observation 2824 2018-01-01 2025-09-24 25.657419 24.949029 2025-09-24 None None None None None NaN NaN
1 19W100U reference 2824 2018-01-01 2025-09-24 25.757644 24.873000 2025-09-24 None None None NaN NaN 0.0 None
well_object = gr.Well("95_2", is_reference=True, timeseries=lagga_levels)
model.add_well(well_object)

model.fit("19W110U", "95_2", offset="0D")  # both are sampled daily
2026-01-08 21:28:04,642 - gwrefpy.methods.linregressfit - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:04,643 - gwrefpy.methods.linregressfit - WARNING - tmax is None, setting to max common time of both wells
Fit Results: 19W110U ~ 95_2
Statistic Value Description
RMSE 0.3025 Root Mean Square Error
R² 0.8736 Coefficient of Determination
R-value 0.9346 Correlation Coefficient
Slope 0.9033 Linear Regression Slope
Intercept 2.3905 Linear Regression Intercept
P-value 0.0000 Statistical Significance
N 2824 Number of Data Points
Std Error 0.3026 Standard Error
Confidence 95.0% Confidence Level

Calibration Period: 2018-01-01 00:00:00 to 2025-09-24 00:00:00
Time Offset: 0D
Aggregation Method: mean

_ = model.plot_fits()
../_images/527164b75e253450ad04d0ad93b70aa535a9f2402335684c9728bbaf0fe03a92.png

Fitting to modeled groundwater levels#

SGU serves an API for accessing modeled groundwater levels. Let’s explore how we can integrate it with gwrefpy.

More examples on fetching modeled groundwater levels can be found here.

Below, we first get the metadata for the monitoring well 4_3 to retrieve its coordinates. We use that to get modeled groundwater levels at the site. Lastly, we get the observed levels which we will need for the fitting.

Note

The example below assumes we are interested in modeled minor resources (i.e. shallow or fast responding groundwater systems). To use this in production, the user should make sure to now the modeling workflow and the associated uncertainties related to SGUs modeled groundwater levels.

with SGUClient() as client:
    station = client.levels.observed.get_station_by_name("4_3")
    lon, lat = station.geometry.coordinates

    modeled = client.levels.modeled.get_levels_by_coords(lat, lon).to_series()
    observed = client.levels.observed.get_measurements_by_name("4_3").to_series()

# ensure daily data for compatability with modeled levels
observed = observed.resample("1D").median()
observed.index = observed.index.tz_convert(None)  # remove tz-info

# force modeled to float dtype
modeled = modeled.astype(float)

Below, we create a model and add the observed levels as a observation well and the modeled levels as a reference well.

model = gr.Model("modeled ref test")

model.add_well([
    gr.Well("obs", False, observed),
    gr.Well("ref", True, modeled)
])
model.wells_summary()
name well_type data_points start_date end_date mean_level latest_value latest_date latitude longitude elevation best_fit_ref_well best_rmse num_fits avg_rmse
0 obs observation 20123 1970-12-05 2026-01-07 150.006587 149.7 2026-01-07 None None None None None NaN NaN
1 ref reference 23745 1961-01-01 2026-01-04 49.698126 39.0 2026-01-04 None None None NaN NaN 0.0 None

We then perform a fit between the two with two polynomial methods with varying degree. I suspect that the observation well has been affected by a storm in 2005, but this should not be reflected in the modeled levels which do not take such an event into account.

from itertools import product

for n, poly in product(
    range(1, 10), ("npolyfit", "chebyshev")
):
    model.fit(
        "obs",
        "ref",
        offset="0D",
        method=poly,
        degree=n,
        tmax="2004"
    )

Hide code cell output

2026-01-08 21:28:07,424 - gwrefpy.methods.npolyfit - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,439 - gwrefpy.methods.chebyshev - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,452 - gwrefpy.methods.npolyfit - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,465 - gwrefpy.methods.chebyshev - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,477 - gwrefpy.methods.npolyfit - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,490 - gwrefpy.methods.chebyshev - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,501 - gwrefpy.methods.npolyfit - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,514 - gwrefpy.methods.chebyshev - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,527 - gwrefpy.methods.npolyfit - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,540 - gwrefpy.methods.chebyshev - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,550 - gwrefpy.methods.npolyfit - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,573 - gwrefpy.methods.chebyshev - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,600 - gwrefpy.methods.npolyfit - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,613 - gwrefpy.methods.chebyshev - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,631 - gwrefpy.methods.npolyfit - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,645 - gwrefpy.methods.chebyshev - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,657 - gwrefpy.methods.npolyfit - WARNING - tmin is None, setting to min common time of both wells
2026-01-08 21:28:07,667 - gwrefpy.methods.chebyshev - WARNING - tmin is None, setting to min common time of both wells

Let’s keep only the best fit and plot.

model.remove_fits_by_n("obs", 1)
_ = model.plot_fitmethod()
../_images/a065e941066c9f69a770ab5d84ac98ae308ae12a1fffdf17ae481ca7283be2b4.png
_ = model.plot_fits(show_initiation_period=True)
../_images/8dd0e9995464ff9acef898c1e25065563603bd48c35d8005bec2ba60f1738d58.png

Looks like the number of outliers greatly increase after 2005?

This concludes this notebook for integrating gwrefpy and sgu-client. Workflows associated to this technique will most likely benefit from reading our tutorial on how to use gwrefpy with live data.

Happy fitting and big thanks to SGU for their excellent monitoring network!