Skip to content

Commit 6b2fdd3

Browse files
Add mkdocs-material docs site scaffold + v0.7.x DiD guides
Introduce a proper documentation site for StatsPAI using mkdocs + mkdocs-material, with a focused v0.7.x pass over the DiD module. Build verified locally: `mkdocs build --clean --strict` succeeds in ~0.2 s with zero warnings. * `mkdocs.yml` — site config (theme, nav, Pygments + MathJax extras). * `docs/index.md` — project-level landing page; lead code block is the one-call `sp.cs_report(..., save_to=...)` pipeline. * `docs/guides/callaway_santanna.md` — from-zero walkthrough of the CS2021 estimator: raw estimation, aggte with uniform bands, RCS mode, R-R sensitivity, pointer to `cs_report()`. * `docs/guides/cs_report.md` — `CSReport` fields and all five export routes (text / markdown / latex / excel / plot / save_to= bundle). * `docs/guides/repeated_cross_sections.md` — MathJax-rendered estimator + influence-function formulas + scope guardrails. * `docs/guides/honest_did.md` — polymorphic usage across CS / SA / BJS / aggte event studies. * `docs/reference/did.md` — API-reference tables for all public DiD entry points. * `docs/changelog.md` — copy of the repo-root CHANGELOG so the site carries its own release history. The new files are force-added because `.gitignore` blanket-excludes `docs/` at line 23 (pre-existing user intent for everything that used to live there). Force-adding only the seven new files lets the published site track them while leaving the blanket rule in place for any future docs/ content the user wants to keep private by default. Full DiD suite unchanged: 161/161 green across 12 test files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2a9c135 commit 6b2fdd3

9 files changed

Lines changed: 498 additions & 0 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,7 @@ StatsPAI/statspai-推文-agent时代的统计包生态.md
171171
test-notebooks/
172172
/Users/brycewang/Documents/GitHub/StatsPAI/test-notebooks/demo_causal_inference.ipynb
173173
test-notebooks/demo_causal_inference.ipynb
174+
175+
# mkdocs build output
176+
/site/
177+

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../CHANGELOG.md

docs/guides/callaway_santanna.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Staggered Difference-in-Differences — Callaway & Sant'Anna (2021)
2+
3+
StatsPAI implements the Callaway–Sant'Anna estimator from first
4+
principles, matching the R `did` package's core functionality while
5+
adding new convenience layers on top.
6+
7+
## Basic usage
8+
9+
```python
10+
import statspai as sp
11+
12+
cs = sp.callaway_santanna(
13+
df,
14+
y='earnings', # outcome
15+
g='first_treat', # first-treatment period (0 = never-treated)
16+
t='year', # time period
17+
i='worker_id', # unit id
18+
estimator='dr', # 'dr' (default), 'ipw', or 'reg'
19+
control_group='nevertreated', # or 'notyettreated'
20+
anticipation=0, # periods of anticipation (CS2021 §3.2)
21+
)
22+
23+
print(cs.summary())
24+
cs.detail # one row per (group, time) with ATT + pointwise CI
25+
cs.model_info['event_study'] # event-study aggregation
26+
cs.model_info['pretrend_test'] # joint Wald pre-trend test
27+
```
28+
29+
## Aggregation with uniform bands
30+
31+
The raw `callaway_santanna()` result is a grid of ATT(g, t) estimates.
32+
Collapse to a scalar or an event-study curve with `aggte()`, which
33+
layers the Mammen (1993) multiplier bootstrap on top and returns
34+
*simultaneous* confidence bands:
35+
36+
```python
37+
es = sp.aggte(cs, type='dynamic',
38+
n_boot=500, random_state=0,
39+
balance_e=3) # balance across cohorts for e ≤ 3
40+
41+
print(es.detail)
42+
# relative_time att se ci_lower ci_upper cband_lower cband_upper ...
43+
```
44+
45+
The `cband_lower` / `cband_upper` columns give a sup-t uniform band —
46+
valid for simultaneous inference across the entire event window,
47+
unlike the pointwise CI.
48+
49+
Other aggregation types:
50+
51+
| `type=` | Meaning |
52+
| --- | --- |
53+
| `'simple'` | cohort-share-weighted overall ATT |
54+
| `'dynamic'` | event-study curve ATT(e) |
55+
| `'group'` | per-cohort average ATT(g) |
56+
| `'calendar'` | per-calendar-time ATT(t) |
57+
58+
## Repeated cross-sections
59+
60+
Pass `panel=False` when observations are not matched across time
61+
(e.g. CPS pooled cross-sections). The estimator switches to the
62+
unconditional 2×2 cell-mean DID with observation-level influence
63+
functions; downstream `aggte`, `cs_report`, `ggdid`, and `honest_did`
64+
all work unchanged.
65+
66+
```python
67+
cs_rcs = sp.callaway_santanna(
68+
survey_df,
69+
y='wage', g='first_treat', t='year', i='respondent_id',
70+
estimator='reg', # only 'reg' supported in RCS mode
71+
x=['age', 'education'], # optional covariate residualisation
72+
panel=False,
73+
)
74+
```
75+
76+
## Sensitivity — Rambachan & Roth (2023)
77+
78+
Every event-study result (from CS, SA, BJS, or `aggte`) feeds into
79+
the Rambachan–Roth sensitivity framework:
80+
81+
```python
82+
sens = sp.honest_did(es, e=2) # robust CI at e=2 across an M grid
83+
m_star = sp.breakdown_m(es, e=2) # largest M* under which effect is significant
84+
```
85+
86+
## One-call report
87+
88+
For a ready-to-publish summary — raw estimation + four aggregations
89+
with uniform bands + pre-trend Wald + R-R breakdown M\* per post
90+
event time — call [`cs_report()`](cs_report.md).

docs/guides/cs_report.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# `cs_report()` — the one-call report card
2+
3+
`sp.cs_report()` runs the full Callaway–Sant'Anna workflow under a
4+
single random seed and bundles the outputs into a `CSReport`
5+
dataclass that pretty-prints, plots, and exports.
6+
7+
## Minimal example
8+
9+
```python
10+
import statspai as sp
11+
12+
rpt = sp.cs_report(
13+
df,
14+
y='y', g='g', t='t', i='id',
15+
n_boot=500,
16+
random_state=0,
17+
verbose=True, # prints the report to stdout
18+
)
19+
```
20+
21+
## Structured fields
22+
23+
```python
24+
rpt.overall # dict: overall ATT, SE, CI, p
25+
rpt.simple # DataFrame: simple aggregation
26+
rpt.dynamic # DataFrame: event study with uniform bands
27+
rpt.group # DataFrame: per-cohort θ(g)
28+
rpt.calendar # DataFrame: per-calendar-time θ(t)
29+
rpt.pretrend # dict: χ² pre-trend Wald test
30+
rpt.breakdown # DataFrame: R-R breakdown M* per post event time
31+
rpt.meta # dict: run metadata (n_units, estimator, …)
32+
```
33+
34+
## Export formats
35+
36+
```python
37+
rpt.to_text() # fixed-width ASCII
38+
rpt.to_markdown() # GitHub-flavoured Markdown (floatfmt configurable)
39+
rpt.to_latex() # booktabs LaTeX fragment (no jinja2 needed)
40+
rpt.to_excel('out.xlsx') # six-sheet workbook
41+
rpt.plot() # 2×2 summary figure via matplotlib
42+
```
43+
44+
### One-call bundle
45+
46+
Pass `save_to='prefix'` to emit every format in one go:
47+
48+
```python
49+
sp.cs_report(
50+
df, y='y', g='g', t='t', i='id',
51+
n_boot=500, random_state=0,
52+
save_to='~/studies/cs_v1',
53+
)
54+
# writes:
55+
# ~/studies/cs_v1.txt .md .tex .xlsx .png
56+
```
57+
58+
Missing parent directories are created on the fly; optional
59+
dependencies (`openpyxl`, `matplotlib`) are skipped silently.
60+
61+
## From a pre-fitted result
62+
63+
Skip re-running estimation if you already have a
64+
`callaway_santanna()` result:
65+
66+
```python
67+
cs = sp.callaway_santanna(df, ...)
68+
rpt = sp.cs_report(cs, n_boot=500, random_state=0)
69+
```

docs/guides/honest_did.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Sensitivity to parallel trends — Rambachan & Roth (2023)
2+
3+
Parallel trends is an assumption, not a fact. Rambachan & Roth
4+
(2023) answer the practical question: *how wrong could parallel
5+
trends be before my post-treatment conclusion flips?* StatsPAI wires
6+
their tooling directly into every event-study object in the library.
7+
8+
## Functions
9+
10+
| Function | Purpose |
11+
| --- | --- |
12+
| `sp.honest_did(result, e, m_grid=None, method='smoothness')` | Robust CI at event time `e` across a grid of violation magnitudes `M` |
13+
| `sp.breakdown_m(result, e, method='smoothness')` | Largest `M*` at which the effect remains significant |
14+
15+
Both functions are *polymorphic*: they accept
16+
17+
- a `callaway_santanna()` result (event study in `model_info`),
18+
- a `sun_abraham()` result,
19+
- or an `aggte(type='dynamic')` result (event study in `detail` with
20+
Mammen uniform bands).
21+
22+
## Typical pipeline
23+
24+
```python
25+
cs = sp.callaway_santanna(df, y='y', g='g', t='t', i='id')
26+
es = sp.aggte(cs, type='dynamic', n_boot=500, random_state=0)
27+
s = sp.honest_did(es, e=1) # robust CI table across M grid
28+
mst = sp.breakdown_m(es, e=1) # scalar breakdown value
29+
```
30+
31+
## Restriction types
32+
33+
- `method='smoothness'` — bounds $|\Delta \delta_t| \le M$ for every
34+
consecutive pair of periods (Δ^SD(M)). The `M` parameter has units
35+
of the outcome per period.
36+
- `method='relative_magnitude'` — post-period violation bounded as
37+
`M̄ × max|δ_pre|`. Useful when you want to express the sensitivity
38+
relative to the magnitude of the observed pre-trend violations.
39+
40+
## One-shot via `cs_report()`
41+
42+
The [`cs_report`](cs_report.md) report card already runs
43+
`breakdown_m` at every post-treatment event time and stores the
44+
results in `rpt.breakdown`. Column `robust_at_1_SE` flags whether
45+
the effect survives a violation equal to one pointwise standard error.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Repeated cross-sections (`panel=False`)
2+
3+
Classic Callaway–Sant'Anna requires a balanced panel: every unit
4+
observed in every period. Many causal-inference datasets are
5+
*repeated cross-sections* (RCS) — pooled surveys (CPS, ACS), rolling
6+
polls, independent samples per period — where no within-unit first
7+
difference is available. StatsPAI's RCS path solves this with the
8+
unconditional 2×2 cell-mean DID.
9+
10+
## Estimator
11+
12+
For each (g, t, base) triple with never-treated control c = 0:
13+
14+
$$
15+
\widehat{\text{ATT}}(g, t)
16+
= (\bar{Y}_{g, t} - \bar{Y}_{g, b})
17+
- (\bar{Y}_{c, t} - \bar{Y}_{c, b})
18+
$$
19+
20+
and observation-level influence functions
21+
22+
$$
23+
\psi_i
24+
= \frac{\mathbf{1}\{G_i=g,\,T_i=t\}(Y_i - \bar{Y}_{g,t})}{p_{g,t}}
25+
- \frac{\mathbf{1}\{G_i=g,\,T_i=b\}(Y_i - \bar{Y}_{g,b})}{p_{g,b}}
26+
- \frac{\mathbf{1}\{G_i=c,\,T_i=t\}(Y_i - \bar{Y}_{c,t})}{p_{c,t}}
27+
+ \frac{\mathbf{1}\{G_i=c,\,T_i=b\}(Y_i - \bar{Y}_{c,b})}{p_{c,b}}
28+
$$
29+
30+
where $p_{\cdot,\cdot}$ is the empirical cell share. The influence
31+
functions are stacked into the same `inf_matrix` contract used by the
32+
panel estimator, so `aggte`, `cs_report`, `ggdid`, and `honest_did`
33+
work downstream without modification.
34+
35+
## Usage
36+
37+
```python
38+
sp.callaway_santanna(
39+
df, y='y', g='first_treat', t='year', i='obs_id',
40+
estimator='reg', # required for panel=False
41+
panel=False,
42+
)
43+
```
44+
45+
## Covariate adjustment
46+
47+
Add `x=[...]` for regression-adjusted RCS. Y is residualised on X
48+
using OLS fit on the never-treated pool (with period fixed effects);
49+
the cell-mean DID then runs on the residualised outcome:
50+
51+
```python
52+
sp.callaway_santanna(
53+
survey_df, y='wage', g='g', t='year', i='obs',
54+
estimator='reg', panel=False,
55+
x=['age', 'education', 'female'],
56+
)
57+
```
58+
59+
Influence functions treat β̂ as known (plug-in IF), which is
60+
asymptotically valid at √n. A fully-coupled Sant'Anna–Zhao (2020)
61+
IF augmentation is planned.
62+
63+
## Scope of the current implementation
64+
65+
- `estimator` must be `'reg'`
66+
- `control_group` must be `'nevertreated'`
67+
- IPW / DR for RCS: planned for a future release
68+
69+
All other paths raise `NotImplementedError` with an actionable message.

docs/index.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# StatsPAI
2+
3+
**Python causal inference toolkit** — reimplemented from the original
4+
papers, zero upstream DID-package runtime dependencies.
5+
6+
```python
7+
import statspai as sp
8+
9+
rpt = sp.cs_report(
10+
data, y='y', g='g', t='t', i='id',
11+
n_boot=500, random_state=0,
12+
save_to='~/study/cs_v1', # writes .txt / .md / .tex / .xlsx / .png
13+
)
14+
```
15+
16+
## What's inside
17+
18+
StatsPAI brings the R `did` / `HonestDiD` + Python `csdid` / `differences`
19+
stack into a single consistent Python API, with every core algorithm
20+
reimplemented from the original paper (no wrappers).
21+
22+
### Staggered Difference-in-Differences
23+
24+
- **Callaway & Sant'Anna (2021)**`sp.callaway_santanna()` with
25+
DR / IPW / REG estimators, never-treated or not-yet-treated control,
26+
`anticipation=δ`, and `panel=False` for repeated cross-sections
27+
(with optional covariate regression adjustment).
28+
- **Four aggregations + Mammen uniform bands**`sp.aggte()` with
29+
`type='simple' | 'dynamic' | 'group' | 'calendar'` and simultaneous
30+
sup-t confidence bands (Callaway–Sant'Anna 2021 §4.2).
31+
- **Sensitivity analysis**`sp.honest_did()` and `sp.breakdown_m()`
32+
(Rambachan–Roth 2023). Both accept either a `callaway_santanna()`
33+
result or an `aggte(type='dynamic')` result.
34+
- **One-call report**`sp.cs_report()` runs the full pipeline under a
35+
single seed and produces a structured `CSReport` with Markdown /
36+
LaTeX / Excel export and a 2×2 summary figure (`.plot()`).
37+
- **Sun & Abraham (2021) IW** with Liang–Zeger cluster-robust SEs.
38+
- **Borusyak–Jaravel–Spiess (2024)** imputation estimator +
39+
`sp.bjs_pretrend_joint()` cluster-bootstrap joint Wald pre-trend test.
40+
- **de Chaisemartin–D'Haultfoeuille** with joint placebo Wald and
41+
average cumulative effect (dCDH 2024).
42+
43+
### Broader toolkit
44+
45+
OLS / GLM / quantile / survival / panel / spatial / RD / IV / matching /
46+
synthetic control / DML / DeepIV / neural causal / causal forests /
47+
policy learning / conformal causal / TMLE / Mendelian randomization /
48+
sensitivity analysis / reporting via `outreg2` + `modelsummary`.
49+
50+
See the left-hand navigation for guides.
51+
52+
## Installation
53+
54+
```bash
55+
pip install statspai
56+
```
57+
58+
Optional extras for exports:
59+
60+
```bash
61+
pip install openpyxl matplotlib # Excel + figures
62+
```
63+
64+
## Citation
65+
66+
If you use StatsPAI in research, please cite the underlying papers
67+
implemented by each estimator (every `CausalResult` carries a
68+
`.cite()` method that returns the correct BibTeX entry) and this
69+
package:
70+
71+
```bibtex
72+
@software{statspai,
73+
author = {Wang, Bryce},
74+
title = {StatsPAI: Python Causal Inference Toolkit},
75+
year = {2026},
76+
url = {https://github.com/brycewang-stanford/StatsPAI}
77+
}
78+
```

0 commit comments

Comments
 (0)