Skip to content

Commit f0aa060

Browse files
igerberclaude
andcommitted
stacked_did: address CI codex R1 P2 + P3 polish
CI codex R1 verdict was ✅ with 2 polish items: P2 (Maintainability): StackedDiDResults.summary() didn't surface the analytical variance family (vcov_type), even though hc1 vs hc2_bm are now materially different user-facing inference modes. Other result classes (DiD/MPD/TWFE) already print a Variance: line via the shared `_format_vcov_label` helper at results.py:49-89. Fix: added a Variance: line to StackedDiDResults.summary() using the shared helper, suppressed under survey_metadata (the survey block already surfaces inference source). My earlier plan note ("mirror SA: don't add Variance: row") was based on a wrong assumption — SA-results uses the same shared helper, and StackedDiD inherits the same precedent. P3 (Performance): on `vcov_type="hc2_bm"` fits with `aggregate=None` or `"simple"`, the code built per-event Bell-McCaffrey contrast DOFs that were never surfaced (only overall ATT is exposed). Avoidable CR2 cost on every overall-only hc2_bm fit. Fix: gated the per-event contrast construction (es_keys, es_cols_full) on `aggregate == "event_study"`. The overall ATT contrast still gets computed unconditionally (it's the user-facing scalar). 89 tests still pass — no behavioral regression on the event-study path; overall-only fits are now slightly faster. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent aedea7f commit f0aa060

2 files changed

Lines changed: 32 additions & 6 deletions

File tree

diff_diff/stacked_did.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -550,14 +550,18 @@ def fit(
550550
# Only build contrasts whose target column is identified; if a
551551
# delta_h column itself was dropped, that event-time will get
552552
# NaN inference (left to safe_inference's df=None path).
553+
# Per CI codex R1 P3: skip per-event contrast DOFs when the
554+
# event-study surface is not user-visible (aggregate != "event_study").
555+
# The overall ATT contrast still gets computed below.
553556
es_keys: List[int] = []
554557
es_cols_full: List[np.ndarray] = []
555-
for h in event_times:
556-
if h in interaction_indices and _identified[interaction_indices[h]]:
557-
c = np.zeros(k_design)
558-
c[interaction_indices[h]] = 1.0
559-
es_keys.append(h)
560-
es_cols_full.append(c)
558+
if aggregate == "event_study":
559+
for h in event_times:
560+
if h in interaction_indices and _identified[interaction_indices[h]]:
561+
c = np.zeros(k_design)
562+
c[interaction_indices[h]] = 1.0
563+
es_keys.append(h)
564+
es_cols_full.append(c)
561565
# Overall ATT contrast: average of post-period delta_h columns
562566
# (the same 1/K * ones contrast used for overall_se below). Only
563567
# construct if ALL post-period delta_h are identified — otherwise

diff_diff/stacked_did_results.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,28 @@ def summary(self, alpha: Optional[float] = None) -> str:
176176
"",
177177
]
178178

179+
# Variance family label (per CI codex R1 P2): surface the analytical
180+
# vcov_type when the survey path didn't override. Mirrors the shared
181+
# `_format_vcov_label` helper from results.py used by DiD/MPD/TWFE.
182+
# Suppress under survey_metadata since the survey block above already
183+
# reports the inference source.
184+
if self.survey_metadata is None and self.vcov_type:
185+
from diff_diff.results import _format_vcov_label
186+
187+
# StackedDiD is intrinsically clustered; the cluster name is
188+
# either "unit" or "unit_subexp" (config-time, not stored
189+
# explicitly on results — derive from the stacked data if
190+
# available, else fall back to generic label).
191+
label = _format_vcov_label(
192+
self.vcov_type,
193+
cluster_name=None, # cluster identity is config; print plain label
194+
n_clusters=None,
195+
n_obs=self.n_stacked_obs,
196+
)
197+
if label is not None:
198+
lines.append(f"{'Variance:':<30} {label:>50}")
199+
lines.append("")
200+
179201
# Add survey design info
180202
if self.survey_metadata is not None:
181203
sm = self.survey_metadata

0 commit comments

Comments
 (0)