Multi-Year Queries¶
WONDER splits data into separate datasets by year range. To collect a continuous time series spanning more than one era — say, mortality from 1979 to 2024 — you query each era's dataset separately and merge the results.
The epoch map for mortality¶
1968–1978 D74 Compressed Mortality (ICD-8)
1979–1998 D16 Compressed Mortality (ICD-9)
1999–2020 D77 Multiple Cause of Death (ICD-10, bridged race, final)
2018–2023 D157 Final Mortality, Single Race (ICD-10, final — overlaps D77)
2018–present D176 Provisional Mortality (ICD-10, most current)
For a 1979–2024 all-cause series use: D16 + D77 + D176.
For a 1999–2024 series use: D77 + D176 (simpler, same ICD-10 era).
ICD-9 vs ICD-10 discontinuity
D16 uses ICD-9 codes (pre-1999); D77 and later use ICD-10. Cause-specific time series crossing 1999 need a bridging adjustment — totals can be compared directly, but individual cause groupings differ. See CDC ICD bridging comparability.
Rate limit pattern¶
WONDER enforces ≥15 seconds between requests. Always sleep between epoch queries.
import time
RATE_LIMIT = 16 # one second of margin
for i, epoch in enumerate(epochs):
if i > 0:
time.sleep(RATE_LIMIT)
result = run_query(epoch)
Example: mortality by year, 1979–2024¶
This is the full pattern used in src/wonder/queries/fetch_mortality_by_year.py.
Step 1 — save XML for each epoch¶
Generate XML with the LLM builder (one-time, then commit the files):
# D16: 1979–1998, ICD-9, grouped by year and ICD chapter
uv run python -m wonder build \
"all-cause deaths by year 1979-1998 grouped by year and ICD chapter, dataset D16" \
> src/wonder/queries/mortality-by-year-cause-1979-1998-req.xml
# D77: 1999–2020
uv run python -m wonder build \
"all-cause deaths by year 1999-2020 grouped by year and ICD chapter, dataset D77" \
> src/wonder/queries/mortality-by-year-cause-1999-2020-req.xml
# D176: 2021–present
uv run python -m wonder build \
"all-cause deaths by year 2021-2024 grouped by year and ICD chapter, dataset D176" \
> src/wonder/queries/mortality-by-year-cause-2021-2024-req.xml
Step 2 — fetch and merge¶
import time
import sys
from pathlib import Path
from collections import defaultdict
PROJECT_ROOT = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
from wonder.client import WonderClient
QUERIES = [
# (dataset_id, xml_path, hierarchical_rows)
("D16", "mortality-by-year-cause-1979-1998-req.xml", True),
("D77", "mortality-by-year-cause-1999-2020-req.xml", False),
("D176", "mortality-by-year-cause-2021-2024-req.xml", False),
]
client = WonderClient(timeout=120)
queries_dir = PROJECT_ROOT / "src/wonder/queries"
all_records = []
for i, (dataset_id, filename, hierarchical) in enumerate(QUERIES):
if i > 0:
print(f"Sleeping 16s for rate limit...")
time.sleep(16)
xml_path = queries_dir / filename
print(f"Querying {dataset_id}...")
xml_result = client.execute_query_file(str(xml_path))
if hierarchical:
# D16 uses merged-cell rows: year only appears on the first sub-row
records = parse_hierarchical(client, xml_result)
else:
records = parse_flat(client, xml_result)
all_records.extend(records)
print(f" {len(records)} rows")
# Merge: keep max deaths on any overlap between datasets
merged = defaultdict(lambda: defaultdict(int))
for rec in all_records:
y, cause, deaths = rec["year"], rec["cause"], rec["deaths"]
merged[y][cause] = max(merged[y][cause], deaths)
Step 3 — handle D16's hierarchical row format¶
D76 and later datasets return flat rows: every row has [year, cause, deaths].
D16 returns "merged-cell" rows: the year column only appears on the first sub-row for each year group; subsequent rows for the same year omit the year column.
def _is_year(label: str) -> bool:
s = label.strip()
return len(s) == 4 and s.isdigit() and 1900 <= int(s) <= 2099
def parse_hierarchical(client, xml: str) -> list[dict]:
"""Parser for D16 merged-cell rows."""
rows = client.parse_response_table(xml)
records = []
current_year = None
for row in rows:
if row.is_total:
continue
cells = row.cells
if not cells:
continue
c0 = (cells[0].label or "").strip()
if _is_year(c0):
# First sub-row for this year: [year, cause, deaths, ...]
current_year = int(c0)
cause = (cells[1].label or "").strip() if len(cells) > 1 else ""
deaths = cells[2].get_numeric_value() if len(cells) > 2 else None
else:
# Continuation sub-row: [cause, deaths, ...]
cause = c0
deaths = cells[1].get_numeric_value() if len(cells) > 1 else None
if cause and deaths is not None and current_year is not None:
records.append({"year": current_year, "cause": cause, "deaths": int(deaths)})
return records
def parse_flat(client, xml: str) -> list[dict]:
"""Parser for D77/D176 flat rows."""
rows = client.parse_response_table(xml)
records = []
for row in rows:
if row.is_total:
continue
cells = row.cells
if len(cells) < 3:
continue
year_label = (cells[0].label or "").strip()
cause = (cells[1].label or "").strip()
deaths = cells[2].get_numeric_value()
if _is_year(year_label) and cause and deaths is not None:
records.append({"year": int(year_label), "cause": cause, "deaths": int(deaths)})
return records
Running the full mortality script¶
Outputs to data/raw/wonder/:
mortality-total-by-year.csv— total deaths per year, 1979–2024mortality-top5-causes-by-year.csv— top 5 ICD chapters per year (long format)
Quick multi-year via the LLM (single dataset)¶
For queries within a single dataset epoch (≤25 years, single ICD era), just include the full year range in the prompt:
# 22 years in one shot — D157 covers this entire span
uv run python -m wonder query \
"cancer death rates 1999-2023 by year, age-adjusted rate, dataset D157" -f csv
# 7 years in one shot on D176
uv run python -m wonder query \
"opioid overdose deaths 2018-2024 by year, grouped by year only" -f csv
WONDER's F_D176.V1 filter accepts a list of years — the LLM expands the range automatically.
Natality across epochs¶
Births from 1995–2024 require three datasets:
| Dataset | Years | Notes |
|---|---|---|
| D10 | 1995–2002 | |
| D27 | 2003–2006 | Requires full base template — see troubleshooting |
| D66 | 2007–2024 | Default |
# Pre-built XML queries for each epoch are in src/wonder/queries/
uv run python -m wonder run src/wonder/queries/births-by-year-1995-2002-req.xml
sleep 16
uv run python -m wonder run src/wonder/queries/births-by-year-2003-2006-req.xml
sleep 16
uv run python -m wonder run src/wonder/queries/births-by-year-2007-2024-req.xml
Infant mortality across epochs¶
Same pattern: one XML per epoch, sleep between them, merge on Year.
Merging results when epochs overlap¶
D77 ends in 2020 and D176 starts in 2018 — they overlap on 2018–2020. For totals and long-term trends, keep the final data (D77) for overlapping years and use D176 only for years after D77's coverage. The merge logic above uses max() as a pragmatic heuristic; for publication-quality work, choose the final dataset explicitly:
# Prefer D157 (final) over D176 (provisional) for 2018–2023
EPOCH_PRIORITY = {
"D157": 3, # highest: confirmed final data
"D77": 2,
"D176": 1, # lowest: provisional
"D16": 2,
}
merged = {}
for rec in all_records:
key = (rec["year"], rec["cause"])
priority = EPOCH_PRIORITY.get(rec["dataset"], 0)
if key not in merged or priority > merged[key]["priority"]:
merged[key] = {**rec, "priority": priority}