University of Michigan Consumer Sentiment (UMCSENT)

A reproducible, monthly demand signal from FRED: UMCSENT. We align to month‑end, compute YoY and 3‑month momentum, apply a robust z‑score to the level to capture relative optimism/pessimism, and classify regimes. No plotting is required.

FRED UMCSENT Momentum Robust Z (Level) Regime Classification

Abstract

We transform the University of Michigan Consumer Sentiment index into a monthly, classification‑ready signal. After resampling to month‑end and forward‑filling, we derive YoY and 3‑month percent changes to capture momentum. We also standardise the level of sentiment with a robust rolling z‑score (median & MAD) that adapts to shorter histories. The final regime (Bullish, Neutral, Bearish) is determined by combining level context (z) with momentum (YoY/3M).

1) Data (FRED Identifier)

We standardise to a month‑end frequency ("M") and .ffill() within month to avoid gaps around release dates.

# Fetch & align (fredapi)
umich = fred.get_series("UMCSENT")
df_sent = pd.DataFrame(umich, columns=["UMCSENT"])
df_sent = df_sent.resample("M").last().ffill()
print("✅ UMCSENT retrieved."); print(df_sent.tail())

2) Feature Engineering

We compute long‑ and short‑horizon momentum on the level \(X_t\):

\( \text{YoY\_pct}_t = 100\times\left(\dfrac{X_t}{X_{t-12}}-1\right) \)
\( \text{3M\_pct}_t = 100\times\left(\dfrac{X_t}{X_{t-3}}-1\right) \)
df_sent["YoY_pct"] = df_sent["UMCSENT"].pct_change(12) * 100
df_sent["3M_pct"]  = df_sent["UMCSENT"].pct_change(3)  * 100

3) Robust Z‑Score on Level

To contextualise the absolute level versus its own history, we compute a robust z‑score with an adaptive window (default 120 months, minimum 36). The median & MAD provide resilience to outliers. If history is shorter than the target window, we scale to at least 36 months or 80% of available observations.

\( z_t = \dfrac{X_t - \text{median}_W(X)}{1.4826\,\text{MAD}_W(X)} \)

Window choice: \(W = \max\{36, \min(120, \lfloor 0.8\,N \rfloor)\}\) where \(N\) is the number of non‑missing observations.

def robust_z(series, window=120, min_window=36):
    s = series.astype(float)
    w = max(min_window, window if s.notna().sum() >= window else int(max(min_window, s.notna().sum()*0.8)))
    med = s.rolling(w, min_periods=min_window).median()
    mad = (s - med).abs().rolling(w, min_periods=min_window).median()
    return (s - med) / (1.4826 * mad.replace(0, np.nan))

df_sent["Level_z"] = robust_z(df_sent["UMCSENT"], window=120, min_window=36)

4) Regime Classification

We combine level context (z) with momentum (YoY/3M) to form regimes:

  • Bullish (demand tailwind): Level_z > +0.5 and (YoY_pct > 0 or 3M_pct > +1)
  • Bearish (demand headwind): Level_z < −0.5 and (YoY_pct < 0 or 3M_pct < −1)
  • Neutral: otherwise (including any NaNs)
def classify_umich(row, z_hi=0.5, z_lo=-0.5, yoy_up=0.0, yoy_dn=0.0, m3_up=1.0, m3_dn=-1.0):
    z, yoy, m3 = row["Level_z"], row["YoY_pct"], row["3M_pct"]
    if np.isnan(z) or np.isnan(yoy) or np.isnan(m3):
        return "Neutral"
    if (z > z_hi) and ((yoy > yoy_up) or (m3 > m3_up)):
        return "Bullish"
    if (z < z_lo) and ((yoy < yoy_dn) or (m3 < m3_dn)):
        return "Bearish"
    return "Neutral"

df_sent["UMCSENT_Signal"] = df_sent.apply(classify_umich, axis=1)

Thresholds are tunable for your downstream use (e.g., sensitivity to inflections).

5) Outputs & Columns

# Core columns
["UMCSENT","YoY_pct","3M_pct","Level_z","UMCSENT_Signal"]

Console prints echo the tail of the aligned series and the final signal columns for a quick sanity check. No plots are generated by design.

6) Data Handling & Validation

7) Interpretation & Use

Higher consumer sentiment is historically associated with stronger consumption growth. Bullish regimes suggest a supportive demand backdrop; Bearish regimes warn of headwinds. Use this alongside labor, inflation, and financial‑conditions signals to triangulate macro regime.