Detecting Inflationary Regimes via Producer–Consumer Price Divergence

A reproducible statistical specification based on monthly US price indices (2005–present).

Abstract

This paper formalises a rule-based indicator of inflationary pressure that exploits divergence between the Producer Price Index (PPI) and the Consumer Price Index (CPI). Year-on-year rates are transformed into a differential series and standardised using a rolling median and median absolute deviation (MAD) to obtain a robust z-score. Thresholds applied to the robustised differential, conditional on the absolute level of CPI, yield categorical regimes—HOT, NEUTRAL, and COOL. The specification is designed for operational use in macro monitoring and portfolio overlays.

1. Data

Observations are merged on Date using an outer join, restricted to 2005‑01‑01 onwards to focus on the modern inflation regime. Where higher-frequency irregularities occur, the series are conformed to month-end by last observation carried forward within the month.

2. Methodology

  1. Convert Date to timezone-naïve monthly timestamps; sort ascending.
  2. Compute year-on-year inflation rates:
    CPIYoY,t = 100 × (CPIt − CPIt−12)/CPIt−12
    PPIYoY,t = 100 × (PPIt − PPIt−12)/PPIt−12
  3. Calculate divergence and robust z‑score:
    Dt = PPIYoY,t − CPIYoY,t
    zt = (Dt − Median(Dt−w+1:t)) / (1.4826 × MAD(Dt−w+1:t))
  4. Apply classification rules:
    • HOT: zt ≥ 1.0 or (zt ≥ 0.75 and CPIYoY,t ≥ 3.0%)
    • COOL: zt ≤ −1.0 or (zt ≤ −0.5 and CPIYoY,t ≤ 1.5%)
    • NEUTRAL: otherwise

The robust standardisation uses a 60‑month rolling window to balance responsiveness and stability. Windows with MAD = 0 are excluded from classification.

3. Sensitivity and Limitations

4. Implementation Notes

def _robust_zscore(s, window=60):
    s = s.astype(float)
    med = s.rolling(window, min_periods=1).median()
    mad = (s - med).abs().rolling(window, min_periods=1).median()
    z = (s - med) / (mad.replace(0, float('nan')) * 1.4826)
    return z

monthly['CPI_YoY'] = monthly['CPIAUCSL_Value'].pct_change(12) * 100
monthly['PPI_YoY'] = monthly['PPIACO_Value'].pct_change(12) * 100
monthly['PPI_minus_CPI_YoY'] = monthly['PPI_YoY'] - monthly['CPI_YoY']
monthly['divergence_z'] = _robust_zscore(monthly['PPI_minus_CPI_YoY'], window=60)

# Classification
def map_inflation_signal(row):
    if pd.isna(row['CPI_YoY']) or pd.isna(row['PPI_YoY']) or pd.isna(row['divergence_z']):
        return 'NEUTRAL'
    if (row['CPI_YoY'] >= 3.0 and row['divergence_z'] >= 0.75) or row['divergence_z'] >= 1.0:
        return 'HOT'
    if (row['CPI_YoY'] <= 1.5 and row['divergence_z'] <= -0.5) or row['divergence_z'] <= -1.0:
        return 'COOL'
    return 'NEUTRAL'

5. Reproducibility

6. Applications

This indicator can be integrated into macroeconomic dashboards, inflation‑linked asset overlays, and multi‑factor allocation models. Combining it with recession overlays and survey price expectations enhances signal validation.