5. Module d’analyse fréquentielle - Analyse régionale¶
[1]:
import matplotlib.pyplot as plt
import xdatasets as xd
from lmoments3.distr import KappaGen
from sklearn.cluster import HDBSCAN, OPTICS, AgglomerativeClustering
import xhydro as xh
import xhydro.frequency_analysis as xhfa
import xhydro.gis as xhgis
from xhydro.frequency_analysis.regional import *
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'>)
This page demonstrate how to use the xhydro
package to perform regional frequency analysis on a dataset of streamflow data. The first steps will be similar to the local frequency analysis example, but we will keep it simple to focus on the regional frequency analysis.
Lets start with getting the 02 region stations that are natural and have a minimum duration of 30 years.
[2]:
ds = (
xd.Query(
**{
"datasets": {
"deh": {
"id": ["02*"],
"regulated": ["Natural"],
"variables": ["streamflow"],
}
},
"time": {"start": "1970-01-01", "minimum_duration": (30 * 365, "d")},
}
)
.data.squeeze()
.load()
)
# This dataset lacks some attributes, so let's 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",
}
ds
[2]:
<xarray.Dataset> Size: 1MB Dimensions: (id: 15, time: 20454) Coordinates: (12/15) drainage_area (id) float32 60B 647.0 626.0 772.0 ... 914.0 2.33e+03 227.0 end_date (id) datetime64[ns] 120B 2024-06-02 2024-06-02 ... 2010-11-19 * id (id) object 120B '020404' '020602' ... '024007' '024013' latitude (id) float32 60B 48.81 48.98 49.04 ... 46.31 46.19 46.05 longitude (id) float32 60B -64.92 -64.7 -66.48 ... -71.45 -72.28 -71.45 name (id) object 120B 'York' 'Dartmouth' ... 'Bécancour' ... ... spatial_agg <U9 36B 'watershed' start_date (id) datetime64[ns] 120B 1980-10-01 1970-10-01 ... 1979-05-12 * time (time) datetime64[ns] 164kB 1970-01-01 ... 2025-12-31 time_agg <U4 16B 'mean' timestep <U1 4B 'D' variable <U10 40B 'streamflow' Data variables: streamflow (id, time) float32 1MB nan nan nan nan ... nan nan nan nan
Here, we hide years with more than 15% of missing data and get the annual maximum.
[3]:
timeargs = {
"annual": {},
}
ds_4fa = xh.indicators.get_yearly_op(
ds, op="max", timeargs=timeargs, missing="pct", missing_options={"tolerance": 0.15}
)
5.1. Explanatory variables¶
5.1.1. a) Extraction à l’aide de xhydro.gis
¶
Les analyses fréquentielles régionales s’appuient sur des variables explicatives pour relier les informations aux différents sites. Pour cet exemple, nous utiliserons les propriétés du bassin versant, mais d’autres variables telles que les moyennes climatologiques ou les données d’occupation du sol peuvent également être utilisées. Reportez-vous à l’exemple SIG pour plus de détails.
[4]:
gdf = xd.Query(
**{
"datasets": {
"deh_polygons": {
"id": ["02*"],
"regulated": ["Natural"],
"variables": ["streamflow"],
}
},
"time": {"start": "1970-01-01", "minimum_duration": (30 * 365, "d")},
}
).data.reset_index()
dswp = xhgis.watershed_properties(
gdf[["Station", "geometry"]], unique_id="Station", output_format="xarray"
)
cent = dswp["centroid"].to_numpy()
lon = [ele[0] for ele in cent]
lat = [ele[1] for ele in cent]
dswp = dswp.assign(lon=("Station", lon))
dswp = dswp.assign(lat=("Station", lat))
dswp = dswp.drop("centroid")
dswp
[4]:
<xarray.Dataset> Size: 720B Dimensions: (Station: 15) Coordinates: * Station (Station) object 120B '020404' '020602' ... '024007' '024013' Data variables: area (Station) float64 120B 6.641e+08 6.27e+08 ... 2.331e+09 2.318e+08 perimeter (Station) float64 120B 2.496e+05 2.067e+05 ... 1.151e+05 gravelius (Station) float64 120B 2.732 2.329 2.51 ... 2.429 2.906 2.132 lon (Station) float64 120B -65.24 -64.91 -66.19 ... -71.6 -71.33 lat (Station) float64 120B 48.9 49.04 48.96 ... 46.1 46.19 46.08
5.1.2. b) Analyse en composantes principales¶
Pour effectuer notre analyse fréquentielle régionale, nous traiterons les données avec une analyse en composantes principales (ACP) pour réduire la dimensionnalité de notre ensemble de données : La fonction xhydro.regional.fit_pca
prend un xarray.Dataset
en entrée et renvoie un xarray.Dataset
avec les composantes principales.
[5]:
data, pca = xhfa.regional.fit_pca(dswp, n_components=3)
Nous pouvons voir que la corrélation est proche de 0 entre les composantes, ce qui signifie que les 3 premières composantes sont suffisamment indépendantes pour être utilisées pour le reste de notre analyse.
[6]:
data.to_dataframe(name="value").reset_index().pivot(
index="Station", columns="components"
).corr()
[6]:
value | ||||
---|---|---|---|---|
components | 0 | 1 | 2 | |
components | ||||
value | 0 | 1.000000e+00 | 2.637918e-16 | -1.410455e-16 |
1 | 2.637918e-16 | 1.000000e+00 | -2.900868e-16 | |
2 | -1.410455e-16 | -2.900868e-16 | 1.000000e+00 |
5.1.3. b) Regroupement¶
Dans cet exemple, nous utiliserons AgglomerativeClustering
, mais d’autres méthodes fourniraient également des résultats valides. Le clustering régional lui-même est effectué à l’aide de xhfa.regional.get_group_from_fit
, qui peut prendre les arguments des fonctions skleanr
en dictionnaire.
[7]:
groups = xhfa.regional.get_group_from_fit(
AgglomerativeClustering, {"n_clusters": 3}, data
)
groups
[7]:
[array(['022507', '022704', '023002', '023303', '023401', '023422',
'023428', '023432', '023701', '024003', '024013'], dtype=object),
array(['020404', '020602', '021407'], dtype=object),
array(['024007'], dtype=object)]
5.2. Analyse régionale¶
Hosking et Wallis ont développé une méthode d’analyse fréquentielle régionale qui utilise les L-moments pour analyser les valeurs extrêmes dans différentes régions. Voici un aperçu concis : 1. L-Moments : Les L-moments sont des combinaisons linéaires de moments statistiques. Ils sont moins sensibles aux valeurs aberrantes que les moments traditionnels (comme la moyenne et la variance) et fournissent des estimations plus robustes, en particulier pour les échantillons de petite taille. 2. Analyse fréquentielle régionale : Cette approche consiste à regrouper des données provenant de plusieurs sites ou régions pour déterminer la distribution de fréquence des événements extrêmes, tels que les inondations. Les méthodes de Hosking et Wallis impliquent l’estimation des paramètres des distributions de fréquence régionales et l’évaluation de l’adéquation de ces distributions aux données. 3. L-Moments régionaux : Ils sont utilisés pour résumer les données de divers sites au sein d’une région. En appliquant des méthodes basées sur les L-moments, les paramètres peuvent être estimés et la fréquence des événements extrêmes peut être évaluée dans toute la région.
Nous calculons les L-moments pour chaque station
[8]:
ds_moment = calc_moments(ds_4fa)
ds_moment
[8]:
<xarray.Dataset> Size: 2kB Dimensions: (id: 15, lmom: 6) Coordinates: (12/15) drainage_area (id) float32 60B 647.0 626.0 772.0 ... 2.33e+03 227.0 end_date (id) datetime64[ns] 120B 2024-06-02 ... 2010-11-19 * id (id) object 120B '020404' '020602' ... '024013' latitude (id) float32 60B 48.81 48.98 49.04 ... 46.19 46.05 longitude (id) float32 60B -64.92 -64.7 ... -72.28 -71.45 name (id) object 120B 'York' 'Dartmouth' ... 'Bécancour' ... ... spatial_agg <U9 36B 'watershed' start_date (id) datetime64[ns] 120B 1980-10-01 ... 1979-05-12 time_agg <U4 16B 'mean' timestep <U1 4B 'D' variable <U10 40B 'streamflow' * lmom (lmom) <U4 96B 'l1' 'l2' 'l3' 'tau' 'tau3' 'tau4' Data variables: streamflow_max_annual (id, lmom) float64 720B 145.1 31.03 ... 0.0697 0.1307 Attributes: cat:xrfreq: YS-JAN cat:frequency: yr cat:processing_level: indicators cat:variable: ('streamflow_max_annual',) cat:id:
Nous devons remodeler nos ensembles de données de maximums annuels et de L-moments en fonction des regroupements trouvés à l’aide de l’algorithme de clustering. Comme il n’existe aucune convention sur le nom de cette nouvelle dimension, il a été décidé dans xHydro qu’elle devrait s’appeler group_id
.
5.2.1. Score H (Score d’homogénéité)¶
Le Score H mesure l’homogénéité des données sur différents sites ou régions par rapport au modèle régional :
H < 1 : Homogène – Indique que les données provenant de différents sites sont assez similaires et correspondent bien au modèle régional. Cela suggère que le modèle est adapté à la région dans son ensemble.
1 ≤ H < 2 : Peut-être homogène – Suggère un certain degré d’hétérogénéité, mais les données pourraient néanmoins correspondre raisonnablement bien au modèle régional. Il pourrait y avoir des variations que le modèle ne prend pas entièrement en compte.
H ≥ 2 : hétérogène – Indique des différences significatives entre les sites ou les régions, suggérant que le modèle peut ne pas être adapté à toutes les données. Les régions peuvent être trop diverses ou le modèle peut nécessiter des ajustements.
5.2.2. Score Z (qualité d’ajustement)¶
Le Score Z évalue dans quelle mesure la distribution théorique (basée sur le modèle régional) correspond aux données observées :
Calcul du Score Z : Ce score quantifie l’écart entre les valeurs observées et les valeurs attendues, normalisé par leur variabilité. Il indique si les différences sont statistiquement significatives.
Interprétation:
Faible score Z : bonne adéquation du modèle aux données observées. En règle générale, une valeur absolue du score Z inférieure à 1,64 suggère que le modèle est approprié et que l’adéquation est statistiquement acceptable.
Score Z élevé : indique des écarts significatifs entre les valeurs observées et les valeurs attendues. Une valeur absolue supérieure à 1,64 suggère que le modèle peut ne pas bien correspondre aux données et que des ajustements peuvent être nécessaires.
To calculate H and Z, we also need a KappaGen
object from the lmoment3 library. This library is not part of the default xhydro environment and will need to be installed seperately.
[10]:
kap = KappaGen()
ds_H_Z = calc_h_z(ds_groups, ds_moments_groups, kap)
ds_H_Z
[10]:
<xarray.Dataset> Size: 600B Dimensions: (group_id: 3, crit: 2) Coordinates: * group_id (group_id) int64 24B 0 1 2 source <U102 408B 'Ministère de l’Environnement, de la Lu... spatial_agg <U9 36B 'watershed' time_agg <U4 16B 'mean' timestep <U1 4B 'D' variable <U10 40B 'streamflow' lmom <U4 16B 'l1' * crit (crit) <U1 8B 'H' 'Z' Data variables: streamflow_max_annual (crit, group_id) float64 48B 0.6787 0.6149 ... nan
Nous filtrons les données pour n’inclure que les données dont H et Z sont inférieurs aux seuils. Les seuils peuvent être spécifiés mais sont fixés par defaut à 1 et 1,64 respectivement pour H et Z.
[11]:
mask = mask_h_z(ds_H_Z)
ds_groups_H1 = ds_groups.where(mask).load()
ds_moments_groups_H1 = ds_moments_groups.where(mask).load()
[12]:
# Centiles and return periods :
centiles = [x / 100.0 for x in range(101)]
return_periods = [
2,
10,
20,
100,
350,
]
We can now calculate the return periods for each group and return period. Also, since we don’t want to do our analysis on really small regions, remove_small_regions
removes any region below a certain threshold. By default this threshold is 5.
[13]:
Q_T = calculate_rp_from_afr(ds_groups_H1, ds_moments_groups_H1, return_periods)
Q_T = remove_small_regions(Q_T)
Pour tracer, voyons à quoi cela ressemble sur 023401
[14]:
Q_reg = Q_T.sel(id="023401").dropna(dim="group_id", how="all")
reg = Q_reg.streamflow_max_annual.squeeze()
Comparons le local et le régional
[15]:
params_loc = xhfa.local.fit(ds_4fa)
Q_loc = xhfa.local.parametric_quantiles(params_loc, return_periods)
loc = Q_loc.sel(id="023401", scipy_dist="genextreme").streamflow_max_annual
[16]:
fig = plt.figure(figsize=(15, 4))
plt.plot(reg.return_period.values, reg.values, "blue")
plt.plot(loc.return_period.values, loc.values, "red")
plt.xscale("log")
plt.grid(visible=True)
plt.xlabel("Return period (years)")
plt.ylabel("Discharge (m$^3$/s)")
plt.legend()
[16]:
<matplotlib.legend.Legend at 0x7f14565947a0>
6. Incertitudes¶
6.1. Incertitudes de l’analyse fréquentielle locale¶
To add some uncertainities, we will work with only one catchment and two distributions, as uncertainities can be intensive in computation. We select the station 023401, and distribution “genextreme” and “pearson3”.
Pour l’analyse fréquentielle locale, nous devons ajuster la distribution ce qui fait en sorte que le temps de calcul est important.
[17]:
ds_4fa_one_station = ds_4fa.sel(id="023401")
params_loc_one_station = params_loc.sel(
id="023401", scipy_dist=["genextreme", "pearson3"]
)
6.1.1. Rééchantillonnage des observations¶
A way to get uncertainities is to bootstrap the observations. For this example, we will boostrap the observations a low amount of times, although a higher number (e.g. 5000) would be preferable in practice.
[18]:
ds_4fa_iter = xhfa.uncertainities.boostrap_obs(ds_4fa_one_station, 35)
params_boot_obs = xhfa.local.fit(ds_4fa_iter, distributions=["genextreme", "pearson3"])
[19]:
Q_boot_obs = xhfa.local.parametric_quantiles(
params_boot_obs.load(), return_periods
).squeeze()
Q_boot_obs = Q_boot_obs.streamflow_max_annual
6.1.2. Rééchantillonnage des distributions ajustées¶
Here, instead of resampling the observations, we resample the fitted distributions to get the uncertainty. Once again this example will only bootstrap a minimal amount of times to reduce computation loads, but higher numbers would be preferable.
[20]:
values = xhfa.uncertainities.boostrap_dist(
ds_4fa_one_station, params_loc_one_station, 35
)
params_boot_dist = xhfa.uncertainities.fit_boot_dist(values)
[21]:
Q_boot_dist = xhfa.local.parametric_quantiles(
params_boot_dist.load(), return_periods
).squeeze()
Q_boot_dist = Q_boot_dist.streamflow_max_annual
[22]:
loc_dist = Q_boot_dist.sel(scipy_dist="genextreme")
loc_obs = Q_boot_obs.sel(scipy_dist="genextreme")
[23]:
fig, ax = plt.subplots()
fig.set_figheight(4)
fig.set_figwidth(15)
ax.plot(reg.return_period.values, reg.values, "blue", label="Regional")
ax.plot(
loc_obs.return_period.values,
loc_obs.quantile(0.5, "samples"),
"red",
label="bootstrap obs",
)
loc_obs_05 = loc_obs.quantile(0.05, "samples")
loc_obs_95 = loc_obs.quantile(0.95, "samples")
ax.fill_between(loc_dist.return_period.values, loc_obs_05, loc_obs_95, alpha=0.2)
loc_dist_05 = loc_dist.quantile(0.05, "samples")
loc_dist_95 = loc_dist.quantile(0.95, "samples")
ax.fill_between(loc_dist.return_period.values, loc_dist_05, loc_dist_95, alpha=0.2)
ax.plot(
loc_dist.return_period.values,
loc_dist.quantile(0.5, "samples"),
"green",
label="bootstrap dist",
)
plt.xscale("log")
plt.grid(visible=True)
plt.xlabel("Return period (years)")
plt.ylabel("Discharge (m$^3$/s)")
ax.legend()
[23]:
<matplotlib.legend.Legend at 0x7f145655cd40>
6.2. Incertitudes de l’analyse fréquentielle régionale¶
6.2.1. Rééchantillonnage des observations¶
Pour l’analyse régionale, nous utilisons à nouveau boostrap_obs
pour rééchantillonner les observations, mais, cette fois, c’est beaucoup plus rapide, car aucun ajustement n’est impliqué.
[24]:
ds_reg_samples = xhfa.uncertainities.boostrap_obs(ds_4fa, 35)
ds_moments_iter = xhfa.uncertainities.calc_moments_iter(ds_reg_samples).load()
[25]:
Q_reg_boot = xhfa.uncertainities.calc_q_iter(
"023401", "streamflow_max_annual", ds_groups_H1, ds_moments_iter, return_periods
)
[26]:
reg_boot = Q_reg_boot.streamflow_max_annual.sel(id="023401")
Puisque nous allons faire quelques tracés pour illustrer les résultats, créons une fonction pour simplifier un peu les choses.
[27]:
def plot_ds_with_CI(
ds_list, CI_dim_list, color_list, label_list, x_label, y_label, title=None
):
fig, ax = plt.subplots()
fig.set_figheight(4)
fig.set_figwidth(15)
plt.xscale("log")
plt.grid(visible=True)
for i, ds in enumerate(ds_list):
x = ds.return_period.values
CI_dim = CI_dim_list[i]
y_5 = ds.quantile(0.5, CI_dim)
y_05 = ds.quantile(0.05, CI_dim)
y_95 = ds.quantile(0.95, CI_dim)
color = color_list[i]
label = label_list[i]
plt.plot(x, y_5, color, label=label)
ax.fill_between(x, y_05, y_95, alpha=0.2, color=color)
plt.xscale("log")
plt.grid(visible=True)
plt.xlabel(x_label)
plt.ylabel(y_label)
plt.title(title)
ax.legend()
[28]:
plot_ds_with_CI(
[loc_obs, loc_dist, reg_boot],
["samples", "samples", "samples"],
["blue", "green", "red"],
["bootstrap obs", "bootstrap dist", "Regional bootstrap"],
"Return period (years)",
"Discharge (m$^3$/s)",
)
6.2.2. Plusieurs régions¶
Une autre façon d’obtenir l’incertitude est d’avoir de nombreuses régions pour un bassin d’intérêt. Nous pouvons y parvenir en essayant différentes méthodes de clustering
. Ou en effectuant un jackknife sur la liste des stations. Nous ne faisons pas trop de tests ici car cela peut prendre un certain temps et le but est simplement d’illustrer les possibilités
Nous allons essayer trois méthodes de clustering et pour chaque méthode, nous essaierons de modifier certains des paramètres.
[29]:
PARAM = {
AgglomerativeClustering: {"arg_name": "n_clusters", "range": range(2, 12)},
HDBSCAN: {"arg_name": "min_cluster_size", "range": range(6, 7)},
OPTICS: {"arg_name": "min_samples", "range": range(4, 5)},
}
Nous générons maintenant une combinaison de stations en supprimant les stations 0 à n.
[30]:
n = 2
combinations_list = xhfa.uncertainities.generate_combinations(data, n)
So our station, instead of being in one region, will be in many of the regions.
[31]:
groups = []
for model in [AgglomerativeClustering, HDBSCAN, OPTICS]:
for p in PARAM[model]["range"]:
d_param = {}
d_param[PARAM[model]["arg_name"]] = p
for combination in combinations_list:
# Extract data for the current combination
data_com = data.sel(Station=list(combination))
# Get groups from the fit and add to the list
groups = groups + get_group_from_fit(model, d_param, data_com)
unique_groups = [list(x) for x in {tuple(x) for x in groups}]
The following steps are similar to the previous one, just with more regions.
[32]:
[33]:
kap = KappaGen()
ds_H_Z = calc_h_z(ds_groups, ds_moments_groups, kap)
[34]:
mask = mask_h_z(ds_H_Z)
ds_groups_H1 = ds_groups.where(mask).load()
ds_moments_groups_H1 = ds_moments_groups.where(mask).load()
Q_T = calculate_rp_from_afr(ds_groups_H1, ds_moments_groups_H1, return_periods)
Q_T = remove_small_regions(Q_T)
Q = Q_T.sel(id="023401").dropna(dim="group_id", how="all")
[35]:
regional_multiple_region = Q.streamflow_max_annual
[36]:
ds_moment = calc_moments(ds_4fa)
[37]:
plot_ds_with_CI(
[loc_obs, loc_dist, regional_multiple_region],
["samples", "samples", "group_id"],
["blue", "green", "red"],
["bootstrap obs", "bootstrap dist", "regional_multiple_region"],
"Return period (years)",
"Discharge (m$^3$/s)",
)
6.2.3. Combinaison de bootstrap et de plusieurs régions¶
calc_q_iter will check in how many group_id
the station is present, and stack it with samples. In this case, it will be stacked with 35 bootstraps, and we have 434 regions, so 15 190 samples are generated.
[38]:
Q_reg_boot = xhfa.uncertainities.calc_q_iter(
"023401", "streamflow_max_annual", ds_groups_H1, ds_moments_iter, return_periods
)
Q_reg_boot
[38]:
<xarray.Dataset> Size: 11MB Dimensions: (return_period: 5, id: 15, samples: 7595) Coordinates: (12/19) source <U102 408B 'Ministère de l’Environnement, de la Lu... spatial_agg <U9 36B 'watershed' time_agg <U4 16B 'mean' timestep <U1 4B 'D' variable <U10 40B 'streamflow' lmom <U4 16B 'l1' ... ... province (id, samples) object 911kB nan nan nan ... nan nan regulated (id, samples) object 911kB nan nan nan ... nan nan start_date (id, samples) datetime64[ns] 911kB NaT NaT ... NaT * samples (samples) object 61kB MultiIndex * group_id (samples) int64 61kB 0 0 0 0 0 ... 432 432 432 432 * obs_samples (samples) int64 61kB 0 1 2 3 4 5 ... 30 31 32 33 34 Data variables: streamflow_max_annual (return_period, id, samples) float64 5MB nan ... nan
[39]:
regional_multiple_region_boot = Q_reg_boot.sel(id="023401").streamflow_max_annual
[40]:
plot_ds_with_CI(
[loc_obs, loc_dist, regional_multiple_region, regional_multiple_region_boot],
["samples", "samples", "group_id", "samples"],
["blue", "green", "red", "black"],
[
"bootstrap obs",
"bootstrap dist",
"regional_multiple_region",
"regional_multiple_region_boot",
],
"Return period (years)",
"Discharge (m$^3$/s)",
)