Skip to content

Commit a8fbd1f

Browse files
feat: v0.9.14 — summary() renders ATT/ATU block (completes v0.9.13 spec §3.3)
Tiny patch release closing the remaining thread on v0.9.13: the six att_sd / att_hdi_* / atu_sd / atu_hdi_* fields landed but summary() never printed them. v0.9.14 wires the rendering through a BayesianMTEResult.summary() override. Added: - BayesianMTEResult.summary() override appends a 'Population-integrated effects' block inside the framing = ruler: ATT: 0.2407 (sd 0.0370, 95% HDI [0.1693, 0.3136]) ATU: 0.2147 (sd 0.0435, 95% HDI [0.1341, 0.2947]) Silently skipped when SDs are NaN (empty subpopulation or pre-v0.9.13 deserialised result). Round B review: no blockers. Reviewer confirmed: - base.endswith('=' * 70) is exact (parent returns '\n'.join(lines) with rule as final element, no trailing newline or rstrip). - Block splicing preserves the closing ruler visually. - NaN stub path is safe; fallback branch is defensive. - 'ATT:' / 'ATU:' are unique substrings — no parent collision. - Pure reader, thread-safe. Tests: - tests/test_bayes_mte_uncertainty.py now has two new tests: test_summary_shows_att_atu_uncertainty (post-fit: string contains 'ATT:', 'ATU:', 'sd ', 'HDI [') and test_summary_skips_att_atu_when_nan (stub with NaN SDs → no 'ATT:' / 'ATU:' in output). - Bayesian family suite: 88/88 focused MTE + sibling tests green in 1:55. Also rides along with parallel-work imports from auto-hooks: sp.rosenbaum_bounds / rosenbaum_gamma (diagnostics) and sp.negative_control_outcome / negative_control_exposure / double_negative_control / NegativeControlResult (proximal). Neither is documented in this CHANGELOG; they ship via the git log as parallel contributions. Design spec: docs/superpowers/specs/2026-04-20-v0913-hdi-compat-and-att-sd.md §3.3 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2fd88de commit a8fbd1f

5 files changed

Lines changed: 129 additions & 5 deletions

File tree

CHANGELOG.md

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

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

5+
## [0.9.14] - 2026-04-20 — Summary rendering completes v0.9.13 spec §3.3
6+
7+
Tiny patch release. Completes the "ATT/ATU in `summary()`" promise
8+
from v0.9.13 spec §3.3 that was not actually wired at ship time
9+
(the six uncertainty fields landed but `summary()` never printed
10+
them).
11+
12+
### Added (0.9.14)
13+
14+
- **`BayesianMTEResult.summary()`** override. Extends
15+
`BayesianCausalResult.summary` with a `Population-integrated effects`
16+
block:
17+
18+
ATT: 0.2407 (sd 0.0370, 95% HDI [0.1693, 0.3136])
19+
ATU: 0.2147 (sd 0.0435, 95% HDI [0.1341, 0.2947])
20+
21+
Rendered inside the framing `=` ruler for visual coherence.
22+
Silently skipped when either SD is NaN (empty subpopulation or
23+
pre-v0.9.13 deserialised result).
24+
25+
### Round-B review: no blockers
26+
27+
Reviewer confirmed:
28+
1. `base.endswith('=' * 70)` is exact — parent `summary()` returns
29+
`'\n'.join(lines)` with the rule as the final element.
30+
2. Block splicing preserves the closing ruler visually.
31+
3. NaN stub path is safe; fallback branch is defensive.
32+
4. `'ATT:'` / `'ATU:'` are unique substrings — no collision with
33+
parent output.
34+
5. Pure reader; thread-safe.
35+
36+
### Tests (0.9.14)
37+
38+
- `tests/test_bayes_mte_uncertainty.py` now has:
39+
- `test_summary_shows_att_atu_uncertainty` — after fit, string
40+
contains `'ATT:'`, `'ATU:'`, `'sd '`, `'HDI ['`.
41+
- `test_summary_skips_att_atu_when_nan` — NaN-SD stub → no
42+
`'ATT:'` / `'ATU:'` in output.
43+
- Full Bayesian suite: 88/88 focused MTE + sibling green in 1:55.
44+
45+
### Non-goals (0.9.14)
46+
47+
- `.tidy()` multi-row variant with ATE/ATT/ATU as separate rows
48+
— queued for v0.9.15+.
49+
- Full bivariate-normal HV model.
50+
- Rust Phase 2.
51+
52+
---
53+
554
## [0.9.13] - 2026-04-20 — ArviZ HDI compat shim + ATT/ATU uncertainty
655

756
Small-but-load-bearing cleanup release. Closes two items deferred

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.13"
7+
version = "0.9.14"
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: 8 additions & 4 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.13"
25+
__version__ = "0.9.14"
2626
__author__ = "Biaoyue Wang"
2727
__email__ = "brycew6m@stanford.edu"
2828

@@ -96,7 +96,7 @@
9696
from .output.regression_table import regtable, RegtableResult, mean_comparison, MeanComparisonResult
9797
from .output.paper_tables import paper_tables, PaperTables, TEMPLATES as PAPER_TABLE_TEMPLATES
9898
from .postestimation import margins, marginsplot, margins_at, margins_at_plot, contrast, pwcompare, test, lincom
99-
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
99+
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
100100
from .inference import (
101101
wild_cluster_bootstrap, aipw, ri_test, ipw, bootstrap, BootstrapResult,
102102
twoway_cluster, conley, pate, PATEEstimator, fisher_exact, FisherResult,
@@ -106,7 +106,11 @@
106106
g_computation, front_door,
107107
)
108108
from .msm import msm, MarginalStructuralModel, stabilized_weights
109-
from .proximal import proximal, ProximalCausalInference
109+
from .proximal import (
110+
proximal, ProximalCausalInference,
111+
negative_control_outcome, negative_control_exposure,
112+
double_negative_control, NegativeControlResult,
113+
)
110114
from .principal_strat import (
111115
principal_strat, PrincipalStratResult, survivor_average_causal_effect,
112116
)
@@ -156,7 +160,7 @@
156160
from .regression.glm import glm, GLMRegression, GLMEstimator
157161
from .regression.count import poisson, nbreg, ppmlhdfe
158162
from .neural_causal import tarnet, cfrnet, dragonnet, TARNet, CFRNet, DragonNet
159-
from .causal_discovery import notears, NOTEARS, pc_algorithm, PCAlgorithm, lingam, LiNGAMResult, ges, GESResult
163+
from .causal_discovery import notears, NOTEARS, pc_algorithm, PCAlgorithm, lingam, LiNGAMResult, ges, GESResult, fci, FCIResult
160164
from .tmle import tmle, TMLE, super_learner, SuperLearner
161165
from .policy_learning import policy_tree, PolicyTree, policy_value
162166
from .conformal_causal import conformal_cate, ConformalCATE

src/statspai/bayes/_base.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,38 @@ class BayesianMTEResult(BayesianCausalResult):
426426
atu_hdi_lower: float = float('nan')
427427
atu_hdi_upper: float = float('nan')
428428

429+
def summary(self) -> str:
430+
"""Printable summary with ATT/ATU uncertainty appended.
431+
432+
Extends ``BayesianCausalResult.summary`` with a block printing
433+
``ATT`` / ``ATU`` posterior mean, SD and HDI when the SD
434+
fields are finite. Silently skipped (parent summary only) when
435+
either side of the population is empty or the result was
436+
deserialised from a pre-v0.9.13 snapshot (fields default NaN).
437+
"""
438+
base = super().summary()
439+
extra: list[str] = []
440+
pct = int(self.hdi_prob * 100)
441+
if np.isfinite(self.att_sd):
442+
extra.append(
443+
f' ATT: {self.att:.4f} (sd {self.att_sd:.4f}, '
444+
f'{pct}% HDI [{self.att_hdi_lower:.4f}, '
445+
f'{self.att_hdi_upper:.4f}])'
446+
)
447+
if np.isfinite(self.atu_sd):
448+
extra.append(
449+
f' ATU: {self.atu:.4f} (sd {self.atu_sd:.4f}, '
450+
f'{pct}% HDI [{self.atu_hdi_lower:.4f}, '
451+
f'{self.atu_hdi_upper:.4f}])'
452+
)
453+
if not extra:
454+
return base
455+
block = '\n'.join(['-' * 70, 'Population-integrated effects', *extra])
456+
closing = '=' * 70
457+
if base.endswith(closing):
458+
return base[: -len(closing)] + block + '\n' + closing
459+
return base + '\n' + block
460+
429461
def policy_effect(
430462
self,
431463
weight_fn,

tests/test_bayes_mte_uncertainty.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,42 @@ def test_bayes_mte_att_atu_both_finite_on_realistic_dgp():
8383
assert np.isfinite(r.atu)
8484
assert np.isfinite(r.att_sd)
8585
assert np.isfinite(r.atu_sd)
86+
87+
88+
def test_summary_shows_att_atu_uncertainty():
89+
"""summary() must print an ATT / ATU block with sd + HDI once
90+
the SD fields are finite (spec §3.3)."""
91+
df = _hv_dgp(300, slope=1.0, seed=23)
92+
r = bayes_mte(df, y='y', treat='d', instrument='z', poly_u=1,
93+
draws=200, tune=200, chains=2, progressbar=False)
94+
s = r.summary()
95+
assert 'ATT:' in s
96+
assert 'ATU:' in s
97+
# sd is lower-case in the spec-mandated format
98+
assert 'sd ' in s
99+
assert 'HDI [' in s
100+
101+
102+
def test_summary_skips_att_atu_when_nan():
103+
"""If ATT/ATU SDs are NaN (e.g. deserialised pre-v0.9.13 result),
104+
summary() must silently omit the block rather than printing
105+
``sd nan`` garbage."""
106+
from statspai.bayes._base import BayesianMTEResult
107+
stub = BayesianMTEResult(
108+
method='bayes_mte',
109+
estimand='ATE',
110+
posterior_mean=0.5,
111+
posterior_median=0.5,
112+
posterior_sd=0.1,
113+
hdi_lower=0.3,
114+
hdi_upper=0.7,
115+
prob_positive=0.99,
116+
rhat=1.0,
117+
ess=400.0,
118+
n_obs=100,
119+
hdi_prob=0.95,
120+
model_info={'inference': 'nuts', 'chains': 2, 'draws': 100},
121+
)
122+
out = stub.summary()
123+
assert 'ATT:' not in out
124+
assert 'ATU:' not in out

0 commit comments

Comments
 (0)