Library

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   4.0.0     ✔ tibble    3.3.0
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.0.4     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)
library(fredr)
## Warning: package 'fredr' was built under R version 4.5.2
library(scales)
## 
## Attaching package: 'scales'
## 
## The following object is masked from 'package:purrr':
## 
##     discard
## 
## The following object is masked from 'package:readr':
## 
##     col_factor

Introduction

The Federal Reserve has a simple but demanding mandate from Congress: - Keep prices stable - Maintain maximum employment

In practice, these goals often pull in opposite directions. Raising interest rates to fight inflation can slow the economy and cost jobs. Cutting rates to support employment can risk higher inflation.

This data story asks: Has the Federal Reserve been able to fulfill its dual mandate over the past 25 years?

To explore this question, I look at three monthly series from 1999 to the present:

I first examine each objective separately (inflation and unemployment), then overlay the Fed’s policy rate to see how the central bank reacts to changing economic conditions.

Load Data

fredr_set_key("5b17c40f404da59e1ed890efd6eaf575")

start_date <- as.Date("1999-01-01")
end_date <- Sys.Date()


# CPI
cpi_raw <- fredr(
  series_id = "CPIAUCSL",
  observation_start = start_date,
  observation_end = end_date
)

# Unemployument rate
unemp_raw <- fredr(
  series_id = "UNRATE",
  observation_start = start_date,
  observation_end   = end_date
)

# Fed funds rate
fedfunds_raw <- fredr(
  series_id = "FEDFUNDS",
  observation_start = start_date,
  observation_end = end_date
)

head(cpi_raw)
head(unemp_raw)
head(fedfunds_raw)

Clean & Combine the Data

I convert CPI into a year over year inflation rate and then merge all three series into a single monthly data frame.

cpi <- cpi_raw %>%
  select(date, cpi_index = value) %>%
  arrange(date) %>%
  mutate(
    inflation_yoy = (cpi_index / lag(cpi_index, 12) - 1)* 100) %>%
  filter(!is.na(inflation_yoy))

# clean unemployment
unemp <- unemp_raw %>%
  select(date, unemployment_rate = value)

# clean fed funds rate
fedfunds <- fedfunds_raw %>%
  select(date, fed_funds_rate = value)

# merge by date
macro <- cpi %>%
  inner_join(unemp, by = "date") %>%
  inner_join(fedfunds, by = "date") %>%
  filter(date >= start_date, date <= end_date)

head(macro)
summary(macro)
##       date              cpi_index     inflation_yoy    unemployment_rate
##  Min.   :2000-01-01   Min.   :169.3   Min.   :-1.959   Min.   : 3.400   
##  1st Qu.:2006-06-01   1st Qu.:201.8   1st Qu.: 1.641   1st Qu.: 4.200   
##  Median :2012-11-01   Median :231.2   Median : 2.334   Median : 5.000   
##  Mean   :2012-10-31   Mean   :232.6   Mean   : 2.583   Mean   : 5.652   
##  3rd Qu.:2019-04-01   3rd Qu.:255.2   3rd Qu.: 3.322   3rd Qu.: 6.300   
##  Max.   :2025-09-01   Max.   :324.4   Max.   : 8.999   Max.   :14.800   
##  fed_funds_rate 
##  Min.   :0.050  
##  1st Qu.:0.150  
##  Median :1.260  
##  Mean   :1.989  
##  3rd Qu.:3.970  
##  Max.   :6.540

Analysis

1. Price Stability: Inflation vs the Fed’s 2% Target

The Fed’s long-run inflation goal is roughly 2% per year. The chart below shows year-over-year CPI inflation since 1999, with a light band around the 2% target.

ggplot(macro, aes(x = date, y = inflation_yoy)) +
  geom_hline(yintercept = 2, linetype = "dashed", color = "grey40") +
  geom_ribbon(aes(ymin = 1.5, ymax = 2.5),
              fill = "grey90", alpha = 0.5) +
  geom_line(color = "firebrick") +
  labs(
    title = "Year over Year CPI Inflation (1999-Present)",
    x = "Year",
    y = "Inflation Rate (%, year over year)",
    caption = "Source: BLS CPI (via FRED)"
  ) +
  scale_y_continuous(labels = percent_format(scale = 1)) +
  theme_minimal()

For much of the early 2000s and the decade after the Great Recession, inflation hovered near the 2% target band. There were temporary dips during the 2008–2009 financial crisis and in the early COVID period, followed by a sharp spike in 2021–2022 when inflation surged well above 5%.

Overall, the Fed stayed reasonably close to its price-stability goal for long stretches of time, but the post-pandemic period stands out as a clear miss where inflation persistently ran above target.

2. Maximum Employment: The Unemployment Rate

Economists often view an unemployment rate around 4%–5% as compatible with “maximum employment.” The next chart shows how the job market has evolved over the same period.

ggplot(macro, aes(x = date, y = unemployment_rate)) +
  geom_hline(yintercept = 4, linetype = "dashed", color = "grey40") +
  geom_line(color = "steelblue") +
  labs(
    title = "U.S. Unemployment Rate (1999-Present)",
    x = "Year",
    y = "Unemployment rate(%)",
    caption = "Source: BLS Unemployment Rate (via FRED)"
  ) +
  theme_minimal()

The unemployment rate tells a different part of the story. It was low and fairly stable in the late 1990s and mid-2000s, then spiked above 10% during the Great Recession. After a long recovery, unemployment fell to historically low levels before the COVID shock pushed it abruptly higher again in 2020.

In both major downturns, the Fed eventually helped bring unemployment back down, but only after long adjustment periods. Over the full 25 years, the labor market spends more time in “healthy” territory than in crisis, yet there are clear episodes when the mandate of maximum employment was not met.

3. How Does the Fed React? Policy Rate vs Inflation and Unemployment

To see how the Fed responds to changing conditions, I standardize each series (inflation, unemployment, and the Fed funds rate) so we can compare their ups and downs on the same scale.

macro_std <- macro %>%
  mutate(
    inflation_z = as.numeric(scale(inflation_yoy)),
    unemployment_z = as.numeric(scale(unemployment_rate)),
    fedfunds_z = as.numeric(scale(fed_funds_rate))
  ) %>%
  select(date, inflation_z, unemployment_z, fedfunds_z) %>%
  pivot_longer(
    cols = -date,
    names_to = "series",
    values_to = "z"
  ) %>%
  mutate(
    series = recode(series,
                    inflation_z = "Inflation(YoY)",
                    unemployment_z = "Unemployment Rate",
                    fedfunds_z = "Fed Funds Rate")
  )
ggplot(macro_std, aes(x = date, y = z, color = series)) +
  geom_line() +
  labs(
    title = "Standardized Inflation, Unemployment, and Fed Funds Rate",
    subtitle = "Each line shows deviations from its own 25 years average",
    x = "Year",
    y = "Standardized Value (z-score)",
    color = "Series",
    caption = "Sources: BLS, Federal Reserve"
  ) +
  theme_minimal()

This plot highlights how the Fed tends to move interest rates in response to inflation and unemployment.

  • During the early 2000s and especially the Great Recession, unemployment spikes upward while the Fed funds rate drops sharply below its average. The Fed cuts rates aggressively to support the labor market.
  • In the long recovery of the 2010s, unemployment gradually falls and inflation stays subdued; the Fed slowly raises rates back toward more normal levels.
  • After COVID, rates are cut to near zero as unemployment jumps, then raised very quickly once inflation surges in 2021–2022.

The timing shows the usual sequence: inflation picks up, the Fed raises rates, and higher borrowing costs eventually cool the economy and push unemployment higher with a lag. The Fed is clearly reacting to the data, but it cannot avoid all pain—there are periods when either unemployment or inflation is far from the desired range.

4. Inflation vs Unemployment

Finally, I look directly at the relationship between inflation and unemployment over time. Each point is a month, colored by year.

macro %>%
  mutate(year = year(date)) %>% # color by year to show evolution
  ggplot(aes(x = inflation_yoy, y = unemployment_rate, color = year)) +
  geom_point(alpha = 0.7) +
  scale_color_viridis_c(option = "plasma") +
  labs(
    title = "Inflation vs Unemployment (Monthly, 1999–Present)",
    x = "Inflation rate (%, year over year)",
    y = "Unemployment rate (%)",
    color = "Year"
    ) +
theme_minimal()

If the Fed could perfectly balance its mandate, we might expect most points to cluster around low inflation and low unemployment. Instead, the scatter shows that the economy moves through many different combinations:

  • High unemployment with low or even negative inflation during deep recessions.
  • Low unemployment with moderate inflation during “good times.”
  • Recently, high inflation with relatively low unemployment.

There is no single, stable trade-off curve. Instead, the Fed is constantly trying to steer the economy back toward the sweet spot of low, stable inflation and low unemployment, but global shocks and structural changes frequently push it away.

Conclusion

So, has the Fed fulfilled the mandate given to it by Congress?

On inflation, the Fed kept price growth close to 2% for much of the 2000s and 2010s, but the post-COVID surge shows that price stability is fragile. The recent overshoot is a clear miss on the price-stability part of the mandate.

In terms of policy actions, the Fed funds rate clearly responds to inflation and unemployment. Rates are cut when unemployment spikes and raised when inflation becomes a concern. However, these changes work with a lag and cannot fully offset powerful shocks such as financial crises or pandemics.

Overall, the evidence suggests that the Fed has only partially achieved its dual mandate. It has generally maintained low inflation and low unemployment over long stretches of time, but there are significant periods—most notably the early-2000s downturn, the Great Recession, and the post-COVID inflation surge—when either inflation or unemployment (and sometimes both) have been far from the desired range.