Structural Breaks: A Simple Teaching Note

Author

AS

What Is a Structural Break?

A structural break means the relationship in the data changes at some point in time.

In simple words:

  • the model worked one way before
  • and a different way after

This change can happen because of:

  • a new policy (tariff,
  • a recession or financial crisis
  • a war or pandemic
  • a change in technology
  • a change in institutions or behavior

Intuition

Suppose we are studying how x affects y.

Before a certain date, a 1-unit increase in x may increase y by a lot. After that date, the same 1-unit increase in x may increase y by less (or more).

That is a structural break.

Why Structural Breaks Matter

If we ignore a structural break, then:

  • coefficient estimates can be misleading
  • forecasts can be poor
  • policy conclusions can be wrong

This is especially important in macroeconomics and finance, where regimes can change.

Types of Structural Breaks (Basic)

No structural break

1. Level (Intercept) Break

The average level changes, even if the slope stays similar.

Example:

  • inflation is suddenly higher on average after a policy shift

  • MSFT Gaming Division CEO resigns (stock trading roughly as same level but sharp drop on Monday opening). If you increase the time period, you might think there is a intercept and slope change as well.

    Phil Spencer is retiring as Microsoft Gaming CEO after 38 years, effective February 23, 2026, alongside the departure of Xbox President Sarah Bond. Asha Sharma, previously head of Microsoft’s CoreAI, takes over as CEO, marking a major AI-focused pivot for the division, focusing on “breaking down barriers” for multi-platform experiences.

2. Slope Break

The effect of one variable on another changes.

Example:

  • interest rate changes affect output more strongly in one period than another

3. Multiple Breaks

There may be more than one break date.

Example:

  • pre-crisis, crisis, and post-crisis periods

Lets classify -

A Very Simple Equation View

Before the break:

\[ y_t = \beta_0 + \beta_1 x_t + u_t \]

After the break:

\[ y_t = \beta_0' + \beta_1' x_t + u_t \]

If beta0 or beta1 changes, the structure of the model has changed.

Example: Monetary Policy and Money Supply

The Idea

In macroeconomics, the relationship between money supply growth and inflation (or output growth) may change after a major monetary policy shift.

A structural break can happen if:

  • the central bank changed its policy framework
  • inflation targeting became more credible
  • interest-rate policy replaced money-growth targeting

Then the same money supply growth may have a different effect than before.

Example Story

Imagine two periods:

  • Period 1 (earlier): Money supply growth is strongly associated with inflation.
  • Period 2 (later): The relationship is weaker because the central bank anchors expectations better.

If you estimate one model over the full sample without allowing a break, you may miss this change.

A Simple Break Model

Let:

  • Inflation_t = inflation rate
  • MoneyGrowth_t = money supply growth
  • PostPolicy_t = 1 after the policy change, 0 before

Then a simple break model is:

\[ Inflation_t = \beta_0 + \beta_1 MoneyGrowth_t + \beta_2 PostPolicy_t + \beta_3 (MoneyGrowth_t \times PostPolicy_t) + u_t \]

Interpretation:

  • beta2 = change in average inflation after the policy shift (level change)
  • beta3 = change in the slope (the effect of money growth on inflation)

If beta3 is different from zero, that suggests a structural break in the relationship.

NoteThe statistical model

See the estimating equations here -

https://ds4ps.org/PROG-EVAL-III/TimeSeries.html

A Basic Workflow for Identifying Structural Breaks

Step 1: Start with a Plot

Plot the variable over time and check for:

  • a clear shift in level
  • a different trend after a specific date
  • much higher or lower volatility in one period

Step 2: Use Economic History

Match the timing to economic events such as:

  • policy regime change?
  • recession/crisis?
  • law or institutional reform?

This connects the statistical pattern to the economic narrative.

Step 3: Compare Before vs After

Estimate:

  • one model for the full sample
  • one model before the break
  • one model after the break

Then compare coefficients.

Step 4: Formal Test (Later)

Once students understand the idea, introduce formal tests such as:

  • Chow test (break date known)
  • tests for unknown break date (later topic)

Real Data Examples (FRED via fredr, 1960 Onward)

These examples use:

  • fredr to pull FRED data directly
  • fpp3 for plotting/modeling
  • strucchange for structural break detection

Packages

remove(list=ls())
library(tidyverse)
Warning: package 'dplyr' was built under R version 4.5.2
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.0     ✔ 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.1.0     
── 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(fredr)
library(fpp3)
Registered S3 method overwritten by 'tsibble':
  method               from 
  as_tibble.grouped_df dplyr
── Attaching packages ──────────────────────────────────────────── fpp3 1.0.2 ──
✔ tsibble     1.1.6     ✔ feasts      0.4.2
✔ tsibbledata 0.4.1     ✔ fable       0.4.1
── Conflicts ───────────────────────────────────────────────── fpp3_conflicts ──
✖ lubridate::date()    masks base::date()
✖ dplyr::filter()      masks stats::filter()
✖ tsibble::intersect() masks base::intersect()
✖ tsibble::interval()  masks lubridate::interval()
✖ dplyr::lag()         masks stats::lag()
✖ tsibble::setdiff()   masks base::setdiff()
✖ tsibble::union()     masks base::union()
library(strucchange)
Loading required package: zoo

Attaching package: 'zoo'

The following object is masked from 'package:tsibble':

    index

The following objects are masked from 'package:base':

    as.Date, as.Date.numeric

Loading required package: sandwich

Attaching package: 'strucchange'

The following object is masked from 'package:stringr':

    boundary
library(splines)

FRED API Key

fredr requires a FRED API key.

fredr_set_key("8a9ec1330374c1696f05cc8e526233b5") # replace with your own key please

Example 1: U.S. Money Supply M2 (Level/Trend Breaks), 1960 Onward

FRED series M2SL is used to show structural breaks in the level and trend of money supply.

m2_raw <- fredr(
  series_id = "M2SL",
  observation_start = as.Date("1960-01-01")
)

m2_ts <- m2_raw |>
  transmute(
    date = yearmonth(date),
    m2 = value
  ) |>
  as_tsibble(index = date) |>
  mutate(t = row_number())
autoplot(m2_ts, m2) +
  labs(
    title = "U.S. Money Supply M2 (FRED: M2SL), 1960 Onward",
    x = "Year",
    y = "Billions of Dollars"
  )

Example 1B: Plot with User-Specified Structural Break Dates (Red Lines)

Enter structural break dates in the manual_breaks vector. The chart will draw red vertical lines at those dates.

manual_breaks <- yearmonth(c(
  "1979 Jan",
  "1984 Jan",
  "2008 Sep",
  "2020 Mar"
))

manual_breaks_df <- tibble(break_date = as.Date(manual_breaks)) |>
  mutate(
    label = format(break_date, "%Y-%m")
  )

m2_plot_df <- m2_ts |>
  mutate(date_plot = as.Date(date))

manual_breaks_df <- manual_breaks_df |>
  mutate(y_pos = max(m2_plot_df$m2, na.rm = TRUE) * 0.98)

ggplot(m2_plot_df, aes(x = date_plot, y = m2)) +
  geom_line(color = "steelblue", linewidth = 0.8) +
  geom_vline(
    data = manual_breaks_df,
    aes(xintercept = break_date),
    color = "red",
    linetype = "dashed",
    linewidth = 0.8,
    inherit.aes = FALSE
  ) +
  geom_text(
    data = manual_breaks_df,
    aes(x = break_date, y = y_pos, label = label),
    color = "red",
    angle = 90,
    vjust = -0.3,
    hjust = 1,
    size = 3,
    inherit.aes = FALSE
  ) +
  labs(
    title = "U.S. Money Supply M2 with User-Specified Structural Break Dates",
    x = "Year",
    y = "Billions of Dollars"
  )
Warning in geom_vline(data = manual_breaks_df, aes(xintercept = break_date), :
Ignoring unknown parameters: `inherit.aes`

bp_m2 <- breakpoints(m2 ~ t, data = m2_ts)
summary(bp_m2)

     Optimal (m+1)-segment partition: 

Call:
breakpoints.formula(formula = m2 ~ t, data = m2_ts)

Breakpoints at observation number:
                           
m = 1               570    
m = 2           462     651
m = 3   178     488     656
m = 4   181     420 557 675
m = 5   182 318 439 557 675

Corresponding to breakdates:
                                                                               
m = 1                                                         0.718789407313998
m = 2                                       0.582597730138714                  
m = 3   0.224464060529634                   0.615384615384615                  
m = 4   0.228247162673392                   0.529634300126103 0.702395964691047
m = 5   0.229508196721311 0.401008827238335 0.55359394703657  0.702395964691047
                         
m = 1                    
m = 2   0.82093316519546 
m = 3   0.827238335435057
m = 4   0.851197982345523
m = 5   0.851197982345523

Fit:
                                                               
m   0         1         2         3         4         5        
RSS 6.102e+09 4.526e+08 2.283e+08 1.964e+08 1.819e+08 1.799e+08
BIC 1.484e+04 1.280e+04 1.228e+04 1.218e+04 1.214e+04 1.215e+04

Interpretation:

  • Detected breaks often align with changes in monetary regimes, crises, and policy responses.
TipNew FOMC Chair appointment can affect monetary policy.

Janet Yellen served as the 15th Chair of the Federal Reserve from February 3, 2014, to February 3, 2018, becoming the first woman to lead the U.S. central bank. Nominated by President Obama in 2013 and confirmed by the Senate on January 6, 2014, she focused on strengthening the labor market and normalizing monetary policy post-financial crisis.

Key Details of Yellen’s FOMC Leadership:

  • Appointment: Nominated on October 9, 2013, to succeed Ben Bernanke, and sworn in on February 3, 2014.

  • Term: Served a four-year term, ending on February 3, 2018, and was succeeded by Jerome Powell.

  • Background: Prior to her chair position, she was Vice Chair (2010–2014) and President of the Federal Reserve Bank of San Francisco (2004–2010).

  • Policy Focus: She advocated for a gradual approach to raising interest rates to support the economic recovery and employment, while increasing transparency in Fed decision-making.

Following her Fed chair tenure, she later served as the 78th U.S. Secretary of the Treasury from 2021 to 2025.

Structural break test logic:

  1. Fit one model on all data (assumes coefficients are constant).

  2. Fit model(s) that allow coefficients to differ across subperiods.

  3. Compare fit: does allowing a break reduce residual error a lot?

Hypotheses:

  • H0: no structural break (same coefficients over time)

  • H1: structural break exists (coefficients change)

Decision logic:

  • If split-model improvement is small, keep H0.

  • If improvement is large (large test statistic, small p-value), reject H0 and conclude a break is likely.

Known break date:

  • Use Chow-type logic at that specific date (pooled vs pre/post regressions).

Unknown break date:

  • Search across many candidate dates, compute test values, and pick date(s) with strongest evidence (subject to minimum segment length).

Spline: What It Is and Why It Is Useful

A spline is a flexible trend curve built from smaller pieces joined together at points called knots.

  • Each piece is a polynomial (for example, a line or a cubic curve).
  • The knots are the locations where the curve is allowed to change shape.
  • The full curve stays connected across knots.

Why splines are useful

Splines are useful when one straight trend line is too simple.

  • They capture changes in trend over time.
  • They can fit gradual changes, not only sudden jumps.
  • They summarize long series (like M2) more clearly than a single linear trend.

Comparison: spline vs intercept/slope break regression

An intercept/slope break regression (with dummy variables) is best when:

  • there is a specific break date (for example, 1979, 1984, 2020)
  • the goal is to estimate and interpret coefficient changes directly

A spline is best when:

  • the trend changes at several points
  • the goal is to describe the shape of the series over time
  • a smoother fitted path is more useful than a single before/after coefficient comparison

In short:

  • Dummy break model = clearer coefficient interpretation (intercept change, slope change)
  • Spline model = clearer trend-shape description across multiple periods

Example 1A: Piecewise Linear Spline Using Break Dates

This uses the real FRED M2 series (M2SL) and applies the spline approach used in the sales example: estimated break dates are used as knots in a piecewise linear spline.

# Select a small number of breakpoints for a clean classroom plot
bp_m2_2 <- breakpoints(m2 ~ t, data = m2_ts, breaks = 2)

knot_vec <- bp_m2_2$breakpoints
knot_vec <- knot_vec[!is.na(knot_vec)]
fit_m2_spline <- lm(
  m2 ~ bs(t, knots = knot_vec, degree = 3),
  data = m2_ts
)

x_grid <- seq(min(m2_ts$t), max(m2_ts$t), length.out = 400)

pred_m2_spline <- predict(
  fit_m2_spline,
  newdata = tibble(t = x_grid),
  se.fit = TRUE
)
plot(
  m2_ts$t, m2_ts$m2,
  type = "l",
  xlab = "Time (months since 1960-01)",
  ylab = "M2",
  main = "M2 with Piecewise Linear Spline (Knots from Breakpoints)",
  col = "red" 
)

lines(x_grid, pred_m2_spline$fit, lwd = 2)
lines(x_grid, pred_m2_spline$fit + 1.96 * pred_m2_spline$se.fit, lty = 2)
lines(x_grid, pred_m2_spline$fit - 1.96 * pred_m2_spline$se.fit, lty = 2)
abline(v = knot_vec, lty = 3)

Interpretation:

  • Vertical dashed lines mark estimated break locations used as spline knots.
  • The spline summarizes changing slope segments in the money supply path.
TipTypes of Splines

The most common splines used for time series analysis and modeling are natural cubic splines, B-splines (Basis splines), and smoothing splines. These methods are popular for fitting non-linear trends, capturing complex seasonality, and handling irregularly sampled data because they provide smooth, flexible, and computationally efficient approximations, often used within Generalized Additive Models (GAMs).

Example 2: M2 Growth (YoY) as a Structural Break Series

Because M2 has a strong trend in levels, breaks are often easier to see in growth rates.

m2_growth_ts <- m2_ts |>
  mutate(
    m2_growth_yoy = 100 * (log(m2) - log(lag(m2, 12)))
  ) |>
  filter(!is.na(m2_growth_yoy))
autoplot(m2_growth_ts, m2_growth_yoy) +
  labs(
    title = "U.S. M2 Growth (YoY), 1960 Onward",
    x = "Year",
    y = "Percent"
  )

bp_m2_growth <- breakpoints(m2_growth_yoy ~ 1, data = m2_growth_ts)
summary(bp_m2_growth)

     Optimal (m+1)-segment partition: 

Call:
breakpoints.formula(formula = m2_growth_yoy ~ 1, data = m2_growth_ts)

Breakpoints at observation number:
                           
m = 1       317            
m = 2       318 437        
m = 3   121 318 437        
m = 4   121 318 439 586    
m = 5   121 313 430 547 664

Corresponding to breakdates:
                                                                               
m = 1                     0.405889884763124                                    
m = 2                     0.407170294494238 0.559539052496799                  
m = 3   0.154929577464789 0.407170294494238 0.559539052496799                  
m = 4   0.154929577464789 0.407170294494238 0.562099871959027 0.750320102432778
m = 5   0.154929577464789 0.400768245838668 0.550576184379001 0.700384122919334
                         
m = 1                    
m = 2                    
m = 3                    
m = 4                    
m = 5   0.850192061459667

Fit:
                                 
m   0    1    2    3    4    5   
RSS 9624 8102 7442 7033 7020 7108
BIC 4191 4070 4017 3986 3998 4021

Interpretation:

  • This separates breaks in the level trend from breaks in the growth process.

Example 3: Money Supply Growth and Inflation (Simple Break Regression)

This example shows a break in a relationship (slope break), not just a break in one series.

cpi_raw <- fredr(
  series_id = "CPIAUCSL",
  observation_start = as.Date("1960-01-01")
)

cpi_ts <- cpi_raw |>
  transmute(
    date = yearmonth(date),
    cpi = value
  ) |>
  as_tsibble(index = date)

macro_break <- m2_ts |>
  left_join(cpi_ts, by = "date") |>
  mutate(
    m2_growth = 100 * (log(m2) - log(lag(m2, 12))),
    inflation_yoy = 100 * (log(cpi) - log(lag(cpi, 12))),
    post_1984 = if_else(date >= yearmonth("1984 Jan"), 1, 0),
    m2g_post_1984 = m2_growth * post_1984
  ) |>
  filter(!is.na(m2_growth), !is.na(inflation_yoy))
autoplot(macro_break, inflation_yoy) +
  labs(
    title = "U.S. CPI Inflation (YoY), 1960 Onward",
    x = "Year",
    y = "Percent"
  )

fit_macro_break <- macro_break |>
  model(TSLM(inflation_yoy ~ m2_growth + post_1984 + m2g_post_1984))

report(fit_macro_break)
Series: inflation_yoy 
Model: TSLM 

Residuals:
    Min      1Q  Median      3Q     Max 
-4.7620 -1.1553 -0.2647  1.0922  8.2806 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)    
(Intercept)    6.25181    0.51708  12.091  < 2e-16 ***
m2_growth     -0.11743    0.05990  -1.960   0.0503 .  
post_1984     -3.02109    0.55141  -5.479 5.79e-08 ***
m2g_post_1984  0.04016    0.06631   0.606   0.5450    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 2.348 on 776 degrees of freedom
Multiple R-squared: 0.2134, Adjusted R-squared: 0.2103
F-statistic: 70.15 on 3 and 776 DF, p-value: < 2.22e-16

Interpretation:

  • post_1984 = did average inflation shift after 1984?
  • m2g_post_1984 = did the money growth-inflation relationship change after 1984?
  • This regression is a simple break specification for illustrating slope and level changes.
ImportantImportant Note on Break Location

Structural break tests are usually more likely to detect breaks in the middle of a time series than near the beginning or end.

Reasons:

  • enough observations are needed on both sides of a candidate break
  • many procedures trim endpoints and do not test very early/late dates
  • statistical power is weaker near sample boundaries

A break near the start or end of the sample can be real but harder to detect.

Common Mistakes

  • treating every outlier as a structural break
  • choosing a break date with no economic reason
  • ignoring breaks and interpreting one average coefficient for all periods
  • claiming causality from a simple break regression without more evidence

Why One Outlier Is Not Usually a Structural Break

An outlier is typically a one-time unusual observation.
A structural break is a persistent change in the data-generating process.

Key difference:

  • outlier: one point (or a few points) far from the usual pattern
  • structural break: the pattern after a date is systematically different

In break testing, one outlier usually does not create a stable improvement when the sample is split into before/after regimes. A true break usually shows a repeated shift in level, slope, or both across many observations.

Key Takeaway

A structural break means the economy may be operating under a different regime. In macro examples like monetary policy and money supply, checking for breaks helps students avoid forcing one model onto two different periods.

Extensions

  • Chow test example (known break date)
  • Pre/post comparison table
  • Version using a course-specific dataset

Appendix