Fix broken series-ID builders; env-var config; project README

Repair every dead series-ID encoding (library now 82/82 query helpers
return live data, verified against the BLS API):

- JOLTS: 21-char format (was 18) — add state/area/sizeclass fields
- OES: national area code 0000000 (was invalid 0000400)
- ECI: correct owner/component/estimate encoding, default unadjusted (CIU)
- Productivity: 4-digit sector + 4-digit measure codes (was 2+3)
- QCEW: 13-char timeseries-API form (ENUUS00010510 / ENU{fips}00010{own}10)
- PPI: repoint finished-goods -> final demand (WPUFD4); keep alias
- ECEC: drop fabricated health-insurance/retirement helpers; add total benefits
- wages SOC: software developers 151132 -> 151252 (2018 SOC)

Tooling/docs:
- config reads BLS_API_KEY env var (takes precedence; config.py gitignored)
- add requirements.txt
- rewrite README as project front door + coverage table + limitations
- correct JOLTS/OES tables in series_id_formats.md, USAGE.md, dataset explorer

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 09:45:27 -04:00
parent 20bd3e9a2f
commit 4fe734381e
10 changed files with 246 additions and 245 deletions

View File

@ -261,11 +261,11 @@ def oes_national(
Examples:
oes_national() → all occupations, employment
oes_national("151132", data_type="annual_median") → software devs median wage
oes_national("151252", data_type="annual_median") → software devs median wage
"""
dtype = _OES_DATATYPE.get(data_type, data_type)
# OE+U+N(area_type)+0000400(national area 7 chars)+industry(6)+occupation(6)+dtype(2) = 25
return f"OEUN0000400{industry_code:0<6}{occupation_code:0<6}{dtype}"
# OE + U + N(area_type) + 0000000(national area, 7 chars) + industry(6) + occupation(6) + dtype(2) = 25
return f"OEUN0000000{industry_code:0<6}{occupation_code:0<6}{dtype}"
# ---------------------------------------------------------------------------
@ -297,72 +297,80 @@ def jolts(
seasonal: True for SA
Examples:
jolts() → "JTS000000000000JOL" (openings level)
jolts("quits", rate_level="R") → "JTS000000000000QUR" (quits rate)
jolts("hires", seasonal=False) → "JTU000000000000HIL" (hires level, NSA)
jolts() → "JTS000000000000000JOL" (openings level)
jolts("quits", rate_level="R") → "JTS000000000000000QUR" (quits rate)
jolts("hires", seasonal=False) → "JTU000000000000000HIL" (hires level, NSA)
"""
adj = "S" if seasonal else "U"
elem = _JOLTS_ELEMENT.get(element, element)
# Format: JT+adj+industry(6)+zeros(6)+elem(2)+rate_level(1) = 18 chars
# Format: JT + adj + industry(6) + state(2) + area(5) + sizeclass(2) + elem(2) + rate_level(1) = 21 chars
# state/area/sizeclass default to national / all (the "ownership" arg is reserved; not part of the ID)
ind = industry[:6].ljust(6, "0")
return f"JT{adj}{ind}{'0'*6}{elem}{rate_level}"
return f"JT{adj}{ind}{'0'*9}{elem}{rate_level}"
# ---------------------------------------------------------------------------
# ECI — Employment Cost Index
# ---------------------------------------------------------------------------
def eci(
worker_type: str = "10", # 10=civilian, 20=private, 30=state/local
occupation: str = "00", # 00=all, 10=mgmt/prof, 20=service, etc.
industry: str = "000000000",
component: str = "A", # A=total comp, W=wages, B=benefits
seasonal: bool = True,
owner: str = "10", # 10=civilian, 20=private, 30=state/local gov
component: str = "10", # 10=total compensation, 20=wages & salaries, 30=benefits
estimate: str = "A", # A=12-month % change, Q=3-month % change, I=index
seasonal: bool = False, # the 12-month % change estimate is published unadjusted (CIU)
) -> str:
"""
ECI series for employment cost changes.
Format: CI + adj + owner(2) + component(2) + 9 zeros + estimate(1) = 17 chars.
Note: the 12-month % change estimate ("A") is published unadjusted only;
seasonally adjusted ECI ("CIS") publishes the 3-month change ("Q") and index ("I").
Examples:
eci() → "CIU1010000000000A" (civilian, all workers, total compensation)
eci(component="W") → wages only
eci() "CIU1010000000000A" (civilian, total compensation, 12-mo % chg)
eci(component="20") "CIU1020000000000A" (civilian, wages & salaries)
eci(component="30") → "CIU1030000000000A" (civilian, benefits)
eci(owner="20") → "CIU2010000000000A" (private, total compensation)
"""
adj = "S" if seasonal else "U"
return f"CI{adj}{worker_type}{occupation}{industry}{component}"
return f"CI{adj}{owner}{component}{'0'*9}{estimate}"
# ---------------------------------------------------------------------------
# Productivity (Major Sector)
# ---------------------------------------------------------------------------
_PR_SECTOR = {
"business": "85",
"nonfarm_business":"86",
"manufacturing": "88",
"durable_mfg": "89",
"nondurable_mfg": "90",
"business": "8400",
"nonfarm_business": "8500",
"manufacturing": "3000",
"durable_mfg": "3100",
"nondurable_mfg": "3200",
}
_PR_MEASURE = {
"output_per_hour": "092",
"output": "041",
"hours": "051",
"compensation": "061",
"real_comp_per_hr": "071",
"unit_labor_cost": "111",
"unit_nonlabor_pay":"112",
"output_per_hour": "6092",
"output": "6042",
"hours": "6032",
"compensation": "6062", # hourly compensation
"real_comp_per_hr": "6152", # real hourly compensation
"unit_labor_cost": "6112",
}
def productivity(
sector: str = "business",
sector: str = "nonfarm_business",
measure: str = "output_per_hour",
seasonal: bool = True,
) -> str:
"""
Major Sector Productivity series.
Format: PR + adj + sector(4) + measure(4) = 11 chars. Published seasonally adjusted (PRS).
Examples:
productivity() → "PRS85006092" (business output per hour, SA)
productivity("manufacturing", "unit_labor_cost")
productivity() "PRS85006092" (nonfarm business output per hour)
productivity("manufacturing", "unit_labor_cost")"PRS30006112"
"""
adj = "S" if seasonal else "U"
sec = _PR_SECTOR.get(sector, sector)
mea = _PR_MEASURE.get(measure, measure)
return f"PR{adj}{sec}06{mea}"
return f"PR{adj}{sec}{mea}"