Repository: Ouranosinc/xclim
Pull request #1877: Standardized Streamflow Index and Standardized Groundwater level Index
Author: login: LamAdr
Labels: name: CI
description: Automation and Contiunous Integration
name: docs
description: Improvements to documenation
name: approved
description: Approved for additional tests
name: indicators
description: Climate indices and indicators
Created: 2024-08-08T19:30:40Z | Merged: 2025-03-13T18:14:56Z

<!--Please ensure the PR fulfills the following requirements! -->
<!-- If this is your first PR, make sure to add your details to the AUTHORS.rst! -->
### Pull Request Checklist:
- [x] This PR addresses an already opened issue (for bug fixes / features)
    - This PR fixes #1444
- [x] Tests for the changes have been added (for bug fixes / features)
  - [x] (If applicable) Documentation has been added / updated (for bug fixes / features)
- [x] CHANGELOG.rst has been updated (with summary of main changes)
  - [x] Link to issue (:issue:`number`) and pull request (:pull:`number`) has been added

### What kind of change does this PR introduce?

* 2 new indices/indicators : SSI and SGI

### Does this PR introduce a breaking change?
No

### Other information:
* SSI will use GEV or log-logistic (scipy does not support tweedie)
* SGI will use log-normal, gamma or GEV

Linked issue #1444: New standardized indices
### Addressing a Problem?

New standardized indices will be needed for a project in the future.

*  streamflow index (SSI)     GEV, log-logistic (Vincente-Serrano et al., 2012) Tweedie (Barker et al., 2016)
* groundwater index (SGI)   log-normal, gamma, GEV (Bloomfield et Marchant, 2013)	                         
* This includes adding new distributions: `GEV, log-normal`
https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.genextreme.html#scipy.stats.genextreme
https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.lognorm.html

### Potential Solution

_No response_

### Additional context

Moved from comments previously within the code base. See #1311 

### Contribution

- [X] I would be willing/able to open a Pull Request to contribute this feature.

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

Diff:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c8dacd0ba..1f7f15243 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -19,7 +19,7 @@ on:
       - submitted
 
 env:
-  XCLIM_TESTDATA_BRANCH: v2025.1.8
+  XCLIM_TESTDATA_BRANCH: v2025.3.11
 
 concurrency:
   # For a given workflow, if we push to the same branch, cancel all previous builds on that branch except on main.
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index eb5988ee8..09e9604bc 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -4,7 +4,7 @@ Changelog
 
 v0.56.0 (unreleased)
 --------------------
-Contributors to this version: Trevor James Smith (:user:`Zeitsperre`), Hui-Min Wang (:user:`Hem-W`), Jack Kit-tai Wong(:user:`jack-ktw`), Éric Dupuis (:user:`coxipi`).
+Contributors to this version: Trevor James Smith (:user:`Zeitsperre`), Hui-Min Wang (:user:`Hem-W`), Jack Kit-tai Wong(:user:`jack-ktw`), Adrien Lamarche (:user:`LamAdr`), Éric Dupuis (:user:`coxipi`).
 
 Bug fixes
 ^^^^^^^^^
@@ -28,6 +28,11 @@ Internal changes
 * The `xclim` documentation now has a ``support`` page for detailing the project's usage and version support policies. (:pull:`2100`).
 * The indicator `heat_wave_index` now uses `hot_spell_total_length` index. The `heat_wave_index` index is identitical to `hot_spell_total_length` and will be dropped in future versions. (:issue:`2031`, :pull:`2102`).
 
+New indicators
+^^^^^^^^^^^^^^
+* Added standardized indicators for hydrology: ``xclim.land.standardized_streamflow_index`` and ``xclim.landstandardized_groundwater_index``.  (:issue:`1444`, :pull:`1877`).
+
+
 v0.55.1 (2025-02-26)
 --------------------
 Contributors to this version: Éric Dupuis (:user:`coxipi`).
diff --git a/docs/references.bib b/docs/references.bib
index ef78be3a7..e7dd2e3cf 100644
--- a/docs/references.bib
+++ b/docs/references.bib
@@ -2236,6 +2236,32 @@ @article{knol_1989
 	number = "1",
 }
 
+@article{vicente-serrano_2012,
+	author = {Sergio M. Vicente-Serrano  and Juan I. López-Moreno  and Santiago Beguería  and Jorge Lorenzo-Lacruz  and Cesar Azorin-Molina  and Enrique Morán-Tejeda},
+	title = {Accurate Computation of a Streamflow Drought Index},
+	journal = {Journal of Hydrologic Engineering},
+	volume = {17},
+	number = {2},
+	pages = {318-332},
+	year = {2012},
+	doi = {10.1061/(ASCE)HE.1943-5584.0000433},
+	URL = {https://ascelibrary.org/doi/abs/10.1061/%28ASCE%29HE.1943-5584.0000433},
+	eprint = {https://ascelibrary.org/doi/pdf/10.1061/%28ASCE%29HE.1943-5584.0000433},
+    abstract = "In this study, the authors investigated an approach to calculate the standardized streamflow index (SSI), which allows accurate spatial and temporal comparison of the hydrological conditions of a stream or set of streams. For this purpose, the capability of six three-parameter distributions (lognormal, Pearson Type III, log-logistic, general extreme value, generalized Pareto, and Weibull) and two different approaches to select the most suitable distribution the best monthly fit (BMF) and the minimum orthogonal distance (MD), were tested by using a monthly streamflow data set for the Ebro Basin (Spain). This large Mediterranean basin is characterized by high variability in the magnitude of streamflows and in seasonal regimes. The results show that the most commonly used probability distributions for flow frequency analysis provided good fits to the streamflow series. Thus, the visual inspection of the L-moment diagrams and the results of the Kolmogorov-Smirnov test did not enable the selection of a single optimum probability distribution. However, no single probability distribution for all the series was suitable for obtaining a robust standardized streamflow series because each of the distributions had one or more limitations. The BMF and MD approaches improved the results in the expected average, standard deviation, and the frequencies of extreme events of the SSI series in relation to the selection of a unique distribution for each station. The BMF and MD approaches involved using different probability distributions for each gauging station and month of the year to calculate the SSI. Both approaches are easy to apply and they provide very similar results in the quality of the obtained hydrological drought indexes. The proposed procedures are very flexible for analyses involving contrasting hydrological regimes and flow characteristics."
+}
+
+@article{bloomfield_2013,
+	AUTHOR = {Bloomfield, J. P. and Marchant, B. P.},
+	TITLE = {Analysis of groundwater drought building on the standardised precipitation index approach},
+	JOURNAL = {Hydrology and Earth System Sciences},
+	VOLUME = {17},
+	YEAR = {2013},
+	NUMBER = {12},
+	PAGES = {4769--4787},
+	URL = {https://hess.copernicus.org/articles/17/4769/2013/},
+	DOI = {10.5194/hess-17-4769-2013}
+}
+
 @article{luedeling_chill_2009,
 	title = {Climate change impacts on winter chill for temperate fruit and nut production: A review},
 	journal = {Scientia Horticulturae},
diff --git a/src/xclim/core/indicator.py b/src/xclim/core/indicator.py
index 8e0568232..88cef9654 100644
--- a/src/xclim/core/indicator.py
+++ b/src/xclim/core/indicator.py
@@ -1939,3 +1939,10 @@ def _merge_attrs(dbase, dextra, attr, sep):
             load_locale(loc_dict, locale)
 
     return mod
+
+
+class StandardizedIndexes(ResamplingIndicator):
+    """Resampling but flexible inputs indicators."""
+
+    src_freq = ["D", "MS"]
+    context = "hydro"
diff --git a/src/xclim/data/fr.json b/src/xclim/data/fr.json
index 8efa0c85b..d2fdc32e6 100644
--- a/src/xclim/data/fr.json
+++ b/src/xclim/data/fr.json
@@ -269,6 +269,12 @@
     "title": "Précipitation liquide accumulée totale",
     "abstract": "Précipitation liquide accumulée totale. Les précipitations sont considérées liquides lorsque la température quotidienne moyenne est au-dessus de 0°C."
   },
+  "SGI": {
+    "long_name": "Indice d'eau souterraine standardisé (SGI)",
+    "description": "Niveau de l'eau souterraine sur une fenêtre mobile de {window} {freq:nom}, normalisé telle que la moyenne du SGI est 0 pour les données de calibration. L'unité de la fenêtre 'X' est l'unité de temps déterminée par la fréquence de rééchantillonage,",
+    "title": "Indice d'eau souterraine standardisé (SGI)",
+    "abstract": "Niveau de l'eau souterraine sur une fenêtre mobile, normalisée telle que la moyenne du SGI est 0 pour les données de calibration. L'unité de la fenêtre est l'unité de temps déterminée par la fréquence de rééchantillonnage."
+  },
   "SOLIDPRCPTOT": {
     "long_name": "Précipitation totale lorsque la température moyenne est en dessous ou égale à {thresh}",
     "description": "Précipitation solide totale {freq:f}, estimée comme la précipitation lorsque la température moyenne est en dessous ou égale à {thresh}.",
@@ -287,6 +293,12 @@
     "title": "Indice de précipitation évapotranspiration standardisé (SPEI)",
     "abstract": "Budget d'eau (précipitation - évapotranspiration) sur une fenêtre mobile, normalisé tel que la moyenne du SPEI est 0 pour les données de calibration. L'unité de la fenêtre est l'unité de temps déterminée par la fréquence de rééchantillonnage."
   },
+  "SSI": {
+    "long_name": "Indice d'écoulement standardisé (SSI)",
+    "description": "Écoulement sur une fenêtre mobile de {window} {freq:nom}, normalisée telle que la moyenne du SSI est 0 pour les données de calibration. L'unité de la fenêtre 'X' est l'unité de temps déterminée par la fréquence de rééchantillonage,",
+    "title": "Indice d'écoulement standardisé (SSI)",
+    "abstract": "Écoulement sur une fenêtre mobile, normalisée telle que la moyenne du SSI est 0 pour les données de calibration. L'unité de la fenêtre est l'unité de temps déterminée par la fréquence de rééchantillonnage."
+  },
   "DC": {
     "long_name": "Indice de sécheresse",
     "description": "Code numérique estimant la teneur en humidité moyenne des couches organiques.",
diff --git a/src/xclim/data/variables.yml b/src/xclim/data/variables.yml
index 71a7b7dd0..e4bc0d7e8 100644
--- a/src/xclim/data/variables.yml
+++ b/src/xclim/data/variables.yml
@@ -30,6 +30,10 @@ variables:
     standard_name: water_potential_evapotranspiration_flux
     data_flags:
       - negative_accumulation_values:
+  gwl:
+    canonical_units: m
+    description: Groundwater level.
+    dimensions: "[length]"
   hurs:
     canonical_units: '%'
     cell_methods: "time: mean"
diff --git a/src/xclim/indicators/atmos/_precip.py b/src/xclim/indicators/atmos/_precip.py
index cd83ad218..8fdfc7077 100644
--- a/src/xclim/indicators/atmos/_precip.py
+++ b/src/xclim/indicators/atmos/_precip.py
@@ -10,8 +10,8 @@
     Daily,
     Hourly,
     Indicator,
-    ResamplingIndicator,
     ResamplingIndicatorWithIndexing,
+    StandardizedIndexes,
 )
 from xclim.core.utils import InputKind
 
@@ -115,13 +115,6 @@ def cfcheck(pr: DataArray, tas: DataArray):
         cfchecks.check_valid(tas, "standard_name", "air_temperature")
 
 
-class StandardizedIndexes(ResamplingIndicator):
-    """Resampling but flexible inputs indicators."""
-
-    src_freq = ["D", "MS"]
-    context = "hydro"
-
-
 class HrPrecip(Hourly):
     """Indicator involving hourly pr series."""
 
@@ -372,6 +365,7 @@ class HrPrecip(Hourly):
 )
 
 standardized_precipitation_index = StandardizedIndexes(
+    realm="atmos",
     title="Standardized Precipitation Index (SPI)",
     identifier="spi",
     units="",
@@ -387,6 +381,7 @@ class HrPrecip(Hourly):
 )
 
 standardized_precipitation_evapotranspiration_index = StandardizedIndexes(
+    realm="atmos",
     title="Standardized Precipitation Evapotranspiration Index (SPEI)",
     identifier="spei",
     units="",
diff --git a/src/xclim/indicators/land/_streamflow.py b/src/xclim/indicators/land/_streamflow.py
index 1dc5b0786..08b1b1696 100644
--- a/src/xclim/indicators/land/_streamflow.py
+++ b/src/xclim/indicators/land/_streamflow.py
@@ -8,6 +8,7 @@
 from xclim.core.indicator import (
     ReducingIndicator,
     ResamplingIndicator,
+    StandardizedIndexes,
 )
 from xclim.core.units import declare_units
 from xclim.indices import (
@@ -17,6 +18,8 @@
     high_flow_frequency,
     low_flow_frequency,
     rb_flashiness_index,
+    standardized_groundwater_index,
+    standardized_streamflow_index,
 )
 
 __all__ = [
@@ -27,6 +30,8 @@
     "high_flow_frequency",
     "low_flow_frequency",
     "rb_flashiness_index",
+    "standardized_groundwater_index",
+    "standardized_streamflow_index",
 ]
 
 
@@ -97,6 +102,7 @@ def cfcheck(q: DataArray):
     parameters={"op": generic.doymin, "out_units": None},
 )
 
+
 flow_index = ReducingIndicator(
     realm="land",
     context="hydro",
@@ -130,3 +136,36 @@ def cfcheck(q: DataArray):
     units="days",
     compute=low_flow_frequency,
 )
+
+standardized_streamflow_index = StandardizedIndexes(
+    realm="land",
+    title="Standardized Streamflow Index (SSI)",
+    identifier="ssi",
+    units="",
+    standard_name="ssi",
+    long_name="Standardized Streamflow Index (SSI)",
+    description="Streamflow over a moving {window}-X window, normalized such that SSI averages to 0 for "
+    "calibration data. The window unit `X` is the minimal time period defined by resampling frequency {freq}.",
+    abstract="Streamflow over a moving window, normalized such that SSI averages to 0 for the calibration data. "
+    "The window unit `X` is the minimal time period defined by the resampling frequency.",
+    cell_methods="",
+    keywords="streamflow",
+    compute=standardized_streamflow_index,
+)
+
+
+standardized_groundwater_index = StandardizedIndexes(
+    realm="land",
+    title="Standardized Groundwater Index (SGI)",
+    identifier="sgi",
+    units="",
+    standard_name="sgi",
+    long_name="Standardized Groundwater Index (SGI)",
+    description="Groundwater over a moving {window}-X window, normalized such that SGI averages to 0 for "
+    "calibration data. The window unit `X` is the minimal time period defined by resampling frequency {freq}.",
+    abstract="Groundwater over a moving window, normalized such that SGI averages to 0 for the calibration data. "
+    "The window unit `X` is the minimal time period defined by the resampling frequency.",
+    cell_methods="",
+    keywords="groundwater",
+    compute=standardized_groundwater_index,
+)
diff --git a/src/xclim/indices/_agro.py b/src/xclim/indices/_agro.py
index 6f6246e3f..e560527a8 100644
--- a/src/xclim/indices/_agro.py
+++ b/src/xclim/indices/_agro.py
@@ -1113,8 +1113,8 @@ def standardized_precipitation_index(
     pr : xarray.DataArray
         Daily precipitation.
     freq : str, optional
-        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
-        has already been applied input dataset and will skip the resampling step.
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes
+        that the desired resampling has already been applied input dataset and will skip the resampling step.
     window : int
         Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
         i.e. a monthly resampling, the window is an integer number of months.
@@ -1254,8 +1254,8 @@ def standardized_precipitation_evapotranspiration_index(
     wb : xarray.DataArray
         Daily water budget (pr - pet).
     freq : str, optional
-        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
-        has already been applied input dataset and will skip the resampling step.
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes
+        that the desired resampling has already been applied input dataset and will skip the resampling step.
     window : int
         Averaging window length relative to the resampling frequency. For example, if `freq="MS"`, i.e. a monthly
         resampling, the window is an integer number of months.
diff --git a/src/xclim/indices/_hydrology.py b/src/xclim/indices/_hydrology.py
index 13758e62f..ae5c0983d 100644
--- a/src/xclim/indices/_hydrology.py
+++ b/src/xclim/indices/_hydrology.py
@@ -3,12 +3,15 @@
 from __future__ import annotations
 
 import numpy as np
-import xarray as xr
+import xarray
+from scipy.stats import rv_continuous
 
+from xclim.core._types import DateStr, Quantified
 from xclim.core.calendar import get_calendar
 from xclim.core.missing import at_least_n_valid
 from xclim.core.units import declare_units, rate2amount, to_agg_units
 from xclim.indices.generic import threshold_count
+from xclim.indices.stats import standardized_index
 
 from . import generic
 
@@ -24,11 +27,13 @@
     "snow_melt_we_max",
     "snw_max",
     "snw_max_doy",
+    "standardized_groundwater_index",
+    "standardized_streamflow_index",
 ]
 
 
 @declare_units(q="[discharge]")
-def base_flow_index(q: xr.DataArray, freq: str = "YS") -> xr.DataArray:
+def base_flow_index(q: xarray.DataArray, freq: str = "YS") -> xarray.DataArray:
     r"""
     Base flow index.
 
@@ -71,7 +76,7 @@ def base_flow_index(q: xr.DataArray, freq: str = "YS") -> xr.DataArray:
 
 
 @declare_units(q="[discharge]")
-def rb_flashiness_index(q: xr.DataArray, freq: str = "YS") -> xr.DataArray:
+def rb_flashiness_index(q: xarray.DataArray, freq: str = "YS") -> xarray.DataArray:
     r"""
     Richards-Baker flashiness index.
 
@@ -109,8 +114,138 @@ def rb_flashiness_index(q: xr.DataArray, freq: str = "YS") -> xr.DataArray:
     return out
 
 
+@declare_units(
+    q="[discharge]",
+    params="[]",
+)
+def standardized_streamflow_index(
+    q: xarray.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str | rv_continuous = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xarray.DataArray:
+    r"""
+    Standardized Streamflow Index (SSI).
+
+    Parameters
+    ----------
+    q : xarray.DataArray
+        Rate of river discharge.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes
+        that the desired resampling has already been applied input dataset and will skip the resampling step.
+    window : int
+        Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
+        i.e. a monthly resampling, the window is an integer number of months.
+    dist : {"genextreme", "fisk"} or `rv_continuous` function
+        Name of the univariate distribution, or a callable `rv_continuous` (see :py:mod:`scipy.stats`).
+    method : {"APP", "ML", "PWM"}
+        Name of the fitting method, such as `ML` (maximum likelihood), `APP` (approximate). The approximate method
+        uses a deterministic function that does not involve any optimization.
+        `PWM` should be used with a `lmoments3` distribution.
+    fitkwargs : dict, optional
+        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certain parameters (`floc`, `fscale`).
+    cal_start : DateStr, optional
+        Start date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period begins at the start of the input dataset.
+    cal_end : DateStr, optional
+        End date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period finishes at the end of the input dataset.
+    params : xarray.DataArray, optional
+        Fit parameters.
+        The `params` can be computed using ``xclim.indices.stats.standardized_index_fit_params`` in advance.
+        The output can be given here as input, and it overrides other options.
+    **indexer : Indexer
+        Indexing parameters to compute the indicator on a temporal subset of the data.
+        It accepts the same arguments as :py:func:`xclim.indices.generic.select_time`.
+
+    Returns
+    -------
+    xarray.DataArray, [unitless]
+        Standardized Streamflow Index.
+
+    See Also
+    --------
+    xclim.indices._agro.standardized_precipitation_index : Standardized Precipitation Index.
+    xclim.indices.stats.standardized_index : Standardized Index.
+    xclim.indices.stats.standardized_index_fit_params : Standardized Index Fit Params.
+
+    Notes
+    -----
+    * N-month SSI / N-day SSI is determined by choosing the `window = N` and the appropriate frequency `freq`.
+    * Supported statistical distributions are: ["genextreme", "fisk"], where "fisk" is scipy's implementation of
+       a log-logistic distribution.
+    * If `params` is provided, it overrides the `cal_start`, `cal_end`, `freq`, `window`, `dist`, and `method` options.
+    * "APP" method only supports two-parameter distributions. Parameter `loc` needs to be fixed to use method "APP".
+    * The standardized index is bounded by ±8.21. 8.21 is the largest standardized index as constrained by the
+      float64 precision in the inversion to the normal distribution.
+
+    References
+    ----------
+    :cite:cts:`vicente-serrano_2012`
+
+    Examples
+    --------
+    >>> from datetime import datetime
+    >>> from xclim.indices import standardized_streamflow_index
+    >>> ds = xr.open_dataset(path_to_q_file)
+    >>> q = ds.q_sim
+    >>> cal_start, cal_end = "2006-05-01", "2008-06-01"
+    >>> ssi_3 = standardized_streamflow_index(
+    ...     q,
+    ...     freq="MS",
+    ...     window=3,
+    ...     dist="genextreme",
+    ...     method="ML",
+    ...     cal_start=cal_start,
+    ...     cal_end=cal_end,
+    ... )  # Computing SSI-3 months using a GEV distribution for the fit
+    >>> # Fitting parameters can also be obtained first, then reused as input.
+    >>> from xclim.indices.stats import standardized_index_fit_params
+    >>> params = standardized_index_fit_params(
+    ...     q.sel(time=slice(cal_start, cal_end)),
+    ...     freq="MS",
+    ...     window=3,
+    ...     dist="genextreme",
+    ...     method="ML",
+    ... )  # First getting params
+    >>> ssi_3 = standardized_streamflow_index(q, params=params)
+    """
+    fitkwargs = fitkwargs or {}
+    dist_methods = {"genextreme": ["ML", "APP"], "fisk": ["ML", "APP"]}
+    if isinstance(dist, str):
+        if dist in dist_methods:
+            if method not in dist_methods[dist]:
+                raise NotImplementedError(f"{method} method is not implemented for {dist} distribution")
+        else:
+            raise NotImplementedError(f"{dist} distribution is not yet implemented.")
+
+    zero_inflated = False
+    ssi = standardized_index(
+        q,
+        freq=freq,
+        window=window,
+        dist=dist,
+        method=method,
+        zero_inflated=zero_inflated,
+        fitkwargs=fitkwargs,
+        cal_start=cal_start,
+        cal_end=cal_end,
+        params=params,
+        **indexer,
+    )
+
+    return ssi
+
+
 @declare_units(snd="[length]")
-def snd_max(snd: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray:
+def snd_max(snd: xarray.DataArray, freq: str = "YS-JUL") -> xarray.DataArray:
     """
     Maximum snow depth.
 
@@ -132,7 +267,7 @@ def snd_max(snd: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray:
 
 
 @declare_units(snd="[length]")
-def snd_max_doy(snd: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray:
+def snd_max_doy(snd: xarray.DataArray, freq: str = "YS-JUL") -> xarray.DataArray:
     """
     Day of year of maximum snow depth.
 
@@ -162,7 +297,7 @@ def snd_max_doy(snd: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray:
 
 
 @declare_units(snw="[mass]/[area]")
-def snw_max(snw: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray:
+def snw_max(snw: xarray.DataArray, freq: str = "YS-JUL") -> xarray.DataArray:
     """
     Maximum snow amount.
 
@@ -184,7 +319,7 @@ def snw_max(snw: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray:
 
 
 @declare_units(snw="[mass]/[area]")
-def snw_max_doy(snw: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray:
+def snw_max_doy(snw: xarray.DataArray, freq: str = "YS-JUL") -> xarray.DataArray:
     """
     Day of year of maximum snow amount.
 
@@ -214,7 +349,7 @@ def snw_max_doy(snw: xr.DataArray, freq: str = "YS-JUL") -> xr.DataArray:
 
 
 @declare_units(snw="[mass]/[area]")
-def snow_melt_we_max(snw: xr.DataArray, window: int = 3, freq: str = "YS-JUL") -> xr.DataArray:
+def snow_melt_we_max(snw: xarray.DataArray, window: int = 3, freq: str = "YS-JUL") -> xarray.DataArray:
     """
     Maximum snow melt.
 
@@ -247,7 +382,9 @@ def snow_melt_we_max(snw: xr.DataArray, window: int = 3, freq: str = "YS-JUL") -
 
 
 @declare_units(snw="[mass]/[area]", pr="[precipitation]")
-def melt_and_precip_max(snw: xr.DataArray, pr: xr.DataArray, window: int = 3, freq: str = "YS-JUL") -> xr.DataArray:
+def melt_and_precip_max(
+    snw: xarray.DataArray, pr: xarray.DataArray, window: int = 3, freq: str = "YS-JUL"
+) -> xarray.DataArray:
     """
     Maximum snow melt and precipitation.
 
@@ -284,8 +421,140 @@ def melt_and_precip_max(snw: xr.DataArray, pr: xr.DataArray, window: int = 3, fr
     return out
 
 
+@declare_units(
+    gwl="[length]",
+    params="[]",
+)
+def standardized_groundwater_index(
+    gwl: xarray.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str | rv_continuous = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xarray.DataArray:
+    r"""
+    Standardized Groundwater Index (SGI).
+
+    Parameters
+    ----------
+    gwl : xarray.DataArray
+        Groundwater head level.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes
+        that the desired resampling has already been applied input dataset and will skip the resampling step.
+    window : int
+        Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
+        i.e. a monthly resampling, the window is an integer number of months.
+    dist : {"gamma", "genextreme", "lognorm"} or `rv_continuous`
+        Name of the univariate distribution, or a callable `rv_continuous` (see :py:mod:`scipy.stats`).
+    method : {"APP", "ML", "PWM"}
+        Name of the fitting method, such as `ML` (maximum likelihood), `APP` (approximate).
+        The approximate method uses a deterministic function that does not involve any optimization.
+        `PWM` should be used with a `lmoments3` distribution.
+    fitkwargs : dict, optional
+        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certain parameters (`floc`, `fscale`).
+    cal_start : DateStr, optional
+        Start date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period begins at the start of the input dataset.
+    cal_end : DateStr, optional
+        End date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period finishes at the end of the input dataset.
+    params : xarray.DataArray, optional
+        Fit parameters.
+        The `params` can be computed using ``xclim.indices.stats.standardized_index_fit_params`` in advance.
+        The output can be given here as input, and it overrides other options.
+    **indexer : Indexer
+        Indexing parameters to compute the indicator on a temporal subset of the data.
+        It accepts the same arguments as :py:func:`xclim.indices.generic.select_time`.
+
+    Returns
+    -------
+    xarray.DataArray, [unitless]
+        Standardized Groundwater Index.
+
+    See Also
+    --------
+    xclim.indices._agro.standardized_precipitation_index : Standardized Precipitation Index.
+    xclim.indices.stats.standardized_index : Standardized Index.
+    xclim.indices.stats.standardized_index_fit_params : Standardized Index Fit Params.
+
+    Notes
+    -----
+    * N-month SGI / N-day SGI is determined by choosing the `window = N` and the appropriate frequency `freq`.
+    * Supported statistical distributions are: ["gamma", "genextreme", "lognorm"].
+    * If `params` is provided, it overrides the `cal_start`, `cal_end`, `freq`, `window`, `dist`, `method` options.
+    * "APP" method only supports two-parameter distributions. Parameter `loc` needs to be fixed to use method "APP".
+
+    References
+    ----------
+    :cite:cts:`bloomfield_2013`
+
+    Examples
+    --------
+    >>> from datetime import datetime
+    >>> from xclim.indices import standardized_groundwater_index
+    >>> ds = xr.open_dataset(path_to_gwl_file)
+    >>> gwl = ds.gwl
+    >>> cal_start, cal_end = "1980-05-01", "1982-06-01"
+    >>> sgi_3 = standardized_groundwater_index(
+    ...     gwl,
+    ...     freq="MS",
+    ...     window=3,
+    ...     dist="gamma",
+    ...     method="ML",
+    ...     cal_start=cal_start,
+    ...     cal_end=cal_end,
+    ... )  # Computing SGI-3 months using a Gamma distribution for the fit
+    >>> # Fitting parameters can also be obtained first, then reused as input.
+    >>> from xclim.indices.stats import standardized_index_fit_params
+    >>> params = standardized_index_fit_params(
+    ...     gwl.sel(time=slice(cal_start, cal_end)),
+    ...     freq="MS",
+    ...     window=3,
+    ...     dist="gamma",
+    ...     method="ML",
+    ... )  # First getting params
+    >>> sgi_3 = standardized_groundwater_index(gwl, params=params)
+    """
+    fitkwargs = fitkwargs or {}
+
+    dist_methods = {
+        "gamma": ["ML", "APP"],
+        "genextreme": ["ML", "APP"],
+        "lognorm": ["ML", "APP"],
+    }
+    if isinstance(dist, str):
+        if dist in dist_methods:
+            if method not in dist_methods[dist]:
+                raise NotImplementedError(f"{method} method is not implemented for {dist} distribution")
+        else:
+            raise NotImplementedError(f"{dist} distribution is not yet implemented.")
+
+    zero_inflated = False
+    sgi = standardized_index(
+        gwl,
+        freq=freq,
+        window=window,
+        dist=dist,
+        method=method,
+        zero_inflated=zero_inflated,
+        fitkwargs=fitkwargs,
+        cal_start=cal_start,
+        cal_end=cal_end,
+        params=params,
+        **indexer,
+    )
+
+    return sgi
+
+
 @declare_units(q="[discharge]")
-def flow_index(q: xr.DataArray, p: float = 0.95) -> xr.DataArray:
+def flow_index(q: xarray.DataArray, p: float = 0.95) -> xarray.DataArray:
     """
     Flow index.
 
@@ -293,14 +562,14 @@ def flow_index(q: xr.DataArray, p: float = 0.95) -> xr.DataArray:
 
     Parameters
     ----------
-    q : xr.DataArray
+    q : xarray.DataArray
         Daily streamflow data.
     p : float
         Percentile for calculating the flow index, between 0 and 1. Default of 0.95 is for high flows.
 
     Returns
     -------
-    xr.DataArray
+    xarray.DataArray
         Normalized Qp, which is the p th percentile of daily streamflow normalized by the median flow.
 
     References
@@ -315,7 +584,7 @@ def flow_index(q: xr.DataArray, p: float = 0.95) -> xr.DataArray:
 
 
 @declare_units(q="[discharge]")
-def high_flow_frequency(q: xr.DataArray, threshold_factor: int = 9, freq: str = "YS-OCT") -> xr.DataArray:
+def high_flow_frequency(q: xarray.DataArray, threshold_factor: int = 9, freq: str = "YS-OCT") -> xarray.DataArray:
     """
     High flow frequency.
 
@@ -325,7 +594,7 @@ def high_flow_frequency(q: xr.DataArray, threshold_factor: int = 9, freq: str =
 
     Parameters
     ----------
-    q : xr.DataArray
+    q : xarray.DataArray
         Daily streamflow data.
     threshold_factor : int
         Factor by which the median flow is multiplied to set the high flow threshold, default is 9.
@@ -334,7 +603,7 @@ def high_flow_frequency(q: xr.DataArray, threshold_factor: int = 9, freq: str =
 
     Returns
     -------
-    xr.DataArray
+    xarray.DataArray
         Number of high flow days.
 
     References
@@ -348,7 +617,7 @@ def high_flow_frequency(q: xr.DataArray, threshold_factor: int = 9, freq: str =
 
 
 @declare_units(q="[discharge]")
-def low_flow_frequency(q: xr.DataArray, threshold_factor: float = 0.2, freq: str = "YS-OCT") -> xr.DataArray:
+def low_flow_frequency(q: xarray.DataArray, threshold_factor: float = 0.2, freq: str = "YS-OCT") -> xarray.DataArray:
     """
     Low flow frequency.
 
@@ -358,7 +627,7 @@ def low_flow_frequency(q: xr.DataArray, threshold_factor: float = 0.2, freq: str
 
     Parameters
     ----------
-    q : xr.DataArray
+    q : xarray.DataArray
         Daily streamflow data.
     threshold_factor : float
         Factor by which the mean flow is multiplied to set the low flow threshold, default is 0.2.
@@ -367,7 +636,7 @@ def low_flow_frequency(q: xr.DataArray, threshold_factor: float = 0.2, freq: str
 
     Returns
     -------
-    xr.DataArray
+    xarray.DataArray
         Number of low flow days.
 
     References
diff --git a/src/xclim/indices/stats.py b/src/xclim/indices/stats.py
index d563fb7f3..1d7b6219b 100644
--- a/src/xclim/indices/stats.py
+++ b/src/xclim/indices/stats.py
@@ -589,6 +589,24 @@ def _fit_start(x, dist: str, **fitkwargs: Any) -> tuple[tuple, dict]:
         c0 = np.pi * m / np.sqrt(3) / np.sqrt(m2 - m**2)
         kwargs = {"scale": scale0, "loc": loc0}
         return (c0,), kwargs
+
+    if dist in ["lognorm"]:
+        if "floc" in fitkwargs:
+            loc0 = fitkwargs["floc"]
+        else:
+            # muralidhar_1992
+            xs = sorted(x)
+            x1, xn, xp = xs[0], xs[-1], xs[int(len(x) / 2)]
+            loc0 = (x1 * xn - xp**2) / (x1 + xn - 2 * xp)
+        x_pos = x - loc0
+        x_pos = x_pos[x_pos > 0]
+        # MLE estimation
+        log_x_pos = np.log(x_pos)
+        shape0 = log_x_pos.std()
+        scale0 = np.exp(log_x_pos.mean())
+        kwargs = {"scale": scale0, "loc": loc0}
+        return (shape0,), kwargs
+
     return (), {}
 
 
@@ -768,8 +786,8 @@ def standardized_index_fit_params(
     da : xarray.DataArray
         Input array.
     freq : str, optional
-        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
-        has already been applied input dataset and will skip the resampling step.
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes
+        that the desired resampling has already been applied input dataset and will skip the resampling step.
     window : int
         Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
         i.e. a monthly resampling, the window is an integer number of months.
@@ -813,11 +831,17 @@ def standardized_index_fit_params(
     if method == "APP":
         if "floc" not in fitkwargs.keys():
             raise ValueError(
-                "The APP method is only supported for two-parameter distributions with `gamma` or `fisk` "
-                "with `loc` being fixed. Pass a value for `floc` in `fitkwargs`."
+                "The APP method is only supported for two-parameter distributions with `gamma`, `fisk`, "
+                "`lognorm`, or `genextreme` with `loc` being fixed. Pass a value for `floc` in `fitkwargs`."
             )
 
-    dist_and_methods = {"gamma": ["ML", "APP"], "fisk": ["ML", "APP"]}
+    # "WPM" method doesn't seem to work for gamma or pearson3
+    dist_and_methods = {
+        "gamma": ["ML", "APP"],
+        "fisk": ["ML", "APP"],
+        "genextreme": ["ML", "APP"],
+        "lognorm": ["ML", "APP"],
+    }
     dist = get_dist(dist)
     if method != "PWM":
         if dist.name not in dist_and_methods:
@@ -874,8 +898,8 @@ def standardized_index(
     da : xarray.DataArray
         Daily input data.
     freq : str, optional
-        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
-        has already been applied input dataset and will skip the resampling step.
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes
+        that the desired resampling has already been applied input dataset and will skip the resampling step.
     window : int
         Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
         i.e. a monthly resampling, the window is an integer number of months.
diff --git a/src/xclim/testing/helpers.py b/src/xclim/testing/helpers.py
index 370aa4811..e0c6e69ec 100644
--- a/src/xclim/testing/helpers.py
+++ b/src/xclim/testing/helpers.py
@@ -120,7 +120,9 @@ def add_example_file_paths() -> dict[str, str | list[xr.DataArray]]:
     """
     namespace = {
         "path_to_ensemble_file": "EnsembleReduce/TestEnsReduceCriteria.nc",
+        "path_to_gwl_file": "Raven/gwl_obs.nc",
         "path_to_pr_file": "NRCANdaily/nrcan_canada_daily_pr_1990.nc",
+        "path_to_q_file": "Raven/q_sim.nc",
         "path_to_sfcWind_file": "ERA5/daily_surface_cancities_1990-1993.nc",
         "path_to_tas_file": "ERA5/daily_surface_cancities_1990-1993.nc",
         "path_to_tasmax_file": "NRCANdaily/nrcan_canada_daily_tasmax_1990.nc",
diff --git a/src/xclim/testing/registry.txt b/src/xclim/testing/registry.txt
index 5a219a55d..edf50168a 100644
--- a/src/xclim/testing/registry.txt
+++ b/src/xclim/testing/registry.txt
@@ -18,6 +18,7 @@ HadGEM2-CC_360day/tasmin_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-
 NRCANdaily/nrcan_canada_daily_pr_1990.nc sha256:144479ec7a976cfecb6a10762d128a771356093d72caf5f075508ee86d25a1b0
 NRCANdaily/nrcan_canada_daily_tasmax_1990.nc sha256:84880205b798740e37a102c7f40e595d7a4fde6e35fb737a1ef68b8dad447526
 NRCANdaily/nrcan_canada_daily_tasmin_1990.nc sha256:13d61fc54cdcb4c1617ec777ccbf59575d8fdc24754f914042301bc1b024d7f7
+Raven/gwl_obs.nc sha256:94213ee5f1c0fd73156de9638647338c9f58af9d136651275f58367817e8cf26
 Raven/q_sim.nc sha256:f7a0ae73c498235e1c3e7338a184c5ca3729941b81521e606aa60b2c639f6e71
 SpatialAnalogs/CanESM2_ScenGen_Chibougamau_2041-2070.nc sha256:b6cfc4a963d68b6da8978acd26ffb506f33c9c264d8057badd90bf47cd9f3f3d
 SpatialAnalogs/NRCAN_SECan_1981-2010.nc sha256:bde680ddad84106caad3a2e83a70ecdd8138578a70e875d77c2ec6d3ff868fee
diff --git a/src/xclim/testing/utils.py b/src/xclim/testing/utils.py
index fff7e0cb6..100f5c5e1 100644
--- a/src/xclim/testing/utils.py
+++ b/src/xclim/testing/utils.py
@@ -69,7 +69,7 @@
     "testing_setup_warnings",
 ]
 
-default_testdata_version = "v2025.1.8"
+default_testdata_version = "v2025.3.11"
 """Default version of the testing data to use when fetching datasets."""
 
 default_testdata_repo_url = "https://raw.githubusercontent.com/Ouranosinc/xclim-testdata/"
diff --git a/tests/test_hydrology.py b/tests/test_hydrology.py
index 0572d12e1..265716291 100644
--- a/tests/test_hydrology.py
+++ b/tests/test_hydrology.py
@@ -1,8 +1,13 @@
 from __future__ import annotations
 
+from pathlib import Path
+
 import numpy as np
+import pytest
 
 from xclim import indices as xci
+from xclim import land
+from xclim.core.units import convert_units_to
 
 
 class TestBaseFlowIndex:
@@ -23,6 +28,55 @@ def test_simple(self, q_series):
         np.testing.assert_array_equal(out, 2)
 
 
+class TestStandardizedStreamflow:
+    @pytest.mark.slow
+    def test_3d_data_with_nans(self, open_dataset):
+        nc_ds = Path("Raven", "q_sim.nc")
+        # test with data
+        ds = open_dataset(nc_ds)
+        q = ds.q_obs.sel(time=slice("2008")).rename("q")
+        qMM = convert_units_to(q, "mm**3/s", context="hydro")  # noqa
+        # put a nan somewhere
+        qMM.values[10] = np.nan
+        q.values[10] = np.nan
+
+        out1 = land.standardized_streamflow_index(
+            q,
+            freq="MS",
+            window=1,
+            dist="genextreme",
+            method="APP",
+            fitkwargs={"floc": 0},
+        )
+        out2 = land.standardized_streamflow_index(
+            qMM,
+            freq="MS",
+            window=1,
+            dist="genextreme",
+            method="APP",
+            fitkwargs={"floc": 0},
+        )
+        np.testing.assert_array_almost_equal(out1, out2, 3)
+
+    @pytest.mark.slow
+    def test_3d_data_with_nans_value(self, open_dataset):
+        nc_ds = Path("Raven", "q_sim.nc")
+        # test with data
+        ds = open_dataset(nc_ds)
+        q = ds.q_obs.sel(time=slice("2008", "2018")).rename("q")
+        q[{"time": 10}] = np.nan
+
+        out1 = land.standardized_streamflow_index(
+            q,
+            freq=None,
+            window=1,
+            dist="genextreme",
+            method="APP",
+            fitkwargs={"floc": 0},
+        )
+        assert np.isnan(out1[{"time": 10}])
+
+
 class TestSnwMax:
     def test_simple(self, snw_series):
         a = np.zeros(366)
diff --git a/tests/test_indices.py b/tests/test_indices.py
index 7732299fc..47a78266f 100644
--- a/tests/test_indices.py
+++ b/tests/test_indices.py
@@ -841,6 +841,272 @@ def test_standardized_precipitation_evapotranspiration_index(
 
         np.testing.assert_allclose(spei.values, values, rtol=0, atol=diff_tol)
 
+    @pytest.mark.slow
+    @pytest.mark.parametrize(
+        "freq, window, dist, method,  values, diff_tol",
+        [
+            # reference results: Obtained with R package `standaRdized`
+            (
+                "D",
+                1,
+                "genextreme",
+                "ML",
+                [0.5331, 0.5338, 0.5098, 0.4656, 0.4937],
+                9e-2,
+            ),
+            (
+                "D",
+                12,
+                "genextreme",
+                "ML",
+                [0.4414, 0.4695, 0.4861, 0.4838, 0.4877],
+                9e-2,
+            ),
+            # reference results : xclim version where the test was implemented
+            (
+                "D",
+                1,
+                "genextreme",
+                "ML",
+                [0.6105, 0.6167, 0.5957, 0.5520, 0.5794],
+                2e-2,
+            ),
+            (
+                "D",
+                1,
+                "genextreme",
+                "APP",
+                [-0.0259, -0.0141, -0.0080, -0.0098, 0.0089],
+                2e-2,
+            ),
+            ("D", 1, "fisk", "ML", [0.3514, 0.3741, 0.1349, 0.4332, 0.1724], 2e-2),
+            ("D", 1, "fisk", "APP", [0.3321, 0.3477, 0.3536, 0.3468, 0.3723], 2e-2),
+            (
+                "D",
+                12,
+                "genextreme",
+                "ML",
+                [0.5131, 0.5442, 0.5645, 0.5660, 0.5720],
+                2e-2,
+            ),
+            (
+                "D",
+                12,
+                "genextreme",
+                "APP",
+                [-0.0697, -0.0550, -0.0416, -0.0308, -0.0194],
+                2e-2,
+            ),
+            ("D", 12, "fisk", "ML", [0.2096, 0.2728, 0.3259, 0.3466, 0.2836], 2e-2),
+            ("D", 12, "fisk", "APP", [0.2667, 0.2893, 0.3088, 0.3233, 0.3385], 2e-2),
+            (
+                "MS",
+                1,
+                "genextreme",
+                "ML",
+                [0.7315, -1.4919, -0.5405, 0.9965, -0.7449],
+                2e-2,
+            ),
+            (
+                "MS",
+                1,
+                "genextreme",
+                "APP",
+                [0.0979, -1.6806, -0.5345, 0.7355, -0.7583],
+                2e-2,
+            ),
+            (
+                "MS",
+                1,
+                "fisk",
+                "ML",
+                [0.533154, -1.5777, -0.436331, 0.29581, -0.814988],
+                2e-2,
+            ),
+            ("MS", 1, "fisk", "APP", [0.4663, -1.9076, -0.5362, 0.8070, -0.8035], 2e-2),
+            (
+                "MS",
+                12,
+                "genextreme",
+                "ML",
+                [-0.9795, -1.0398, -1.9019, -1.6970, -1.4761],
+                2e-2,
+            ),
+            (
+                "MS",
+                12,
+                "genextreme",
+                "APP",
+                [-0.9095, -1.0996, -1.9207, -2.2665, -2.1746],
+                2e-2,
+            ),
+            (
+                "MS",
+                12,
+                "fisk",
+                "ML",
+                [-1.0776, -1.0827, -1.9333, -1.7764, -1.8391],
+                2e-2,
+            ),
+            (
+                "MS",
+                12,
+                "fisk",
+                "APP",
+                [-0.9607, -1.1265, -1.7004, -1.8747, -1.8132],
+                2e-2,
+            ),
+        ],
+    )
+    def test_standardized_streamflow_index(self, open_dataset, freq, window, dist, method, values, diff_tol):
+        ds = open_dataset("Raven/q_sim.nc")
+        q = ds.q_obs.rename("q")
+        q_cal = ds.q_sim.rename("q").fillna(ds.q_sim.mean())
+        if freq == "D":
+            q = q.sel(time=slice("2008-01-01", "2008-01-30")).fillna(ds.q_obs.mean())
+        else:
+            q = q.sel(time=slice("2008-01-01", "2009-12-31")).fillna(ds.q_obs.mean())
+        fitkwargs = {"floc": 0} if method == "APP" else {}
+        params = xci.stats.standardized_index_fit_params(
+            q_cal,
+            freq=freq,
+            window=window,
+            dist=dist,
+            method=method,
+            fitkwargs=fitkwargs,
+            zero_inflated=True,
+        )
+        ssi = xci.standardized_streamflow_index(q, params=params)
+        ssi = ssi.isel(time=slice(-11, -1, 2)).values.flatten()
+
+        np.testing.assert_allclose(ssi, values, rtol=0, atol=diff_tol)
+
+    # TODO: Find another package to test against
+    # For now, we just take a snapshot of what xclim produces when this function
+    # was added
+    @pytest.mark.slow
+    @pytest.mark.parametrize(
+        "freq, window, dist, method,  values, diff_tol",
+        [
+            (
+                "MS",
+                12,
+                "gamma",
+                "APP",
+                [0.053303, 0.243638, 0.184645, 0.365087, 0.702955],
+                2e-2,
+            ),
+            (
+                "MS",
+                12,
+                "gamma",
+                "ML",
+                [0.054521, 0.244173, 0.185881, 0.360743, 0.695511],
+                0.04,
+            ),
+            (
+                "D",
+                12,
+                "gamma",
+                "APP",
+                [0.697812, 0.822368, 0.980493, 1.088905, 1.210871],
+                2e-2,
+            ),
+            (
+                "D",
+                12,
+                "gamma",
+                "ML",
+                [0.689838, 0.806486, 0.945229, 1.066726, 1.164071],
+                2e-2,
+            ),
+            (
+                "MS",
+                12,
+                "lognorm",
+                "APP",
+                [0.054521, 0.244173, 0.185881, 0.360743, 0.695511],
+                2e-2,
+            ),
+            (
+                "MS",
+                12,
+                "lognorm",
+                "ML",
+                [0.052334, 0.243673, 0.185901, 0.360868, 0.695515],
+                0.04,
+            ),
+            (
+                "D",
+                12,
+                "lognorm",
+                "APP",
+                [0.697812, 0.822368, 0.980493, 1.088905, 1.210871],
+                2e-2,
+            ),
+            (
+                "D",
+                12,
+                "lognorm",
+                "ML",
+                [0.698288, 0.822422, 0.983334, 1.094167, 1.212815],
+                2e-2,
+            ),
+            (
+                "MS",
+                12,
+                "genextreme",
+                "ML",
+                [-0.266746, -0.043151, -0.149119, -0.036864, 1.01006],
+                2e-2,
+            ),
+            (
+                "D",
+                12,
+                "genextreme",
+                "ML",
+                [0.466671, 0.69093, 1.126953, 3.09, 2.489967],
+                2e-2,
+            ),
+            (
+                "D",
+                12,
+                "genextreme",
+                "APP",
+                [0.901014, 1.017546, 1.161481, 1.258072, 1.364903],
+                2e-2,
+            ),
+        ],
+    )
+    def test_standardized_groundwater_index(self, open_dataset, freq, window, dist, method, values, diff_tol):
+        if method == "ML" and freq == "D" and Version(__numpy_version__) < Version("2.0.0"):
+            pytest.skip("Skipping SPI/ML/D on older numpy")
+        ds = open_dataset("Raven/gwl_obs.nc")
+        gwl0 = ds.gwl
+
+        gwl = gwl0.sel(time=slice("1989", "1991"))
+
+        gwl_cal = gwl0
+        fitkwargs = {}
+        if method == "APP":
+            fitkwargs["floc"] = 0
+        params = xci.stats.standardized_index_fit_params(
+            gwl_cal,
+            freq=freq,
+            window=window,
+            dist=dist,
+            method=method,
+            fitkwargs=fitkwargs,
+            zero_inflated=True,
+        )
+        sgi = xci.standardized_groundwater_index(gwl, params=params)
+        # Only a few moments before year 2000 are tested
+        sgi = sgi.isel(time=slice(-11, -1, 2))
+
+        sgi = sgi.clip(-3.09, 3.09)
+
+        np.testing.assert_allclose(sgi.values, values, rtol=0, atol=diff_tol)
+
     @pytest.mark.parametrize(
         "indexer",
         [


Reviews:
user: login: LamAdr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-08-12T19:57:38Z
user: login: LamAdr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-08-13T14:42:33Z
user: login: Zeitsperre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2024-08-13T14:52:29Z
user: login: LamAdr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2024-08-13T16:06:52Z
user: login: Zeitsperre
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2024-08-13T16:37:32Z
user: login: coxipi
body: .
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T18:08:20Z
user: login: huard
body: I just want to make sure this PR is not impeding the use of PWM for genextreme and gamma. 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-02-26T16:42:16Z
user: login: coxipi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:44:18Z
user: login: coxipi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T13:51:52Z
user: login: Zeitsperre
body: These changes shouldn't be needed.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-12T17:17:59Z
user: login: coxipi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T20:20:12Z
user: login: Zeitsperre
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-13T17:35:44Z

Review comments:
diff_hunk: @@ -751,6 +751,54 @@ def test_standardized_precipitation_evapotranspiration_index(
 
         np.testing.assert_allclose(spei.values, values, rtol=0, atol=diff_tol)
 
+    # reference results: Obtained with R package `standaRdized`
path: tests/test_indices.py
user: login: LamAdr
body: @SarahG-579462 @coxipi
I couldn't find any other reference to test against for SSI (same for SGI I think). I could only replicate two conditions out of many. When the values get too far from zero, `standaRdized` outputs lots of NaNs and the non-NaNs are far away from us. Also in these replicable values, the offset is somewhat constant.

It looks like the problem is on their side?

Do you think this is still a valuable test then?

I'll also add a couple tests against ourselves.
created_at: 2024-08-12T19:57:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,6 +31,13 @@ def cfcheck(q):
         check_valid(q, "standard_name", "water_volume_transport_in_river_channel")
 
 
+class StandardizedStreamflowIndexes(ResamplingIndicator):
path: xclim/indicators/land/_streamflow.py
user: login: LamAdr
body: Not sure if this should be a new class or `standardized_streamflow_index` should instantiate `StandardizedIndexes` (currently in xclim/indicators/atmos/_precip, but would be relocated in xclim.core.indicator).

Would that break something?
created_at: 2024-08-13T14:42:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,6 +28,38 @@ def test_simple(self, q_series):
         np.testing.assert_array_equal(out, 2)
 
 
+class TestStandardizedStreamflow:
+    nc_ds = Path("Raven", "q_sim.nc")
path: tests/test_hydrology.py
user: login: Zeitsperre
body: This is really a nitpick, but you could probably just throw this into the test function
created_at: 2024-08-13T14:44:38Z
author_association: COLLABORATOR
diff_hunk: @@ -5,13 +5,19 @@
 from xclim.core.cfchecks import check_valid
 from xclim.core.indicator import ResamplingIndicator
 from xclim.core.units import declare_units
-from xclim.indices import base_flow_index, generic, rb_flashiness_index
+from xclim.indices import (
+    base_flow_index,
+    generic,
+    rb_flashiness_index,
+    standardized_streamflow_index,
+)
 
 __all__ = [
     "base_flow_index",
     "doy_qmax",
     "doy_qmin",
     "rb_flashiness_index",
+    "standardized_streamflow_index",
path: xclim/indicators/land/_streamflow.py
user: login: Zeitsperre
body: I'm surprised that there isn't a namespace collision here due to the shadowing of `standardized_streamflow_index`. My tendency is to import the indice with an underline, i.e.
```
from xclim.indices import standardized_streamflow_index as _standardized_streamflow_index
```
Then expose the Indicator without the underline. If this isn't emitting any warnings though, no worries.
created_at: 2024-08-13T14:48:42Z
author_association: COLLABORATOR
diff_hunk: @@ -25,6 +31,13 @@ def cfcheck(q):
         check_valid(q, "standard_name", "water_volume_transport_in_river_channel")
 
 
+class StandardizedStreamflowIndexes(ResamplingIndicator):
path: xclim/indicators/land/_streamflow.py
user: login: Zeitsperre
body: I think you're free to create a new class here.
created_at: 2024-08-13T14:49:31Z
author_association: COLLABORATOR
diff_hunk: @@ -104,6 +107,135 @@ def rb_flashiness_index(q: xarray.DataArray, freq: str = "YS") -> xarray.DataArr
     return out
 
 
+@declare_units(
+    q="[discharge]",
+    params="[]",
+)
+def standardized_streamflow_index(
+    q: xarray.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xarray.DataArray:
+    r"""Standardized Streamflow Index (SSI).
+
+    Parameters
+    ----------
+    q : xarray.DataArray
+        Rate of river discharge.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
+        has already been applied input dataset and will skip the resampling step.
+    window : int
+        Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
+        i.e. a monthly resampling, the window is an integer number of months.
+    dist : {"genextreme", "fisk"}
+        Name of the univariate distribution. (see :py:mod:`scipy.stats`).
+    method : {'APP', 'ML', 'PWM'}
+        Name of the fitting method, such as `ML` (maximum likelihood), `APP` (approximate). The approximate method
+        uses a deterministic function that doesn't involve any optimization.
+    fitkwargs : dict, optional
+        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certains parameters (`floc`, `fscale`).
+    cal_start : DateStr, optional
+        Start date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period begins at the start of the input dataset.
+    cal_end : DateStr, optional
+        End date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period finishes at the end of the input dataset.
+    params : xarray.DataArray
path: xclim/indices/_hydrology.py
user: login: Zeitsperre
body: ```suggestion
    params : xarray.DataArray, optional
```
created_at: 2024-08-13T14:50:24Z
author_association: COLLABORATOR
diff_hunk: @@ -104,6 +107,135 @@ def rb_flashiness_index(q: xarray.DataArray, freq: str = "YS") -> xarray.DataArr
     return out
 
 
+@declare_units(
+    q="[discharge]",
+    params="[]",
+)
+def standardized_streamflow_index(
+    q: xarray.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xarray.DataArray:
+    r"""Standardized Streamflow Index (SSI).
+
+    Parameters
+    ----------
+    q : xarray.DataArray
+        Rate of river discharge.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
+        has already been applied input dataset and will skip the resampling step.
+    window : int
+        Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
+        i.e. a monthly resampling, the window is an integer number of months.
+    dist : {"genextreme", "fisk"}
+        Name of the univariate distribution. (see :py:mod:`scipy.stats`).
+    method : {'APP', 'ML', 'PWM'}
+        Name of the fitting method, such as `ML` (maximum likelihood), `APP` (approximate). The approximate method
+        uses a deterministic function that doesn't involve any optimization.
+    fitkwargs : dict, optional
+        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certains parameters (`floc`, `fscale`).
+    cal_start : DateStr, optional
+        Start date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period begins at the start of the input dataset.
+    cal_end : DateStr, optional
+        End date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period finishes at the end of the input dataset.
+    params : xarray.DataArray
+        Fit parameters.
+        The `params` can be computed using ``xclim.indices.stats.standardized_index_fit_params`` in advance.
+        The output can be given here as input, and it overrides other options.
+    \*\*indexer
path: xclim/indices/_hydrology.py
user: login: Zeitsperre
body: ```suggestion
    \*\*indexer : dict
```
created_at: 2024-08-13T14:50:47Z
author_association: COLLABORATOR
diff_hunk: @@ -104,6 +107,135 @@ def rb_flashiness_index(q: xarray.DataArray, freq: str = "YS") -> xarray.DataArr
     return out
 
 
+@declare_units(
+    q="[discharge]",
+    params="[]",
+)
+def standardized_streamflow_index(
+    q: xarray.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xarray.DataArray:
+    r"""Standardized Streamflow Index (SSI).
+
+    Parameters
+    ----------
+    q : xarray.DataArray
+        Rate of river discharge.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
+        has already been applied input dataset and will skip the resampling step.
+    window : int
+        Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
+        i.e. a monthly resampling, the window is an integer number of months.
+    dist : {"genextreme", "fisk"}
+        Name of the univariate distribution. (see :py:mod:`scipy.stats`).
+    method : {'APP', 'ML', 'PWM'}
+        Name of the fitting method, such as `ML` (maximum likelihood), `APP` (approximate). The approximate method
+        uses a deterministic function that doesn't involve any optimization.
+    fitkwargs : dict, optional
+        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certains parameters (`floc`, `fscale`).
+    cal_start : DateStr, optional
+        Start date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period begins at the start of the input dataset.
+    cal_end : DateStr, optional
+        End date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period finishes at the end of the input dataset.
+    params : xarray.DataArray
+        Fit parameters.
+        The `params` can be computed using ``xclim.indices.stats.standardized_index_fit_params`` in advance.
+        The output can be given here as input, and it overrides other options.
+    \*\*indexer
+        Indexing parameters to compute the indicator on a temporal subset of the data.
+        It accepts the same arguments as :py:func:`xclim.indices.generic.select_time`.
+
+    Returns
+    -------
+    xarray.DataArray, [unitless]
+        Standardized Streamflow Index.
+
+    Notes
+    -----
+    * N-month SSI / N-day SSI is determined by choosing the `window = N` and the appropriate frequency `freq`.
+    * Supported statistical distributions are: ["genextreme", "fisk"], where "fisk" is scipy's implementation of
+       a log-logistic distribution
+    * If `params` is given as input, it overrides the `cal_start`, `cal_end`, `freq` and `window`, `dist` and `method` options.
+    * "APP" method only supports two-parameter distributions. Parameter `loc` needs to be fixed to use method `APP`.
+    * The standardized index is bounded by ±8.21. 8.21 is the largest standardized index as constrained by the float64 precision in
+      the inversion to the normal distribution.
+
+    Example
+    -------
+    >>> from datetime import datetime
+    >>> from xclim.indices import standardized_streamflow_index
+    >>> ds = xr.open_dataset(path_to_q_file)
+    >>> q = ds.q
+    >>> cal_start, cal_end = "2006-05-01", "2008-06-01"
+    >>> ssi_3 = standardized_streamflow_index(
+    ...     q,
+    ...     freq="MS",
+    ...     window=3,
+    ...     dist="genextreme",
+    ...     method="ML",
+    ...     cal_start=cal_start,
+    ...     cal_end=cal_end,
+    ... )  # Computing SSI-3 months using a GEV distribution for the fit
+    >>> # Fitting parameters can also be obtained first, then re-used as input.
+    >>> from xclim.indices.stats import standardized_index_fit_params
+    >>> params = standardized_index_fit_params(
+    ...     q.sel(time=slice(cal_start, cal_end)),
+    ...     freq="MS",
+    ...     window=3,
+    ...     dist="genextreme",
+    ...     method="ML",
+    ... )  # First getting params
+    >>> ssi_3 = standardized_streamflow_index(q, params=params)
+
+    See Also
+    --------
+    standardized_precipitation_index
+
+    References
+    ----------
+    :cite:cts:`vicente-serrano_2012`
+    """
+    fitkwargs = fitkwargs or {}
+    dist_methods = {"genextreme": ["ML", "APP", "PWM"], "fisk": ["ML", "APP"]}
+    if dist in dist_methods.keys():
+        if method not in dist_methods[dist]:
+            raise NotImplementedError(
+                f"{method} method is not implemented for {dist} distribution"
path: xclim/indices/_hydrology.py
user: login: Zeitsperre
body: ```suggestion
                f"{method} method is not implemented for {dist} distribution."
```
created_at: 2024-08-13T14:51:32Z
author_association: COLLABORATOR
diff_hunk: @@ -756,7 +756,7 @@ def standardized_index_fit_params(
     if method == "APP":
         if "floc" not in fitkwargs.keys():
             raise ValueError(
-                "The APP method is only supported for two-parameter distributions with `gamma` or `fisk` with `loc` being fixed."
+                "The APP method is only supported for two-parameter distributions with `gamma`, `fisk` or `genextreme` with `loc` being fixed."
path: xclim/indices/stats.py
user: login: Zeitsperre
body: ```suggestion
                "The APP method is only supported for two-parameter distributions with `gamma`, `fisk`, or `genextreme` with `loc` being fixed."
```
created_at: 2024-08-13T14:52:17Z
author_association: COLLABORATOR
diff_hunk: @@ -104,6 +107,135 @@ def rb_flashiness_index(q: xarray.DataArray, freq: str = "YS") -> xarray.DataArr
     return out
 
 
+@declare_units(
+    q="[discharge]",
+    params="[]",
+)
+def standardized_streamflow_index(
+    q: xarray.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xarray.DataArray:
+    r"""Standardized Streamflow Index (SSI).
+
+    Parameters
+    ----------
+    q : xarray.DataArray
+        Rate of river discharge.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
+        has already been applied input dataset and will skip the resampling step.
+    window : int
+        Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
+        i.e. a monthly resampling, the window is an integer number of months.
+    dist : {"genextreme", "fisk"}
+        Name of the univariate distribution. (see :py:mod:`scipy.stats`).
+    method : {'APP', 'ML', 'PWM'}
+        Name of the fitting method, such as `ML` (maximum likelihood), `APP` (approximate). The approximate method
+        uses a deterministic function that doesn't involve any optimization.
+    fitkwargs : dict, optional
+        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certains parameters (`floc`, `fscale`).
+    cal_start : DateStr, optional
+        Start date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period begins at the start of the input dataset.
+    cal_end : DateStr, optional
+        End date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period finishes at the end of the input dataset.
+    params : xarray.DataArray
+        Fit parameters.
+        The `params` can be computed using ``xclim.indices.stats.standardized_index_fit_params`` in advance.
+        The output can be given here as input, and it overrides other options.
+    \*\*indexer
path: xclim/indices/_hydrology.py
user: login: LamAdr
body: it seems someone doesn't like that
created_at: 2024-08-13T16:06:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,6 +107,135 @@ def rb_flashiness_index(q: xarray.DataArray, freq: str = "YS") -> xarray.DataArr
     return out
 
 
+@declare_units(
+    q="[discharge]",
+    params="[]",
+)
+def standardized_streamflow_index(
+    q: xarray.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xarray.DataArray:
+    r"""Standardized Streamflow Index (SSI).
+
+    Parameters
+    ----------
+    q : xarray.DataArray
+        Rate of river discharge.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
+        has already been applied input dataset and will skip the resampling step.
+    window : int
+        Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
+        i.e. a monthly resampling, the window is an integer number of months.
+    dist : {"genextreme", "fisk"}
+        Name of the univariate distribution. (see :py:mod:`scipy.stats`).
+    method : {'APP', 'ML', 'PWM'}
+        Name of the fitting method, such as `ML` (maximum likelihood), `APP` (approximate). The approximate method
+        uses a deterministic function that doesn't involve any optimization.
+    fitkwargs : dict, optional
+        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certains parameters (`floc`, `fscale`).
+    cal_start : DateStr, optional
+        Start date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period begins at the start of the input dataset.
+    cal_end : DateStr, optional
+        End date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period finishes at the end of the input dataset.
+    params : xarray.DataArray
+        Fit parameters.
+        The `params` can be computed using ``xclim.indices.stats.standardized_index_fit_params`` in advance.
+        The output can be given here as input, and it overrides other options.
+    \*\*indexer
path: xclim/indices/_hydrology.py
user: login: Zeitsperre
body: OK. No problem. Feel free to ignore.
created_at: 2024-08-13T16:37:32Z
author_association: COLLABORATOR
diff_hunk: @@ -292,6 +428,139 @@ def melt_and_precip_max(
     return out
 
 
+@declare_units(
+    gwl="[length]",
+    params="[]",
+)
+def standardized_groundwater_index(
+    gwl: xr.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str | rv_continuous = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xr.DataArray:
+    r"""
+    Standardized Groundwater Index (SGI).
+
+    Parameters
+    ----------
+    gwl : xarray.DataArray
+        Groundwater head level.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
+        has already been applied input dataset and will skip the resampling step.
+    window : int
+        Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
+        i.e. a monthly resampling, the window is an integer number of months.
+    dist : {"gamma", "genextreme", "lognorm"} or `rv_continuous`
+        Name of the univariate distribution, or a callable `rv_continuous` (see :py:mod:`scipy.stats`).
+    method : {"APP", "ML", "PWM"}
+        Name of the fitting method, such as `ML` (maximum likelihood), `APP` (approximate). The approximate method
+        uses a deterministic function that does not involve any optimization. `PWM` should be used with a `lmoments3` distribution.
+    fitkwargs : dict, optional
+        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certains parameters (`floc`, `fscale`).
+    cal_start : DateStr, optional
+        Start date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period begins at the start of the input dataset.
+    cal_end : DateStr, optional
+        End date of the calibration period. A `DateStr` is expected, that is a `str` in format `"YYYY-MM-DD"`.
+        Default option `None` means that the calibration period finishes at the end of the input dataset.
+    params : xarray.DataArray, optional
+        Fit parameters.
+        The `params` can be computed using ``xclim.indices.stats.standardized_index_fit_params`` in advance.
+        The output can be given here as input, and it overrides other options.
+    **indexer : Indexer
+        Indexing parameters to compute the indicator on a temporal subset of the data.
+        It accepts the same arguments as :py:func:`xclim.indices.generic.select_time`.
+
+    Returns
+    -------
+    xarray.DataArray, [unitless]
+        Standardized Groundwater Index.
+
+    See Also
+    --------
+    xclim.indices._agro.standardized_precipitation_index : Standardized Precipitation Index.
+    xclim.indices.stats.standardized_index : Standardized Index.
+    xclim.indices.stats.standardized_index_fit_params : Standardized Index Fit Params.
+
+    Notes
+    -----
+    * N-month SGI / N-day SGI is determined by choosing the `window = N` and the appropriate frequency `freq`.
+    * Supported statistical distributions are: ["gamma", "genextreme", "lognorm"]
+    * If `params` is given as input, it overrides the `cal_start`, `cal_end`, `freq` and `window`, `dist` and `method` options.
+    * "APP" method only supports two-parameter distributions. Parameter `loc` needs to be fixed to use method `APP`.
+
+    References
+    ----------
+    :cite:cts:`bloomfield_2013`
+
+    Examples
+    --------
+    >>> from datetime import datetime
+    >>> from xclim.indices import standardized_groundwater_index
+    >>> ds = xr.open_dataset(path_to_gwl_file)
path: src/xclim/indices/_hydrology.py
user: login: coxipi
body: this would need a xclim-testdata

same in the tests, I just used false data
created_at: 2025-02-13T06:10:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2007,3 +2007,10 @@ def _merge_attrs(dbase, dextra, attr, sep):
             load_locale(loc_dict, locale)
 
     return mod
+
+
+class StandardizedIndexes(ResamplingIndicator):
+    """Resampling but flexible inputs indicators."""
+
+    src_freq = ["D", "MS"]
+    context = "hydro"
path: src/xclim/core/indicator.py
user: login: coxipi
body: @aulemahal any opposition to this reorginazation? Previously I defined it in _precip, but now it's also needed in _hydrology
created_at: 2025-02-13T18:07:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1198,7 +1198,7 @@ def standardized_precipitation_index(
     >>> from xclim.indices import standardized_precipitation_index
     >>> ds = xr.open_dataset(path_to_pr_file)
     >>> pr = ds.pr
-    >>> cal_start, cal_end = "1990-05-01", "1990-08-31"
+    >>> cal_start, cal_end = "1990-05-01", "1992-06-01"
path: src/xclim/indices/_agro.py
user: login: coxipi
body: ```suggestion
    >>> cal_start, cal_end = "1990-05-01", "1990-08-31"
```
created_at: 2025-02-13T18:08:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,6 +114,136 @@ def rb_flashiness_index(q: xr.DataArray, freq: str = "YS") -> xr.DataArray:
     return out
 
 
+@declare_units(
+    q="[discharge]",
+    params="[]",
+)
+def standardized_streamflow_index(
+    q: xr.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str | rv_continuous = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xr.DataArray:
+    r"""
+    Standardized Streamflow Index (SSI).
+
+    Parameters
+    ----------
+    q : xarray.DataArray
+        Rate of river discharge.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
+        has already been applied input dataset and will skip the resampling step.
path: src/xclim/indices/_hydrology.py
user: login: huard
body: ```suggestion
        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that the desired resampling
        has already been applied to the input dataset and will skip the resampling step.
```
created_at: 2025-02-26T16:30:22Z
author_association: COLLABORATOR
diff_hunk: @@ -109,6 +114,136 @@ def rb_flashiness_index(q: xr.DataArray, freq: str = "YS") -> xr.DataArray:
     return out
 
 
+@declare_units(
+    q="[discharge]",
+    params="[]",
+)
+def standardized_streamflow_index(
+    q: xr.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str | rv_continuous = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xr.DataArray:
+    r"""
+    Standardized Streamflow Index (SSI).
+
+    Parameters
+    ----------
+    q : xarray.DataArray
+        Rate of river discharge.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
+        has already been applied input dataset and will skip the resampling step.
+    window : int
+        Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
+        i.e. a monthly resampling, the window is an integer number of months.
+    dist : {"genextreme", "fisk"} or `rv_continuous` function
+        Name of the univariate distribution, or a callable `rv_continuous` (see :py:mod:`scipy.stats`).
+    method : {"APP", "ML", "PWM"}
+        Name of the fitting method, such as `ML` (maximum likelihood), `APP` (approximate). The approximate method
+        uses a deterministic function that does not involve any optimization.
+        `PWM` should be used with a `lmoments3` distribution.
+    fitkwargs : dict, optional
+        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certains parameters (`floc`, `fscale`).
path: src/xclim/indices/_hydrology.py
user: login: huard
body: ```suggestion
        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certain parameters (`floc`, `fscale`).
```
created_at: 2025-02-26T16:31:39Z
author_association: COLLABORATOR
diff_hunk: @@ -284,6 +419,138 @@ def melt_and_precip_max(snw: xr.DataArray, pr: xr.DataArray, window: int = 3, fr
     return out
 
 
+@declare_units(
+    gwl="[length]",
+    params="[]",
+)
+def standardized_groundwater_index(
+    gwl: xr.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str | rv_continuous = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xr.DataArray:
+    r"""
+    Standardized Groundwater Index (SGI).
+
+    Parameters
+    ----------
+    gwl : xarray.DataArray
+        Groundwater head level.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
+        has already been applied input dataset and will skip the resampling step.
path: src/xclim/indices/_hydrology.py
user: login: huard
body: ```suggestion
        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that the desired resampling
        has already been applied to the input dataset and will skip the resampling step.
```
created_at: 2025-02-26T16:35:42Z
author_association: COLLABORATOR
diff_hunk: @@ -284,6 +419,138 @@ def melt_and_precip_max(snw: xr.DataArray, pr: xr.DataArray, window: int = 3, fr
     return out
 
 
+@declare_units(
+    gwl="[length]",
+    params="[]",
+)
+def standardized_groundwater_index(
+    gwl: xr.DataArray,
+    freq: str | None = "MS",
+    window: int = 1,
+    dist: str | rv_continuous = "genextreme",
+    method: str = "ML",
+    fitkwargs: dict | None = None,
+    cal_start: DateStr | None = None,
+    cal_end: DateStr | None = None,
+    params: Quantified | None = None,
+    **indexer,
+) -> xr.DataArray:
+    r"""
+    Standardized Groundwater Index (SGI).
+
+    Parameters
+    ----------
+    gwl : xarray.DataArray
+        Groundwater head level.
+    freq : str, optional
+        Resampling frequency. A monthly or daily frequency is expected. Option `None` assumes that desired resampling
+        has already been applied input dataset and will skip the resampling step.
+    window : int
+        Averaging window length relative to the resampling frequency. For example, if `freq="MS"`,
+        i.e. a monthly resampling, the window is an integer number of months.
+    dist : {"gamma", "genextreme", "lognorm"} or `rv_continuous`
+        Name of the univariate distribution, or a callable `rv_continuous` (see :py:mod:`scipy.stats`).
+    method : {"APP", "ML", "PWM"}
+        Name of the fitting method, such as `ML` (maximum likelihood), `APP` (approximate).
+        The approximate method uses a deterministic function that does not involve any optimization.
+        `PWM` should be used with a `lmoments3` distribution.
+    fitkwargs : dict, optional
+        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certains parameters (`floc`, `fscale`).
path: src/xclim/indices/_hydrology.py
user: login: huard
body: ```suggestion
        Kwargs passed to ``xclim.indices.stats.fit`` used to impose values of certain parameters (`floc`, `fscale`).
```
created_at: 2025-02-26T16:36:08Z
author_association: COLLABORATOR
diff_hunk: @@ -813,11 +831,17 @@ def standardized_index_fit_params(
     if method == "APP":
         if "floc" not in fitkwargs.keys():
             raise ValueError(
-                "The APP method is only supported for two-parameter distributions with `gamma` or `fisk` "
-                "with `loc` being fixed. Pass a value for `floc` in `fitkwargs`."
+                "The APP method is only supported for two-parameter distributions with `gamma`, `fisk`, "
+                "`lognorm`, or `genextreme` with `loc` being fixed. Pass a value for `floc` in `fitkwargs`."
             )
 
-    dist_and_methods = {"gamma": ["ML", "APP"], "fisk": ["ML", "APP"]}
+    # "WPM" method doesn't seem to work for gamma or pearson3
+    dist_and_methods = {
+        "gamma": ["ML", "APP"],
+        "fisk": ["ML", "APP"],
+        "genextreme": ["ML", "APP"],
+        "lognorm": ["ML", "APP"],
+    }
path: src/xclim/indices/stats.py
user: login: huard
body: Do you mean PWM ? Why add genextreme and lognorm here ?
created_at: 2025-02-26T16:38:49Z
author_association: COLLABORATOR
diff_hunk: @@ -23,6 +28,37 @@ def test_simple(self, q_series):
         np.testing.assert_array_equal(out, 2)
 
 
+class TestStandardizedStreamflow:
+    @pytest.mark.slow
+    def test_3d_data_with_nans(self, open_dataset):
+        nc_ds = Path("Raven", "q_sim.nc")
+        # test with data
+        ds = open_dataset(nc_ds)
+        q = ds.q_obs.sel(time=slice("2008")).rename("q")
+        qMM = convert_units_to(q, "mm**3/s", context="hydro")  # noqa
+        # put a nan somewhere
+        qMM.values[10] = np.nan
+        q.values[10] = np.nan
+
+        out1 = land.standardized_streamflow_index(
+            q,
+            freq="MS",
+            window=1,
+            dist="genextreme",
+            method="APP",
+            fitkwargs={"floc": 0},
+        )
+        out2 = land.standardized_streamflow_index(
+            qMM,
+            freq="MS",
+            window=1,
+            dist="genextreme",
+            method="APP",
+            fitkwargs={"floc": 0},
+        )
+        np.testing.assert_array_almost_equal(out1, out2, 3)
path: tests/test_hydrology.py
user: login: huard
body: Not sure I understand what is checked here, beyond unit changes. 
created_at: 2025-02-26T16:41:03Z
author_association: COLLABORATOR
diff_hunk: @@ -23,6 +28,37 @@ def test_simple(self, q_series):
         np.testing.assert_array_equal(out, 2)
 
 
+class TestStandardizedStreamflow:
+    @pytest.mark.slow
+    def test_3d_data_with_nans(self, open_dataset):
+        nc_ds = Path("Raven", "q_sim.nc")
+        # test with data
+        ds = open_dataset(nc_ds)
+        q = ds.q_obs.sel(time=slice("2008")).rename("q")
+        qMM = convert_units_to(q, "mm**3/s", context="hydro")  # noqa
+        # put a nan somewhere
+        qMM.values[10] = np.nan
+        q.values[10] = np.nan
+
+        out1 = land.standardized_streamflow_index(
+            q,
+            freq="MS",
+            window=1,
+            dist="genextreme",
+            method="APP",
+            fitkwargs={"floc": 0},
+        )
+        out2 = land.standardized_streamflow_index(
+            qMM,
+            freq="MS",
+            window=1,
+            dist="genextreme",
+            method="APP",
+            fitkwargs={"floc": 0},
+        )
+        np.testing.assert_array_almost_equal(out1, out2, 3)
path: tests/test_hydrology.py
user: login: coxipi
body: I didn't write the test, Adrien did. But looking at xclim code, this is how many `test_3d_data_with_nans` start. In this case, there is no nan at the end to check specifically if a nan is kept in place, because with the monthly resampling there is no more nan, so it becomes a bit dull indeed. I added another test with a daily computation with a nan insertion, and check that the SSI is a nan as expected.

The values of the output are already tested in `test_indices`
created_at: 2025-03-11T13:44:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -813,11 +831,17 @@ def standardized_index_fit_params(
     if method == "APP":
         if "floc" not in fitkwargs.keys():
             raise ValueError(
-                "The APP method is only supported for two-parameter distributions with `gamma` or `fisk` "
-                "with `loc` being fixed. Pass a value for `floc` in `fitkwargs`."
+                "The APP method is only supported for two-parameter distributions with `gamma`, `fisk`, "
+                "`lognorm`, or `genextreme` with `loc` being fixed. Pass a value for `floc` in `fitkwargs`."
             )
 
-    dist_and_methods = {"gamma": ["ML", "APP"], "fisk": ["ML", "APP"]}
+    # "WPM" method doesn't seem to work for gamma or pearson3
+    dist_and_methods = {
+        "gamma": ["ML", "APP"],
+        "fisk": ["ML", "APP"],
+        "genextreme": ["ML", "APP"],
+        "lognorm": ["ML", "APP"],
+    }
path: src/xclim/indices/stats.py
user: login: coxipi
body: `genextreme` is among the suggest choices for Standardized Streamflow index (SSI): https://agupubs.onlinelibrary.wiley.com/doi/10.1029/2019WR026315 . `lognormal` is suggested for SGI: https://hess.copernicus.org/articles/17/4769/2013/

For the method PWM, no restriction is placed on the choice of the distribution, that's why it doesn't appear here. You just have to supply a lmoments3 distribution. Since we don't support officially lmoments3, I thought we might as well not restrict the use of PWM with lmoments3, we give no insurance quality, contrary to the other distributions: those combinations  of distribution/method above  work with scipy and have been tested for standardized indices computations.


created_at: 2025-03-11T13:51:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -611,6 +612,8 @@ def open_dataset(
     --------
     xarray.open_dataset : Open and read a dataset from a file or file-like object.
     """
+    branch = "v2025.3.11"
+
path: src/xclim/testing/utils.py
user: login: Zeitsperre
body: ```suggestion
```
created_at: 2025-03-12T17:17:36Z
author_association: COLLABORATOR
diff_hunk: @@ -520,6 +520,7 @@ def nimbus(  # noqa: PR01
         example_file = nimbus().fetch("example.nc")
         data = xr.open_dataset(example_file)
     """
+    branch = "v2025.3.11"
path: src/xclim/testing/utils.py
user: login: Zeitsperre
body: ```suggestion
```
created_at: 2025-03-12T17:17:45Z
author_association: COLLABORATOR
diff_hunk: @@ -611,6 +612,8 @@ def open_dataset(
     --------
     xarray.open_dataset : Open and read a dataset from a file or file-like object.
     """
+    branch = "v2025.3.11"
+
path: src/xclim/testing/utils.py
user: login: coxipi
body: oups, I thought I had remove all this troubleshooting before I fixed regristry, thanks
created_at: 2025-03-12T20:20:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +30,11 @@ variables:
     standard_name: water_potential_evapotranspiration_flux
     data_flags:
       - negative_accumulation_values:
+  gwl:
+    canonical_units: m
+    description: Groundwater level.
+    dimensions: "[length]"
+
path: src/xclim/data/variables.yml
user: login: Zeitsperre
body: ```suggestion
```
created_at: 2025-03-13T17:33:53Z
author_association: COLLABORATOR

PR comments:
user: login: LamAdr
created_at: 2024-08-23T20:44:24Z
body: Where things stand
SSI is completed
SGI
* needs tests (xclim-testdata lacks groundwater data. Maybe we can get some [here](https://www.environnement.gouv.qc.ca/eau/piezo/).)
* I am not sure about the APP for lognorm
author_association: CONTRIBUTOR
user: login: Zeitsperre
created_at: 2024-08-23T21:06:47Z
body: Just an FYI, but some tests are expected to fail until #1889 is merged!
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-02-13T06:09:57Z
body: > [!WARNING]
> This Pull Request is coming from a fork and must be manually tagged `approved` in order to perform additional testing.
author_association: NONE
user: login: coxipi
created_at: 2025-02-13T17:18:34Z
body: > * I am not sure about the APP for lognorm

It was exactly the MLE with saddle point estimators, good job
author_association: CONTRIBUTOR
user: login: coxipi
created_at: 2025-02-13T17:28:41Z
body: * I can't seem to resolve the coveralls issue
* still no groundwater testdata
* I can't be the reviewer on this anymore
author_association: CONTRIBUTOR
user: login: Zeitsperre
created_at: 2025-02-13T17:36:22Z
body: @coxipi 

Ignore the Coveralls problem for now (I think their service is coming back online gradually). I've removed that as a required check for the time being.

I think we could safely add `gwl` data to the `xclim-testdata` repository. Something no bigger than 250 KB would be fine.

For reviewing, I don't mind jumping in, but I think @huard would be better. 
author_association: COLLABORATOR
user: login: coxipi
created_at: 2025-03-11T13:30:15Z
body: Sorry, I didn't see you had reviewed the PR @huard . I will address your comments now.
author_association: CONTRIBUTOR
user: login: coxipi
created_at: 2025-03-11T13:59:02Z
body: > I just want to make sure this PR is not impeding the use of PWM for genextreme and gamma.

On the contrary, PWM is allowed with any distribution found in lmoments3. It doesn't appear in the `dist_and_methods` constrained pairs because it's not constrained at all

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-12T16:01:18Z
body: > [!NOTE]
> Workflow changes in this Pull Request have been approved!
> [!NOTE]
> Workflow changes in this Pull Request have been approved!
> [!NOTE]
> Workflow changes in this Pull Request have been approved!
> [!NOTE]
> Workflow changes in this Pull Request have been approved!
> [!NOTE]
> Workflow changes in this Pull Request have been approved!
> [!NOTE]
> Workflow changes in this Pull Request have been approved!
> [!NOTE]
> Workflow changes in this Pull Request have been approved!
> [!NOTE]
> Workflow changes in this Pull Request have been approved!
> [!NOTE]
> Workflow changes in this Pull Request have been approved!
author_association: NONE
user: login: coxipi
created_at: 2025-03-12T16:10:00Z
body: Once tests pass here, I will merge
author_association: CONTRIBUTOR
user: login: coxipi
created_at: 2025-03-12T20:21:41Z
body: Btw, I don't know why the tests fail here on GH, on my machine it's fine, I need to check what is going on
author_association: CONTRIBUTOR
user: login: coxipi
created_at: 2025-03-12T20:22:28Z
body: > These changes shouldn't be needed.

I thought I had removed this, thanks
author_association: CONTRIBUTOR
user: login: coxipi
created_at: 2025-03-13T17:50:49Z
body: yes, we were speaking of triggering tests too often. Removing an inconsequential white space should not need to run new tests, but drawing the line automatically is probably complicated
author_association: CONTRIBUTOR
user: login: Zeitsperre
created_at: 2025-03-13T18:11:28Z
body: > Removing an inconsequential white space should not need to run new tests, but drawing the line automatically is probably complicated

The issue is that before changes are merged to `main`, the tests need to be fully run. You can skip running the tests by prepending `[skip ci]` or `[no ci]` to your commit message, but we often don't do that.
author_association: COLLABORATOR
user: login: coxipi
created_at: 2025-03-13T18:14:49Z
body: >  You can skip running the tests by prepending [skip ci] or [no ci] to your commit message, but we often don't do that.

I'll try this next time, nice
author_association: CONTRIBUTOR