Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/20261026-v4-pr5a-overall-att-readers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
### Internal
- 4.0 cut, PR 5a (the reader half of cut-sequence item 5, the `M-050`..`M-058` storage
flip): every in-repo reader of the 3.x `overall_att` / `overall_se` / `overall_t_stat` /
`overall_p_value` / `overall_conf_int` field names (and ContinuousDiD's `overall_att_*`
spellings) on the nine flipping containers in SOURCE and TESTS - consumers, the
containers' own `summary()` / `to_dict()` / `__repr__` / aggregation bodies, and every
test suite - now reads the canonical `att` / `se` /
`t_stat` / `p_value` / `conf_int` quintet. No user-visible change: the canonical names
already resolve as properties, numbers are bit-identical and nothing warns yet; the
storage flip itself, the `FutureWarning` shims and the report-schema changes follow in
PR 5b; the documentation / guides / tutorial / benchmark half of the reader migration
is PR 5a-ii (split off because the combined diff exceeded the AI review input budget). `power.py`'s `_extract_staggered` (a silent `getattr` fallback chain over the old
names) is deleted - the staggered power profiles use `_extract_simple`; the
`practitioner_next_steps` `overall_att` fallback is gone. Bootstrap draw containers
(`CSBootstrapResults.overall_att_se` and siblings) and `StaggeredTripleDiffResults`
(unified at cut item 6) are untouched.
6 changes: 3 additions & 3 deletions diff_diff/chaisemartin_dhaultfoeuille.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ class ChaisemartinDHaultfoeuille(ChaisemartinDHaultfoeuilleBootstrapMixin, BaseE
on the post-filter estimation sample used by ``DID_M``. When
the ragged-panel filter or ``drop_larger_lower`` drops groups,
the fitted ``results.twfe_*`` values describe a LARGER sample
(pre-filter) than ``results.overall_att`` and a ``UserWarning``
(pre-filter) than ``results.att`` and a ``UserWarning``
is emitted to make the divergence explicit. See REGISTRY.md
``ChaisemartinDHaultfoeuille`` ``Note (TWFE diagnostic sample
contract)`` for the full rationale.
Expand Down Expand Up @@ -724,7 +724,7 @@ class ChaisemartinDHaultfoeuille(ChaisemartinDHaultfoeuilleBootstrapMixin, BaseE
... data, outcome="outcome", unit="group",
... time="period", treatment="treatment",
... )
>>> abs(results.overall_att - 2.0) < 1.0 # close to the true effect
>>> abs(results.att - 2.0) < 1.0 # close to the true effect
True
"""

Expand Down Expand Up @@ -1567,7 +1567,7 @@ def fit(
f"switch group(s). The fitted results.twfe_* values (if "
f"the diagnostic succeeded) were computed on the FULL "
f"pre-filter cell sample, so they describe a LARGER "
f"sample (pre-filter) than overall_att. The standalone "
f"sample (pre-filter) than att. The standalone "
f"twowayfeweights() function also uses the pre-filter "
f"sample. This is the documented Phase 1 contract — see "
f"REGISTRY.md ChaisemartinDHaultfoeuille `Note (TWFE "
Expand Down
74 changes: 37 additions & 37 deletions diff_diff/chaisemartin_dhaultfoeuille_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ class ChaisemartinDHaultfoeuilleResults(BaseResults, AggregationMixin):
estimation sample described by ``overall_att`` and
``units``. When ``fit()`` drops groups via the ragged-panel
or ``drop_larger_lower`` filters, ``results.twfe_*`` and
``results.overall_att`` describe different samples and a
``results.att`` describe different samples and a
``UserWarning`` is emitted; see REGISTRY.md
``ChaisemartinDHaultfoeuille`` ``Note (TWFE diagnostic
sample contract)`` for the rationale. Only populated when
Expand Down Expand Up @@ -760,15 +760,15 @@ def _aggregate_compute(
# The delta is weighted over horizon-specific N_l - no truthful
# scalar count exists.
n_val, n_kind = float("nan"), None
ci = self.overall_conf_int if self.overall_conf_int is not None else (np.nan, np.nan)
ci = self.conf_int if self.conf_int is not None else (np.nan, np.nan)
return AggregationResult(
level="simple",
label=np.array(["overall"], dtype=object),
target=np.array([self._estimand_label()], dtype=object),
att=np.array([self.overall_att], dtype=float),
se=np.array([self.overall_se], dtype=float),
t_stat=np.array([self.overall_t_stat], dtype=float),
p_value=np.array([self.overall_p_value], dtype=float),
att=np.array([self.att], dtype=float),
se=np.array([self.se], dtype=float),
t_stat=np.array([self.t_stat], dtype=float),
p_value=np.array([self.p_value], dtype=float),
conf_int_lower=np.array([ci[0]], dtype=float),
conf_int_upper=np.array([ci[1]], dtype=float),
n=np.array([n_val], dtype=float),
Expand Down Expand Up @@ -877,34 +877,34 @@ def t_stat(self) -> float:

def __repr__(self) -> str:
"""Concise string representation."""
sig = _get_significance_stars(self.overall_p_value)
sig = _get_significance_stars(self.p_value)
label = self._estimand_label()
return (
f"ChaisemartinDHaultfoeuilleResults("
f"{label}={self.overall_att:.4f}{sig}, "
f"SE={self.overall_se:.4f}, "
f"{label}={self.att:.4f}{sig}, "
f"SE={self.se:.4f}, "
f"n_units={len(self.units)}, "
f"n_switcher_cells={self.n_switcher_cells})"
)

@property
def coef_var(self) -> float:
"""SE / abs(DID_M); NaN when DID_M is 0 or SE non-finite."""
if not (np.isfinite(self.overall_se) and self.overall_se >= 0):
if not (np.isfinite(self.se) and self.se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
if not np.isfinite(self.att) or self.att == 0:
return np.nan
return self.overall_se / abs(self.overall_att)
return self.se / abs(self.att)

@property
def is_significant(self) -> bool:
"""True iff overall ``DID_M`` p-value is below ``alpha``."""
return bool(self.overall_p_value < self.alpha)
return bool(self.p_value < self.alpha)

@property
def significance_stars(self) -> str:
"""Significance stars for the overall ``DID_M``."""
return _get_significance_stars(self.overall_p_value)
return _get_significance_stars(self.p_value)

# ------------------------------------------------------------------
# Summary
Expand Down Expand Up @@ -1013,16 +1013,16 @@ def summary(self, alpha: Optional[float] = None) -> str:
thin,
_format_inference_row(
overall_row_label,
self.overall_att,
self.overall_se,
self.overall_t_stat,
self.overall_p_value,
self.att,
self.se,
self.t_stat,
self.p_value,
),
thin,
"",
f"{conf_level}% Confidence Interval: "
f"[{_fmt_float(self.overall_conf_int[0])}, "
f"{_fmt_float(self.overall_conf_int[1])}]",
f"[{_fmt_float(self.conf_int[0])}, "
f"{_fmt_float(self.conf_int[1])}]",
]
)

Expand All @@ -1037,7 +1037,7 @@ def summary(self, alpha: Optional[float] = None) -> str:
)
# Footer labeling is keyed off what the displayed fields actually
# are — with the bootstrap-contract NaN-on-invalid fix on the fit()
# side, ``self.overall_se`` is NaN when the bootstrap produced
# side, ``self.se`` is NaN when the bootstrap produced
# non-finite output, so the "multiplier-bootstrap percentile
# inference" claim correctly fires only when the displayed overall
# SE / p-value / CI were actually populated from finite bootstrap
Expand Down Expand Up @@ -1079,15 +1079,15 @@ def summary(self, alpha: Optional[float] = None) -> str:
np.isfinite(v.get("crit_value", np.nan)) for v in self.path_sup_t_bands.values()
)
any_finite_bootstrap_inference = (
np.isfinite(self.overall_se)
np.isfinite(self.se)
or event_study_has_finite_bootstrap_se
or joiners_has_finite_bootstrap_se
or leavers_has_finite_bootstrap_se
or path_effects_has_finite_bootstrap_se
or path_placebo_has_finite_bootstrap_se
or path_sup_t_has_finite_crit
)
if self.bootstrap_results is not None and np.isfinite(self.overall_se) and not is_delta:
if self.bootstrap_results is not None and np.isfinite(self.se) and not is_delta:
lines.append("Note: p-value and CI are multiplier-bootstrap percentile inference")
lines.append(
f" ({self.bootstrap_results.n_bootstrap} iterations, "
Expand Down Expand Up @@ -1648,8 +1648,8 @@ def to_dict(self) -> Dict[str, Any]:
"se": self.se,
"t_stat": self.t_stat,
"p_value": self.p_value,
"conf_int_lower": self.overall_conf_int[0],
"conf_int_upper": self.overall_conf_int[1],
"conf_int_lower": self.conf_int[0],
"conf_int_upper": self.conf_int[1],
"n_obs": self.n_obs,
"n_treated_obs": self.n_treated_obs,
"n_switcher_cells": self.n_switcher_cells,
Expand Down Expand Up @@ -1749,12 +1749,12 @@ def to_dataframe(self, level: str = "overall") -> pd.DataFrame:
[
{
"estimand": self._estimand_label(),
"effect": self.overall_att,
"se": self.overall_se,
"t_stat": self.overall_t_stat,
"p_value": self.overall_p_value,
"conf_int_lower": self.overall_conf_int[0],
"conf_int_upper": self.overall_conf_int[1],
"effect": self.att,
"se": self.se,
"t_stat": self.t_stat,
"p_value": self.p_value,
"conf_int_lower": self.conf_int[0],
"conf_int_upper": self.conf_int[1],
}
]
)
Expand All @@ -1774,12 +1774,12 @@ def to_dataframe(self, level: str = "overall") -> pd.DataFrame:
rows = [
{
"estimand": overall_est_label,
"effect": self.overall_att,
"se": self.overall_se,
"t_stat": self.overall_t_stat,
"p_value": self.overall_p_value,
"conf_int_lower": self.overall_conf_int[0],
"conf_int_upper": self.overall_conf_int[1],
"effect": self.att,
"se": self.se,
"t_stat": self.t_stat,
"p_value": self.p_value,
"conf_int_lower": self.conf_int[0],
"conf_int_upper": self.conf_int[1],
"n_cells": self.n_switcher_cells,
"n_obs": (
self.n_treated_obs
Expand Down
4 changes: 2 additions & 2 deletions diff_diff/continuous_did.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
adjust the dose-response *shape* (ACRT(d) would be identical to the
unconditional fit); it raises ``NotImplementedError``. ``reg`` and ``dr``
share the dose-response shape and ACRT(d); ``dr`` differs only in the
``overall_att`` / ATT(d) level and in its doubly-robust standard errors.
``att`` / ATT(d) level and in its doubly-robust standard errors.
pscore_trim : float, default=0.01
Propensity-score trimming bound for the ``dr`` path (scores clipped to
``[pscore_trim, 1 - pscore_trim]``). Must be in ``(0, 0.5)`` and
Expand Down Expand Up @@ -248,7 +248,7 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
>>> results = est.fit(data, outcome="outcome", unit="unit",
... time="period", first_treat="first_treat",
... dose="dose")
>>> results.overall_att # doctest: +SKIP
>>> results.att # doctest: +SKIP
>>> results.aggregate("dose") # doctest: +SKIP
"""

Expand Down
38 changes: 19 additions & 19 deletions diff_diff/continuous_did_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,11 @@ def overall_t_stat(self) -> float:
return self.overall_att_t_stat

def __repr__(self) -> str:
sig_att = _get_significance_stars(self.overall_att_p_value)
sig_att = _get_significance_stars(self.p_value)
sig_acrt = _get_significance_stars(self.overall_acrt_p_value)
return (
f"ContinuousDiDResults("
f"ATT_glob={self.overall_att:.4f}{sig_att}, "
f"ATT_glob={self.att:.4f}{sig_att}, "
f"ACRT_glob={self.overall_acrt:.4f}{sig_acrt}, "
f"n_groups={len(self.groups)}, "
f"n_periods={len(self.time_periods)})"
Expand All @@ -261,11 +261,11 @@ def __repr__(self) -> str:
@property
def coef_var(self) -> float:
"""Coefficient of variation: SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite."""
if not (np.isfinite(self.overall_att_se) and self.overall_att_se >= 0):
if not (np.isfinite(self.se) and self.se >= 0):
return np.nan
if not np.isfinite(self.overall_att) or self.overall_att == 0:
if not np.isfinite(self.att) or self.att == 0:
return np.nan
return self.overall_att_se / abs(self.overall_att)
return self.se / abs(self.att)

def summary(self, alpha: Optional[float] = None) -> str:
"""Generate formatted summary.
Expand Down Expand Up @@ -332,10 +332,10 @@ def summary(self, alpha: Optional[float] = None) -> str:
for label, est, se, t, p in [
(
"ATT_glob",
self.overall_att,
self.overall_att_se,
self.overall_att_t_stat,
self.overall_att_p_value,
self.att,
self.se,
self.t_stat,
self.p_value,
),
(
"ACRT_glob",
Expand All @@ -354,7 +354,7 @@ def summary(self, alpha: Optional[float] = None) -> str:
"-" * w,
"",
f"{conf_level}% CI for ATT_glob: "
f"[{self.overall_att_conf_int[0]:.4f}, {self.overall_att_conf_int[1]:.4f}]",
f"[{self.conf_int[0]:.4f}, {self.conf_int[1]:.4f}]",
f"{conf_level}% CI for ACRT_glob: "
f"[{self.overall_acrt_conf_int[0]:.4f}, {self.overall_acrt_conf_int[1]:.4f}]",
]
Expand Down Expand Up @@ -443,8 +443,8 @@ def to_dict(self) -> Dict[str, Any]:
"se": self.se,
"t_stat": self.t_stat,
"p_value": self.p_value,
"conf_int_lower": self.overall_att_conf_int[0],
"conf_int_upper": self.overall_att_conf_int[1],
"conf_int_lower": self.conf_int[0],
"conf_int_upper": self.conf_int[1],
"acrt": self.overall_acrt,
"acrt_se": self.overall_acrt_se,
"acrt_t_stat": self.overall_acrt_t_stat,
Expand Down Expand Up @@ -536,12 +536,12 @@ def to_dataframe(self, level: str = "dose_response") -> pd.DataFrame:
@property
def is_significant(self) -> bool:
"""Check if overall ATT is significant."""
return bool(self.overall_att_p_value < self.alpha)
return bool(self.p_value < self.alpha)

@property
def significance_stars(self) -> str:
"""Significance stars for overall ATT."""
return _get_significance_stars(self.overall_att_p_value)
return _get_significance_stars(self.p_value)

# ------------------------------------------------------------------
# Post-fit aggregation (row M-025, on the M-122 contract).
Expand Down Expand Up @@ -586,19 +586,19 @@ def _aggregate_compute(
# bit-exact - on bootstrap fits the stored quintet includes a
# FINITE safe_inference t beside the percentile p/CI and it
# relays through unchanged; only the df column is NaN there.
att_ci = self.overall_att_conf_int
att_ci = self.conf_int
acrt_ci = self.overall_acrt_conf_int
n_total = float(self.n_treated_units + self.n_control_units)
df_val = self._stored_inference_df()
return AggregationResult(
level="simple",
label=np.array(["overall", "overall"], dtype=object),
target=np.array(["att", "acrt"], dtype=object),
att=np.array([self.overall_att, self.overall_acrt], dtype=float),
se=np.array([self.overall_att_se, self.overall_acrt_se], dtype=float),
t_stat=np.array([self.overall_att_t_stat, self.overall_acrt_t_stat], dtype=float),
att=np.array([self.att, self.overall_acrt], dtype=float),
se=np.array([self.se, self.overall_acrt_se], dtype=float),
t_stat=np.array([self.t_stat, self.overall_acrt_t_stat], dtype=float),
p_value=np.array(
[self.overall_att_p_value, self.overall_acrt_p_value],
[self.p_value, self.overall_acrt_p_value],
dtype=float,
),
conf_int_lower=np.array([att_ci[0], acrt_ci[0]], dtype=float),
Expand Down
2 changes: 1 addition & 1 deletion diff_diff/dml_did_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def __repr__(self) -> str:
if self.effective_n_folds is not None:
folds += f" (effective {self.effective_n_folds})"
return (
f"DMLDiDResults(att={self.overall_att:.6g}, se={self.overall_se:.6g}, "
f"DMLDiDResults(att={self.att:.6g}, se={self.se:.6g}, "
f"n_cells={len(self.group_time_effects)}, {folds}, "
f"propensity_learner={self.propensity_learner!r}, "
f"outcome_learner={self.outcome_learner!r})"
Expand Down
8 changes: 4 additions & 4 deletions diff_diff/efficient_did.py
Original file line number Diff line number Diff line change
Expand Up @@ -1536,8 +1536,8 @@ def _nan_result() -> HausmanPretestResult:
df=0,
reject=False,
alpha=alpha,
att_all=result_all.overall_att,
att_post=result_post.overall_att,
att_all=result_all.att,
att_post=result_post.att,
recommendation="inconclusive",
gt_details=None,
)
Expand Down Expand Up @@ -1712,8 +1712,8 @@ def _eif_cov(eif_mat: np.ndarray) -> np.ndarray:
df=effective_rank,
reject=reject,
alpha=alpha,
att_all=result_all.overall_att,
att_post=result_post.overall_att,
att_all=result_all.att,
att_post=result_post.att,
recommendation="pt_post" if reject else "pt_all",
gt_details=es_details,
)
Loading
Loading