Accelerated Foundation in Applied Causal InferenceFor Economists · 15 modules · 45 lessons

15.3 CI/CD, the Executive Memo and the Capstone Submission

about 2 hoursArchitecture: serving a causal model via FastAPI to a Next.js dashboard
Lesson 15.3 · Module 15
CI/CD, the Executive Memo and the Capstone Submission
Video coming soon. The written lesson and the Colab notebook are ready below.
Colab notebook (.ipynb)
By the end of this lesson you can
  • ✓Automate data-pipeline and API tests with GitHub Actions
  • ✓Structure the capstone repository so anyone can reproduce it
  • ✓Write a two-page executive memo that translates ATE, LATE and HTE into decisions
  • ✓Assess your own capstone against the grading rubric
Lesson plan (120 minutes)
Written lesson

The capstone repository

Fork the template into GitHub Codespaces. Each layer has its own folder, and the README doubles as the executive memo.

Text
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.

Python · Colab cell
%%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}")
Python · Colab cell
%%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
Python · Colab cell
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.

YAML
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.

Key takeaway

Executive memo template

  1. Decision and bottom line (3 sentences): what to do, the expected effect in dollars or percentage points, and for whom.
  2. Policy context: the intervention, the population, the alternative it is compared with.
  3. 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.
  4. 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.
  5. Targeting and value: the recommended rule, its cost and expected net benefit versus treating everyone and treating no one (Module 8).
  6. Robustness and limits: sensitivity results (Module 14), possible spillovers (Module 13), what would change the recommendation.
  7. Deployment: how the model is served, how often it is retrained, and how decisions are logged.
Markdown
# 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

AreaWeightYou can show that…
Data engineering and automation20%the pipeline builds a tidy panel with Polars/DuckDB; the repo runs in a Codespace; CI tests run on every push
Methodological and structural rigour30%the DAG justifies controls, instruments or the design; key assumptions are tested (balance, overlap, pre-trends, density); sensitivity or bounds are reported
Causal ML execution25%DoubleML / EconML are used correctly with cross-fitting; nuisance learners are compared and tuned; effects have valid intervals
Full-stack executive deployment25%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
Exercise

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.