Skip to content

Commit 858d2fd

Browse files
Release v0.4.0: Major module architecture overhaul
- DID: Add DDD, did_analysis() workflow, 8 plot functions - Synth: Modular rewrite with demeaned/robust/gsynth/staggered/conformal variants - Panel: Hausman, BP-LM, Pesaran CD, Wooldridge tests; diagnostic plots - RD: Comprehensive rd_diagnostics() suite - IV: Rewritten with proper first-stage diagnostics and weak IV detection - Matching: Add CEM, optimal matching, genetic matching - DAG: 15+ example DAGs, dag_simulate(), backdoor/frontdoor identification - AI Registry: Expanded function schemas for LLM tool-use - CausalResult: Added .to_json(), .to_dict() 884 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4063b38 commit 858d2fd

34 files changed

Lines changed: 11181 additions & 1021 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,5 @@ Thumbs.db
164164
# Files
165165
Start Superpower for R.md
166166

167-
test-notebooks/
167+
test-notebooks/
168+
StatsPAI/statspai-推文-agent时代的统计包生态.md

README.md

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![Tests](https://github.com/brycewang-stanford/statspai/workflows/CI%2FCD%20Pipeline/badge.svg)](https://github.com/brycewang-stanford/statspai/actions)
77
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/statspai?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/statspai)
88

9-
StatsPAI is a unified Python package for causal inference and applied econometrics. One `import`, 150+ functions, covering the complete empirical research workflow — from classical econometrics to cutting-edge ML/AI causal methods to publication-ready tables in Word, Excel, and LaTeX.
9+
StatsPAI is a unified Python package for causal inference and applied econometrics. One `import`, 170+ functions, covering the complete empirical research workflow — from classical econometrics to cutting-edge ML/AI causal methods to publication-ready tables in Word, Excel, and LaTeX.
1010

1111
It brings R's [Causal Inference Task View](https://cran.r-project.org/web/views/CausalInference.html) (fixest, did, rdrobust, gsynth, DoubleML, MatchIt, CausalImpact, ...) and Stata's core econometrics commands into a single, consistent Python API.
1212

@@ -344,7 +344,7 @@ sp.subgroup_analysis(df, formula="wage ~ education + experience",
344344
## API at a Glance
345345

346346
```text
347-
150+ public functions/classes
347+
170+ public functions/classes
348348
349349
Regression: regress, ivreg, panel, heckman, qreg, sqreg, tobit, xtabond
350350
DID: did, did_2x2, callaway_santanna, sun_abraham, bacon_decomposition, honest_did
@@ -373,6 +373,67 @@ Output: modelsummary, outreg2, sumstats, balance_table, tab, coefplot, b
373373

374374
---
375375

376+
## Release Notes
377+
378+
### v0.4.0 (2026-04-05) — Module Architecture Overhaul
379+
380+
**Major refactoring and expansion of core modules (+5,800 lines of new code):**
381+
382+
- **DID**: Added Triple Differences (`ddd()`), one-call `did_analysis()` workflow (auto design detection → Bacon decomposition → estimation → event study → sensitivity), and 8 publication-ready plot functions (`parallel_trends_plot`, `bacon_plot`, `group_time_plot`, `enhanced_event_study_plot`, `treatment_rollout_plot`, `sensitivity_plot`, `cohort_event_study_plot`)
383+
- **Synthetic Control**: Modular rewrite — `demeaned_synth()`, `robust_synth()` (penalized SCM), `gsynth()` (Generalized SCM with interactive fixed effects), `staggered_synth()` (multi-unit staggered adoption), `conformal_synth()` (distribution-free inference), and comprehensive `synth_plot()` / `synth_weight_plot()` / `synth_gap_plot()`
384+
- **Panel**: Major expansion of `panel()` — Hausman test, Breusch-Pagan LM, Pesaran CD, Wooldridge autocorrelation, panel unit root tests; added `panel_summary_plot()`, `fe_plot()`, `re_comparison_plot()`
385+
- **RD**: New `rd_diagnostics()` suite — bandwidth sensitivity, placebo cutoffs, donut-hole robustness, covariate balance at cutoff, density test
386+
- **IV / 2SLS**: Rewritten `ivreg()` with proper first-stage diagnostics (Cragg-Donald, Kleibergen-Paap), weak IV detection, Sargan-Hansen overidentification test, Anderson canonical correlation test, Stock-Yogo critical values
387+
- **Matching**: Enhanced `match()` — added CEM (Coarsened Exact Matching), optimal matching, genetic matching; improved balance diagnostics with Love plot and standardized mean difference
388+
- **DAG**: Expanded `dag()` with 15+ built-in example DAGs (`dag_example()`), `dag_simulate()` for data generation from causal graphs, backdoor/frontdoor criterion identification
389+
- **Causal Impact**: Enhanced Bayesian structural time-series with automatic model selection and improved inference
390+
- **AI Agent Registry**: Expanded `list_functions()`, `describe_function()`, `function_schema()`, `search_functions()` for LLM/agent tool-use integration
391+
- **CausalResult**: Added `.to_json()`, `.to_dict()`, enhanced `.summary()` formatting
392+
393+
### v0.3.1 (2025-12-20)
394+
395+
- Fix PyPI badge displaying stale version
396+
397+
### v0.3.0 (2025-12-20) — ML & Advanced Causal Methods
398+
399+
- **Meta-Learners**: S/T/X/R/DR-Learner for CATE estimation with `compare_metalearners()` and CATE diagnostics (`gate_test`, `blp_test`)
400+
- **Neural Causal Models**: TARNet, CFRNet, DragonNet for deep CATE estimation
401+
- **Causal Discovery**: `notears()` (continuous DAG optimization), `pc_algorithm()` (constraint-based)
402+
- **TMLE**: Targeted Maximum Likelihood Estimation with Super Learner
403+
- **Policy Learning**: `policy_tree()` optimal treatment rules, `policy_value()` evaluation
404+
- **Conformal Causal**: Distribution-free prediction intervals for ITE
405+
- **Bayesian Causal Forest**: `bcf()` with separate prognostic/treatment functions
406+
- **Dose-Response**: Continuous treatment GPS curves
407+
- **Bounds**: Lee bounds (sample selection), Manski bounds (partial identification)
408+
- **Interference**: `spillover()` direct + indirect effect decomposition
409+
- **DTR**: `g_estimation()` multi-stage optimal treatment regimes
410+
- **Multi-Treatment**: AIPW for multi-valued treatments
411+
- **Bunching**: Kink/notch bunching estimator with elasticity
412+
- **Matrix Completion**: `mc_panel()` nuclear-norm panel estimator
413+
- **Robustness**: `spec_curve()`, `robustness_report()`, `subgroup_analysis()`
414+
- **New Regression**: DeepIV, Heckman selection, quantile regression, Tobit, Arellano-Bond GMM
415+
- **New Diagnostics**: E-value, Anderson-Rubin weak IV test, Sensemakr, RD density test
416+
- **Other**: Entropy balancing, Sun-Abraham event study, Bacon decomposition, HonestDiD
417+
418+
### v0.2.0 (2025-11-15) — Post-Estimation & Output
419+
420+
- **Post-Estimation**: `margins()`, `marginsplot()`, `test()`, `lincom()`
421+
- **Output Tables**: `modelsummary()`, `outreg2()`, `sumstats()`, `balance_table()`, `tab()`, `coefplot()`, `binscatter()`
422+
- **Inference**: `wild_cluster_bootstrap()`, `aipw()`, `ri_test()`
423+
- **New Modules**: DML, Causal Forest, Matching (PSM/Mahalanobis), Synthetic Control (ADH + SDID), Panel (FE/RE/FD), Causal Impact, Mediation, Bartik IV
424+
- **Diagnostics**: `oster_bounds()`, `mccrary_test()`, `hausman_test()`, `het_test()`, `reset_test()`, `vif()`
425+
- **Utilities**: Variable labeling, `describe()`, `pwcorr()`, `winsor()`, `read_data()`
426+
427+
### v0.1.0 (2025-10-01) — Initial Release
428+
429+
- Core regression: `regress()` OLS with robust/clustered/HAC standard errors
430+
- Instrumental variables: `ivreg()` 2SLS
431+
- Difference-in-Differences: `did()`, `did_2x2()`, `callaway_santanna()`
432+
- Regression discontinuity: `rdrobust()`
433+
- Unified `CausalResult` object with `.summary()`, `.plot()`, `.to_latex()`, `.to_docx()`, `.cite()`
434+
435+
---
436+
376437
## About
377438

378439
**StatsPAI Inc.** is the research infrastructure company behind [CoPaper.AI](https://copaper.ai) — the AI co-authoring platform for empirical research, born out of Stanford's [REAP](https://reap.fsi.stanford.edu/) program.
@@ -405,7 +466,7 @@ pytest
405466
author={Wang, Bryce},
406467
year={2025},
407468
url={https://github.com/brycewang-stanford/statspai},
408-
version={0.3.1}
469+
version={0.4.0}
409470
}
410471
```
411472

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "StatsPAI"
7-
version = "0.3.1"
7+
version = "0.4.0"
88
description = "The Causal Inference & Econometrics Toolkit for Python"
99
readme = "README.md"
1010
license = {text = "MIT"}

src/statspai/__init__.py

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,35 @@
2222
>>> sp.outreg2(result, filename="results.xlsx")
2323
"""
2424

25-
__version__ = "0.3.1"
25+
__version__ = "0.4.0"
2626
__author__ = "Bryce Wang"
2727
__email__ = "bryce@copaper.ai"
2828

2929
from .core.results import EconometricResults, CausalResult
3030
from .regression.ols import regress
31-
from .regression.iv import ivreg, IVRegression
31+
from .regression.iv import iv, ivreg, IVRegression
3232
from .causal.causal_forest import CausalForest, causal_forest
33-
from .did import did, did_2x2, callaway_santanna, sun_abraham, bacon_decomposition, honest_did, breakdown_m, event_study
34-
from .rd import rdrobust, rdplot
33+
from .did import (
34+
did, did_2x2, ddd, callaway_santanna, sun_abraham,
35+
bacon_decomposition, honest_did, breakdown_m, event_study,
36+
did_analysis, DIDAnalysis,
37+
parallel_trends_plot, bacon_plot, group_time_plot, did_plot,
38+
enhanced_event_study_plot, treatment_rollout_plot,
39+
sensitivity_plot, cohort_event_study_plot,
40+
)
41+
from .rd import rdrobust, rdplot, rdplotdensity, rdbwsensitivity, rdbalance, rdplacebo, rdsummary
3542
from .synth import (
36-
synth, SyntheticControl, sdid, augsynth,
43+
synth, SyntheticControl, synthplot, sdid, augsynth,
44+
demeaned_synth, robust_synth, gsynth, staggered_synth, conformal_synth,
3745
synthdid_estimate, sc_estimate, did_estimate,
3846
synthdid_placebo, synthdid_plot, synthdid_units_plot, synthdid_rmse_plot,
3947
california_prop99,
4048
)
41-
from .matching import match, MatchEstimator, ebalance
49+
from .matching import match, MatchEstimator, ebalance, balanceplot, psplot
4250
from .dml import dml, DoubleML
4351
from .deepiv import deepiv, DeepIV
44-
from .panel import panel, PanelRegression
45-
from .causal_impact import causal_impact, CausalImpactEstimator
52+
from .panel import panel, panel_compare, PanelResults, PanelCompareResults, PanelRegression
53+
from .causal_impact import causal_impact, CausalImpactEstimator, impactplot
4654
from .mediation import mediate, MediationAnalysis
4755
from .bartik import bartik, BartikIV
4856
from .output.outreg2 import OutReg2, outreg2
@@ -76,7 +84,7 @@
7684
from .multi_treatment import multi_treatment, MultiTreatment
7785
from .robustness import spec_curve, SpecCurveResult, robustness_report, RobustnessResult, subgroup_analysis, SubgroupResult
7886
from .survey import svydesign, SurveyDesign, svymean, svytotal, svyglm
79-
from .dag import dag, DAG
87+
from .dag import dag, DAG, dag_example, dag_examples, dag_example_positions, dag_simulate
8088
from .registry import list_functions, describe_function, function_schema, search_functions, all_schemas
8189

8290
__all__ = [
@@ -85,23 +93,45 @@
8593
"CausalResult",
8694
# Regression
8795
"regress",
96+
"iv",
8897
"ivreg",
8998
"IVRegression",
9099
# DID
91100
"did",
92101
"did_2x2",
102+
"ddd",
93103
"callaway_santanna",
94104
"sun_abraham",
95105
"bacon_decomposition",
96106
"honest_did",
97107
"breakdown_m",
98108
"event_study",
109+
"did_analysis",
110+
"DIDAnalysis",
111+
"parallel_trends_plot",
112+
"bacon_plot",
113+
"group_time_plot",
114+
"did_plot",
115+
"enhanced_event_study_plot",
116+
"treatment_rollout_plot",
117+
"sensitivity_plot",
118+
"cohort_event_study_plot",
99119
# RD
100120
"rdrobust",
101121
"rdplot",
122+
"rdplotdensity",
123+
"rdbwsensitivity",
124+
"rdbalance",
125+
"rdplacebo",
126+
"rdsummary",
102127
# Synthetic Control
103128
"synth",
104129
"SyntheticControl",
130+
"demeaned_synth",
131+
"robust_synth",
132+
"gsynth",
133+
"staggered_synth",
134+
"conformal_synth",
105135
"sdid",
106136
"synthdid_estimate",
107137
"sc_estimate",
@@ -116,6 +146,8 @@
116146
"match",
117147
"MatchEstimator",
118148
"ebalance",
149+
"balanceplot",
150+
"psplot",
119151
# Double ML
120152
"dml",
121153
"DoubleML",
@@ -124,6 +156,9 @@
124156
"DeepIV",
125157
# Panel
126158
"panel",
159+
"panel_compare",
160+
"PanelResults",
161+
"PanelCompareResults",
127162
"PanelRegression",
128163
# Causal Impact
129164
"causal_impact",
@@ -278,6 +313,10 @@
278313
# DAG
279314
"dag",
280315
"DAG",
316+
"dag_example",
317+
"dag_examples",
318+
"dag_example_positions",
319+
"dag_simulate",
281320
# AI / Agent Registry
282321
"list_functions",
283322
"describe_function",

src/statspai/causal_impact/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
*Annals of Applied Statistics*, 9(1), 247-274.
1515
"""
1616

17-
from .impact import causal_impact, CausalImpactEstimator
17+
from .impact import causal_impact, CausalImpactEstimator, impactplot
1818

19-
__all__ = ['causal_impact', 'CausalImpactEstimator']
19+
__all__ = ['causal_impact', 'CausalImpactEstimator', 'impactplot']

src/statspai/causal_impact/impact.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,135 @@ def _predict(self, model, X_full):
326326
return Y_pred, Y_pred_se
327327

328328

329+
# ------------------------------------------------------------------
330+
# Plotting
331+
# ------------------------------------------------------------------
332+
333+
def impactplot(
334+
result: CausalResult,
335+
type: str = 'all',
336+
ax=None,
337+
figsize: tuple = (12, 9),
338+
title=None,
339+
):
340+
"""
341+
Causal Impact visualization (Google-style 3-panel plot).
342+
343+
Parameters
344+
----------
345+
result : CausalResult
346+
Result from ``causal_impact()``.
347+
type : str, default 'all'
348+
'all': 3-panel (original + pointwise + cumulative).
349+
'original': actual vs counterfactual.
350+
'pointwise': pointwise causal effect.
351+
'cumulative': cumulative effect.
352+
ax : matplotlib Axes, optional
353+
Only for single-panel types.
354+
figsize : tuple
355+
title : str, optional
356+
357+
Returns
358+
-------
359+
(fig, ax) or (fig, axes)
360+
"""
361+
try:
362+
import matplotlib.pyplot as plt
363+
except ImportError:
364+
raise ImportError("matplotlib required.")
365+
366+
mi = result.model_info
367+
detail = result.detail
368+
if detail is None or 'actual' not in detail.columns:
369+
raise ValueError("No detail table. Use causal_impact() result.")
370+
371+
times = detail['time'].values
372+
actual = detail['actual'].values
373+
predicted = detail['predicted'].values
374+
effect = detail['effect'].values
375+
eff_lo = detail['effect_lower'].values
376+
eff_hi = detail['effect_upper'].values
377+
post = detail['post_intervention'].values
378+
intervention_time = mi.get('intervention_time')
379+
cum_raw = mi.get('cumulative_effect')
380+
if cum_raw is not None and len(cum_raw) < len(times):
381+
# cumulative_effect only covers post-period; pad pre with zeros
382+
cumulative = np.zeros(len(times))
383+
cumulative[post] = cum_raw
384+
elif cum_raw is not None:
385+
cumulative = cum_raw
386+
else:
387+
cumulative = np.cumsum(effect * post)
388+
389+
if type == 'all':
390+
fig, axes = plt.subplots(3, 1, figsize=figsize, sharex=True)
391+
_original_panel(axes[0], times, actual, predicted,
392+
intervention_time, result.alpha)
393+
_pointwise_panel(axes[1], times, effect, eff_lo, eff_hi,
394+
post, intervention_time)
395+
_cumulative_panel(axes[2], times, cumulative, post,
396+
intervention_time)
397+
fig.suptitle(title or 'Causal Impact Analysis', fontsize=14, y=1.01)
398+
fig.tight_layout()
399+
return fig, axes
400+
401+
if ax is None:
402+
fig, ax = plt.subplots(figsize=(figsize[0], figsize[1] // 3 + 1))
403+
else:
404+
fig = ax.get_figure()
405+
406+
if type == 'pointwise':
407+
_pointwise_panel(ax, times, effect, eff_lo, eff_hi,
408+
post, intervention_time)
409+
elif type == 'cumulative':
410+
_cumulative_panel(ax, times, cumulative, post, intervention_time)
411+
else:
412+
_original_panel(ax, times, actual, predicted,
413+
intervention_time, result.alpha)
414+
415+
ax.set_title(title or f'Causal Impact: {type}', fontsize=13)
416+
fig.tight_layout()
417+
return fig, ax
418+
419+
420+
def _original_panel(ax, times, actual, predicted, t0, alpha):
421+
ax.plot(times, actual, color='#2C3E50', linewidth=1.5, label='Observed')
422+
ax.plot(times, predicted, color='#3498DB', linewidth=1.5,
423+
linestyle='--', label='Counterfactual')
424+
if t0 is not None:
425+
ax.axvline(x=t0, color='#E74C3C', linestyle=':', linewidth=1,
426+
alpha=0.7, label='Intervention')
427+
ax.set_ylabel('Value', fontsize=11)
428+
ax.legend(fontsize=9, frameon=False)
429+
ax.spines['top'].set_visible(False)
430+
ax.spines['right'].set_visible(False)
431+
432+
433+
def _pointwise_panel(ax, times, effect, lo, hi, post, t0):
434+
ax.plot(times, effect, color='#2C3E50', linewidth=1.5)
435+
ax.fill_between(times, lo, hi, alpha=0.15, color='#3498DB')
436+
ax.axhline(y=0, color='gray', linestyle='--', linewidth=0.8)
437+
if t0 is not None:
438+
ax.axvline(x=t0, color='#E74C3C', linestyle=':', linewidth=1,
439+
alpha=0.7)
440+
ax.set_ylabel('Pointwise Effect', fontsize=11)
441+
ax.spines['top'].set_visible(False)
442+
ax.spines['right'].set_visible(False)
443+
444+
445+
def _cumulative_panel(ax, times, cumulative, post, t0):
446+
ax.plot(times, cumulative, color='#2C3E50', linewidth=1.5)
447+
ax.fill_between(times, 0, cumulative, alpha=0.15, color='#3498DB')
448+
ax.axhline(y=0, color='gray', linestyle='--', linewidth=0.8)
449+
if t0 is not None:
450+
ax.axvline(x=t0, color='#E74C3C', linestyle=':', linewidth=1,
451+
alpha=0.7)
452+
ax.set_xlabel('Time', fontsize=11)
453+
ax.set_ylabel('Cumulative Effect', fontsize=11)
454+
ax.spines['top'].set_visible(False)
455+
ax.spines['right'].set_visible(False)
456+
457+
329458
# Citation
330459
CausalResult._CITATIONS['causal_impact'] = (
331460
"@article{brodersen2015inferring,\n"

0 commit comments

Comments
 (0)