usdata

NOAA · NWS Watch, Warning, and Advisory Geometries

NWS watch, warning, and advisory polygons and areas

Every National Weather Service watch, warning, and advisory with its geometry, as zipped shapefiles (with the same attributes as CSV) from the Iowa Environmental Mesonet's archive of NWS products; the NWS API itself keeps no archive.

zipped shapefile and CSV (no reader)Source only
The walkthrough's first look at nws watch, warning, and advisory polygons and areas

At a glance

Spatial
A storm-based warning polygon, or one county, parish, or forecast zone outline, per row
Time step
Issue, expiry, and polygon times to the minute
Updates
Not stated by IEM, which processes the live NWS product stream; rows change as later products update an event
Files
zipped shapefile and CSV (no reader)
Selection
Events starting inside an inclusive UTC window, one zip per UTC month; optionally one state or offices, event types, and polygon options
You provide
Both timestamps; optionally a state location or wfo, events such as TO.W, storm_based, and followups
Full description

Every National Weather Service watch, warning, and advisory with its geometry, as zipped shapefiles (with the same attributes as CSV) from the Iowa Environmental Mesonet's archive of NWS products; the NWS API itself keeps no archive. Storm-based warning polygons from 2002, and the counties and forecast zones each product was issued for, from 1986 for tornado, severe thunderstorm, flash flood, and marine warnings and from November 2005 for the rest, each row with VTEC phenomena, significance, event number, status, issue and expiry times, and the issuing product. A window selects events by their start time, not by when they were in effect, and is fetched as one zip per UTC month. Optionally narrowed by state, offices, event types, or to storm-based polygons; zips are rewritten in a canonical form so repeated fetches match.

Quick start

Terminal

python -m pip install "usdata"
usdata fetch noaa:nws-warnings \
  --location Oklahoma \
  --start 2024-05-06T18:00Z \
  --end 2024-05-07T12:00Z \
  -p events=TO.W,TO.A

Python

from usdata import build_query, fetch, get

items = fetch(
    get("noaa:nws-warnings"),
    build_query(
        location="Oklahoma",
        start="2024-05-06T18:00Z",
        end="2024-05-07T12:00Z",
        events="TO.W,TO.A",
    ),
)
print(items[0].path)

Manifest

# dataset.yaml, then: usdata pull dataset.yaml
name: oklahoma-tornado-warnings-may-2024
sources:
  - name: oklahoma
    dataset: noaa:nws-warnings
    # A state, not a county: the service filters by state or issuing office.
    location: Oklahoma
    # Selected by ISSUED, the event's start, in UTC: 1 PM CDT on 6 May to 7 AM the next day.
    start: 2024-05-06T18:00Z
    end: 2024-05-07T12:00Z
    params: {events: "TO.W,TO.A"}

The same query the walkthrough below ran. No bundled reader opens these files; use the tools named in the guide.

Saved results from a run against the live service; the notebook records when it ran and the checksums of what it read. Run it yourself.

Every National Weather Service watch, warning, and advisory carries a VTEC code: a phenomenon (TO tornado, SV severe thunderstorm), a significance (W warning, A watch, Y advisory), an issuing office, and an event number. Since 2007 a tornado or severe thunderstorm warning is drawn as a polygon around the threat; watches and most other products are still issued for whole counties or forecast zones. The Iowa Environmental Mesonet (IEM) at Iowa State University archives the NWS product stream and serves every product with its geometry, for any window, as a zipped shapefile; the NWS's own API keeps no archive.

This walkthrough pulls the tornado warnings and watches issued for Oklahoma on the evening of 6 May 2024 and maps them. It needs matplotlib, pandas, and pyshp, which reads shapefiles, since usdata has no shapefile reader; the file is under a megabyte and the run takes a few seconds.

import zipfile
from datetime import UTC, datetime
from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd
import shapefile
from matplotlib.patches import PathPatch
from matplotlib.path import Path as Outline

import usdata
from usdata import cite_lockfile, pull, verify

# One figure style for every usdata notebook, so previews look alike.
plt.rcParams.update(
    {
        "figure.figsize": (8, 4.5),
        "figure.dpi": 120,
        "axes.spines.top": False,
        "axes.spines.right": False,
        "axes.grid": True,
        "grid.alpha": 0.3,
        "font.size": 10,
    }
)
manifest = Path("dataset.yaml")
print("Executed (UTC):", datetime.now(UTC).isoformat(timespec="seconds"))
print(f"usdata {usdata.__version__}; pandas {pd.__version__}; pyshp {shapefile.__version__}")
Executed (UTC): 2026-10-08T23:57:26+00:00
usdata 0.34.0; pandas 3.0.6; pyshp 3.1.6

Select

The manifest names a state, a window, and two event types. location: Oklahoma becomes IEM's state filter; a county is refused, since the service filters by state or issuing office (params: {wfo: OUN}), not by county. The window selects rows by ISSUED, the event's start, inclusive at both ends: from 18 UTC on 6 May (1 PM CDT) to 12 UTC the next morning. events names VTEC phenomenon and significance pairs; without it every product comes back, heat and flood and winter advisories included.

print(manifest.read_text())
name: oklahoma-tornado-warnings-may-2024
sources:
  - name: oklahoma
    dataset: noaa:nws-warnings
    # A state, not a county: the service filters by state or issuing office.
    location: Oklahoma
    # Selected by ISSUED, the event's start, in UTC: 1 PM CDT on 6 May to 7 AM the next day.
    start: 2024-05-06T18:00Z
    end: 2024-05-07T12:00Z
    params: {events: "TO.W,TO.A"}

What arrives

One zip per UTC month the window touches, so one here. Inside are the shapefile's parts and a CSV of the same attributes. IEM builds the zip on request and stamps it with the time, so two identical requests return different bytes; usdata rewrites it in a canonical form, the same members stored uncompressed with fixed stamps, and records that in the provenance. That is what lets the lockfile written beside the manifest pin it.

result = pull(manifest)
item = result.one("oklahoma")
print("File:", item.path.name)
print(f"Bytes: {item.provenance.size:,}")
print("Source:", item.provenance.source_url)
print("Checksum:", item.provenance.checksum)
print("Transformations:", *item.provenance.transformations, sep="\n  ")
with zipfile.ZipFile(item.path) as archive:
    for info in archive.infolist():
        print(f"  {info.filename:42} {info.file_size:>9,} bytes")
File: wwa_20240506_20240507_cbe2ee89a613d926d180.zip
Bytes: 326,270
Source: https://mesonet.agron.iastate.edu/cgi-bin/request/gis/watchwarn.py?accept=shapefile&location_group=states&states=OK&limitps=1&phenomena=TO%2CTO&significance=W%2CA&sts=2024-05-06T18%3A00%3A00Z&ets=2024-05-07T12%3A00%3A01Z
Checksum: sha256:20535ffc9408817303aa7bcb80d0d44d03331fd0f6858d57f9167b7d6f91a91b
Transformations:
  iem canonical zip: members kept in order, stored uncompressed with timestamps fixed at 1980-01-01 00:00 and no extra fields; the DBF header's last-update date set to 1980-01-01
  wwa_202405061800_202405071200.shp            239,364 bytes
  wwa_202405061800_202405071200.shx              1,436 bytes
  wwa_202405061800_202405071200.dbf             48,326 bytes
  wwa_202405061800_202405071200.prj                145 bytes
  wwa_202405061800_202405071200.cpg                 10 bytes
  wwa_202405061800_202405071200.csv             36,115 bytes

Open

There is no bundled shapefile reader, so open() refuses the zip. pyshp reads it directly; the helper below turns its records into a DataFrame with one row per shape and parses the timestamps, which the DBF stores as YYYYMMDDHHMM text in UTC. (geopandas.read_file(item.path) would do the same in one line, with a heavier install.)

GTYPE says what each row's geometry is: P for the polygon a forecaster drew, C for one county or zone the product was issued for, named by NWS_UGC. A polygon warning appears both ways.

def read_zip(path: Path, times: list[str]) -> pd.DataFrame:
    """One row per shape: the DBF attributes, the named UTC times parsed, and the shape."""
    with shapefile.Reader(str(path)) as reader:
        names = [field[0] for field in reader.fields[1:]]
        frame = pd.DataFrame([list(record) for record in reader.records()], columns=names)
        frame["shape"] = reader.shapes()
    for column in times:
        frame[column] = pd.to_datetime(
            frame[column], format="%Y%m%d%H%M", utc=True, errors="coerce"
        )
    return frame


def patch(shape, **style) -> PathPatch | None:
    """A matplotlib patch for a polygon shape, its parts as rings so holes stay holes."""
    if not shape.points:
        return None
    vertices, codes = [], []
    for start, stop in zip(shape.parts, [*shape.parts[1:], len(shape.points)], strict=True):
        ring = shape.points[start:stop]
        vertices += ring
        codes += [Outline.MOVETO] + [Outline.LINETO] * (len(ring) - 1)
    return PathPatch(Outline(vertices, codes), **style)


TIMES = ["ISSUED", "EXPIRED", "INIT_ISS", "INIT_EXP", "UPDATED", "POLY_BEG", "POLY_END"]
rows = read_zip(item.path, TIMES)
print(f"{len(rows)} rows from offices {', '.join(sorted(rows['WFO'].unique()))}")
rows.groupby(["PHENOM", "SIG", "GTYPE"]).size().rename("rows").to_frame()
167 rows from offices OUN, SHV, TSA
rows
PHENOM SIG GTYPE
TO A C 82
W C 58
P 27

One warning, both ways: the one in effect when the Barnsdall EF4 began at 02:12 UTC.

moment = pd.Timestamp("2024-05-07T02:12Z")
barnsdall = rows[
    (rows["WFO"] == "TSA")
    & (rows["PHENOM"] == "TO")
    & (rows["SIG"] == "W")
    & (rows["ISSUED"] <= moment)
    & (rows["EXPIRED"] > moment)
]
columns = ["ETN", "GTYPE", "NWS_UGC", "ISSUED", "EXPIRED", "TORNTAG", "AREA_KM2", "PROD_ID"]
barnsdall[columns]
ETN GTYPE NWS_UGC ISSUED EXPIRED TORNTAG AREA_KM2 PROD_ID
23 45 P 2024-05-07 01:56:00+00:00 2024-05-07 02:45:00+00:00 RADAR INDICATED 1374.900592 202405070156-KTSA-WFUS54-TORTSA
68 45 C OKC113 2024-05-07 01:56:00+00:00 2024-05-07 02:45:00+00:00 5965.106445 202405070156-KTSA-WFUS54-TORTSA

Tulsa's warning 45, issued at 01:56 UTC, is one polygon row and one county row. The polygon carries the impact tag, RADAR INDICATED; the county row names Osage County, OKC113, the only county it was issued for, and its area is the county's, more than four times the polygon's. Counting warnings by county overstates where they applied; the polygon is the warned area.

A first look

The evening's tornado watches as the counties they covered, shaded, and every tornado warning polygon drawn over them, colored by the hour it was issued. Coordinates are longitude and latitude, stretched by the cosine of the mean latitude so shapes look right.

fig, ax = plt.subplots(figsize=(8, 5), layout="constrained")
for shape in rows.loc[(rows["PHENOM"] == "TO") & (rows["SIG"] == "A"), "shape"]:
    if (county := patch(shape, facecolor="#f2d7b6", edgecolor="white", linewidth=0.4)) is not None:
        ax.add_patch(county)
polygons = rows[(rows["PHENOM"] == "TO") & (rows["SIG"] == "W") & (rows["GTYPE"] == "P")]
hours = (polygons["ISSUED"] - pd.Timestamp("2024-05-06T18:00Z")).dt.total_seconds() / 3600
norm = plt.Normalize(0, 14)
colors = plt.cm.viridis(norm(hours))
for shape, color in zip(polygons["shape"], colors, strict=True):
    ax.add_patch(patch(shape, facecolor="none", edgecolor=color, linewidth=1.1))
ax.autoscale_view()
ax.set_aspect(1 / 0.81)
ax.set_xlabel("Longitude")
ax.set_ylabel("Latitude")
scale = plt.cm.ScalarMappable(cmap="viridis", norm=norm)
bar = fig.colorbar(scale, ax=ax, shrink=0.8)
bar.set_label("Warning issued, hours after 18 UTC on 6 May")
ax.set_title(
    f"{len(polygons)} tornado warning polygons over tornado watch counties, Oklahoma, 6-7 May 2024"
)
plt.show()
Saved plot from NWS watch, warning, and advisory polygons and areas

Two tornado watches, 189 and 193, covered 74 of Oklahoma's 77 counties, and the warnings moved east with the storms, from the northwest at 21:33 UTC to the Arkansas line at 07:26 the next morning.

The window selects by ISSUED, the event's start as last updated, and INIT_ISS is when the product that started it was sent. Usually they are the same instant. They are not here for tornado watch 193: the offices' county products for it went out at 03:13 and 03:14, a minute or two before the watch's own start at 03:15. For a winter storm watch sent a day ahead of the start it gives, the gap is a day. To keep only what was known before a moment, filter on INIT_ISS.

late = rows[rows["ISSUED"] != rows["INIT_ISS"]]
print(f"Rows whose ISSUED differs from INIT_ISS: {len(late)} of {len(rows)}")
late.groupby(["PHENOM", "SIG", "ETN", "ISSUED", "INIT_ISS"]).size().rename("counties").to_frame()
Rows whose ISSUED differs from INIT_ISS: 22 of 167
counties
PHENOM SIG ETN ISSUED INIT_ISS
TO A 193 2024-05-07 03:15:00+00:00 2024-05-07 03:13:00+00:00 7
2024-05-07 03:14:00+00:00 15

Pin and cite

verify checks the cached file against the lockfile's checksum; because the zip is rewritten canonically, a later pull of the same window restores the same bytes. Keep the manifest and lockfile with your analysis; the citation below is what a methods section needs, and usdata cite dataset.yaml prints the same.

assert verify(manifest) == []
for citation in cite_lockfile(manifest):
    print(citation.as_text())
noaa:nws-warnings
  National Weather Service watch, warning, and advisory products, as archived and served by the Iowa Environmental Mesonet of Iowa State University, accessed via usdata
  homepage: https://mesonet.agron.iastate.edu/request/gis/watchwarn.phtml
  license: Public domain (NWS products; IEM materials are public domain, attribution appreciated)
  terms: https://mesonet.agron.iastate.edu/disclaimer.php
  retrieved: 2026-10-08; 1 checksummed asset (326,270 bytes) pinned by usdata 0.34.0
  sources: oklahoma

What was awkward

  • There is no shapefile reader, so every notebook needs pyshp or geopandas and a helper like read_zip to get from the zip to a frame with times.
  • The DBF truncates column names to ten characters (TORNTAG, PROD_ID), while the CSV in the same zip spells them out (TORNADOTAG, PRODUCT_ID), so code written against one does not run on the other.
  • Timestamps are text with no timezone marker; they are UTC, which only the documentation says.
  • The window selects by ISSUED, the event start as last updated, which is neither when the product was sent nor when the event was in effect. For "what was known before this moment", widen the window and filter on INIT_ISS yourself.
  • Canonical zips are stored uncompressed, four to five times the size IEM sends, so a month of every product nationwide is large; narrow with events, a state, or storm_based.

Reference

Cite as National Weather Service watch, warning, and advisory products, as archived and served by the Iowa Environmental Mesonet of Iowa State University, accessed via usdata