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:
@ -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"
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
|
||||
@ -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).
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user