The capstone repository
Fork the template into GitHub Codespaces. Each layer has its own folder, and the README doubles as the executive memo.
causal-policy-capstone/
├── .devcontainer/devcontainer.json # Python + Node pre-installed for Codespaces
├── .github/workflows/ci.yml # tests on every push
├── data/
│ ├── raw/ # untidy source files (git-ignored if large)
│ └── process_data.py # Polars/DuckDB pipeline -> data/clean/panel.parquet
├── notebooks/
│ ├── 01_exploration.ipynb
│ ├── 02_dag_and_identification.ipynb
│ └── 03_estimation_and_sensitivity.ipynb
├── backend/
│ ├── app.py # FastAPI decision engine (Lesson 15.1)
│ ├── requirements.txt
│ └── models/cate_model.joblib
├── frontend/ # Next.js dashboard (Lesson 15.2)
├── tests/
│ ├── test_data_pipeline.py
│ └── test_api.py
└── README.md # the executive memo
Testing the data pipeline
Upstream data change without warning: a renamed column, a new province code, a missing month. A pipeline test fails loudly before a broken table reaches the estimator.
%%writefile data/process_data.py
"""Build the tidy modelling panel. Run: python data/process_data.py"""
from pathlib import Path
import duckdb
import numpy as np
import polars as pl
OUT = Path(__file__).parent / "clean" / "panel.parquet"
def raw_inputs(seed: int = 1) -> tuple[pl.DataFrame, pl.DataFrame]:
rng = np.random.default_rng(seed)
months = pl.date_range(pl.date(2019, 1, 1), pl.date(2023, 12, 1), "1mo", eager=True)
wide = pl.DataFrame({"month": months} | {p: rng.normal(6, 0.5, len(months)) for p in ["ON", "QC", "AB"]})
policy = pl.DataFrame({"province": ["ON", "QC", "AB"], "policy_start": [None, "2021-01-01", "2022-07-01"]})
return wide, policy
def build_panel(wide: pl.DataFrame, policy: pl.DataFrame) -> pl.DataFrame:
long = wide.unpivot(index="month", variable_name="province", value_name="unemp_rate")
return duckdb.sql("""
SELECT l.month, l.province, l.unemp_rate,
CASE WHEN p.policy_start IS NOT NULL AND l.month >= CAST(p.policy_start AS DATE) THEN 1 ELSE 0 END AS treated
FROM long l LEFT JOIN policy p USING (province)
ORDER BY province, month
""").pl()
if __name__ == "__main__":
OUT.parent.mkdir(parents=True, exist_ok=True)
build_panel(*raw_inputs()).write_parquet(OUT)
print(f"Wrote {OUT}")
%%writefile tests/test_data_pipeline.py
import polars as pl
from data.process_data import build_panel, raw_inputs
def test_panel_is_tidy_and_complete():
panel = build_panel(*raw_inputs())
assert set(panel.columns) == {"month", "province", "unemp_rate", "treated"}
assert panel.select(pl.struct("province", "month").is_duplicated().any()).item() is False
assert panel.group_by("province").len()["len"].n_unique() == 1 # balanced panel
def test_values_are_plausible():
panel = build_panel(*raw_inputs())
assert panel["unemp_rate"].is_between(0, 40).all()
assert panel["treated"].is_in([0, 1]).all()
assert panel.filter(pl.col("province") == "ON")["treated"].sum() == 0 # Ontario never treated
import pytest
code = pytest.main(["-q", "tests/test_data_pipeline.py"])
assert code == 0
GitHub Actions
Put this file in .github/workflows/ci.yml. On every push and pull request, GitHub starts a clean Ubuntu
machine, installs your dependencies and runs both test suites. A red cross on a commit means something broke.
name: Causal pipeline CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r backend/requirements.txt polars duckdb pytest httpx
- name: Test the data pipeline
run: pytest tests/test_data_pipeline.py -v
- name: Build the model artifact
run: python notebooks/train_model.py # export the training cell of Lesson 15.1 to a script
- name: Test the API
run: pytest tests/test_api.py -v
The executive memo
The README is a two-page memo for a decision-maker. Lead with the decision, not the method.
Executive memo template
- Decision and bottom line (3 sentences): what to do, the expected effect in dollars or percentage points, and for whom.
- Policy context: the intervention, the population, the alternative it is compared with.
- How we know (non-technical): the design (randomised, IV, RDD, DiD, SCM or selection on observables) and the one assumption it rests on, in plain words, with the DAG as a figure.
- Impact estimates: say whether each number is an ATE (everyone), an ATT (participants), a LATE (people moved by the instrument or the cutoff) or HTE/CATE (subgroups). Give intervals.
- Targeting and value: the recommended rule, its cost and expected net benefit versus treating everyone and treating no one (Module 8).
- Robustness and limits: sensitivity results (Module 14), possible spillovers (Module 13), what would change the recommendation.
- Deployment: how the model is served, how often it is retrained, and how decisions are logged.
# Retraining Subsidy Decision Engine
**Recommendation.** Offer the retraining subsidy to applicants unemployed for more than 9 months with fewer
than 13 years of schooling. For this group the subsidy raises annual earnings by about $2,100
(95% CI $1,500–$2,700), against a cost of $1,200, a net gain of roughly $900 per participant.
Offering it to everyone would yield a net gain of only about $100 per participant.
**How we know.** Participation was not random, so we compared participants with non-participants who had
the same unemployment history, education and poverty index, using double machine learning. The key
assumption is that these records capture the reasons people joined. An unmeasured factor would need to be
three times stronger than unemployment duration to overturn the result.
Rubric self-assessment
| Area | Weight | You can show that… |
|---|---|---|
| Data engineering and automation | 20% | the pipeline builds a tidy panel with Polars/DuckDB; the repo runs in a Codespace; CI tests run on every push |
| Methodological and structural rigour | 30% | the DAG justifies controls, instruments or the design; key assumptions are tested (balance, overlap, pre-trends, density); sensitivity or bounds are reported |
| Causal ML execution | 25% | DoubleML / EconML are used correctly with cross-fitting; nuisance learners are compared and tuned; effects have valid intervals |
| Full-stack executive deployment | 25% | the model is served by a tested FastAPI endpoint; the Next.js dashboard is usable by a non-specialist; the README memo translates estimates into a decision |
Before submitting, ask a classmate who has not seen your project to read only the first paragraph of your memo and say what they would do. If their answer differs from your recommendation, rewrite the paragraph.
Checklist and recap
- My repository follows the template and runs from a fresh Codespace.
- Data-pipeline and API tests run automatically in GitHub Actions.
- My README is a two-page memo that leads with the decision and names the estimand of every number.
- I have scored my project against the rubric.