← Cuba hurricanes AA

8  Visualize Options (Forecast + Observational)

This chapter compares combinations of an action (forecast) trigger, using forecasts up to 3 days ahead, and an observational trigger. Every combination has the same overall return period: 3.7 years, or 7 triggering years in 2000-2024. The options presented to the working group are numbered as they were in the slides.

For both the forecast and the observational trigger we varied:

and looked for combinations that maximise:

Code
import ocha_stratus as stratus
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

from src.datasources import ibtracs
from src.datasources.ibtracs import knots2cat
from src.constants import *

8.1 Load and process data

8.1.1 Load Optimization Results

The optimization output: every trigger combination with a 3.7-year return period, with its impact metrics.

Code
blob_name = (
    f"{PROJECT_PREFIX}/processed/fcast_obsv_combined_trigger_metrics.parquet"
)
df_metrics = stratus.load_parquet_from_blob(blob_name)

8.1.2 Load Combined Storm Statistics

Forecast and observed statistics and impact data for each storm, used for the per-storm plots and tables below.

Code
blob_name = f"{PROJECT_PREFIX}/processed/fcast_obsv_combined_stats.parquet"
df_stats = stratus.load_parquet_from_blob(blob_name)

CERF started in 2006, so the target number of years for CERF comparisons is based on that shorter period.

Code
### Calculate CERF Target Years
# note that we have to pick a differet number of triggered years for CERF since it only started in 2006
cerf_target_years = int((2024 - 2006 + 1 + 1) / 3.7)

Years with CERF allocations and their amounts. There were 6, but the return period allows targeting at most 5.

Code
### Analyze Historical CERF Years
# we see that there were 6 CERF years, but we can only hope to target up to 5, because otherwise the RP is too low
df_stats[df_stats["cerf"]].groupby("year")[
    "Amount in US$"
].sum().reset_index().sort_values("Amount in US$", ascending=False)
year Amount in US$
5 2024 9499457
3 2017 7999469
4 2022 7827734
0 2008 7367516
1 2012 5522753
2 2016 5352736

The best possible CERF total is the sum over the top CERF years. Trigger options are compared against it.

Code
### Calculate Maximum Possible CERF Amount

max_cerf_amount = (
    df_stats[df_stats["cerf"]]
    .groupby("year")["Amount in US$"]
    .sum()
    .reset_index()
    .sort_values("Amount in US$", ascending=False)
    .iloc[:cerf_target_years]
    .sum()["Amount in US$"]
)

For people affected the target is 7 years, the 3.7-year return period over 2000-2024.

Code
### Set Target Years for Impact Analysis
target_years = 7

The best possible people-affected total is the sum over the 7 years with the most people affected.

Code
### Calculate Maximum Possible Total Affected
max_total_affected = (
    df_stats.groupby("year")["Total Affected"]
    .sum()
    .reset_index()
    .sort_values("Total Affected", ascending=False)
    .iloc[:target_years]
    .sum()["Total Affected"]
)

max_total_affected
np.int64(29450226)

8.1.3 Remove redundant optimizations

How many more years the forecast trigger fires than the observational one (n_years_diff). Zero means the two are balanced.

Code
df_metrics["n_years_diff"] = (
    df_metrics["n_years_fcast"] - df_metrics["n_years_obsv"]
)
df_metrics["n_years_diff_abs"] = df_metrics["n_years_diff"].abs()
df_metrics["n_years_total"] = (
    df_metrics["n_years_fcast"] + df_metrics["n_years_obsv"]
)

Many combinations use the same rainfall aggregation and give identical impact totals. From each such group we keep the one with the lowest thresholds.

Code
### Define Function to Remove Redundant Thresholds
def drop_redundant_thresholds(df, min_cols, id_cols, drop_high=True):
    for min_col in min_cols:
        unique_cols = [x for x in min_cols if x != min_col] + id_cols
        df = df.sort_values(min_col, ascending=drop_high).drop_duplicates(
            subset=unique_cols
        )
    return df
Code
### Define Column Categories

thresh_cols = [
    "fcast_wind",
    "fcast_rain_thresh",
    "obsv_wind",
    "obsv_rain_thresh",
]
rain_agg_cols = ["fcast_rain_col", "obsv_rain_col"]
impact_cols = [
    "Total Affected",
    "Total Deaths",
    "Total Damage, Adjusted ('000 US$)",
    "Amount in US$",
]
Code
df_metrics_lowest = drop_redundant_thresholds(
    df_metrics, thresh_cols, impact_cols + rain_agg_cols
)

The remaining combinations:

Code
### Display Filtered Results
df_metrics_lowest
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
332720 24745221.0 41.0 8135595.0 23194719.0 4.0 80.0 q50 41.205720 125.0 q50_obsv 2.44 7 4 3 3 11
198088 16341819.0 52.0 13714030.0 26242474.0 5.0 35.0 q95 141.738710 125.0 q50_obsv 2.44 7 4 3 3 11
22746 24843276.0 41.0 11268506.0 28547455.0 5.0 120.0 q80 70.166730 130.0 q50_obsv 2.44 7 4 3 3 11
82763 21735221.0 38.0 11280927.0 20719721.0 4.0 110.0 q80 78.203220 115.0 q50_obsv 2.44 7 6 1 1 13
314406 21495776.0 38.0 8110595.0 15366985.0 3.0 75.0 q50 42.807594 130.0 q50_obsv 2.44 7 4 3 3 11
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
25202 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 mean 21.936502 30.0 q80_obsv NaN 7 0 7 7 7
25203 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 mean 21.936502 30.0 q95_obsv NaN 7 0 7 7 7
100940 16292374.0 52.0 13714030.0 26242474.0 5.0 90.0 q95 140.547760 30.0 mean_obsv NaN 7 0 7 7 7
100943 16292374.0 52.0 13714030.0 26242474.0 5.0 90.0 q95 140.547760 30.0 q80_obsv NaN 7 0 7 7 7
328878 24695776.0 41.0 8135595.0 23194719.0 4.0 80.0 q50 41.205720 30.0 q80_obsv NaN 7 0 7 7 7

2543 rows × 16 columns

8.2 Visualization functions

8.2.1 Define Main Scatter Plot Function

A scatter plot of all combinations, people affected against CERF amount, with the best possible values marked.

Code
def plot_thresh_scatter(
    x="Total Affected",
    y="Amount in US$",
    color="n_years_diff_abs",
    zorder_rev=True,
    fcast_pref_only=False,
    same_wind=False,
    same_rain_col=False,
    zero_intercept=False,
    df=None,  # override default dataframe
):
    fig, ax = plt.subplots(figsize=(7, 7))

    if df is None:
        df_plot = df_metrics_lowest.copy()
    else:
        df_plot = df.copy()

    if same_wind:
        df_plot = df_plot[df_plot["fcast_wind"] == df_plot["obsv_wind"]]
    if same_rain_col:
        df_plot = df_plot[
            df_plot["fcast_rain_col"]
            == df_plot["obsv_rain_col"].str.removesuffix("_obsv")
        ]

    if fcast_pref_only:
        df_plot = df_plot[df_plot["n_years_diff"] >= 0]

    for n_years_diff_abs, group in df_plot.groupby(color):
        group.plot(
            x=x,
            y=y,
            marker=".",
            linewidth=0,
            alpha=1,
            label=n_years_diff_abs,
            ax=ax,
            zorder=-n_years_diff_abs if zorder_rev else n_years_diff_abs,
        )

    ax.axhline(max_cerf_amount, linestyle="--", color="dodgerblue")
    ax.axvline(max_total_affected, linestyle="--", color="dodgerblue")

    if zero_intercept:
        ax.set_xlim(left=0)
        ax.set_ylim(bottom=0)

    ax.legend(title=color)
    ax.set_ylabel(y)
    ax.set_xlabel(x)
    ax.spines.top.set_visible(False)
    ax.spines.right.set_visible(False)

    return fig, ax

Axis limit for the wind speed plots.

Code
### Get Maximum Wind Speed
max_wind = df_stats[["wind", "wind_obsv"]].max().max()

Apply one combination to the storms and flag which ones the forecast and observational triggers would catch.

Code
### Define Function to Get Triggered Storms
def get_triggered_storms(index):
    trig = df_metrics_lowest.loc[index]

    df_stats["fcast_trig"] = (df_stats["wind"] >= trig["fcast_wind"]) & (
        df_stats[trig["fcast_rain_col"]] >= trig["fcast_rain_thresh"]
    )

    df_stats["obsv_trig"] = (df_stats["wind_obsv"] >= trig["obsv_wind"]) & (
        df_stats[trig["obsv_rain_col"]] >= trig["obsv_rain_thresh"]
    )

    return df_stats

For one combination, plot each storm’s wind and rainfall against the thresholds, forecast and observed side by side. Bubble size is people affected and colour is CERF status.

Code
def plot_selected_threshs(index, impact_col="Total Affected"):
    trig_color = "gold"
    cerf_color = "crimson"

    fig, axs = plt.subplots(1, 2, figsize=(14, 7), dpi=200)

    trig = df_metrics_lowest.loc[index]
    # print(trig)

    df_stats = get_triggered_storms(index)

    figs = []
    for stage, ax in zip(["fcast", "obsv"], axs):
        other_stage = "fcast" if stage == "obsv" else "obsv"
        wind_col = "wind" if stage == "fcast" else "wind_obsv"
        wind_thresh = trig[f"{stage}_wind"]
        rain_col = trig[f"{stage}_rain_col"]
        rain_thresh = trig[f"{stage}_rain_thresh"]

        ymax = df_stats[rain_col].max() * 1.1
        xmax = max_wind * 1.1

        # fig, ax = plt.subplots(dpi=200, figsize=(7, 7))

        bubble_sizes = df_stats[impact_col].fillna(0)
        bubble_sizes_scaled = bubble_sizes / bubble_sizes.max() * 5000

        ax.scatter(
            df_stats[wind_col],
            df_stats[rain_col],
            s=bubble_sizes_scaled,
            c=df_stats["cerf"].apply(lambda x: cerf_color if x else "grey"),
            alpha=0.3,
            edgecolor="none",
            zorder=1,
        )

        for _, row in df_stats.iterrows():
            triggered = row[f"{stage}_trig"]
            other_triggered = row[f"{other_stage}_trig"]
            ax.annotate(
                row["name"].capitalize() + "\n" + str(row["year"]),
                (row[wind_col], row[rain_col]),
                ha="center",
                va="center",
                fontsize=6,
                color=cerf_color if row["cerf"] == True else "k",
                zorder=10 if row["cerf"] else 9,
                alpha=0.8,
                fontstyle="italic" if other_triggered else "normal",
                fontweight="bold" if triggered else "normal",
            )

        ax.axvline(
            wind_thresh,
            color=trig_color,
            linewidth=0.5,
            zorder=0,
        )
        ax.axhline(
            rain_thresh,
            color=trig_color,
            linewidth=0.5,
            zorder=0,
        )
        ax.add_patch(
            mpatches.Rectangle(
                (wind_thresh, rain_thresh),  # bottom left
                xmax - wind_thresh,  # width
                ymax - rain_thresh,  # height
                facecolor=trig_color,
                alpha=0.1,
                zorder=0,
            )
        )

        for cat_value, cat_name in CAT_LIMITS:
            ax.annotate(
                cat_name + " -",
                (cat_value, 0),
                fontstyle="italic",
                color="grey",
                rotation=90,
                va="top",
                ha="center",
                fontsize=8,
            )

        ax.annotate(
            f" {wind_thresh:.0f} ",
            (wind_thresh, 0),
            color=trig_color,
            rotation=90,
            fontsize=10,
            va="top",
            ha="center",
            fontweight="bold",
        )

        ax.annotate(
            f" {rain_thresh:.1f} ",
            (0, rain_thresh),
            color=trig_color,
            fontsize=10,
            va="center",
            ha="right",
            fontweight="bold",
        )

        if rain_col == "mean":
            rain_agg_str = "mean"
        else:
            q = rain_col.removeprefix("q").removesuffix("_obsv")
            if q == "50":
                rain_agg_str = "median"
            else:
                rain_agg_str = f"{q}th quantile"
        ax.set_ylabel(
            f"Two-day rainfall, {rain_agg_str} over whole country (mm)"
        )
        ax.set_xlabel("\nMax. wind speed while in ZMA (knots)")

        ax.set_xlim(left=0, right=xmax)
        ax.set_ylim(bottom=0, top=ymax)

        ax.set_title(
            "Action (forecast)" if stage == "fcast" else "Observational"
        )

        ax.spines.top.set_visible(False)
        ax.spines.right.set_visible(False)

        # figs.append((fig, ax))
    return fig, axs
Code
def set_cerf_str(row):
    if row["cerf"]:
        return "Yes"
    else:
        if row["year"] >= 2006:
            return "No"
        else:
            return "pre-"


def color_df(val):
    if val == "Yes":
        return "background-color: crimson"
    elif val == "No":
        return "background-color: dodgerblue"
    elif val == "Trig.":
        return "background-color: darkorange"
    else:
        return ""

A table of the storms a combination would trigger, with CERF status and people affected.

Code
### Define Detailed Storm Display Function

def disp_selected_threshs(index, impact_col="Total Affected"):
    trig_color = "gold"
    cerf_color = "crimson"

    df_disp = get_triggered_storms(index).copy()

    df_disp["CERF"] = df_disp.apply(set_cerf_str, axis=1)

    df_disp["Storm"] = (
        df_disp["name"].str.capitalize() + " " + df_disp["year"].astype(str)
    )
    df_disp["Action"] = df_disp["fcast_trig"].apply(
        lambda x: "Trig." if x else "No trig."
    )
    df_disp["Obsv."] = df_disp["obsv_trig"].apply(
        lambda x: "Trig." if x else "No trig."
    )

    cols = ["Action", "Obsv.", "CERF", impact_col]
    display(
        df_disp.set_index("Storm")[cols]
        .sort_values(impact_col, ascending=False)
        .style.bar(
            subset=impact_col,
            color="mediumpurple",
            # vmax=500000,
            props="width: 150px;",
        )
        .map(color_df)
        .set_table_styles(
            {
                impact_col: [
                    {"selector": "th", "props": [("text-align", "left")]},
                    {"selector": "td", "props": [("text-align", "left")]},
                ]
            }
        )
        .format({"Total Affected": "{:,}"})
    )

8.3 Plot possible combinations

Each point is one combination. The line at USD 28 million marks the top cluster of balanced combinations (option 1), where the forecast and observational triggers fire in the same number of years.

Code
fig, ax = plot_thresh_scatter(
    "Total Affected", "Amount in US$", "n_years_diff_abs"
)
ax.axhline(2.8e7)

8.4 Constrain options

8.4.1 Filter to Balanced Trigger Combinations

Combinations where the forecast and observational triggers fire in the same number of years:

Code
df_metrics_balanced = df_metrics_lowest[
    df_metrics_lowest["n_years_diff_abs"] == 0
]

8.4.2 Filter to High-CERF Balanced Combinations

Of those, the ones capturing at least USD 28 million of CERF allocations:

Code
df_metrics_balanced_high = df_metrics_balanced[
    df_metrics_balanced["Amount in US$"] >= 2.8e7
]
Code
df_metrics_balanced_high
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
71389 24843276.0 41.0 11268506.0 28547455.0 5.0 110.0 q95 143.712340 105.0 q50_obsv 45.957497 5 5 0 0 10
119458 25087281.0 45.0 12817901.0 28547455.0 5.0 100.0 q95 143.712340 105.0 q50_obsv 45.957497 5 5 0 0 10
119486 25187281.0 49.0 13866888.0 28547455.0 5.0 100.0 q95 143.409800 105.0 q50_obsv 45.957497 5 5 0 0 10
71417 24943276.0 45.0 12317493.0 28547455.0 5.0 110.0 q95 143.409800 105.0 q50_obsv 45.957497 5 5 0 0 10
60832 24865326.0 52.0 8098174.0 28717472.0 5.0 120.0 q50 26.483074 75.0 q50_obsv 45.957497 6 6 0 0 12
60935 24815881.0 52.0 8098174.0 28717472.0 5.0 120.0 q50 28.462177 75.0 q50_obsv 45.957497 6 6 0 0 12
191429 25154827.0 57.0 9647569.0 28717472.0 5.0 35.0 q80 96.462585 105.0 q80_obsv 96.369003 5 5 0 0 10
98070 25109331.0 56.0 9647569.0 28717472.0 5.0 90.0 q80 87.754250 105.0 q80_obsv 96.369003 5 5 0 0 10
313372 24865326.0 52.0 8098174.0 28717472.0 5.0 75.0 q80 103.080360 105.0 q80_obsv 96.369003 5 5 0 0 10
191479 24910822.0 53.0 8098174.0 28717472.0 5.0 35.0 q80 103.080360 105.0 q80_obsv 96.369003 5 5 0 0 10
312969 24865326.0 52.0 8098174.0 28717472.0 5.0 75.0 mean 59.768433 105.0 q80_obsv 96.369003 5 5 0 0 10
313322 25109331.0 56.0 9647569.0 28717472.0 5.0 75.0 q80 96.462585 105.0 q80_obsv 96.369003 5 5 0 0 10
191184 24910822.0 53.0 8098174.0 28717472.0 5.0 35.0 mean 59.768433 105.0 q80_obsv 96.369003 5 5 0 0 10

8.5 Plot Selected Balanced Trigger Combination

Option 1 (combination 60832):

Code
plot_selected_threshs(60832);

8.6 Best Total Affected

Option 2: the cluster with the most people affected.

8.6.1 Create Scatter Plot for High-Impact Analysis

The line at 27 million people marks that cluster.

Code
fig, ax = plot_thresh_scatter(
    "Total Affected", "Amount in US$", "n_years_diff_abs"
)
ax.axvline(2.7e7)

Combinations with at least 27 million people affected:

Code
### Filter for High-Impact Trigger Combinations
df_metrics_high_impact = df_metrics_lowest[
    df_metrics_lowest["Total Affected"] >= 2.7e7
]
Code
### Display High-Impact Combinations
df_metrics_high_impact
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
201131 28412062.0 52.0 4971882.0 32229929.0 5.0 35.0 mean 71.266540 75.0 q80_obsv 96.369003 4 7 -3 3 11
201388 28412062.0 52.0 4971882.0 32229929.0 5.0 35.0 q80 128.088730 75.0 q80_obsv 96.369003 3 7 -4 4 10
213486 28412062.0 52.0 4971882.0 32229929.0 5.0 NaN q80 35.698547 75.0 q80_obsv 96.369003 0 7 -7 7 7
201218 28412062.0 52.0 4971882.0 32229929.0 5.0 35.0 q50 80.616330 75.0 q80_obsv 96.369003 3 7 -4 4 10
321926 28412062.0 52.0 4971882.0 32229929.0 5.0 75.0 q50 65.069570 75.0 q80_obsv 96.369003 3 7 -4 4 10
321743 28412062.0 52.0 4971882.0 32229929.0 5.0 75.0 mean 69.391884 75.0 q80_obsv 96.369003 4 7 -3 3 11
201286 28457558.0 53.0 4971882.0 32229929.0 5.0 35.0 q50 65.069570 75.0 q80_obsv 96.369003 4 7 -3 3 11
213390 28412062.0 52.0 4971882.0 32229929.0 5.0 NaN q50 7.418959 75.0 q80_obsv 96.369003 0 7 -7 7 7
214074 28412062.0 52.0 4971882.0 32229929.0 5.0 NaN q95 67.368840 75.0 q80_obsv 96.369003 0 7 -7 7 7
213054 28412062.0 52.0 4971882.0 32229929.0 5.0 NaN mean 21.936502 75.0 q80_obsv 96.369003 0 7 -7 7 7
201114 28457558.0 53.0 4971882.0 32229929.0 5.0 35.0 mean 69.391884 75.0 q80_obsv 96.369003 4 7 -3 3 11
170489 28412062.0 52.0 4971882.0 32229929.0 5.0 130.0 q95 177.149400 75.0 q80_obsv 96.369003 1 7 -6 6 8
201608 28412062.0 52.0 4971882.0 32229929.0 5.0 35.0 q95 263.916200 75.0 q80_obsv 96.369003 1 7 -6 6 8

8.6.2 Visualize High-Impact Trigger Combination

Option 2 (combination 321743):

Code
plot_selected_threshs(321743);

Code
### Display Storm Details for High-Impact Combination
disp_selected_threshs(321743)
  Action Obsv. CERF Total Affected
Storm        
Irma 2017 Trig. Trig. Yes 10,000,000
Michelle 2001 No trig. Trig. pre- 5,900,012
Rafael 2024 No trig. Trig. Yes 4,000,000
Ian 2022 No trig. Trig. Yes 3,200,000
Ike 2008 Trig. Trig. Yes 2,600,000
Dennis 2005 Trig. Trig. pre- 2,500,000
Gustav 2008 No trig. No trig. Yes 450,019
Oscar 2024 No trig. No trig. Yes 320,000
Lili 2002 No trig. No trig. pre- 281,470
Charley 2004 No trig. No trig. pre- 244,005
Noel 2007 No trig. No trig. No 192,488
Matthew 2016 No trig. No trig. Yes 190,000
Sandy 2012 Trig. Trig. Yes 162,605
Wilma 2005 No trig. No trig. pre- 100,000
Paloma 2008 No trig. Trig. No 49,445
Isaac 2012 No trig. No trig. No 45,496
Isidore 2002 No trig. No trig. pre- 42,500
Alberto 2018 No trig. No trig. No 40,000
Alex 2022 No trig. No trig. No 6,780
Ivan 2004 No trig. No trig. pre- 3,245
Michael 2018 No trig. No trig. No 540
Isaias 2020 No trig. No trig. No 500
Alberto 2006 No trig. No trig. No 268
Ida 2021 No trig. No trig. No 0
Elsa 2021 No trig. No trig. No 0
Idalia 2023 No trig. No trig. No 0
Helene 2024 No trig. No trig. No 0
Dean 2007 No trig. No trig. No 0
Fay 2008 No trig. No trig. No 0

8.7 Best CERF amount

Option 3: the cluster with the highest CERF amount.

8.7.1 Create Scatter Plot for High-CERF Analysis

The line at USD 33 million marks that cluster.

Code
fig, ax = plot_thresh_scatter(
    "Total Affected", "Amount in US$", "n_years_diff_abs"
)
ax.axhline(3.3e7)

Combinations capturing at least USD 33 million:

Code
### Filter for High-CERF Trigger Combinations
df_metrics_high_cerf = df_metrics_lowest[
    df_metrics_lowest["Amount in US$"] >= 3.3e7
]
Code
### Display High-CERF Combinations
df_metrics_high_cerf
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
201619 25002636.0 52.0 10958627.0 34070208.0 6.0 35.0 q95 241.6073 75.0 q50_obsv 45.957497 2 6 -4 4 8
91667 25002636.0 52.0 10958627.0 34070208.0 6.0 110.0 q95 177.1494 75.0 q50_obsv 45.957497 3 6 -3 3 9

8.7.2 Visualize High-CERF Trigger Combination

Option 3 (combination 91667):

Code
plot_selected_threshs(91667);

Code
### Display Storm Details for High-CERF Combination
disp_selected_threshs(91667)
  Action Obsv. CERF Total Affected
Storm        
Irma 2017 Trig. Trig. Yes 10,000,000
Michelle 2001 No trig. Trig. pre- 5,900,012
Rafael 2024 No trig. No trig. Yes 4,000,000
Ian 2022 No trig. Trig. Yes 3,200,000
Ike 2008 Trig. Trig. Yes 2,600,000
Dennis 2005 No trig. Trig. pre- 2,500,000
Gustav 2008 No trig. Trig. Yes 450,019
Oscar 2024 No trig. No trig. Yes 320,000
Lili 2002 No trig. No trig. pre- 281,470
Charley 2004 No trig. No trig. pre- 244,005
Noel 2007 No trig. No trig. No 192,488
Matthew 2016 Trig. No trig. Yes 190,000
Sandy 2012 No trig. Trig. Yes 162,605
Wilma 2005 No trig. No trig. pre- 100,000
Paloma 2008 No trig. No trig. No 49,445
Isaac 2012 No trig. No trig. No 45,496
Isidore 2002 No trig. No trig. pre- 42,500
Alberto 2018 No trig. No trig. No 40,000
Alex 2022 No trig. No trig. No 6,780
Ivan 2004 No trig. No trig. pre- 3,245
Michael 2018 No trig. No trig. No 540
Isaias 2020 No trig. No trig. No 500
Alberto 2006 No trig. No trig. No 268
Ida 2021 No trig. No trig. No 0
Elsa 2021 No trig. No trig. No 0
Idalia 2023 No trig. No trig. No 0
Helene 2024 No trig. No trig. No 0
Dean 2007 No trig. No trig. No 0
Fay 2008 No trig. No trig. No 0

8.8 Two years diff

A small cluster where the forecast trigger fires two years more or fewer than the observational one. Nothing stood out, so it was not presented to the working group.

Code
### Analyze Two-Year Difference Combinations
fig, ax = plot_thresh_scatter(
    "Total Affected", "Amount in US$", "n_years_diff_abs"
)
ax.axhline(3.0e7)

Code
### Filter for Two-Year Difference Combinations
df_twoyears = df_metrics_lowest[
    (df_metrics_lowest["n_years_diff_abs"] == 2)
    & (df_metrics_lowest["Amount in US$"] >= 3.0e7)
]
Code
df_twoyears
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
312998 24602062.0 52.0 8142214.0 31582777.0 5.0 75.0 mean 69.391884 105.0 q80_obsv 96.217003 4 6 -2 2 10
191213 24647558.0 53.0 8142214.0 31582777.0 5.0 35.0 mean 69.391884 105.0 q80_obsv 96.217003 4 6 -2 2 10
191235 24602062.0 52.0 8142214.0 31582777.0 5.0 35.0 mean 71.266540 105.0 q80_obsv 96.217003 4 6 -2 2 10
Code
plot_selected_threshs(312998);

Code
disp_selected_threshs(312998)
  Action Obsv. CERF Total Affected
Storm        
Irma 2017 Trig. Trig. Yes 10,000,000
Michelle 2001 No trig. Trig. pre- 5,900,012
Rafael 2024 No trig. No trig. Yes 4,000,000
Ian 2022 No trig. Trig. Yes 3,200,000
Ike 2008 Trig. Trig. Yes 2,600,000
Dennis 2005 Trig. Trig. pre- 2,500,000
Gustav 2008 No trig. No trig. Yes 450,019
Oscar 2024 No trig. No trig. Yes 320,000
Lili 2002 No trig. No trig. pre- 281,470
Charley 2004 No trig. No trig. pre- 244,005
Noel 2007 No trig. No trig. No 192,488
Matthew 2016 No trig. Trig. Yes 190,000
Sandy 2012 Trig. No trig. Yes 162,605
Wilma 2005 No trig. No trig. pre- 100,000
Paloma 2008 No trig. Trig. No 49,445
Isaac 2012 No trig. No trig. No 45,496
Isidore 2002 No trig. No trig. pre- 42,500
Alberto 2018 No trig. No trig. No 40,000
Alex 2022 No trig. No trig. No 6,780
Ivan 2004 No trig. No trig. pre- 3,245
Michael 2018 No trig. No trig. No 540
Isaias 2020 No trig. No trig. No 500
Alberto 2006 No trig. No trig. No 268
Ida 2021 No trig. No trig. No 0
Elsa 2021 No trig. No trig. No 0
Idalia 2023 No trig. No trig. No 0
Helene 2024 No trig. No trig. No 0
Dean 2007 No trig. No trig. No 0
Fay 2008 No trig. No trig. No 0

8.9 One year diff

Option 4: a small cluster with the second-highest number of people affected, where the two triggers differ by one year.

Code
fig, ax = plot_thresh_scatter(
    "Total Affected", "Amount in US$", "n_years_diff_abs"
)
ax.axvline(2.55e7)

Code
df_oneyear = df_metrics_lowest[
    (df_metrics_lowest["Total Affected"] >= 2.55e7)
    & (df_metrics_lowest["n_years_diff_abs"] == 1)
]
Code
df_oneyear
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
201091 25710822.0 50.0 8073174.0 26889626.0 5.0 35.0 mean 59.768433 75.0 q80_obsv 98.341003 5 6 -1 1 11
201377 25710822.0 50.0 8073174.0 26889626.0 5.0 35.0 q80 103.080360 75.0 q80_obsv 98.341003 5 6 -1 1 11
111447 25909331.0 53.0 9622569.0 26889626.0 5.0 90.0 q80 87.754250 75.0 q80_obsv 98.341003 5 6 -1 1 11
60958 25665326.0 49.0 8073174.0 26889626.0 5.0 120.0 q50 42.302090 75.0 q80_obsv 98.341003 5 6 -1 1 11
322018 25665326.0 49.0 8073174.0 26889626.0 5.0 75.0 q80 103.080360 75.0 q80_obsv 98.341003 5 6 -1 1 11
321720 25665326.0 49.0 8073174.0 26889626.0 5.0 75.0 mean 59.768433 75.0 q80_obsv 98.341003 5 6 -1 1 11
201331 25954827.0 54.0 9622569.0 26889626.0 5.0 35.0 q80 96.462585 75.0 q80_obsv 98.341003 5 6 -1 1 11
321972 25909331.0 53.0 9622569.0 26889626.0 5.0 75.0 q80 96.462585 75.0 q80_obsv 98.341003 5 6 -1 1 11
Code
plot_selected_threshs(321972);

Code
disp_selected_threshs(321972)
  Action Obsv. CERF Total Affected
Storm        
Irma 2017 Trig. Trig. Yes 10,000,000
Michelle 2001 No trig. Trig. pre- 5,900,012
Rafael 2024 No trig. Trig. Yes 4,000,000
Ian 2022 No trig. No trig. Yes 3,200,000
Ike 2008 Trig. Trig. Yes 2,600,000
Dennis 2005 Trig. Trig. pre- 2,500,000
Gustav 2008 Trig. No trig. Yes 450,019
Oscar 2024 No trig. No trig. Yes 320,000
Lili 2002 No trig. No trig. pre- 281,470
Charley 2004 Trig. No trig. pre- 244,005
Noel 2007 No trig. No trig. No 192,488
Matthew 2016 No trig. No trig. Yes 190,000
Sandy 2012 Trig. Trig. Yes 162,605
Wilma 2005 No trig. No trig. pre- 100,000
Paloma 2008 No trig. Trig. No 49,445
Isaac 2012 No trig. No trig. No 45,496
Isidore 2002 No trig. No trig. pre- 42,500
Alberto 2018 No trig. No trig. No 40,000
Alex 2022 No trig. No trig. No 6,780
Ivan 2004 Trig. No trig. pre- 3,245
Michael 2018 No trig. No trig. No 540
Isaias 2020 No trig. No trig. No 500
Alberto 2006 No trig. No trig. No 268
Ida 2021 No trig. No trig. No 0
Elsa 2021 No trig. No trig. No 0
Idalia 2023 No trig. No trig. No 0
Helene 2024 No trig. No trig. No 0
Dean 2007 No trig. No trig. No 0
Fay 2008 No trig. No trig. No 0

8.10 Forecast preference

How far the trigger can lean towards the forecast. The results are variants of option 1.

Code
fig, ax = plot_thresh_scatter(
    "Total Affected",
    "Amount in US$",
    "n_years_diff",
    fcast_pref_only=True,
    zorder_rev=False,
)
ax.axhline(2.7e7)
ax.axvline(2.3e7)

Code
df_fcast_pref = df_metrics_lowest[
    (df_metrics_lowest["Total Affected"] >= 2.3e7)
    & (df_metrics_lowest["Amount in US$"] >= 2.7e7)
    & (df_metrics_lowest["n_years_diff"] >= 0)
]
Code
df_fcast_pref
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
22746 24843276.0 41.0 11268506.0 28547455.0 5.0 120.0 q80 70.166730 130.0 q50_obsv 2.44 7 4 3 3 11
38993 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 mean 21.936502 115.0 q50_obsv 2.44 7 6 1 1 13
44003 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 q95 67.368840 115.0 q50_obsv 2.44 7 6 1 1 13
39109 24892721.0 41.0 11268506.0 28547455.0 5.0 120.0 mean 26.405380 115.0 q50_obsv 2.44 7 6 1 1 13
42119 24892721.0 41.0 11268506.0 28547455.0 5.0 120.0 q80 38.042336 115.0 q50_obsv 2.44 7 6 1 1 13
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
25792 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 q95 67.368840 30.0 q95_obsv NaN 7 0 7 7 7
25421 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 q50 7.418959 30.0 q80_obsv NaN 7 0 7 7 7
25422 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 q50 7.418959 30.0 q95_obsv NaN 7 0 7 7 7
25202 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 mean 21.936502 30.0 q80_obsv NaN 7 0 7 7 7
25203 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 mean 21.936502 30.0 q95_obsv NaN 7 0 7 7 7

160 rows × 16 columns

Code
df_fcast_only = df_fcast_pref[df_fcast_pref["n_years_diff"] == 0]
Code
df_fcast_only
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
71389 24843276.0 41.0 11268506.0 28547455.0 5.0 110.0 q95 143.712340 105.0 q50_obsv 45.957497 5 5 0 0 10
119458 25087281.0 45.0 12817901.0 28547455.0 5.0 100.0 q95 143.712340 105.0 q50_obsv 45.957497 5 5 0 0 10
119486 25187281.0 49.0 13866888.0 28547455.0 5.0 100.0 q95 143.409800 105.0 q50_obsv 45.957497 5 5 0 0 10
71417 24943276.0 45.0 12317493.0 28547455.0 5.0 110.0 q95 143.409800 105.0 q50_obsv 45.957497 5 5 0 0 10
60832 24865326.0 52.0 8098174.0 28717472.0 5.0 120.0 q50 26.483074 75.0 q50_obsv 45.957497 6 6 0 0 12
60935 24815881.0 52.0 8098174.0 28717472.0 5.0 120.0 q50 28.462177 75.0 q50_obsv 45.957497 6 6 0 0 12
191429 25154827.0 57.0 9647569.0 28717472.0 5.0 35.0 q80 96.462585 105.0 q80_obsv 96.369003 5 5 0 0 10
98070 25109331.0 56.0 9647569.0 28717472.0 5.0 90.0 q80 87.754250 105.0 q80_obsv 96.369003 5 5 0 0 10
313372 24865326.0 52.0 8098174.0 28717472.0 5.0 75.0 q80 103.080360 105.0 q80_obsv 96.369003 5 5 0 0 10
191479 24910822.0 53.0 8098174.0 28717472.0 5.0 35.0 q80 103.080360 105.0 q80_obsv 96.369003 5 5 0 0 10
312969 24865326.0 52.0 8098174.0 28717472.0 5.0 75.0 mean 59.768433 105.0 q80_obsv 96.369003 5 5 0 0 10
313322 25109331.0 56.0 9647569.0 28717472.0 5.0 75.0 q80 96.462585 105.0 q80_obsv 96.369003 5 5 0 0 10
191184 24910822.0 53.0 8098174.0 28717472.0 5.0 35.0 mean 59.768433 105.0 q80_obsv 96.369003 5 5 0 0 10
Code
plot_selected_threshs(25477)  # not sure if worth leaving in book.;

8.11 Simplified triggers (Phase 1)

Only combinations where the forecast and observational triggers use the same rainfall aggregation and the same wind speed threshold.

Code
df_simplified = df_metrics_lowest[
    (
        df_metrics_lowest["fcast_rain_col"]
        == df_metrics_lowest["obsv_rain_col"].str.removesuffix("_obsv")
    )
    & (df_metrics_lowest["fcast_wind"] == df_metrics_lowest["obsv_wind"])
]
Code
df_metrics_lowest[
    (df_metrics_lowest["fcast_wind"] == df_metrics_lowest["obsv_wind"])
].sort_values("Total Affected", ascending=False)
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
321743 28412062.0 52.0 4971882.0 32229929.0 5.0 75.0 mean 69.391884 75.0 q80_obsv 96.369003 4 7 -3 3 11
321926 28412062.0 52.0 4971882.0 32229929.0 5.0 75.0 q50 65.069570 75.0 q80_obsv 96.369003 3 7 -4 4 10
321972 25909331.0 53.0 9622569.0 26889626.0 5.0 75.0 q80 96.462585 75.0 q80_obsv 98.341003 5 6 -1 1 11
322018 25665326.0 49.0 8073174.0 26889626.0 5.0 75.0 q80 103.080360 75.0 q80_obsv 98.341003 5 6 -1 1 11
321720 25665326.0 49.0 8073174.0 26889626.0 5.0 75.0 mean 59.768433 75.0 q80_obsv 98.341003 5 6 -1 1 11
321846 25215307.0 49.0 5256761.0 24402195.0 4.0 75.0 q50 61.708008 75.0 q80_obsv 98.341003 4 6 -2 2 10
321994 25215307.0 49.0 5256761.0 24402195.0 4.0 75.0 q80 106.357285 75.0 q80_obsv 98.341003 5 6 -1 1 11
321679 25215307.0 49.0 5256761.0 24402195.0 4.0 75.0 mean 63.864970 75.0 q80_obsv 98.341003 5 6 -1 1 11
321923 25136606.0 55.0 7863137.0 28717472.0 5.0 75.0 q50 65.069570 75.0 q50_obsv 35.949997 3 7 -4 4 10
321740 25136606.0 55.0 7863137.0 28717472.0 5.0 75.0 mean 69.391884 75.0 q50_obsv 35.949997 4 7 -3 3 11
321970 25059886.0 56.0 9647569.0 28717472.0 5.0 75.0 q80 96.462585 75.0 q50_obsv 45.957497 5 6 -1 1 11
321924 24855136.0 52.0 7825716.0 28717472.0 5.0 75.0 q50 65.069570 75.0 q50_obsv 38.012497 3 7 -4 4 10
321741 24855136.0 52.0 7825716.0 28717472.0 5.0 75.0 mean 69.391884 75.0 q50_obsv 38.012497 4 7 -3 3 11
321718 24815881.0 52.0 8098174.0 28717472.0 5.0 75.0 mean 59.768433 75.0 q50_obsv 45.957497 5 6 -1 1 11
321844 24815881.0 52.0 8098174.0 28717472.0 5.0 75.0 q50 61.708008 75.0 q50_obsv 45.957497 4 6 -2 2 10
322016 24815881.0 52.0 8098174.0 28717472.0 5.0 75.0 q80 103.080360 75.0 q50_obsv 45.957497 5 6 -1 1 11
321920 24454562.0 52.0 5009303.0 26230041.0 4.0 75.0 q50 65.069570 75.0 mean_obsv 51.583454 3 7 -4 4 10
321737 24454562.0 52.0 5009303.0 26230041.0 4.0 75.0 mean 69.391884 75.0 mean_obsv 51.583454 4 7 -3 3 11
321678 24365862.0 52.0 5281761.0 26230041.0 4.0 75.0 mean 63.864970 75.0 q50_obsv 47.079998 5 6 -1 1 11
321845 24365862.0 52.0 5281761.0 26230041.0 4.0 75.0 q50 61.708008 75.0 q50_obsv 47.079998 4 6 -2 2 10
321993 24365862.0 52.0 5281761.0 26230041.0 4.0 75.0 q80 106.357285 75.0 q50_obsv 47.079998 5 6 -1 1 11
321965 21951831.0 53.0 9659990.0 20889738.0 4.0 75.0 q80 96.462585 75.0 mean_obsv 55.230145 5 6 -1 1 11
321967 21902386.0 53.0 9659990.0 20889738.0 4.0 75.0 q80 96.462585 75.0 mean_obsv 74.488056 5 6 -1 1 11
321644 21707826.0 49.0 8110595.0 20889738.0 4.0 75.0 mean 58.365227 75.0 mean_obsv 55.230145 6 6 0 0 12
322011 21707826.0 49.0 8110595.0 20889738.0 4.0 75.0 q80 103.080360 75.0 mean_obsv 55.230145 5 6 -1 1 11
321660 21707826.0 49.0 8110595.0 20889738.0 4.0 75.0 mean 58.365227 75.0 q80_obsv 103.079987 6 5 1 1 11
321655 21658381.0 49.0 8110595.0 20889738.0 4.0 75.0 mean 58.365227 75.0 q50_obsv 58.065002 6 5 1 1 11
321647 21658381.0 49.0 8110595.0 20889738.0 4.0 75.0 mean 58.365227 75.0 mean_obsv 74.488056 6 6 0 0 12
322013 21658381.0 49.0 8110595.0 20889738.0 4.0 75.0 q80 103.080360 75.0 mean_obsv 74.488056 5 6 -1 1 11
321659 21658381.0 49.0 8110595.0 20889738.0 4.0 75.0 mean 58.365227 75.0 q80_obsv 112.148994 6 5 1 1 11
321810 21495776.0 38.0 8110595.0 15366985.0 3.0 75.0 q50 42.807594 75.0 q50_obsv 60.637505 7 4 3 3 11
321747 21444562.0 49.0 8154635.0 23755043.0 4.0 75.0 mean 69.391884 75.0 q95_obsv 186.113480 4 7 -3 3 11
321930 21444562.0 49.0 8154635.0 23755043.0 4.0 75.0 q50 65.069570 75.0 q95_obsv 186.113480 3 7 -4 4 10
321672 21257807.0 49.0 5294182.0 18402307.0 3.0 75.0 mean 63.864970 75.0 mean_obsv 55.230145 5 6 -1 1 11
321829 21257807.0 49.0 5294182.0 18402307.0 3.0 75.0 q50 55.458720 75.0 q80_obsv 103.079987 5 5 0 0 10
321821 21257807.0 49.0 5294182.0 18402307.0 3.0 75.0 q50 55.458720 75.0 mean_obsv 55.230145 5 6 -1 1 11
321987 21257807.0 49.0 5294182.0 18402307.0 3.0 75.0 q80 106.357285 75.0 mean_obsv 55.230145 5 6 -1 1 11
321674 21208362.0 49.0 5294182.0 18402307.0 3.0 75.0 mean 63.864970 75.0 mean_obsv 74.488056 5 6 -1 1 11
321989 21208362.0 49.0 5294182.0 18402307.0 3.0 75.0 q80 106.357285 75.0 mean_obsv 74.488056 5 6 -1 1 11
321823 21208362.0 49.0 5294182.0 18402307.0 3.0 75.0 q50 55.458720 75.0 mean_obsv 74.488056 5 6 -1 1 11
321826 21208362.0 49.0 5294182.0 18402307.0 3.0 75.0 q50 55.458720 75.0 q50_obsv 58.065002 5 5 0 0 10
321828 21208362.0 49.0 5294182.0 18402307.0 3.0 75.0 q50 55.458720 75.0 q80_obsv 112.148994 5 5 0 0 10
321960 21045757.0 38.0 5294182.0 12879554.0 2.0 75.0 q50 47.932934 75.0 q50_obsv 60.637505 6 4 2 2 10
Code
plot_thresh_scatter(
    "Total Affected",
    "Amount in US$",
    color="n_years_diff_abs",
    zero_intercept=True,
);

Code
plot_thresh_scatter(
    "Total Affected",
    "Amount in US$",
    color="n_years_diff_abs",
    same_wind=True,
    zero_intercept=True,
);

Code
plot_thresh_scatter(
    "Total Affected",
    "Amount in US$",
    color="n_years_diff_abs",
    same_rain_col=True,
    zero_intercept=True,
);

Code
plot_thresh_scatter(
    "Total Affected",
    "Amount in US$",
    color="n_years_diff_abs",
    same_rain_col=True,
    same_wind=True,
    zero_intercept=True,
);

Code
df_simplified.sort_values(
    ["Amount in US$", "Total Affected"],
    ascending=False,
)
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
321923 25136606.0 55.0 7863137.0 28717472.0 5.0 75.0 q50 65.069570 75.0 q50_obsv 35.949997 3 7 -4 4 10
321924 24855136.0 52.0 7825716.0 28717472.0 5.0 75.0 q50 65.069570 75.0 q50_obsv 38.012497 3 7 -4 4 10
321844 24815881.0 52.0 8098174.0 28717472.0 5.0 75.0 q50 61.708008 75.0 q50_obsv 45.957497 4 6 -2 2 10
321972 25909331.0 53.0 9622569.0 26889626.0 5.0 75.0 q80 96.462585 75.0 q80_obsv 98.341003 5 6 -1 1 11
322018 25665326.0 49.0 8073174.0 26889626.0 5.0 75.0 q80 103.080360 75.0 q80_obsv 98.341003 5 6 -1 1 11
321737 24454562.0 52.0 5009303.0 26230041.0 4.0 75.0 mean 69.391884 75.0 mean_obsv 51.583454 4 7 -3 3 11
321845 24365862.0 52.0 5281761.0 26230041.0 4.0 75.0 q50 61.708008 75.0 q50_obsv 47.079998 4 6 -2 2 10
321994 25215307.0 49.0 5256761.0 24402195.0 4.0 75.0 q80 106.357285 75.0 q80_obsv 98.341003 5 6 -1 1 11
321644 21707826.0 49.0 8110595.0 20889738.0 4.0 75.0 mean 58.365227 75.0 mean_obsv 55.230145 6 6 0 0 12
321647 21658381.0 49.0 8110595.0 20889738.0 4.0 75.0 mean 58.365227 75.0 mean_obsv 74.488056 6 6 0 0 12
321672 21257807.0 49.0 5294182.0 18402307.0 3.0 75.0 mean 63.864970 75.0 mean_obsv 55.230145 5 6 -1 1 11
321826 21208362.0 49.0 5294182.0 18402307.0 3.0 75.0 q50 55.458720 75.0 q50_obsv 58.065002 5 5 0 0 10
321674 21208362.0 49.0 5294182.0 18402307.0 3.0 75.0 mean 63.864970 75.0 mean_obsv 74.488056 5 6 -1 1 11
321810 21495776.0 38.0 8110595.0 15366985.0 3.0 75.0 q50 42.807594 75.0 q50_obsv 60.637505 7 4 3 3 11
321960 21045757.0 38.0 5294182.0 12879554.0 2.0 75.0 q50 47.932934 75.0 q50_obsv 60.637505 6 4 2 2 10
Code
plot_selected_threshs(321844);

Code
disp_selected_threshs(321844)
  Action Obsv. CERF Total Affected
Storm        
Irma 2017 Trig. Trig. Yes 10,000,000
Michelle 2001 No trig. Trig. pre- 5,900,012
Rafael 2024 No trig. No trig. Yes 4,000,000
Ian 2022 No trig. Trig. Yes 3,200,000
Ike 2008 Trig. Trig. Yes 2,600,000
Dennis 2005 Trig. Trig. pre- 2,500,000
Gustav 2008 No trig. Trig. Yes 450,019
Oscar 2024 No trig. No trig. Yes 320,000
Lili 2002 No trig. No trig. pre- 281,470
Charley 2004 No trig. No trig. pre- 244,005
Noel 2007 No trig. No trig. No 192,488
Matthew 2016 No trig. No trig. Yes 190,000
Sandy 2012 No trig. Trig. Yes 162,605
Wilma 2005 No trig. No trig. pre- 100,000
Paloma 2008 No trig. No trig. No 49,445
Isaac 2012 No trig. No trig. No 45,496
Isidore 2002 No trig. No trig. pre- 42,500
Alberto 2018 No trig. No trig. No 40,000
Alex 2022 No trig. No trig. No 6,780
Ivan 2004 Trig. No trig. pre- 3,245
Michael 2018 No trig. No trig. No 540
Isaias 2020 No trig. No trig. No 500
Alberto 2006 No trig. No trig. No 268
Ida 2021 No trig. No trig. No 0
Elsa 2021 No trig. No trig. No 0
Idalia 2023 No trig. No trig. No 0
Helene 2024 No trig. No trig. No 0
Dean 2007 No trig. No trig. No 0
Fay 2008 No trig. No trig. No 0
Code
df_metrics_lowest
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total
332720 24745221.0 41.0 8135595.0 23194719.0 4.0 80.0 q50 41.205720 125.0 q50_obsv 2.44 7 4 3 3 11
198088 16341819.0 52.0 13714030.0 26242474.0 5.0 35.0 q95 141.738710 125.0 q50_obsv 2.44 7 4 3 3 11
22746 24843276.0 41.0 11268506.0 28547455.0 5.0 120.0 q80 70.166730 130.0 q50_obsv 2.44 7 4 3 3 11
82763 21735221.0 38.0 11280927.0 20719721.0 4.0 110.0 q80 78.203220 115.0 q50_obsv 2.44 7 6 1 1 13
314406 21495776.0 38.0 8110595.0 15366985.0 3.0 75.0 q50 42.807594 130.0 q50_obsv 2.44 7 4 3 3 11
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
25202 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 mean 21.936502 30.0 q80_obsv NaN 7 0 7 7 7
25203 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 mean 21.936502 30.0 q95_obsv NaN 7 0 7 7 7
100940 16292374.0 52.0 13714030.0 26242474.0 5.0 90.0 q95 140.547760 30.0 mean_obsv NaN 7 0 7 7 7
100943 16292374.0 52.0 13714030.0 26242474.0 5.0 90.0 q95 140.547760 30.0 q80_obsv NaN 7 0 7 7 7
328878 24695776.0 41.0 8135595.0 23194719.0 4.0 80.0 q50 41.205720 30.0 q80_obsv NaN 7 0 7 7 7

2543 rows × 16 columns

8.12 “Reasonable” triggers (Phase 2)

Combinations that meet all of:

  • Same rainfall aggregation, limited to q50 and q80
  • Windspeed thresholds within one category of each other
  • Forecast wind speed is >= to observational wind speed
  • Windspeed thresholds at least Cat. 1 (64 knots)
Code
for stage in ["fcast", "obsv"]:
    df_metrics_lowest[f"{stage}_cat"] = df_metrics_lowest[
        f"{stage}_wind"
    ].apply(knots2cat)

df_metrics_lowest["cat_diff"] = (
    df_metrics_lowest["fcast_cat"] - df_metrics_lowest["obsv_cat"]
)

df_metrics_lowest["min_cat"] = df_metrics_lowest[
    ["fcast_cat", "obsv_cat"]
].min(axis=1)
Code
df_metrics_reasonable = df_metrics_lowest[
    (df_metrics_lowest["cat_diff"].isin([0, 1]))
    & (df_metrics_lowest["min_cat"] >= 1)
    & (
        df_metrics_lowest["fcast_rain_col"]
        == df_metrics_lowest["obsv_rain_col"].str.removesuffix("_obsv")
    )
    & (df_metrics_lowest["fcast_rain_col"].isin(["q50", "q80"]))
].copy()
Code
len(df_metrics_reasonable)
59
Code
fig, ax = plot_thresh_scatter(
    df=df_metrics_reasonable,
    zero_intercept=True,
    # color="n_years_diff",
    # zorder_rev=False,
)

Code
fig, ax = plot_thresh_scatter(
    df=df_metrics_reasonable,
    zero_intercept=True,
    # color="n_years_diff",
    # zorder_rev=False,
)
ax.axhline(2.8e7)
ax.axvline(2.55e7)

Code
df_metrics_reasonable_bestcerf = df_metrics_reasonable[
    df_metrics_reasonable["Amount in US$"] >= 2.8e7
]
Code
df_metrics_reasonable_bestcerf[
    df_metrics_reasonable_bestcerf["n_years_diff"] == 1
]
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total fcast_cat obsv_cat cat_diff min_cat
40495 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 q50 7.418959 115.0 q50_obsv 2.440000 7 6 1 1 13 4 4 0 4
40206 24892721.0 41.0 11268506.0 28547455.0 5.0 120.0 q50 17.138527 115.0 q50_obsv 2.440000 7 6 1 1 13 4 4 0 4
42148 24892721.0 41.0 11268506.0 28547455.0 5.0 120.0 q80 38.042336 115.0 q80_obsv 12.709001 7 6 1 1 13 4 4 0 4
40872 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 q80 35.698547 115.0 q80_obsv 12.709001 7 6 1 1 13 4 4 0 4
13724 24892721.0 41.0 11268506.0 28547455.0 5.0 120.0 q80 38.042336 105.0 q80_obsv 96.217003 7 6 1 1 13 4 3 1 3
13136 24992721.0 45.0 12317493.0 28547455.0 5.0 120.0 q80 35.698547 105.0 q80_obsv 96.217003 7 6 1 1 13 4 3 1 3

8.12.1 Option 1b

Among the reasonable combinations: at least USD 28 million of CERF allocations, closest to balanced (n_years_diff == 1), and the lowest observational wind threshold.

Code
plot_selected_threshs(13136);

Code
disp_selected_threshs(13136)
  Action Obsv. CERF Total Affected
Storm        
Irma 2017 Trig. Trig. Yes 10,000,000
Michelle 2001 Trig. Trig. pre- 5,900,012
Rafael 2024 No trig. No trig. Yes 4,000,000
Ian 2022 Trig. Trig. Yes 3,200,000
Ike 2008 Trig. Trig. Yes 2,600,000
Dennis 2005 Trig. Trig. pre- 2,500,000
Gustav 2008 Trig. No trig. Yes 450,019
Oscar 2024 No trig. No trig. Yes 320,000
Lili 2002 No trig. No trig. pre- 281,470
Charley 2004 No trig. No trig. pre- 244,005
Noel 2007 No trig. No trig. No 192,488
Matthew 2016 Trig. Trig. Yes 190,000
Sandy 2012 No trig. No trig. Yes 162,605
Wilma 2005 Trig. No trig. pre- 100,000
Paloma 2008 Trig. Trig. No 49,445
Isaac 2012 No trig. No trig. No 45,496
Isidore 2002 No trig. No trig. pre- 42,500
Alberto 2018 No trig. No trig. No 40,000
Alex 2022 No trig. No trig. No 6,780
Ivan 2004 Trig. No trig. pre- 3,245
Michael 2018 No trig. No trig. No 540
Isaias 2020 No trig. No trig. No 500
Alberto 2006 No trig. No trig. No 268
Ida 2021 No trig. No trig. No 0
Elsa 2021 No trig. No trig. No 0
Idalia 2023 No trig. No trig. No 0
Helene 2024 No trig. No trig. No 0
Dean 2007 No trig. No trig. No 0
Fay 2008 No trig. No trig. No 0

8.12.2 Option 4

Maximising people affected among the reasonable combinations gives option 4 again.

Code
df_metrics_reasonable_bestimpact = df_metrics_reasonable[
    df_metrics_reasonable["Total Affected"] >= 2.55e7
]
Code
df_metrics_reasonable_bestimpact
Total Affected Total Deaths Total Damage, Adjusted ('000 US$) Amount in US$ cerf fcast_wind fcast_rain_col fcast_rain_thresh obsv_wind obsv_rain_col obsv_rain_thresh n_years_fcast n_years_obsv n_years_diff n_years_diff_abs n_years_total fcast_cat obsv_cat cat_diff min_cat
111447 25909331.0 53.0 9622569.0 26889626.0 5.0 90.0 q80 87.754250 75.0 q80_obsv 98.341003 5 6 -1 1 11 2 1 1 1
322018 25665326.0 49.0 8073174.0 26889626.0 5.0 75.0 q80 103.080360 75.0 q80_obsv 98.341003 5 6 -1 1 11 1 1 0 1
321972 25909331.0 53.0 9622569.0 26889626.0 5.0 75.0 q80 96.462585 75.0 q80_obsv 98.341003 5 6 -1 1 11 1 1 0 1
Code
plot_selected_threshs(321972);

Code
disp_selected_threshs(321972)
  Action Obsv. CERF Total Affected
Storm        
Irma 2017 Trig. Trig. Yes 10,000,000
Michelle 2001 No trig. Trig. pre- 5,900,012
Rafael 2024 No trig. Trig. Yes 4,000,000
Ian 2022 No trig. No trig. Yes 3,200,000
Ike 2008 Trig. Trig. Yes 2,600,000
Dennis 2005 Trig. Trig. pre- 2,500,000
Gustav 2008 Trig. No trig. Yes 450,019
Oscar 2024 No trig. No trig. Yes 320,000
Lili 2002 No trig. No trig. pre- 281,470
Charley 2004 Trig. No trig. pre- 244,005
Noel 2007 No trig. No trig. No 192,488
Matthew 2016 No trig. No trig. Yes 190,000
Sandy 2012 Trig. Trig. Yes 162,605
Wilma 2005 No trig. No trig. pre- 100,000
Paloma 2008 No trig. Trig. No 49,445
Isaac 2012 No trig. No trig. No 45,496
Isidore 2002 No trig. No trig. pre- 42,500
Alberto 2018 No trig. No trig. No 40,000
Alex 2022 No trig. No trig. No 6,780
Ivan 2004 Trig. No trig. pre- 3,245
Michael 2018 No trig. No trig. No 540
Isaias 2020 No trig. No trig. No 500
Alberto 2006 No trig. No trig. No 268
Ida 2021 No trig. No trig. No 0
Elsa 2021 No trig. No trig. No 0
Idalia 2023 No trig. No trig. No 0
Helene 2024 No trig. No trig. No 0
Dean 2007 No trig. No trig. No 0
Fay 2008 No trig. No trig. No 0

8.13 Chosen Trigger With Readiness

The working group chose option 1b in July 2025. Below it is evaluated again with a readiness stage added, which uses the action wind threshold.

Code
blob_name = f"{PROJECT_PREFIX}/processed/nhc/monitors_nhc_chirpsgefs.parquet"
df_monitors_fcast = stratus.load_parquet_from_blob(blob_name)

df_stats_fcast_readiness = (
    df_monitors_fcast[df_monitors_fcast["lt_name"] == "readiness"]
    .groupby("atcf_id")
    .max()
    .reset_index()
    .dropna()
    .drop(columns=["issue_time", "lt_name"])
)
blob_name = f"{PROJECT_PREFIX}/processed/impact/emdat_cerf_upto2024.parquet"
df_impact = stratus.load_parquet_from_blob(blob_name)

df_storms = ibtracs.load_storms()
cols = ["sid", "atcf_id"]
df_storm_lookup = df_storms[cols].copy()

df_storm_lookup["atcf_id"] = df_storm_lookup["atcf_id"].str.lower()

df_stats_fcast_readiness_labelled = df_stats_fcast_readiness.merge(
    df_storm_lookup
)


df_stats_fcast_readiness_labelled["Readiness"] = (
    df_stats_fcast_readiness_labelled.apply(
        lambda row: (
            "Trig."
            if (
                # use actual readiness trigger (same as action)
                row["wind"]
                >= THRESHS["readiness"]["s"]
            )
            else "No trig."
        ),
        axis=1,
    )
)
Code
# Function to get trigger results with readiness analysis
def get_action_obsv_trig_results(index, impact_col="Total Affected"):
    """
    Returns a dataframe with trigger results including readiness column
    """
    df_disp = get_triggered_storms(index).copy()

    df_disp["CERF"] = df_disp.apply(set_cerf_str, axis=1)
    df_disp["Storm"] = (
        df_disp["name"].str.capitalize() + " " + df_disp["year"].astype(str)
    )
    df_disp["Action"] = df_disp["fcast_trig"].apply(
        lambda x: "Trig." if x else "No trig."
    )
    df_disp["Obsv."] = df_disp["obsv_trig"].apply(
        lambda x: "Trig." if x else "No trig."
    )
 
    cols = ["sid","Storm", "Action", "Obsv.", "CERF", impact_col]
    
    return df_disp[cols].sort_values(impact_col, ascending=False)



df_metrics
# Get results for option 1b (index 13136 from earlier analysis)
df_1b_with_readiness = get_action_obsv_trig_results(13136).merge(df_stats_fcast_readiness_labelled)
Code
# Optional: Create a styled display of the results
def display_styled_results(df_results, impact_col="Total Affected"):
    """
    Display the results with styling if needed
    """
    def color_df_extended(val):
        if val == "Yes":
            return "background-color: crimson"
        elif val == "No":
            return "background-color: dodgerblue"
        elif val == "Trig.":
            return "background-color: darkorange"
        else:
            return ""
    
    cols_for_display = ["Readiness","Action", "Obsv.", "CERF", impact_col]
    
    return (
        df_results.set_index("Storm")[cols_for_display]
        .style.bar(
            subset=impact_col,
            color="mediumpurple",
            props="width: 150px;",
        )
        .map(color_df_extended)
        .set_table_styles(
            {
                impact_col: [
                    {"selector": "th", "props": [("text-align", "left")]},
                    {"selector": "td", "props": [("text-align", "left")]},
                ]
            }
        )
        .format({"Total Affected": "{:,}"})
    )

# Display styled version if desired
display_styled_results(df_1b_with_readiness)
  Readiness Action Obsv. CERF Total Affected
Storm          
Irma 2017 Trig. Trig. Trig. Yes 10,000,000
Michelle 2001 Trig. Trig. Trig. pre- 5,900,012
Rafael 2024 No trig. No trig. No trig. Yes 4,000,000
Ian 2022 Trig. Trig. Trig. Yes 3,200,000
Ike 2008 Trig. Trig. Trig. Yes 2,600,000
Dennis 2005 Trig. Trig. Trig. pre- 2,500,000
Gustav 2008 Trig. Trig. No trig. Yes 450,019
Oscar 2024 No trig. No trig. No trig. Yes 320,000
Lili 2002 No trig. No trig. No trig. pre- 281,470
Charley 2004 No trig. No trig. No trig. pre- 244,005
Noel 2007 No trig. No trig. No trig. No 192,488
Matthew 2016 Trig. Trig. Trig. Yes 190,000
Sandy 2012 No trig. No trig. No trig. Yes 162,605
Wilma 2005 Trig. Trig. No trig. pre- 100,000
Paloma 2008 Trig. Trig. Trig. No 49,445
Isaac 2012 No trig. No trig. No trig. No 45,496
Isidore 2002 No trig. No trig. No trig. pre- 42,500
Alberto 2018 No trig. No trig. No trig. No 40,000
Alex 2022 No trig. No trig. No trig. No 6,780
Ivan 2004 Trig. Trig. No trig. pre- 3,245
Michael 2018 No trig. No trig. No trig. No 540
Alberto 2006 No trig. No trig. No trig. No 268
Ida 2021 No trig. No trig. No trig. No 0
Elsa 2021 No trig. No trig. No trig. No 0
Idalia 2023 No trig. No trig. No trig. No 0
Helene 2024 No trig. No trig. No trig. No 0
Fay 2008 No trig. No trig. No trig. No 0

8.13.1 Calculate joint return period for the three-stage trigger system

Code
def calculate_joint_return_period(df_results):
    """
    Calculate the return period when any of the three triggers (Action, Obsv., Readiness) fires
    """
    # Create boolean columns for each trigger type
    df_calc = df_results.copy()
    df_calc["action_bool"] = df_calc["Action"] == "Trig."
    df_calc["obsv_bool"] = df_calc["Obsv."] == "Trig."
    df_calc["readiness_bool"] = df_calc["Readiness"] == "Trig."

    # Create combined trigger: TRUE if ANY of the three triggers fire
    df_calc["any_trig"] = (
        df_calc["action_bool"]
        | df_calc["obsv_bool"]
        | df_calc["readiness_bool"]
    )

    # Extract year from Storm column (assuming format "Name YYYY")
    df_calc["year"] = df_calc["Storm"].str.extract(r"(\d{4})").astype(int)

    # Get distinct years where any trigger fired
    triggered_years = df_calc[df_calc["any_trig"]]["year"].unique()
    n_triggered_years = len(triggered_years)

    # Calculate return period using Weibull formula
    # Total period: 2000-2024 = 25 years
    total_years = 25
    return_period = (total_years + 1) / n_triggered_years

    # print(f"Triggered years: {sorted(triggered_years)}")
    print(f"Number of triggered years: {n_triggered_years}")
    print(f"Total analysis period: {total_years} years (2000-2024)")
    print(f"Joint return period: {return_period:.2f} years")

    return {
        # "triggered_years": triggered_years,
        "n_triggered_years": n_triggered_years,
        "return_period": return_period,
        "df_with_any_trig": df_calc,
    }


# Calculate joint return period for Option 1b
joint_rp_results = calculate_joint_return_period(df_1b_with_readiness)
Number of triggered years: 7
Total analysis period: 25 years (2000-2024)
Joint return period: 3.71 years
Code
# Show detailed breakdown by trigger type
def analyze_trigger_breakdown(joint_results):
    """
    Analyze which storms are triggered by which combination of triggers
    """
    df_analysis = joint_results["df_with_any_trig"]

    # Create trigger combination categories - use a more systematic approach
    df_analysis["trigger_combo"] = "No trigger"  # Default value

    # First handle all triggered cases (any_trig == True)
    triggered_mask = df_analysis["any_trig"]

    # Use nested conditions to ensure mutually exclusive categories
    df_analysis.loc[
        triggered_mask
        & df_analysis["action_bool"]
        & df_analysis["obsv_bool"]
        & df_analysis["readiness_bool"],
        "trigger_combo",
    ] = "All three"
    df_analysis.loc[
        triggered_mask
        & df_analysis["action_bool"]
        & df_analysis["obsv_bool"]
        & ~df_analysis["readiness_bool"],
        "trigger_combo",
    ] = "Action + Obsv"
    df_analysis.loc[
        triggered_mask
        & df_analysis["action_bool"]
        & ~df_analysis["obsv_bool"]
        & df_analysis["readiness_bool"],
        "trigger_combo",
    ] = "Action + Readiness"
    df_analysis.loc[
        triggered_mask
        & ~df_analysis["action_bool"]
        & df_analysis["obsv_bool"]
        & df_analysis["readiness_bool"],
        "trigger_combo",
    ] = "Obsv + Readiness"
    df_analysis.loc[
        triggered_mask
        & df_analysis["action_bool"]
        & ~df_analysis["obsv_bool"]
        & ~df_analysis["readiness_bool"],
        "trigger_combo",
    ] = "Action only"
    df_analysis.loc[
        triggered_mask
        & ~df_analysis["action_bool"]
        & df_analysis["obsv_bool"]
        & ~df_analysis["readiness_bool"],
        "trigger_combo",
    ] = "Obsv only"
    df_analysis.loc[
        triggered_mask
        & ~df_analysis["action_bool"]
        & ~df_analysis["obsv_bool"]
        & df_analysis["readiness_bool"],
        "trigger_combo",
    ] = "Readiness only"

    # Aggregate to year level to see which years had which trigger combinations
    year_summary = (
        df_analysis.groupby("year")
        .agg(
            {
                "action_bool": "any",  # True if ANY storm in that year triggered Action
                "obsv_bool": "any",  # True if ANY storm in that year triggered Obsv
                "readiness_bool": "any",  # True if ANY storm in that year triggered Readiness
                "any_trig": "any",  # True if ANY storm in that year triggered anything
                "Total Affected": "sum",  # Sum total affected for the year
            }
        )
        .reset_index()
    )

    # Create year-level trigger combination categories
    year_summary["year_trigger_combo"] = "No trigger"
    triggered_years_mask = year_summary["any_trig"]

    year_summary.loc[
        triggered_years_mask
        & year_summary["action_bool"]
        & year_summary["obsv_bool"]
        & year_summary["readiness_bool"],
        "year_trigger_combo",
    ] = "All three"
    year_summary.loc[
        triggered_years_mask
        & year_summary["action_bool"]
        & year_summary["obsv_bool"]
        & ~year_summary["readiness_bool"],
        "year_trigger_combo",
    ] = "Action + Obsv"
    year_summary.loc[
        triggered_years_mask
        & year_summary["action_bool"]
        & ~year_summary["obsv_bool"]
        & year_summary["readiness_bool"],
        "year_trigger_combo",
    ] = "Action + Readiness"
    year_summary.loc[
        triggered_years_mask
        & ~year_summary["action_bool"]
        & year_summary["obsv_bool"]
        & year_summary["readiness_bool"],
        "year_trigger_combo",
    ] = "Obsv + Readiness"
    year_summary.loc[
        triggered_years_mask
        & year_summary["action_bool"]
        & ~year_summary["obsv_bool"]
        & ~year_summary["readiness_bool"],
        "year_trigger_combo",
    ] = "Action only"
    year_summary.loc[
        triggered_years_mask
        & ~year_summary["action_bool"]
        & year_summary["obsv_bool"]
        & ~year_summary["readiness_bool"],
        "year_trigger_combo",
    ] = "Obsv only"
    year_summary.loc[
        triggered_years_mask
        & ~year_summary["action_bool"]
        & ~year_summary["obsv_bool"]
        & year_summary["readiness_bool"],
        "year_trigger_combo",
    ] = "Readiness only"

    # Show year-level breakdown
    year_trigger_summary = (
        year_summary.groupby("year_trigger_combo")
        .agg({"year": lambda x: sorted(x.tolist()), "Total Affected": "sum"})
        .round(0)
    )

    year_trigger_summary.columns = ["Years", "Total Affected"]
    year_trigger_summary["Number of Years"] = year_trigger_summary[
        "Years"
    ].apply(len)

    print("\nYear-Level Trigger Combination Breakdown:")
    print("=" * 50)
    for combo, row in year_trigger_summary.iterrows():
        print(
            f"{combo}: {row['Number of Years']} years, {row['Total Affected']:,.0f} total affected"
        )
        if len(row["Years"]) > 0:
            print(f"  Years: {row['Years']}")

    return year_trigger_summary


# Analyze the trigger breakdown
trigger_breakdown = analyze_trigger_breakdown(joint_rp_results)
trigger_breakdown

Year-Level Trigger Combination Breakdown:
==================================================
Action + Readiness: 1 years, 247,250 total affected
  Years: [2004]
All three: 6 years, 24,996,256 total affected
  Years: [2001, 2005, 2008, 2016, 2017, 2022]
No trigger: 8 years, 5,085,367 total affected
  Years: [2002, 2006, 2007, 2012, 2018, 2021, 2023, 2024]
Years Total Affected Number of Years
year_trigger_combo
Action + Readiness [2004] 247250 1
All three [2001, 2005, 2008, 2016, 2017, 2022] 24996256 6
No trigger [2002, 2006, 2007, 2012, 2018, 2021, 2023, 2024] 5085367 8

8.14 Historical record by window

Code
# It seems something was wrong w/ dev db connection for ibtracs 2025-11-20 when I was running this so I had to do a hack to get # this notebook to run and get the `joint_rp_results` object I wanted loaded
# As df_storms = ibtrac.load_storms() (ln 941) was not working - i did this instead to recreated it (and then proceeded):
# cols = ["sid", "atcf_id"]
# df_storm_lookup = df_stats[cols].drop_duplicates().copy()
# df_storm_lookup["atcf_id"] = df_storm_lookup["atcf_id"].str.lower()

df_joint_rp_results = joint_rp_results["df_with_any_trig"]
df_trigger_bools = df_joint_rp_results[
    ["year", "readiness_bool", "action_bool", "obsv_bool"]
]

# Collapse to unique years - any TRUE in a year becomes TRUE for that year
df_trigger_bools_by_year = (
    df_trigger_bools.groupby("year")
    .agg({"readiness_bool": "any", "action_bool": "any", "obsv_bool": "any"})
    .reset_index()
)

# Fill in all missing years from 2000-2024 with False
all_years = pd.DataFrame({"year": range(2000, 2026)})
df_trigger_bools_by_year = all_years.merge(
    df_trigger_bools_by_year, on="year", how="left"
).fillna(False)

# Set all triggers to True for 2025
df_trigger_bools_by_year.loc[
    df_trigger_bools_by_year["year"] == 2025,
    ["readiness_bool", "action_bool", "obsv_bool"],
] = True
Code
# write for financial risk transfer solution project
df_trigger_bools_by_year_uid = df_trigger_bools_by_year.rename(
    columns={
        "readiness_bool": "cub_storms_v1_wt1",
        "action_bool": "cub_storms_v1_wt2",
        "obsv_bool": "cub_storms_v1_wt3",
    }
)
df_trigger_bools_by_year_uid

stratus.upload_csv_to_blob(
    df_trigger_bools_by_year_uid,
    "ds-aa-cerf-global-trigger-allocations/aa_historical/yearly/v4/cub_storms_v1.csv",
)