5. Analyses fréquentielles locales

[1]:
# Basic imports
import hvplot.xarray  # noqa
import numpy as np
import xarray as xr
import xdatasets

import xhydro as xh
import xhydro.frequency_analysis as xhfa
ERROR 1: PROJ: proj_create_from_database: Open of /home/docs/checkouts/readthedocs.org/user_builds/xhydro-fr/conda/latest/share/proj failed
Redefining 'percent' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
Redefining '%' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
Redefining 'year' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
Redefining 'yr' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
Redefining 'C' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
Redefining 'd' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
Redefining 'h' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
Redefining 'degrees_north' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
Redefining 'degrees_east' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
Redefining 'degrees' (<class 'pint.delegates.txt_defparser.plain.UnitDefinition'>)
Redefining '[speed]' (<class 'pint.delegates.txt_defparser.plain.DerivedDimensionDefinition'>)

5.1. Extraction et préparation des données

Dans cet exemple, nous effectuerons une analyse fréquentielle à l’aide de données historiques pises à partir de divers sites. La première étape consiste à acquérir un jeu de données hydrologiques. Pour ce faire, nous utilisons la librairie xdatasets pour récupérer des données hydrologiques du Ministère de l’Environnement, de la Lutte contre les changements climatiques, de la Faune et des Parcs au Québec, Canada. Notre requête se concentrera sur les stations ayant des identifiants commençant par 020, et spécifiquement celles ayant un régime de débit naturel. Certaines stations disposent de données sur les niveaux d’eau, mais nous ne traiterons que les données de débit.

Alternativement, les utilisateurs peuvent fournir leur propre xarray.DataArray. Lors de la préparation des données pour l’analyse fréquentielle, celles-ci doivent respecter les exigences suivantes :

  • Le jeu de données doit inclure une dimension time.

  • Si une dimension spatiale 1D existe (par exemple, id dans l’exemple ci-dessous), elle doit contenir un attribut cf_role avec la valeur timeseries_id.

  • La variable doit au moins avoir un attribut units. Bien que d’autres attributs, comme long_name et cell_methods, ne soient pas strictement requis, ils sont attendus par xclim, qui est invoqué lors de l’analyse fréquentielle. L’absence de ces attributs entraînera des avertissements.

[2]:
ds = (
    xdatasets.Query(
        **{
            "datasets": {
                "deh": {
                    "id": ["020*"],
                    "regulated": ["Natural"],
                    "variables": ["streamflow"],
                }
            },
            "time": {"start": "1970-01-01", "minimum_duration": (15 * 365, "d")},
        }
    )
    .data.squeeze()
    .load()
)

# This dataset lacks some of the aforementioned attributes, so we need to add them.
ds["id"].attrs["cf_role"] = "timeseries_id"
ds["streamflow"].attrs = {
    "long_name": "Streamflow",
    "units": "m3 s-1",
    "standard_name": "water_volume_transport_in_river_channel",
    "cell_methods": "time: mean",
}

# Clean some of the coordinates that are not needed for this example
ds = ds.drop_vars([c for c in ds.coords if c not in ["id", "time", "name"]])

# For the purpose of this example, we keep only 2 stations
ds = ds.isel(id=slice(3, 5))
ds
[2]:
<xarray.Dataset> Size: 327kB
Dimensions:     (id: 2, time: 20454)
Coordinates:
  * id          (id) object 16B '020602' '020802'
    name        (id) object 16B 'Dartmouth' 'Madeleine'
  * time        (time) datetime64[ns] 164kB 1970-01-01 1970-01-02 ... 2025-12-31
Data variables:
    streamflow  (id, time) float32 164kB nan nan nan nan nan ... nan nan nan nan
[3]:
ds.streamflow.dropna("time", how="all").hvplot(
    x="time", grid=True, widget_location="bottom", groupby="id"
)
[3]:

5.2. Acquisition des maxima par blocs

La fonction xhydro.indicators.get_yearly_op peut être utilisée pour extraire les maxima par blocs à partir d’une série chronologique. Cette fonction offre plusieurs options pour personnaliser le processus d’extraction, comme le choix des périodes temporelles et plus encore. Dans la section suivante, nous présenterons les principaux arguments disponibles pour vous aider à adapter l’extraction des maxima de blocs à vos besoins.

Help on function get_yearly_op in module xhydro.indicators.generic:

get_yearly_op(ds, op, *, input_var: str = 'streamflow', window: int = 1, timeargs: dict | None = None, missing: str = 'skip', missing_options: dict | None = None, interpolate_na: bool = False) -> xarray.core.dataset.Dataset
    Compute yearly operations on a variable.

    Parameters
    ----------
    ds : xr.Dataset
        Dataset containing the variable to compute the operation on.
    op : str
        Operation to compute. One of ["max", "min", "mean", "sum"].
    input_var : str
        Name of the input variable. Defaults to "streamflow".
    window : int
        Size of the rolling window. A "mean" operation is performed on the rolling window before the call to xclim.
        This parameter cannot be used with the "sum" operation.
    timeargs : dict, optional
        Dictionary of time arguments for the operation.
        Keys are the name of the period that will be added to the results (e.g. "winter", "summer", "annual").
        Values are up to two dictionaries, with both being optional.
        The first is {'freq': str}, where str is a frequency supported by xarray (e.g. "YS", "YS-JAN", "YS-DEC").
        It needs to be a yearly frequency. Defaults to "YS-JAN".
        The second is an indexer as supported by :py:func:`xclim.core.calendar.select_time`.
        Defaults to {}, which means the whole year.
        See :py:func:`xclim.core.calendar.select_time` for more information.
        Examples: {"winter": {"freq": "YS-DEC", "date_bounds": ["12-01", "02-28"]}}, {"jan": {"freq": "YS", "month": 1}}, {"annual": {}}.
    missing : str
        How to handle missing values. One of "skip", "any", "at_least_n", "pct", "wmo".
        See :py:func:`xclim.core.missing` for more information.
    missing_options : dict, optional
        Dictionary of options for the missing values' method. See :py:func:`xclim.core.missing` for more information.
    interpolate_na : bool
        Whether to interpolate missing values before computing the operation. Only used with the "sum" operation.
        Defaults to False.

    Returns
    -------
    xr.Dataset
        Dataset containing the computed operations, with one variable per indexer.
        The name of the variable follows the pattern `{input_var}{window}_{op}_{indexer}`.

    Notes
    -----
    If you want to perform a frequency analysis on a frequency that is finer than annual, simply use multiple timeargs
    (e.g. 1 per month) to create multiple distinct variables.

5.2.1. a) Définir les saisons

Les saisons peuvent être définies à l’aide d’indexeurs compatibles avec xclim.core.calendar.select_time. Quatre types d’indexeurs sont actuellement soutenus :

  • month : Une séquence de numéros de mois (par exemple, [1, 2, 12] pour janvier, février et décembre).

  • season : Une séquence d’abréviations de saisons, les options étant 'DJF', 'MAM', 'JJA', et 'SON' (représentant l’hiver, le printemps, l’été et l’automne, respectivement).

  • doy_bounds : Une séquence spécifiant les limites inclusives de la période en termes de jour de l’année (par exemple, [152, 243]).

  • date_bounds : Semblable à doy_bounds, mais utilisant un format mois-jour ('%m-%d'), tel que ["01-15", "02-23"].

Lors de l’utilisation de xhydro.indicators.get_yearly_op pour calculer les maxima par blocs, les indexeurs doivent être regroupés dans un dictionnaire et passés à l’argument timeargs. Les clés du dictionnaire doivent représenter la période demandée (par exemple, 'winter', 'summer') et seront ajoutées au nom de la variable. Chaque entrée du dictionnaire peut inclure ce qui suit :

  • L’indexeur, tel que défini ci-dessus (par exemple, "date_bounds": ["02-11", "06-19"]).

  • (Facultatif) Une fréquence de rééchantillonnage annuelle. Cela est principalement utilisé pour les indexeurs qui couvrent l’année. Par exemple, en définissant "freq": "YS-DEC", l’année commencera en décembre au lieu de janvier.

[5]:
# Some examples
timeargs = {
    "spring": {"date_bounds": ["02-11", "06-19"]},
    "summer": {"doy_bounds": [152, 243]},
    "fall": {"month": [9, 10, 11]},
    "winter": {
        "season": ["DJF"],
        "freq": "YS-DEC",
    },  # To correctly wrap around the year, we need to specify the resampling frequency.
    "august": {"month": [8]},
    "annual": {},
}

5.2.2. b) Définir les critères pour les valeurs manquantes

La fonction xhydro.indicators.get_yearly_op inclut également deux arguments, missing et missing_options, qui permettent de définir des tolérances pour les données manquantes. Ces arguments utilisent xclim pour gérer les valeurs manquantes, et les options disponibles sont détaillées dans la documentation xclim.

[6]:
import xclim

print(xclim.core.missing.__doc__)

Missing Values Identification
=============================

Indicators may use different criteria to determine whether a computed indicator value should be
considered missing. In some cases, the presence of any missing value in the input time series should result in a
missing indicator value for that period. In other cases, a minimum number of valid values or a percentage of missing
values should be enforced. The World Meteorological Organisation (WMO) suggests criteria based on the number of
consecutive and overall missing values per month.

`xclim` has a registry of missing value detection algorithms that can be extended by users to customize the behavior
of indicators. Once registered, algorithms can be used within indicators by setting the `missing` attribute of an
`Indicator` subclass. By default, `xclim` registers the following algorithms:

 * `any`: A result is missing if any input value is missing.
 * `at_least_n`: A result is missing if less than a given number of valid values are present.
 * `pct`: A result is missing if more than a given fraction of values are missing.
 * `wmo`: A result is missing if 11 days are missing, or 5 consecutive values are missing in a month.
 * `skip`: Skip missing value detection.
 * `from_context`: Look-up the missing value algorithm from options settings. See :py:func:`xclim.set_options`.

To define another missing value algorithm, subclass :py:class:`MissingBase` and decorate it with
:py:func:`xclim.core.options.register_missing_method`.

5.2.3. c) Exemple simple

Commençons par un exemple simple en utilisant le dictionnaire timeargs défini précédemment. Dans ce cas, nous allons définir une tolérance où un maximum de 15 % de données manquantes dans une année sera considéré comme acceptable pour que l’année soit considérée valide.

[7]:
ds_4fa = xh.indicators.get_yearly_op(
    ds, op="max", timeargs=timeargs, missing="pct", missing_options={"tolerance": 0.15}
)

ds_4fa
[7]:
<xarray.Dataset> Size: 3kB
Dimensions:                (id: 2, time: 56)
Coordinates:
  * id                     (id) object 16B '020602' '020802'
    name                   (id) object 16B 'Dartmouth' 'Madeleine'
  * time                   (time) datetime64[ns] 448B 1970-01-01 ... 2025-01-01
Data variables:
    streamflow_max_spring  (id, time) float32 448B nan 241.0 223.0 ... nan nan
    streamflow_max_summer  (id, time) float32 448B nan 27.2 123.0 ... nan nan
    streamflow_max_fall    (id, time) float32 448B nan 11.6 18.2 ... nan nan nan
    streamflow_max_august  (id, time) float32 448B nan 12.1 21.9 ... nan nan nan
    streamflow_max_annual  (id, time) float32 448B nan 241.0 223.0 ... nan nan
    streamflow_max_winter  (id, time) float32 448B 8.5 5.52 3.48 ... nan nan nan
Attributes:
    cat:frequency:         yr
    cat:processing_level:  indicators
    cat:id:                
[8]:
ds_4fa.streamflow_max_summer.dropna("time", how="all").hvplot(
    x="time", grid=True, widget_location="bottom", groupby="id"
)
[8]:

5.2.4. d) Exemple avancé : Utiliser des saisons personnalisées par année ou par station

La personnalisation des plages de dates pour chaque année ou chaque station n’est pas directement prise en charge par xhydro.indicators.get_yearly_op. Cependant, les utilisateurs peuvent contourner cette limitation en masquant leurs données avant d’appeler la fonction. Lors de l’application de cette approche, assurez-vous d’ajuster l’argument missing pour tenir compte des modifications dans la disponibilité des données.

Dans cet exemple, nous définirons une saison qui commence à une date aléatoire en avril et se termine à une date aléatoire en juin. Comme nous allons masquer presque toute l’année, la tolérance pour les données manquantes doit être ajustée en conséquence. Au lieu de définir une tolérance générale pour les données manquantes, nous utiliserons la méthode at_least_n pour spécifier qu’au moins 45 jours de données doivent être disponibles pour que la période soit considérée valide.

[9]:
nyears = np.unique(ds.time.dt.year).size
dom_start = xr.DataArray(
    np.random.randint(1, 30, size=(nyears,)).astype("str"),
    dims=("year"),
    coords={"year": np.unique(ds.time.dt.year)},
)
dom_end = xr.DataArray(
    np.random.randint(1, 30, size=(nyears,)).astype("str"),
    dims=("year"),
    coords={"year": np.unique(ds.time.dt.year)},
)

mask = xr.zeros_like(ds["streamflow"])
for y in dom_start.year.values:
    # Random mask of dates per year, between April and June.
    mask.loc[
        {
            "time": slice(
                str(y) + "-04-" + str(dom_start.sel(year=y).item()),
                str(y) + "-06-" + str(dom_end.sel(year=y).item()),
            )
        }
    ] = 1
[10]:
mask.hvplot(x="time", grid=True, widget_location="bottom", groupby="id")
[10]:
[11]:
# The name of the indexer will be used to identify the variable created here
timeargs_custom = {"custom": {}}

# We use .where() to mask the data that we want to ignore
masked = ds.where(mask == 1)
# Since we masked almost all the year, our tolerance for missing data should be changed accordingly
missing = "at_least_n"
missing_options = {"n": 45}

# We use xr.merge() to combine the results with the previous dataset.
ds_4fa = xr.merge(
    [
        ds_4fa,
        xh.indicators.get_yearly_op(
            masked,
            op="max",
            timeargs=timeargs_custom,
            missing=missing,
            missing_options=missing_options,
        ),
    ]
)
[12]:
ds_4fa.streamflow_max_custom.dropna("time", how="all").hvplot(
    x="time", grid=True, widget_location="bottom", groupby="id"
)
[12]:

5.2.5. e) Variable alternative : Calcul des volumes

L’analyse fréquentielle peut également être appliquée aux volumes, suivant un processus similaire à celui des données de débit. La principale différence est que si nous commençons avec des données de débit, nous devons d’abord les convertir en volumes en utilisant xhydro.indicators.compute_volume (par exemple, passer de m3 s-1 à m3). De plus, si nécessaire, la fonction get_yearly_op inclut un argument, interpolate_na, qui peut être utilisé pour interpoler les données manquantes avant de calculer la somme.

[13]:
# Get a daily volume from a daily streamflow
ds["volume"] = xh.indicators.compute_volume(ds["streamflow"], out_units="hm3")

# We'll take slightly different indexers
timeargs_vol = {"spring": {"date_bounds": ["04-30", "06-15"]}, "annual": {}}

# The operation that we want here is the sum, not the max.
ds_4fa = xr.merge(
    [
        ds_4fa,
        xh.indicators.get_yearly_op(
            ds,
            op="sum",
            input_var="volume",
            timeargs=timeargs_vol,
            missing="pct",
            missing_options={"tolerance": 0.15},
            interpolate_na=True,
        ),
    ]
)
[14]:
ds_4fa.volume_sum_spring.dropna("time", how="all").hvplot(
    x="time", grid=True, widget_location="bottom", groupby="id"
)
[14]:

5.3. Analyse fréquentielle locale

Après avoir extrait les données brutes, telles que les maximums ou minimums annuels, l’analyse fréquentielle locale est réalisée en trois étapes :

  1. Utiliser xhydro.frequency_analysis.local.fit pour déterminer le meilleur ensemble de paramètres pour un nombre donné de distributions statistiques.

  2. (Facultatif) Utiliser xhydro.frequency_analysis.local.criteria pour calculer les critères de qualité d’ajustement et évaluer à quel point chaque distribution statistique correspond bien aux données.

  3. Utiliser xhydro.frequency_analysis.local.parametric_quantiles pour calculer les niveaux de retour basés sur les paramètres ajustés.

Pour accélérer le Notebook, nous effectuerons l’analyse uniquement sur un sous-ensemble de variables.

[15]:
Help on function fit in module xhydro.frequency_analysis.local:

fit(ds, *, distributions: list[str] | None = None, min_years: int | None = None, method: str = 'ML', periods: list[str] | list[list[str]] | None = None) -> xarray.core.dataset.Dataset
    Fit multiple distributions to data.

    Parameters
    ----------
    ds : xr.Dataset
        Dataset containing the data to fit. All variables will be fitted.
    distributions : list of str, optional
        List of distribution names as defined in `scipy.stats`. See https://docs.scipy.org/doc/scipy/reference/stats.html#continuous-distributions.
        Defaults to ["genextreme", "pearson3", "gumbel_r", "expon"].
    min_years : int, optional
        Minimum number of years required for a distribution to be fitted.
    method : str
        Fitting method. Defaults to "ML" (maximum likelihood).
    periods : list of str or list of list of str, optional
        Either [start, end] or list of [start, end] of periods to be considered.
        If multiple periods are given, the output will have a `horizon` dimension.
        If None, all data is used.

    Returns
    -------
    xr.Dataset
        Dataset containing the parameters of the fitted distributions, with a new dimension `scipy_dist` containing the distribution names.

    Notes
    -----
    In order to combine the parameters of multiple distributions, the size of the `dparams` dimension is set to the
    maximum number of unique parameters between the distributions.

La fonction fit permet d’ajuster simultanément plusieurs distributions statistiques, telles que ["genextreme", "pearson3", "gumbel_r", "expon"]. Comme différentes distributions ont des ensembles de paramètres variables (et parfois des conventions de nommage différentes), xHydro gère cette complexité en utilisant une dimension dparams et en remplissant avec des valeurs NaN là où nécessaire. Lorsque les résultats interagissent avec SciPy, comme la fonction parametric_quantiles, seuls les paramètres pertinents pour chaque distribution sont passés. Les distributions sélectionnées sont stockées dans une nouvelle dimension scipy_dist.

[16]:
params = xhfa.local.fit(
    ds_4fa[["streamflow_max_spring", "volume_sum_spring"]], min_years=15
)

params
[16]:
<xarray.Dataset> Size: 820B
Dimensions:                (scipy_dist: 4, id: 2, dparams: 4)
Coordinates:
    horizon                <U9 36B '1970-2025'
  * id                     (id) object 16B '020602' '020802'
  * dparams                (dparams) <U5 80B 'c' 'skew' 'loc' 'scale'
  * scipy_dist             (scipy_dist) <U10 160B 'genextreme' ... 'expon'
    name                   (id) object 16B dask.array<chunksize=(2,), meta=np.ndarray>
Data variables:
    streamflow_max_spring  (scipy_dist, id, dparams) float64 256B dask.array<chunksize=(1, 2, 4), meta=np.ndarray>
    volume_sum_spring      (scipy_dist, id, dparams) float64 256B dask.array<chunksize=(1, 2, 4), meta=np.ndarray>
Attributes:
    cat:frequency:         yr
    cat:processing_level:  indicators
    cat:id:                

Les critères tels que l’AIC (Critère d’Information d’Akaike), le BIC (Critère Bayésien d’Information), et l’AICC (AIC corrigé) sont des outils précieux pour comparer l’ajustement de différents modèles statistiques. Ces critères équilibrent la qualité d’ajustement d’un modèle avec sa complexité, aidant à éviter l’overfitting. L’AIC et l’AICC sont particulièrement utiles pour comparer des modèles avec des nombres de paramètres différents, tandis que le BIC a tendance à pénaliser plus sévèrement la complexité, ce qui le rend plus conservateur. Des valeurs plus faibles de ces critères indiquent de meilleures performances du modèle, l’AICC étant particulièrement utile pour les petits échantillons. En utilisant ces métriques, nous pouvons déterminer objectivement le modèle le plus approprié pour nos données.

Ces trois critères peuvent être consultés en utilisant xhydro.frequency_analysis.local.criteria. Les résultats sont ajoutés à une nouvelle dimension criterion. Dans cet exemple, l’AIC, le BIC et l’AICC donnent tous une faible indication que la distribution Generalized Extreme Value (GEV) est la meilleure correspondance pour les données, bien que la distribution de Gumbel puisse également être un choix approprié. En revanche, le Pearson III n’a pas convergé et la distribution exponentielle a été rejetée en fonction de ces critères, ce qui suggère qu’elles ne correspondent pas correctement aux données.

[17]:
Help on function criteria in module xhydro.frequency_analysis.local:

criteria(ds: xarray.core.dataset.Dataset, p: xarray.core.dataset.Dataset) -> xarray.core.dataset.Dataset
    Compute information criteria (AIC, BIC, AICC) from fitted distributions, using the log-likelihood.

    Parameters
    ----------
    ds : xr.Dataset
        Dataset containing the yearly data that was fitted.
    p : xr.Dataset
        Dataset containing the parameters of the fitted distributions.
        Must have a dimension `dparams` containing the parameter names and a dimension `scipy_dist` containing the distribution names.

    Returns
    -------
    xr.Dataset
        Dataset containing the information criteria for the distributions.

[18]:
criteria = xhfa.local.criteria(
    ds_4fa[["streamflow_max_spring", "volume_sum_spring"]], params
)

criteria["streamflow_max_spring"].isel(id=0).compute()
[18]:
<xarray.DataArray 'streamflow_max_spring' (scipy_dist: 4, criterion: 3)> Size: 96B
array([[594.59883563, 598.57680372, 594.83412974],
       [         inf,          inf,          inf],
       [595.61108485, 599.58905294, 595.84637896],
       [635.55247999, 639.53044808, 635.7877741 ]])
Coordinates:
    id          <U6 24B '020602'
    name        object 8B 'Dartmouth'
    horizon     <U9 36B '1970-2025'
  * scipy_dist  (scipy_dist) <U10 160B 'genextreme' 'pearson3' ... 'expon'
  * criterion   (criterion) <U4 48B 'aic' 'bic' 'aicc'
Attributes:
    original_long_name:      Maximum of variable
    original_units:          m3 s-1
    original_standard_name:  water_volume_transport_in_river_channel
    cell_methods:            time: mean
    history:                 [2025-04-10 15:49:56] streamflow_max_spring: fa....
    original_description:    Annual maximum of variable (02-11 to 06-19).
    long_name:               Information criteria
    description:             Information criteria for the distribution parame...
    scipy_dist:              ['genextreme', 'pearson3', 'gumbel_r', 'expon']
    units:                   

Enfin, les périodes de retour peuvent être obtenues en utilisant xhfa.local.parametric_quantiles.

Help on function parametric_quantiles in module xhydro.frequency_analysis.local:

parametric_quantiles(p: xarray.core.dataset.Dataset, t: float | list[float], mode: str = 'max') -> xarray.core.dataset.Dataset
    Compute quantiles from fitted distributions.

    Parameters
    ----------
    p : xr.Dataset
        Dataset containing the parameters of the fitted distributions.
        Must have a dimension `dparams` containing the parameter names and a dimension `scipy_dist` containing the distribution names.
    t : float or list of float
        Return period(s) in years.
    mode : {'max', 'min'}
        Whether the return period is the probability of exceedance (max) or non-exceedance (min).

    Returns
    -------
    xr.Dataset
        Dataset containing the quantiles of the distributions.

[20]:
rp = xhfa.local.parametric_quantiles(params, t=[2, 20, 100])

rp.load()
[20]:
<xarray.Dataset> Size: 660B
Dimensions:                (id: 2, scipy_dist: 4, return_period: 3)
Coordinates:
    horizon                <U9 36B '1970-2025'
  * id                     (id) object 16B '020602' '020802'
  * scipy_dist             (scipy_dist) <U10 160B 'genextreme' ... 'expon'
    name                   (id) object 16B 'Dartmouth' 'Madeleine'
  * return_period          (return_period) int64 24B 2 20 100
    p_quantile             (return_period) float64 24B 0.5 0.95 0.99
Data variables:
    streamflow_max_spring  (scipy_dist, return_period, id) float64 192B 185.5...
    volume_sum_spring      (scipy_dist, return_period, id) float64 192B 255.7...
Attributes:
    cat:frequency:         yr
    cat:processing_level:  indicators
    cat:id:                

Dans une future version, l’affichage des graphiques sera géré par une fonction dédiée. Pour l’instant, nous démontrons le processus en utilisant les utilitaires préliminaires dans ce Notebook.

La fonction xhfa.local._prepare_plots génère les points de données nécessaires pour visualiser les résultats de l’analyse fréquentielle. Si log=True, elle renverra des valeurs x espacées logarithmiquement entre xmin et xmax. En parallèle, xhfa.local._get_plotting_positions calcule les positions des graphiques pour toutes les variables dans le jeu de données. Elle accepte les arguments alpha et beta. Pour des valeurs typiques, consultez la documentation SciPy. Par défaut, (0.4, 0.4) est utilisé, ce qui correspond à la méthode des quantiles non biaisée (Cunnane).

[21]:
data = xhfa.local._prepare_plots(params, xmin=1, xmax=1000, npoints=50, log=True)
pp = xhfa.local._get_plotting_positions(ds_4fa[["streamflow_max_spring"]])
[22]:
# Let's plot the observations
p1 = data.streamflow_max_spring.hvplot(
    x="return_period", by="scipy_dist", grid=True, groupby=["id"], logx=True
)
data.streamflow_max_spring.hvplot(
    x="return_period", by="scipy_dist", grid=True, groupby=["id"], logx=True
)

# Let's now plot the distributions
p2 = pp.hvplot.scatter(
    x="streamflow_max_spring_pp",
    y="streamflow_max_spring",
    grid=True,
    groupby=["id"],
    logx=True,
)
pp.hvplot.scatter(
    x="streamflow_max_spring_pp",
    y="streamflow_max_spring",
    grid=True,
    groupby=["id"],
    logx=True,
)

# And now combining the plots
p1 * p2
[22]:

5.4. Incertitudes

Les incertitudes sont un aspect important de l’analyse fréquentielle et doivent être prises en compte lors de l’interprétation des résultats. Ces incertitudes proviennent souvent de la qualité des données, du choix de la distribution et de l’estimation des paramètres. Bien que les visualisations puissent fournir des informations sur l’ajustement du modèle, il est essentiel de quantifier et de prendre en compte les incertitudes, telles que les intervalles de confiance pour les estimations des paramètres, afin d’assurer des conclusions robustes.

Pour gérer l’intensité computationnelle, nous nous concentrerons sur un seul bassin versant et limiterons l’analyse aux deux distributions qui semblaient les mieux correspondre aux données.

[23]:
ds_4fa = ds_4fa.sel(id="020602")[["streamflow_max_spring"]]
params = params.sel(id="020602", scipy_dist=["genextreme", "gumbel_r"])[
    ["streamflow_max_spring"]
]

5.4.1. a) Bootstrap des observations

Une méthode pour quantifier les incertitudes consiste à effectuer un bootstrap des observations. Dans cet exemple, nous effectuerons le bootstrap un petit nombre de fois pour illustrer le processus, bien qu’en pratique, un nombre plus élevé d’itérations (par exemple, 5000) soit recommandé pour obtenir des estimations plus fiables. Le bootstrap consiste à rééchantillonner les données observées avec remplacement pour générer plusieurs ensembles de données, qui peuvent ensuite être utilisés pour évaluer la variabilité des paramètres et des résultats du modèle.

Cela peut être accompli en appelant xhydro.frequency_analysis.uncertainties.bootstrap_obs.

Help on function bootstrap_obs in module xhydro.frequency_analysis.uncertainties:

bootstrap_obs(obs: xarray.core.dataarray.DataArray, *, n_samples: int, seed: int | None = None) -> xarray.core.dataarray.DataArray
    Generate bootstrap samples from observed data.

    Parameters
    ----------
    obs : xarray.DataArray
        The observed data to bootstrap.
    n_samples : int
        The number of bootstrap samples to generate.
    seed : int, optional
        Seed for the random number generator.

    Returns
    -------
    xarray.DataArray
        Bootstrap samples with dimensions [samples, time].

[25]:
ds_4fa_boot_obs = xhfa.uncertainties.bootstrap_obs(ds_4fa, n_samples=35)
ds_4fa_boot_obs
[25]:
<xarray.Dataset> Size: 9kB
Dimensions:                (samples: 35, time: 56)
Coordinates:
    id                     <U6 24B '020602'
    name                   object 8B 'Dartmouth'
  * time                   (time) datetime64[ns] 448B 1970-01-01 ... 2025-01-01
  * samples                (samples) int64 280B 0 1 2 3 4 5 ... 30 31 32 33 34
Data variables:
    streamflow_max_spring  (samples, time) float32 8kB nan 244.0 ... 145.4 nan

Ce processus ajoutera une nouvelle dimension, samples, au jeu de données. Lorsqu’il est utilisé en conjonction avec xhydro.frequency_analysis.local.fit, un nouvel ensemble de paramètres sera calculé pour chaque échantillon. En conséquence, le bootstrap peut devenir coûteux en termes de calcul, surtout à mesure que le nombre d’itérations de bootstrap augmente.

[26]:
params_boot_obs = xhfa.local.fit(
    ds_4fa_boot_obs, distributions=["genextreme", "gumbel_r"]
)
rp_boot_obs = xhfa.local.parametric_quantiles(params_boot_obs, t=[2, 20, 100])
rp_boot_obs.load()
[26]:
<xarray.Dataset> Size: 2kB
Dimensions:                (samples: 35, scipy_dist: 2, return_period: 3)
Coordinates:
    horizon                <U9 36B '1970-2025'
  * samples                (samples) int64 280B 0 1 2 3 4 5 ... 30 31 32 33 34
  * scipy_dist             (scipy_dist) <U10 80B 'genextreme' 'gumbel_r'
    id                     <U6 24B '020602'
    name                   object 8B 'Dartmouth'
  * return_period          (return_period) int64 24B 2 20 100
    p_quantile             (return_period) float64 24B 0.5 0.95 0.99
Data variables:
    streamflow_max_spring  (scipy_dist, return_period, samples) float64 2kB 1...

5.4.2. b) Bootstrap des distributions

Dans cette approche, plutôt que de rééchantillonner directement les observations, nous rééchantillonnons les distributions ajustées pour estimer l’incertitude. Cette méthode nous permet d’évaluer la variabilité des paramètres des distributions ajustées. Comme dans l’exemple précédent, nous effectuerons un nombre minimal d’itérations de bootstrap pour réduire la charge computationnelle, mais en pratique, un nombre plus élevé d’itérations fournirait des estimations plus robustes de l’incertitude.

Cela peut être accompli en appelant xhydro.frequency_analysis.uncertainties.bootstrap_dist. Contrairement à bootstrap_obs, cette méthode ne prend pas en charge l’évaluation paresseuse et requiert une fonction spécifique pour l’étape d’ajustement : xhydro.frequency_analysis.uncertainties.fit_boot_dist.

Help on function bootstrap_dist in module xhydro.frequency_analysis.uncertainties:

bootstrap_dist(ds_obs: xarray.core.dataset.Dataset, ds_params: xarray.core.dataset.Dataset, *, n_samples: int) -> xarray.core.dataset.Dataset
    Generate bootstrap samples from a fitted distribution.

    Parameters
    ----------
    ds_obs : xarray.Dataset
        The observed data.
    ds_params : xarray.Dataset
        The fitted distribution parameters.
    n_samples : int
        The number of bootstrap samples to generate.

    Returns
    -------
    xarray.Dataset
        Bootstrap samples with dimensions [samples, time].

    Notes
    -----
    This function does not support lazy evaluation.

Help on function fit_boot_dist in module xhydro.frequency_analysis.uncertainties:

fit_boot_dist(ds: xarray.core.dataset.Dataset) -> xarray.core.dataset.Dataset
    Fit distributions to bootstrap samples.

    Parameters
    ----------
    ds : xarray.Dataset
        The bootstrap samples to fit.

    Returns
    -------
    xarray.Dataset
        Fitted distribution parameters for each bootstrap sample.

[29]:
tmp_values = xhfa.uncertainties.bootstrap_dist(ds_4fa, params, n_samples=35)
params_boot_dist = xhfa.uncertainties.fit_boot_dist(tmp_values)
params_boot_dist
[29]:
<xarray.Dataset> Size: 2kB
Dimensions:                (scipy_dist: 2, samples: 35, dparams: 3)
Coordinates:
  * samples                (samples) int64 280B 0 1 2 3 4 5 ... 30 31 32 33 34
  * dparams                (dparams) <U5 60B 'c' 'loc' 'scale'
  * scipy_dist             (scipy_dist) <U10 80B 'genextreme' 'gumbel_r'
    horizon                <U9 36B '1970-2025'
    id                     <U6 24B '020602'
    name                   object 8B 'Dartmouth'
Data variables:
    streamflow_max_spring  (scipy_dist, samples, dparams) float64 2kB dask.array<chunksize=(1, 35, 2), meta=np.ndarray>
[30]:
rp_boot_dist = xhfa.local.parametric_quantiles(
    params_boot_dist.load(), t=[2, 20, 100]
)  # Lazy computing is not supported
rp_boot_dist
[30]:
<xarray.Dataset> Size: 2kB
Dimensions:                (samples: 35, scipy_dist: 2, return_period: 3)
Coordinates:
  * samples                (samples) int64 280B 0 1 2 3 4 5 ... 30 31 32 33 34
  * scipy_dist             (scipy_dist) <U10 80B 'genextreme' 'gumbel_r'
    horizon                <U9 36B '1970-2025'
    id                     <U6 24B '020602'
    name                   object 8B 'Dartmouth'
  * return_period          (return_period) int64 24B 2 20 100
    p_quantile             (return_period) float64 24B 0.5 0.95 0.99
Data variables:
    streamflow_max_spring  (scipy_dist, return_period, samples) float64 2kB 1...

5.4.3. c) Comparaison

Illustrons la différence entre les deux approches.

[31]:
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
fig.set_figheight(4)
fig.set_figwidth(15)

# Subset the data
rp_orig = rp.streamflow_max_spring.sel(id="020602", scipy_dist="genextreme")
boot_obs = rp_boot_obs.streamflow_max_spring.sel(scipy_dist="genextreme")
boot_dist = rp_boot_dist.streamflow_max_spring.sel(scipy_dist="genextreme")

# Original fit
ax.plot(
    rp_orig.return_period.values,
    rp_orig,
    "black",
    label="Original fit",
)

ax.plot(
    boot_obs.return_period.values,
    boot_obs.quantile(0.5, "samples"),
    "red",
    label="Bootstrapped observations",
)
boot_obs_05 = boot_obs.quantile(0.05, "samples")
boot_obs_95 = boot_obs.quantile(0.95, "samples")
ax.fill_between(
    boot_obs.return_period.values, boot_obs_05, boot_obs_95, alpha=0.2, color="red"
)

ax.plot(
    boot_dist.return_period.values,
    boot_dist.quantile(0.5, "samples"),
    "blue",
    label="Bootstrapped distribution",
)
boot_dist_05 = boot_dist.quantile(0.05, "samples")
boot_dist_95 = boot_dist.quantile(0.95, "samples")
ax.fill_between(
    boot_dist.return_period.values, boot_dist_05, boot_dist_95, alpha=0.2, color="blue"
)

plt.xscale("log")
plt.grid(visible=True)
plt.xlabel("Return period (years)")
plt.ylabel("Streamflow (m$^3$/s)")
ax.legend()
[31]:
<matplotlib.legend.Legend at 0x72905c698200>
../_images/notebooks_local_frequency_analysis_48_1.png