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 attributcf_role
avec la valeurtimeseries_id
.La variable doit au moins avoir un attribut
units
. Bien que d’autres attributs, commelong_name
etcell_methods
, ne soient pas strictement requis, ils sont attendus parxclim
, 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.
[4]:
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 :
Utiliser
xhydro.frequency_analysis.local.fit
pour déterminer le meilleur ensemble de paramètres pour un nombre donné de distributions statistiques.(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.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(xhfa.local.fit)
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(xhfa.local.criteria)
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
.
[19]:
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
.
[24]:
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
.
[27]:
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.
[28]:
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>
