Initial BLS data reference and DC/MD/VA unemployment dashboard

Includes:
- API v1/v2 documentation, endpoints, request/response schemas
- Complete survey catalog (60 surveys, live-fetched from API)
- Series ID decode tables: LAUS, CES, SM, QCEW, OES, JOLTS, CPI, PPI
- QCEW quarterly (47 fields) and annual (43 fields) CSV schemas
- dc_md_va_unemployment.py: pulls 16 LAUS series for DC/MD/VA + DC MSA
- Example API response and 5-year CSV output

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 11:03:14 -04:00
commit 2b9d3e6d68
9 changed files with 3218 additions and 0 deletions

214
dc_md_va_unemployment.py Normal file
View File

@ -0,0 +1,214 @@
"""
DC/MD/VA Unemployment Dashboard
Pulls LAUS data for DC, Maryland, Virginia (state) plus the DC-Arlington-Alexandria metro.
Outputs a formatted console table and saves results to CSV.
"""
import requests
import csv
import sys
from datetime import datetime
from config import BLS_API_KEY, BLS_API_BASE
# ---------------------------------------------------------------------------
# Series definitions
# Format: LAUS[S=SA / U=unadj][area code 15 chars][measure 2 chars]
# Measures: 03=rate 04=unemployed 05=employed 06=labor force
# ---------------------------------------------------------------------------
SERIES = {
# DC
"DC_rate": "LAUST110000000000003",
"DC_unemployed": "LAUST110000000000004",
"DC_employed": "LAUST110000000000005",
"DC_laborforce": "LAUST110000000000006",
# Maryland
"MD_rate": "LAUST240000000000003",
"MD_unemployed": "LAUST240000000000004",
"MD_employed": "LAUST240000000000005",
"MD_laborforce": "LAUST240000000000006",
# Virginia
"VA_rate": "LAUST510000000000003",
"VA_unemployed": "LAUST510000000000004",
"VA_employed": "LAUST510000000000005",
"VA_laborforce": "LAUST510000000000006",
# DC-Arlington-Alexandria MSA (state FIPS 11 + CBSA 47900)
"DCMSA_rate": "LAUMT114790000000003",
"DCMSA_unemployed":"LAUMT114790000000004",
"DCMSA_employed": "LAUMT114790000000005",
"DCMSA_laborforce":"LAUMT114790000000006",
}
LABELS = {
"DC": "District of Columbia",
"MD": "Maryland",
"VA": "Virginia",
"DCMSA": "DC Metro (MSA)",
}
MEASURES = ["rate", "unemployed", "employed", "laborforce"]
MEASURE_LABELS = {
"rate": "Unemp Rate %",
"unemployed": "Unemployed",
"employed": "Employed",
"laborforce": "Labor Force",
}
# ---------------------------------------------------------------------------
def fetch(series_ids: list[str], start_year: int, end_year: int) -> dict:
"""POST to BLS API v2, return dict keyed by seriesID."""
payload = {
"seriesid": series_ids,
"startyear": str(start_year),
"endyear": str(end_year),
"registrationkey": BLS_API_KEY,
"catalog": True,
"calculations": True,
"annualaverage": False,
}
r = requests.post(f"{BLS_API_BASE}/timeseries/data/", json=payload, timeout=30)
r.raise_for_status()
body = r.json()
if body["status"] != "REQUEST_SUCCEEDED":
raise RuntimeError(f"BLS API error: {body['message']}")
return {s["seriesID"]: s["data"] for s in body["Results"]["series"]}
def latest(data: list[dict]) -> dict | None:
"""Return the most recent non-null observation."""
for obs in data:
if obs["value"] != "-":
return obs
return None
def get_calc(obs: dict, window: str) -> str:
"""Pull a net_change or pct_change value safely."""
try:
return obs["calculations"]["net_changes"].get(window, "")
except (KeyError, TypeError):
return ""
def fmt_num(val: str, is_rate: bool = False) -> str:
if val in (None, "", "-"):
return ""
try:
n = float(val)
return f"{n:.1f}%" if is_rate else f"{n:,.0f}"
except ValueError:
return val
def fmt_chg(val: str, is_rate: bool = False) -> str:
if val in (None, "", ""):
return ""
try:
n = float(val)
sign = "+" if n > 0 else ""
return f"{sign}{n:.1f}pp" if is_rate else f"{sign}{n:,.0f}"
except ValueError:
return val
def print_dashboard(results: dict):
geo_keys = ["DC", "MD", "VA", "DCMSA"]
now = datetime.now().strftime("%Y-%m-%d %H:%M")
print()
print("=" * 72)
print(f" DC / MD / VA UNEMPLOYMENT DASHBOARD pulled {now}")
print("=" * 72)
for geo in geo_keys:
rate_series = SERIES[f"{geo}_rate"]
rate_data = results.get(rate_series, [])
obs = latest(rate_data)
if not obs:
print(f"\n {LABELS[geo]}: no data\n")
continue
period = f"{obs['periodName']} {obs['year']}"
rate = fmt_num(obs["value"], is_rate=True)
mom = fmt_chg(get_calc(obs, "1"), is_rate=True)
yoy = fmt_chg(get_calc(obs, "12"), is_rate=True)
print()
print(f" {LABELS[geo]} ({period})")
print(f" {'' * 60}")
print(f" {'Unemployment Rate:':<28} {rate:>8} MoM: {mom:>8} YoY: {yoy:>8}")
for measure in ["unemployed", "employed", "laborforce"]:
sid = SERIES[f"{geo}_{measure}"]
data = results.get(sid, [])
o = latest(data)
if not o:
continue
label = MEASURE_LABELS[measure]
val = fmt_num(o["value"])
m = fmt_chg(get_calc(o, "1"))
y = fmt_chg(get_calc(o, "12"))
print(f" {label + ':':<28} {val:>10} MoM: {m:>8} YoY: {y:>8}")
print()
print("=" * 72)
print(" Note: Not seasonally adjusted. MoM/YoY = net change.")
print("=" * 72)
print()
def save_csv(results: dict, path: str):
rows = []
geo_keys = ["DC", "MD", "VA", "DCMSA"]
for geo in geo_keys:
for measure in MEASURES:
key = f"{geo}_{measure}"
sid = SERIES[key]
data = results.get(sid, [])
for obs in data:
if obs["value"] == "-":
continue
rows.append({
"geography": LABELS[geo],
"geo_code": geo,
"measure": measure,
"series_id": sid,
"year": obs["year"],
"period": obs["period"],
"period_name": obs["periodName"],
"value": obs["value"],
"mom_chg": get_calc(obs, "1"),
"qoq_chg": get_calc(obs, "3"),
"yoy_chg": get_calc(obs, "12"),
})
if not rows:
print("No data to save.")
return
fieldnames = list(rows[0].keys())
with open(path, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=fieldnames)
w.writeheader()
w.writerows(rows)
print(f" Saved {len(rows)} rows → {path}")
def main():
current_year = datetime.now().year
start_year = current_year - 4 # 5 years of history (within 20-yr v2 limit)
print(f"Fetching {len(SERIES)} LAUS series ({start_year}{current_year})...")
# API allows 50 series per call; we have 16, so one call is fine
results = fetch(list(SERIES.values()), start_year, current_year)
print_dashboard(results)
out_csv = "dc_md_va_unemployment.csv"
save_csv(results, out_csv)
if __name__ == "__main__":
main()