Skip to content

Commit 5fb2266

Browse files
committed
Add DurationDiD for absorbing individual outcomes
1 parent 4c8e76d commit 5fb2266

4 files changed

Lines changed: 24 additions & 4 deletions

File tree

diff_diff/duration_did_results.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,23 +254,24 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame:
254254
raise ValueError("level must be 'event_study', 'simple', 'survival', or 'diagnostics'")
255255

256256
def summary(self, alpha: Optional[float] = None) -> str:
257-
"""Summarize stored whole-population absorption ATT and availability."""
257+
"""Summarize stored absorption ATT and availability, printing shared reasons once."""
258258
_require_fit_alpha(alpha, self.alpha, message=_SUMMARY_ALPHA_MESSAGE)
259259
units = (
260260
"hazard gap per normalized observation interval"
261261
if self.method == "common_dynamics"
262262
else "dimensionless hazard ratio"
263263
)
264+
reasons = dict.fromkeys(
265+
reason for values in self.inference_reasons.values() for reason in values
266+
)
264267
return (
265268
f"DurationDiD ({self.method})\n"
266269
f"Coefficient ({units}): {self.coefficient:.6g}\n"
267270
f"Mean cumulative absorption ATT: {self.att:.6g}; SE: {self.se:.6g}\n"
268271
f"{_coverage_pct(self.alpha)}% CI: {self.conf_int}; p-value: {self.p_value:.6g}\n"
269272
f"Estimation: {self.estimation_status}; inference: {self.inference_status}\n"
270273
f"Pooled individual bootstrap: {self.n_bootstrap_valid}/{self.n_bootstrap} valid effect draws"
271-
+ "".join(
272-
"\n" + reason for values in self.inference_reasons.values() for reason in values
273-
)
274+
+ "".join("\n" + reason for reason in reasons)
274275
+ "".join("\n" + warning for warning in self.support_warnings)
275276
+ "\n"
276277
+ self.pretrend_results.summary()

diff_diff/guides/llms-full.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ Owns `effects`, `survival_curve`, `bootstrap_failures`, B-by-post `bootstrap_eff
115115
`inference_reasons` have pointwise/simultaneous/simple keys; available/unavailable,
116116
plus partial for pointwise. `support_warnings` and `pretrend_results` remain
117117
inspectable. Properties: `inference_method='bootstrap'`, uniform raw post `raw_att`.
118+
`summary()` prints distinct inference-unavailability reasons once in first appearance
119+
order; the full per-family `inference_reasons` mapping remains in the result and `to_dict()`.
118120

119121
Native table column order (chronological RangeIndex):
120122
- effects (post only): period, event_time, att, se, t_stat, p_value, conf_int_lower,

docs/api/duration_did.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ mappings as period/value records. Native tables have chronological RangeIndex.
135135
``support_warnings`` and stored ``pretrend_results`` remain inspectable.
136136
- Properties: ``inference_method='bootstrap'`` and uniform raw post mean ``raw_att``.
137137

138+
``summary()`` prints each distinct inference-unavailability reason once, in first
139+
appearance order. The full per-family ``inference_reasons`` mapping is preserved
140+
on the result and in ``to_dict()``.
141+
138142
Native schemas, in column order:
139143

140144
- ``effects`` (post only): period, event_time, att, se, t_stat, p_value,

tests/test_duration_did_results.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,19 @@ def test_serialization_confidence_and_metadata(result):
9393
assert "dimensionless hazard ratio" in ph.summary()
9494

9595

96+
def test_summary_reports_shared_inference_failure_once():
97+
data = duration_panel(control=(3, 2, 1, 1, 1, 1), treated=(3, 2, 1, 1, 1, 1), n=3, scale=1)
98+
r = fit_duration(data, n_bootstrap=15, seed=2)
99+
stored = r.to_dict()
100+
reason = "one or more effect bootstrap draws failed"
101+
assert all(reason in r.inference_reasons[family] for family in r.inference_status)
102+
text = r.summary()
103+
assert text.splitlines().count(reason) == 1
104+
assert all(warning in text for warning in r.support_warnings)
105+
assert r.pretrend_results.summary() in text
106+
assert r.to_dict() == stored
107+
108+
96109
@pytest.mark.parametrize(
97110
"clock", [pd.date_range("2020", periods=6), pd.timedelta_range("0 days", periods=6)]
98111
)

0 commit comments

Comments
 (0)