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

@ -113,21 +113,27 @@ def jolts_dashboard() -> dict:
# QCEW
# ---------------------------------------------------------------------------
def qcew_national_private() -> str:
"""QCEW — national, private sector, all industries, quarterly."""
return "ENU0000010510000"
"""QCEW — national, private sector, all industries (monthly employment)."""
return "ENUUS00010510"
def qcew_dc_private() -> str:
"""QCEW — DC, private sector, all industries."""
return "ENU1100010510000"
return qcew_state(11, "5")
def qcew_state(state_fips: int, ownership: str = "5") -> str:
"""
QCEW state-level series.
Format: EN + U + area(5: 2-digit FIPS + "000") + datatype "1" + size "0"
+ ownership(1) + industry "10" (total, all industries) = 13 chars.
Args:
state_fips: 2-digit FIPS
state_fips: 2-digit FIPS (e.g. 11=DC)
ownership: "0"=all, "5"=private, "1"=federal, "2"=state, "3"=local
Note: BLS serves the full QCEW catalog (county/industry detail) through its
dedicated QCEW Open Data API, not the timeseries API used here.
"""
return f"ENU{state_fips:02d}0001{ownership}10000"
return f"ENU{state_fips:02d}00010{ownership}10"

View File

@ -66,9 +66,13 @@ def ppi_all_commodities() -> str:
return ppi_commodity("00000000")
def ppi_finished_goods() -> str:
"""PPI — finished goods."""
return ppi_commodity("3")
def ppi_final_demand() -> str:
"""PPI — final demand (successor to the discontinued 'finished goods' index)."""
return ppi_commodity("FD4")
# Backwards-compatible alias; the legacy "finished goods" index was replaced by final demand.
ppi_finished_goods = ppi_final_demand
def ppi_energy() -> str:
@ -82,7 +86,7 @@ def ppi_food() -> str:
def ppi_dashboard() -> dict:
return {
"All Commodities": ppi_all_commodities(),
"Finished Goods": ppi_finished_goods(),
"Final Demand": ppi_final_demand(),
"Food": ppi_food(),
"Energy": ppi_energy(),
}

View File

@ -17,7 +17,7 @@ def occupation_annual_median_wage(soc_code: str) -> str:
Annual median wage for a specific occupation (national, all industries).
Args:
soc_code: 6-digit SOC code (e.g. "151132" for software developers,
soc_code: 6-digit SOC code (e.g. "151252" for software developers,
"291141" for registered nurses, "119021" for construction mgrs)
"""
return oes_national(soc_code, "000000", "annual_median")
@ -30,7 +30,7 @@ def occupation_employment(soc_code: str) -> str:
# Common occupation codes
SOC_CODES = {
"software_developers": "151132",
"software_developers": "151252",
"registered_nurses": "291141",
"teachers_elementary": "252021",
"accountants": "132011",
@ -51,29 +51,29 @@ SOC_CODES = {
# ---------------------------------------------------------------------------
# ECI — Employment Cost Index
# ---------------------------------------------------------------------------
def eci_total_compensation(seasonal: bool = True) -> str:
"""ECI — civilian workers, all industries, total compensation."""
return eci("10", "10", component="A", seasonal=seasonal)
def eci_total_compensation(seasonal: bool = False) -> str:
"""ECI — civilian workers, all industries, total compensation (12-mo % change)."""
return eci(owner="10", component="10", seasonal=seasonal)
def eci_wages(seasonal: bool = True) -> str:
"""ECI — civilian workers, wages and salaries only."""
return eci("10", "10", component="W", seasonal=seasonal)
def eci_wages(seasonal: bool = False) -> str:
"""ECI — civilian workers, wages and salaries only (12-mo % change)."""
return eci(owner="10", component="20", seasonal=seasonal)
def eci_benefits(seasonal: bool = True) -> str:
"""ECI — civilian workers, benefit costs only."""
return eci("10", "10", component="B", seasonal=seasonal)
def eci_benefits(seasonal: bool = False) -> str:
"""ECI — civilian workers, benefit costs only (12-mo % change)."""
return eci(owner="10", component="30", seasonal=seasonal)
def eci_private(seasonal: bool = True) -> str:
"""ECI — private sector, total compensation."""
return eci("20", "10", component="A", seasonal=seasonal)
def eci_private(seasonal: bool = False) -> str:
"""ECI — private sector, total compensation (12-mo % change)."""
return eci(owner="20", component="10", seasonal=seasonal)
def eci_state_local(seasonal: bool = True) -> str:
"""ECI — state and local government, total compensation."""
return eci("30", "10", component="A", seasonal=seasonal)
def eci_state_local(seasonal: bool = False) -> str:
"""ECI — state and local government, total compensation (12-mo % change)."""
return eci(owner="30", component="10", seasonal=seasonal)
def eci_dashboard() -> dict:
@ -90,18 +90,18 @@ def eci_dashboard() -> dict:
# ECEC — Employer Costs for Employee Compensation
# ---------------------------------------------------------------------------
def ecec_total_compensation() -> str:
"""ECEC — civilian workers, total compensation cost per hour."""
"""ECEC — civilian workers, total compensation cost per hour worked."""
return "CMU1010000000000D"
def ecec_health_insurance() -> str:
"""ECEC — health insurance cost per hour worked."""
return "CMU1010000000000H"
def ecec_total_benefits() -> str:
"""ECEC — civilian workers, total benefits cost per hour worked."""
return "CMU1036000000000D"
def ecec_retirement() -> str:
"""ECEC — retirement & savings cost per hour worked."""
return "CMU1010000000000R"
# Note: ECEC benefit subcomponents (health insurance, retirement & savings, etc.)
# are encoded as specific benefit-subcell codes in the series ID (suffix stays "D").
# Look them up against the ECEC component list before adding helpers — do not
# fabricate them with a letter suffix (the old "...H"/"...R" forms were invalid).
# ---------------------------------------------------------------------------