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.

FRED JOLTS Robust Z Regime Classification

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)

Both are monthly. We coerce to a common monthly index at Month‑Start ("MS") and forward‑fill within month to handle publication alignment.

Data source overview (what each series measures)

  • JTSQUR (Quits rate) — the share of employed workers who voluntarily separate from their job in a given month. Often interpreted as a revealed-preference measure of worker confidence and outside options (job-to-job transitions).
  • JTSLDR (Layoffs & discharges rate) — the share of employed workers whose separations are employer-initiated (layoffs or discharges) in a given month. Often interpreted as a measure of firm-level retrenchment or demand weakness.

Release cadence & lag: JOLTS is released monthly with roughly a one‑month lag (e.g., January data released in early March). This means the signal is not a high-frequency “nowcast” input; it is best used as a slow-moving confirmation signal within the labor pillar.

Revisions & benchmarking: JOLTS estimates are revised as part of the BLS annual revision process (including seasonal-factor updates and benchmarking). Recent years of data can change materially, so historical backtests should treat the series as real-time noisy and revision-prone rather than fixed truth.

Optional auxiliary series (recommended for v1.1+ extensions):

  • JTSJOL (Job openings level) and UNEMPLOY (Unemployment level) — can be combined into an unemployed-per-opening ratio (UNEMPLOY / JTSJOL) as a direct slack / tightness proxy.
  • JTSHIR (Hires rate) — can be used to separate worker confidence (quits) from employer hiring appetite (hires), and to compute alternative turnover composites.
# 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:

\( \text{Turnover\_Diff}^{pp}_t = \text{QuitsRate}_t - \text{LayoffRate}_t \)

We then compute a robust z‑score using a rolling window with median and MAD, guarding against outliers and short histories.

\( z_t = \dfrac{x_t - \text{median}_{W}(x)}{1.4826\,\text{MAD}_{W}(x)} \)

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)

Methodology amendment (v1.1)

  • Data latency: treat the signal as a lagged labor-tightness confirmation input; do not interpret short-term shifts as real-time turning points.
  • Benchmarking revisions: maintain a small revision-aware “transition band” (or require sustained confirmation) to reduce false regime flips caused by later revisions.
  • Emphasize quits: optionally up-weight quits relative to layoffs in the level composite to reflect that quits often carry more information about worker outside options than layoffs do about demand at the margin: \( x^{pp}_t = lpha\, ext{QuitsRate}_t - ext{LayoffRate}_t \) with α > 1 (e.g., 1.25–1.50) tested via backtest.
  • Add tightness ratio: incorporate unemployed per job opening (UNEMPLOY / JTSJOL). High values indicate slack; falling values indicate tightening. Use as a modifier or confirmation filter.
  • Momentum via moving z-score: compute a rolling z-score on the 3–6 month change in the composite (and/or on the tightness ratio) to better capture turning momentum without over-reacting to one-month noise.

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]
If level < −0.20 or z < −0.50 → Bearish
Else if level > 0.50 and z > 0.50 → Bullish
Else → Neutral
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

6. Model Interpretation (how to read the signal)

What the level means

  • Turnover_Diff_pp > 0: quits exceed layoffs, consistent with a “tight” labor market where workers have outside options and firms are not broadly shedding labor.
  • Turnover_Diff_pp near 0: tighter and looser forces are balanced; interpret as “late‑cycle” or “transitional” unless other labor indicators confirm.
  • Turnover_Diff_pp < 0: layoffs exceed quits, consistent with a loosening labor market (often an early warning for rising unemployment risk).

What the robust z-score means

The robust z-score measures how unusual the current level is relative to its recent history (rolling median/MAD). It is best interpreted as extremeness, not “causality”:

  • z > +0.5: tighter-than-normal vs history (worker confidence unusually strong and/or layoffs unusually low).
  • z between −0.5 and +0.5: typical / noisy range; avoid over-interpreting.
  • z < −0.5: looser-than-normal vs history (quits weak and/or layoffs elevated).

How to use it inside a macro model

  • As a labor-tightness confirmation: treat “Bullish” as confirming labor strength (supportive for growth/consumption, wage pressure risk); treat “Bearish” as confirming deterioration (higher recession risk).
  • Combine with vacancy/unemployment tightness: the unemployed-per-opening ratio (UNEMPLOY / JTSJOL) is a direct slack proxy linked to search-and-matching theory and Beveridge-curve dynamics. Use it to validate whether quits/layoffs are signaling the same tightness direction.
  • Look for sustained shifts: because JOLTS is lagged and revised, prefer sustained moves (multi‑month) and cross-confirmation rather than reacting to one print.

Academic anchors (why these variables matter)

  • Search & matching / Beveridge curve: tightness is often summarized by the vacancy–unemployment relationship; shifts can reflect changes in matching efficiency and flows.
  • Worker flows (quits/layoffs): quits and layoffs are core separations margins; they help decompose labor-market dynamics beyond the unemployment rate.
  • Tightness and wage growth: recent work emphasizes quits and vacancies-per-searcher as informative tightness measures for wage inflation.

Selected references

  • Barlevy, G. (2024). “The Shifting Reasons for Beveridge Curve Shifts.” Journal of Economic Perspectives.
  • Davis, S. J., Faberman, R. J., & Haltiwanger, J. (2011/2012). “Labor Market Flows in the Cross Section and Over Time.” (JOLTS-based worker flows).
  • Domash, A., & Summers, L. H. (2022). “How Tight are U.S. Labor Markets?” (tightness indicators and u/v measures).
  • Heise, S. et al. (NY Fed Staff Reports, 2024). “Wage Growth and Labor Market Tightness.” (quits and vacancies per effective searcher).
  • BLS JOLTS Technical Note & annual revisions notices (data definitions, benchmarking, and revision windows).

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