"""fed-funds-vs-cpi-real-rates.py — the effective fed funds rate minus CPI inflation, 1955 to the latest month. Input : datasets/fed-funds-vs-cpi-real-rates.csv (FRED FEDFUNDS monthly %, CPIAUCSL index 1982-84=100), retrieved 2026-09-05 via the public fredgraph CSV. Re-download with --download. Method: cpi_yoy = 100 * (CPI_t / CPI_{t-12} - 1); real = FEDFUNDS - cpi_yoy. This is an ex-post real rate (realized inflation), not a rate against expected inflation. Tables by decade, the longest negative stretches, and the latest month. Run : python code/fed-funds-vs-cpi-real-rates.py [--download] Needs : Python 3.13, pandas 3.0.2. """ import io import sys import urllib.request import pandas as pd CSV = "datasets/fed-funds-vs-cpi-real-rates.csv" def download(): frames = [] for sid in ("FEDFUNDS", "CPIAUCSL"): req = urllib.request.Request(f"https://fred.stlouisfed.org/graph/fredgraph.csv?id={sid}", headers={"User-Agent": "prism-data-lab/1.0"}) txt = urllib.request.urlopen(req, timeout=60).read().decode() frames.append(pd.read_csv(io.StringIO(txt), na_values=".", index_col=0).iloc[:, 0].rename(sid)) df = pd.concat(frames, axis=1) df.index.name = "date" df[df.index >= "1954-07-01"].to_csv(CSV) def main(): if "--download" in sys.argv: download() df = pd.read_csv(CSV, parse_dates=["date"], index_col="date") df["cpi_yoy"] = 100 * (df["CPIAUCSL"] / df["CPIAUCSL"].shift(12) - 1) df["real"] = df["FEDFUNDS"] - df["cpi_yoy"] d = df.dropna(subset=["real"]).copy() d["decade"] = (d.index.year // 10) * 10 print("| decade | months | mean real rate (pp) | min (month) | max (month) | months below zero |") print("|---|---|---|---|---|---|") for dec, s in d.groupby("decade")["real"]: print(f"| {dec}s | {len(s)} | {s.mean():.2f} | {s.min():.2f} ({s.idxmin():%Y-%m}) | " f"{s.max():.2f} ({s.idxmax():%Y-%m}) | {int((s < 0).sum())} |") neg = (d["real"] < 0) grp = (neg != neg.shift()).cumsum() runs = [] for _, x in d[neg].groupby(grp[neg]): runs.append((x.index[0], x.index[-1], len(x))) runs = sorted(runs, key=lambda r: -r[2])[:5] print("\nlongest stretches of a negative real rate:") for s, e, n in runs: print(f" {s:%Y-%m} to {e:%Y-%m}: {n} months") last = d.iloc[-1] print(f"\nlatest month with both series: {d.index[-1]:%Y-%m}: FEDFUNDS {last['FEDFUNDS']:.2f}, " f"CPI y/y {last['cpi_yoy']:.2f}, real {last['real']:.2f}") print(f"whole sample {d.index[0]:%Y-%m} to {d.index[-1]:%Y-%m}: mean real {d['real'].mean():.2f}, " f"median {d['real'].median():.2f}, share of months below zero {100 * (d['real'] < 0).mean():.1f}%") peak = d["cpi_yoy"].idxmax() print(f"peak CPI y/y in sample: {d.loc[peak, 'cpi_yoy']:.2f} in {peak:%Y-%m}; " f"2020s peak: {d.loc['2020':, 'cpi_yoy'].max():.2f} in {d.loc['2020':, 'cpi_yoy'].idxmax():%Y-%m}") lo = d.loc["2020":, "real"] print(f"2020s real-rate low: {lo.min():.2f} in {lo.idxmin():%Y-%m}; first positive month after it: " f"{lo[lo.index > lo.idxmin()][lo[lo.index > lo.idxmin()] > 0].index[0]:%Y-%m}") if __name__ == "__main__": main()