Labor Market Turnover Differential
A reproducible, monthly signal that gauges labor‑market tightness by comparing worker‑initiated quits to employer‑initiated layoffs & discharges. Built from FRED JOLTS series using robust z‑scores and clear regime rules.
Abstract
We compute the monthly Turnover Differential as the quits rate minus the layoffs & discharges rate. Intuition: a rising gap (quits ≫ layoffs) reflects worker confidence and tight conditions; a shrinking or negative gap signals loosening. We standardise the differential with a robust rolling z‑score and map it, alongside the level, into categorical regimes (Bullish, Neutral, Bearish) for downstream macro & markets use.
1. Data (FRED Identifiers)
- JTSQUR: Quits Rate (%), total nonfarm
- JTSLDR: Layoffs & Discharges Rate (%), total nonfarm
Both are monthly. We coerce to a common monthly index at Month‑Start ("MS") and forward‑fill within month to handle publication alignment.
# Fetch & align (fredapi)
q = fred.get_series("JTSQUR").to_frame("QuitsRate")
l = fred.get_series("JTSLDR").to_frame("LayoffRate")
q.index = pd.to_datetime(q.index); l.index = pd.to_datetime(l.index)
turnover_raw_df = (
pd.concat([q, l], axis=1).sort_index()
.resample("MS").last().ffill().reset_index()
.rename(columns={"index": "Date"})
)
2. Signal Construction
The level differential (percentage points) is:
We then compute a robust z‑score using a rolling window with median and MAD, guarding against outliers and short histories.
Default window \(W=60\) months with a minimum of 24. If history is shorter, the window adapts: \(W = \max(24, \lfloor0.8\cdot N\rfloor)\).
def _robust_z(s, window=60, min_window=24):
s = pd.to_numeric(s, errors="coerce").astype(float)
n = s.notna().sum()
w = max(min_window, window if n >= window else int(n * 0.8) if n else min_window)
med = s.rolling(w, min_periods=min_window).median()
mad = (s - med).abs().rolling(w, min_periods=min_window).median()
return (s - med) / (mad.replace(0, np.nan) * 1.4826)
3. Regime Classification
We combine information from the level (pp) and the robust z‑score into three regimes. Neutral “bands” prevent over‑trading on noise:
- Level neutral band (pp): [-0.20, 0.50]
- Z neutral band: [-0.50, 0.50]
def _classify(row):
lvl, z = row["Turnover_Diff_pp"], row["Turnover_Diff_z"]
if pd.isna(lvl) or pd.isna(z): return "Neutral"
if (lvl < -0.20) or (z < -0.50): return "Bearish"
if (lvl > 0.50) and (z > 0.50): return "Bullish"
return "Neutral"
Thresholds are configurable via level_neutral_band and z_neutral_band.
4. Outputs & Metadata
# Core columns
["Date","QuitsRate","LayoffRate",
"Turnover_Diff_pp","Turnover_Diff_z","Turnover_Regime"]
# Attached metadata
attrs["series"] = {"quits": "JTSQUR", "layoffs": "JTSLDR"}
attrs["generated_at"] = datetime.utcnow().isoformat() + "Z"
attrs["rules"] = {
"level_neutral_band_pp": (-0.20, 0.50),
"z_neutral_band": (-0.50, 0.50),
"definition": "Bullish if quits ≫ layoffs; Bearish if layoffs ≥ quits and/or negative momentum."
}
For reporting, the last five years can be rendered as an HTML table for dashboards or emails.
5. Data Handling & Validation
- Frequency alignment: resample to Month‑Start and forward‑fill within month to ensure synchronous comparisons.
- Short‑history safety: adaptive robust z‑window prevents unstable scaling during early sample periods.
- NaN policy: coercion to numeric; z‑scores undefined where MAD = 0 are set to NaN and naturally mapped to Neutral.
- Preview hooks: print/display last rows of the raw and final data frames for quick inspection.
6. Interpretation & Use Cases
The Turnover Differential is a concise barometer of labor‑market tightness. Bullish (quits ≫ layoffs) historically co‑moves with strong wage pressure and resilient consumption; Bearish regimes often precede labor softening and increased recession risk. Use alongside other macro pillars (growth, inflation, financial conditions) to condition asset‑allocation tilts.
7. Implementation Notes (Python)
# ➊ Fetch & harmonise monthly series (fredapi)
# ➋ Compute level differential in percentage points
# ➌ Apply robust rolling z-score with adaptive window
# ➍ Classify regimes with level & z neutral bands
# ➎ Persist metadata & (optional) HTML table for last 5y
8. Governance & Change Control
- Review thresholds and windows semi‑annually with backtests around turning points.
- Document rationale for any rule changes (e.g., neutral bands, window length).
- Version outputs and store data‑quality logs for auditability.