Joint Probability Example#

  • This notebook demonstrates how to compute the probability of joint water level and precipitation extremes.

  • The analysis considers two cases:

    • wl_prcond: Water level peaks-over-threshold events paired with the maximum precipitation event occurring within ±1 day.

    • pr_wlcond: Precipitation peaks-over-threshold events paired with the maximum water level event occurring within ±1 day.

  • Univariate distributions are fitted to the marginals (water levels and precipitation) separately. Joint events are then analyzed using copulas, which are selected based on the Bayesian Information Criterion (BIC).

  • For methodology and limitations, see the technical report: Braschi, L.C., & Leys, V. (2025). Extrêmes de niveaux d’eau côtiers et extrêmes conjoints de niveaux d’eau et de précipitations au Canada. Rapport présenté à Ouranos. Montréal: CBCL Limited, 37 p.

The data for this notebook is stored in the cloud, so the first step is to access it.

Imports and Paths#

import pandas as pd
from scipy.stats import kendalltau
import xarray as xr
from pathlib import Path
import s3fs


from peach.frontend.wl_parameters import IndicatorObsWL, IndicatorSimWL
from peach.frontend.cbcl_utils import matching_events
from peach.frontend.jp_parameters import (
    IndicatorObsWLCOND,
    IndicatorObsPRPOT,
    IndicatorObsPRCOND,
    IndicatorSimPRPOT,
    IndicatorSimWLCOND,
    IndicatorSimPRPOT,
    IndicatorSimPRCOND,
    IndicatorObsJP,
    IndicatorSimJP
)

copulas = ["gaussian", "student", "clayton", "frank", "gumbel"]
scipy_dists = ["norm", "t", "gamma", 'genextreme']

stn_num = '07330'
ahccd_num = '1018620'

s3r = s3fs.S3FileSystem(anon=True, use_ssl=False, client_kwargs={"endpoint_url": "https://minio.ouranos.ca"})

def open_link(filename: str) -> str:
    """Read NetCDF object from MinIO server and return as a string."""
    root = "portail-ing/tutorial_data"
    link = s3r.open(f"{root}/{filename}")
    return link

# Peaks-over-threshold (pot) data
wl_pot_file = f"{stn_num}_wl_pot.nc"
pr_pot_file = f"{ahccd_num}_pr_pot.nc"

# Timeseries data (used to select conditional extremes)
wl_timeseries_file = f"{stn_num}_wl.nc"
pr_timeseries_file = f"{ahccd_num}_pr.nc"

# Future simulations: decadal sea-level delta and daily precipitation timeseries
sl_sim_file = f"{stn_num}_sl.nc"
pr_sim_file = f"{ahccd_num}_pr_sim.nc"
WARNING:matplotlib.font_manager:Matplotlib is building the font cache; this may take a moment.
config.py - Using local cache: True, with file None
2026-02-09 20:10:53,324 | INFO | peach.frontend.parameters | Logger successfully configured
2026-02-09 20:10:53,366 | INFO | peach.frontend.parameters | Workspace: /home/docs/checkouts/readthedocs.org/user_builds/cs-peach/conda/latest/lib/python3.14/workspace
2026-02-09 20:10:53,368 | INFO | URLParameterized | Logger successfully configured
2026-02-09 20:10:53,381 | INFO | peach.frontend.idf_parameters | Logger successfully configured

Step 1 - User Settings#

ref_period = (1995, 2014)
fut_period = (2070, 2100)
pval_thresh = 0.05 # p-value for the kendall dependence test
pr_rp = 10 # Return period for precipitation
wl_rp = 10 # Return period for water level

Step 2 - Load Data#

# Load peaks-over-threshold (pot) data
wl_pot_backend = xr.open_dataarray(open_link(wl_pot_file))
pr_pot_backend = xr.open_dataarray(open_link(pr_pot_file))

# Load timeseries data
wl = xr.open_dataarray(open_link(wl_timeseries_file))
pr = xr.open_dataarray(open_link(pr_timeseries_file))

# Load and format future simulation data
sl_sim_backend = xr.open_dataset(open_link(sl_sim_file))['sl_delta']
sim = xr.open_dataarray(open_link(pr_sim_file))
multi_index = pd.MultiIndex.from_arrays(
    [sim['variant_label'].values, sim['source_id'].values, sim['experiment_id'].values],
    names=["variant_label", "source_id", "experiment_id"]
)
pr_sim_backend = sim.assign_coords(realization=("realization", multi_index))

Step 3 - Conditional Events#

# Select maximum precipitation events occuring within 1 day of water level peaks
wl_pot_backend, pr_cond_backend = matching_events(pot_da=wl_pot_backend, timeseries_da=pr)

# Select maximum water levels events occuring within 1 day of precpitation peaks
pr_pot_backend, wl_cond_backend = matching_events(pot_da=pr_pot_backend, timeseries_da=wl)

Step 4 - Dependence#

# Assess statistical dependence with Kendall's Tau
wl_prcond_ktau, wl_prcond_pval = kendalltau(wl_pot_backend.values, pr_cond_backend.values)
pr_wlcond_ktau, pr_wlcond_pval = kendalltau(pr_pot_backend.values, wl_cond_backend.values)

def sig_string(pval, pval_thresh):
    if pval <= pval_thresh:
        return 'SIGNIFICANT'
    else:
        return 'NOT SIGNIFICANT'

print(f'Water level POT & conditional precipitation extremes: {sig_string(wl_prcond_pval, pval_thresh)}\n'
      f'p-value {round(wl_prcond_pval, 4)} and kendall tau {round(wl_prcond_ktau, 2)}')
print(f'Precipitation POT & conditional water level extremes: {sig_string(pr_wlcond_pval, pval_thresh)}\n'
      f'p-value {round(pr_wlcond_pval, 4)} and kendall tau {round(pr_wlcond_ktau, 2)}')
Water level POT & conditional precipitation extremes: SIGNIFICANT
p-value 0.0 and kendall tau 0.18
Precipitation POT & conditional water level extremes: SIGNIFICANT
p-value 0.0 and kendall tau 0.15

Step 5 - Joint analysis for wl_prcond#

# Marginals for observations
wl_pot = IndicatorObsWL(data=wl_pot_backend, period=ref_period)
pr_cond = IndicatorObsPRCOND(data=pr_cond_backend, period=ref_period)

# Marginals for simulations
wl_sim = IndicatorSimWL(obs=wl_pot, data=sl_sim_backend, period=fut_period)
pr_sim = IndicatorSimPRCOND(obs=pr_cond, data=pr_sim_backend, wl_pot=wl_pot_backend, period=fut_period)

# Copula for observations
jp_obs = IndicatorObsJP(obs_pot=wl_pot, obs_cond=pr_cond, name="wl_prcond", period=ref_period)

# Copula for simulations
jp_sim = IndicatorSimJP(sim_pot=wl_sim, sim_cond=pr_sim, obs_cop=jp_obs, period=fut_period)

# Example joint exceedance assessment for wl_prcond
result_obs = round(jp_obs.sf([wl_pot.isf(1/wl_rp).item(), pr_cond.isf(1/pr_rp).item()]).item(),2)
result_sim = round(jp_sim.sf([wl_pot.isf(1/wl_rp).item(), pr_cond.isf(1/pr_rp).item()]).item(),2)

result_text = (f"""
    The probability of precipitation exceeding a 1-in-{pr_rp} event and water levels exceeding a 1-in-{wl_rp} event
    during the reference period {ref_period} is {round((1/pr_rp)*(1/wl_rp), 4)} under the assumption of independence
    (0.1 x 0.1 = 0.01).

    Under the present analysis, the estimated probability of joint exceedance is {result_obs} for the reference period
    {ref_period} and {result_sim} for the future period {fut_period}.
    """)
print(result_text)
    The probability of precipitation exceeding a 1-in-10 event and water levels exceeding a 1-in-10 event
    during the reference period (1995, 2014) is 0.01 under the assumption of independence
    (0.1 x 0.1 = 0.01).

    Under the present analysis, the estimated probability of joint exceedance is 0.03 for the reference period
    (1995, 2014) and 0.43 for the future period (2070, 2100).
    

Step 6 - Joint analysis for pr_wlcond#

# Marginals for observations
pr_pot = IndicatorObsPRPOT(data=pr_pot_backend, period=ref_period)
wl_cond = IndicatorObsWLCOND(data=wl_cond_backend, period=ref_period)

# Marginals for simulations
pr_sim = IndicatorSimPRPOT(obs=pr_pot, data=pr_sim_backend, period=fut_period)
wl_sim = IndicatorSimWLCOND(obs=wl_cond, data=sl_sim_backend, period=fut_period)

# Copula for observations
jp_obs = IndicatorObsJP(obs_pot=pr_pot, obs_cond=wl_cond, name="pr_wlcond", period=ref_period)

# Copula for simulations
jp_sim = IndicatorSimJP(sim_pot=pr_sim, sim_cond=wl_sim, obs_cop=jp_obs, period=fut_period)

# Example joint exceedance assessment for pr_wlcond
result_obs = round(jp_obs.sf([pr_pot.isf(1/pr_rp).item(), wl_cond.isf(1/wl_rp).item()]).item(),2)
result_sim = round(jp_sim.sf([pr_pot.isf(1/pr_rp).item(), wl_cond.isf(1/wl_rp).item()]).item(),2)

result_text = (f"""
    The probability of precipitation exceeding a 1-in-{pr_rp} event and water levels exceeding a 1-in-{wl_rp} event
    during the reference period {ref_period} is {round((1/pr_rp)*(1/wl_rp), 4)} under the assumption of independence
    (0.1 x 0.1 = 0.01).

    Under the present analysis, the estimated probability of joint exceedance is {result_obs} for the reference period
    {ref_period} and {result_sim} for the future period {fut_period}.
    """)
print(result_text)
    The probability of precipitation exceeding a 1-in-10 event and water levels exceeding a 1-in-10 event
    during the reference period (1995, 2014) is 0.01 under the assumption of independence
    (0.1 x 0.1 = 0.01).

    Under the present analysis, the estimated probability of joint exceedance is 0.05 for the reference period
    (1995, 2014) and 0.12 for the future period (2070, 2100).
    
# The reliability of these results should be assessed based on confidence in event selection, marginal fits, and copula fit.

# Use the `dist` method to see which copula was selected and the `bic` method to see its Bayesian Information Criterion (BIC).
print(f'The BIC for the {jp_obs.dist} copula is: {round(jp_obs.bic.item(),4)}')
The BIC for the gumbel copula is: -0.0517