Exemple de cas d’utilisation¶
Cet exemple illustre un cas d’utilisation qui couvre les étapes essentielles de la construction d’un modèle hydrologique et de l’analyse de l’impact des changements climatiques :
Identification du bassin versant et de ses principales caractéristiques
Bassin versant de la rivière Beaurivage dans le sud du Québec, à l’emplacement de la station 023401.
Collecte des données observées
Données ERA5-Land et données de débit.
Préparation et calage du modèle hydrologique
GR4JCN émulé par le cadre hydrologique Raven.
Calcul des indicateurs hydrologiques
Débit moyen estival
Débit moyen mensuel
Débit maximum sur 20 et 100 ans
Débit estival minimal sur 7 jours de récurrence 2 ans
Évaluation de l’impact des changements climatiques
Simulations CMIP6 post-traitées du jeu de données ESPO-G6-R2
Identification d’un bassin versant et de ses caractéristiques¶
INFO
Pour plus d’informations sur cette section et les options disponibles, consultez le Notebook GIS.
Cette première étape dépend fortement du modèle hydrologique. Puisque nous utiliserons GR4JCN dans notre exemple, nous devons obtenir l’aire de drainage, les coordonnées du centroïde et l’altitude. Nous aurons également besoin de la délimitation du bassin versant pour extraire les données météorologiques. Ces informations proviendront de HydroBASINS via le module xhydro.gis
.
Délimitation du bassin versant et caractéristiques de base :
xhydro.gis.watershed_delineation
etxhydro.gis.watershed_properties
Altitude :
xhydro.gis.surface_properties
[1]:
# Workaround for determining the notebook folder within a running notebook
# This cell is not visible when the documentation is built.
from __future__ import annotations
try:
from _finder import _find_current_folder
notebook_folder = _find_current_folder()
except ImportError:
from pathlib import Path
notebook_folder = Path().cwd()
(notebook_folder / "_data").mkdir(exist_ok=True)
[2]:
import leafmap
import numpy as np
import pandas as pd
import xhydro.gis as xhgis
[3]:
# Watershed delineation
coords = (-71.28878, 46.65692)
m = leafmap.Map(center=(coords[1], coords[0]), zoom=8)
gdf = xhgis.watershed_delineation(coordinates=coords, map=m)
gdf
[3]:
HYBAS_ID | Upstream Area (sq. km). | geometry | category | color | |
---|---|---|---|---|---|
0 | 7120365812 | 693.5 | POLYGON ((-71.09758 46.40035, -71.09409 46.403... | 1 | #ffffd9 |
Afficher m
produira cette carte :
[4]:
# Watershed properties
gdf_wat = xhgis.watershed_properties(gdf)
gdf_wat
[4]:
HYBAS_ID | Upstream Area (sq. km). | category | color | area | perimeter | gravelius | centroid | |
---|---|---|---|---|---|---|---|---|
0 | 7120365812 | 693.5 | 1 | #ffffd9 | 5.845762e+08 | 158155.350574 | 1.845263 | (-71.26046365140014, 46.45216102567744) |
[5]:
# Surface properties
gdf_surf = xhgis.surface_properties(gdf)
gdf_surf
[5]:
elevation | slope | aspect | time | id | proj:transform | platform | epsg | proj:shape | proj:epsg | title | band | gsd | spatial_ref | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
geometry | ||||||||||||||
0 | 224.201508 | 1.416482 | 214.740387 | 2021-04-22 | Copernicus_DSM_COG_30_N46_00_W072_00_DEM | {0.0, -0.0008333333333333334, 47.0004166666666... | TanDEM-X | 4326 | {1200} | 4326 | N46_00_W072_00 | data | 90 | 0 |
Nous conserverons les informations nécessaires et les formaterons pour GR4JCN.
[6]:
drainage_area = np.array([gdf_wat["Upstream Area (sq. km)."].iloc[0]])
latitude = np.array([gdf_wat.centroid[0][1]])
longitude = np.array([gdf_wat.centroid[0][0]])
elevation = np.array([gdf_surf["elevation"].iloc[0]])
Puisque xhgis.watershed_delineation
extrait le polygone HydroBASINS le plus proche, le bassin versant pourrait ne pas correspondre exactement aux coordonnées demandées. La station de jaugeage 023401 a une aire de drainage associée de 708 km², ce qui diffère de nos résultats. Le débit devra être ajusté en utilisant un facteur d’échelle.
[7]:
gauge_area = 708
scaling_factor = drainage_area / gauge_area
scaling_factor
[7]:
array([0.97951977])
Collecte des données observées¶
[8]:
import geopandas as gpd
import matplotlib.pyplot as plt
import xarray as xr
# For easy access to the specific streamflow data used here
import xdatasets
import xscen
Données météorologiques¶
INFO
Plusieurs librairies peuvent être utilisées pour effectuer ces étapes. Par souci de simplicité, cet exemple utilisera les modules subset et aggregate de la librairie xscen.
Cet exemple utilisera les données journalières ERA5-Land hébergées sur la plateforme PAVICS.
[9]:
# Extraction of ERA5-Land data
meteo_ref = xr.open_dataset(
"https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/datasets/reanalyses/day_ERA5-Land_NAM.ncml",
engine="netcdf4",
chunks={"time": 365, "lon": 50, "lat": 50},
)[["pr", "tasmin", "tasmax"]]
meteo_ref
[9]:
<xarray.Dataset> Size: 429GB Dimensions: (time: 26298, lat: 800, lon: 1700) Coordinates: * lat (lat) float32 3kB 10.0 10.1 10.2 10.3 10.4 ... 89.6 89.7 89.8 89.9 * lon (lon) float32 7kB -179.9 -179.8 -179.7 -179.6 ... -10.2 -10.1 -10.0 * time (time) datetime64[ns] 210kB 1950-01-01 1950-01-02 ... 2021-12-31 Data variables: pr (time, lat, lon) float32 143GB dask.array<chunksize=(365, 50, 50), meta=np.ndarray> tasmin (time, lat, lon) float32 143GB dask.array<chunksize=(365, 50, 50), meta=np.ndarray> tasmax (time, lat, lon) float32 143GB dask.array<chunksize=(365, 50, 50), meta=np.ndarray> Attributes: (12/26) Conventions: CF-1.8 cell_methods: time: mean (interval: 1 day) data_specs_version: 00.00.07 domain: NAM format: netcdf frequency: day ... ... dataset_description: https://www.ecmwf.int/en/era5-land license_type: permissive license: Please acknowledge the use of ERA5-Land as stated i... attribution: Contains modified Copernicus Climate Change Service... citation: Muñoz Sabater, J., (2021): ERA5-Land hourly data fr... doi: https://doi.org/10.24381/cds.e2161bac
Ce jeu de données couvre l’ensemble du globe et possède plus de 70 ans de données. La première étape consistera donc à sous-échantillonner les données spatialement et temporellement. Pour le sous-échantillonnage spatial, le GeoDataFrame obtenu précédemment peut être utilisé.
[10]:
meteo_ref = meteo_ref.sel(time=slice("1991", "2020")) # Temporal subsetting
meteo_ref = xscen.spatial.subset(
meteo_ref, method="shape", tile_buffer=2, shape=gdf
) # Spatial subsetting, with a buffer of 2 grid cells
meteo_ref = meteo_ref.assign_coords({"crs": meteo_ref.crs})
meteo_ref
[10]:
<xarray.Dataset> Size: 9MB Dimensions: (time: 10958, lat: 8, lon: 8) Coordinates: * lat (lat) float32 32B 46.1 46.2 46.3 46.4 46.5 46.6 46.7 46.8 * lon (lon) float32 32B -71.6 -71.5 -71.4 -71.3 -71.2 -71.1 -71.0 -70.9 * time (time) datetime64[ns] 88kB 1991-01-01 1991-01-02 ... 2020-12-31 crs int64 8B 1 Data variables: pr (time, lat, lon) float32 3MB dask.array<chunksize=(355, 8, 8), meta=np.ndarray> tasmin (time, lat, lon) float32 3MB dask.array<chunksize=(355, 8, 8), meta=np.ndarray> tasmax (time, lat, lon) float32 3MB dask.array<chunksize=(355, 8, 8), meta=np.ndarray> Attributes: (12/27) Conventions: CF-1.8 cell_methods: time: mean (interval: 1 day) data_specs_version: 00.00.07 domain: NAM format: netcdf frequency: day ... ... license_type: permissive license: Please acknowledge the use of ERA5-Land as stated i... attribution: Contains modified Copernicus Climate Change Service... citation: Muñoz Sabater, J., (2021): ERA5-Land hourly data fr... doi: https://doi.org/10.24381/cds.e2161bac crs: EPSG:4326
[11]:
ax = plt.subplot(1, 1, 1)
meteo_ref.tasmin.isel(time=0).plot(ax=ax)
gdf.plot(ax=ax)
[11]:
<Axes: title={'center': 'time = 1991-01-01, crs = 1'}, xlabel='longitude [degrees_east]', ylabel='latitude [degrees_north]'>

Puisque GR4JCN est un modèle global, nous devons moyenner les données météorologiques sur le bassin versant. Cela sera accompli en utilisant xscen.spatial_mean
. Il existe plusieurs méthodes, mais pour les fichiers shapefile, la plus précise est « xesmf », qui utilise le SpatialAverager de xESMF pour suivre précisément la représentation de chaque point de grille sur le polygone. Cela peut être très lent pour des polygones détaillés avec de nombreux sommets, mais l’argument « simplify_tolerance » aide à accélérer ce processus.
[12]:
meteo_ref = xscen.spatial_mean(
meteo_ref,
method="xesmf",
region={"method": "shape", "shape": gdf},
simplify_tolerance=0.1,
kwargs={"skipna": True},
)
# Some housekeeping to associate the HYBAS_ID dimension (coming from the shapefile) to other coordinates added from the GeoDataFrame.
for c in meteo_ref.coords:
if len(meteo_ref[c].dims) == 0:
if c not in ["lon", "lat"]:
meteo_ref = meteo_ref.drop_vars([c])
else:
meteo_ref[c] = meteo_ref[c].expand_dims("HYBAS_ID")
meteo_ref
[12]:
<xarray.Dataset> Size: 351kB Dimensions: (time: 10958, HYBAS_ID: 1) Coordinates: * time (time) datetime64[ns] 88kB 1991-01-01 1991-01-02 ... 2020-12-31 lon (HYBAS_ID) float64 8B -71.28 lat (HYBAS_ID) float64 8B 46.46 * HYBAS_ID (HYBAS_ID) int64 8B 7120365812 Data variables: pr (time) float64 88kB dask.array<chunksize=(355,), meta=np.ndarray> tasmin (time) float64 88kB dask.array<chunksize=(355,), meta=np.ndarray> tasmax (time) float64 88kB dask.array<chunksize=(355,), meta=np.ndarray> Attributes: (12/28) Conventions: CF-1.8 cell_methods: time: mean (interval: 1 day) data_specs_version: 00.00.07 domain: NAM format: netcdf frequency: day ... ... license: Please acknowledge the use of ERA5-Land as stated i... attribution: Contains modified Copernicus Climate Change Service... citation: Muñoz Sabater, J., (2021): ERA5-Land hourly data fr... doi: https://doi.org/10.24381/cds.e2161bac crs: EPSG:4326 regrid_method: conservative
Raven attend des températures en Celsius et des précipitations en millimètres, mais elles sont actuellement conforme aux conventions CF, en Kelvin et en kg m-2 s-1, respectivement. Encore une fois, xscen
peut être utilisé pour cette opération.
[13]:
meteo_ref = xscen.utils.change_units(
meteo_ref, {"tasmax": "degC", "tasmin": "degC", "pr": "mm"}
)
[14]:
# Save
meteo_ref.to_netcdf(notebook_folder / "_data" / "meteo.nc")
Données hydrométriques¶
Les données de débit du gouvernement du Québec sont accessibles via la librairie xdatasets
.
[15]:
qobs = (
xdatasets.Query(
**{
"datasets": {
"deh": {
"id": ["023401"],
"variables": ["streamflow"],
}
},
"time": {"start": "1991-01-01", "end": "2020-12-31"},
}
)
.data.squeeze()
.load()
).sel(spatial_agg="watershed")
[16]:
# Once again, some housekeeping is required on the metadata to make sure that xHydro understands the dataset.
qobs["id"].attrs["cf_role"] = "timeseries_id"
qobs = qobs.rename({"id": "station_id"})
qobs["streamflow"].attrs = {
"long_name": "Streamflow",
"units": "m3 s-1",
"standard_name": "water_volume_transport_in_river_channel",
"cell_methods": "time: mean",
}
for c in qobs.coords:
if len(qobs[c].dims) == 0 and "time" not in c:
qobs[c] = qobs[c].expand_dims("station_id")
qobs
[16]:
<xarray.Dataset> Size: 132kB Dimensions: (time: 10958, station_id: 1) Coordinates: (12/15) drainage_area (station_id) float32 4B 708.0 end_date (station_id) datetime64[ns] 8B 2020-12-31 * station_id (station_id) <U6 24B '023401' latitude (station_id) float32 4B 46.66 longitude (station_id) float32 4B -71.29 name (station_id) object 8B 'Beaurivage' ... ... spatial_agg (station_id) <U9 36B 'watershed' start_date (station_id) datetime64[ns] 8B 1991-01-01 * time (time) datetime64[ns] 88kB 1991-01-01 ... 2020-12-31 time_agg <U4 16B 'mean' timestep <U1 4B 'D' variable (station_id) <U10 40B 'streamflow' Data variables: streamflow (time) float32 44kB 46.0 33.9 26.2 21.0 ... 19.98 15.76 12.93
[17]:
plt.figure(figsize=[10, 5])
qobs.streamflow.plot()
[17]:
[<matplotlib.lines.Line2D at 0x7fa33872faa0>]

Comme spécifié plus tôt, les observations de débit doivent être modifiées pour tenir compte des différences de taille des bassins versants entre la jauge et le polygone.
[18]:
with xr.set_options(keep_attrs=True):
qobs["streamflow"] = qobs["streamflow"] * scaling_factor
[19]:
# Save (necessary for model calibration)
qobs.to_netcdf(notebook_folder / "_data" / "qobs.nc")
Préparation et calage du modèle hydrologique (xhydro.modelling)¶
INFO
Pour plus d’informations sur cette section et les options disponibles, consultez le Notebook de modélisation hydrologique.
[20]:
import xhydro as xh
from xhydro.modelling.calibration import perform_calibration
from xhydro.modelling.obj_funcs import get_objective_function
La fonction perform_calibration
nécessite un argument model_config
qui lui permet de construire le modèle hydrologique correspondant. Toutes les informations requises ont été acquises dans les sections précédentes, il ne reste plus qu’à remplir les entrées du modèle RavenPy/GR4JCN.
Pour simplifier, l’altitude de la station météorologique sera considérée comme l’altitude moyenne du bassin versant réel. Le calcul de l’altitude des points de grille dans ERA5-Land n’est pas toujours trivial et ne fait pas partie de l’exemple. De même, l’équivalent en eau de neige n’est actuellement pas disponible dans la base de données de PAVICS, donc « AVG_ANNUAL_SNOW » a été estimé approximativement à l’aide de Brown & Brasnett (2010).
[21]:
# Model configuration
model_config = {
"model_name": "GR4JCN",
"parameters": [0.529, -3.396, 407.29, 1.072, 16.9, 0.947],
"drainage_area": drainage_area,
"elevation": elevation,
"latitude": latitude,
"longitude": longitude,
"start_date": "1991-01-01",
"end_date": "2020-12-31",
"meteo_file": notebook_folder / "_data" / "meteo.nc",
"data_type": ["TEMP_MAX", "TEMP_MIN", "PRECIP"],
"alt_names_meteo": {"TEMP_MIN": "tasmin", "TEMP_MAX": "tasmax", "PRECIP": "pr"},
"meteo_station_properties": {
"ALL": {"elevation": elevation, "latitude": latitude, "longitude": longitude}
},
"rain_snow_fraction": "RAINSNOW_DINGMAN",
"evaporation": "PET_HARGREAVES_1985",
"global_parameter": {"AVG_ANNUAL_SNOW": 100.00},
}
# Parameter bounds for GR4JCN
bounds_low = [0.01, -15.0, 10.0, 0.0, 1.0, 0.0]
bounds_high = [2.5, 10.0, 700.0, 7.0, 30.0, 1.0]
[22]:
# Calibration / validation period
mask_calib = xr.where(qobs.time.dt.year <= 2010, 1, 0).values
mask_valid = xr.where(qobs.time.dt.year > 2010, 1, 0).values
# Model calibration
best_parameters, best_simulation, best_objfun = perform_calibration(
model_config,
"kge",
qobs=notebook_folder / "_data" / "qobs.nc",
bounds_low=bounds_low,
bounds_high=bounds_high,
evaluations=8,
algorithm="DDS",
mask=mask_calib,
sampler_kwargs=dict(trials=1),
)
Initializing the Dynamically Dimensioned Search (DDS) algorithm with 8 repetitions
The objective function will be maximized
Starting the DDS algotrithm with 8 repetitions...
Finding best starting point for trial 1 using 5 random samples.
1 of 8, maximal objective function=0.164749, time remaining: 00:00:30
Initialize database...
['csv', 'hdf5', 'ram', 'sql', 'custom', 'noData']
2 of 8, maximal objective function=0.164749, time remaining: 00:00:30
3 of 8, maximal objective function=0.239819, time remaining: 00:00:25
4 of 8, maximal objective function=0.466832, time remaining: 00:00:20
5 of 8, maximal objective function=0.466832, time remaining: 00:00:14
6 of 8, maximal objective function=0.466832, time remaining: 00:00:07
7 of 8, maximal objective function=0.481349, time remaining: 00:00:00
8 of 8, maximal objective function=0.481349, time remaining: 23:59:53
Best solution found has obj function value of 0.48134887704825247 at 5
*** Final SPOTPY summary ***
Total Duration: 63.2 seconds
Total Repetitions: 8
Maximal objective value: 0.481349
Corresponding parameter setting:
param0: 0.46099
param1: -9.77586
param2: 216.456
param3: 0.384076
param4: 13.2655
param5: 0.563789
******************************
Best parameter set:
param0=0.4609896022995832, param1=-9.775860334349964, param2=216.455502549914, param3=0.38407572808259516, param4=13.265462690482325, param5=0.5637885058813862
Run number 6 has the highest objectivefunction with: 0.4813
Pour réduire les temps de calcul dans cet exemple, seulement 10 étapes ont été utilisées pour la fonction de calage, ce qui est bien en dessous du nombre recommandé. Les paramètres ci-dessous ont été obtenus en exécutant le code ci-dessus avec 150 évaluations.
[23]:
# Replace the results with parameters obtained using 150 evaluations
best_parameters = [
0.3580270511815579,
-2.187141388684563,
24.012067980309702,
0.000781,
1.9330212374187332,
0.5491789347783598,
]
model_config["parameters"] = best_parameters
best_simulation = xh.modelling.hydrological_model(model_config).run()
Le vrai KGE doit être calculé à partir d’une période de validation, en utilisant get_objective_function
.
[24]:
get_objective_function(
qobs=qobs.streamflow,
qsim=best_simulation,
obj_func="kge",
mask=mask_valid,
).values
[24]:
array(0.69988518)
[25]:
ax = plt.figure(figsize=(10, 5))
qobs.streamflow.plot(color="k", linewidth=3)
best_simulation.streamflow.plot(color="r")
[25]:
[<matplotlib.lines.Line2D at 0x7fa314379790>]

Calcul des indicateurs hydroclimatologiques¶
[26]:
import xclim
import xhydro.frequency_analysis as xhfa
Indicateurs non fréquenciels¶
INFO
Pour plus d’informations sur cette section et les options disponibles, consultez le Notebook d’analyse de l’impact des changements climatiques.
INFO
Les indicateurs personnalisés dans xHydro
sont construits en suivant le formatage YAML requis par xclim
. »
Un indicateur personnalisé construit avec xclim.core.indicator.Indicator.from_dict
nécessitera ces éléments
« data » : Un dictionnaire avec les informations suivantes :
« base » : L“« ID YAML » obtenu depuis cette page.
« input » : Un dictionnaire reliant l’entrée xclim par défaut au nom de votre variable. Nécessaire uniquement si elle diffère. Dans le lien ci-dessus, ce sont les chaînes suivant « Uses: ».
« parameters » : Un dictionnaire contenant tous les autres paramètres pour un indicateur donné. Dans le lien ci-dessus, la manière la plus simple d’y accéder est de cliquer sur le lien dans le coin supérieur droit de la boîte décrivant un indicateur donné.
Des entrées supplémentaires peuvent être utilisées ici, comme décrit dans la documentation xclim sous « identifier ».
« identifier » : Un nom personnalisé pour votre indicateur. Ce sera le nom retourné dans les résultats.
« module »: Nécessaire, mais peut être n’importe quoi. Pour éviter un écrasement accidentel des indicateurs
xclim
, il est préférable d’utiliser quelque chose de différent de : [« atmos », « land », « generic »].
Pour une analyse des impacts des changements climatiques, le processus typique pour calculer les indicateurs non-fréquenciels serait de :
Définir les indicateurs soit par les fonctionnalités
xclim
présentées ci-dessous ou par un fichier YAML.Appeler
xhydro.indicators.compute_indicators
, qui produira des résultats annuels par le biais d’un dictionnaire, où chaque clé représente les fréquences demandées.Appeler
xhydro.cc.climatological_op
pour chaque entrée du dictionnaire afin de calculer la moyenne sur 30 ans.Recombiner les jeux de données.
Cependant, si les résultats annuels ne sont pas nécessaires, xhydro.cc.produce_horizon
peut contourner les étapes 2 à 4 et éviter beaucoup de tracas. Il le fait en supprimant l’axe time
et en le remplaçant par une dimension horizon
qui représente une tranche de temps. Dans le cas des indicateurs saisonniers ou mensuels, une dimension correspondante season
ou month
est également ajoutée.
Nous allons calculer le débit moyen estival (un indicateur annuel) et les débits moyens mensuels.
[27]:
indicators = [
# Mean summer flow
xclim.core.indicator.Indicator.from_dict(
data={
"base": "stats",
"input": {"da": "streamflow"},
"parameters": {"op": "mean", "indexer": {"month": [6, 7, 8]}},
},
identifier="qmoy_summer",
module="hydro",
),
# Mean monthly flow
xclim.core.indicator.Indicator.from_dict(
data={
"base": "stats",
"input": {"da": "streamflow"},
"parameters": {"op": "mean", "freq": "MS"},
},
identifier="qmoy_monthly",
module="hydro",
),
]
ds_indicators = xh.cc.produce_horizon(
best_simulation, indicators=indicators, periods=["1991", "2020"]
)
ds_indicators
[27]:
<xarray.Dataset> Size: 312B Dimensions: (horizon: 1, month: 12) Coordinates: basin_name <U7 28B 'sub_001' * horizon (horizon) <U9 36B '1991-2020' * month (month) <U3 144B 'JAN' 'FEB' 'MAR' 'APR' ... 'OCT' 'NOV' 'DEC' Data variables: qmoy_summer (horizon) float64 8B 11.24 qmoy_monthly (horizon, month) float64 96B 7.05 5.431 14.18 ... 14.53 10.85 Attributes: cat:xrfreq: fx cat:frequency: fx cat:processing_level: horizons cat:variable: ('qmoy_monthly',)
Analyse fréquencielle¶
INFO
Pour plus d’informations sur cette section et les options disponibles, consultez le Notebook d’analyse fréquencielle locale.
Une analyse fréquencielle suit généralement ces étapes :
Obtenir les données brutes nécessaires à l’analyse, telles que les maximums annuels, via
xhydro.indicators.get_yearly_op
.Appeler
xhfa.local.fit
pour obtenir les paramètres d’un nombre spécifié de distributions, telles que Gumbel, GEV et Pearson-III.(Facultatif) Appeler
xhfa.local.criteria
pour obtenir les paramètres de qualité d’ajustement.Appeler
xhfa.local.parametric_quantiles
pour obtenir les niveaux de retour.
Nous allons calculer les maximums annuels sur 20 et 100 ans, ainsi que le minimum des débits estivaux sur 7 jours pour une période de 2 ans.
[28]:
qref_max = xh.indicators.get_yearly_op(
best_simulation,
op="max",
timeargs={"annual": {}},
missing="pct",
missing_options={"tolerance": 0.15},
)
qref_min = xh.indicators.get_yearly_op(
best_simulation,
op="min",
window=7,
timeargs={"summer": {"date_bounds": ["05-01", "11-30"]}},
missing="pct",
missing_options={"tolerance": 0.15},
)
[29]:
ax = plt.figure(figsize=(10, 5))
qref_max.streamflow_max_annual.dropna("time", how="all").plot()
qref_min.streamflow7_min_summer.dropna("time", how="all").plot()
[29]:
[<matplotlib.lines.Line2D at 0x7fa31436f290>]

[30]:
params_max = xhfa.local.fit(
qref_max,
distributions=["genextreme", "gumbel_r", "norm", "pearson3"],
method="MLE",
min_years=20,
).compute()
params_min = xhfa.local.fit(
qref_min,
distributions=["genextreme", "gumbel_r", "norm", "pearson3"],
method="MLE",
min_years=20,
).compute()
params_max
[30]:
<xarray.Dataset> Size: 432B Dimensions: (scipy_dist: 4, dparams: 4) Coordinates: horizon <U9 36B '1991-2020' * dparams (dparams) <U5 80B 'c' 'skew' 'loc' 'scale' * scipy_dist (scipy_dist) <U10 160B 'genextreme' ... 'pearson3' basin_name <U7 28B 'sub_001' Data variables: streamflow_max_annual (scipy_dist, dparams) float64 128B 0.01648 ... 98.41 Attributes: cat:xrfreq: YS-JAN cat:frequency: yr cat:processing_level: indicators cat:variable: ('streamflow_max_annual',) cat:id:
Bien que non infaillible, le meilleur ajustement peut être identifié en utilisant le Critère d’Information Bayésien.
[31]:
criteria_max = xhfa.local.criteria(qref_max, params_max)
criteria_max = str(
criteria_max.isel(
scipy_dist=criteria_max.streamflow_max_annual.sel(criterion="bic")
.argmin()
.values
).scipy_dist.values
) # Get the best fit as a string
print(f"Best distribution for the annual maxima: {criteria_max}")
criteria_min = xhfa.local.criteria(qref_min, params_min)
criteria_min = str(
criteria_min.isel(
scipy_dist=criteria_min.streamflow7_min_summer.sel(criterion="bic")
.argmin()
.values
).scipy_dist.values
) # Get the best fit as a string
print(f"Best distribution for the summer 7-day minima: {criteria_min}")
Best distribution for the annual maxima: pearson3
Best distribution for the summer 7-day minima: genextreme
Les fonctions de traçage viendront éventuellement à xhydro.frequency_analysis
, mais elles sont actuellement en cours de développement et sont cachées par défaut. Jusqu’à ce qu’une fonction publique soit ajoutée à la libraries, ces fonctions cachées peuvent toujours être appelées pour illustrer les résultats.
[32]:
# Plotting
data_max = xhfa.local._prepare_plots(
params_max, xmin=1, xmax=1000, npoints=50, log=True
)
data_max["streamflow_max_annual"].attrs["long_name"] = "streamflow"
pp = xhfa.local._get_plotting_positions(qref_max[["streamflow_max_annual"]])
ax = plt.figure(figsize=(10, 5))
data_max.streamflow_max_annual.sel(scipy_dist=criteria_max).plot(color="k")
plt.xscale("log")
pp.plot.scatter(
x="streamflow_max_annual_pp",
y="streamflow_max_annual",
color="k",
)
[32]:
<matplotlib.collections.PathCollection at 0x7fa3572e6ae0>

[33]:
# Plotting
data_min = xhfa.local._prepare_plots(
params_min, xmin=1, xmax=1000, npoints=50, log=True
)
data_min["streamflow7_min_summer"].attrs["long_name"] = "streamflow"
pp = xhfa.local._get_plotting_positions(qref_min[["streamflow7_min_summer"]])
ax = plt.figure(figsize=(10, 5))
data_min.streamflow7_min_summer.sel(scipy_dist=criteria_min).plot(color="k")
plt.xscale("log")
pp.plot.scatter(
x="streamflow7_min_summer_pp",
y="streamflow7_min_summer",
color="k",
)
[33]:
<matplotlib.collections.PathCollection at 0x7fa3575d9970>

[34]:
# Computation of return levels
rl_max = xhfa.local.parametric_quantiles(
params_max.sel(scipy_dist=criteria_max).expand_dims("scipy_dist"), t=[20, 100]
).squeeze()
rl_min = xhfa.local.parametric_quantiles(
params_min.sel(scipy_dist=criteria_min).expand_dims("scipy_dist"), t=[2], mode="min"
).squeeze()
rl_max
[34]:
<xarray.Dataset> Size: 144B Dimensions: (return_period: 2) Coordinates: horizon <U9 36B '1991-2020' scipy_dist <U8 32B 'pearson3' basin_name <U7 28B 'sub_001' * return_period (return_period) int64 16B 20 100 p_quantile (return_period) float64 16B 0.95 0.99 Data variables: streamflow_max_annual (return_period) float64 16B 418.2 536.4 Attributes: cat:xrfreq: YS-JAN cat:frequency: yr cat:processing_level: indicators cat:variable: ('streamflow_max_annual',) cat:id:
[35]:
print(
f"20-year annual maximum: {np.round(rl_max.streamflow_max_annual.sel(return_period=20).values, 1)} m³/s"
)
print(
f"100-year annual maximum: {np.round(rl_max.streamflow_max_annual.sel(return_period=100).values, 1)} m³/s"
)
print(
f"2-year minimum 7-day summer flow: {np.round(rl_min.streamflow7_min_summer.values, 1)} m³/s"
)
20-year annual maximum: 418.2 m³/s
100-year annual maximum: 536.4 m³/s
2-year minimum 7-day summer flow: 2.8 m³/s
Simulations et indicateurs de débit futur¶
Données météorologiques futures¶
Maintenant que nous avons accès à un modèle hydrologique calé et à des indicateurs historiques, nous pouvons réaliser l’analyse d’impact des changements climatiques. Cet exemple utilisera un ensemble de modèles CMIP6 qui ont été débiaisés en utilisant ERA5-Land, pour assurer la cohérence avec le produit de référence. Plus précisément, nous utiliserons le jeu de données ESPO-G6-E5L, également hébergé sur PAVICS. Bien qu’il soit recommandé d’utiliser plusieurs scénarios d’émission, cet exemple se limitera à utiliser les simulations SSP2-4.5 provenant de 14 modèles climatiques.
Nous pouvons généralement réutiliser le même code que ci-dessus. Une différence est que les modèles climatiques utilisent souvent des calendriers personnalisés qui doivent être convertis en calendriers standard. Encore une fois, xscen
peut être utilisé pour cela.
[36]:
models = [
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_CAS_FGOALS-g3_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_CMCC_CMCC-ESM2_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_CSIRO-ARCCSS_ACCESS-CM2_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_CSIRO_ACCESS-ESM1-5_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_DKRZ_MPI-ESM1-2-HR_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_INM_INM-CM5-0_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_MIROC_MIROC6_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_MPI-M_MPI-ESM1-2-LR_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_MRI_MRI-ESM2-0_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_NCC_NorESM2-LM_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_CNRM-CERFACS_CNRM-ESM2-1_ssp245_r1i1p1f2_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_NIMS-KMA_KACE-1-0-G_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_NOAA-GFDL_GFDL-ESM4_ssp245_r1i1p1f1_1950-2100.ncml",
"day_ESPO-G6-E5L_v1.0.0_CMIP6_ScenarioMIP_NAM_BCC_BCC-CSM2-MR_ssp245_r1i1p1f1_1950-2100.ncml",
]
Le code ci-dessous montrera comment procéder avec la première simulation, mais il pourrait être utilisé dans une boucle pour traiter les 14 simulations.
[37]:
model = models[0]
meteo_sim = xr.open_dataset(
f"https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/datasets/simulations/bias_adjusted/cmip6/ouranos/ESPO-G/ESPO-G6-E5Lv1.0.0/{model}",
engine="netcdf4",
chunks={"time": 365, "lon": 50, "lat": 50},
)
meteo_sim = xscen.spatial.subset(meteo_sim, method="shape", tile_buffer=2, shape=gdf)
meteo_sim = meteo_sim.assign_coords({"crs": meteo_sim.crs})
meteo_sim = xscen.spatial_mean(
meteo_sim,
method="xesmf",
region={"method": "shape", "shape": gdf},
simplify_tolerance=0.1,
)
for c in meteo_sim.coords:
if len(meteo_sim[c].dims) == 0:
if c not in ["lon", "lat"]:
meteo_sim = meteo_sim.drop_vars([c])
else:
meteo_sim[c] = meteo_sim[c].expand_dims("HYBAS_ID")
meteo_sim = xscen.utils.change_units(
meteo_sim, {"tasmax": "degC", "tasmin": "degC", "pr": "mm"}
)
# Manage calendars
meteo_sim = xscen.utils.clean_up(
meteo_sim,
convert_calendar_kwargs={"calendar": "standard", "align_on": "random"},
missing_by_var={"tasmin": "interpolate", "tasmax": "interpolate", "pr": 0},
)
meteo_sim.to_netcdf(notebook_folder / "_data" / f"meteo_sim0.nc")
Données de débit et indicateurs futurs¶
Encore une fois, le même code que précédemment peut être réutilisé ici, mais avec xhydro.modelling.hydrological_model
. La principale différence est que les meilleurs paramètres peuvent être utilisés lors de la configuration du modèle hydrologique, et que les dates couvrent toute la période allant jusqu’en 2100.
Les indicateurs sont également calculés de manière similaire, avec l’ajout de l’utilisation d’une liste de périodes dans l’argument periods
pour créer une dimension horizon
, au lieu d’une période unique. La fonction xhydro.frequency_analysis.fit
peut également accepter un argument periods
.
Le code ci-dessous ne montrera encore qu’une seule simulation, mais pourrait être utilisé pour traiter les 14 simulations.
[38]:
from copy import deepcopy
import xhydro.modelling as xhm
[39]:
model_cfg = deepcopy(model_config)
model_cfg["meteo_file"] = notebook_folder / "_data" / f"meteo_sim0.nc"
model_cfg["parameters"] = np.array(best_parameters)
model_cfg["start_date"] = "1991-01-01"
model_cfg["end_date"] = "2099-12-31"
qsim = xhm.hydrological_model(model_cfg).run()
[40]:
plt.figure(figsize=(10, 5))
qsim.streamflow.plot()
[40]:
[<matplotlib.lines.Line2D at 0x7fa3573e0470>]

[41]:
# Non-frequential indicators
ds_indicators = xh.cc.produce_horizon(
qsim, indicators=indicators, periods=[["1991", "2020"], ["2070", "2099"]]
)
# Frequency analysis
qsim_max = xh.indicators.get_yearly_op(
qsim,
op="max",
timeargs={"annual": {}},
missing="pct",
missing_options={"tolerance": 0.15},
)
qsim_min = xh.indicators.get_yearly_op(
qsim,
op="min",
window=7,
timeargs={"summer": {"date_bounds": ["05-01", "11-30"]}},
missing="pct",
missing_options={"tolerance": 0.15},
)
params_max = xhfa.local.fit(
qsim_max,
distributions=["genextreme", "gumbel_r", "norm", "pearson3"],
method="MLE",
min_years=20,
periods=[["1991", "2020"], ["2070", "2099"]],
).compute()
params_min = xhfa.local.fit(
qsim_min,
distributions=["genextreme", "gumbel_r", "norm", "pearson3"],
method="MLE",
min_years=20,
periods=[["1991", "2020"], ["2070", "2099"]],
).compute()
criteria_max = xhfa.local.criteria(
qsim_max.sel(time=slice("1991", "2020")), params_max.sel(horizon="1991-2020")
)
criteria_max = str(
criteria_max.isel(
scipy_dist=criteria_max.streamflow_max_annual.sel(criterion="bic")
.argmin()
.values
).scipy_dist.values
) # Get the best fit as a string
criteria_min = xhfa.local.criteria(
qsim_min.sel(time=slice("1991", "2020")), params_min.sel(horizon="1991-2020")
)
criteria_min = str(
criteria_min.isel(
scipy_dist=criteria_min.streamflow7_min_summer.sel(criterion="bic")
.argmin()
.values
).scipy_dist.values
) # Get the best fit as a string
# Computation of return levels
rl_max = xhfa.local.parametric_quantiles(
params_max.sel(scipy_dist=criteria_max).expand_dims("scipy_dist"), t=[20, 100]
).squeeze()
rl_min = xhfa.local.parametric_quantiles(
params_min.sel(scipy_dist=criteria_min).expand_dims("scipy_dist"),
t=[2],
mode="min",
).squeeze()
# Combine all indicators
ds_indicators = xr.merge([ds_indicators, rl_max, rl_min], compat="override")
ds_indicators
[41]:
<xarray.Dataset> Size: 564B Dimensions: (horizon: 2, month: 12, return_period: 2) Coordinates: basin_name <U7 28B 'sub_001' * horizon (horizon) <U9 72B '1991-2020' '2070-2099' * month (month) <U3 144B 'JAN' 'FEB' 'MAR' ... 'NOV' 'DEC' scipy_dist <U8 32B 'pearson3' * return_period (return_period) int64 16B 20 100 p_quantile (return_period) float64 16B 0.95 0.99 Data variables: qmoy_summer (horizon) float64 16B 11.34 10.04 qmoy_monthly (horizon, month) float64 192B 8.837 7.288 ... 13.95 streamflow_max_annual (return_period, horizon) float64 32B 371.0 ... 560.9 streamflow7_min_summer (horizon) float64 16B 2.412 2.402 Attributes: cat:xrfreq: fx cat:frequency: fx cat:processing_level: horizons cat:variable: ('qmoy_monthly',)
Impacts des changements climatiques¶
INFO
Pour plus d’informations sur cette section et les options disponibles, consultez le Notebook d’analyse de l’impact des changements climatiques.
Cet exemple gardera l’analyse de l’impact des changements climatiques relativement simple.
Calculez la différence entre les périodes future et de référence en utilisant
xhydro.cc.compute_deltas
.Utilisez ces différences pour calculer les statistiques d’ensemble en utilisant
xhydro.cc.ensemble_stats
: percentiles de l’ensemble et accord entre les modèles climatiques.
[42]:
# Differences
deltas = xh.cc.compute_deltas(
ds_indicators, reference_horizon="1991-2020", kind="%", rename_variables=False
).isel(horizon=-1)
# Save the results
deltas.to_netcdf(notebook_folder / "_data" / f"deltas_sim0.nc")
deltas.squeeze()
[42]:
<xarray.Dataset> Size: 400B Dimensions: (month: 12, return_period: 2) Coordinates: basin_name <U7 28B 'sub_001' horizon <U9 36B '2070-2099' * month (month) <U3 144B 'JAN' 'FEB' 'MAR' ... 'NOV' 'DEC' scipy_dist <U8 32B 'pearson3' * return_period (return_period) int64 16B 20 100 p_quantile (return_period) float64 16B 0.95 0.99 Data variables: qmoy_summer float64 8B -11.47 qmoy_monthly (month) float64 96B 13.49 11.94 61.1 ... 22.05 24.16 streamflow_max_annual (return_period) float64 16B 15.6 25.8 streamflow7_min_summer float64 8B -0.4241 Attributes: cat:xrfreq: fx cat:frequency: fx cat:processing_level: deltas cat:variable: ('qmoy_monthly',)
Il existe de nombreuses façons de créer l’ensemble lui-même. Si vous utilisez un dictionnaire de jeux de données, la clé sera utilisée pour nommer chaque élément de la nouvelle dimension realization
. Cela peut être très utile lorsque l’on effectue des analyses plus détaillées ou lorsque l’on souhaite pondérer les différents modèles en fonction, par exemple, du nombre de simulations disponibles. Dans notre cas, puisque nous souhaitons seulement calculer des statistiques d’ensemble, nous pouvons simplifier et fournir simplement une liste.
[43]:
import pooch
# Acquire deltas for the other 13 simulations
from xhydro.testing.helpers import ( # In-house function to access xhydro-testdata
deveraux,
)
deltas_files = deveraux().fetch("use_case/deltas.zip", processor=pooch.Unzip())
deltas_files.extend([notebook_folder / "_data" / f"deltas_sim0.nc"])
[44]:
# Statistics to compute
statistics = {
"ensemble_percentiles": {"values": [10, 25, 50, 75, 90], "split": False},
"robustness_fractions": {"test": None},
}
ens_stats = xh.cc.ensemble_stats(deltas_files, statistics)
ens_stats
[44]:
<xarray.Dataset> Size: 2kB Dimensions: (percentiles: 5, month: 12, return_period: 2) Coordinates: basin_name <U7 28B 'sub_001' horizon <U9 36B '2070-2099' * percentiles (percentiles) int64 40B 10 ... 90 * month (month) <U3 144B 'JAN' ... 'DEC' * return_period (return_period) int64 16B 20 100 p_quantile (return_period) float64 16B 0.95... Data variables: (12/32) qmoy_summer (percentiles) float64 40B -14.0 ... qmoy_monthly (month, percentiles) float64 480B dask.array<chunksize=(12, 5), meta=np.ndarray> streamflow_max_annual (return_period, percentiles) float64 80B dask.array<chunksize=(2, 5), meta=np.ndarray> streamflow7_min_summer (percentiles) float64 40B -28.54... qmoy_summer_changed float64 8B 1.0 qmoy_summer_positive float64 8B 0.5714 ... ... streamflow7_min_summer_positive float64 8B 0.2143 streamflow7_min_summer_changed_positive float64 8B 0.2143 streamflow7_min_summer_negative float64 8B 0.7857 streamflow7_min_summer_changed_negative float64 8B 0.7857 streamflow7_min_summer_valid float64 8B 1.0 streamflow7_min_summer_agree float64 8B 0.7857 Attributes: cat:xrfreq: fx cat:frequency: fx cat:processing_level: ensemble cat:variable: qmoy_monthly cat:id: ensemble_size: 14
[45]:
# Recreate the boxplots based on the computed percentiles
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(13, 5), sharey=True)
ax = plt.subplot(1, 3, 1)
for i, rp in enumerate(ens_stats.return_period.values):
stats = [
{
"label": rp,
"med": ens_stats.streamflow_max_annual.sel(
percentiles=50, return_period=rp
).values,
"q1": ens_stats.streamflow_max_annual.sel(
percentiles=25, return_period=rp
).values,
"q3": ens_stats.streamflow_max_annual.sel(
percentiles=75, return_period=rp
).values,
"whislo": ens_stats.streamflow_max_annual.sel(
percentiles=10, return_period=rp
).values,
"whishi": ens_stats.streamflow_max_annual.sel(
percentiles=90, return_period=rp
).values,
}
]
ax.bxp(stats, showfliers=False, positions=[i], widths=0.5)
ax.set_title("Maximum annual streamflow")
plt.xlabel("Return period")
plt.ylabel("Difference Fut-Hist (%)")
ax = plt.subplot(1, 3, 2)
for i, rp in enumerate(ens_stats.return_period.values):
stats = [
{
"label": rp,
"med": ens_stats.streamflow7_min_summer.sel(percentiles=50).values,
"q1": ens_stats.streamflow7_min_summer.sel(percentiles=25).values,
"q3": ens_stats.streamflow7_min_summer.sel(percentiles=75).values,
"whislo": ens_stats.streamflow7_min_summer.sel(percentiles=10).values,
"whishi": ens_stats.streamflow7_min_summer.sel(percentiles=90).values,
}
]
ax.bxp(stats, showfliers=False, positions=[i], widths=0.5)
ax.set_title("Minimum summer streamflow (7-day avg)")
plt.xlabel("")
ax = plt.subplot(1, 3, 3)
stats = [
{
"label": "",
"med": ens_stats.qmoy_summer.sel(percentiles=50).values,
"q1": ens_stats.qmoy_summer.sel(percentiles=25).values,
"q3": ens_stats.qmoy_summer.sel(percentiles=75).values,
"whislo": ens_stats.qmoy_summer.sel(percentiles=10).values,
"whishi": ens_stats.qmoy_summer.sel(percentiles=90).values,
}
]
ax.bxp(stats, showfliers=False, positions=[i], widths=0.25)
ax.set_title("Mean summer flow")
plt.show()

[46]:
print(
f"Fraction of simulations with a positive change (maximum streamflow): {ens_stats.streamflow_max_annual_positive.values}"
)
print(
f"Fraction of simulations with a positive change (minimum summer streamflow): {ens_stats.streamflow7_min_summer_positive.values}"
)
print(
f"Fraction of simulations with a positive change (mean summer streamflow): {ens_stats.qmoy_summer_positive.values}"
)
Fraction of simulations with a positive change (maximum streamflow): [0.5 0.64285714]
Fraction of simulations with a positive change (minimum summer streamflow): 0.21428571428571427
Fraction of simulations with a positive change (mean summer streamflow): 0.5714285714285714