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:
294
README.md
294
README.md
@ -1,16 +1,117 @@
|
|||||||
# BLS Data Reference
|
# BLS Data Library
|
||||||
|
|
||||||
Bureau of Labor Statistics — API access, dataset catalog, series ID formats, and field schemas.
|
A small, dependency-light Python library for pulling U.S. Bureau of Labor Statistics
|
||||||
|
(BLS) time series — unemployment, payrolls, inflation, wages, job openings, and
|
||||||
|
productivity — with pre-built series-ID helpers so you never have to hand-encode a
|
||||||
|
series ID.
|
||||||
|
|
||||||
## Quick Start
|
```python
|
||||||
|
from bls_client import BLSClient
|
||||||
|
from bls_client.queries import employment, prices
|
||||||
|
|
||||||
1. Register for an API key: https://data.bls.gov/registrationEngine/
|
client = BLSClient(API_KEY)
|
||||||
2. Base API endpoint: `https://api.bls.gov/publicAPI/v2/timeseries/data/`
|
|
||||||
3. Content-Type: `application/json` (POST)
|
# Latest national nonfarm payrolls
|
||||||
|
client.fetch_latest(employment.nonfarm_payrolls(), years=1)
|
||||||
|
|
||||||
|
# A labeled CPI dashboard in one call
|
||||||
|
client.fetch_named(prices.cpi_dashboard(), 2024, 2025)
|
||||||
|
```
|
||||||
|
|
||||||
|
All 82 zero-argument query helpers are verified against the live API.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API Versions
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt # just `requests`
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configure your API key
|
||||||
|
|
||||||
|
Register for a free key (instant, no approval): https://data.bls.gov/registrationEngine/
|
||||||
|
The free v2 key allows 500 queries/day, 50 series/query, 20 years/query.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp config.example.py config.py
|
||||||
|
export BLS_API_KEY="your-key" # preferred — config.py reads this env var
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.py` is gitignored; the env var takes precedence over anything written in the file.
|
||||||
|
You can also pass the key directly: `BLSClient("your-key")`.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 examples/basic_pull.py # payrolls, CPI dashboard, DC-region unemployment
|
||||||
|
python3 examples/custom_series.py # building custom series IDs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's in the box
|
||||||
|
|
||||||
|
```
|
||||||
|
bls_client/
|
||||||
|
├── client.py BLSClient — batching, named fetches, catalog metadata, row flattening
|
||||||
|
├── series.py low-level series-ID builders (LAUS, CES, CPI, PPI, OES, JOLTS, ECI, QCEW, productivity)
|
||||||
|
└── queries/
|
||||||
|
├── employment.py payrolls, unemployment (LAUS), JOLTS, QCEW
|
||||||
|
├── prices.py CPI, PPI, average prices, import/export prices
|
||||||
|
├── wages.py OES occupational wages, ECI, ECEC
|
||||||
|
└── productivity.py major-sector productivity & costs
|
||||||
|
```
|
||||||
|
|
||||||
|
`BLSClient` highlights:
|
||||||
|
- `fetch(ids, start, end)` — auto-batches >50 series, returns `{series_id: {data, catalog}}`
|
||||||
|
- `fetch_latest(ids, years=N)` — most recent N years
|
||||||
|
- `fetch_named({label: id})` — returns results keyed by your labels
|
||||||
|
- `latest_obs(series)` / `to_rows(results)` — convenience for the most recent value / CSV-ready rows
|
||||||
|
|
||||||
|
See **[USAGE.md](USAGE.md)** for the full API and **[series_id_formats.md](series_id_formats.md)**
|
||||||
|
for the series-ID decode tables.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
| Survey | Helpers | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| LAUS — local area unemployment | state / metro / county rates, DC-region dashboard | ✅ |
|
||||||
|
| CES — payroll employment | national by supersector; state/metro | ✅ |
|
||||||
|
| CPS — household survey | national unemployment rate, participation | ✅ |
|
||||||
|
| JOLTS — job openings & turnover | openings/hires/quits/layoffs, dashboard | ✅ |
|
||||||
|
| CPI — consumer prices | all-items, core, food, energy, gasoline, …; dashboard | ✅ |
|
||||||
|
| PPI — producer prices | all commodities, final demand, food, energy | ✅ |
|
||||||
|
| OES — occupational wages | employment + wage percentiles by SOC; 15 common occupations | ✅ |
|
||||||
|
| ECI — employment cost index | total comp / wages / benefits × civilian/private/gov | ✅ |
|
||||||
|
| ECEC — employer cost levels | total compensation, total benefits | ✅ (totals only) |
|
||||||
|
| Productivity & costs | output/hr, ULC, comp, hours × business/nonfarm/manufacturing | ✅ |
|
||||||
|
| QCEW — quarterly census | national & state private totals | ⚠️ totals only via this API |
|
||||||
|
|
||||||
|
## Known limitations / what "complete" would add
|
||||||
|
|
||||||
|
- **QCEW** is only partially served by the BLS *timeseries* API used here (national and
|
||||||
|
state-level totals work). County- and industry-level QCEW detail requires the separate
|
||||||
|
**QCEW Open Data API** (CSV: `https://data.bls.gov/cew/data/api/...`). Not yet wired up.
|
||||||
|
- **ECEC benefit subcomponents** (health insurance, retirement & savings, etc.) need
|
||||||
|
specific benefit-subcell codes from the ECEC component list; only the compensation and
|
||||||
|
benefits totals are currently exposed.
|
||||||
|
- **No caching / rate-limit handling.** Repeated runs spend against the 500/day quota; a
|
||||||
|
small on-disk cache and a friendly error on `REQUEST_NOT_PROCESSED` (quota hit) would help.
|
||||||
|
- **No packaging.** Importable in-tree but not `pip install`-able; add a `pyproject.toml`
|
||||||
|
to ship it as a real package.
|
||||||
|
- **No automated tests.** A pytest suite asserting builder outputs against known-good IDs
|
||||||
|
(and a live smoke test behind a marker) would lock the series-ID encodings in place.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: BLS API reference
|
||||||
|
|
||||||
|
Reference material for working directly with the API (the library wraps all of this).
|
||||||
|
|
||||||
|
### API versions
|
||||||
|
|
||||||
| Feature | v1 (no key) | v2 (registered) |
|
| Feature | v1 (no key) | v2 (registered) |
|
||||||
|---------------------------|-------------|-----------------|
|
|---------------------------|-------------|-----------------|
|
||||||
@ -21,174 +122,45 @@ Bureau of Labor Statistics — API access, dataset catalog, series ID formats, a
|
|||||||
| Series descriptions | No | Yes |
|
| Series descriptions | No | Yes |
|
||||||
| Calculations | No | Yes |
|
| Calculations | No | Yes |
|
||||||
|
|
||||||
---
|
### Endpoints
|
||||||
|
|
||||||
## Registration
|
|
||||||
|
|
||||||
- URL: https://data.bls.gov/registrationEngine/
|
|
||||||
- Free, no approval needed — instant key via email
|
|
||||||
- Key goes in the JSON payload as `"registrationkey": "YOUR_KEY"`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
### GET: Survey List
|
|
||||||
```
|
```
|
||||||
GET https://api.bls.gov/publicAPI/v2/surveys
|
GET /v2/surveys # all survey codes and names (see surveys.json)
|
||||||
GET https://api.bls.gov/publicAPI/v2/surveys/{survey_abbreviation}
|
GET /v2/surveys/{abbr} # one survey
|
||||||
```
|
GET /v2/timeseries/popular?survey={abbr} # 25 most-requested series IDs for a survey
|
||||||
Returns all survey codes and names. See `surveys.json` for full list.
|
POST /v2/timeseries/data/ # the time-series data endpoint
|
||||||
|
|
||||||
### GET: Popular Series
|
|
||||||
```
|
|
||||||
GET https://api.bls.gov/publicAPI/v2/timeseries/popular
|
|
||||||
GET https://api.bls.gov/publicAPI/v2/timeseries/popular?survey={abbreviation}
|
|
||||||
```
|
|
||||||
Returns the 25 most-requested series IDs for a survey.
|
|
||||||
|
|
||||||
### POST: Time Series Data
|
|
||||||
```
|
|
||||||
POST https://api.bls.gov/publicAPI/v2/timeseries/data/
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Minimal request (unregistered):**
|
Base URL: `https://api.bls.gov/publicAPI/v2`
|
||||||
|
|
||||||
|
**POST body (registered, all v2 features):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"seriesid": ["LAUST110000000000003", "CES0000000001"],
|
"seriesid": ["LAUST110000000000003", "CES0000000001"],
|
||||||
"startyear": "2023",
|
"startyear": "2020", "endyear": "2025",
|
||||||
"endyear": "2025"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Full request (registered, v2 features):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"seriesid": ["LAUST110000000000003", "CES0000000001"],
|
|
||||||
"startyear": "2020",
|
|
||||||
"endyear": "2025",
|
|
||||||
"registrationkey": "YOUR_KEY",
|
"registrationkey": "YOUR_KEY",
|
||||||
"catalog": true,
|
"catalog": true, "calculations": true, "annualaverage": true
|
||||||
"calculations": true,
|
|
||||||
"annualaverage": true,
|
|
||||||
"aspects": true
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response schema:**
|
**Period codes:** monthly `M01`–`M12` (`M13` = annual avg); quarterly `Q01`–`Q04` (`Q05` = annual avg); annual `A01`.
|
||||||
```json
|
**Status codes:** `REQUEST_SUCCEEDED`, `REQUEST_FAILED`, `REQUEST_NOT_PROCESSED` (often = daily quota hit).
|
||||||
{
|
**Footnote codes:** `R` revised, `P` preliminary, `X`/`N` unavailable.
|
||||||
"status": "REQUEST_SUCCEEDED",
|
|
||||||
"responseTime": 114,
|
|
||||||
"message": [],
|
|
||||||
"Results": {
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"seriesID": "LAUST110000000000003",
|
|
||||||
"catalog": {
|
|
||||||
"series_title": "...",
|
|
||||||
"survey_name": "...",
|
|
||||||
"measure_data_type": "..."
|
|
||||||
},
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"year": "2025",
|
|
||||||
"period": "M12",
|
|
||||||
"periodName": "December",
|
|
||||||
"value": "6.4",
|
|
||||||
"footnotes": [
|
|
||||||
{ "code": "R", "text": "Data were subject to revision on April 8, 2026." }
|
|
||||||
],
|
|
||||||
"calculations": {
|
|
||||||
"net_changes": { "1": "0.1", "3": "-0.2", "6": "0.5", "12": "-0.3" },
|
|
||||||
"pct_changes": { "1": "1.6", "3": "-3.0", "6": "8.3", "12": "-4.5" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Period codes:**
|
A full real response is saved in `api_response_example.json`.
|
||||||
- Monthly: `M01`–`M12`, `M13` (annual average)
|
|
||||||
- Quarterly: `Q01`–`Q04`, `Q05` (annual average)
|
|
||||||
- Annual: `A01`
|
|
||||||
|
|
||||||
**Status codes:** `REQUEST_SUCCEEDED`, `REQUEST_FAILED`, `REQUEST_NOT_PROCESSED`
|
### Bulk flat files
|
||||||
|
|
||||||
**Error footnote codes:**
|
Base: `https://download.bls.gov/pub/time.series/` — each survey folder has
|
||||||
- `R` — Revised
|
`{prefix}.series` (master list), `{prefix}.data.*` (observations), and `{prefix}.{dimension}`
|
||||||
- `P` — Preliminary
|
decode tables (area, industry, measure). Tab-delimited; handy for bulk PostgreSQL ingest.
|
||||||
- `X` — Data unavailable (e.g., government shutdown gap)
|
Key prefixes: `la/` LAUS, `ce/` CES, `sm/` state-metro, `en/` QCEW, `oe/` OES, `jt/` JOLTS,
|
||||||
- `N` — Not available
|
`cu/` CPI-U, `wp/` PPI.
|
||||||
|
|
||||||
---
|
### Other files in this repo
|
||||||
|
|
||||||
## Python Example
|
- `series_id_formats.md` — series-ID decode tables for each survey
|
||||||
|
- `qcew_field_schema.md` — QCEW quarterly/annual CSV field layouts
|
||||||
```python
|
|
||||||
import requests
|
|
||||||
|
|
||||||
API_KEY = "YOUR_KEY"
|
|
||||||
BASE_URL = "https://api.bls.gov/publicAPI/v2/timeseries/data/"
|
|
||||||
|
|
||||||
def get_series(series_ids, start_year, end_year):
|
|
||||||
payload = {
|
|
||||||
"seriesid": series_ids,
|
|
||||||
"startyear": str(start_year),
|
|
||||||
"endyear": str(end_year),
|
|
||||||
"registrationkey": API_KEY,
|
|
||||||
"catalog": True,
|
|
||||||
"calculations": True,
|
|
||||||
"annualaverage": True,
|
|
||||||
}
|
|
||||||
r = requests.post(BASE_URL, json=payload)
|
|
||||||
r.raise_for_status()
|
|
||||||
data = r.json()
|
|
||||||
if data["status"] != "REQUEST_SUCCEEDED":
|
|
||||||
raise ValueError(f"BLS API error: {data['message']}")
|
|
||||||
return data["Results"]["series"]
|
|
||||||
|
|
||||||
# DC unemployment rate (LAUS)
|
|
||||||
series = get_series(["LAUST110000000000003"], 2020, 2025)
|
|
||||||
for obs in series[0]["data"]:
|
|
||||||
print(obs["year"], obs["periodName"], obs["value"])
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bulk Download (flat files)
|
|
||||||
|
|
||||||
Base URL: `https://download.bls.gov/pub/time.series/`
|
|
||||||
|
|
||||||
Each survey folder contains:
|
|
||||||
- `{prefix}.series` — master list of all series IDs with metadata
|
|
||||||
- `{prefix}.data.{N}.{name}` — actual observations, split by category
|
|
||||||
- `{prefix}.{dimension}` — lookup/decode tables (area, industry, measure, etc.)
|
|
||||||
|
|
||||||
Key folder prefixes:
|
|
||||||
```
|
|
||||||
la/ — LAUS (local area unemployment)
|
|
||||||
ce/ — CES national employment
|
|
||||||
sm/ — State & Metro employment (CES state)
|
|
||||||
en/ — QCEW
|
|
||||||
oe/ — OES (occupational employment)
|
|
||||||
jt/ — JOLTS
|
|
||||||
cu/ — CPI-U
|
|
||||||
wp/ — PPI commodities
|
|
||||||
```
|
|
||||||
|
|
||||||
Files are tab-delimited. Useful for bulk PostgreSQL ingest.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files in this folder
|
|
||||||
|
|
||||||
- `README.md` — this file
|
|
||||||
- `surveys.json` — complete survey list from the API
|
- `surveys.json` — complete survey list from the API
|
||||||
- `series_id_formats.md` — series ID decode tables for each key dataset
|
- `bls_dataset_explorer.py` / `.html` — browsable survey catalog
|
||||||
- `qcew_field_schema.md` — QCEW quarterly and annual CSV field layouts
|
- `dc_md_va_unemployment.py` / `generate_report.py` — example report generators
|
||||||
- `api_response_example.json` — real API response sample
|
|
||||||
|
|||||||
2
USAGE.md
2
USAGE.md
@ -196,7 +196,7 @@ graph LR
|
|||||||
| `dc_unemployment_rate()` | LAUST110000000000003 | DC, not SA |
|
| `dc_unemployment_rate()` | LAUST110000000000003 | DC, not SA |
|
||||||
| `state_unemployment(fips)` | 4 LAUS series | Rate/employed/unemployed/LF |
|
| `state_unemployment(fips)` | 4 LAUS series | Rate/employed/unemployed/LF |
|
||||||
| `dc_region_unemployment()` | 6 series | DC/MD/VA + 3 MSAs |
|
| `dc_region_unemployment()` | 6 series | DC/MD/VA + 3 MSAs |
|
||||||
| `job_openings_level()` | JTS000000000000JOL | JOLTS openings |
|
| `job_openings_level()` | JTS000000000000000JOL | JOLTS openings |
|
||||||
| `jolts_dashboard()` | 5 series | Full JOLTS flow |
|
| `jolts_dashboard()` | 5 series | Full JOLTS flow |
|
||||||
| `qcew_state(fips)` | EN… | QCEW quarterly by state |
|
| `qcew_state(fips)` | EN… | QCEW quarterly by state |
|
||||||
|
|
||||||
|
|||||||
@ -113,21 +113,27 @@ def jolts_dashboard() -> dict:
|
|||||||
# QCEW
|
# QCEW
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def qcew_national_private() -> str:
|
def qcew_national_private() -> str:
|
||||||
"""QCEW — national, private sector, all industries, quarterly."""
|
"""QCEW — national, private sector, all industries (monthly employment)."""
|
||||||
return "ENU0000010510000"
|
return "ENUUS00010510"
|
||||||
|
|
||||||
|
|
||||||
def qcew_dc_private() -> str:
|
def qcew_dc_private() -> str:
|
||||||
"""QCEW — DC, private sector, all industries."""
|
"""QCEW — DC, private sector, all industries."""
|
||||||
return "ENU1100010510000"
|
return qcew_state(11, "5")
|
||||||
|
|
||||||
|
|
||||||
def qcew_state(state_fips: int, ownership: str = "5") -> str:
|
def qcew_state(state_fips: int, ownership: str = "5") -> str:
|
||||||
"""
|
"""
|
||||||
QCEW state-level series.
|
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:
|
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
|
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")
|
return ppi_commodity("00000000")
|
||||||
|
|
||||||
|
|
||||||
def ppi_finished_goods() -> str:
|
def ppi_final_demand() -> str:
|
||||||
"""PPI — finished goods."""
|
"""PPI — final demand (successor to the discontinued 'finished goods' index)."""
|
||||||
return ppi_commodity("3")
|
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:
|
def ppi_energy() -> str:
|
||||||
@ -82,7 +86,7 @@ def ppi_food() -> str:
|
|||||||
def ppi_dashboard() -> dict:
|
def ppi_dashboard() -> dict:
|
||||||
return {
|
return {
|
||||||
"All Commodities": ppi_all_commodities(),
|
"All Commodities": ppi_all_commodities(),
|
||||||
"Finished Goods": ppi_finished_goods(),
|
"Final Demand": ppi_final_demand(),
|
||||||
"Food": ppi_food(),
|
"Food": ppi_food(),
|
||||||
"Energy": ppi_energy(),
|
"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).
|
Annual median wage for a specific occupation (national, all industries).
|
||||||
|
|
||||||
Args:
|
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)
|
"291141" for registered nurses, "119021" for construction mgrs)
|
||||||
"""
|
"""
|
||||||
return oes_national(soc_code, "000000", "annual_median")
|
return oes_national(soc_code, "000000", "annual_median")
|
||||||
@ -30,7 +30,7 @@ def occupation_employment(soc_code: str) -> str:
|
|||||||
|
|
||||||
# Common occupation codes
|
# Common occupation codes
|
||||||
SOC_CODES = {
|
SOC_CODES = {
|
||||||
"software_developers": "151132",
|
"software_developers": "151252",
|
||||||
"registered_nurses": "291141",
|
"registered_nurses": "291141",
|
||||||
"teachers_elementary": "252021",
|
"teachers_elementary": "252021",
|
||||||
"accountants": "132011",
|
"accountants": "132011",
|
||||||
@ -51,29 +51,29 @@ SOC_CODES = {
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# ECI — Employment Cost Index
|
# ECI — Employment Cost Index
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def eci_total_compensation(seasonal: bool = True) -> str:
|
def eci_total_compensation(seasonal: bool = False) -> str:
|
||||||
"""ECI — civilian workers, all industries, total compensation."""
|
"""ECI — civilian workers, all industries, total compensation (12-mo % change)."""
|
||||||
return eci("10", "10", component="A", seasonal=seasonal)
|
return eci(owner="10", component="10", seasonal=seasonal)
|
||||||
|
|
||||||
|
|
||||||
def eci_wages(seasonal: bool = True) -> str:
|
def eci_wages(seasonal: bool = False) -> str:
|
||||||
"""ECI — civilian workers, wages and salaries only."""
|
"""ECI — civilian workers, wages and salaries only (12-mo % change)."""
|
||||||
return eci("10", "10", component="W", seasonal=seasonal)
|
return eci(owner="10", component="20", seasonal=seasonal)
|
||||||
|
|
||||||
|
|
||||||
def eci_benefits(seasonal: bool = True) -> str:
|
def eci_benefits(seasonal: bool = False) -> str:
|
||||||
"""ECI — civilian workers, benefit costs only."""
|
"""ECI — civilian workers, benefit costs only (12-mo % change)."""
|
||||||
return eci("10", "10", component="B", seasonal=seasonal)
|
return eci(owner="10", component="30", seasonal=seasonal)
|
||||||
|
|
||||||
|
|
||||||
def eci_private(seasonal: bool = True) -> str:
|
def eci_private(seasonal: bool = False) -> str:
|
||||||
"""ECI — private sector, total compensation."""
|
"""ECI — private sector, total compensation (12-mo % change)."""
|
||||||
return eci("20", "10", component="A", seasonal=seasonal)
|
return eci(owner="20", component="10", seasonal=seasonal)
|
||||||
|
|
||||||
|
|
||||||
def eci_state_local(seasonal: bool = True) -> str:
|
def eci_state_local(seasonal: bool = False) -> str:
|
||||||
"""ECI — state and local government, total compensation."""
|
"""ECI — state and local government, total compensation (12-mo % change)."""
|
||||||
return eci("30", "10", component="A", seasonal=seasonal)
|
return eci(owner="30", component="10", seasonal=seasonal)
|
||||||
|
|
||||||
|
|
||||||
def eci_dashboard() -> dict:
|
def eci_dashboard() -> dict:
|
||||||
@ -90,18 +90,18 @@ def eci_dashboard() -> dict:
|
|||||||
# ECEC — Employer Costs for Employee Compensation
|
# ECEC — Employer Costs for Employee Compensation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def ecec_total_compensation() -> str:
|
def ecec_total_compensation() -> str:
|
||||||
"""ECEC — civilian workers, total compensation cost per hour."""
|
"""ECEC — civilian workers, total compensation cost per hour worked."""
|
||||||
return "CMU1010000000000D"
|
return "CMU1010000000000D"
|
||||||
|
|
||||||
|
|
||||||
def ecec_health_insurance() -> str:
|
def ecec_total_benefits() -> str:
|
||||||
"""ECEC — health insurance cost per hour worked."""
|
"""ECEC — civilian workers, total benefits cost per hour worked."""
|
||||||
return "CMU1010000000000H"
|
return "CMU1036000000000D"
|
||||||
|
|
||||||
|
# Note: ECEC benefit subcomponents (health insurance, retirement & savings, etc.)
|
||||||
def ecec_retirement() -> str:
|
# are encoded as specific benefit-subcell codes in the series ID (suffix stays "D").
|
||||||
"""ECEC — retirement & savings cost per hour worked."""
|
# Look them up against the ECEC component list before adding helpers — do not
|
||||||
return "CMU1010000000000R"
|
# fabricate them with a letter suffix (the old "...H"/"...R" forms were invalid).
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -261,11 +261,11 @@ def oes_national(
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
oes_national() → all occupations, employment
|
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)
|
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
|
# OE + U + N(area_type) + 0000000(national area, 7 chars) + industry(6) + occupation(6) + dtype(2) = 25
|
||||||
return f"OEUN0000400{industry_code:0<6}{occupation_code:0<6}{dtype}"
|
return f"OEUN0000000{industry_code:0<6}{occupation_code:0<6}{dtype}"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -297,72 +297,80 @@ def jolts(
|
|||||||
seasonal: True for SA
|
seasonal: True for SA
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
jolts() → "JTS000000000000JOL" (openings level)
|
jolts() → "JTS000000000000000JOL" (openings level)
|
||||||
jolts("quits", rate_level="R") → "JTS000000000000QUR" (quits rate)
|
jolts("quits", rate_level="R") → "JTS000000000000000QUR" (quits rate)
|
||||||
jolts("hires", seasonal=False) → "JTU000000000000HIL" (hires level, NSA)
|
jolts("hires", seasonal=False) → "JTU000000000000000HIL" (hires level, NSA)
|
||||||
"""
|
"""
|
||||||
adj = "S" if seasonal else "U"
|
adj = "S" if seasonal else "U"
|
||||||
elem = _JOLTS_ELEMENT.get(element, element)
|
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")
|
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
|
# ECI — Employment Cost Index
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def eci(
|
def eci(
|
||||||
worker_type: str = "10", # 10=civilian, 20=private, 30=state/local
|
owner: str = "10", # 10=civilian, 20=private, 30=state/local gov
|
||||||
occupation: str = "00", # 00=all, 10=mgmt/prof, 20=service, etc.
|
component: str = "10", # 10=total compensation, 20=wages & salaries, 30=benefits
|
||||||
industry: str = "000000000",
|
estimate: str = "A", # A=12-month % change, Q=3-month % change, I=index
|
||||||
component: str = "A", # A=total comp, W=wages, B=benefits
|
seasonal: bool = False, # the 12-month % change estimate is published unadjusted (CIU)
|
||||||
seasonal: bool = True,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
ECI series for employment cost changes.
|
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:
|
Examples:
|
||||||
eci() → "CIU1010000000000A" (civilian, all workers, total compensation)
|
eci() → "CIU1010000000000A" (civilian, total compensation, 12-mo % chg)
|
||||||
eci(component="W") → wages only
|
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"
|
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)
|
# Productivity (Major Sector)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_PR_SECTOR = {
|
_PR_SECTOR = {
|
||||||
"business": "85",
|
"business": "8400",
|
||||||
"nonfarm_business":"86",
|
"nonfarm_business": "8500",
|
||||||
"manufacturing": "88",
|
"manufacturing": "3000",
|
||||||
"durable_mfg": "89",
|
"durable_mfg": "3100",
|
||||||
"nondurable_mfg": "90",
|
"nondurable_mfg": "3200",
|
||||||
}
|
}
|
||||||
|
|
||||||
_PR_MEASURE = {
|
_PR_MEASURE = {
|
||||||
"output_per_hour": "092",
|
"output_per_hour": "6092",
|
||||||
"output": "041",
|
"output": "6042",
|
||||||
"hours": "051",
|
"hours": "6032",
|
||||||
"compensation": "061",
|
"compensation": "6062", # hourly compensation
|
||||||
"real_comp_per_hr": "071",
|
"real_comp_per_hr": "6152", # real hourly compensation
|
||||||
"unit_labor_cost": "111",
|
"unit_labor_cost": "6112",
|
||||||
"unit_nonlabor_pay":"112",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def productivity(
|
def productivity(
|
||||||
sector: str = "business",
|
sector: str = "nonfarm_business",
|
||||||
measure: str = "output_per_hour",
|
measure: str = "output_per_hour",
|
||||||
seasonal: bool = True,
|
seasonal: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Major Sector Productivity series.
|
Major Sector Productivity series.
|
||||||
|
|
||||||
|
Format: PR + adj + sector(4) + measure(4) = 11 chars. Published seasonally adjusted (PRS).
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
productivity() → "PRS85006092" (business output per hour, SA)
|
productivity() → "PRS85006092" (nonfarm business output per hour)
|
||||||
productivity("manufacturing", "unit_labor_cost")
|
productivity("manufacturing", "unit_labor_cost") → "PRS30006112"
|
||||||
"""
|
"""
|
||||||
adj = "S" if seasonal else "U"
|
adj = "S" if seasonal else "U"
|
||||||
sec = _PR_SECTOR.get(sector, sector)
|
sec = _PR_SECTOR.get(sector, sector)
|
||||||
mea = _PR_MEASURE.get(measure, measure)
|
mea = _PR_MEASURE.get(measure, measure)
|
||||||
return f"PR{adj}{sec}06{mea}"
|
return f"PR{adj}{sec}{mea}"
|
||||||
|
|||||||
@ -48,7 +48,7 @@ DATASETS = [
|
|||||||
|
|
||||||
("JT", "Employment & Labor Force", "active",
|
("JT", "Employment & Labor Force", "active",
|
||||||
"Job Openings & Labor Turnover (JOLTS)",
|
"Job Openings & Labor Turnover (JOLTS)",
|
||||||
"JTS000000000000JOL",
|
"JTS000000000000000JOL",
|
||||||
"Monthly job openings, hires, quits, layoffs, and total separations "
|
"Monthly job openings, hires, quits, layoffs, and total separations "
|
||||||
"by industry. Key measure of labor demand."),
|
"by industry. Key measure of labor demand."),
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ DATASETS = [
|
|||||||
# ── Wages & Compensation ────────────────────────────────────────────────
|
# ── Wages & Compensation ────────────────────────────────────────────────
|
||||||
("OE", "Wages & Compensation", "active",
|
("OE", "Wages & Compensation", "active",
|
||||||
"Occupational Employment & Wage Statistics (OEWS)",
|
"Occupational Employment & Wage Statistics (OEWS)",
|
||||||
"OEUN000040000000000000001",
|
"OEUN000000000000000000001",
|
||||||
"Annual employment and wage estimates for ~800 occupations "
|
"Annual employment and wage estimates for ~800 occupations "
|
||||||
"at national, state, and metro levels."),
|
"at national, state, and metro levels."),
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,14 @@
|
|||||||
# 2. Copy this file to config.py:
|
# 2. Copy this file to config.py:
|
||||||
# cp config.example.py config.py
|
# cp config.example.py config.py
|
||||||
#
|
#
|
||||||
# 3. Paste your key below and save.
|
# 3. Provide your key one of two ways:
|
||||||
|
# a) Preferred — set an environment variable (nothing to edit here):
|
||||||
|
# export BLS_API_KEY="your-key"
|
||||||
|
# b) Or replace YOUR_KEY_HERE below with your key.
|
||||||
|
#
|
||||||
|
# config.py is gitignored, so a key placed here is never committed.
|
||||||
|
|
||||||
BLS_API_KEY = "YOUR_KEY_HERE"
|
import os
|
||||||
|
|
||||||
|
BLS_API_KEY = os.environ.get("BLS_API_KEY", "YOUR_KEY_HERE")
|
||||||
BLS_API_BASE = "https://api.bls.gov/publicAPI/v2"
|
BLS_API_BASE = "https://api.bls.gov/publicAPI/v2"
|
||||||
|
|||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
requests>=2.28
|
||||||
@ -191,33 +191,36 @@ ENU0000010510000 → National, quarterly, private, all industries
|
|||||||
| 14 | Annual 75th percentile wage |
|
| 14 | Annual 75th percentile wage |
|
||||||
| 15 | Annual 90th percentile wage |
|
| 15 | Annual 90th percentile wage |
|
||||||
|
|
||||||
**Examples:**
|
**Examples** (national area type is `N`, national area code is `0000000`):
|
||||||
```
|
```
|
||||||
OEUM0000400000000000001 → National, all industries, all occupations, employment
|
OEUN000000000000000000001 → National, all industries, all occupations, employment
|
||||||
OEUM0000400000015113201 → National, all industries, software devs, employment
|
OEUN000000000000000000004 → National, all industries, all occupations, annual mean wage
|
||||||
|
OEUN000000000000015125213 → National, all industries, software devs (15-1252), annual median wage
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## JOLTS — Job Openings & Labor Turnover (prefix: JT)
|
## JOLTS — Job Openings & Labor Turnover (prefix: JT)
|
||||||
|
|
||||||
**Series ID:** `JT[S/U][IIIIII][E/J][RR][LL]`
|
**Series ID:** `JT[S/U][IIIIII][SS][AAAAA][ZZ][EE][L/R]` — 21 chars
|
||||||
|
|
||||||
| Pos | Length | Field | Notes |
|
| Pos | Length | Field | Notes |
|
||||||
|-----|--------|-------|-------|
|
|-----|--------|-------|-------|
|
||||||
| 1-2 | 2 | Prefix | `JT` |
|
| 1-2 | 2 | Prefix | `JT` |
|
||||||
| 3 | 1 | Seasonal adj | `S` or `U` |
|
| 3 | 1 | Seasonal adj | `S` or `U` |
|
||||||
| 4-9 | 6 | Industry code | NAICS supersector |
|
| 4-9 | 6 | Industry code | NAICS supersector; `000000` = total nonfarm |
|
||||||
| 10 | 1 | Job status | `E`=total; `J`=job openings; `H`=hires; `L`=layoffs/discharges; `Q`=quits; `T`=total separations |
|
| 10-11 | 2 | State code | `00` = national |
|
||||||
| 11-12 | 2 | Rate/level | `R`=rate; `L`=level (thousands) |
|
| 12-16 | 5 | Area code | `00000` = all areas |
|
||||||
| 13-14 | 2 | Ownership | `00`=total; `10`=private; `20`=government |
|
| 17-18 | 2 | Size class | `00` = all sizes |
|
||||||
|
| 19-20 | 2 | Data element | `JO`=job openings; `HI`=hires; `QU`=quits; `LD`=layoffs/discharges; `TS`=total separations; `OS`=other separations |
|
||||||
|
| 21 | 1 | Rate/level | `L`=level (thousands); `R`=rate |
|
||||||
|
|
||||||
**Examples:**
|
**Examples** (15 zeros sit between the seasonal code and the 2-char element):
|
||||||
```
|
```
|
||||||
JTS000000000000JOL → Total nonfarm, job openings, level
|
JTS000000000000000JOL → Total nonfarm, job openings, level
|
||||||
JTS000000000000JOR → Total nonfarm, job openings, rate
|
JTS000000000000000JOR → Total nonfarm, job openings, rate
|
||||||
JTS000000000000HIL → Total nonfarm, hires, level
|
JTS000000000000000HIL → Total nonfarm, hires, level
|
||||||
JTS000000000000TSL → Total nonfarm, total separations, level
|
JTS000000000000000TSL → Total nonfarm, total separations, level
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user