A yield is the annual return an investor earns on a bond. The yield curve plots those yields against the bonds’ maturities (time until repayment — e.g., 3 months, 2 years, 10 years).
Under normal conditions investors demand higher yields for longer maturities — compensation for tying up money longer. The curve slopes upward.
An inverted yield curve occurs when short-term yields exceed long-term yields — the curve slopes downward. Economists watch the 10-year minus 2-year Treasury spread (10Y–2Y) most closely. When this spread goes negative, the curve is inverted.
The intuition has three channels:
Channel 1 — Market expectations.
Long-term yields reflect what markets expect short-term rates to average
over many years. If investors expect the Fed to cut rates
(usually in response to a weakening economy), future short-term rates
will be low — pulling long yields below current short yields.
Channel 2 — Bank profitability.
Banks borrow short and lend long. Inversion squeezes that margin,
reducing incentive to make loans. Less credit → slower growth.
Channel 3 — Confidence signal.
Inversion signals that many smart-money bond investors collectively
expect economic trouble ahead.
We’ll work with simulated-but-historically-calibrated data for the 10Y–2Y spread alongside NBER recession indicators. The values below mirror real Federal Reserve data closely enough to teach the concepts accurately.
# --- Historical 10Y-2Y spread (approximate annual averages, 1977–2024) --------
# Negative = inverted. Source: calibrated from FRED data.
spread_data <- tribble(
~year, ~spread_pct, ~recession_started,
1977, 0.60, FALSE,
1978, 0.00, FALSE,
1979, -0.80, TRUE, # → 1980 recession
1980, -1.50, FALSE,
1981, -2.40, TRUE, # → 1981-82 recession
1982, 0.90, FALSE,
1983, 2.10, FALSE,
1984, 1.30, FALSE,
1985, 2.00, FALSE,
1986, 1.80, FALSE,
1987, 0.80, FALSE,
1988, 0.10, FALSE,
1989, -0.40, TRUE, # → 1990-91 recession
1990, 0.30, FALSE,
1991, 1.60, FALSE,
1992, 2.90, FALSE,
1993, 2.70, FALSE,
1994, 0.80, FALSE,
1995, 0.70, FALSE,
1996, 0.60, FALSE,
1997, 0.60, FALSE,
1998, 0.60, FALSE,
1999, 0.20, FALSE,
2000, -0.50, TRUE, # → 2001 recession
2001, 2.30, FALSE,
2002, 2.80, FALSE,
2003, 2.60, FALSE,
2004, 1.60, FALSE,
2005, 0.30, FALSE,
2006, -0.10, TRUE, # → 2007-09 recession
2007, 0.50, FALSE,
2008, 1.50, FALSE,
2009, 2.50, FALSE,
2010, 2.50, FALSE,
2011, 1.60, FALSE,
2012, 1.40, FALSE,
2013, 2.30, FALSE,
2014, 2.10, FALSE,
2015, 1.50, FALSE,
2016, 1.20, FALSE,
2017, 1.00, FALSE,
2018, 0.20, FALSE,
2019, -0.20, TRUE, # → 2020 COVID recession
2020, 0.80, FALSE,
2021, 1.20, FALSE,
2022, -0.50, TRUE, # → predicted recession that did NOT materialize
2023, -1.00, FALSE, # deeply inverted — no recession as of writing
2024, -0.30, FALSE
)
glimpse(spread_data)## Rows: 48
## Columns: 3
## $ year <dbl> 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985…
## $ spread_pct <dbl> 0.6, 0.0, -0.8, -1.5, -2.4, 0.9, 2.1, 1.3, 2.0, 1.8,…
## $ recession_started <lgl> FALSE, FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE…
# Add useful derived columns
spread_data <- spread_data %>%
mutate(
inverted = spread_pct < 0,
spread_label = if_else(inverted, "Inverted", "Normal"),
decade = paste0(floor(year / 10) * 10, "s")
)
# Quick look at the inverted years
spread_data %>%
filter(inverted) %>%
select(year, spread_pct, recession_started) %>%
kable(
col.names = c("Year", "10Y–2Y Spread (%)", "Recession Followed?"),
caption = "Years with Inverted Yield Curve (10Y–2Y < 0)"
)| Year | 10Y–2Y Spread (%) | Recession Followed? |
|---|---|---|
| 1979 | -0.8 | TRUE |
| 1980 | -1.5 | FALSE |
| 1981 | -2.4 | TRUE |
| 1989 | -0.4 | TRUE |
| 2000 | -0.5 | TRUE |
| 2006 | -0.1 | TRUE |
| 2019 | -0.2 | TRUE |
| 2022 | -0.5 | TRUE |
| 2023 | -1.0 | FALSE |
| 2024 | -0.3 | FALSE |
# Shade recession periods
recession_bands <- tribble(
~start, ~end,
1980, 1980,
1981, 1982,
1990, 1991,
2001, 2001,
2007, 2009,
2020, 2020
)
ggplot(spread_data, aes(x = year, y = spread_pct)) +
# Shade recession years
geom_rect(
data = recession_bands,
aes(xmin = start - 0.5, xmax = end + 0.5, ymin = -Inf, ymax = Inf),
inherit.aes = FALSE,
fill = "gray80", alpha = 0.5
) +
# Zero line
geom_hline(yintercept = 0, linetype = "dashed", color = "black", linewidth = 0.7) +
# Spread bars, colored by sign
geom_col(aes(fill = inverted), width = 0.7, alpha = 0.85) +
scale_fill_manual(
values = c("FALSE" = "#42A5F5", "TRUE" = "#EF5350"),
labels = c("FALSE" = "Normal (positive)", "TRUE" = "Inverted (negative)")
) +
scale_x_continuous(breaks = seq(1977, 2024, by = 4)) +
scale_y_continuous(labels = label_number(suffix = "%")) +
labs(
title = "10-Year minus 2-Year Treasury Spread, 1977–2024",
subtitle = "Gray bands = NBER recessions | Red bars = yield curve inversion",
x = NULL,
y = "Spread (percentage points)",
fill = "Curve shape"
) +
theme_minimal(base_size = 12) +
theme(
legend.position = "bottom",
panel.grid.minor = element_blank()
)Reading the chart: Every gray recession band was preceded by red (inverted) bars — but notice 2022–2023: deep red bars with no recession band following.
outcomes <- tribble(
~episode, ~inversion_years, ~recession, ~lag_months, ~called_correctly,
"1979–1981", "1979, 1980, 1981", "1980 & 1981–82", 6, TRUE,
"1988–1989", "1989", "1990–91", 12, TRUE,
"1998–2000", "2000", "2001", 8, TRUE,
"2005–2006", "2006", "2007–09", 16, TRUE,
"2019", "2019", "2020 (COVID)", 8, TRUE,
"2022–2024 (ongoing)","2022, 2023, 2024","None yet", NA, FALSE
)
outcomes %>%
kable(
col.names = c("Episode", "Inversion Year(s)", "Recession",
"Lag (months)", "Recession Followed?"),
caption = "Inverted Yield Curve Episodes Since 1979"
)| Episode | Inversion Year(s) | Recession | Lag (months) | Recession Followed? |
|---|---|---|---|---|
| 1979–1981 | 1979, 1980, 1981 | 1980 & 1981–82 | 6 | TRUE |
| 1988–1989 | 1989 | 1990–91 | 12 | TRUE |
| 1998–2000 | 2000 | 2001 | 8 | TRUE |
| 2005–2006 | 2006 | 2007–09 | 16 | TRUE |
| 2019 | 2019 | 2020 (COVID) | 8 | TRUE |
| 2022–2024 (ongoing) | 2022, 2023, 2024 | None yet | NA | FALSE |
Five out of six inversion episodes (through 2021) correctly preceded a recession. The average lag between inversion and recession onset is roughly 6–16 months — which is why economists say the yield curve is a leading indicator, not an immediate alarm.
near_misses <- tribble(
~year_range, ~situation, ~outcome,
"1966", "Brief inversion; Fed pre-empted with hikes", "No recession",
"1998", "Brief inversion during LTCM crisis", "Recession delayed to 2001",
"2022–present","Deep inversion for 2+ years", "No recession yet (as of 2024)"
)
kable(near_misses,
col.names = c("Period", "Context", "Outcome"),
caption = "Cases Where Inversion Did Not Quickly Produce Recession")| Period | Context | Outcome |
|---|---|---|
| 1966 | Brief inversion; Fed pre-empted with hikes | No recession |
| 1998 | Brief inversion during LTCM crisis | Recession delayed to 2001 |
| 2022–present | Deep inversion for 2+ years | No recession yet (as of 2024) |
ggplot(spread_data, aes(x = spread_pct, fill = spread_label)) +
geom_histogram(binwidth = 0.25, color = "white", alpha = 0.85) +
geom_vline(xintercept = 0, linetype = "dashed", linewidth = 0.8) +
scale_fill_manual(values = c("Normal" = "#42A5F5", "Inverted" = "#EF5350")) +
labs(
title = "Distribution of 10Y–2Y Spread (1977–2024)",
x = "Spread (percentage points)",
y = "Count of years",
fill = NULL
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")spread_data %>%
group_by(spread_label) %>%
summarise(
n = n(),
mean = mean(spread_pct),
median = median(spread_pct),
sd = sd(spread_pct),
min = min(spread_pct),
max = max(spread_pct),
.groups = "drop"
) %>%
mutate(across(where(is.numeric), ~ round(.x, 2))) %>%
kable(
col.names = c("Curve Shape", "N Years", "Mean", "Median", "SD", "Min", "Max"),
caption = "Summary Statistics: Spread by Curve Shape"
)| Curve Shape | N Years | Mean | Median | SD | Min | Max |
|---|---|---|---|---|---|---|
| Inverted | 10 | -0.77 | -0.50 | 0.71 | -2.4 | -0.1 |
| Normal | 38 | 1.33 | 1.25 | 0.86 | 0.0 | 2.9 |
# Within 2 years of inversion, did a recession start?
# We'll score each inverted year by whether a recession began that year or next.
spread_data <- spread_data %>%
mutate(
recession_next2 = recession_started |
lead(recession_started, 1, default = FALSE)
)
spread_data %>%
group_by(inverted) %>%
summarise(
n_years = n(),
recession_count = sum(recession_next2),
recession_rate = mean(recession_next2),
.groups = "drop"
) %>%
mutate(
recession_rate = percent(recession_rate, accuracy = 1),
inverted = if_else(inverted, "Yes (inverted)", "No (normal)")
) %>%
kable(
col.names = c("Yield Curve Inverted?", "N Years",
"Recession Within 2 Yrs", "Rate"),
caption = "Recession Probability Conditional on Yield Curve Shape"
)| Yield Curve Inverted? | N Years | Recession Within 2 Yrs | Rate |
|---|---|---|---|
| No (normal) | 38 | 6 | 16% |
| Yes (inverted) | 10 | 8 | 80% |
The conditional rates tell the core story: recession is much more common when the curve is inverted than when it is not — but inversion is neither necessary nor sufficient on its own.
spread_data %>%
group_by(decade) %>%
summarise(
avg_spread = mean(spread_pct),
pct_inverted = mean(inverted) * 100,
.groups = "drop"
) %>%
pivot_longer(cols = c(avg_spread, pct_inverted),
names_to = "metric",
values_to = "value") %>%
mutate(metric = recode(metric,
"avg_spread" = "Average Spread (%pts)",
"pct_inverted" = "% of Years Inverted")) %>%
ggplot(aes(x = decade, y = value, fill = decade)) +
geom_col(show.legend = FALSE) +
facet_wrap(~ metric, scales = "free_y") +
scale_fill_brewer(palette = "Blues") +
labs(
title = "Yield Curve Characteristics by Decade",
x = NULL, y = NULL
) +
theme_minimal(base_size = 12) +
theme(panel.grid.minor = element_blank())| Claim | Evidence |
|---|---|
| Inversion reliably precedes recession | 5 of 6 modern episodes produced a recession |
| It is a leading indicator, not immediate | Lag ranges from ~6 to 18+ months |
| The signal is probabilistic, not deterministic | ~40–60% of inverted years → recession within 2 yrs |
| 2022–2024 is the most notable exception so far | Deepest inversion in 40 years with no recession yet |
| External forces can distort the signal | QE, global capital flows, fiscal stimulus all matter |
Compute the lead time. For each successful
inversion episode in the outcomes table, calculate the
average number of months between the start of inversion and the
start of recession. What is the mean and standard
deviation?
Filter and visualize. Using
spread_data, filter to only years where
inverted == TRUE and create a bar chart of
spread_pct colored by whether
recession_started is TRUE or FALSE. What pattern do you
see?
Running average. Use mutate() and
lag() to compute a 3-year rolling average of
spread_pct. Plot the original series and the smoothed
series together using pivot_longer() and
ggplot2. Does the smoothed version still dip below zero
before recessions?
Contingency table. Use table() or
count() to build a 2×2 contingency table of
inverted × recession_started. Calculate the
odds ratio. What does it tell you about the association between
inversion and recession?
Research question. The 2022–2024 inversion is ongoing. Using what you know about the lag, when would you expect a recession to begin if the historical relationship holds? Write 2–3 sentences using your calculations to support your answer.
Data calibrated from FRED (Federal Reserve Bank of St. Louis). Recession dates per NBER Business Cycle Dating Committee. This document is for educational purposes.