The pipeline pulls the United States and euro-area series from FRED, builds three Taylor-rule variants, measures how far the federal funds rate strayed from each, and then tests whether that deviation carries predictive content for the inflation that followed. Stationarity tests, Granger causality, and a cross-correlation function run inside a single analysis script; the figures are generated last.

What the code does

This is the analysis pipeline behind the paper Discretion Over Rules: Federal Reserve Policy Departures and the Inflation Surge of 2021–2022.

The project is around 1,800 lines of R across seven scripts, driven by a single run_all.R. The empirical claim of the paper is narrow and testable: the Fed's departure from rules-based benchmarks was not merely coincident with the inflation surge. It carried statistically significant predictive content for it. Every number behind that claim is produced by the two scripts described below.

The pipeline

Stage Script Does Produces
1 R/01_data.R FRED pull, quarterly conversion, output gap df_quarterly.rds
2 R/02_analysis.R Taylor variants, ECB rule, ADF, Granger, CCF df_analysis.rds, df_ccf.rds
3 R/04_hook_figure.R The lead dual-axis chart fig0
4 R/03_figures.R Chart.js widgets output/widgets/
5 R/04_pngs.R Static PNG fallbacks output/figures/

run_all.R wraps each stage in a timed handler that halts the pipeline on the first error and prints which script failed, so a broken FRED pull never produces half-built figures downstream.

Walkthrough

Two transforms set up everything that follows: getting monthly FRED series onto a quarterly grid, and turning that grid into rule prescriptions and deviations.

The frequency conversion is a small step with long reach: the choice of which observation represents a quarter changes every downstream number.

`R/01_data.R`: collapsing monthly series to quarter-end observations
to_quarterly <- function(df) {
  col <- names(df)[2]
  df |>
    dplyr::mutate(quarter = as.Date(as.yearqtr(date))) |>
    dplyr::group_by(quarter) |>
    dplyr::slice_tail(n = 1) |>
    dplyr::ungroup() |>
    dplyr::select(date = quarter, !!col := !!sym(col))
}

The function takes the last observation of each quarter rather than averaging, which matches the convention used for end-of-period policy rates and keeps the funds rate aligned with the quarter in which it was actually set. Tidy evaluation, the !!col and !!sym(col) machinery, lets one function serve every series without naming any column, so the same five lines convert the funds rate, inflation, output, and the euro-area block alike.

With the data quarterly, the analysis script writes the rules. The detail worth pausing on is the order of two operations: the zero lower bound and the inertial smoothing.

`R/02_analysis.R`: Taylor variants with the floor applied before inertia
df <- df |>
  dplyr::mutate(
    taylor_pure_1 = R_STAR_BASE + PCEPILFE + 0.5 * (PCEPILFE - PI_STAR) + 0.5 * output_gap,
    taylor_pure_2 = R_STAR_ALT  + PCEPILFE + 0.5 * (PCEPILFE - PI_STAR) + 0.5 * output_gap,
    taylor_pure_3 = R_STAR_BASE + PCEPILFE + 0.5 * (PCEPILFE - PI_STAR) + 0.5 * output_gap,

    # Floor at zero before applying inertia so the bound is respected in the
    # inertial term as well, not just the final output.
    Taylor_1 = pmax(taylor_pure_1, 0),
    Taylor_2 = pmax(taylor_pure_2, 0),
    Taylor_3 = pmax(
      0.75 * dplyr::lag(FEDFUNDS, 1) + 0.25 * pmax(taylor_pure_3, 0),
      0
    ),

    Dev_1 = Taylor_1 - FEDFUNDS,
    Dev_2 = Taylor_2 - FEDFUNDS,
    Dev_3 = Taylor_3 - FEDFUNDS
  ) |>
  dplyr::select(-taylor_pure_1, -taylor_pure_2, -taylor_pure_3)

The third variant is the inertial rule: a partial adjustment that puts three-quarters weight on last quarter's rate and one-quarter weight on the fresh prescription. The bound is enforced twice, once on the fresh prescription inside the smoother and once on the smoothed result. Flooring only the final output would let a deeply negative prescription drag the inertial term below zero before the outer clip caught it, which would understate how loose the rule judged policy to be at the bound. Each variant reduces to a deviation, Dev = Taylor - FEDFUNDS, the single quantity the rest of the paper analyzes.

Everything to this point is construction. The empirical question begins with whether the deviation leads inflation, which the script tests with bivariate Granger causality across four lags in both directions.

`R/02_analysis.R`: Granger causality in both directions, lags one to four
cat("── Granger causality: Dev_1 → PCEPILFE (deviation causes inflation?) ────\n")
for (lag in 1:4) {
  gt <- grangertest(PCEPILFE ~ Dev_1, order = lag, data = granger_data)
  cat(sprintf("  lag %d : F = %.3f  p = %.4f  %s\n",
              lag, gt$F[2], gt$`Pr(>F)`[2],
              ifelse(gt$`Pr(>F)`[2] < 0.05, "[significant]", "")))
}

cat("\n── Granger causality: PCEPILFE → Dev_1 (inflation causes deviation?) ───\n")
for (lag in 1:4) {
  gt <- grangertest(Dev_1 ~ PCEPILFE, order = lag, data = granger_data)
  cat(sprintf("  lag %d : F = %.3f  p = %.4f  %s\n",
              lag, gt$F[2], gt$`Pr(>F)`[2],
              ifelse(gt$`Pr(>F)`[2] < 0.05, "[significant]", "")))
}

Running the test in both directions is the discipline that makes the result credible. A finding that deviations predict inflation means little if inflation equally predicts deviations, since that pattern would point to a common driver rather than a causal channel. The loop reports the F-statistic and p-value at each lag, and the asymmetry between the two blocks is the evidence the paper rests on. A VAR(4) specification follows in the same script as a joint check that does not depend on the single-direction framing.

The cross-correlation function then sharpens the timing, and here a one-line filter encodes the causal hypothesis.

`R/02_analysis.R`: keeping only non-negative lags of the deviation
ccf_obj <- ccf(
  ccf_data$Dev_1,
  ccf_data$PCEPILFE,
  lag.max = 8,
  plot    = FALSE
)

# Keep only non-negative lags (deviation leading or coincident with inflation)
df_ccf <- data.frame(
  lag         = ccf_obj$lag[ccf_obj$lag >= 0],
  correlation = ccf_obj$acf[ccf_obj$lag >= 0]
)

The cross-correlation function returns correlations at every lead and lag. Retaining only the non-negative lags restricts the picture to the deviation leading or coinciding with inflation, which is the direction the theory predicts and the Granger tests support. The retained correlations are what the cross-correlation figure plots, so the chart shows the lead structure rather than the full symmetric function.

Reproducing it

The pipeline needs a FRED API key and a recent R installation. Rscript run_all.R runs every stage in sequence and prints a final manifest of the expected outputs with their file sizes. The euro-area block uses a simplified Taylor specification without an output-gap term, because euro-area potential output is not available on FRED; this is documented in the code and in the paper's methodology. The data vintage is locked to 2015 Q1 through 2024 Q4.