From 175e9ba4e201aaf43ae70fdb28943e3eed8fa5f0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 30 May 2025 16:02:18 -0400 Subject: [PATCH] Vendored wheels and FRED snapshot --- .gitignore | 14 +- repo-context.txt | 2 +- requirements.txt | 2 +- scripts/lagged_oil_unrate_chart_styled.py | 183 ++++++++++++++++------ 4 files changed, 141 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 4626bb5..ad0b2e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,9 @@ -# Python ### +# Byte-code / venv __pycache__/ -*.py[cod] -*$py.class - -# Virtualenv ### venv/ -# Data & outputs ### -data/ -outputs/ +# Local data & wheels +wheelhouse/*.log +wheelhouse/*.md +# keep wheel files themselves! +data/*.bak diff --git a/repo-context.txt b/repo-context.txt index 200e6d0..d08967f 100644 --- a/repo-context.txt +++ b/repo-context.txt @@ -143,7 +143,7 @@ This project is released under the MIT License. ## python_charts/requirements.txt ``` pandas>=1.5.0 -pandas\_datareader>=0.10.0 +pandas_datareader>=0.10.0 # good matplotlib>=3.5.0 lxml>=4.6.0 python-dateutil>=2.8.1 diff --git a/requirements.txt b/requirements.txt index 3033efc..04e6025 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pandas>=1.5.0 -pandas_datareader>=0.10.0 # good +pandas_datareader>=0.10.0 matplotlib>=3.5.0 lxml>=4.6.0 python-dateutil>=2.8.1 diff --git a/scripts/lagged_oil_unrate_chart_styled.py b/scripts/lagged_oil_unrate_chart_styled.py index 3dd1bc3..49dea8b 100644 --- a/scripts/lagged_oil_unrate_chart_styled.py +++ b/scripts/lagged_oil_unrate_chart_styled.py @@ -2,119 +2,202 @@ """ lagged_oil_unrate_chart_styled.py --------------------------------- -Fetch and plot UNRATE vs. WTI oil price with a configurable lag, -date range, and polished dual-axis styling. +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 lagged_oil_unrate_chart_styled.py \ - --offset 18 \ - --start 1973-01-01 \ - --end 2025-05-31 \ - --extend-years 3 - -Dependencies: - pip install pandas pandas_datareader matplotlib python-dateutil +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 dateutil.relativedelta import relativedelta +from pathlib import Path import matplotlib.dates as mdates import matplotlib.pyplot as plt import pandas as pd -import pandas_datareader.data as web +from dateutil.relativedelta import relativedelta +import sqlite3 -def fetch_series(start, end): - # FRED series - unrate = web.DataReader("UNRATE", "fred", start, end) - oil = web.DataReader("DCOILWTICO", "fred", start, end) +# ────────────────────────────────────────────────────────────────────────── +# 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 timestamps, too + # 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 -): - # Shift oil by offset: positive = lag; negative = lead + extend_years: int, +) -> None: + """Render the dual-axis chart.""" oil_shifted = oil.shift(offset_months) - # Build figure 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 + y=0.93, ) - # Plot UNRATE on left axis - ax1.plot(unrate.index, unrate["UNRATE"], - label="UNRATE (LHS)", linewidth=2.5, color="#1f77b4") + # 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(0, unrate["UNRATE"].max() * 1.1) + ax1.set_ylim(0, unrate["value"].max() * 1.1) ax1.yaxis.set_major_formatter("{x:.0f}%") ax1.grid(axis="y", linestyle="--", alpha=0.4) - # Plot shifted oil on right axis + # Right axis – shifted oil ax2 = ax1.twinx() - ax2.plot(oil_shifted.index, oil_shifted["DCOILWTICO"], - label="USOIL (RHS)", linewidth=2, linestyle="-", alpha=0.8, color="#FF9900") + 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) - ax2.set_ylim(0, oil_shifted["DCOILWTICO"].max() * 1.1) + ax2.set_ylim(0, oil_shifted["value"].max() * 1.1) ax2.yaxis.set_major_formatter("${x:,.0f}") - # X-axis formatting + # X-axis 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) - # Combine legends + # 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) - # Footnotes + # Footnote foot = ( - f"Dates: {start_date.strftime('%Y')} through {unrate.index[-1].strftime('%d %b %Y')}.\n" - "Source: FRED, pandas_datareader." + f"Dates: {start_date.strftime('%Y')} through " + f"{unrate.index[-1].strftime('%d %b %Y')}.\n" + "Source: Local FRED snapshot (offline)." ) plt.annotate( foot, - (0,0), (0, -40), - xycoords="axes fraction", textcoords="offset points", - va="top", fontsize=10, color="gray" + (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() -if __name__ == "__main__": +# ────────────────────────────────────────────────────────────────────────── +# 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( + "-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) + end_dt = datetime.fromisoformat(args.end) - unrate, oil = fetch_series(start_dt, end_dt) + 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()