Fetch and analyze FRED macroeconomic time series data using fredapi; covers GDP, unemployment, inflation, yield curves, recession shading, and HP filtering.
Fetch and analyze FRED macroeconomic time series data using fredapi; covers GDP, unemployment, inflation, yield curves, recession shading, and HP filtering.
This skill covers end-to-end macroeconomic data workflows using the Federal Reserve
Economic Data (FRED) API via the fredapi Python package. You will learn to
authenticate, fetch single and panel series, compute growth rates, apply the
Hodrick-Prescott filter, date business cycles, and produce publication-quality charts
with recession shading.
1. Authentication and Setup
Store your FRED API key as an environment variable. Never hard-code keys in source files.
# Obtain a free key at https://fred.stlouisfed.org/docs/api/api_key.htmlexport FRED_API_KEY="<paste-your-key>"
pip install fredapi pandas numpy matplotlib statsmodels openpyxl
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from fredapi import Fred
from statsmodels.tsa.filters.hp_filter import hpfilter
# ---------------------------------------------------------------------------# Authenticate# ---------------------------------------------------------------------------
FRED_API_KEY = os.environ["FRED_API_KEY"]
fred = Fred(api_key=FRED_API_KEY)
# Key FRED series IDs used throughout this skill
SERIES = {
"gdp": "GDPC1", # Real GDP (quarterly, chained 2017 $)"unemployment": "UNRATE", # Civilian unemployment rate (monthly)"cpi": "CPIAUCSL", # CPI All Urban Consumers (monthly)"pce": "PCEPI", # PCE Price Index (monthly)
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
}
"ppi"
"PPIACO"
# PPI All Commodities (monthly)
"fedfunds"
"FEDFUNDS"
# Effective federal funds rate (monthly)
"t10y2y"
"T10Y2Y"
# 10Y-2Y Treasury yield spread (daily)
"t10y3m"
"T10Y3M"
# 10Y-3M Treasury yield spread (daily)
"recession"
"USREC"
# NBER recession indicator (monthly, 0/1)
"indpro"
"INDPRO"
# Industrial production index (monthly)
"m2"
"M2SL"
# M2 money supply (monthly)
"realwage"
"AHETPI"
# Average hourly earnings (monthly)
2. Core Helper Functions
2.1 Fetch a Single Series
defget_fred_series(
series_id: str,
start: str = "2000-01-01",
end: str | None = None,
frequency: str | None = None,
units: str = "lin",
) -> pd.Series:
"""
Fetch a FRED series as a pandas Series with a DatetimeIndex.
Parameters
----------
series_id : str
FRED series identifier, e.g. 'GDPC1'.
start : str
ISO date string for the start of the sample.
end : str or None
ISO date string for the end of the sample. Defaults to today.
frequency : str or None
Aggregation frequency override: 'a', 'q', 'm', 'w', 'd'.
Pass None to keep the native frequency.
units : str
Transformation: 'lin' (levels), 'chg', 'ch1', 'pch', 'pc1',
'pca', 'cch', 'cca', 'log'.
Returns
-------
pd.Series
Named after series_id with a DatetimeIndex.
"""
kwargs = {
"observation_start": start,
"units": units,
}
if end:
kwargs["observation_end"] = end
if frequency:
kwargs["frequency"] = frequency
series = fred.get_series(series_id, **kwargs)
series.name = series_id
series.index = pd.to_datetime(series.index)
return series.dropna()
defget_recession_shading(start: str = "1950-01-01", end: str | None = None) -> pd.Series:
"""
Return the NBER recession indicator (USREC) as a boolean Series.
Value is 1 during recession months, 0 otherwise.
"""
rec = get_fred_series("USREC", start=start, end=end)
return rec.astype(bool)
2.2 Growth Rates
defcalculate_growth_rates(series: pd.Series, freq: str = "YoY") -> pd.Series:
"""
Compute period-over-period growth rates.
Parameters
----------
series : pd.Series
Level series with DatetimeIndex.
freq : str
'MoM' — month-over-month % change
'YoY' — year-over-year % change
'QoQ' — quarter-over-quarter % change (annualised if series is quarterly)
Returns
-------
pd.Series
Percentage change series, same index as input.
"""
freq = freq.upper()
if freq == "MOM":
return series.pct_change(1) * 100elif freq == "YOY":
periods = 4if series.index.inferred_freq in ("QS", "Q", "QE") else12return series.pct_change(periods) * 100elif freq == "QOQ":
return series.pct_change(1) * 100else:
raise ValueError(f"Unknown freq '{freq}'. Choose MoM, YoY, or QoQ.")
2.3 Hodrick-Prescott Decomposition
defhp_decompose(series: pd.Series, lamb: int | None = None) -> pd.DataFrame:
"""
Apply the Hodrick-Prescott filter to separate trend from cycle.
Parameters
----------
series : pd.Series
Macroeconomic level series.
lamb : int or None
Smoothing parameter. Defaults: 1600 for quarterly, 129600 for monthly,
6.25 for annual (Ravn-Uhlig rule).
Returns
-------
pd.DataFrame
Columns: ['original', 'trend', 'cycle']
"""if lamb isNone:
freq = pd.infer_freq(series.index)
if freq and freq.startswith("Q"):
lamb = 1600elif freq and freq.startswith("M"):
lamb = 129600else:
lamb = 6.25# annual
cycle, trend = hpfilter(series.dropna(), lamb=lamb)
return pd.DataFrame(
{"original": series, "trend": trend, "cycle": cycle},
index=series.index,
)
2.4 Build a Macro Panel
defbuild_macro_panel(
series_dict: dict[str, str],
start: str = "2000-01-01",
end: str | None = None,
frequency: str = "m",
) -> pd.DataFrame:
"""
Fetch multiple FRED series and combine into a wide DataFrame.
Parameters
----------
series_dict : dict
Mapping of column_name -> FRED series_id.
start, end : str
Sample bounds.
frequency : str
Target frequency for all series ('m', 'q', 'a').
Returns
-------
pd.DataFrame
Wide DataFrame; rows are dates, columns are variable names.
"""
frames = {}
for name, sid in series_dict.items():
try:
frames[name] = get_fred_series(sid, start=start, end=end, frequency=frequency)
except Exception as exc:
print(f"Warning: could not fetch {sid} ({exc})")
return pd.DataFrame(frames)
2.5 Recession-Shaded Plot
defplot_with_recession_shading(
df: pd.DataFrame,
series_col: str,
title: str,
ylabel: str = "",
color: str = "steelblue",
figsize: tuple = (12, 4),
) -> plt.Figure:
"""
Plot a time series with NBER recession shading.
Parameters
----------
df : pd.DataFrame
Must contain `series_col` and optionally 'recession' column.
series_col : str
Column name to plot.
title : str
Chart title.
ylabel : str
Y-axis label.
color : str
Line color.
figsize : tuple
Figure dimensions.
Returns
-------
matplotlib Figure
"""
fig, ax = plt.subplots(figsize=figsize)
# Draw series
ax.plot(df.index, df[series_col], color=color, linewidth=1.8, label=series_col)
# Recession shadingif"recession"in df.columns:
rec = df["recession"].fillna(0).astype(bool)
in_rec = False
rec_start = Nonefor date, val in rec.items():
if val andnot in_rec:
rec_start = date
in_rec = Trueelifnot val and in_rec:
ax.axvspan(rec_start, date, color="lightgray", alpha=0.7, label="_nolegend_")
in_rec = Falseif in_rec:
ax.axvspan(rec_start, df.index[-1], color="lightgray", alpha=0.7)
ax.axhline(0, color="black", linewidth=0.6, linestyle="--")
ax.set_title(title, fontsize=13, fontweight="bold")
ax.set_ylabel(ylabel)
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
ax.xaxis.set_major_locator(mdates.YearLocator(2))
fig.autofmt_xdate()
ax.legend()
plt.tight_layout()
return fig
3. Business Cycle Dating
defdate_business_cycles(recession_series: pd.Series) -> pd.DataFrame:
"""
Extract recession peak/trough dates from the USREC indicator.
Returns a DataFrame with columns: ['peak', 'trough', 'duration_months'].
"""
rec = recession_series.astype(int)
starts, ends = [], []
in_rec = Falsefor date, val in rec.items():
if val == 1andnot in_rec:
starts.append(date)
in_rec = Trueelif val == 0and in_rec:
ends.append(date)
in_rec = Falseif in_rec:
ends.append(rec.index[-1])
records = []
for s, e inzip(starts, ends):
duration = len(rec.loc[s:e])
records.append({"peak": s, "trough": e, "duration_months": duration})
return pd.DataFrame(records)
4. Export Utilities
defexport_to_excel(df: pd.DataFrame, path: str, sheet_name: str = "FRED Data") -> None:
"""Save DataFrame to Excel with auto-fit column widths."""with pd.ExcelWriter(path, engine="openpyxl", datetime_format="YYYY-MM-DD") as writer:
df.to_excel(writer, sheet_name=sheet_name)
ws = writer.sheets[sheet_name]
for col_cells in ws.columns:
length = max(len(str(cell.value or"")) for cell in col_cells) + 2
ws.column_dimensions[col_cells[0].column_letter].width = min(length, 30)
print(f"Saved to {path}")
defexport_to_csv(df: pd.DataFrame, path: str) -> None:
"""Save DataFrame to CSV."""
df.to_csv(path)
print(f"Saved to {path}")
5. Example A — Yield Curve Inversion Tracker with Recession Overlay
This example plots the 10Y-2Y and 10Y-3M yield spreads, highlights inversion zones, and
overlays NBER recession shading to illustrate the leading indicator property.
import os
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from fredapi import Fred
fred = Fred(api_key=os.environ["FRED_API_KEY"])
# ---- Fetch data ---------------------------------------------------------------
t10y2y = get_fred_series("T10Y2Y", start="1990-01-01", frequency="m")
t10y3m = get_fred_series("T10Y3M", start="1990-01-01", frequency="m")
usrec = get_recession_shading(start="1990-01-01")
panel = pd.DataFrame({"t10y2y": t10y2y, "t10y3m": t10y3m, "recession": usrec}).dropna(subset=["t10y2y"])
# ---- Plot ---------------------------------------------------------------------
fig, ax = plt.subplots(figsize=(14, 5))
ax.plot(panel.index, panel["t10y2y"], label="10Y-2Y Spread", color="navy", linewidth=1.6)
ax.plot(panel.index, panel["t10y3m"], label="10Y-3M Spread", color="crimson", linewidth=1.3, linestyle="--")
ax.axhline(0, color="black", linewidth=0.8)
# Shade inversions (spread < 0)
ax.fill_between(panel.index, panel["t10y2y"], 0,
where=panel["t10y2y"] < 0, alpha=0.25, color="navy", label="Inversion (10Y-2Y)")
# Recession shading
rec = panel["recession"].fillna(False).astype(bool)
in_rec, rec_start = False, Nonefor date, val in rec.items():
if val andnot in_rec:
rec_start, in_rec = date, Trueelifnot val and in_rec:
ax.axvspan(rec_start, date, color="lightgray", alpha=0.6)
in_rec = Falseif in_rec:
ax.axvspan(rec_start, panel.index[-1], color="lightgray", alpha=0.6, label="NBER Recession")
ax.set_title("U.S. Treasury Yield Curve Spreads vs NBER Recessions", fontsize=13, fontweight="bold")
ax.set_ylabel("Percentage Points")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
ax.xaxis.set_major_locator(mdates.YearLocator(4))
fig.autofmt_xdate()
ax.legend(loc="lower left")
plt.tight_layout()
plt.savefig("yield_curve_inversion.png", dpi=150)
plt.show()
# ---- Business cycle table -------------------------------------------------------
cycles = date_business_cycles(usrec.reindex(panel.index).fillna(False))
print(cycles.to_string(index=False))
6. Example B — Inflation Decomposition Dashboard (CPI, PCE, PPI)
Frequency mismatch: FRED series have different native frequencies. Use the
frequency parameter in get_fred_series to aggregate to a common frequency before
merging. Prefer frequency="m" for most macro panels.
Vintage data: fredapi returns the most recent vintage by default. For real-time
analysis use get_series_all_releases or the vintage endpoint.
Units transformation: Passing units="pc1" returns the YoY percent change
directly from FRED, avoiding manual calculation—useful as a sanity check.
Rate limits: The FRED API allows 120 requests per minute. Add time.sleep(0.5)
between large batch fetches.
Recession indicator alignment: USREC is released with a lag. For the most
up-to-date recession dating, cross-check with the NBER website.
HP filter criticism: The HP filter has known endpoint and spurious-cycle issues.
Use the Hamilton (2018) filter (statsmodels.tsa.filters.filtertools) as an
alternative for robustness.