2  Real-Time GDACS Exposure Retrieval

This chapter explores the feasibility of retrieving population exposure estimates in real time from GDACS during an active or developing storm. Unlike the historical CSV exports explored in Section 11.3 (which had null exposure values for 2016–2021), the GDACS REST API provides per-advisory exposure data going back to at least 2015.

Code
import sys

sys.path.insert(0, "..")

import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from shapely.geometry import Polygon

from src.datasets.gdacs import (
    get_active_cyclones,
    get_event_detail,
    get_timeline,
    get_impact_by_country,
    load_ibtracs_lookup,
    match_gdacs_to_ibtracs,
    wind_radii_polygon,
)

# -- Plotting helpers (inline, not in src module) --

_INTENSITY_BREAKS = [
    (17.5, "#5ebaff", "TD"),
    (25.0, "#00faf4", "TS"),
    (33.0, "#ffffcc", "Cat 1"),
    (43.0, "#ffe775", "Cat 2"),
    (50.0, "#ffc140", "Cat 3"),
    (58.0, "#ff8f20", "Cat 4"),
    (999, "#ff6060", "Cat 5"),
]


def _wind_color(wind_ms):
    if np.isnan(wind_ms):
        return "#cccccc"
    for threshold, color, _ in _INTENSITY_BREAKS:
        if wind_ms < threshold:
            return color
    return "#ff6060"


def plot_track_interactive(timeline, event_name="", show_radii=True, radii_interval=4):
    df = timeline.dropna(subset=["latitude", "longitude"]).copy()
    df["color"] = df["wind_speed"].apply(_wind_color)
    df["wind_kmh"] = (df["wind_speed"] * 3.6).round(1)
    df["is_actual"] = df["actual"].astype(str).str.lower() == "true"

    fig = make_subplots(
        rows=2, cols=1, row_heights=[0.7, 0.3],
        specs=[[{"type": "map"}], [{"type": "xy"}]],
        vertical_spacing=0.05,
    )

    # Track line
    fig.add_trace(
        go.Scattermap(
            lat=df["latitude"], lon=df["longitude"],
            mode="lines", line={"width": 2, "color": "gray"},
            showlegend=False, hoverinfo="skip",
        ), row=1, col=1,
    )

    # Advisory points
    for _, row in df.iterrows():
        status = row.get("storm_status", "")
        hover = (
            f"<b>{row.get('name', '')}</b><br>"
            f"Advisory {int(row['advisory_number'])}<br>"
            f"{row['advisory_datetime']}<br>"
            f"Wind: {row['wind_kmh']} km/h ({row['wind_speed']:.1f} m/s)<br>"
            f"Status: {status}<br>"
            f"Pop 39kt: {row['pop39']:,.0f}<br>"
            f"Pop 74kt: {row['pop74']:,.0f}"
        )
        fig.add_trace(
            go.Scattermap(
                lat=[row["latitude"]], lon=[row["longitude"]],
                mode="markers",
                marker={"size": 10, "color": row["color"],
                        "opacity": 1.0 if row["is_actual"] else 0.5},
                text=hover, hoverinfo="text", showlegend=False,
            ), row=1, col=1,
        )

    # Wind radii
    if show_radii:
        radii_config = {
            "34kt": ("rgba(51,136,255,{a})", 0.10),
            "50kt": ("rgba(255,136,0,{a})", 0.15),
            "64kt": ("rgba(255,0,0,{a})", 0.20),
        }
        for idx, row in df.iterrows():
            if idx % radii_interval != 0:
                continue
            for kt, (color, opacity) in radii_config.items():
                ne = row.get(f"windrad_nm_{kt}_ne", 0) or 0
                se = row.get(f"windrad_nm_{kt}_se", 0) or 0
                sw = row.get(f"windrad_nm_{kt}_sw", 0) or 0
                nw = row.get(f"windrad_nm_{kt}_nw", 0) or 0
                poly = wind_radii_polygon(
                    row["latitude"], row["longitude"], ne, se, sw, nw
                )
                if poly is None:
                    continue
                lons, lats = poly.exterior.xy
                fig.add_trace(
                    go.Scattermap(
                        lat=list(lats), lon=list(lons),
                        mode="lines", fill="toself",
                        fillcolor=color.format(a=opacity),
                        line={"width": 0.5, "color": color.format(a=1.0)},
                        showlegend=False, hoverinfo="skip",
                    ), row=1, col=1,
                )

    # Population subplot
    fig.add_trace(
        go.Scatter(
            x=df["advisory_datetime"], y=df["pop39"],
            fill="tozeroy", fillcolor="rgba(51,136,255,0.2)",
            line={"color": "#3388ff", "width": 1.5}, name="Pop >= 39 kt",
        ), row=2, col=1,
    )
    fig.add_trace(
        go.Scatter(
            x=df["advisory_datetime"], y=df["pop74"],
            fill="tozeroy", fillcolor="rgba(255,0,0,0.2)",
            line={"color": "#ff0000", "width": 1.5}, name="Pop >= 74 kt",
        ), row=2, col=1,
    )

    fig.update_layout(
        title=f"GDACS Track: {event_name}" if event_name else "GDACS Storm Track",
        map={"style": "carto-positron",
             "center": {"lat": df["latitude"].mean(), "lon": df["longitude"].mean()},
             "zoom": 3},
        height=900, margin={"l": 40, "r": 20, "t": 60, "b": 40},
        legend={"yanchor": "bottom", "y": 0.02, "x": 0.01},
    )
    fig.update_yaxes(title_text="Population Exposed", row=2, col=1)
    fig.update_xaxes(title_text="Advisory Time", row=2, col=1)
    return fig

2.1 Active Cyclones

The GDACS event list endpoint returns all recent tropical cyclones, filterable by alert level and date range. No authentication is required.

Code
events = get_active_cyclones()
events[
    ["eventid", "name", "alert_level", "country", "is_current", "severity_kmh"]
].head(10)
eventid name alert_level country is_current severity_kmh
0 1001270 SINLAKU-26 Red Northern Mariana Islands, Guam False 287.035200
1 1001256 GEZANI-26 Red Madagascar, Mozambique False 211.109760
2 1001254 FYTIA-26 Orange Madagascar False 179.443296
3 1001238 DITWAH-25 Orange Sri Lanka, India False 64.814400
4 1001234 FUNG-WONG-25 Red Philippines, China, Japan False 212.961600
5 1001233 KALMAEGI-25 Red Viet Nam, Cambodia, Philippines, Laos False 212.961600
6 1001230 MELISSA-25 Red Jamaica, Cuba, Bahamas, Bermuda, Haiti, Canada False 296.294400
7 1001232 MONTHA-25 Orange India False 83.332800
8 1001229 FENGSHEN-25 Orange Viet Nam, China, Cambodia, Philippines, Laos, ... False 92.592000
9 1001219 MATMO-25 Red China, Viet Nam, Philippines False 166.665600

2.2 Advisory Timeline

The timeline endpoint is the richest data source. Each row is one advisory (typically 6-hourly) with:

  • Position: latitude, longitude
  • Intensity: wind speed (m/s), gusts, pressure
  • Exposure: pop39, pop74 (population inside the wind polygon at that advisory – instantaneous snapshots, not cumulative)
  • Wind radii: quadrant radii at 34/50/64 kt in nautical miles
Code
current = events[events["is_current"]]
event = current.iloc[0] if len(current) > 0 else events.iloc[0]
eid = event["eventid"]
name = event["name"]

tl = get_timeline(eid)
print(f"{name}: {len(tl)} advisories")
print(f"Peak wind: {tl['wind_speed'].max():.1f} m/s")
print(f"Max pop exposed >= 39 kt: {tl['pop39'].max():,.0f}")
print(f"Max pop exposed >= 74 kt: {tl['pop74'].max():,.0f}")
SINLAKU-26: 46 advisories
Peak wind: 79.7 m/s
Max pop exposed >= 39 kt: 231,261
Max pop exposed >= 74 kt: 50,254
Code
tl[
    [
        "advisory_number",
        "advisory_datetime",
        "wind_speed",
        "storm_status",
        "pop39",
        "pop74",
    ]
].head(15)
advisory_number advisory_datetime wind_speed storm_status pop39 pop74
0 1 2026-04-09 00:00:00 12.860 Tropical Depression 0 0
1 2 2026-04-09 06:00:00 15.432 Tropical Depression 37915 0
2 3 2026-04-09 12:00:00 15.432 Tropical Depression 42028 0
3 4 2026-04-09 18:00:00 18.004 Tropical Storm 41585 0
4 5 2026-04-10 00:00:00 20.576 Tropical Storm 47277 0
5 6 2026-04-10 06:00:00 23.148 Tropical Storm 47277 0
6 7 2026-04-10 12:00:00 28.292 Tropical Storm 43592 0
7 8 2026-04-10 18:00:00 30.864 Tropical Storm 42572 0
8 9 2026-04-11 00:00:00 38.580 Hurricane/Typhoon > 74 mph 42838 2346
9 10 2026-04-11 06:00:00 41.152 Hurricane/Typhoon > 74 mph 42838 2239
10 11 2026-04-11 12:00:00 43.724 Hurricane/Typhoon > 74 mph 47277 13
11 12 2026-04-11 18:00:00 48.868 Hurricane/Typhoon > 74 mph 47277 0
12 13 2026-04-12 00:00:00 56.584 Hurricane/Typhoon > 74 mph 42838 0
13 14 2026-04-12 06:00:00 66.872 Hurricane/Typhoon > 74 mph 42129 0
14 15 2026-04-12 12:00:00 77.160 Hurricane/Typhoon > 74 mph 22683 0

2.3 Interactive Track Map

Track is colored by Saffir-Simpson intensity. Hover over advisory points to see wind speed, storm status, and population exposure. Wind radii polygons show the asymmetric wind field at 34/50/64 kt thresholds.

Code
fig = plot_track_interactive(tl, event_name=name)
fig.show()

2.4 Country-Level Impact

The impact endpoint provides population exposure broken down by country at two wind thresholds. Note: this endpoint only returns data for events from ~2022 onward.

Code
impact = get_impact_by_country(eid, aggregate=True)
for buf_name, df_buf in impact.items():
    if len(df_buf) > 0:
        total = df_buf["pop_affected"].sum()
        print(f"\n{buf_name}: {total:,} total")
        display(df_buf)

buffer39: 208,114 total
iso3 country pop_affected
0 GUM Guam 166535
1 MNP Northern Mariana Islands 41579

buffer74: 41,579 total
iso3 country pop_affected
0 MNP Northern Mariana Islands 41579

2.5 Hindcast: Hurricane MATTHEW (2016)

A key finding: the timeline endpoint has exposure data for events back to at least 2015, even though the static CSV export had null values for 2016–2021. This means we can fill the historical gap using the API.

Code
tl_matthew = get_timeline(1000316)
print(f"MATTHEW-16: {len(tl_matthew)} advisories")
print(f"Peak wind: {tl_matthew['wind_speed'].max():.1f} m/s")
print(f"Max pop39: {tl_matthew['pop39'].max():,.0f}")
print(f"Max pop74: {tl_matthew['pop74'].max():,.0f}")
MATTHEW-16: 48 advisories
Peak wind: 72.0 m/s
Max pop39: 18,318,843
Max pop74: 1,288,831
Code
fig_matthew = plot_track_interactive(
    tl_matthew, event_name="MATTHEW-16 (Hindcast)"
)
fig_matthew.show()

2.6 Timeline vs Impact Endpoint

Two GDACS endpoints provide population exposure, but with different coverage and granularity:

Timeline (gettimeline) Impact (getimpact)
Granularity Per advisory (6-hourly) Per admin unit
Geographic Total (no country breakdown) Country + admin level
Temporal 2015+ (works) ~2022+ only
Exposure type Instantaneous snapshot Cumulative unique count
Use case Real-time monitoring, hindcasts Country-level reporting

The original CSV (gdacs_historical_national_exposure.csv) used the impact endpoint exclusively, which is why 2016–2021 had null exposure values. The timeline endpoint fills this gap.

2.6.1 Snapshot vs cumulative exposure

The timeline pop39 at each advisory is the number of people inside the wind polygon at that moment. The same people appear in consecutive advisories as the storm passes over them. Summing across advisories double-counts.

The impact endpoint computes the union of all wind polygons across the full storm track and counts each person only once. For MILTON-24:

  • Timeline max: 23.9M (single advisory peak)
  • Impact total: 25.0M (cumulative unique)
  • Timeline sum: 124.6M (meaningless – overlapping counts)

2.7 Wind Threshold Mapping

The GDACS API names its impact buffers buffer39 and buffer74, but according to the JRC technical report (Probst & Annunziato, 2016, EUR 28331 EN, doi:10.2788/504291), the wind radii used in TC bulletins are at 34, 50, and 64 kt – the standard advisory thresholds from NOAA NHC and JTWC.

We validated this by reconstructing the impact figures from timeline wind radii + population rasters. Using MILTON-24 as a test case, we built cumulative wind footprints (including track corridor buffering) and overlaid them on multiple population grids:

Radii threshold GHSL 2025 (1km) WorldPop 2026 WorldPop 2018 GDACS impact
34 kt 23.8M (95%) 27.3M (109%) 23.8M (95%) buffer39 = 25.0M
50 kt 12.8M 10.8M
64 kt 3.6M (78%) 4.6M (98%) 3.7M (81%) buffer74 = 4.6M

Key conclusions:

  • buffer39 is built from the 34 kt wind radii (not 39 kt)
  • buffer74 is built from the 64 kt wind radii (not 74 kt)
  • The original CSV column names (pop_34kt, pop_64kt) are correct; the API buffer names are misleading
  • GDACS uses GHSL (GHS-POP), the JRC’s own Global Human Settlement Layer, as its population grid. Remaining discrepancies between our reconstruction and GDACS figures are driven by raster and methodology configuration, not a fundamental mismatch – see Section 2.8.4

2.8 Reconstructing Cumulative Exposure

To fill the 2016–2021 gap where the GDACS impact endpoint returns zeros, we reconstructed cumulative exposure from the timeline wind radii + a population raster. Two methodological choices matter:

2.8.1 Wind radii polygon construction

TC advisories report wind radii as the maximum extent in each quadrant (NE, SE, SW, NW) in nautical miles. Our wind_radii_polygon() function supports two methods via a method parameter:

  • "asymmetric" (default): cosine-interpolated between quadrant center azimuths (45, 135, 225, 315 degrees), producing smooth, physically realistic shapes that preserve the true wind field asymmetry.

  • "symmetric": takes max(NE, SE, SW, NW) and builds a uniform circle. This replicates the GDACS/JRC methodology: the 2026 GDACS guide and earlier JRC documentation confirm that although bulletins list four quadrant radii, GDACS uses only the maximum value in all directions, overstating the affected area relative to the true asymmetric field.

2.8.2 Track corridor filling

Advisory positions are 6 hours apart. Simply unioning the instantaneous wind polygons leaves gaps between consecutive advisories where the storm was present but no polygon exists. We fill these gaps by taking the convex hull of each adjacent pair of wind polygons, creating a smooth swath along the track.

2.8.3 Validation: symmetric vs asymmetric

Using WorldPop 2026 (1km), tested against the GDACS impact endpoint for two recent storms and one 2016 storm where the impact endpoint returns zeros (demonstrating gap-filling):

Storm Buffer GDACS Impact Asymmetric Symmetric
KALMAEGI-25 34kt 39.0M 33.7M (86%) 37.3M (96%)
KALMAEGI-25 64kt 12.2M 9.4M (77%) 11.7M (96%)
MILTON-24 34kt 25.0M 27.0M (108%) 30.6M (122%)
MILTON-24 64kt 4.6M 5.0M (108%) 6.1M (133%)
MATTHEW-16 34kt 0 (gap) 36.0M 72.8M
MATTHEW-16 64kt 0 (gap) 3.7M 7.9M

Key findings:

  • The symmetric method matches GDACS within 4% for multi-country storms (KALMAEGI), confirming the JRC symmetric-circle methodology.
  • The symmetric method overshoots for MILTON (122–133%) because MILTON’s radii were already fairly symmetric, and GDACS may clip to coastlines or apply other adjustments we don’t replicate.
  • MATTHEW-16 gap-fill works: the impact endpoint returns zeros for this 2016 storm, but we reconstruct 36M (asymmetric) or 73M (symmetric) exposed at 34kt. The large difference between methods reflects MATTHEW’s highly asymmetric wind field.
  • The asymmetric method is more physically realistic and avoids overstating exposure. For operational use, it may be preferable to the GDACS approach.
  • Remaining discrepancies are partly explained by GDACS attributing population at the admin-unit level rather than grid-cell level: 16 of 30 admin units for KALMAEGI show pop_affected exceeding the admin’s actual population by up to 4x.

2.8.4 Sources of reconstruction discrepancy

The differences between our reconstructed figures and GDACS are not random error – they trace to specific configuration choices in three areas:

  1. Population raster vintage. GDACS uses the latest GHSL release (GHS-POP E2025, R2023A). Using the same raster and symmetric method, we match within 1–7%. Switching to an older vintage (e.g. GHSL E2020) systematically underestimates by ~5–6% because it reflects lower population counts.

    Storm Buffer GDACS GHSL E2025 (1km) GHSL E2020 (100m)
    KALMAEGI-25 34kt 39.0M 39.6M (102%) 37.1M (95%)
    KALMAEGI-25 64kt 12.2M 12.1M (99%) 11.4M (94%)
    MILTON-24 34kt 25.0M 26.7M (107%) 25.9M (104%)
    MILTON-24 64kt 4.6M 5.0M (107%) 4.8M (104%)

    Note: the higher spatial resolution of GHSL E2020 (3 arc-sec vs 30 arc-sec) does not compensate for the older population estimate. Vintage matters more than resolution.

  2. Wind polygon method. Symmetric (GDACS approach) vs asymmetric changes results by 10–30% depending on how asymmetric the storm’s wind field is. MATTHEW-16 shows a 2x difference because its wind field was highly lopsided.

  3. Spatial attribution method. Our reconstruction counts population at the grid-cell level (mask raster pixels inside the wind polygon). GDACS attributes population at the admin-unit level, counting the entire admin unit’s population if it intersects the wind polygon. This explains cases where GDACS pop_affected exceeds the admin unit’s actual population.

When reconstructing exposure for the 2016–2021 gap, these choices should be made deliberately. Using GHSL E2025 + symmetric method + grid-cell attribution gives the closest match to current GDACS figures.

2.9 GDACS Methodology Summary

Based on the JRC technical report (Probst & Annunziato, 2016, EUR 28331 EN, doi:10.2788/504291), the 2026 GDACS API guide, and our reconstruction testing:

  • Data sources: NOAA bulletins (Atlantic, E/C Pacific) and JTWC bulletins (W Pacific, Indian Ocean, S Hemisphere), ingested every 30 minutes
  • Wind impact: advisory wind radii (34/50/64 kt); GDACS uses the maximum of the four quadrant radii as a symmetric circle, overlaid on a GHSL population grid
  • Storm surge: wind radii + Holland’s parametric model + HyFlux2
  • Rainfall: NASA GPM (previously NOAA eTRaP)

2.10 API Update Latency

For the monitoring pipeline, the key operational question is how quickly each source has new data available after an NHC advisory. The two sources expose very different levels of metadata, so the measurements are not equivalent.

2.10.1 ADAM: episode publication lag

ADAM includes an updated_at field on every episode record, enabling direct measurement of how long after an advisory ADAM finishes processing and publishes the episode. ADAM’s to_date aligns with NHC advisory hours (03, 09, 15, 21 UTC), so lags are measured from the nominal advisory time.

The script below fetches all episodes since 2022 in a single paginated pass — publication timestamps are in the listing metadata, so no per-storm CSV downloads are required.

Code
import ocha_stratus as stratus
from dotenv import load_dotenv

load_dotenv()

df_adam_lat = stratus.load_parquet_from_blob(
    "ds-storm-impact-harmonisation/processed/adam_episode_latency.parquet"
)
df_adam_lat["lag_min"] = df_adam_lat["lag_hrs"] * 60

palette = {"NOAA": "#3388ff", "JTWC": "#ff6600"}
fig_adam = go.Figure()
for source, color in palette.items():
    vals = df_adam_lat.loc[df_adam_lat["source"] == source, "lag_min"]
    n = len(vals)
    fig_adam.add_trace(go.Histogram(
        x=vals,
        name=f"{source} (n={n:,})",
        xbins=dict(start=0, end=360, size=15),
        opacity=0.7,
        marker_color=color,
        histnorm="percent",
    ))
fig_adam.add_vline(
    x=20,
    line_dash="dot",
    line_color="rgba(220,50,50,0.8)",
    line_width=1.5,
    annotation_text="20 min (pipeline send)",
    annotation_position="top right",
    annotation_font=dict(color="rgba(220,50,50,0.9)"),
)
fig_adam.update_layout(
    barmode="overlay",
    title="ADAM episode publication latency by source basin (2022–present)",
    xaxis_title="Lag after NHC/JTWC advisory time (minutes)",
    yaxis_title="Share of episodes (%)",
    xaxis_range=[0, 360],
    plot_bgcolor="white",
    paper_bgcolor="white",
    height=430,
    margin=dict(l=50, r=30, t=60, b=50),
    legend=dict(yanchor="top", y=0.95, x=0.6),
)
fig_adam.update_xaxes(gridcolor="rgba(0,0,0,0.06)", zeroline=False)
fig_adam.update_yaxes(gridcolor="rgba(0,0,0,0.06)", zeroline=False)
fig_adam.show()

For NOAA-sourced events (Atlantic and Eastern Pacific), 80% of episodes are published within 30 minutes of the NHC advisory time with a median of ~17 minutes. JTWC-sourced events (Western Pacific and Indian Ocean) have a median lag of ~2.5 hours. Since the monitoring pipeline covers NOAA basins only, the JTWC lag is not a concern for our use case.

2.10.2 GDACS: GTS bulletin receipt lag

GDACS does not expose per-episode publication timestamps, so we cannot measure directly when a processed episode becomes available. What we can measure is when GDACS receives the raw NHC bulletin via the WMO Global Telecommunication System (GTS).

The GTS is push-based: NHC does not wait to be polled. When NHC finalises an advisory it transmits the bulletin onto the GTS network and all registered subscribers — including GDACS/JRC — receive it automatically within seconds. GDACS then parses the bulletin and runs the exposure calculation to produce the episode. The GTS receipt time (pubdate in the GDACS GTS endpoint) is therefore a lower bound on episode publication; the additional processing time is not observable from the API.

NHC issues the WTNT43 forecast/advisory bulletin (the product containing track position and wind radii at 34/50/64 kt) 5–15 minutes before the nominal advisory hour. We parse the actual issuance time from the WMO bulletin identifier and compute the lag relative to the nearest nominal advisory time for a like-for-like comparison with ADAM.

Code
df_gdacs_lat = stratus.load_parquet_from_blob(
    "ds-storm-impact-harmonisation/processed/gdacs_gts_latency.parquet"
)
df_gdacs_lat["lag_min"] = df_gdacs_lat["lag_hrs"] * 60
n_gdacs = len(df_gdacs_lat)

fig_gdacs = go.Figure()
fig_gdacs.add_trace(go.Histogram(
    x=df_gdacs_lat["lag_min"],
    name=f"GDACS GTS receipt (n={n_gdacs:,})",
    xbins=dict(start=-40, end=60, size=5),
    marker_color="#2ca02c",
    opacity=0.8,
    histnorm="percent",
))
fig_gdacs.add_vline(
    x=0,
    line_dash="solid",
    line_color="rgba(0,0,0,0.3)",
    line_width=1,
    annotation_text="Nominal advisory time",
    annotation_position="top left",
    annotation_font=dict(color="rgba(0,0,0,0.5)", size=11),
)
fig_gdacs.add_vline(
    x=20,
    line_dash="dot",
    line_color="rgba(220,50,50,0.8)",
    line_width=1.5,
    annotation_text="20 min (pipeline send)",
    annotation_position="top right",
    annotation_font=dict(color="rgba(220,50,50,0.9)"),
)
fig_gdacs.update_layout(
    title="GDACS GTS bulletin receipt latency — NOAA WTNT43 (2022–present)",
    xaxis_title="Minutes relative to nominal NHC advisory time",
    yaxis_title="Share of bulletins (%)",
    xaxis_range=[-40, 60],
    plot_bgcolor="white",
    paper_bgcolor="white",
    height=430,
    margin=dict(l=50, r=30, t=60, b=50),
    showlegend=True,
    legend=dict(yanchor="top", y=0.95, x=0.01),
)
fig_gdacs.update_xaxes(gridcolor="rgba(0,0,0,0.06)", zeroline=False)
fig_gdacs.update_yaxes(gridcolor="rgba(0,0,0,0.06)", zeroline=False)
fig_gdacs.show()

The bulk of WTNT43 bulletins arrive at GDACS 5–20 minutes before the nominal advisory time, reflecting NHC’s practice of issuing slightly early. By the time the pipeline fires at +20 minutes, GDACS has had the raw bulletin data for ~25–35 minutes. The unobserved processing step sits between GTS receipt and the episode appearing in the API; based on the GDACS documentation noting a 30-minute ingestion cycle, this is likely sub-minute for parsing and a few minutes for the exposure calculation.

2.10.3 Pipeline timing summary

Source What is measured Median lag from advisory time
GDACS GTS bulletin receipt (lower bound) ~−10 min (before advisory hour)
ADAM Episode published in API +17 min
Pipeline fires +20 min

GDACS has the raw data well before the pipeline runs. ADAM publishes within the pipeline window for the majority of advisories. Both sources are operationally viable for NOAA basin storms at the current send time.

2.11 IBTrACS Matching

For integration with the CERF pipeline, GDACS events need to be matched to IBTrACS storm IDs (sid). Matching by storm name + season achieves 95%+ accuracy.

Code
ibtracs = load_ibtracs_lookup()
matched = match_gdacs_to_ibtracs(events, ibtracs)
matched[["eventid", "name", "sid", "country"]].head(10)
eventid name sid country
0 1001270 SINLAKU-26 2026099N09152 Northern Mariana Islands, Guam
1 1001256 GEZANI-26 2026039S18057 Madagascar, Mozambique
2 1001254 FYTIA-26 2026030S16043 Madagascar
3 1001238 DITWAH-25 2025331N07081 Sri Lanka, India
4 1001234 FUNG-WONG-25 2025308N10143 Philippines, China, Japan
5 1001233 KALMAEGI-25 2025305N10138 Viet Nam, Cambodia, Philippines, Laos
6 1001230 MELISSA-25 2025294N14290 Jamaica, Cuba, Bahamas, Bermuda, Haiti, Canada
7 1001232 MONTHA-25 2025300N11086 India
8 1001229 FENGSHEN-25 2025291N13126 Viet Nam, China, Cambodia, Philippines, Laos, ...
9 1001219 MATMO-25 2025274N15131 China, Viet Nam, Philippines