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>
377 lines
13 KiB
Python
377 lines
13 KiB
Python
"""
|
|
Series ID builders for major BLS surveys.
|
|
|
|
These functions construct the coded series IDs used by the BLS API.
|
|
Pass the returned string directly to BLSClient.fetch().
|
|
|
|
Example:
|
|
from bls_client.series import laus_state, ces_national
|
|
client.fetch([laus_state(11, "rate"), ces_national("00000000", "01")], 2020, 2025)
|
|
"""
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LAUS — Local Area Unemployment Statistics
|
|
# Series format: LA[adj][area_type+area_code 15 chars][measure 2 chars]
|
|
# ---------------------------------------------------------------------------
|
|
_LAUS_MEASURE = {
|
|
"rate": "03",
|
|
"unemployed": "04",
|
|
"employed": "05",
|
|
"laborforce": "06",
|
|
"emp_pop": "07",
|
|
"lfpr": "08",
|
|
"pop": "09",
|
|
}
|
|
|
|
def laus_state(state_fips: int, measure: str = "rate", seasonal: bool = False) -> str:
|
|
"""
|
|
LAUS series for a state.
|
|
|
|
Args:
|
|
state_fips: 2-digit state FIPS (e.g. 11=DC, 24=MD, 51=VA)
|
|
measure: "rate" | "unemployed" | "employed" | "laborforce"
|
|
seasonal: True for seasonally adjusted
|
|
|
|
Examples:
|
|
laus_state(11, "rate") → "LAUST110000000000003"
|
|
laus_state(24, "employed") → "LAUST240000000000005"
|
|
"""
|
|
adj = "S" if seasonal else "U"
|
|
m = _LAUS_MEASURE.get(measure, measure)
|
|
# Area code = ST(2) + state_fips(2) + 11 zeros = 15 chars → total series = 20
|
|
return f"LA{adj}ST{state_fips:02d}{'0'*11}{m}"
|
|
|
|
|
|
def laus_county(state_fips: int, county_fips: int, measure: str = "rate", seasonal: bool = False) -> str:
|
|
"""
|
|
LAUS series for a county.
|
|
|
|
Args:
|
|
state_fips: 2-digit state FIPS
|
|
county_fips: 3-digit county FIPS
|
|
measure: "rate" | "unemployed" | "employed" | "laborforce"
|
|
seasonal: True for seasonally adjusted
|
|
|
|
Examples:
|
|
laus_county(11, 1, "rate") → "LAUCN110010000000003"
|
|
"""
|
|
adj = "S" if seasonal else "U"
|
|
m = _LAUS_MEASURE.get(measure, measure)
|
|
# Area code = CN(2) + state_fips(2) + county_fips(3) + 8 zeros = 15 chars → total = 20
|
|
return f"LA{adj}CN{state_fips:02d}{county_fips:03d}{'0'*8}{m}"
|
|
|
|
|
|
def laus_msa(state_fips: int, cbsa_code: int, measure: str = "rate") -> str:
|
|
"""
|
|
LAUS series for a Metropolitan Statistical Area.
|
|
|
|
The state_fips should be the primary state in the MSA.
|
|
|
|
Args:
|
|
state_fips: 2-digit FIPS of the primary state
|
|
cbsa_code: 5-digit CBSA code
|
|
measure: "rate" | "unemployed" | "employed" | "laborforce"
|
|
|
|
Examples:
|
|
laus_msa(11, 47900, "rate") → DC-Arlington MSA unemployment rate
|
|
laus_msa(24, 12580, "rate") → Baltimore MSA unemployment rate
|
|
laus_msa(51, 40060, "rate") → Richmond MSA unemployment rate
|
|
"""
|
|
m = _LAUS_MEASURE.get(measure, measure)
|
|
return f"LAUMT{state_fips:02d}{cbsa_code:05d}000000{m}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CES — Current Employment Statistics (National)
|
|
# Series format: CE[adj][industry 8 chars][data_type 2 chars]
|
|
# ---------------------------------------------------------------------------
|
|
_CES_SUPERSECTOR = {
|
|
"total_nonfarm": "00000000",
|
|
"total_private": "05000000",
|
|
"mining_logging": "10000000",
|
|
"construction": "20000000",
|
|
"manufacturing": "30000000",
|
|
"durable": "31000000",
|
|
"nondurable": "32000000",
|
|
"trade_trans_util":"40000000",
|
|
"wholesale": "41000000",
|
|
"retail": "42000000",
|
|
"information": "50000000",
|
|
"financial": "55000000",
|
|
"professional": "60000000",
|
|
"education_health":"65000000",
|
|
"leisure": "70000000",
|
|
"other_services": "80000000",
|
|
"government": "90000000",
|
|
}
|
|
|
|
_CES_DATATYPE = {
|
|
"employees": "01",
|
|
"avg_weekly_hours": "02",
|
|
"avg_hourly_earn": "03",
|
|
"prod_employees": "06",
|
|
"women_employees": "10",
|
|
"avg_weekly_earn": "11",
|
|
}
|
|
|
|
def ces_national(
|
|
industry: str = "total_nonfarm",
|
|
data_type: str = "employees",
|
|
seasonal: bool = True,
|
|
) -> str:
|
|
"""
|
|
CES national employment series.
|
|
|
|
Args:
|
|
industry: Supersector name (see _CES_SUPERSECTOR) or 8-digit code string
|
|
data_type: Measure name (see _CES_DATATYPE) or 2-digit code string
|
|
seasonal: True for seasonally adjusted
|
|
|
|
Examples:
|
|
ces_national() → "CES0000000001"
|
|
ces_national("manufacturing", "employees") → "CES3000000001"
|
|
ces_national("retail", "avg_hourly_earn") → "CES4200000003"
|
|
"""
|
|
adj = "S" if seasonal else "U"
|
|
ind = _CES_SUPERSECTOR.get(industry, industry)
|
|
dtype = _CES_DATATYPE.get(data_type, data_type)
|
|
return f"CE{adj}{ind}{dtype}"
|
|
|
|
|
|
def ces_state(
|
|
state_fips: int,
|
|
industry: str = "total_nonfarm",
|
|
data_type: str = "employees",
|
|
seasonal: bool = True,
|
|
area_code: str = "00000",
|
|
) -> str:
|
|
"""
|
|
State or metro employment series (SM prefix).
|
|
|
|
Args:
|
|
state_fips: 2-digit state FIPS
|
|
industry: Supersector name or 8-digit code
|
|
data_type: Measure name or 2-digit code
|
|
seasonal: True for SA
|
|
area_code: 5-digit metro area code; "00000" = statewide
|
|
|
|
Examples:
|
|
ces_state(11) → "SMS110000000000001" (DC total nonfarm)
|
|
ces_state(24, "retail") → "SMS240000042000001" (MD retail)
|
|
"""
|
|
adj = "S" if seasonal else "U"
|
|
ind = _CES_SUPERSECTOR.get(industry, industry)
|
|
dtype = _CES_DATATYPE.get(data_type, data_type)
|
|
return f"SM{adj}{state_fips:02d}{area_code}{ind}{dtype}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CPI — Consumer Price Index
|
|
# Series format: CU[adj][periodicity][area 4 chars][item 6 chars]
|
|
# ---------------------------------------------------------------------------
|
|
_CPI_ITEM = {
|
|
"all_items": "SA0",
|
|
"core": "SA0L1E", # all items less food & energy
|
|
"food": "SAF",
|
|
"food_at_home": "SAF1",
|
|
"energy": "SA0E",
|
|
"shelter": "SAH1",
|
|
"apparel": "SAA",
|
|
"transportation":"SAT",
|
|
"medical": "SAM",
|
|
"education": "SAE",
|
|
"recreation": "SAR",
|
|
"gasoline": "SS47014",
|
|
"electricity": "SEHF01",
|
|
"new_vehicles": "SETA01",
|
|
}
|
|
|
|
def cpi(
|
|
item: str = "all_items",
|
|
area: str = "0000",
|
|
seasonal: bool = False,
|
|
series: str = "U",
|
|
) -> str:
|
|
"""
|
|
CPI series ID.
|
|
|
|
Args:
|
|
item: Item code name (see _CPI_ITEM) or raw item code
|
|
area: 4-char area code ("0000" = US city average)
|
|
seasonal: True for seasonally adjusted
|
|
series: "U" = CPI-U, "W" = CPI-W, "S" = Chained CPI-U
|
|
|
|
Examples:
|
|
cpi() → "CUUR0000SA0"
|
|
cpi("core", seasonal=True) → "CUSR0000SA0L1E"
|
|
cpi("gasoline", seasonal=True) → "CUSR0000SS47014"
|
|
cpi("all_items", series="W") → "CWUR0000SA0"
|
|
"""
|
|
prefix_map = {"U": "CU", "W": "CW", "S": "SU"}
|
|
prefix = prefix_map.get(series, "CU")
|
|
adj = "S" if seasonal else "U"
|
|
itm = _CPI_ITEM.get(item, item)
|
|
return f"{prefix}{adj}R{area}{itm}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PPI — Producer Price Index (commodity-based)
|
|
# ---------------------------------------------------------------------------
|
|
def ppi_commodity(commodity_code: str = "00000000") -> str:
|
|
"""
|
|
PPI commodity series.
|
|
|
|
Args:
|
|
commodity_code: 8-char commodity code ("00000000" = all commodities)
|
|
|
|
Examples:
|
|
ppi_commodity() → "WPU00000000"
|
|
ppi_commodity("1012") → "WPU1012" (iron & steel scrap)
|
|
"""
|
|
return f"WPU{commodity_code}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OES — Occupational Employment & Wage Statistics
|
|
# ---------------------------------------------------------------------------
|
|
_OES_DATATYPE = {
|
|
"employment": "01",
|
|
"hourly_mean": "03",
|
|
"annual_mean": "04",
|
|
"hourly_10pct": "06",
|
|
"hourly_25pct": "07",
|
|
"hourly_median":"08",
|
|
"hourly_75pct": "09",
|
|
"hourly_90pct": "10",
|
|
"annual_median":"13",
|
|
}
|
|
|
|
def oes_national(
|
|
occupation_code: str = "000000",
|
|
industry_code: str = "000000",
|
|
data_type: str = "employment",
|
|
) -> str:
|
|
"""
|
|
OES national series.
|
|
|
|
Args:
|
|
occupation_code: 6-digit SOC code or "000000" for all occupations
|
|
industry_code: 6-digit NAICS or "000000" for cross-industry
|
|
data_type: Measure name (see _OES_DATATYPE) or 2-digit code
|
|
|
|
Examples:
|
|
oes_national() → all occupations, employment
|
|
oes_national("151252", data_type="annual_median") → software devs median wage
|
|
"""
|
|
dtype = _OES_DATATYPE.get(data_type, data_type)
|
|
# 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}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# JOLTS — Job Openings & Labor Turnover
|
|
# ---------------------------------------------------------------------------
|
|
_JOLTS_ELEMENT = {
|
|
"job_openings": "JO",
|
|
"hires": "HI",
|
|
"quits": "QU",
|
|
"layoffs": "LD",
|
|
"total_separations": "TS",
|
|
}
|
|
|
|
def jolts(
|
|
element: str = "job_openings",
|
|
rate_level: str = "L",
|
|
industry: str = "000000",
|
|
ownership: str = "00",
|
|
seasonal: bool = True,
|
|
) -> str:
|
|
"""
|
|
JOLTS series.
|
|
|
|
Args:
|
|
element: "job_openings" | "hires" | "quits" | "layoffs" | "total_separations"
|
|
rate_level: "L" = level (thousands) | "R" = rate
|
|
industry: 6-digit industry code; "000000" = total nonfarm
|
|
ownership: "00" = total | "10" = private | "20" = government
|
|
seasonal: True for SA
|
|
|
|
Examples:
|
|
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) + 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'*9}{elem}{rate_level}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ECI — Employment Cost Index
|
|
# ---------------------------------------------------------------------------
|
|
def eci(
|
|
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, 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}{owner}{component}{'0'*9}{estimate}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Productivity (Major Sector)
|
|
# ---------------------------------------------------------------------------
|
|
_PR_SECTOR = {
|
|
"business": "8400",
|
|
"nonfarm_business": "8500",
|
|
"manufacturing": "3000",
|
|
"durable_mfg": "3100",
|
|
"nondurable_mfg": "3200",
|
|
}
|
|
|
|
_PR_MEASURE = {
|
|
"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 = "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" (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}{mea}"
|