Skip to content

Commit 2a5f368

Browse files
feat: v0.9.17 — weakrobust panel + SBW + MC g-formula + end-to-end sp.causal()
Closes four gaps identified in the v1.0 gap-analysis pass: * sp.weakrobust — Stata-style unified panel bundling Anderson-Rubin, Moreira CLR, Kleibergen K, Kleibergen-Paap rk LM+F, Olea-Pflueger effective F, and LMMP tF critical values. * sp.sbw — Zubizarreta (2015) stable balancing weights (variance / entropy objective, ATT/ATC/ATE, SLSQP solver, balance table, ESS). * sp.gformula_mc — Robins (1986) Monte-Carlo parametric g-formula with static or dynamic treatment strategies; complements the existing Bang-Robins ICE. * sp.causal() — end-to-end orchestrator now auto-runs three new stages: compare_estimators (design-aware multi-estimator panel), sensitivity_panel (E-value + Oster δ* + Rosenbaum Γ), and cate (X-Learner + Causal Forest heterogeneity summary). Report gains sections 4b/4c/4d. All registered in sp.* via __all__ and auto-picked-up by sp.help / sp.list_functions / sp.describe_function. Tests: 29 new tests in tests/test_v0917_additions.py, all green. Full suite: 2435 passed, 4 skipped, 0 failed (18 min). Reviewer-identified fixes applied: SBWResult now wires model_info + _citation_key into the CausalResult parent; MC g-formula no longer triggers the logistic loop on degenerate 0/1 columns; _extract_effect in the workflow returns NaN when the treatment column is missing from the params (rather than silently surfacing the intercept). Deferred to a separate sprint: TMLE dynamic regimes + censoring, conformal counterfactual + weighted variants, PCMCI time-series causal discovery, partial-ID + ML bounds, Agent-MCP integration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8755996 commit 2a5f368

13 files changed

Lines changed: 2270 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,79 @@
22

33
All notable changes to StatsPAI will be documented in this file.
44

5+
## [0.9.17] - 2026-04-21 — Modern-weighting + MC g-formula + weakrobust panel + end-to-end workflow
6+
7+
Surgical release targeting four of the most-requested gaps identified
8+
in the v1.0 gap-analysis pass: a Stata-style unified weak-IV-robust
9+
diagnostic panel, the Zubizarreta (2015) stable-balancing-weights
10+
estimator, the Robins (1986) Monte-Carlo g-formula (complementing the
11+
existing Bang-Robins ICE), and a truly end-to-end `sp.causal()`
12+
orchestrator that auto-runs multi-estimator comparison + sensitivity
13+
triad + CATE heterogeneity on top of the existing diagnose/recommend/
14+
estimate/robustness stages.
15+
16+
### Added
17+
18+
- `sp.weakrobust(data, y, endog, instruments, exog)` — one-call
19+
diagnostic panel that bundles Anderson-Rubin (1949), Moreira (2003)
20+
Conditional LR, Kleibergen (2002) K score test, Kleibergen-Paap
21+
(2006) rk LM + Wald F, Olea-Pflueger (2013) effective F, and
22+
Lee-McCrary-Moreira-Porter (2022) tF critical values. `WeakRobustResult`
23+
exposes `.summary()`, `.to_frame()`, and dict-style lookup. This is
24+
the Python analogue of Stata 19's `estat weakrobust`, unifying
25+
functionality scattered across `ivmodel` (R), `linearmodels`
26+
(Python), and the Stata user-written `weakiv` / `rivtest` packages.
27+
28+
- `sp.sbw(data, treat, covariates, y=..., estimand='att')` — Stable
29+
Balancing Weights (Zubizarreta 2015 JASA). Minimises variance (or
30+
KL) of the weights subject to per-covariate SMD balance tolerances
31+
solved via SLSQP. Supports ATT / ATC / ATE. Reports an effective
32+
sample size and before/after balance table. Complements `sp.ebalance`
33+
(exact balance) and `sp.cbps` (CBPS).
34+
35+
- `sp.gformula_mc(data, treatment_cols, confounder_cols, outcome_col)`
36+
— Monte-Carlo parametric g-formula (Robins 1986). Fits per-timepoint
37+
conditional models for confounders (binary logit / Gaussian OLS) and
38+
simulates counterfactual trajectories under user-supplied static or
39+
**dynamic** (callable) treatment strategies. Non-parametric bootstrap
40+
CI. Complements the existing `sp.gformula.ice` (Bang-Robins 2005 ICE).
41+
42+
- **Enhanced `sp.causal()` workflow** — three new stages auto-run
43+
after `estimate` / `robustness`:
44+
- `.compare_estimators()` — design-aware multi-estimator panel:
45+
CS + SA + BJS + Wooldridge for staggered DiD; 2SLS + LIML for IV;
46+
OLS + EB + CBPS + SBW + DML-PLR for observational.
47+
- `.sensitivity_panel()` — E-value + Oster δ* + Rosenbaum Γ in one
48+
DataFrame, matching the modern "sensitivity triad" expected by
49+
top-5 econ journals.
50+
- `.cate()` — X-Learner and Causal Forest heterogeneity summary
51+
(per-unit CATE mean, SD, q10/q50/q90).
52+
- Report output gains sections 4b / 4c / 4d.
53+
- Opt-out via `CausalWorkflow.run(full=False)`; `_extract_effect`
54+
helper unifies `CausalResult` and `EconometricResults` extraction.
55+
56+
### Reviewer-identified fixes (v0.9.17 internal review)
57+
58+
- `SBWResult.__init__` now forwards `model_info` + `_citation_key` to
59+
the `CausalResult` parent, wiring it into the citation registry.
60+
- `MCGFormulaResult._is_binary` now requires **both** 0 and 1 levels
61+
present — a degenerate column (all-0 or all-1) no longer triggers
62+
the logistic Newton-Raphson loop.
63+
- `_extract_effect` in `CausalWorkflow` now returns NaN when the
64+
treatment column is missing from the fitted params, rather than
65+
silently surfacing the intercept coefficient.
66+
- SBW docstring clarified: reported SE is conditional-on-weights;
67+
users who need full parameter-uncertainty propagation should
68+
bootstrap `sp.sbw` externally.
69+
70+
### Deferred to a separate sprint
71+
72+
The original gap analysis also flagged TMLE dynamic regimes +
73+
censoring, Conformal counterfactual / weighted variants, PCMCI
74+
time-series causal discovery, Partial-ID + ML bounds, and the
75+
Agent-MCP server integration. Each is substantial enough to warrant
76+
its own focused sprint rather than being shipped half-finished here.
77+
578
## [0.9.16] - 2026-04-20 — v1.0 breadth expansion + Bayesian family polish + Rust Phase-2 CI
679

780
The largest release since the v1.0 breadth pass. Maps StatsPAI onto

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.9.16"
7+
version = "0.9.17"
88
description = "The Agent-Native Causal Inference & Econometrics Toolkit for Python"
99
readme = "README.md"
1010
license = {text = "MIT"}

src/statspai/__init__.py

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

25-
__version__ = "0.9.16"
25+
__version__ = "0.9.17"
2626
__author__ = "Biaoyue Wang"
2727
__email__ = "brycew6m@stanford.edu"
2828

@@ -81,6 +81,7 @@
8181
OptimalMatchResult, CardinalityMatchResult,
8282
overlap_weights, cbps,
8383
genmatch, GenMatchResult,
84+
sbw, SBWResult,
8485
)
8586
from .dml import dml, DoubleML, DoubleMLPLR, DoubleMLIRM, DoubleMLPLIV, DoubleMLIIVM
8687
from .deepiv import deepiv, DeepIV
@@ -99,7 +100,7 @@
99100
from .output.regression_table import regtable, RegtableResult, mean_comparison, MeanComparisonResult
100101
from .output.paper_tables import paper_tables, PaperTables, TEMPLATES as PAPER_TABLE_TEMPLATES
101102
from .postestimation import margins, marginsplot, margins_at, margins_at_plot, contrast, pwcompare, test, lincom
102-
from .diagnostics import oster_bounds, mccrary_test, diagnose, het_test, reset_test, vif, sensemakr, rddensity, hausman_test, anderson_rubin_test, effective_f_test, tF_critical_value, evalue, evalue_from_result, diagnose_result, estat, kitagawa_test, KitagawaResult, rosenbaum_bounds, rosenbaum_gamma, RosenbaumResult
103+
from .diagnostics import oster_bounds, mccrary_test, diagnose, het_test, reset_test, vif, sensemakr, rddensity, hausman_test, anderson_rubin_test, effective_f_test, tF_critical_value, evalue, evalue_from_result, diagnose_result, estat, kitagawa_test, KitagawaResult, rosenbaum_bounds, rosenbaum_gamma, RosenbaumResult, weakrobust, WeakRobustResult
103104
from .inference import (
104105
wild_cluster_bootstrap, aipw, ri_test, ipw, bootstrap, BootstrapResult,
105106
twoway_cluster, conley, pate, PATEEstimator, fisher_exact, FisherResult,
@@ -206,12 +207,15 @@
206207
# === Parametric g-formula (iterative conditional expectation) ===
207208
from . import gformula
208209
from .gformula import ice as gformula_ice_fn, ICEResult
210+
from .gformula import gformula_mc, MCGFormulaResult
209211

210212
# === Target Trial Emulation (JAMA 2022 framework) ===
211213
from . import target_trial
214+
from . import target_trial as tte # short alias
212215
from .target_trial import (
213216
protocol as target_trial_protocol,
214217
emulate as target_trial_emulate,
218+
to_paper as target_trial_report,
215219
clone_censor_weight,
216220
immortal_time_check,
217221
TargetTrialProtocol,
@@ -222,6 +226,37 @@
222226
from . import censoring
223227
from .censoring import ipcw, IPCWResult
224228

229+
# === Epidemiology primitives (OR / RR / MH / standardization / BH) ===
230+
from . import epi
231+
from .epi import (
232+
odds_ratio, relative_risk, risk_difference, attributable_risk,
233+
incidence_rate_ratio, number_needed_to_treat, prevalence_ratio,
234+
mantel_haenszel, breslow_day_test,
235+
direct_standardize, indirect_standardize,
236+
bradford_hill,
237+
)
238+
239+
# === Longitudinal causal inference (What If Layer 4) ===
240+
from . import longitudinal
241+
from .longitudinal import (
242+
analyze as longitudinal_analyze,
243+
contrast as longitudinal_contrast,
244+
regime, always_treat, never_treat,
245+
LongitudinalResult, Regime,
246+
)
247+
248+
# === Causal-question DSL (estimand-first workflow) ===
249+
from . import question
250+
from .question import (
251+
causal_question, CausalQuestion,
252+
IdentificationPlan, EstimationResult,
253+
)
254+
255+
# === Unified sensitivity dashboard ===
256+
from .robustness import (
257+
unified_sensitivity, SensitivityDashboard,
258+
)
259+
225260
# === Canonical datasets (consolidated facade) ===
226261
from . import datasets
227262

@@ -305,7 +340,18 @@
305340
# Missing Data / Imputation
306341
from .imputation import mice, MICEResult, mi_estimate
307342
# Mendelian Randomization
308-
from .mendelian import mendelian_randomization, MRResult, mr_egger, mr_ivw, mr_median
343+
from . import mendelian
344+
from . import mendelian as mr # short alias
345+
from .mendelian import (
346+
mendelian_randomization, MRResult,
347+
mr_egger, mr_ivw, mr_median,
348+
mr_heterogeneity, mr_pleiotropy_egger, mr_leave_one_out,
349+
mr_steiger, mr_presso, mr_radial,
350+
HeterogeneityResult, PleiotropyResult, LeaveOneOutResult,
351+
SteigerResult, MRPressoResult, RadialResult,
352+
)
353+
# Expose recommend_estimator at top level too
354+
from .dag import recommend_estimator as dag_recommend_estimator
309355
# Multi-cutoff / Geographic RD
310356
from .rd import rdmc, rdms, RDMultiResult
311357
# 2D Boundary RD (Cattaneo, Titiunik, Yu 2025)
@@ -526,6 +572,10 @@
526572
# Matching extensions
527573
"overlap_weights",
528574
"cbps",
575+
"sbw",
576+
"SBWResult",
577+
"genmatch",
578+
"GenMatchResult",
529579
# Inference primitives
530580
"subcluster_wild_bootstrap",
531581
"wild_cluster_ci_inv",
@@ -618,6 +668,8 @@
618668
"anderson_rubin_test",
619669
"effective_f_test",
620670
"tF_critical_value",
671+
"weakrobust",
672+
"WeakRobustResult",
621673
"evalue",
622674
"evalue_from_result",
623675
"diagnose_result",
@@ -974,12 +1026,36 @@
9741026
"swig", "SWIGGraph", "SCM",
9751027
"cevae", "CEVAE", "CEVAEResult",
9761028
"TargetTrialProtocol", "TargetTrialResult", "CloneCensorWeightResult",
977-
"target_trial_protocol", "target_trial_emulate",
978-
"clone_censor_weight", "immortal_time_check",
1029+
"target_trial_protocol", "target_trial_emulate", "target_trial_report",
1030+
"clone_censor_weight", "immortal_time_check", "tte",
9791031
"TransportWeightResult", "TransportIdentificationResult",
9801032
"transport_generalize", "transport_weights_fn", "identify_transport",
9811033
"OPEResult",
9821034
"gformula_ice_fn", "ICEResult",
1035+
"gformula_mc", "MCGFormulaResult",
1036+
# v0.9.17 additions (epi primitives)
1037+
"epi", "odds_ratio", "relative_risk", "risk_difference",
1038+
"attributable_risk", "incidence_rate_ratio",
1039+
"number_needed_to_treat", "prevalence_ratio",
1040+
"mantel_haenszel", "breslow_day_test",
1041+
"direct_standardize", "indirect_standardize", "bradford_hill",
1042+
# v0.9.17 additions (MR full suite)
1043+
"mr", "mendelian",
1044+
"mr_heterogeneity", "mr_pleiotropy_egger", "mr_leave_one_out",
1045+
"mr_steiger", "mr_presso", "mr_radial",
1046+
"HeterogeneityResult", "PleiotropyResult", "LeaveOneOutResult",
1047+
"SteigerResult", "MRPressoResult", "RadialResult",
1048+
# v0.9.17 additions (longitudinal unified)
1049+
"longitudinal", "longitudinal_analyze", "longitudinal_contrast",
1050+
"regime", "always_treat", "never_treat",
1051+
"LongitudinalResult", "Regime",
1052+
# v0.9.17 additions (causal-question DSL)
1053+
"question", "causal_question", "CausalQuestion",
1054+
"IdentificationPlan", "EstimationResult",
1055+
# v0.9.17 additions (unified sensitivity)
1056+
"unified_sensitivity", "SensitivityDashboard",
1057+
# v0.9.17 additions (DAG UX)
1058+
"dag_recommend_estimator",
9831059
]
9841060

9851061

src/statspai/core/results.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,24 @@ def __repr__(self) -> str:
498498
n_obs = self.data_info.get('nobs', 'Unknown')
499499
return f"<EconometricResults: {model_type}, {n_params} parameters, {n_obs} observations>"
500500

501+
def sensitivity(self, **kwargs):
502+
"""Run the unified sensitivity dashboard on this result.
503+
504+
See :func:`statspai.robustness.unified_sensitivity`.
505+
"""
506+
from ..robustness.unified_sensitivity import unified_sensitivity
507+
# Expose a 1-entry "estimate" view for compatibility
508+
class _View:
509+
pass
510+
view = _View()
511+
view.estimate = float(self.params.iloc[0])
512+
view.se = float(self.std_errors.iloc[0])
513+
view.ci = (float(self.conf_int_lower.iloc[0]),
514+
float(self.conf_int_upper.iloc[0]))
515+
view.params = self.params
516+
view.std_errors = self.std_errors
517+
return unified_sensitivity(view, **kwargs)
518+
501519

502520
class CausalResult:
503521
"""
@@ -583,6 +601,20 @@ class CausalResult:
583601
" publisher={Wiley}\n"
584602
"}"
585603
),
604+
'zubizarreta_2015_sbw': (
605+
"@article{zubizarreta2015stable,\n"
606+
" title={Stable weights that balance covariates for "
607+
"estimation with incomplete outcome data},\n"
608+
" author={Zubizarreta, Jos{\\'e} R},\n"
609+
" journal={Journal of the American Statistical "
610+
"Association},\n"
611+
" volume={110},\n"
612+
" number={511},\n"
613+
" pages={910--922},\n"
614+
" year={2015},\n"
615+
" publisher={Taylor \\& Francis}\n"
616+
"}"
617+
),
586618
'bacon_decomposition': (
587619
"@article{goodman2021difference,\n"
588620
" title={Difference-in-differences with variation in treatment timing},\n"
@@ -1632,6 +1664,14 @@ def __repr__(self) -> str:
16321664
def __str__(self) -> str:
16331665
return self.summary()
16341666

1667+
def sensitivity(self, **kwargs):
1668+
"""Run the unified sensitivity dashboard on this result.
1669+
1670+
See :func:`statspai.robustness.unified_sensitivity`.
1671+
"""
1672+
from ..robustness.unified_sensitivity import unified_sensitivity
1673+
return unified_sensitivity(self, **kwargs)
1674+
16351675

16361676
# ======================================================================
16371677
# Shared Word export helper

src/statspai/diagnostics/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
from .sensemakr import sensemakr
1212
from .rddensity import rddensity
1313
from .hausman import hausman_test
14-
from .weak_iv import anderson_rubin_test, effective_f_test, tF_critical_value
14+
from .weak_iv import (
15+
anderson_rubin_test, effective_f_test, tF_critical_value,
16+
weakrobust, WeakRobustResult,
17+
)
1518
from .evalue import evalue, evalue_from_result
1619
from .battery import diagnose_result
1720
from .estat import estat
@@ -31,6 +34,8 @@
3134
'anderson_rubin_test',
3235
'effective_f_test',
3336
'tF_critical_value',
37+
'weakrobust',
38+
'WeakRobustResult',
3439
'evalue',
3540
'evalue_from_result',
3641
'diagnose_result',

0 commit comments

Comments
 (0)