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.
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)
- UMCSENT: University of Michigan: Consumer Sentiment®
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\):
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.
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.5and (YoY_pct > 0or3M_pct > +1) - Bearish (demand headwind):
Level_z < −0.5and (YoY_pct < 0or3M_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
- Frequency alignment: resample to month‑end (
"M") with.last()and.ffill(). - NaN policy: initial lags from percentage change calculations are NaN; regime treats any NaN as Neutral.
- Robust scaling: level z‑scores use median/MAD; if MAD = 0, z is NaN rather than infinite.
- Reproducibility: all transformations are deterministic given the UMCSENT series.
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.