Time-series normalization v1.0
What it does
Three columns in pricing_aggregates carry time-adjusted and metro-adjusted versions of the nominal median.
cpi_normalized_median deflates the raw permit median to current-quarter dollars using BLS CPI-U. It answers the question: if the work were done today, what would the median cost be in today's general-price-level terms?
material_cost_indexed_median deflates using BLS PPI for the single dominant commodity per trade. It captures commodity-specific shocks (copper has run up roughly 85 percent, asphalt shingles roughly 52 percent over 2017 to 2026) that general inflation does not.
labor_rate_msa_indexed_median adjusts the raw median to a national-average wage market using BLS OES per-MSA hourly wages. It answers: what would the same project cost in a metro paying the national-median wage for this trade? This is a cross-metro level adjustment, not time deflation.
All three fields are returned at the Scale tier ($4,999 a month) and above. Sandbox, Starter, and Growth tiers receive null for these fields by design.
Formulas
For each pricing_aggregates row with a non-null date_range_start and date_range_end:
midpoint = month-truncated middle of date_range_start and date_range_end
cpi_normalized_median = median_price * (latest_cpi / cpi_at_midpoint)
material_cost_indexed_median = median_price * (latest_ppi / ppi_at_midpoint)
labor_rate_msa_indexed_median = median_price * (national_median_wage / msa_wage)
latest is the most recent year_month in each reference table. Values recompute monthly when BLS publishes new data. If the midpoint falls before the earliest available reference point, the earliest value is used and the calculation is still performed.
Trade-to-commodity mapping
Single dominant commodity per trade. The mapping is auditable in 30 seconds against the trade's typical bill of materials. V2 may add per-trade weighted blends if customers request deeper material decomposition.
| Trade | Commodity | BLS PPI series | Rationale | |---|---|---|---| | HVAC | copper | WPU1022 | Compressors, condenser coils, refrigerant line sets | | Roofing | asphalt shingle | WPU0571 | Primary roofing material on residential projects | | Electrical | copper | WPU1022 | Wiring is the dominant material spend | | Plumbing | copper | WPU1022 | Pipe is the dominant material spend | | Foundation | concrete | WPU133 | Primary structural material | | Solar | steel | WPU101 | Mounting and racking; panels are commoditized globally, so local cost is dominated by steel and labor |
What this does not do
No time deflation on labor. BLS OES is annual cadence and the public API returns only the latest one or two reference periods. The labor adjustment is therefore cross-metro level, not historical wage growth. Customers needing historical wage time series should source ECI (Employment Cost Index) separately or request a Custom tier engagement.
No state-fallback labor adjustment. Aggregates with aggregate_scope = 'state' (state-fallback rows where city-specific permit volume is below the 30-permit gate) leave labor_rate_msa_indexed_median NULL. There is no MSA wage to compare for an entire state.
No per-trade weighted commodity blends. V1 uses one commodity per trade. Customers who need finer material decomposition (for example, HVAC as 60 percent copper plus 25 percent steel plus 15 percent other) can request a Custom tier engagement.
No proprietary normalization. Every formula above is reproducible from public BLS data and the row's own date_range_start, date_range_end, median_price, metro_id, and trade_id.
BLS source citations
| Source | Series ID | Cadence | History available (registered tier) |
|---|---|---|---|
| CPI-U all items | CUUR0000SA0 | Monthly | 20 years |
| PPI lumber | WPU081 | Monthly | 20 years |
| PPI steel | WPU101 | Monthly | 20 years |
| PPI copper | WPU1022 | Monthly | 20 years |
| PPI asphalt shingle | WPU0571 | Monthly | 20 years |
| PPI concrete | WPU133 | Monthly | 20 years |
| OES per-MSA hourly median wage | OEUM<MSA><000000><SOC>03 | Annual | 1 to 2 reference periods (BLS API limit) |
Recompute cadence
Monthly. The GitHub Actions workflow pipeline-bls-monthly.yml runs on the 5th of each month at 08:00 UTC. The normalize step depends on the three BLS ingest jobs completing successfully. If any upstream BLS job fails, normalize does not run and the prior month's normalized values remain in place.
Methodology version
hq_methodology_v1.0_2026. Stamped on every pricing_aggregates row. A version bump requires a new sub-document covering the formula change and a 30-day reviewer comment window.
Worked example
Dallas HVAC city aggregate, raw median $5,838.50 from permits filed 1978-11 through 2023-11. The midpoint anchors at 2001-05. CPI at 2001-05 was approximately 177; the latest CPI is 333. Copper PPI at 2001-05 was around 130; the latest is 551. The Dallas HVAC median wage is $28.50 an hour, and the national HVAC median is $31.60.
cpi_normalized_median = 5838.50 * (333.020 / 177.5) ~ 10,953
material_cost_indexed_median = 5838.50 * (551.077 / 130) ~ 24,750
labor_rate_msa_indexed_median = 5838.50 * (31.60 / 28.50) ~ 6,473
The CPI value is what you would have paid for the median Dallas HVAC permit today in general-inflation-adjusted terms. The material value layers copper-specific inflation, which has moved far harder than CPI. The labor value is what the same project would cost in a national-average wage market; Dallas labor is below the national median, so the adjustment lifts the value modestly.
Actual production values may differ slightly from this worked example because the midpoint anchor is recomputed on each refresh and the latest BLS index values shift each month.