#!/usr/bin/env python3 """ lagged_oil_unrate_chart_styled.py --------------------------------- Offline version: reads UNRATE & WTI series from data/fred.db (populated by scripts/refresh_data.py) and plots them with a configurable lag. Usage ----- python scripts/lagged_oil_unrate_chart_styled.py \ --offset 18 \ --start 1973-01-01 \ --end 2025-05-31 \ --extend-years 3 """ from __future__ import annotations import argparse from datetime import datetime from pathlib import Path import matplotlib.dates as mdates import matplotlib.pyplot as plt import matplotlib.ticker as mticker import numpy as np import pandas as pd from dateutil.relativedelta import relativedelta import sqlite3 # ────────────────────────────────────────────────────────────────────────── # Data helpers # ────────────────────────────────────────────────────────────────────────── def fetch_series( start: datetime, end: datetime, db_path: str | Path = Path("data/fred.db"), ) -> tuple[pd.DataFrame, pd.DataFrame]: """ Load UNRATE and DCOILWTICO from a local SQLite DB, then: * convert UNRATE to month-end timestamps * average WTI daily prices to month-end """ db_path = Path(db_path) if not db_path.exists(): raise FileNotFoundError( f"{db_path} not found. Run scripts/refresh_data.py on a " "machine with internet, commit the new DB, then retry." ) with sqlite3.connect(db_path) as conn: unrate = ( pd.read_sql( "SELECT date, UNRATE AS value FROM UNRATE", conn, parse_dates=["date"], index_col="date" ) .loc[start:end] ) oil = ( pd.read_sql( "SELECT date, DCOILWTICO AS value FROM DCOILWTICO", conn, parse_dates=["date"], index_col="date" ) .loc[start:end] ) # Resample WTI to month-end average oil_monthly = oil.resample("M").mean() # Ensure UNRATE is at month-end, too unrate.index = unrate.index.to_period("M").to_timestamp("M") return unrate, oil_monthly # ────────────────────────────────────────────────────────────────────────── # Plotting # ────────────────────────────────────────────────────────────────────────── def plot_lagged( unrate: pd.DataFrame, oil: pd.DataFrame, offset_months: int, start_date: datetime, end_date: datetime, extend_years: int, ) -> None: """Render the dual-axis chart with fixed scales and log ticks on oil.""" oil_shifted = oil.shift(offset_months) fig, ax1 = plt.subplots(figsize=(14, 7)) plt.title("Unemployment Rate and US Oil Price", fontsize=20, weight="bold") plt.suptitle( "Civilian Unemployment Rate and WTI Crude Oil Price", fontsize=14, y=0.93, ) # Left axis – UNRATE ax1.plot( unrate.index, unrate["value"], label="UNRATE (LHS)", linewidth=2.5, color="#1f77b4", ) ax1.set_ylabel("Unemployment rate (%)", fontsize=12) ax1.set_ylim(3, 15) ax1.yaxis.set_major_locator(mticker.MultipleLocator(2)) ax1.yaxis.set_major_formatter("{x:.0f}%") ax1.grid(axis="y", linestyle="--", alpha=0.4) # Right axis – shifted oil (log scale) ax2 = ax1.twinx() ax2.plot( oil_shifted.index, oil_shifted["value"], label="USOIL (RHS)", linewidth=2, linestyle="-", alpha=0.8, color="#FF9900", ) ax2.set_ylabel("Oil price (USD)", fontsize=12) # Build doubling ticks from $3 upward min_tick = 3 max_val = oil_shifted["value"].max() * 1.1 ticks = [] i = 0 while min_tick * 2**i < max_val: ticks.append(min_tick * 2**i) i += 1 ticks.append(min_tick * 2**i) ax2.set_yscale("log") ax2.set_ylim(min_tick, ticks[-1]) ax2.set_yticks(ticks) ax2.yaxis.set_major_formatter("${x:.0f}") # X-axis formatting ax1.set_xlim(start_date, end_date + relativedelta(years=extend_years)) ax1.xaxis.set_major_locator(mdates.YearLocator(base=5)) ax1.xaxis.set_minor_locator(mdates.YearLocator(1)) ax1.xaxis.set_major_formatter(mdates.DateFormatter("%Y")) plt.setp(ax1.get_xticklabels(), rotation=0, fontsize=11) # Legends lines, labels = ax1.get_legend_handles_labels() l2, l2l = ax2.get_legend_handles_labels() ax1.legend(lines + l2, labels + l2l, loc="upper left", frameon=False, fontsize=12) # Footnote foot = ( f"Dates: {start_date.strftime('%Y')} through " f"{unrate.index[-1].strftime('%d %b %Y')}\." "\nSource: Local FRED snapshot (offline)." ) plt.annotate( foot, (0, 0), (0, -40), xycoords="axes fraction", textcoords="offset points", va="top", fontsize=10, color="gray", ) fig.tight_layout(rect=[0, 0.03, 1, 0.95]) plt.show() # ────────────────────────────────────────────────────────────────────────── # CLI # ────────────────────────────────────────────────────────────────────────── def main() -> None: p = argparse.ArgumentParser() p.add_argument( "-o", "--offset", type=int, default=18, help="lag in months (positive=lag, negative=lead)", ) p.add_argument( "--start", type=str, default="1973-01-01", help="series start date (YYYY-MM-DD)", ) p.add_argument( "--end", type=str, default=datetime.today().strftime("%Y-%m-%d"), help="series end date (YYYY-MM-DD)", ) p.add_argument( "--extend-years", type=int, default=3, help="how many years beyond end_date to show on the x-axis", ) p.add_argument( "--db", type=str, default="data/fred.db", help="path to local FRED SQLite file", ) args = p.parse_args() start_dt = datetime.fromisoformat(args.start) end_dt = datetime.fromisoformat(args.end) unrate, oil = fetch_series(start_dt, end_dt, args.db) plot_lagged(unrate, oil, args.offset, start_dt, end_dt, args.extend_years) if __name__ == "__main__": main()