This chapter covers the two primary datasets — CERF storm allocations and OCHA cyclone population exposure estimates — along with the storm ID matching process that links them and the merge that produces the analysis dataset.
Code
import syssys.path.insert(0, "..")import ocha_stratus as stratusimport pandas as pdimport numpy as npimport matplotlib.pyplot as pltimport matplotlib.ticker as mtickerfrom great_tables import GT, loc, stylefrom plotnine import*import warningsfrom dotenv import load_dotenvwarnings.filterwarnings("ignore", module="plotnine")load_dotenv()df_ocha = stratus.load_parquet_from_blob("ds-storm-impact-harmonisation/processed/adm0_ibtracs_exp_all.parquet")
10.1 OCHA Cyclone Exposure Dataset
The ADAM and GDACS exposure datasets proved unusable for this analysis due to temporal gaps (see Section 11.3). However, the OCHA exposure dataset (adm0_ibtracs_exp_all.parquet) covers 2001–2026 with 1,271 storms and uses IBTrACS sid directly — giving us near-complete overlap with CERF.
Three cumulative wind speed thresholds (34, 50, 64 kt) with population exposure per country. The data is confirmed cumulative: pop at 34 kt >= 50 kt >= 64 kt.
Number of storms per year in the OCHA exposure dataset.
10.2 CERF Storm Allocations (API Pipeline)
CERF storm allocation data is sourced from the CERF GMS API, filtered to Storm emergency type and Rapid Response window. Each allocation is linked to an IBTrACS storm via a curated ApplicationCode -> sid dictionary (CERFCODE_TO_SID in src/datasets/cerf.py).
Code
from src.datasets.cerf import ( load_cerf_api_data, apply_cerfcode_sids, build_analysis_dataset_api, CERFCODE_TO_SID,)df_cerf_api = load_cerf_api_data()print(f"CERF API storm allocations: {len(df_cerf_api)}")print(f"Year range: {int(df_cerf_api['Year'].min())}–{int(df_cerf_api['Year'].max())}")print(f"Countries: {df_cerf_api['CountryCode'].nunique()}")n_matched =sum(1for v in CERFCODE_TO_SID.values() if v isnotNone)n_non_tc =sum(1for v in CERFCODE_TO_SID.values() if v isNone)print(f"\nCERFCODE_TO_SID: {len(CERFCODE_TO_SID)} entries")print(f" Matched to TC: {n_matched}")print(f" Non-TC (excluded): {n_non_tc}")
CERF API storm allocations: 95
Year range: 2007–2026
Countries: 34
CERFCODE_TO_SID: 96 entries
Matched to TC: 85
Non-TC (excluded): 11
Code
# Build a global storm name lookup from IBTrACSengine = stratus.get_engine("prod")all_sids =sorted({s for s in CERFCODE_TO_SID.values() if s isnotNone})with engine.connect() as conn: sid_names = pd.read_sql(f"SELECT sid, name FROM storms.ibtracs_storms "f"WHERE sid IN ({','.join(repr(s) for s in all_sids)})", conn, )sid_name_map =dict(zip(sid_names["sid"], sid_names["name"]))
10.2.1 Non-TC Allocations
These allocations are classified as “Storm” in the CERF system but are not tropical cyclones (inland flooding, tornadoes, etc.). They are excluded from the analysis.
Code
df_matched = apply_cerfcode_sids(df_cerf_api)non_tc_codes = {k for k, v in CERFCODE_TO_SID.items() if v isNone}non_tc = df_cerf_api[df_cerf_api["ApplicationCode"].isin(non_tc_codes)]display( non_tc[["ApplicationCode", "CountryName", "Year", "TotalAmountApproved"]] .sort_values("Year") .reset_index(drop=True) .rename(columns={"CountryName": "Country","TotalAmountApproved": "Amount (USD)", }))
n_tc =len(cerf_valid_api)n_with_exp = merged_api["has_exposure"].sum()n_without_exp = (~merged_api["has_exposure"]).sum()n_sids_missing = merged_api.loc[~merged_api["has_exposure"], "sid"].nunique()print(f"CERF allocations matched to TC sids: {n_tc}")print(f" → with exposure data: {n_with_exp}")print(f" → without exposure data: {n_without_exp} ({n_sids_missing} unique storms)")
CERF allocations matched to TC sids: 84
→ with exposure data: 77
→ without exposure data: 7 (6 unique storms)
10.4.2 Missing storm–country pairs
The table below lists CERF allocations that have a valid IBTrACS sid but no matching record in the OCHA exposure data. There are two causes:
Storm absent entirely — the storm was never processed by the OCHA exposure pipeline (no records for any country).
Country not covered — the storm exists in the exposure data but not for the specific country that received CERF funding.
Code
gap = merged_api[~merged_api["has_exposure"]].copy()# Check which sids have *any* record in OCHA exposureall_ocha_sids =set(df_ocha["sid"].unique())gap["sid_in_ocha"] = gap["sid"].isin(all_ocha_sids)gap["gap_reason"] = gap["sid_in_ocha"].map( {False: "Storm absent from exposure data", True: "Country not in exposure for this storm"})gap["storm_name"] = gap["sid"].map(sid_name_map)display( gap[["Country", "iso3", "sid", "storm_name", "allocation_year", "Amount in US$", "gap_reason"]] .sort_values(["gap_reason", "allocation_year"]) .reset_index(drop=True))
These gaps represent a data limitation — the OCHA exposure pipeline has not processed every storm. Notably absent are storms like FREDDY (2023), one of the longest-lived tropical cyclones on record, and GAMANE (2024). These allocations are excluded from the regression analysis but remain in the full CERF inventory for reference.
10.5 Comparison with Old CSV Pipeline
The original analysis used a CSV export with manually assigned storm IDs and algorithmic auto-matching. The API pipeline replaces this with a curated dictionary. This section compares the resulting analysis datasets.