Add comment about distutils shim
Python Charts 
This initial chart is a self‐contained toolkit that:
- Fetches U.S. unemployment rate (UNRATE) and WTI crude oil price (DCOILWTICO) from FRED :contentReference[oaicite:1]{index=1}
- Stores the series in a local SQLite database (
data/fred.db) :contentReference[oaicite:2]{index=2} - Produces a dual‐axis plot of unemployment vs. lagged oil price, with customizable lag, date range, and styling :contentReference[oaicite:3]{index=3}
Table of Contents
🚀 Quickstart
# Clone the repo
git clone https://github.com/your-org/python_charts.git
cd python_charts
# Create and activate your virtual environment
python3 -m venv venv
source venv/bin/activate
# Plot the lagged UNRATE vs. Oil chart
./startup.sh lagged_oil_unrate --offset 12 --end 2025-05-31 --extend-years 5
This will:
- Upgrade
pip&setuptoolsglobally - Install required packages from
requirements.txtautomatically - Download UNRATE & DCOILWTICO into
data/fred.dbif missing ([Stack Overflow][3], [martinheinz.dev][4]) - Generate the chart with a 12‐month oil lead, extending the x‐axis by 5 years ([martinheinz.dev][4], [Real Python][1])
📁 Repository Structure
python_charts/
├── data/
│ └── fred.db # Local FRED snapshot (SQLite)
├── scripts/
│ ├── __init__.py # Marks this folder as a Python package
│ ├── common.py # Data‐loading, validation, and save_figure utils
│ ├── refresh_data.py # Downloads and stores FRED series
│ ├── lagged_oil_unrate_chart_styled.py # Offline plot of UNRATE vs. WTI oil with lag
│ ├── bitcoin_m2_chart.py # Plot Bitcoin vs. Global M2 overlay
│ └── custom_chart.py # Generic plotting for arbitrary FRED series
├── outputs/ # Generated chart files (PNGs)
├── Makefile # High‐level targets (install, data, plot, etc.)
├── startup.sh # Installs dependencies, ensures data, runs commands
├── requirements.txt # Python dependencies
└── README.md # This file
Note: An empty
scripts/__init__.pyfile has been added so that Python treatsscripts/as a package. Because of this, each script can importcommon.pyvia:from scripts.common import fetch_series_db, save_figure, validate_dataframe, validate_overlaprather than
from common import …. ([Real Python][1], [Software Engineering Stack Exchange][2])
⚙️ Usage
1. Refresh FRED Data
To refresh (or initially populate) the SQLite database with FRED series, run:
# Either let startup.sh dispatch via alias:
./startup.sh refresh_data --series UNRATE CPIAUCSL --start 1960-01-01 --output outputs/chart.png
# Or invoke it directly as a module (dependencies must already be installed):
python -m scripts.refresh_data --series UNRATE CPIAUCSL --start 1960-01-01 --output outputs/chart.png
This command:
- Connects to FRED via
pandas_datareader - Downloads the specified series (e.g.,
UNRATEandCPIAUCSL) from the start date through today - Writes each series to its own table in
data/fred.db - Saves a PNG of any default plot (if
--outputis provided) tooutputs/([Stack Overflow][3], [GitHub][5])
refresh_data.py options
| Option | Description |
|---|---|
--series |
One or more FRED series names (e.g., UNRATE, DCOILWTICO) |
--start |
Start date in YYYY-MM-DD format |
--end |
End date in YYYY-MM-DD format (defaults to today) |
Example:
# Populate fred.db with four series (UNRATE, WTI, Bitcoin, and Global M2)
./startup.sh refresh_data --series UNRATE DCOILWTICO CBBTCUSD GLOBAL_M2 --start 1973-01-01
2. Generate the Default UNRATE vs. Oil Chart
Once data is present, generate the dual‐axis plot of UNRATE vs. lagged WTI oil price:
# Default: 18‐month lag, default date range (1973‐01‐01 to today), and 3‐year extension
./startup.sh lagged_oil_unrate --offset 18
What happens under the hood:
-
startup.shsees--offset 18and forwards it to the default modulescripts.lagged_oil_unrate_chart_styledas:python -m scripts.lagged_oil_unrate_chart_styled --offset 18 -
The script imports
UNRATEandDCOILWTICOfromfred.dbvia:from scripts.common import fetch_series_db, save_figure, validate_dataframe, validate_overlap([Real Python][1], [Software Engineering Stack Exchange][2])
-
Converts both series to month-end indices, resamples oil to a monthly average, and aligns their date ranges.
-
Clamps both series to the last month where
UNRATEhas a value so any trailing NaN is removed. -
Validates that neither series is empty, contains
NaN, or has no overlap (raising aValueErrorif any issue). -
Applies the specified lag (e.g., 18 months), then plots UNRATE on the left y-axis and log-scaled WTI on the right y-axis.
-
Saves the figure to
outputs/lagged_oil_unrate_chart_styled_<timestamp>.png(unless--outputis specified). ([martinheinz.dev][4], [Real Python][1])
Customizing the UNRATE vs. Oil Chart
# 6‐month lag, start in 1980, end in 2025‐05‐31, extend by 2 years
./startup.sh lagged_oil_unrate --offset 6 --start 1980-01-01 --end 2025-05-31 --extend-years 2
| Option | Description |
|---|---|
--offset |
Lag in months (positive means oil is shifted forward relative to UNRATE) |
--start |
Start date for both series (YYYY-MM-DD) |
--end |
End date for both series (YYYY-MM-DD) |
--extend-years |
Number of years to extend the x‐axis beyond the end date |
--output |
Save the figure to this path (PNG or PDF) |
Note: If you do not specify
--output,save_figure(...)creates a timestamped PNG inoutputs/by default:outputs/lagged_oil_unrate_chart_styled_20250531_121045.png([Python Packaging][6], [Real Python][1])
Custom Charts
You can plot any combination of FRED series stored in data/fred.db using scripts/custom_chart.py. Each column in the DataFrame is plotted on a shared axis.
# Either let startup.sh dispatch via alias:
./startup.sh custom_chart --series UNRATE DCOILWTICO --start 2000-01-01 --end 2020-12-31
# Or invoke it directly as a module (dependencies must already be installed):
python -m scripts.custom_chart --series UNRATE DCOILWTICO --start 2000-01-01 --end 2020-12-31
-
Internally,
custom_chart.pyinvokes:from scripts.common import fetch_series_db, save_figureto load the requested series and save the figure.
-
The same invocation is available via Make:
make plot-custom ARGS="--series UNRATE DCOILWTICO --start 2000-01-01 --end 2020-12-31" ``` :contentReference[oaicite:12]{index=12}
Bitcoin vs. Global M2
Before running bitcoin_m2_chart.py, make sure the Bitcoin price
(CBBTCUSD) and global M2 (GLOBAL_M2) series exist in data/fred.db. If the database is missing these tables, fetch them with refresh_data.py:
# Either let startup.sh dispatch via alias:
./startup.sh refresh_data --series CBBTCUSD GLOBAL_M2 --start 2010-01-01
# Or invoke it directly as a module (dependencies must already be installed):
python -m scripts.refresh_data --series CBBTCUSD GLOBAL_M2 --start 2010-01-01
Once the data is present, generate the Bitcoin‐Global M2 overlay chart:
# Either let startup.sh dispatch via alias:
./startup.sh bitcoin_m2 --btc-series CBBTCUSD --m2-series GLOBAL_M2 --output outputs/chart.png
# Or invoke it directly as a module (dependencies must already be installed):
python -m scripts.bitcoin_m2_chart --btc-series CBBTCUSD --m2-series GLOBAL_M2 --output outputs/chart.png
-
This script:
-
Imports both series from
fred.dbwith:from scripts.common import fetch_series_db, save_figure([Real Python][1], [Software Engineering Stack Exchange][2])
-
Renames each column to
"value"for consistency. -
Finds the overlapping date range, shifts M2 by
--offset(default = 94 days), and overlays the two series on dual axes. -
Saves the figure—e.g.,
outputs/bitcoin_m2_chart_20250531_090608.png([martinheinz.dev][4], [Real Python][1])
-
CLI Options for
bitcoin_m2_chart.py:
Option Default Description --btc-seriesCBBTCUSDFRED series code for Bitcoin price --m2-seriesGLOBAL_M2FRED series code for global M2 money supply --start2010-01-01Start date for fetching both series --endToday End date for fetching both series --offset94Shift M2 by this many days (positive → M2 leads) --extend-years1Years beyond endto extend the x‐axis--outputNone Optional path to save the figure (PNG/PDF)
📝 Details
-
Data storage:
data/fred.db(SQLite) contains one table per FRED series—each table has columnsdate(as the index) and<SERIES_NAME>for the value ([Stack Overflow][3], [martinheinz.dev][4]).
-
Plot styling:
-
Lagged oil vs. UNRATE chart:
- Left y‐axis: Unemployment rate (%) from 3% to 15%, with tick marks every 2%
- Right y‐axis: WTI oil price (USD) on a log scale; ticks double starting at $3 (i.e., 3, 6, 12, 24, etc.) ([martinheinz.dev][4], [Real Python][1])
- X‐axis: Major tick every 5 years, minor tick every year; format ticks as
%Y(e.g., 1980, 1985, 1990) ([Real Python][1], [martinheinz.dev][4])
-
Bitcoin vs. Global M2 chart:
- Left y‐axis: Bitcoin price (USD) as a linear scale
- Right y‐axis: Global M2 (billions of USD) as a linear scale
- X‐axis: Same date formatting as above (major = every 5 years, minor = yearly) ([Real Python][1], [martinheinz.dev][4])
-
-
Utility functions in
common.py:-
fetch_series_db(series_list, start, end, db_path)- Opens
data/fred.db, reads each series table, slices by date range, and concatenates into a single DataFrame, sorted by date. RaisesFileNotFoundErroriffred.dbis missing. ([Stack Overflow][3], [martinheinz.dev][4])
- Opens
-
validate_dataframe(df, name)- Confirms the DataFrame is nonempty, has a
"value"column, contains no nulls, and uses aDatetimeIndex. RaisesValueErrorotherwise. ([devgem.io][7], [Real Python][1])
- Confirms the DataFrame is nonempty, has a
-
validate_overlap(df1, df2)- Checks for a nonempty overlap in dates between two series; raises
ValueErrorif there is no intersection. ([devgem.io][7], [Real Python][1])
- Checks for a nonempty overlap in dates between two series; raises
-
save_figure(fig, output, script_path)- Saves the Matplotlib
Figureto either the givenoutputpath oroutputs/<script>_<timestamp>.png. ([Python Packaging][6], [Real Python][1])
- Saves the Matplotlib
-
📦 Dependencies
All Python dependencies are listed in requirements.txt and are installed by ./startup.sh:
pandas>=1.5.0
pandas_datareader>=0.10.0
matplotlib>=3.5.0
lxml>=4.6.0
python-dateutil>=2.8.1
Tip: If you run into
ModuleNotFoundError: distutils, resolve it by upgradingsetuptools:pip install --upgrade setuptools ``` :contentReference[oaicite:23]{index=23}
🛡️ Troubleshooting
-
ModuleNotFoundError:
scripts.commonnot found-
Ensure you have added
scripts/__init__.pyand that allfrom common import …lines have been updated tofrom scripts.common import …. Then rerun:# Run the default lagged-oil chart via startup.sh ./startup.sh lagged_oil_unrate --offset 18or, if you prefer the module form:
python -m scripts.lagged_oil_unrate_chart_styled --offset 18
-
-
ValueError: UNRATE contains null values-
This indicates that the UNRATE DataFrame has missing (NaN) entries. Possible fixes:
- Drop nulls within SQL—e.g., run
refresh_data.py --endusing the last fully published month rather than "today" ([devgem.io][7], [pyOpenSci][8]) - Clamp both series to
UNRATE.last_valid_index()before validation (the approach used inlagged_oil_unrate_chart_styled.py). - Comment out the strict null check in
validate_dataframe(...)(not recommended for publication-quality charts) ([devgem.io][7], [Real Python][1])
- Drop nulls within SQL—e.g., run
-
-
Database not found
-
If
data/fred.dbdoes not exist (e.g., fresh clone), run:# Using startup.sh to run via alias ./startup.sh refresh_data --series UNRATE DCOILWTICO # Or run it as a module directly python -m scripts.refresh_data --series UNRATE DCOILWTICO ``` :contentReference[oaicite:28]{index=28}
-
-
Permission errors
- Ensure you have write access to the project directory, especially for
data/andoutputs/. ([Real Python][1], [Roy's Blog][9])
- Ensure you have write access to the project directory, especially for
📄 License
Released under the MIT License. See LICENSE for details. ([Roy's Blog][9], [Real Python][1])