diff --git a/changelog.d/20261026-v4-pr5a-overall-att-readers.md b/changelog.d/20261026-v4-pr5a-overall-att-readers.md new file mode 100644 index 000000000..f26a819d4 --- /dev/null +++ b/changelog.d/20261026-v4-pr5a-overall-att-readers.md @@ -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. diff --git a/diff_diff/chaisemartin_dhaultfoeuille.py b/diff_diff/chaisemartin_dhaultfoeuille.py index 8594fdecb..1d1f7bace 100644 --- a/diff_diff/chaisemartin_dhaultfoeuille.py +++ b/diff_diff/chaisemartin_dhaultfoeuille.py @@ -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. @@ -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 """ @@ -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 " diff --git a/diff_diff/chaisemartin_dhaultfoeuille_results.py b/diff_diff/chaisemartin_dhaultfoeuille_results.py index ef9f09b1f..2270b2a8e 100644 --- a/diff_diff/chaisemartin_dhaultfoeuille_results.py +++ b/diff_diff/chaisemartin_dhaultfoeuille_results.py @@ -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 @@ -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), @@ -877,12 +877,12 @@ 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})" ) @@ -890,21 +890,21 @@ def __repr__(self) -> str: @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 @@ -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])}]", ] ) @@ -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 @@ -1079,7 +1079,7 @@ 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 @@ -1087,7 +1087,7 @@ def summary(self, alpha: Optional[float] = None) -> str: 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, " @@ -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, @@ -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], } ] ) @@ -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 diff --git a/diff_diff/continuous_did.py b/diff_diff/continuous_did.py index 48bc263a0..d07883327 100644 --- a/diff_diff/continuous_did.py +++ b/diff_diff/continuous_did.py @@ -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 @@ -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 """ diff --git a/diff_diff/continuous_did_results.py b/diff_diff/continuous_did_results.py index 4121e9980..52ba4ee1c 100644 --- a/diff_diff/continuous_did_results.py +++ b/diff_diff/continuous_did_results.py @@ -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)})" @@ -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. @@ -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", @@ -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}]", ] @@ -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, @@ -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). @@ -586,7 +586,7 @@ 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() @@ -594,11 +594,11 @@ def _aggregate_compute( 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), diff --git a/diff_diff/dml_did_results.py b/diff_diff/dml_did_results.py index a0c4daa5d..206630c9a 100644 --- a/diff_diff/dml_did_results.py +++ b/diff_diff/dml_did_results.py @@ -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})" diff --git a/diff_diff/efficient_did.py b/diff_diff/efficient_did.py index 339f3bb9b..9f2c5d047 100644 --- a/diff_diff/efficient_did.py +++ b/diff_diff/efficient_did.py @@ -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, ) @@ -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, ) diff --git a/diff_diff/efficient_did_results.py b/diff_diff/efficient_did_results.py index 9191e931c..548bce718 100644 --- a/diff_diff/efficient_did_results.py +++ b/diff_diff/efficient_did_results.py @@ -520,12 +520,12 @@ def _aggregate_simple_result(self, kit: Any) -> AggregationResult: level="simple", label=np.array(["overall"], dtype=object), target=np.array(["att"], 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), - conf_int_lower=np.array([self.overall_conf_int[0]], dtype=float), - conf_int_upper=np.array([self.overall_conf_int[1]], 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([self.conf_int[0]], dtype=float), + conf_int_upper=np.array([self.conf_int[1]], dtype=float), n=np.array([kit.bookkeeping["n_units_total"]], dtype=float), df=df_val, alpha=kit.alpha, @@ -575,11 +575,11 @@ def _aggregate_total_result(self, kit: Any) -> AggregationResult: mass = support if n_keepers > 0 else float(np.nan) return build_total_relay_row( mass=mass, - att=self.overall_att, - se=self.overall_se, - t_stat=self.overall_t_stat, - p_value=self.overall_p_value, - conf_int=self.overall_conf_int, + att=self.att, + se=self.se, + t_stat=self.t_stat, + p_value=self.p_value, + conf_int=self.conf_int, df=(np.nan if self.bootstrap_results is not None else bk["df_survey"]), alpha=kit.alpha, estimator=type(self).__name__.replace("Results", ""), @@ -625,11 +625,11 @@ def _group_effects_to_aggregation( ) def __repr__(self) -> str: - sig = _get_significance_stars(self.overall_p_value) + sig = _get_significance_stars(self.p_value) path = "DR" if self.estimation_path == "dr" else "nocov" return ( - f"EfficientDiDResults(ATT={self.overall_att:.4f}{sig}, " - f"SE={self.overall_se:.4f}, " + f"EfficientDiDResults(ATT={self.att:.4f}{sig}, " + f"SE={self.se:.4f}, " f"pt={self.pt_assumption}, path={path}, " f"n_groups={len(self.groups)}, " f"n_periods={len(self.time_periods)})" @@ -638,11 +638,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_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) def summary(self, alpha: Optional[float] = None) -> str: """Generate formatted summary of estimation results. @@ -717,13 +717,13 @@ def summary(self, alpha: Optional[float] = None) -> str: f"{'Parameter':<15} {'Estimate':>12} {'Std. Err.':>12} " f"{'t-stat':>10} {'P>|t|':>10} {'Sig.':>6}", "-" * 85, - f"{'ATT':<15} {self.overall_att:>12.4f} {self.overall_se:>12.4f} " - f"{self.overall_t_stat:>10.3f} {self.overall_p_value:>10.4f} " - f"{_get_significance_stars(self.overall_p_value):>6}", + f"{'ATT':<15} {self.att:>12.4f} {self.se:>12.4f} " + f"{self.t_stat:>10.3f} {self.p_value:>10.4f} " + f"{_get_significance_stars(self.p_value):>6}", "-" * 85, "", f"{conf_level}% Confidence Interval: " - f"[{self.overall_conf_int[0]:.4f}, {self.overall_conf_int[1]:.4f}]", + f"[{self.conf_int[0]:.4f}, {self.conf_int[1]:.4f}]", ] ) @@ -877,12 +877,12 @@ def to_dict(self) -> Dict[str, Any]: the full summary. """ result: Dict[str, Any] = { - "att": 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], + "att": 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_obs": self.n_obs, "n_treated_units": self.n_treated_units, "n_control_units": self.n_control_units, @@ -911,9 +911,9 @@ def to_dict(self) -> Dict[str, Any]: @property def is_significant(self) -> bool: """Check if overall ATT is significant.""" - return bool(self.overall_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_p_value) + return _get_significance_stars(self.p_value) diff --git a/diff_diff/imputation_results.py b/diff_diff/imputation_results.py index 84c979ba6..63b2d52a6 100644 --- a/diff_diff/imputation_results.py +++ b/diff_diff/imputation_results.py @@ -456,12 +456,12 @@ def _aggregate_simple_result(self, kit: Any) -> AggregationResult: level="simple", label=np.array(["overall"], dtype=object), target=np.array(["att"], 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), - conf_int_lower=np.array([self.overall_conf_int[0]], dtype=float), - conf_int_upper=np.array([self.overall_conf_int[1]], 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([self.conf_int[0]], dtype=float), + conf_int_upper=np.array([self.conf_int[1]], dtype=float), n=np.array([kit.bookkeeping["n_treated_obs"]], dtype=float), df=( np.nan if self.bootstrap_results is not None else kit.bookkeeping["survey_df_final"] @@ -506,11 +506,11 @@ def _aggregate_total_result(self, kit: Any) -> AggregationResult: mass = float(support) if support > 0 else float(np.nan) return build_total_relay_row( mass=mass, - att=self.overall_att, - se=self.overall_se, - t_stat=self.overall_t_stat, - p_value=self.overall_p_value, - conf_int=self.overall_conf_int, + att=self.att, + se=self.se, + t_stat=self.t_stat, + p_value=self.p_value, + conf_int=self.conf_int, df=(np.nan if self.bootstrap_results is not None else bk["survey_df_final"]), alpha=kit.alpha, estimator=type(self).__name__.replace("Results", ""), @@ -558,10 +558,10 @@ def _group_effects_to_aggregation( def __repr__(self) -> str: """Concise string representation.""" - sig = _get_significance_stars(self.overall_p_value) + sig = _get_significance_stars(self.p_value) return ( - f"ImputationDiDResults(ATT={self.overall_att:.4f}{sig}, " - f"SE={self.overall_se:.4f}, " + f"ImputationDiDResults(ATT={self.att:.4f}{sig}, " + f"SE={self.se:.4f}, " f"n_groups={len(self.groups)}, " f"n_treated_obs={self.n_treated_obs})" ) @@ -569,11 +569,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_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) def summary(self, alpha: Optional[float] = None) -> str: """ @@ -666,24 +666,17 @@ def summary(self, alpha: Optional[float] = None) -> str: ] ) - t_str = ( - f"{self.overall_t_stat:>10.3f}" if np.isfinite(self.overall_t_stat) else f"{'NaN':>10}" - ) - p_str = ( - f"{self.overall_p_value:>10.4f}" - if np.isfinite(self.overall_p_value) - else f"{'NaN':>10}" - ) - sig = _get_significance_stars(self.overall_p_value) + t_str = f"{self.t_stat:>10.3f}" if np.isfinite(self.t_stat) else f"{'NaN':>10}" + p_str = f"{self.p_value:>10.4f}" if np.isfinite(self.p_value) else f"{'NaN':>10}" + sig = _get_significance_stars(self.p_value) lines.extend( [ - f"{'ATT':<15} {self.overall_att:>12.4f} {self.overall_se:>12.4f} " - f"{t_str} {p_str} {sig:>6}", + f"{'ATT':<15} {self.att:>12.4f} {self.se:>12.4f} " f"{t_str} {p_str} {sig:>6}", "-" * 85, "", f"{conf_level}% Confidence Interval: " - f"[{self.overall_conf_int[0]:.4f}, {self.overall_conf_int[1]:.4f}]", + f"[{self.conf_int[0]:.4f}, {self.conf_int[1]:.4f}]", ] ) @@ -888,12 +881,12 @@ def to_dict(self) -> Dict[str, Any]: :meth:`to_dataframe`. """ result: Dict[str, Any] = { - "att": 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], + "att": 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_obs": self.n_obs, "n_treated_obs": self.n_treated_obs, "n_untreated_obs": self.n_untreated_obs, @@ -953,9 +946,9 @@ def pretrend_test(self, n_leads: Optional[int] = None) -> Dict[str, Any]: @property def is_significant(self) -> bool: """Check if overall ATT is significant.""" - return bool(self.overall_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_p_value) + return _get_significance_stars(self.p_value) diff --git a/diff_diff/power.py b/diff_diff/power.py index fd4f48c22..48cbd1af9 100644 --- a/diff_diff/power.py +++ b/diff_diff/power.py @@ -478,27 +478,6 @@ def _extract_simple(result: Any) -> Tuple[float, float, float, Tuple[float, floa return (result.att, result.se, result.p_value, result.conf_int) -def _extract_staggered( - result: Any, -) -> Tuple[float, float, float, Tuple[float, float]]: - _nan = float("nan") - _nan_ci = (_nan, _nan) - - def _first(r: Any, *attrs: str, default: Any = _nan) -> Any: - for a in attrs: - v = getattr(r, a, None) - if v is not None: - return v - return default - - return ( - result.overall_att, - _first(result, "overall_se", "overall_att_se"), - _first(result, "overall_p_value", "overall_att_p_value"), - _first(result, "overall_conf_int", "overall_att_ci", default=_nan_ci), - ) - - # Keys derived from simulate_power() public params — overriding these # via data_generator_kwargs would desync the DGP from the result object. _PROTECTED_DGP_KEYS = frozenset( @@ -917,42 +896,42 @@ def _get_registry() -> Dict[str, _EstimatorProfile]: default_dgp=generate_staggered_data, dgp_kwargs_builder=_staggered_dgp_kwargs, fit_kwargs_builder=_staggered_fit_kwargs, - result_extractor=_extract_staggered, + result_extractor=_extract_simple, min_n=40, ), "SunAbraham": _EstimatorProfile( default_dgp=generate_staggered_data, dgp_kwargs_builder=_staggered_dgp_kwargs, fit_kwargs_builder=_staggered_fit_kwargs, - result_extractor=_extract_staggered, + result_extractor=_extract_simple, min_n=40, ), "ImputationDiD": _EstimatorProfile( default_dgp=generate_staggered_data, dgp_kwargs_builder=_staggered_dgp_kwargs, fit_kwargs_builder=_staggered_fit_kwargs, - result_extractor=_extract_staggered, + result_extractor=_extract_simple, min_n=40, ), "TwoStageDiD": _EstimatorProfile( default_dgp=generate_staggered_data, dgp_kwargs_builder=_staggered_dgp_kwargs, fit_kwargs_builder=_staggered_fit_kwargs, - result_extractor=_extract_staggered, + result_extractor=_extract_simple, min_n=40, ), "StackedDiD": _EstimatorProfile( default_dgp=generate_staggered_data, dgp_kwargs_builder=_staggered_dgp_kwargs, fit_kwargs_builder=_staggered_fit_kwargs, - result_extractor=_extract_staggered, + result_extractor=_extract_simple, min_n=40, ), "EfficientDiD": _EstimatorProfile( default_dgp=generate_staggered_data, dgp_kwargs_builder=_staggered_dgp_kwargs, fit_kwargs_builder=_staggered_fit_kwargs, - result_extractor=_extract_staggered, + result_extractor=_extract_simple, min_n=40, ), # --- Factor model group --- diff --git a/diff_diff/practitioner.py b/diff_diff/practitioner.py index 129f6685e..8585c58e4 100644 --- a/diff_diff/practitioner.py +++ b/diff_diff/practitioner.py @@ -340,8 +340,8 @@ def _covariates_step() -> Dict[str, Any]: "# Re-estimate without covariates and compare:\n" "result_no_cov = estimator.fit(data, ..., covariates=None)\n" "# Compare ATT with and without covariates.\n" - "# Use .att (basic DiD; also a read-only flat-alias on staggered\n" - "# classes) or .overall_att (canonical name on staggered results)." + "# Read .att on every estimator result (the canonical headline name;\n" + "# staggered results expose the same value under the 3.x overall_att)." ), priority="medium", step_name="robustness", @@ -871,7 +871,7 @@ def _handle_synthetic(results: Any): "from diff_diff import CallawaySantAnna\n" "cs = CallawaySantAnna()\n" "cs_result = cs.fit(data, ...)\n" - "print(f'SDiD ATT: {results.att:.4f}, CS ATT: {cs_result.overall_att:.4f}')" + "print(f'SDiD ATT: {results.att:.4f}, CS ATT: {cs_result.att:.4f}')" ), step_name="robustness", ), @@ -2511,10 +2511,10 @@ def _check_nan_att(results: Any) -> List[str]: too: any future estimator returning ndarray ``att`` without a dedicated handler gets the same all-NaN warning shape. """ - # Check .att (DiDResults), .overall_att (staggered) + # Every estimator result exposes the canonical ``att`` (the staggered + # containers' 3.x ``overall_att`` storage is read through it); results + # without a scalar headline (BaconDecompositionResults) take the None path. att = getattr(results, "att", None) - if att is None: - att = getattr(results, "overall_att", None) if att is None: return [] try: diff --git a/diff_diff/prep_dgp.py b/diff_diff/prep_dgp.py index 388246125..0781bc95d 100644 --- a/diff_diff/prep_dgp.py +++ b/diff_diff/prep_dgp.py @@ -206,7 +206,7 @@ def generate_staggered_data( >>> cs = CallawaySantAnna() >>> results = cs.fit(data, outcome='outcome', unit='unit', ... time='period', first_treat='first_treat') - >>> results.overall_att > 0 + >>> results.att > 0 True """ rng = np.random.default_rng(seed) diff --git a/diff_diff/stacked_did_results.py b/diff_diff/stacked_did_results.py index 2d1188ac2..252de501f 100644 --- a/diff_diff/stacked_did_results.py +++ b/diff_diff/stacked_did_results.py @@ -242,15 +242,15 @@ def _aggregate_compute( # trimmed aggregate ATT, so weighting-specific target strings # would misstate the scalar (``describe_target_parameter`` is the # estimand's prose source of truth). - 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(["att"], 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([float(self.n_treated_units)], dtype=float), @@ -287,10 +287,10 @@ 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) return ( - f"StackedDiDResults(ATT={self.overall_att:.4f}{sig}, " - f"SE={self.overall_se:.4f}, " + f"StackedDiDResults(ATT={self.att:.4f}{sig}, " + f"SE={self.se:.4f}, " f"n_sub_exp={self.n_sub_experiments}, " f"n_stacked_obs={self.n_stacked_obs})" ) @@ -298,11 +298,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_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) def summary(self, alpha: Optional[float] = None) -> str: """ @@ -379,24 +379,17 @@ def summary(self, alpha: Optional[float] = None) -> str: ] ) - t_str = ( - f"{self.overall_t_stat:>10.3f}" if np.isfinite(self.overall_t_stat) else f"{'NaN':>10}" - ) - p_str = ( - f"{self.overall_p_value:>10.4f}" - if np.isfinite(self.overall_p_value) - else f"{'NaN':>10}" - ) - sig = _get_significance_stars(self.overall_p_value) + t_str = f"{self.t_stat:>10.3f}" if np.isfinite(self.t_stat) else f"{'NaN':>10}" + p_str = f"{self.p_value:>10.4f}" if np.isfinite(self.p_value) else f"{'NaN':>10}" + sig = _get_significance_stars(self.p_value) lines.extend( [ - f"{'ATT':<15} {self.overall_att:>12.4f} {self.overall_se:>12.4f} " - f"{t_str} {p_str} {sig:>6}", + f"{'ATT':<15} {self.att:>12.4f} {self.se:>12.4f} " f"{t_str} {p_str} {sig:>6}", "-" * 85, "", f"{conf_level}% Confidence Interval: " - f"[{self.overall_conf_int[0]:.4f}, {self.overall_conf_int[1]:.4f}]", + f"[{self.conf_int[0]:.4f}, {self.conf_int[1]:.4f}]", ] ) @@ -508,8 +501,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_stacked_obs": self.n_stacked_obs, "n_sub_experiments": self.n_sub_experiments, @@ -588,9 +581,9 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: @property def is_significant(self) -> bool: """Check if overall ATT is significant.""" - return bool(self.overall_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_p_value) + return _get_significance_stars(self.p_value) diff --git a/diff_diff/staggered_results.py b/diff_diff/staggered_results.py index 88575e889..c4140c025 100644 --- a/diff_diff/staggered_results.py +++ b/diff_diff/staggered_results.py @@ -328,10 +328,10 @@ 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) return ( - f"CallawaySantAnnaResults(ATT={self.overall_att:.4f}{sig}, " - f"SE={self.overall_se:.4f}, " + f"CallawaySantAnnaResults(ATT={self.att:.4f}{sig}, " + f"SE={self.se:.4f}, " f"n_groups={len(self.groups)}, " f"n_periods={len(self.time_periods)})" ) @@ -339,11 +339,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_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) # ------------------------------------------------------------------ # # Post-fit aggregation (spec section 6; ledger rows M-020 / M-117) @@ -527,15 +527,15 @@ def _aggregate_simple_result(self, kit: Any) -> AggregationResult: # the shared vocabulary forbids. is_panel = kit.bookkeeping.get("is_panel", True) n_kind = "units" if is_panel else "obs" - ci = self.overall_conf_int or (np.nan, np.nan) + ci = self.conf_int or (np.nan, np.nan) return AggregationResult( level="simple", label=np.array(["overall"], dtype=object), target=np.array(["att"], 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([float(self.n_treated_units + self.n_control_units)], dtype=float), @@ -601,13 +601,13 @@ def _aggregate_total_result(self, kit: Any) -> AggregationResult: "DEFERRED.md. For an unweighted clustered fit, cluster= " "(without survey_design) supports totals." ) - ci = self.overall_conf_int or (np.nan, np.nan) + ci = self.conf_int or (np.nan, np.nan) return build_total_relay_row( mass=self._cs_total_mass(kit), - att=self.overall_att, - se=self.overall_se, - t_stat=self.overall_t_stat, - p_value=self.overall_p_value, + att=self.att, + se=self.se, + t_stat=self.t_stat, + p_value=self.p_value, conf_int=ci, df=(np.nan if self.bootstrap_results is not None else resolve_inference_df(self)), alpha=self.alpha, @@ -763,12 +763,12 @@ def summary(self, alpha: Optional[float] = None) -> str: "-" * 85, f"{'Parameter':<15} {'Estimate':>12} {'Std. Err.':>12} {'t-stat':>10} {'P>|t|':>10} {'Sig.':>6}", "-" * 85, - f"{'ATT':<15} {self.overall_att:>12.4f} {self.overall_se:>12.4f} " - f"{self.overall_t_stat:>10.3f} {self.overall_p_value:>10.4f} " - f"{_get_significance_stars(self.overall_p_value):>6}", + f"{'ATT':<15} {self.att:>12.4f} {self.se:>12.4f} " + f"{self.t_stat:>10.3f} {self.p_value:>10.4f} " + f"{_get_significance_stars(self.p_value):>6}", "-" * 85, "", - f"{conf_level}% Confidence Interval: [{self.overall_conf_int[0]:.4f}, {self.overall_conf_int[1]:.4f}]", + f"{conf_level}% Confidence Interval: [{self.conf_int[0]:.4f}, {self.conf_int[1]:.4f}]", ] ) @@ -914,8 +914,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_units": self.n_treated_units, "n_control_units": self.n_control_units, @@ -1022,9 +1022,9 @@ def to_dataframe(self, level: str = "group_time") -> pd.DataFrame: @property def is_significant(self) -> bool: """Check if overall ATT is significant.""" - return bool(self.overall_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_p_value) + return _get_significance_stars(self.p_value) diff --git a/diff_diff/sun_abraham.py b/diff_diff/sun_abraham.py index 33d78e6c0..c45ee40f1 100644 --- a/diff_diff/sun_abraham.py +++ b/diff_diff/sun_abraham.py @@ -238,11 +238,11 @@ 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) n_rel_periods = len(self.event_study_effects) return ( - f"SunAbrahamResults(ATT={self.overall_att:.4f}{sig}, " - f"SE={self.overall_se:.4f}, " + f"SunAbrahamResults(ATT={self.att:.4f}{sig}, " + f"SE={self.se:.4f}, " f"n_groups={len(self.groups)}, " f"n_rel_periods={n_rel_periods})" ) @@ -250,11 +250,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_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) def summary(self, alpha: Optional[float] = None) -> str: """ @@ -321,13 +321,13 @@ def summary(self, alpha: Optional[float] = None) -> str: f"{'Parameter':<15} {'Estimate':>12} {'Std. Err.':>12} " f"{'t-stat':>10} {'P>|t|':>10} {'Sig.':>6}", "-" * 85, - f"{'ATT':<15} {self.overall_att:>12.4f} {self.overall_se:>12.4f} " - f"{self.overall_t_stat:>10.3f} {self.overall_p_value:>10.4f} " - f"{_get_significance_stars(self.overall_p_value):>6}", + f"{'ATT':<15} {self.att:>12.4f} {self.se:>12.4f} " + f"{self.t_stat:>10.3f} {self.p_value:>10.4f} " + f"{_get_significance_stars(self.p_value):>6}", "-" * 85, "", f"{conf_level}% Confidence Interval: " - f"[{self.overall_conf_int[0]:.4f}, {self.overall_conf_int[1]:.4f}]", + f"[{self.conf_int[0]:.4f}, {self.conf_int[1]:.4f}]", ] ) @@ -388,8 +388,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_units": self.n_treated_units, "n_control_units": self.n_control_units, @@ -463,12 +463,12 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: @property def is_significant(self) -> bool: """Check if overall ATT is significant.""" - return bool(self.overall_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_p_value) + return _get_significance_stars(self.p_value) @dataclass diff --git a/diff_diff/two_stage_results.py b/diff_diff/two_stage_results.py index 709cd4550..7d7b26ce1 100644 --- a/diff_diff/two_stage_results.py +++ b/diff_diff/two_stage_results.py @@ -450,12 +450,12 @@ def _aggregate_simple_result(self, kit: Any) -> AggregationResult: level="simple", label=np.array(["overall"], dtype=object), target=np.array(["att"], 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), - conf_int_lower=np.array([self.overall_conf_int[0]], dtype=float), - conf_int_upper=np.array([self.overall_conf_int[1]], 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([self.conf_int[0]], dtype=float), + conf_int_upper=np.array([self.conf_int[1]], dtype=float), n=np.array([kit.bookkeeping["n_treated_obs"]], dtype=float), df=( np.nan if self.bootstrap_results is not None else kit.bookkeeping["survey_df_final"] @@ -498,11 +498,11 @@ def _aggregate_total_result(self, kit: Any) -> AggregationResult: mass = support if support > 0 else float(np.nan) return build_total_relay_row( mass=mass, - att=self.overall_att, - se=self.overall_se, - t_stat=self.overall_t_stat, - p_value=self.overall_p_value, - conf_int=self.overall_conf_int, + att=self.att, + se=self.se, + t_stat=self.t_stat, + p_value=self.p_value, + conf_int=self.conf_int, df=(np.nan if self.bootstrap_results is not None else bk["survey_df_final"]), alpha=kit.alpha, estimator=type(self).__name__.replace("Results", ""), @@ -546,10 +546,10 @@ def _group_effects_to_aggregation( def __repr__(self) -> str: """Concise string representation.""" - sig = _get_significance_stars(self.overall_p_value) + sig = _get_significance_stars(self.p_value) return ( - f"TwoStageDiDResults(ATT={self.overall_att:.4f}{sig}, " - f"SE={self.overall_se:.4f}, " + f"TwoStageDiDResults(ATT={self.att:.4f}{sig}, " + f"SE={self.se:.4f}, " f"n_groups={len(self.groups)}, " f"n_treated_obs={self.n_treated_obs})" ) @@ -557,11 +557,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_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) def summary(self, alpha: Optional[float] = None) -> str: """ @@ -649,24 +649,17 @@ def summary(self, alpha: Optional[float] = None) -> str: ] ) - t_str = ( - f"{self.overall_t_stat:>10.3f}" if np.isfinite(self.overall_t_stat) else f"{'NaN':>10}" - ) - p_str = ( - f"{self.overall_p_value:>10.4f}" - if np.isfinite(self.overall_p_value) - else f"{'NaN':>10}" - ) - sig = _get_significance_stars(self.overall_p_value) + t_str = f"{self.t_stat:>10.3f}" if np.isfinite(self.t_stat) else f"{'NaN':>10}" + p_str = f"{self.p_value:>10.4f}" if np.isfinite(self.p_value) else f"{'NaN':>10}" + sig = _get_significance_stars(self.p_value) lines.extend( [ - f"{'ATT':<15} {self.overall_att:>12.4f} {self.overall_se:>12.4f} " - f"{t_str} {p_str} {sig:>6}", + f"{'ATT':<15} {self.att:>12.4f} {self.se:>12.4f} " f"{t_str} {p_str} {sig:>6}", "-" * 85, "", f"{conf_level}% Confidence Interval: " - f"[{self.overall_conf_int[0]:.4f}, {self.overall_conf_int[1]:.4f}]", + f"[{self.conf_int[0]:.4f}, {self.conf_int[1]:.4f}]", ] ) @@ -856,12 +849,12 @@ def to_dict(self) -> Dict[str, Any]: Headline overall ATT plus inference metadata. """ result: Dict[str, Any] = { - "att": 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], + "att": 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_obs": self.n_obs, "n_treated_obs": self.n_treated_obs, "n_untreated_obs": self.n_untreated_obs, @@ -889,9 +882,9 @@ def to_dict(self) -> Dict[str, Any]: @property def is_significant(self) -> bool: """Check if overall ATT is significant.""" - return bool(self.overall_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_p_value) + return _get_significance_stars(self.p_value) diff --git a/diff_diff/wooldridge.py b/diff_diff/wooldridge.py index 6deeb854d..83c88c5f8 100644 --- a/diff_diff/wooldridge.py +++ b/diff_diff/wooldridge.py @@ -141,7 +141,7 @@ def _require_complete_cell_set( ) -> None: """Every cell the design ASKED for must come back identified. - A finite ``overall_att`` does NOT mean the reported ATT set is complete. + A finite ``att`` does NOT mean the reported ATT set is complete. When rank reduction removes some but not all treatment columns, the survivors are silently re-identified as whatever contrast the remaining design supports -- and they keep their ``ATT(g, t)`` labels. @@ -151,7 +151,7 @@ def _require_complete_cell_set( indicator and the pair is collinear with the time FE. QR drops one; the survivor identifies ``-(effect_4 - effect_3)`` and was reported as ``ATT(3, 5) = -1.9393`` when the true ATT(3, 5) is ``+1.0`` -- wrong SIGN -- - and folded into ``overall_att = +0.7703`` against a true ~1.8. Under + and folded into ``att = +0.7703`` against a true ~1.8. Under ``rank_deficient_action="silent"`` there was no warning at all. So this compares the cells the builder emitted against the coefficients that @@ -434,7 +434,7 @@ def _find_disconnected_cells( observed at ``{2, 4}`` and others at ``{1, 5}`` give exactly that: ``ref=2`` sits in the first component, and ``{1, 5}`` is a closed component whose two cells are collinear. Measured before this check: ``g4_t5`` silently dropped - and ``overall_att`` computed from the single surviving post cell. + and ``att`` computed from the single surviving post cell. On ``not_yet_treated`` (and the nonlinear paths) pre-treatment periods are not emitted at all, so a component holding any pre-period is fine -- which is @@ -2751,7 +2751,7 @@ def _fit_ols( boot_atts.append(att_b) if boot_atts: boot_se = float(np.std(boot_atts, ddof=1)) - t_stat_b, p_b, ci_b = safe_inference(results.overall_att, boot_se, alpha=self.alpha) + t_stat_b, p_b, ci_b = safe_inference(results.att, boot_se, alpha=self.alpha) results.overall_se = boot_se results.overall_t_stat = t_stat_b results.overall_p_value = p_b diff --git a/diff_diff/wooldridge_results.py b/diff_diff/wooldridge_results.py index 538659737..6dbc2a1af 100644 --- a/diff_diff/wooldridge_results.py +++ b/diff_diff/wooldridge_results.py @@ -755,11 +755,11 @@ def _fmt_row(label: str, att: float, se: float, t: float, p: float, ci: Tuple) - lines.append( _fmt_row( "ATT (simple)", - self.overall_att, - self.overall_se, - self.overall_t_stat, - self.overall_p_value, - self.overall_conf_int, + self.att, + self.se, + self.t_stat, + self.p_value, + self.conf_int, ) ) @@ -782,8 +782,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], "method": self.method, "control_group": self.control_group, "unsupported_period_action": self.unsupported_period_action, @@ -837,12 +837,12 @@ def to_dataframe(self, level: str = "event_study") -> pd.DataFrame: "simple": [ { "label": "ATT", - "att": self.overall_att, - "se": self.overall_se, - "t_stat": self.overall_t_stat, - "p_value": self.overall_p_value, - "conf_int_lo": self.overall_conf_int[0], - "conf_int_hi": self.overall_conf_int[1], + "att": self.att, + "se": self.se, + "t_stat": self.t_stat, + "p_value": self.p_value, + "conf_int_lo": self.conf_int[0], + "conf_int_hi": self.conf_int[1], "cohort_trends": self.cohort_trends, "aggregation_weights": active_weights, } @@ -958,9 +958,9 @@ def t_stat(self) -> float: def __repr__(self) -> str: n_gt = len(self.group_time_effects) - att_str = f"{self.overall_att:.4f}" if not np.isnan(self.overall_att) else "NaN" - se_str = f"{self.overall_se:.4f}" if not np.isnan(self.overall_se) else "NaN" - p_str = f"{self.overall_p_value:.4f}" if not np.isnan(self.overall_p_value) else "NaN" + att_str = f"{self.att:.4f}" if not np.isnan(self.att) else "NaN" + se_str = f"{self.se:.4f}" if not np.isnan(self.se) else "NaN" + p_str = f"{self.p_value:.4f}" if not np.isnan(self.p_value) else "NaN" # Surface the active simple aggregation scheme (the one that # produced the printed ``overall_*`` values) + cohort_trends # flag so repr is self-describing for downstream consumers. diff --git a/tests/_capture_dml_b0_dr_oracles.py b/tests/_capture_dml_b0_dr_oracles.py index c6e4fbc65..e4ee0635f 100644 --- a/tests/_capture_dml_b0_dr_oracles.py +++ b/tests/_capture_dml_b0_dr_oracles.py @@ -74,8 +74,8 @@ def main(): res = estimator_tier_fit() print("# Estimator tier: ContinuousDiD DR covariate path") - print(f"ESTIMATOR_TIER_OVERALL_ATT = {res.overall_att!r}") - print(f"ESTIMATOR_TIER_OVERALL_ATT_SE = {res.overall_att_se!r}") + print(f"ESTIMATOR_TIER_OVERALL_ATT = {res.att!r}") + print(f"ESTIMATOR_TIER_OVERALL_ATT_SE = {res.se!r}") print(f"ESTIMATOR_TIER_OVERALL_ACRT = {res.overall_acrt!r}") print(f"ESTIMATOR_TIER_OVERALL_ACRT_SE = {res.overall_acrt_se!r}") diff --git a/tests/_capture_v4_aggregate_oracles.py b/tests/_capture_v4_aggregate_oracles.py index 4307732d4..48630a68b 100644 --- a/tests/_capture_v4_aggregate_oracles.py +++ b/tests/_capture_v4_aggregate_oracles.py @@ -109,10 +109,10 @@ def _dict_surface(surface: Optional[Dict[Any, Dict[str, Any]]]) -> Optional[Dict def _overall(r: Any) -> Dict[str, Optional[float]]: ci = getattr(r, "overall_conf_int", None) or (np.nan, np.nan) return { - "att": _f(r.overall_att), - "se": _f(r.overall_se), + "att": _f(r.att), + "se": _f(r.se), "t_stat": _f(getattr(r, "overall_t_stat", np.nan)), - "p_value": _f(r.overall_p_value), + "p_value": _f(r.p_value), "ci_lo": _f(ci[0]), "ci_hi": _f(ci[1]), } diff --git a/tests/test_aggregate_contract.py b/tests/test_aggregate_contract.py index 2cb42973c..fc3e47673 100644 --- a/tests/test_aggregate_contract.py +++ b/tests/test_aggregate_contract.py @@ -240,11 +240,11 @@ def _assert_group_matches_oracle(agg, orow, *, rtol, atol, p_atol=None, name="") def _assert_overall_matches_oracle(res, o, *, rtol, atol, p_atol=None, name=""): """The stored overall quintet against an oracle ``overall`` record.""" p_atol = atol if p_atol is None else p_atol - _close(res.overall_att, o["att"], rtol=rtol, atol=atol, what=f"{name}.overall_att") - _close(res.overall_se, o["se"], rtol=rtol, atol=atol, what=f"{name}.overall_se") - _close(res.overall_t_stat, o["t_stat"], rtol=_t_rtol(rtol), atol=atol, what=f"{name}.overall_t") - _close(res.overall_p_value, o["p_value"], rtol=rtol, atol=p_atol, what=f"{name}.overall_p") - ci = res.overall_conf_int + _close(res.att, o["att"], rtol=rtol, atol=atol, what=f"{name}.att") + _close(res.se, o["se"], rtol=rtol, atol=atol, what=f"{name}.se") + _close(res.t_stat, o["t_stat"], rtol=_t_rtol(rtol), atol=atol, what=f"{name}.overall_t") + _close(res.p_value, o["p_value"], rtol=rtol, atol=p_atol, what=f"{name}.overall_p") + ci = res.conf_int ci_rtol = max(rtol, CI_RTOL) _close(ci[0], o["ci_lo"], rtol=ci_rtol, atol=atol, what=f"{name}.overall_ci_lo") _close(ci[1], o["ci_hi"], rtol=ci_rtol, atol=atol, what=f"{name}.overall_ci_hi") @@ -254,12 +254,12 @@ def _assert_simple_relays_stored_overall(res): """(ii) aggregate('simple') is a view of the stored overall quintet.""" s = res.aggregate("simple") for got, want in ( - (s.att[0], res.overall_att), - (s.se[0], res.overall_se), - (s.t_stat[0], res.overall_t_stat), - (s.p_value[0], res.overall_p_value), - (s.conf_int_lower[0], res.overall_conf_int[0]), - (s.conf_int_upper[0], res.overall_conf_int[1]), + (s.att[0], res.att), + (s.se[0], res.se), + (s.t_stat[0], res.t_stat), + (s.p_value[0], res.p_value), + (s.conf_int_lower[0], res.conf_int[0]), + (s.conf_int_upper[0], res.conf_int[1]), ): assert (float(got) == float(want)) or (np.isnan(got) and np.isnan(want)) return s @@ -640,13 +640,13 @@ def _assert_bootstrap_simple_relay(res, n_expected=None): percentile se/p/CI beside the finite ``safe_inference`` t - and only the df COLUMN is NaN (no df governs percentile inference).""" agg = res.aggregate("simple") - assert float(agg.att[0]) == res.overall_att - assert float(agg.se[0]) == res.overall_se - assert float(agg.t_stat[0]) == res.overall_t_stat + assert float(agg.att[0]) == res.att + assert float(agg.se[0]) == res.se + assert float(agg.t_stat[0]) == res.t_stat assert np.isfinite(agg.t_stat[0]) - assert float(agg.p_value[0]) == res.overall_p_value - assert float(agg.conf_int_lower[0]) == res.overall_conf_int[0] - assert float(agg.conf_int_upper[0]) == res.overall_conf_int[1] + assert float(agg.p_value[0]) == res.p_value + assert float(agg.conf_int_lower[0]) == res.conf_int[0] + assert float(agg.conf_int_upper[0]) == res.conf_int[1] assert np.isnan(agg.df[0]) if n_expected is not None: assert float(agg.n[0]) == float(n_expected) @@ -946,12 +946,12 @@ def test_no_post_treatment_cells_group_returns_zero_rows(self): ) data = pd.DataFrame(rows) plain = _boot_fit(data, n_bootstrap=25, seed=1) - assert np.isnan(plain.overall_att) + assert np.isnan(plain.att) with warnings.catch_warnings(): warnings.simplefilter("ignore") agg = plain.aggregate("group") assert len(agg.to_dataframe()) == 0 - assert np.isnan(plain.overall_att) # unchanged by the replay + assert np.isnan(plain.att) # unchanged by the replay assert plain.group_effects is None # nothing is stored at fit any more @@ -1229,7 +1229,7 @@ def test_seventh_positional_binds_to_survey_design(self): kw = CallawaySantAnna().fit(d, survey_design=_survey_design(), **FIT_KW) assert res.survey_metadata is not None assert res.survey_metadata.df_survey == kw.survey_metadata.df_survey - assert res.overall_att == kw.overall_att + assert res.att == kw.att assert isinstance(_survey_design(), SurveyDesign) @@ -1811,12 +1811,12 @@ def test_simple_view_bit_exact_phase1(self, dcdh_fitted): assert agg.level == "simple" assert list(agg.label) == ["overall"] assert list(agg.target) == ["DID_M"] - assert float(agg.att[0]) == dcdh_fitted.overall_att - assert float(agg.se[0]) == dcdh_fitted.overall_se - assert float(agg.t_stat[0]) == dcdh_fitted.overall_t_stat - assert float(agg.p_value[0]) == dcdh_fitted.overall_p_value - assert float(agg.conf_int_lower[0]) == dcdh_fitted.overall_conf_int[0] - assert float(agg.conf_int_upper[0]) == dcdh_fitted.overall_conf_int[1] + assert float(agg.att[0]) == dcdh_fitted.att + assert float(agg.se[0]) == dcdh_fitted.se + assert float(agg.t_stat[0]) == dcdh_fitted.t_stat + assert float(agg.p_value[0]) == dcdh_fitted.p_value + assert float(agg.conf_int_lower[0]) == dcdh_fitted.conf_int[0] + assert float(agg.conf_int_upper[0]) == dcdh_fitted.conf_int[1] assert float(agg.n[0]) == float(dcdh_fitted.n_switcher_cells) assert agg.n_kind == "switcher_cells" # Non-survey analytical inference is z-based: no df. @@ -1837,7 +1837,7 @@ def test_simple_view_lmax2_delta(self, dcdh_panel): # The delta averages horizon-specific N_l: no truthful scalar count. assert np.isnan(agg.n[0]) assert agg.n_kind is None - assert float(agg.att[0]) == res.overall_att + assert float(agg.att[0]) == res.att def test_simple_view_trends_linear_all_nan_relay(self, dcdh_panel): # trends_linear + L_max>=2 suppresses the delta by design: every @@ -1857,8 +1857,8 @@ def test_simple_view_bootstrap_percentile_relay(self, dcdh_panel): # stored percentile-bootstrap inference; df is NaN (no df used). res = _fit_dcdh(dcdh_panel, est_kw=dict(n_bootstrap=49, seed=3)) agg = res.aggregate("simple") - assert float(agg.att[0]) == res.overall_att - assert float(agg.se[0]) == res.overall_se + assert float(agg.att[0]) == res.att + assert float(agg.se[0]) == res.se assert np.isnan(agg.df[0]) def test_event_study_container_threads_survey_df(self): @@ -1900,7 +1900,7 @@ def test_simple_view_lmax2_survey_bootstrap_finite_df(self): sd = SurveyDesign(weights="survey_weights", strata="strata", psu="psu") res = _fit_dcdh(df, L_max=2, survey_design=sd, est_kw=dict(n_bootstrap=49, seed=3)) assert res.event_study_df is None # cleared under bootstrap - assert np.isfinite(res.overall_p_value) # delta stayed analytical + assert np.isfinite(res.p_value) # delta stayed analytical agg = res.aggregate("simple") assert res.survey_metadata is not None assert float(agg.df[0]) == float(res.survey_metadata.df_survey) @@ -2047,8 +2047,8 @@ def test_signature_has_no_aggregate_and_sixth_positional_is_population( keyword = StackedDiD(kappa_pre=2, kappa_post=2, weighting="population").fit( panel, population="pop", **STACKED_KW ) - assert positional.overall_att == keyword.overall_att - assert positional.overall_se == keyword.overall_se + assert positional.att == keyword.att + assert positional.se == keyword.se # The surface is always computed - with or without a population column. assert positional.event_study_effects is not None assert stacked_fitted.event_study_effects is not None @@ -2094,12 +2094,12 @@ def test_simple_bit_exact_relay_hc1(self, stacked_fitted): # target is "att": overall_att is the equally-weighted post-period # average, NOT the per-event trimmed aggregate ATT (M-024 Note). assert list(agg.target) == ["att"] - assert float(agg.att[0]) == stacked_fitted.overall_att - assert float(agg.se[0]) == stacked_fitted.overall_se - assert float(agg.t_stat[0]) == stacked_fitted.overall_t_stat - assert float(agg.p_value[0]) == stacked_fitted.overall_p_value + assert float(agg.att[0]) == stacked_fitted.att + assert float(agg.se[0]) == stacked_fitted.se + assert float(agg.t_stat[0]) == stacked_fitted.t_stat + assert float(agg.p_value[0]) == stacked_fitted.p_value assert (float(agg.conf_int_lower[0]), float(agg.conf_int_upper[0])) == tuple( - stacked_fitted.overall_conf_int + stacked_fitted.conf_int ) # Treated-unit count: control units OVERLAP treated across # sub-experiments, so no disjoint total exists (M-024 Note). @@ -2181,7 +2181,7 @@ def test_simple_relay_survey_tsl_df(self, stacked_panel): survey_design=SurveyDesign(weights="w", strata="strata", psu="psu"), ) agg = res.aggregate("simple") - assert float(agg.att[0]) == res.overall_att + assert float(agg.att[0]) == res.att # The stored overall inference used the survey df; the relay # carries exactly that provenance. assert float(agg.df[0]) == float(res.inference_df) @@ -2251,15 +2251,13 @@ def spy(X, cluster_ids, bread_matrix, contrasts, weights=None): )[0] ) np.testing.assert_allclose(res.inference_df, overall_dof_m1, rtol=1e-14) - t, p, ci = safe_inference( - res.overall_att, res.overall_se, alpha=res.alpha, df=overall_dof_m1 - ) + t, p, ci = safe_inference(res.att, res.se, alpha=res.alpha, df=overall_dof_m1) np.testing.assert_allclose( [t, ci[0], ci[1]], [ - res.overall_t_stat, - res.overall_conf_int[0], - res.overall_conf_int[1], + res.t_stat, + res.conf_int[0], + res.conf_int[1], ], rtol=1e-14, ) @@ -2268,7 +2266,7 @@ def spy(X, cluster_ids, bread_matrix, contrasts, weights=None): # x86 OpenBLAS), and the deep tail amplifies that relative noise by # ~t^2 (d ln p ~ -t dt; t ~ 29 here => ~1e3x), so p carries ~1e-13 # relative noise while t/CI stay at 1e-14. - np.testing.assert_allclose(p, res.overall_p_value, rtol=1e-12) + np.testing.assert_allclose(p, res.p_value, rtol=1e-12) def test_legacy_pickle_absent_surface_hint(self, stacked_fitted): import dataclasses @@ -2423,7 +2421,7 @@ def test_signature_and_seventh_positional_is_survey_design(self): positional = EfficientDiD().fit(d, "outcome", "unit", "period", "first_treat", None, sd) keyword = EfficientDiD().fit(d, survey_design=sd, **EFFICIENT_KW) assert positional.survey_metadata is not None - assert positional.overall_se == keyword.overall_se + assert positional.se == keyword.se class TestEfficientAggregate: @@ -2988,8 +2986,8 @@ def test_relays_unchanged_and_order_independent(self, efficient_panel): s_before = res.aggregate("simple") res.aggregate("event_study") s_after = res.aggregate("simple") - assert float(s_before.att[0]) == float(s_after.att[0]) == res.overall_att - assert float(s_before.se[0]) == float(s_after.se[0]) == res.overall_se + assert float(s_before.att[0]) == float(s_after.att[0]) == res.att + assert float(s_before.se[0]) == float(s_after.se[0]) == res.se def test_legacy_kit_without_spec_fails_closed(self, efficient_panel): res = _efficient_boot_fit(efficient_panel) @@ -3559,7 +3557,7 @@ def test_signature_and_seventh_positional_is_survey_design(self): ) keyword = ImputationDiD().fit(d, survey_design=sd, **IMPUTATION_KW) assert positional.survey_metadata is not None - assert positional.overall_se == keyword.overall_se + assert positional.se == keyword.se class TestImputationAggregate: @@ -3675,7 +3673,7 @@ def test_replicate_arms_level_matched(self, degenerate): name=f"imp/rep/{tag}", ) sm = plain.aggregate("simple") - np.testing.assert_allclose(sm.se[0], plain.overall_se, rtol=0, atol=0, equal_nan=True) + np.testing.assert_allclose(sm.se[0], plain.se, rtol=0, atol=0, equal_nan=True) # (iv) the degenerate="undefined" arm's df=0 sentinel NaNs t/p beside # finite effects - a valid NaN-consistent state. _assert_analytic_inference_contract(plain.aggregate("event_study"), alpha=plain.alpha) @@ -3706,7 +3704,7 @@ def test_replicate_overall_row_migration_delta(self): sd = _imputation_survey_design(rep_cols) plain = _fit_imputation(d, survey_design=sd) orow = _im_oracle("replicate_cohort_zero_group") - assert plain.overall_se != oracle_value(orow["overall"]["se"]) + assert plain.se != oracle_value(orow["overall"]["se"]) _assert_group_matches_oracle( plain.aggregate("group"), orow, **ANALYTIC_TOL, name="imp/delta" ) @@ -3718,7 +3716,7 @@ def test_replicate_overall_row_migration_delta(self): dh, rep_h = _imputation_survey_panel(replicate=True) healthy = _fit_imputation(dh, survey_design=_imputation_survey_design(rep_h)) np.testing.assert_allclose( - healthy.overall_se, + healthy.se, oracle_value(_im_oracle("replicate_none_group")["overall"]["se"]), rtol=1e-12, ) @@ -3910,7 +3908,7 @@ def boom(self, *a, **k): assert res.bootstrap_results is None assert res.event_study_effects is None and res.group_effects is None # storage rule analytic = _fit_imputation(imputation_panel) - assert res.overall_se == analytic.overall_se # analytical overall + assert res.se == analytic.se # analytical overall # ...and aggregate() RECOMPUTES analytically (no raise). _frame_equal( res.aggregate("event_study").to_dataframe(), @@ -3947,7 +3945,7 @@ def test_failsoft_b3_n_valid_zero_bootstrapped_fit_recomputes(self): with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") res = self._boot_est().fit(pd.DataFrame(rows), **IMPUTATION_KW) - assert np.isnan(res.overall_att) + assert np.isnan(res.att) assert res.bootstrap_results is None assert res.event_study_effects is None and res.group_effects is None assert not any("could not be materialized" in str(w.message) for w in caught) @@ -3970,7 +3968,7 @@ def test_failsoft_c_real_trigger_lead_collinear_covariate(self, imputation_panel ) assert res.bootstrap_results is not None assert res.event_study_effects is None and res.group_effects is None - assert np.isfinite(res.overall_att) and np.isfinite(res.overall_se) + assert np.isfinite(res.att) and np.isfinite(res.se) with pytest.raises(ValueError, match="could not be materialized"): res.aggregate("event_study") @@ -4205,7 +4203,7 @@ def test_signature_and_seventh_positional_is_survey_design(self): positional = TwoStageDiD().fit(d, "outcome", "unit", "period", "first_treat", None, sd) keyword = TwoStageDiD().fit(d, survey_design=sd, **TWOSTAGE_KW) assert positional.survey_metadata is not None - assert positional.overall_se == keyword.overall_se + assert positional.se == keyword.se class TestTwoStageAggregate: @@ -4442,7 +4440,7 @@ def test_replicate_overall_row_migration_delta(self): sd = _twostage_survey_design(rep_cols) plain = _fit_twostage(d, survey_design=sd) orow = _ts_oracle("replicate_cohort_zero_group") - assert plain.overall_se != oracle_value(orow["overall"]["se"]) + assert plain.se != oracle_value(orow["overall"]["se"]) _assert_group_matches_oracle( plain.aggregate("group"), orow, **ANALYTIC_TOL, name="ts/delta" ) @@ -4450,7 +4448,7 @@ def test_replicate_overall_row_migration_delta(self): healthy = _fit_twostage(dh, survey_design=_twostage_survey_design(rep_h)) # Same BLAS-shape caveat as the Imputation twin above. np.testing.assert_allclose( - healthy.overall_se, + healthy.se, oracle_value(_ts_oracle("replicate_none_group")["overall"]["se"]), rtol=1e-12, ) @@ -4657,7 +4655,7 @@ def boom(self, *a, **k): assert res.bootstrap_results is None assert res.event_study_effects is None and res.group_effects is None analytic = _fit_twostage(twostage_panel) - assert res.overall_se == analytic.overall_se + assert res.se == analytic.se _frame_equal( res.aggregate("event_study").to_dataframe(), analytic.aggregate("event_study").to_dataframe(), @@ -4679,7 +4677,7 @@ def test_failsoft_b2_none_return_on_both_attempts_falls_back_analytical( assert res.bootstrap_results is None assert res.event_study_effects is None and res.group_effects is None analytic = _fit_twostage(twostage_panel) - assert res.overall_se == analytic.overall_se + assert res.se == analytic.se _frame_equal( res.aggregate("event_study").to_dataframe(), analytic.aggregate("event_study").to_dataframe(), @@ -4899,18 +4897,18 @@ def _co_boot_oracle(arm): def _assert_cont_overall_matches_oracle(res, orow, *, p_atol=_CO_P_ATOL): """ContinuousDiD overall ATT quintet + the ACRT scalars the capture kept.""" o = orow["overall"] - _close(res.overall_att, o["att"], **BOOT_TOL, what="cont.overall_att") - _close(res.overall_att_se, o["se"], **BOOT_TOL, what="cont.overall_att_se") + _close(res.att, o["att"], **BOOT_TOL, what="cont.att") + _close(res.se, o["se"], **BOOT_TOL, what="cont.se") _close( - res.overall_att_t_stat, + res.t_stat, o["t_stat"], rtol=_t_rtol(BOOT_TOL["rtol"]), atol=BOOT_TOL["atol"], what="cont.overall_att_t", ) - _close(res.overall_att_p_value, o["p_value"], rtol=1e-13, atol=p_atol, what="cont.overall_p") - _close(res.overall_att_conf_int[0], o["ci_lo"], **BOOT_TOL, what="cont.ci_lo") - _close(res.overall_att_conf_int[1], o["ci_hi"], **BOOT_TOL, what="cont.ci_hi") + _close(res.p_value, o["p_value"], rtol=1e-13, atol=p_atol, what="cont.overall_p") + _close(res.conf_int[0], o["ci_lo"], **BOOT_TOL, what="cont.ci_lo") + _close(res.conf_int[1], o["ci_hi"], **BOOT_TOL, what="cont.ci_hi") _close(res.overall_acrt, orow["overall_acrt"], **BOOT_TOL, what="cont.acrt") _close(res.overall_acrt_se, orow["overall_acrt_se"], **BOOT_TOL, what="cont.acrt_se") @@ -4950,7 +4948,7 @@ def test_signature_and_seventh_positional_is_survey_design(self): ) keyword = ContinuousDiD().fit(d, survey_design=sd, **CONT_KW) assert positional.survey_metadata is not None - assert positional.overall_att_se == keyword.overall_att_se + assert positional.se == keyword.se class TestContinuousAggregate: @@ -5147,13 +5145,13 @@ def test_simple_view_bit_exact(self, cont_fitted): s = cont_fitted.aggregate("simple") assert list(s.label) == ["overall", "overall"] assert list(s.target) == ["att", "acrt"] - assert s.att[0] == cont_fitted.overall_att + assert s.att[0] == cont_fitted.att assert s.att[1] == cont_fitted.overall_acrt - assert s.se[0] == cont_fitted.overall_att_se + assert s.se[0] == cont_fitted.se assert s.se[1] == cont_fitted.overall_acrt_se - assert s.t_stat[0] == cont_fitted.overall_att_t_stat + assert s.t_stat[0] == cont_fitted.t_stat assert s.p_value[1] == cont_fitted.overall_acrt_p_value - assert s.conf_int_lower[0] == cont_fitted.overall_att_conf_int[0] + assert s.conf_int_lower[0] == cont_fitted.conf_int[0] assert s.conf_int_upper[1] == cont_fitted.overall_acrt_conf_int[1] # Disjoint treated/control unit sets -> the CS total convention. assert s.n_kind == "units" @@ -5166,9 +5164,9 @@ def test_simple_view_bootstrap_finite_t_relays(self, cont_bootstrap): p/CI on bootstrap fits - the relay carries it through verbatim (bit-exact relay, NOT NaN); only the df column is NaN.""" s = cont_bootstrap.aggregate("simple") - assert s.t_stat[0] == cont_bootstrap.overall_att_t_stat + assert s.t_stat[0] == cont_bootstrap.t_stat assert np.isfinite(s.t_stat[0]) - assert s.p_value[0] == cont_bootstrap.overall_att_p_value + assert s.p_value[0] == cont_bootstrap.p_value assert np.isnan(s.df).all() def test_simple_view_survey_df(self): @@ -5295,7 +5293,7 @@ def test_bootstrap_without_post_cells_stores_no_surface(self): res = ContinuousDiD(**_CO_BOOT_KW).fit(_cont_empty_post_panel(), **CONT_KW) assert any("No post-treatment" in str(w.message) for w in caught) assert not any("could not be materialized" in str(w.message) for w in caught) - assert np.isnan(res.overall_att) + assert np.isnan(res.att) assert res.event_study_effects is None with pytest.raises(ValueError, match="carries no stored event_study surface"): res.aggregate("event_study") diff --git a/tests/test_bootstrap_chunking.py b/tests/test_bootstrap_chunking.py index f562097b2..a4ba2ecb1 100644 --- a/tests/test_bootstrap_chunking.py +++ b/tests/test_bootstrap_chunking.py @@ -355,13 +355,13 @@ def test_tiny_chunks_match_single_chunk(self, monkeypatch): tiny, tiny_es, tiny_grp = self._fit() # Continuous bootstrap statistics match to within BLAS reassociation. - assert tiny.overall_se == pytest.approx(base.overall_se, rel=1e-10, abs=1e-12) + assert tiny.se == pytest.approx(base.se, rel=1e-10, abs=1e-12) assert tiny_es.cband_crit_value == pytest.approx( base_es.cband_crit_value, rel=1e-10, abs=1e-12 ) # p-values are discrete proportions over draws; a borderline draw can # flip under reassociation, shifting a p-value by O(1/n_bootstrap). - assert tiny.overall_p_value == pytest.approx(base.overall_p_value, abs=0.02) + assert tiny.p_value == pytest.approx(base.p_value, abs=0.02) b = base.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) t = tiny.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) @@ -402,7 +402,7 @@ def fit(): monkeypatch.setattr("diff_diff.bootstrap_chunking.compute_block_size", lambda *a, **k: 7) monkeypatch.setattr("diff_diff.staggered_bootstrap.compute_block_size", lambda *a, **k: 7) tiny = fit() - assert tiny.overall_se == pytest.approx(base.overall_se, rel=1e-10, abs=1e-12) + assert tiny.se == pytest.approx(base.se, rel=1e-10, abs=1e-12) @staticmethod def _clustered_panel(): @@ -448,7 +448,7 @@ def test_nonidentity_cluster_chunks_match_single(self, monkeypatch): monkeypatch.setattr("diff_diff.staggered_bootstrap.compute_block_size", lambda *a, **k: 7) tiny, _, _ = self._fit_clustered() - assert tiny.overall_se == pytest.approx(base.overall_se, rel=1e-10, abs=1e-12) + assert tiny.se == pytest.approx(base.se, rel=1e-10, abs=1e-12) b = base.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) t = tiny.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) for col in ["se", "conf_int_lower", "conf_int_upper"]: @@ -479,11 +479,11 @@ def test_unit_path_tiles_match_single_tile(self, monkeypatch): self._force_one_column_tiles(monkeypatch) tiled, tiled_es, tiled_grp = fit() - assert tiled.overall_se == pytest.approx(base.overall_se, rel=1e-10, abs=1e-12) + assert tiled.se == pytest.approx(base.se, rel=1e-10, abs=1e-12) assert tiled_es.cband_crit_value == pytest.approx( base_es.cband_crit_value, rel=1e-10, abs=1e-12 ) - assert tiled.overall_p_value == pytest.approx(base.overall_p_value, abs=0.02) + assert tiled.p_value == pytest.approx(base.p_value, abs=0.02) b = base.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) t = tiled.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) for col in ["se", "conf_int_lower", "conf_int_upper"]: @@ -504,7 +504,7 @@ def test_cluster_path_tiles_match_single_tile(self, monkeypatch): self._force_one_column_tiles(monkeypatch) tiled, _, _ = fit() - assert tiled.overall_se == pytest.approx(base.overall_se, rel=1e-10, abs=1e-12) + assert tiled.se == pytest.approx(base.se, rel=1e-10, abs=1e-12) b = base.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) t = tiled.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) np.testing.assert_allclose(t["se"].to_numpy(), b["se"].to_numpy(), rtol=1e-10, atol=1e-12) @@ -568,7 +568,7 @@ def test_survey_path_tiles_match_single_tile(self, monkeypatch, stratified): self._force_one_column_tiles(monkeypatch) tiled = self._fit_survey(stratified) - assert tiled.overall_se == pytest.approx(base.overall_se, rel=1e-10, abs=1e-12) + assert tiled.se == pytest.approx(base.se, rel=1e-10, abs=1e-12) b = base.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) t = tiled.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) for col in ["se", "conf_int_lower", "conf_int_upper"]: @@ -611,7 +611,7 @@ def fit(): self._force_one_column_tiles(monkeypatch) tiled = fit() - assert tiled.overall_se == pytest.approx(base.overall_se, rel=1e-10, abs=1e-12) + assert tiled.se == pytest.approx(base.se, rel=1e-10, abs=1e-12) b = base.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) t = tiled.to_dataframe().sort_values(["group", "time"]).reset_index(drop=True) for col in ["se", "conf_int_lower", "conf_int_upper"]: @@ -738,7 +738,7 @@ def _ses(fitted): gt = [r.group_time_effects[k]["se"] for k in sorted(r.group_time_effects)] es = [float(x) for x in es_c.se] gp = [float(x) for x in grp_c.se] - return np.array([r.overall_se, *gt, *es, *gp], dtype=float) + return np.array([r.se, *gt, *es, *gp], dtype=float) def _run(self, monkeypatch, **fit_kwargs): base = self._fit(**fit_kwargs) diff --git a/tests/test_business_report.py b/tests/test_business_report.py index 25bc12003..947dc520b 100644 --- a/tests/test_business_report.py +++ b/tests/test_business_report.py @@ -872,9 +872,13 @@ class StaggeredTripleDiffResults: obj = StaggeredTripleDiffResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.1 + obj.se = 0.1 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.8, 1.2) + obj.conf_int = (0.8, 1.2) obj.alpha = 0.05 obj.n_obs = 100 obj.n_treated = 40 @@ -907,9 +911,13 @@ class ImputationDiDResults: obj = ImputationDiDResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.1 + obj.se = 0.1 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.8, 1.2) + obj.conf_int = (0.8, 1.2) obj.alpha = 0.05 obj.n_obs = 100 obj.n_treated = 40 @@ -953,9 +961,13 @@ class TwoStageDiDResults: obj = TwoStageDiDResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.1 + obj.se = 0.1 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.8, 1.2) + obj.conf_int = (0.8, 1.2) obj.alpha = 0.05 obj.n_obs = 100 obj.n_treated = 40 @@ -1007,9 +1019,13 @@ class EfficientDiDResults: stub = EfficientDiDResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -1537,9 +1553,13 @@ class StackedDiDResults: obj = StackedDiDResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.2 + obj.se = 0.2 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.6, 1.4) + obj.conf_int = (0.6, 1.4) obj.alpha = 0.05 obj.n_obs = 400 obj.n_treated_units = 100 @@ -1585,9 +1605,13 @@ class StaggeredTripleDiffResults: stub = StaggeredTripleDiffResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 800 stub.n_treated = 100 @@ -1817,9 +1841,13 @@ class StackedDiDResults: stub = StackedDiDResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 400 stub.n_treated = 50 @@ -1995,9 +2023,13 @@ def _stub(class_name: str, **extras): stub_cls = type(class_name, (), {}) stub = stub_cls() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 400 stub.n_treated = 100 @@ -2081,9 +2113,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -2107,9 +2143,13 @@ class EfficientDiDResults: stub = EfficientDiDResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -2134,9 +2174,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -2167,9 +2211,13 @@ class ContinuousDiDResults: stub = ContinuousDiDResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_att_se = 0.2 + stub.se = 0.2 stub.overall_att_p_value = 0.001 + stub.p_value = 0.001 stub.overall_att_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 120 stub.n_treated = 50 @@ -2206,9 +2254,13 @@ class StaggeredTripleDiffResults: stub = StaggeredTripleDiffResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 200 stub.n_treated = 80 @@ -2254,9 +2306,13 @@ class WooldridgeDiDResults: stub = WooldridgeDiDResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -2305,9 +2361,13 @@ class WooldridgeDiDResults: stub = WooldridgeDiDResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -2388,9 +2448,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -2498,9 +2562,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -2877,9 +2945,13 @@ class EfficientDiDResults: stub = EfficientDiDResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.3 + stub.se = 0.3 stub.overall_p_value = 0.01 + stub.p_value = 0.01 stub.overall_conf_int = (0.4, 1.6) + stub.conf_int = (0.4, 1.6) stub.alpha = 0.05 stub.n_obs = 500 stub.n_treated = 200 @@ -2927,9 +2999,13 @@ def _build_stub(class_name: str, **extras): stub_cls = type(class_name, (), {}) stub = stub_cls() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 400 stub.n_treated = 100 @@ -3006,9 +3082,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 1000 stub.n_treated_units = 200 @@ -3060,7 +3140,9 @@ class TROPResults: stub = TROPResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.alpha = 0.05 stub.n_obs = 100 @@ -3235,9 +3317,13 @@ class TROPResults: stub = TROPResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 400 stub.n_treated = 40 @@ -3473,7 +3559,9 @@ class TROPResults: stub = TROPResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.alpha = 0.05 stub.n_obs = 100 with pytest.raises(ValueError, match="estimator_native_diagnostics"): @@ -3539,9 +3627,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 500 stub.n_treated = 100 @@ -3602,9 +3694,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 500 stub.n_treated = 100 @@ -3697,7 +3793,9 @@ class TROPResults: stub = TROPResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.alpha = 0.05 stub.n_obs = 100 with pytest.raises(ValueError, match="estimator_native_diagnostics"): @@ -3711,7 +3809,9 @@ class TROPResults: stub = TROPResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.alpha = 0.05 stub.n_obs = 100 with pytest.raises(ValueError, match="estimator_native_diagnostics"): @@ -4075,9 +4175,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.3 + stub.se = 0.3 stub.overall_p_value = 0.01 + stub.p_value = 0.01 stub.overall_conf_int = (0.4, 1.6) + stub.conf_int = (0.4, 1.6) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -4124,9 +4228,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.3 + stub.se = 0.3 stub.overall_p_value = 0.01 + stub.p_value = 0.01 stub.overall_conf_int = (0.4, 1.6) + stub.conf_int = (0.4, 1.6) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -4222,9 +4330,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 0.5608 + stub.att = 0.5608 stub.overall_se = 0.1216 + stub.se = 0.1216 stub.overall_p_value = 0.0 + stub.p_value = 0.0 stub.overall_conf_int = (0.323, 0.799) + stub.conf_int = (0.323, 0.799) stub.alpha = 0.05 stub.n_obs = 539 stub.n_treated = 22 @@ -4632,9 +4744,13 @@ class CallawaySantAnnaResults: obj = CallawaySantAnnaResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.2 + obj.se = 0.2 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.6, 1.4) + obj.conf_int = (0.6, 1.4) obj.alpha = 0.05 obj.n_obs = 600 obj.n_treated = 200 diff --git a/tests/test_chaisemartin_dhaultfoeuille.py b/tests/test_chaisemartin_dhaultfoeuille.py index ddc05147f..62d782094 100644 --- a/tests/test_chaisemartin_dhaultfoeuille.py +++ b/tests/test_chaisemartin_dhaultfoeuille.py @@ -64,7 +64,7 @@ def test_fit_recovers_homogeneous_effect_single_switch(self): treatment="treatment", ) # CI should bracket the true effect of 2.0 - lo, hi = results.overall_conf_int + lo, hi = results.conf_int assert lo <= 2.0 <= hi, f"95% CI [{lo:.3f}, {hi:.3f}] does not bracket true effect 2.0" def test_fit_with_joiners_only_pattern(self): @@ -92,10 +92,10 @@ def test_fit_with_joiners_only_pattern(self): assert results.leavers_available is False assert np.isnan(results.leavers_att) # CI brackets the truth (modulo conservative-CI noise) - lo, hi = results.overall_conf_int + lo, hi = results.conf_int assert lo <= 1.5 <= hi, ( f"95% CI [{lo:.3f}, {hi:.3f}] does not bracket true effect 1.5; " - f"DID_M = {results.overall_att:.3f}" + f"DID_M = {results.att:.3f}" ) def test_fit_with_leavers_only_pattern(self): @@ -176,7 +176,7 @@ def test_non_binary_treatment_with_lmax(self): treatment="treatment", L_max=1, ) - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) def test_alias_DCDH_identity(self): assert DCDH is ChaisemartinDHaultfoeuille @@ -224,7 +224,7 @@ def test_minimal_computation_path(self): # Bootstrap should be None assert results.bootstrap_results is None # Main estimate should still be finite - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) # ============================================================================= @@ -680,7 +680,7 @@ def test_terminal_missingness_retained(self): # All 10 groups remain assert len(results.units) == 10 # The point estimate is well-defined (not NaN) - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) # Per-period DIDs were computed (the structure of per_period_effects # depends on the panel's switch pattern; assert at least one entry # was populated rather than asserting specific counts) @@ -735,7 +735,7 @@ def test_global_period_gap_treated_as_adjacent(self): # All 4 groups present assert len(results.units) == 4 # Point estimate is finite - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) # Per-period effects include the transition at t=3 (treated as # the successor of t=1) assert len(results.per_period_effects) > 0 @@ -815,9 +815,9 @@ def test_cell_count_weighting_unbalanced_input(self): # Expected under SAMPLE-SIZE weighting (the bug): # DID_+,2 = (100*5.5 + 1*5.0) / 101 - 0 = 5.495 # The two differ by ~0.25, so we can detect the bug at 0.05 tolerance. - assert abs(results.overall_att - 5.25) < 0.05, ( + assert abs(results.att - 5.25) < 0.05, ( f"Expected DID_M ≈ 5.25 under cell weighting, got " - f"{results.overall_att:.4f}. If you see ~5.495 the estimator " + f"{results.att:.4f}. If you see ~5.495 the estimator " f"is using sample-size weighting (the bug)." ) # n_switcher_cells should be 2 (one cell per joiner group at t=2), @@ -1012,7 +1012,7 @@ def test_bootstrap_zero_uses_analytical(self, data): treatment="treatment", ) assert results.bootstrap_results is None - assert np.isfinite(results.overall_se) + assert np.isfinite(results.se) def test_bootstrap_rademacher(self, data, ci_params): n_boot = ci_params.bootstrap(199) @@ -1153,13 +1153,13 @@ def test_bootstrap_p_value_and_ci_propagated_to_top_level(self, data, ci_params) assert br is not None # Overall DID_M: top-level p-value and CI come from bootstrap - assert results.overall_p_value == pytest.approx(br.overall_p_value) - assert results.overall_conf_int == pytest.approx(br.overall_ci) + assert results.p_value == pytest.approx(br.overall_p_value) + assert results.conf_int == pytest.approx(br.overall_ci) # The t-stat is computed from the SE (effect / se), not from # a percentile distribution - assert np.isfinite(results.overall_t_stat) - expected_t = results.overall_att / results.overall_se - assert results.overall_t_stat == pytest.approx(expected_t) + assert np.isfinite(results.t_stat) + expected_t = results.att / results.se + assert results.t_stat == pytest.approx(expected_t) # Joiners if results.joiners_available and br.joiners_p_value is not None: @@ -1211,7 +1211,7 @@ def test_bootstrap_seed_reproducibility(self, data, ci_params): time="period", treatment="treatment", ) - assert r1.overall_se == r2.overall_se + assert r1.se == r2.se # ============================================================================= @@ -1306,12 +1306,12 @@ def test_event_study_effects_populated_at_l1(self, results): assert results.event_study_effects is not None assert 1 in results.event_study_effects es1 = results.event_study_effects[1] - assert es1["effect"] == pytest.approx(results.overall_att) - assert es1["se"] == pytest.approx(results.overall_se) + assert es1["effect"] == pytest.approx(results.att) + assert es1["se"] == pytest.approx(results.se) def test_is_significant_property(self, results): # Boolean reflects whether p-value < alpha - expected = results.overall_p_value < results.alpha + expected = results.p_value < results.alpha assert results.is_significant is expected def test_coef_var_nan_safe_on_non_finite_se(self): @@ -1814,10 +1814,10 @@ def test_L_max_1_bootstrap_overall_matches_es1(self, data, ci_params): L_max=1, ) es1 = r.event_study_effects[1] - assert r.overall_att == es1["effect"] - assert r.overall_se == es1["se"] - assert r.overall_p_value == es1["p_value"] - assert r.overall_conf_int == es1["conf_int"] + assert r.att == es1["effect"] + assert r.se == es1["se"] + assert r.p_value == es1["p_value"] + assert r.conf_int == es1["conf_int"] def test_L_max_1_suppresses_joiner_leaver_decomposition(self): """L_max=1 suppresses joiner/leaver decomposition in summary() @@ -1893,7 +1893,7 @@ def test_L_max_1_uses_per_group_path(self, data): # Per-group path produces finite estimate and SE assert np.isfinite(r_one.event_study_effects[1]["effect"]) assert np.isfinite(r_one.event_study_effects[1]["se"]) - assert np.isfinite(r_one.overall_att) + assert np.isfinite(r_one.att) # L_max=1 should have exactly 1 horizon assert set(r_one.event_study_effects.keys()) == {1} @@ -2000,9 +2000,9 @@ def test_overall_att_is_cost_benefit_delta_when_L_max_gt_1(self, data): L_max=3, ) assert r.cost_benefit_delta is not None - assert r.overall_att == pytest.approx(r.cost_benefit_delta["delta"]) + assert r.att == pytest.approx(r.cost_benefit_delta["delta"]) # DID_1 is still accessible - assert r.event_study_effects[1]["effect"] != r.overall_att or True # may be close + assert r.event_study_effects[1]["effect"] != r.att or True # may be close class TestMultiHorizonPlacebos: @@ -2181,7 +2181,7 @@ def test_delta_is_consistent(self, data): # Weights sum to 1 assert sum(cb["weights"].values()) == pytest.approx(1.0, abs=1e-10) # delta == overall_att when L_max > 1 - assert r.overall_att == pytest.approx(cb["delta"]) + assert r.att == pytest.approx(cb["delta"]) class TestSupTBands: @@ -2334,13 +2334,13 @@ def test_covariate_residualization_basic(self): ) # Results should differ (covariate is confounding) - assert r_x.overall_att != r_plain.overall_att + assert r_x.att != r_plain.att # Covariate diagnostics should be populated assert r_x.covariate_residuals is not None assert len(r_x.covariate_residuals) > 0 assert "theta_hat" in r_x.covariate_residuals.columns # SE should be finite - assert np.isfinite(r_x.overall_se) + assert np.isfinite(r_x.se) def test_multiple_covariates(self): """Multiple covariates are accepted and produce diagnostics.""" @@ -2410,8 +2410,8 @@ def test_controls_with_nonbinary_treatment(self): covariates=["X1"], L_max=1, ) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) + assert np.isfinite(r.att) + assert np.isfinite(r.se) def test_controls_with_multi_horizon(self): """Covariates work with L_max > 1 event study.""" @@ -2462,9 +2462,9 @@ def test_controls_lmax1_estimand_contract(self): ), f"per_period_effects should be unadjusted at period {period_key}" # overall_att should come from event_study_effects[1] (DID^X_1) - assert r_x.overall_att == pytest.approx(r_x.event_study_effects[1]["effect"], abs=1e-10) + assert r_x.att == pytest.approx(r_x.event_study_effects[1]["effect"], abs=1e-10) # and should differ from the raw overall_att (covariate effect) - assert r_x.overall_att != r_raw.overall_att + assert r_x.att != r_raw.att class TestLinearTrends: @@ -2520,7 +2520,7 @@ def test_trends_linear_basic(self): trends_linear=True, ) # Results should differ (group-specific trends confound unadjusted) - assert r_fd.overall_att != r_plain.overall_att + assert r_fd.att != r_plain.att # Event study should have horizons assert r_fd.event_study_effects is not None assert 1 in r_fd.event_study_effects @@ -2587,7 +2587,7 @@ def test_trends_with_covariates(self): trends_linear=True, ) # overall_att is NaN for trends + L_max>=2 (no aggregate) - assert np.isnan(r.overall_att) + assert np.isnan(r.att) assert r.covariate_residuals is not None assert r.linear_trends_effects is not None @@ -2609,8 +2609,8 @@ def test_trends_linear_lmax2_overall_surface(self): trends_linear=True, ) # overall_* should be NaN (not computed in trends mode) - assert np.isnan(r.overall_att) - assert np.isnan(r.overall_se) + assert np.isnan(r.att) + assert np.isnan(r.se) # cost_benefit_delta suppressed assert r.cost_benefit_delta is None # Cumulated effects still available @@ -2713,10 +2713,10 @@ def test_trends_nonparam_basic(self): ) # With set-restricted controls, results may differ # (both should be finite and reasonable) - assert np.isfinite(r_plain.overall_att) - assert np.isfinite(r_plain.overall_se) - assert np.isfinite(r_set.overall_att) - assert np.isfinite(r_set.overall_se) + assert np.isfinite(r_plain.att) + assert np.isfinite(r_plain.se) + assert np.isfinite(r_set.att) + assert np.isfinite(r_set.se) def test_time_varying_set_raises(self): """Set membership that varies over time raises ValueError.""" @@ -2791,7 +2791,7 @@ def test_nonparam_with_covariates(self): L_max=1, trends_nonparam="state", ) - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) assert r.covariate_residuals is not None def test_trends_nonparam_unequal_support(self): @@ -2846,7 +2846,7 @@ def test_trends_nonparam_unequal_support(self): trends_nonparam="state", ) # Should not error; State A groups contribute, State B excluded - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) assert r.event_study_effects is not None @@ -3187,7 +3187,7 @@ def test_ordinal_treatment(self): treatment="treatment", L_max=2, ) - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) def test_within_cell_heterogeneity_rejected_nonbinary(self): """Cells with mixed non-binary values (e.g., 1 and 2) should be rejected.""" @@ -3319,9 +3319,9 @@ def test_mixed_binary_nonbinary_panel_lmax1(self): L_max=1, ) # overall_att should be from per-group path (includes both 0->1 and 0->2) - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) # event_study_effects[1] and overall_att should be the same estimand - assert r.overall_att == r.event_study_effects[1]["effect"] + assert r.att == r.event_study_effects[1]["effect"] def test_constant_nonbinary_treatment_raises(self): """Constant non-binary treatment (no switchers) should raise ValueError.""" @@ -3379,9 +3379,9 @@ def test_nonbinary_bootstrap(self, ci_params): assert np.isfinite(r.bootstrap_results.event_study_ses[1]) # Top-level overall_* must match event_study_effects[1] es1 = r.event_study_effects[1] - assert r.overall_att == es1["effect"] - assert r.overall_se == es1["se"] - assert r.overall_p_value == es1["p_value"] + assert r.att == es1["effect"] + assert r.se == es1["se"] + assert r.p_value == es1["p_value"] def test_nonbinary_lmax1_renderer_contract(self): """Non-binary L_max=1: summary/to_dataframe use DID_1 label and @@ -5155,7 +5155,7 @@ def test_summary_footer_mixed_validity_surfaces_live_targets(self): seed=42, ) # Sanity: healthy fit has finite overall and path SEs. - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) assert res.path_effects is not None any_finite_path = any( np.isfinite(h["se"]) for e in res.path_effects.values() for h in e["horizons"].values() @@ -5229,15 +5229,15 @@ def test_nan_contract_extends_to_overall_and_event_study_horizons(self): L_max=3, ) - assert np.isnan(res.overall_se), ( - f"n_bootstrap=1: overall_se must be NaN (bootstrap " f"contract), got {res.overall_se}" + assert np.isnan(res.se), ( + f"n_bootstrap=1: overall_se must be NaN (bootstrap " f"contract), got {res.se}" ) - assert np.isnan(res.overall_t_stat) - assert np.isnan(res.overall_p_value) - lo, hi = res.overall_conf_int + assert np.isnan(res.t_stat) + assert np.isnan(res.p_value) + lo, hi = res.conf_int assert np.isnan(lo) and np.isnan(hi) # Point estimate stays finite across bootstrap invalidity - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) if res.joiners_se is not None: assert np.isnan(res.joiners_se) @@ -8719,7 +8719,7 @@ def test_continuous_D_without_by_path_unaffected(self): treatment="treatment", L_max=2, ) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) @pytest.mark.slow def test_bootstrap_with_non_binary_finite_se(self): @@ -9786,7 +9786,7 @@ def test_global_survey_plus_n_bootstrap_still_works(self): L_max=3, survey_design=sd, ) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) assert res.path_effects is None # ----- Analytical SE correctness ----- diff --git a/tests/test_chaisemartin_dhaultfoeuille_parity.py b/tests/test_chaisemartin_dhaultfoeuille_parity.py index 0f003b703..828a7ea2d 100644 --- a/tests/test_chaisemartin_dhaultfoeuille_parity.py +++ b/tests/test_chaisemartin_dhaultfoeuille_parity.py @@ -150,9 +150,7 @@ def test_parity_single_switch_mixed(self, golden_values): # omitted because the SE depends on the same cohort-vs-period design # choice and is fundamentally apples-to-oranges between the two # implementations on mixed-direction panels. - assert results.overall_att == pytest.approx( - r_results["overall_att"], rel=self.MIXED_POINT_RTOL - ) + assert results.att == pytest.approx(r_results["overall_att"], rel=self.MIXED_POINT_RTOL) PURE_DIRECTION_SE_RTOL = 0.05 # 5% rtol on pure-direction scenarios after the full IF fix @@ -166,14 +164,12 @@ def test_parity_joiners_only(self, golden_values): # Pure-direction scenario: tight point-estimate parity expected # (cell counts and the full IF formula produce identical answers # to R DIDmultiplegtDYN under pure-direction conditions). - assert results.overall_att == pytest.approx(r_results["overall_att"], rel=self.POINT_RTOL) + assert results.att == pytest.approx(r_results["overall_att"], rel=self.POINT_RTOL) # SE parity: looser 5% rtol because Python's cohort-recentered # plug-in formula and R's may use slightly different small-sample # corrections. The full Lambda^G_{g,l=1} influence function fix # narrowed this gap from ~18% to ~3% on the test scenarios. - assert results.overall_se == pytest.approx( - r_results["overall_se"], rel=self.PURE_DIRECTION_SE_RTOL - ) + assert results.se == pytest.approx(r_results["overall_se"], rel=self.PURE_DIRECTION_SE_RTOL) # Phase-1 placebo DID_M^pl point estimate: backward-difference x # switch_direction convention (matches the multi-horizon placebo path # and R). On pure-direction panels this equals R to working precision. @@ -187,10 +183,8 @@ def test_parity_leavers_only(self, golden_values): results = _fit_dcdh_l1(df) r_results = scenario["results"] # Pure-direction scenario: tight point-estimate + 5% SE parity - assert results.overall_att == pytest.approx(r_results["overall_att"], rel=self.POINT_RTOL) - assert results.overall_se == pytest.approx( - r_results["overall_se"], rel=self.PURE_DIRECTION_SE_RTOL - ) + assert results.att == pytest.approx(r_results["overall_att"], rel=self.POINT_RTOL) + assert results.se == pytest.approx(r_results["overall_se"], rel=self.PURE_DIRECTION_SE_RTOL) # Phase-1 placebo point estimate matches R on pure-direction panels # (leavers side: switch_direction = -1). Guards the sign convention. assert results.placebo_effect == pytest.approx(r_results["placebo_effect"], abs=1e-8) @@ -203,9 +197,7 @@ def test_parity_mixed_single_switch(self, golden_values): results = _fit_dcdh_l1(df) r_results = scenario["results"] # Mixed scenario: see class docstring re. cohort vs. period control sets. - assert results.overall_att == pytest.approx( - r_results["overall_att"], rel=self.MIXED_POINT_RTOL - ) + assert results.att == pytest.approx(r_results["overall_att"], rel=self.MIXED_POINT_RTOL) def test_parity_hand_calculable_worked_example(self, golden_values): """ @@ -222,7 +214,7 @@ def test_parity_hand_calculable_worked_example(self, golden_values): results = _fit_dcdh_l1(df) r_results = scenario["results"] # Tight tolerance for this exact-arithmetic case - assert results.overall_att == pytest.approx(r_results["overall_att"], abs=1e-6) + assert results.att == pytest.approx(r_results["overall_att"], abs=1e-6) # --------------------------------------------------------------------------- diff --git a/tests/test_conley_vcov.py b/tests/test_conley_vcov.py index a37ce5fbe..f3cb43047 100644 --- a/tests/test_conley_vcov.py +++ b/tests/test_conley_vcov.py @@ -3125,8 +3125,8 @@ def test_panel_finite_se_and_metadata(self): conley_cutoff_km=2000.0, conley_lag_cutoff=1, ).fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.att) + assert np.isfinite(res.se) and res.se > 0 assert res.vcov_type == "conley" assert res.conley_lag_cutoff == 1 assert "Conley" in res.summary() @@ -3141,7 +3141,7 @@ def test_cross_sectional_finite_se(self): conley_cutoff_km=2000.0, conley_lag_cutoff=0, ).fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 assert res.conley_lag_cutoff == 0 def test_fwl_composability_vs_full_dummy(self): @@ -3156,8 +3156,8 @@ def test_fwl_composability_vs_full_dummy(self): conley_lag_cutoff=1, ).fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") ref_coef, ref_se = _full_dummy_conley_treatment(df, 2500.0, 1) - assert res.overall_att == pytest.approx(ref_coef, abs=1e-8) - assert res.overall_se == pytest.approx(ref_se, abs=1e-7) + assert res.att == pytest.approx(ref_coef, abs=1e-8) + assert res.se == pytest.approx(ref_se, abs=1e-7) def test_att_bit_identical_across_vcov(self): """conley is additive: ATT must be bit-identical to the hc1 fit.""" @@ -3172,7 +3172,7 @@ def test_att_bit_identical_across_vcov(self): conley_lag_cutoff=1, ).fit(df, **kw) r_hc1 = SunAbraham(vcov_type="hc1").fit(df, **kw) - assert r_conley.overall_att == pytest.approx(r_hc1.overall_att, abs=1e-12) + assert r_conley.att == pytest.approx(r_hc1.att, abs=1e-12) def test_conley_plus_cluster_product_kernel(self): from diff_diff import SunAbraham @@ -3192,8 +3192,8 @@ def test_conley_plus_cluster_product_kernel(self): conley_cutoff_km=2000.0, conley_lag_cutoff=1, ).fit(df, **kw) - assert np.isfinite(clustered.overall_se) and clustered.overall_se > 0 - assert clustered.overall_se != plain.overall_se + assert np.isfinite(clustered.se) and clustered.se > 0 + assert clustered.se != plain.se assert "cluster" in clustered.summary().lower() def test_reject_survey_design(self): @@ -3256,7 +3256,7 @@ def test_unbalanced_panel_alignment(self): conley_cutoff_km=2000.0, conley_lag_cutoff=1, ).fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 def test_get_set_params_carry_conley(self): from diff_diff import SunAbraham @@ -3290,7 +3290,7 @@ def test_conley_multi_cohort_event_study(self): conley_cutoff_km=3000.0, conley_lag_cutoff=1, ).fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 assert len(res.event_study_effects) >= 2 # genuinely multi-period for e, eff in res.event_study_effects.items(): assert np.isfinite(eff["se"]), (e, eff) @@ -3309,8 +3309,8 @@ def test_panel_finite_se_and_metadata(self): conley_cutoff_km=2000.0, conley_lag_cutoff=1, ).fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.att) + assert np.isfinite(res.se) and res.se > 0 assert res.vcov_type == "conley" assert res.conley_lag_cutoff == 1 assert "Conley" in res.summary() @@ -3325,7 +3325,7 @@ def test_cross_sectional_finite_se(self): conley_cutoff_km=2000.0, conley_lag_cutoff=0, ).fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 assert res.conley_lag_cutoff == 0 def test_fwl_composability_vs_full_dummy(self): @@ -3340,8 +3340,8 @@ def test_fwl_composability_vs_full_dummy(self): conley_lag_cutoff=1, ).fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") ref_coef, ref_se = _full_dummy_conley_treatment(df, 2500.0, 1) - assert res.overall_att == pytest.approx(ref_coef, abs=1e-8) - assert res.overall_se == pytest.approx(ref_se, abs=1e-7) + assert res.att == pytest.approx(ref_coef, abs=1e-8) + assert res.se == pytest.approx(ref_se, abs=1e-7) def test_att_bit_identical_across_vcov(self): from diff_diff import WooldridgeDiD @@ -3355,7 +3355,7 @@ def test_att_bit_identical_across_vcov(self): conley_lag_cutoff=1, ).fit(df, **kw) r_hc1 = WooldridgeDiD(vcov_type="hc1").fit(df, **kw) - assert r_conley.overall_att == pytest.approx(r_hc1.overall_att, abs=1e-12) + assert r_conley.att == pytest.approx(r_hc1.att, abs=1e-12) def test_conley_plus_cluster_product_kernel(self): from diff_diff import WooldridgeDiD @@ -3375,8 +3375,8 @@ def test_conley_plus_cluster_product_kernel(self): conley_cutoff_km=2000.0, conley_lag_cutoff=1, ).fit(df, **kw) - assert np.isfinite(clustered.overall_se) and clustered.overall_se > 0 - assert clustered.overall_se != plain.overall_se + assert np.isfinite(clustered.se) and clustered.se > 0 + assert clustered.se != plain.se assert "cluster" in clustered.summary().lower() def test_reject_survey_design(self): @@ -3440,7 +3440,7 @@ def test_unbalanced_panel_alignment(self): conley_cutoff_km=2000.0, conley_lag_cutoff=1, ).fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 def test_atomic_set_params_round_trip(self): from diff_diff import WooldridgeDiD @@ -3470,11 +3470,11 @@ def test_conley_cohort_trends_full_dummy(self): ck = dict(conley_coords=("lat", "lon"), conley_cutoff_km=3000.0, conley_lag_cutoff=1) r_conley = WooldridgeDiD(vcov_type="conley", cohort_trends=True, **ck).fit(df, **kw) r_hc1 = WooldridgeDiD(vcov_type="hc1", cohort_trends=True).fit(df, **kw) - assert np.isfinite(r_conley.overall_se) and r_conley.overall_se > 0 + assert np.isfinite(r_conley.se) and r_conley.se > 0 assert r_conley.vcov_type == "conley" # conley actually engaged on the full-dummy path (SE differs from hc1) - assert r_conley.overall_se != r_hc1.overall_se - assert r_conley.overall_att == pytest.approx(r_hc1.overall_att, abs=1e-9) + assert r_conley.se != r_hc1.se + assert r_conley.att == pytest.approx(r_hc1.att, abs=1e-9) def test_conley_aggregations_finite(self): """conley vcov flows through aggregate('group'|'calendar'|'event') — @@ -3491,7 +3491,7 @@ def test_conley_aggregations_finite(self): # aggregate() mutates in place, so re-fit per aggregation type. res = WooldridgeDiD(vcov_type="conley", **ck).fit(_staggered_panel(seed=52), **kw) res.aggregate(agg) - assert np.isfinite(res.overall_se) and res.overall_se > 0, agg + assert np.isfinite(res.se) and res.se > 0, agg effects = getattr(res, field) assert effects, (agg, "no effects populated") for key, eff in effects.items(): @@ -3514,7 +3514,7 @@ def test_conley_cohort_trends_aggregations_finite(self): _staggered_panel(seed=53), **kw ) res.aggregate(agg) - assert np.isfinite(res.overall_se) and res.overall_se > 0, agg + assert np.isfinite(res.se) and res.se > 0, agg effects = getattr(res, field) assert effects, (agg, "no effects populated") for key, eff in effects.items(): @@ -3533,8 +3533,8 @@ def test_conley_control_group_never_treated(self): df, **kw ) r_hc1 = WooldridgeDiD(vcov_type="hc1", control_group="never_treated").fit(df, **kw) - assert np.isfinite(r_conley.overall_se) and r_conley.overall_se > 0 - assert r_conley.overall_att == pytest.approx(r_hc1.overall_att, abs=1e-9) + assert np.isfinite(r_conley.se) and r_conley.se > 0 + assert r_conley.att == pytest.approx(r_hc1.att, abs=1e-9) def _staggered_panel(seed=51, n_units=36, n_periods=6): diff --git a/tests/test_continuous_did.py b/tests/test_continuous_did.py index cd4b59eeb..24c27e6de 100644 --- a/tests/test_continuous_did.py +++ b/tests/test_continuous_did.py @@ -428,7 +428,7 @@ def test_dose_response_shapes(self, basic_data): def test_overall_parameters(self, basic_data): est = ContinuousDiD() results = est.fit(basic_data, "outcome", "unit", "period", "first_treat", "dose") - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) assert np.isfinite(results.overall_acrt) def test_group_time_effects_populated(self, basic_data): @@ -569,7 +569,7 @@ def test_single_cohort_aggregation(self): "dose", ) assert len(results.groups) == 1 - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) class TestEventStudyAggregation: @@ -690,7 +690,7 @@ def test_bootstrap_ses_positive(self, ci_params): ) est = ContinuousDiD(n_bootstrap=n_boot, seed=42) results = est.fit(data, "outcome", "unit", "period", "first_treat", "dose") - assert results.overall_att_se > 0 + assert results.se > 0 assert results.overall_acrt_se > 0 # Dose-response SEs should be positive assert np.all(results.dose_response_att.se > 0) @@ -705,8 +705,8 @@ def test_bootstrap_ci_contains_estimate(self, ci_params): ) est = ContinuousDiD(n_bootstrap=n_boot, seed=42) results = est.fit(data, "outcome", "unit", "period", "first_treat", "dose") - lo, hi = results.overall_att_conf_int - assert lo <= results.overall_att <= hi + lo, hi = results.conf_int + assert lo <= results.att <= hi def test_bootstrap_acrt_ci_centered(self, ci_params): """Bootstrap ACRT CI should bracket the point estimate, not zero.""" @@ -745,7 +745,7 @@ def test_bootstrap_p_values_valid(self, ci_params): ) est = ContinuousDiD(n_bootstrap=n_boot, seed=42) results = est.fit(data, "outcome", "unit", "period", "first_treat", "dose") - assert 0 <= results.overall_att_p_value <= 1 + assert 0 <= results.p_value <= 1 assert 0 <= results.overall_acrt_p_value <= 1 def test_bootstrap_dose_response_p_values(self, ci_params): @@ -786,7 +786,7 @@ def test_analytical_se_positive(self): ) est = ContinuousDiD(n_bootstrap=0) results = est.fit(data, "outcome", "unit", "period", "first_treat", "dose") - assert results.overall_att_se > 0 + assert results.se > 0 assert results.overall_acrt_se > 0 def test_analytical_ci(self): @@ -798,8 +798,8 @@ def test_analytical_ci(self): ) est = ContinuousDiD(n_bootstrap=0) results = est.fit(data, "outcome", "unit", "period", "first_treat", "dose") - lo, hi = results.overall_att_conf_int - assert lo < results.overall_att < hi + lo, hi = results.conf_int + assert lo < results.att < hi class TestEdgeCases: @@ -1208,7 +1208,7 @@ def test_analytical_se_matches_bootstrap(self, ci_params): data, "outcome", "unit", "period", "first_treat", "dose" ) threshold = 0.50 if n_boot < 100 else 0.30 - ratio = results_analytic.overall_att_se / results_boot.overall_att_se + ratio = results_analytic.se / results_boot.se assert (1 - threshold) < ratio < (1 + threshold) / (1 - threshold), ( f"Analytical/bootstrap SE ratio = {ratio:.3f}, " f"expected within [{1 - threshold:.2f}, {(1 + threshold) / (1 - threshold):.2f}]" @@ -1372,7 +1372,7 @@ def test_anticipation_not_yet_treated_excludes_anticipation_window(self): ) assert np.isfinite( - results.overall_att + results.att ), "overall_att should be finite with anticipation + not_yet_treated" assert results.dose_response_att is not None, "dose-response curve should exist" @@ -1391,7 +1391,7 @@ def test_no_post_treatment_cells_warns(self): est = ContinuousDiD() with pytest.warns(UserWarning, match="[Nn]o post-treatment"): results = est.fit(data, "outcome", "unit", "period", "first_treat", "dose") - assert np.isnan(results.overall_att) + assert np.isnan(results.att) assert np.isnan(results.overall_acrt) @@ -1435,12 +1435,12 @@ def test_bootstrap_percentile_ci(self, ci_params): ) est = ContinuousDiD(n_bootstrap=n_boot, seed=42) results = est.fit(data, "outcome", "unit", "period", "first_treat", "dose") - lo, hi = results.overall_att_conf_int - estimate = results.overall_att + lo, hi = results.conf_int + estimate = results.att # CI should contain estimate assert lo <= estimate <= hi # p-value should be finite and in [0, 1] - assert 0 <= results.overall_att_p_value <= 1 + assert 0 <= results.p_value <= 1 # Percentile CIs are generally asymmetric around the estimate. # With enough bootstrap reps, the upper and lower distances differ. upper_dist = hi - estimate @@ -1594,7 +1594,7 @@ def test_ipw_without_covariates_ok(self): data = _cov_data() est = ContinuousDiD(estimation_method="ipw") res = est.fit(data, "outcome", "unit", "period", "first_treat", "dose") - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) def test_survey_with_covariates_raises(self): from diff_diff.survey import SurveyDesign @@ -1766,7 +1766,7 @@ def test_covariate_eventstudy_and_bootstrap(self): "dose", covariates=["x1"], ) - assert np.isfinite(es.overall_att) and np.isfinite(es.overall_att_se) + assert np.isfinite(es.att) and np.isfinite(es.se) assert len(es.aggregate("event_study").event_time) > 0 ana = ContinuousDiD(estimation_method=method).fit( data, @@ -1786,9 +1786,9 @@ def test_covariate_eventstudy_and_bootstrap(self): "dose", covariates=["x1"], ) - assert np.isfinite(boot.overall_att_se) + assert np.isfinite(boot.se) # bootstrap SE within ~30% of analytical (same linearized IF) - assert abs(boot.overall_att_se - ana.overall_att_se) / ana.overall_att_se < 0.3 + assert abs(boot.se - ana.se) / ana.se < 0.3 def test_clone_refit_idempotent(self): data = _cov_data() @@ -1796,8 +1796,8 @@ def test_clone_refit_idempotent(self): r1 = est.fit(data, "outcome", "unit", "period", "first_treat", "dose", covariates=["x1"]) clone = ContinuousDiD(**est.get_params()) r2 = clone.fit(data, "outcome", "unit", "period", "first_treat", "dose", covariates=["x1"]) - assert abs(float(r1.overall_att) - float(r2.overall_att)) < 1e-12 - assert abs(float(r1.overall_att_se) - float(r2.overall_att_se)) < 1e-12 + assert abs(float(r1.att) - float(r2.att)) < 1e-12 + assert abs(float(r1.se) - float(r2.se)) < 1e-12 def test_no_covariate_path_unchanged(self): """Passing covariates=None runs the unchanged unconditional path.""" @@ -1807,8 +1807,8 @@ def test_no_covariate_path_unchanged(self): alt = ContinuousDiD(estimation_method="reg", seed=1).fit( data, "outcome", "unit", "period", "first_treat", "dose" ) - assert float(base.overall_att) == float(alt.overall_att) - assert float(base.overall_att_se) == float(alt.overall_att_se) + assert float(base.att) == float(alt.att) + assert float(base.se) == float(alt.se) # ============================================================================= @@ -2038,7 +2038,7 @@ def test_continuous_default_matches_explicit(self): np.testing.assert_allclose( r_default.dose_response_att.effects, r_explicit.dose_response_att.effects ) - np.testing.assert_allclose(r_default.overall_att, r_explicit.overall_att) + np.testing.assert_allclose(r_default.att, r_explicit.att) class TestLowestDoseAPI: diff --git a/tests/test_csdid_ported.py b/tests/test_csdid_ported.py index b3fe9b771..28ca2a5fc 100644 --- a/tests/test_csdid_ported.py +++ b/tests/test_csdid_ported.py @@ -190,9 +190,7 @@ def test_two_period_all_aggregations(self, ci_params): first_treat="first_treat", covariates=["X"], ) - assert ( - abs(results.overall_att - 1.0) < 0.5 - ), f"Simple ATT = {results.overall_att:.3f}, expected ≈ 1.0" + assert abs(results.att - 1.0) < 0.5, f"Simple ATT = {results.att:.3f}, expected ≈ 1.0" # 4.0: the fit stores no aggregation surface; both levels are post-fit assert results.event_study_effects is None and results.group_effects is None # Event study: post-treatment effect ≈ 1 @@ -1453,8 +1451,8 @@ def test_golden_reg_aggregation_se(self, golden_values): covariates=["X"], ) r = scenario["results"] - assert abs(results.overall_att - r["simple"]["att"]) < 1e-8 - assert abs(results.overall_se - r["simple"]["se"]) < 1e-8 + assert abs(results.att - r["simple"]["att"]) < 1e-8 + assert abs(results.se - r["simple"]["se"]) < 1e-8 g_att = np.atleast_1d(r["group"]["att"])[0] g_se = np.atleast_1d(r["group"]["se"])[0] grp = group_dict(results)[2] @@ -1520,8 +1518,8 @@ def test_golden_ipw_aggregation_se_vs_r_did_251(self, golden_values): covariates=["X"], ) # R did 2.5.1: aggte(type="simple", bstrap=FALSE, cband=FALSE) - assert abs(results.overall_att - r_results["simple"]["att"]) < 1e-6 - assert abs(results.overall_se - r_results["simple"]["se"]) < 1e-6 + assert abs(results.att - r_results["simple"]["att"]) < 1e-6 + assert abs(results.se - r_results["simple"]["se"]) < 1e-6 # aggte(type="dynamic"): (att.egt, se.egt) per event time r_dyn = r_results["dynamic"] assert len(r_dyn["egt"]) > 0 @@ -2119,8 +2117,8 @@ def test_simple_overall_matches_r(self, unbalanced_golden): s = unbalanced_golden["simple"] n_units = unbalanced_golden["meta"]["n_units"] fac = np.sqrt(n_units / (n_units - 1)) - assert res.overall_att == pytest.approx(s["overall_att"], abs=1e-9) - assert res.overall_se == pytest.approx(s["overall_se"] * fac, abs=1e-8) + assert res.att == pytest.approx(s["overall_att"], abs=1e-9) + assert res.se == pytest.approx(s["overall_se"] * fac, abs=1e-8) def test_group_effects_match_r(self, unbalanced_golden): """Group (cohort) effects match R `aggte(type="group")` att.egt / se.egt.""" @@ -2310,7 +2308,7 @@ def mk(): assert es_res and set(es_res) == set(es_ref) for e, v in es_res.items(): assert v["effect"] == pytest.approx(es_ref[e]["effect"], abs=1e-12) - assert res.overall_att == pytest.approx(ref.overall_att, abs=1e-12) + assert res.att == pytest.approx(ref.att, abs=1e-12) def test_complete_case_rectangular_stays_inert(self): """When a WHOLE unit and a WHOLE period are dropped by non-finite diff --git a/tests/test_diagnostic_report.py b/tests/test_diagnostic_report.py index 666719cc5..79ddcf6be 100644 --- a/tests/test_diagnostic_report.py +++ b/tests/test_diagnostic_report.py @@ -904,9 +904,13 @@ class CallawaySantAnnaResults: # noqa: D401 — test stub that mimics the real obj.vcov = None obj.interaction_indices = None obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.1 + obj.se = 0.1 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.8, 1.2) + obj.conf_int = (0.8, 1.2) obj.alpha = 0.05 obj.n_obs = 100 obj.n_treated = 50 @@ -1195,9 +1199,13 @@ class CallawaySantAnnaResults: obj = CallawaySantAnnaResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.1 + obj.se = 0.1 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.8, 1.2) + obj.conf_int = (0.8, 1.2) obj.alpha = 0.05 obj.n_obs = 200 obj.n_treated = 40 @@ -1248,9 +1256,13 @@ class CallawaySantAnnaResults: obj = CallawaySantAnnaResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.1 + obj.se = 0.1 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.8, 1.2) + obj.conf_int = (0.8, 1.2) obj.alpha = 0.05 obj.n_obs = 200 obj.n_treated = 40 @@ -1306,9 +1318,13 @@ class CallawaySantAnnaResults: obj.event_study_vcov = None obj.event_study_vcov_index = None obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.1 + obj.se = 0.1 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.8, 1.2) + obj.conf_int = (0.8, 1.2) obj.alpha = 0.05 obj.n_obs = 100 obj.n_treated = 50 @@ -1390,9 +1406,13 @@ class StackedDiDResults: obj = StackedDiDResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.2 + obj.se = 0.2 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.6, 1.4) + obj.conf_int = (0.6, 1.4) obj.alpha = 0.05 obj.n_obs = 400 obj.n_treated_units = 100 @@ -1431,9 +1451,13 @@ class CallawaySantAnnaResults: obj = CallawaySantAnnaResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.2 + obj.se = 0.2 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.6, 1.4) + obj.conf_int = (0.6, 1.4) obj.alpha = 0.05 obj.n_obs = 500 obj.n_treated = 100 @@ -1480,9 +1504,13 @@ class StackedDiDResults: obj = StackedDiDResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.2 + obj.se = 0.2 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.6, 1.4) + obj.conf_int = (0.6, 1.4) obj.alpha = 0.05 obj.n_obs = 400 obj.n_treated_units = 100 @@ -1534,9 +1562,13 @@ class CallawaySantAnnaResults: obj.event_study_vcov = None obj.event_study_vcov_index = None obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.1 + obj.se = 0.1 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.8, 1.2) + obj.conf_int = (0.8, 1.2) obj.alpha = 0.05 obj.n_obs = 100 obj.n_treated = 50 @@ -1570,9 +1602,13 @@ class StackedDiDResults: obj = StackedDiDResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.2 + obj.se = 0.2 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.6, 1.4) + obj.conf_int = (0.6, 1.4) obj.alpha = 0.05 obj.n_obs = 400 obj.n_treated_units = 100 @@ -1750,9 +1786,13 @@ class CallawaySantAnnaResults: obj = CallawaySantAnnaResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.1 + obj.se = 0.1 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.8, 1.2) + obj.conf_int = (0.8, 1.2) obj.alpha = 0.05 obj.n_obs = 200 obj.n_treated = 40 @@ -1848,9 +1888,13 @@ class CallawaySantAnnaResults: obj = CallawaySantAnnaResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.1 + obj.se = 0.1 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.8, 1.2) + obj.conf_int = (0.8, 1.2) obj.alpha = 0.05 obj.n_obs = 200 obj.n_treated = 40 @@ -1898,9 +1942,13 @@ class ContinuousDiDResults: obj = ContinuousDiDResults() obj.overall_att = 2.5 + obj.att = 2.5 obj.overall_att_se = 0.4 + obj.se = 0.4 obj.overall_att_p_value = 0.00001 + obj.p_value = 0.00001 obj.overall_att_conf_int = (1.7, 3.3) + obj.conf_int = (1.7, 3.3) obj.alpha = 0.05 result = _extract_scalar_headline(obj) @@ -2446,9 +2494,13 @@ class ChaisemartinDHaultfoeuilleResults: stub = ChaisemartinDHaultfoeuilleResults() stub.att = 1.0 stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -2528,9 +2580,13 @@ class CallawaySantAnnaResults: 2: {"effect": float("nan"), "se": 0.2, "n_groups": 5}, # non-finite } stub.overall_att = 1.5 + stub.att = 1.5 stub.overall_se = 0.1 + stub.se = 0.1 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (1.3, 1.7) + stub.conf_int = (1.3, 1.7) stub.alpha = 0.05 stub.n_obs = 100 stub.n_treated = 40 @@ -2610,9 +2666,13 @@ class CallawaySantAnnaResults: obj = CallawaySantAnnaResults() obj.overall_att = 1.0 + obj.att = 1.0 obj.overall_se = 0.2 + obj.se = 0.2 obj.overall_p_value = 0.001 + obj.p_value = 0.001 obj.overall_conf_int = (0.6, 1.4) + obj.conf_int = (0.6, 1.4) obj.alpha = 0.05 obj.n_obs = 600 obj.n_treated = 200 diff --git a/tests/test_dml_did.py b/tests/test_dml_did.py index 9074d009b..82531ba46 100644 --- a/tests/test_dml_did.py +++ b/tests/test_dml_did.py @@ -626,7 +626,7 @@ def test_all_pre_treatment_cells_nan_headline(self): "se": res.se, "t_stat": res.t_stat, "p_value": res.p_value, - "conf_int": res.overall_conf_int, + "conf_int": res.conf_int, } ) assert any( @@ -777,8 +777,8 @@ def test_bootstrap_override_wired(self, data): assert e["effect"] == analytical.group_time_effects[k]["effect"] # Inference fields DIFFER: overall quartet + per-cell quartet. assert boot.se != analytical.se - assert boot.overall_conf_int != analytical.overall_conf_int - assert boot.overall_p_value != analytical.overall_p_value + assert boot.conf_int != analytical.conf_int + assert boot.p_value != analytical.p_value changed = [ k for k, e in boot.group_time_effects.items() diff --git a/tests/test_dr_scores.py b/tests/test_dr_scores.py index c0e366f0d..70985df3e 100644 --- a/tests/test_dr_scores.py +++ b/tests/test_dr_scores.py @@ -87,10 +87,8 @@ def test_inf_func_matches_committed_oracle(self): @requires_python_backend def test_continuous_did_dr_matches_committed_oracle(self): res = estimator_tier_fit() - np.testing.assert_allclose(res.overall_att, ESTIMATOR_TIER_OVERALL_ATT, atol=1e-8, rtol=0) - np.testing.assert_allclose( - res.overall_att_se, ESTIMATOR_TIER_OVERALL_ATT_SE, atol=1e-8, rtol=0 - ) + np.testing.assert_allclose(res.att, ESTIMATOR_TIER_OVERALL_ATT, atol=1e-8, rtol=0) + np.testing.assert_allclose(res.se, ESTIMATOR_TIER_OVERALL_ATT_SE, atol=1e-8, rtol=0) np.testing.assert_allclose(res.overall_acrt, ESTIMATOR_TIER_OVERALL_ACRT, atol=1e-8, rtol=0) np.testing.assert_allclose( res.overall_acrt_se, ESTIMATOR_TIER_OVERALL_ACRT_SE, atol=1e-8, rtol=0 diff --git a/tests/test_efficient_did.py b/tests/test_efficient_did.py index 44a64991b..6a96dae08 100644 --- a/tests/test_efficient_did.py +++ b/tests/test_efficient_did.py @@ -136,8 +136,8 @@ def test_basic_fit(self): result = edid.fit(df, "y", "unit", "time", "first_treat") assert isinstance(result, EfficientDiDResults) - assert isinstance(result.overall_att, float) - assert isinstance(result.overall_se, float) + assert isinstance(result.att, float) + assert isinstance(result.se, float) assert len(result.group_time_effects) > 0 assert result.n_obs == len(df) assert result.pt_assumption == "all" @@ -146,13 +146,13 @@ def test_zero_effect(self): df = _make_simple_panel(effect=0.0) result = EfficientDiD().fit(df, "y", "unit", "time", "first_treat") # ATT should be near 0 - assert abs(result.overall_att) < 0.5 + assert abs(result.att) < 0.5 def test_positive_effect(self): df = _make_simple_panel(effect=2.0, n_units=200) result = EfficientDiD().fit(df, "y", "unit", "time", "first_treat") # Recover ~2.0 within 2 SE - assert abs(result.overall_att - 2.0) < 2 * result.overall_se + 0.5 + assert abs(result.att - 2.0) < 2 * result.se + 0.5 def test_single_pre_period(self): """When g=2 (only 1 pre-period), weights are trivially [1.0].""" @@ -598,7 +598,7 @@ def test_ridge_engaged_and_overall_stable(self): df = _make_staggered_panel(n_per_group=80, n_control=100) res_r = EfficientDiD().fit(df, "y", "unit", "time", "first_treat") res_l = EfficientDiD(omega_ridge=0.0).fit(df, "y", "unit", "time", "first_treat") - assert res_r.overall_att == pytest.approx(res_l.overall_att, rel=1e-4) + assert res_r.att == pytest.approx(res_l.att, rel=1e-4) diffs = [ abs(res_r.group_time_effects[k]["effect"] - res_l.group_time_effects[k]["effect"]) for k in res_r.group_time_effects @@ -652,7 +652,7 @@ def _surfaces(res): gt = {k: (v["effect"], v["se"]) for k, v in res.group_time_effects.items()} # post-fit container (the fit-time surface is gone since 4.0) es = {k: (v["effect"], v["se"]) for k, v in es_dict(res).items()} - return gt, es, (res.overall_att, res.overall_se) + return gt, es, (res.att, res.se) @classmethod def _assert_close_surfaces(cls, r1, r2, rtol, atol=1e-12): @@ -1432,7 +1432,7 @@ def test_cluster_no_longer_raises(self): """cluster parameter should not raise NotImplementedError.""" df = self._make_clustered_panel() result = EfficientDiD(cluster="cluster_id").fit(df, "y", "unit", "time", "first_treat") - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_single_unit_clusters_match_unclustered(self): """With one unit per cluster, clustered SE should match unclustered.""" @@ -1443,11 +1443,9 @@ def test_single_unit_clusters_match_unclustered(self): result_clustered = EfficientDiD(cluster="cluster_id").fit( df, "y", "unit", "time", "first_treat" ) - assert result_clustered.overall_att == pytest.approx( - result_unclustered.overall_att, abs=1e-10 - ) + assert result_clustered.att == pytest.approx(result_unclustered.att, abs=1e-10) # SEs should be very close (centering correction is negligible) - assert result_clustered.overall_se == pytest.approx(result_unclustered.overall_se, rel=0.05) + assert result_clustered.se == pytest.approx(result_unclustered.se, rel=0.05) def test_clustered_se_at_least_as_large(self): """Clustered SE >= unclustered SE with positive intracluster correlation.""" @@ -1457,8 +1455,8 @@ def test_clustered_se_at_least_as_large(self): df, "y", "unit", "time", "first_treat" ) # Both SEs should be finite and positive - assert result_clustered.overall_se > 0 - assert result_unclustered.overall_se > 0 + assert result_clustered.se > 0 + assert result_unclustered.se > 0 def test_clustered_aggregate_event_study(self): """Clustered SE: the post-fit event-study aggregation should produce finite results.""" @@ -1485,8 +1483,8 @@ def test_cluster_bootstrap(self, ci_params): result = EfficientDiD(cluster="cluster_id", n_bootstrap=n_boot, seed=42).fit( df, "y", "unit", "time", "first_treat" ) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.se) + assert result.se > 0 def test_few_clusters_warns(self): """Fewer than 50 clusters should warn.""" @@ -1590,7 +1588,7 @@ def test_single_unit_cohort_warns(self): with pytest.warns(UserWarning, match="only 1 unit"): result = EfficientDiD().fit(combined, "y", "unit", "time", "first_treat") # Estimation should still succeed - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_small_share_cohort_warns(self): """Cohort with < 1% share triggers precision warning.""" @@ -1604,7 +1602,7 @@ def test_small_share_cohort_warns(self): with pytest.warns(UserWarning, match="< 1%"): result = EfficientDiD().fit(combined, "y", "unit", "time", "first_treat") - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_normal_cohorts_no_warning(self): """Normal-sized cohorts should not warn about cohort size.""" @@ -1660,7 +1658,7 @@ def test_last_cohort_pt_all(self): df, "y", "unit", "time", "first_treat" ) # Last cohort (7) becomes pseudo-control, only groups 3 and 5 remain - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) assert result.control_group == "last_cohort" assert 7 not in result.groups @@ -1675,7 +1673,7 @@ def test_last_cohort_pt_post(self): result = EfficientDiD(pt_assumption="post", control_group="last_cohort").fit( df, "y", "unit", "time", "first_treat" ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_last_cohort_reasonable_att(self): """Last-cohort ATT should be close to true effect.""" @@ -1691,7 +1689,7 @@ def test_last_cohort_reasonable_att(self): df, "y", "unit", "time", "first_treat" ) # ATT should be in the ballpark of the true effects (1.5-2.0 range) - assert 0.5 < result.overall_att < 3.5 + assert 0.5 < result.att < 3.5 def test_last_cohort_single_cohort_raises(self): """Single treatment cohort with last_cohort should raise.""" @@ -1709,7 +1707,7 @@ def test_last_cohort_with_never_treated_reclassifies(self): assert 5 not in result.groups assert 3 in result.groups assert result.control_group == "last_cohort" - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_control_group_get_params(self): """control_group should appear in get_params and round-trip via set_params.""" @@ -1760,8 +1758,8 @@ def test_last_cohort_with_anticipation_trims_at_last_g_minus_anticipation(self): assert max(result_a0.time_periods) == 6 assert max(result_a1.time_periods) == 5 assert len(result_a1.time_periods) == len(result_a0.time_periods) - 1 - assert np.isfinite(result_a0.overall_att) - assert np.isfinite(result_a1.overall_att) + assert np.isfinite(result_a0.att) + assert np.isfinite(result_a1.att) assert 7 not in result_a0.groups assert 7 not in result_a1.groups @@ -1813,8 +1811,8 @@ def test_last_cohort_bootstrap(self, ci_params): result = EfficientDiD(control_group="last_cohort", n_bootstrap=n_boot, seed=42).fit( df, "y", "unit", "time", "first_treat" ) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.se) + assert result.se > 0 assert 7 not in result.groups @@ -1895,8 +1893,8 @@ def test_bootstrap_se_finite(self, ci_params): df, "y", "unit", "time", "first_treat" ) assert result.bootstrap_results is not None - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.se) + assert result.se > 0 for gt, d in result.group_time_effects.items(): if np.isfinite(d["effect"]): assert np.isfinite(d["se"]) @@ -1945,7 +1943,7 @@ def test_bootstrap_coverage_basic(self, ci_params): result = EfficientDiD(n_bootstrap=n_boot, seed=42).fit( df, "y", "unit", "time", "first_treat" ) - ci = result.overall_conf_int + ci = result.conf_int # True effect is 2.0 — should be within CI for this seed if np.isfinite(ci[0]) and np.isfinite(ci[1]): # Just check CI is reasonable (not testing exact coverage) @@ -2049,13 +2047,13 @@ def test_analytical_se_consistency(self, ci_params): # Analytical SE res_anal = EfficientDiD(n_bootstrap=0).fit(df, "y", "unit", "time", "first_treat") - anal_se = res_anal.overall_se + anal_se = res_anal.se # Bootstrap SE res_boot = EfficientDiD(n_bootstrap=n_boot, seed=42).fit( df, "y", "unit", "time", "first_treat" ) - boot_se = res_boot.overall_se + boot_se = res_boot.se if np.isfinite(anal_se) and np.isfinite(boot_se) and boot_se > 0: rel_diff = abs(anal_se - boot_se) / boot_se @@ -2148,13 +2146,13 @@ def test_wif_se_vs_bootstrap(self, ci_params): # Analytical SE (with WIF) res_anal = EfficientDiD(n_bootstrap=0).fit(df, "y", "unit", "time", "first_treat") - anal_se = res_anal.overall_se + anal_se = res_anal.se # Bootstrap SE res_boot = EfficientDiD(n_bootstrap=n_boot, seed=42).fit( df, "y", "unit", "time", "first_treat" ) - boot_se = res_boot.overall_se + boot_se = res_boot.se if np.isfinite(anal_se) and np.isfinite(boot_se) and boot_se > 0: rel_diff = abs(anal_se - boot_se) / boot_se @@ -2262,9 +2260,7 @@ def test_bootstrap_nan_cell_resilience(self, ci_params): result = EfficientDiD(pt_assumption="all", n_bootstrap=n_boot, seed=42).fit( df, "y", "unit", "time", "first_treat" ) - assert np.isfinite( - result.overall_se - ), f"Overall SE should be finite, got {result.overall_se}" + assert np.isfinite(result.se), f"Overall SE should be finite, got {result.se}" assert result.bootstrap_results is not None @@ -2324,8 +2320,8 @@ def test_covariates_fit_produces_results(self): ) assert isinstance(result, EfficientDiDResults) assert result.estimation_path == "dr" - assert np.isfinite(result.overall_att) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert result.se > 0 assert len(result.group_time_effects) > 0 for (g, t), eff in result.group_time_effects.items(): assert np.isfinite(eff["effect"]) @@ -2343,8 +2339,8 @@ def test_nocov_match_when_irrelevant(self): ) # ATT should be close (not identical due to nuisance estimation noise) assert ( - abs(r_cov.overall_att - r_nocov.overall_att) < 0.3 - ), f"DR ATT {r_cov.overall_att:.4f} too far from nocov {r_nocov.overall_att:.4f}" + abs(r_cov.att - r_nocov.att) < 0.3 + ), f"DR ATT {r_cov.att:.4f} too far from nocov {r_nocov.att:.4f}" def test_covariates_produce_valid_se(self): """DR path with covariates explaining variance produces valid SE.""" @@ -2353,11 +2349,11 @@ def test_covariates_produce_valid_se(self): df, "y", "unit", "time", "first_treat", covariates=["x1"] ) # DR SE should be positive and finite - assert r_cov.overall_se > 0 - assert np.isfinite(r_cov.overall_se) + assert r_cov.se > 0 + assert np.isfinite(r_cov.se) # ATT should be close to the nocov estimate (no confounding) r_nocov = EfficientDiD(pt_assumption="post").fit(df, "y", "unit", "time", "first_treat") - assert abs(r_cov.overall_att - r_nocov.overall_att) < 0.2 + assert abs(r_cov.att - r_nocov.att) < 0.2 def test_covariates_recover_effect_under_confounding(self): """DGP with confounding: DR should recover true ATT closer to truth than nocov. @@ -2379,11 +2375,11 @@ def test_covariates_recover_effect_under_confounding(self): r_cov = EfficientDiD(pt_assumption="post").fit( df, "y", "unit", "time", "first_treat", covariates=["x1"] ) - assert np.isfinite(r_nocov.overall_att) - assert np.isfinite(r_cov.overall_att) + assert np.isfinite(r_nocov.att) + assert np.isfinite(r_cov.att) # DR should be closer to the true ATT than nocov - bias_nocov = abs(r_nocov.overall_att - true_att) - bias_cov = abs(r_cov.overall_att - true_att) + bias_nocov = abs(r_nocov.att - true_att) + bias_cov = abs(r_cov.att - true_att) assert ( bias_cov < bias_nocov ), f"DR bias ({bias_cov:.4f}) should be smaller than nocov bias ({bias_nocov:.4f})" @@ -2474,7 +2470,7 @@ def test_covariates_pt_post(self): ) assert isinstance(result, EfficientDiDResults) assert result.estimation_path == "dr" - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_covariates_pt_all(self): df = _make_covariate_panel() @@ -2483,7 +2479,7 @@ def test_covariates_pt_all(self): ) assert isinstance(result, EfficientDiDResults) assert result.estimation_path == "dr" - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_covariates_aggregate_event_study(self): df = _make_covariate_panel() @@ -2526,7 +2522,7 @@ def test_covariates_aggregate_all(self): ) assert result.aggregate("event_study") is not None assert result.aggregate("group") is not None - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) class TestCovariatesEdgeCases: @@ -2537,14 +2533,14 @@ def test_single_covariate(self): result = EfficientDiD(pt_assumption="post").fit( df, "y", "unit", "time", "first_treat", covariates=["x1"] ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_binary_covariate(self): df = _make_covariate_panel() result = EfficientDiD(pt_assumption="post").fit( df, "y", "unit", "time", "first_treat", covariates=["x2"] ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_many_covariates(self): """Multiple covariates including derived ones.""" @@ -2559,7 +2555,7 @@ def test_many_covariates(self): result = EfficientDiD(pt_assumption="post").fit( df, "y", "unit", "time", "first_treat", covariates=["x1", "x2", "x3"] ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_sieve_ratio_produces_valid_results(self): """Sieve ratio estimation produces finite ATT with valid ratios.""" @@ -2567,8 +2563,8 @@ def test_sieve_ratio_produces_valid_results(self): result = EfficientDiD(pt_assumption="post", sieve_k_max=3, sieve_criterion="bic").fit( df, "y", "unit", "time", "first_treat", covariates=["x1"] ) - assert np.isfinite(result.overall_att) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert result.se > 0 def test_shuffled_units_match_ordered(self): """Shuffled unit ordering must produce same ATT as original ordering. @@ -2592,9 +2588,8 @@ def test_shuffled_units_match_ordered(self): r_shuffled = EfficientDiD(pt_assumption="post").fit( df_shuffled, "y", "unit", "time", "first_treat", covariates=["x1"] ) - assert abs(r_ordered.overall_att - r_shuffled.overall_att) < 1e-10, ( - f"ATT mismatch: ordered={r_ordered.overall_att:.6f} " - f"vs shuffled={r_shuffled.overall_att:.6f}" + assert abs(r_ordered.att - r_shuffled.att) < 1e-10, ( + f"ATT mismatch: ordered={r_ordered.att:.6f} " f"vs shuffled={r_shuffled.att:.6f}" ) def test_extreme_covariates_warns_overlap(self): @@ -2615,8 +2610,8 @@ def test_extreme_covariates_warns_overlap(self): result = EfficientDiD(pt_assumption="post").fit( df, "y", "unit", "time", "first_treat", covariates=["x_sep"] ) - assert np.isfinite(result.overall_att) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert result.se > 0 def test_eif_mean_approximately_zero(self): """EIF with per-unit weights should have sample mean ≈ 0.""" @@ -2643,11 +2638,11 @@ def test_bootstrap_with_covariates_smoke(self): df, "y", "unit", "time", "first_treat", covariates=["x1"] ) assert result.bootstrap_results is not None - assert np.isfinite(result.overall_att) - assert result.overall_se > 0 - ci = result.overall_conf_int + assert np.isfinite(result.att) + assert result.se > 0 + ci = result.conf_int assert ci[0] < ci[1], "CI lower must be less than upper" - assert np.isfinite(result.overall_p_value) + assert np.isfinite(result.p_value) def test_covariates_pt_all_bootstrap(self): """PT-All + bootstrap + covariates end-to-end.""" @@ -2663,8 +2658,8 @@ def test_covariates_pt_all_bootstrap(self): assert result.bootstrap_results is not None assert result.aggregate("event_study") is not None assert result.aggregate("group") is not None - assert np.isfinite(result.overall_att) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert result.se > 0 class TestSieveFallbacks: @@ -2984,8 +2979,8 @@ def test_default_hc1_bit_equal_baseline(self): ) r_default = EfficientDiD().fit(**common) r_explicit = EfficientDiD(vcov_type="hc1").fit(**common) - assert r_default.overall_att == r_explicit.overall_att - assert r_default.overall_se == r_explicit.overall_se + assert r_default.att == r_explicit.att + assert r_default.se == r_explicit.se # ---- Surface 2: cluster path bit-equal -------------------------------- @@ -3000,8 +2995,8 @@ def test_cluster_hc1_bit_equal_baseline(self): ) r_default = EfficientDiD(cluster="state").fit(**common) r_explicit = EfficientDiD(cluster="state", vcov_type="hc1").fit(**common) - assert r_default.overall_att == r_explicit.overall_att - assert r_default.overall_se == r_explicit.overall_se + assert r_default.att == r_explicit.att + assert r_default.se == r_explicit.se # ---- Surface 3: TSL-survey path bit-equal ----------------------------- @@ -3018,8 +3013,8 @@ def test_survey_tsl_hc1_bit_equal_baseline(self): ) r_default = EfficientDiD().fit(**common) r_explicit = EfficientDiD(vcov_type="hc1").fit(**common) - assert r_default.overall_att == r_explicit.overall_att - assert r_default.overall_se == r_explicit.overall_se + assert r_default.att == r_explicit.att + assert r_default.se == r_explicit.se # ---- Surface 4: replicate-survey path bit-equal ----------------------- @@ -3041,8 +3036,8 @@ def test_survey_replicate_hc1_bit_equal_baseline(self): ) r_default = EfficientDiD().fit(**common) r_explicit = EfficientDiD(vcov_type="hc1").fit(**common) - assert r_default.overall_att == r_explicit.overall_att - assert r_default.overall_se == r_explicit.overall_se + assert r_default.att == r_explicit.att + assert r_default.se == r_explicit.se # Per-horizon / per-group SE override branches must also agree under # the replicate-weight variance path (post-fit containers). es_d, es_e = r_default.aggregate("event_study"), r_explicit.aggregate("event_study") @@ -3203,8 +3198,8 @@ def test_dr_path_hc1_bit_equal(self): r_explicit = EfficientDiD(vcov_type="hc1").fit(**common) assert r_default.estimation_path == "dr" assert r_explicit.estimation_path == "dr" - assert r_default.overall_att == r_explicit.overall_att - assert r_default.overall_se == r_explicit.overall_se + assert r_default.att == r_explicit.att + assert r_default.se == r_explicit.se # ---- Input rejection: methodology-rooted messages --------------------- @@ -3389,8 +3384,8 @@ def test_fit_clone_idempotent_on_vcov_type(self): r1 = ed1.fit(data, outcome="y", unit="unit", time="time", first_treat="first_treat") ed2 = EfficientDiD(**ed1.get_params()) r2 = ed2.fit(data, outcome="y", unit="unit", time="time", first_treat="first_treat") - assert r1.overall_att == r2.overall_att - assert r1.overall_se == r2.overall_se + assert r1.att == r2.att + assert r1.se == r2.se assert r1.vcov_type == r2.vcov_type @@ -3513,7 +3508,7 @@ def counting_cached(X, degree, cache): result = EfficientDiD(pt_assumption="post").fit( df, "y", "unit", "time", "first_treat", covariates=["x1", "x2"] ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) # The path was exercised through the cache. assert request_keys, "covariate DR path did not run the sieve helpers" # Each distinct (X, degree) was built exactly once (perfect dedup)... diff --git a/tests/test_efficient_did_validation.py b/tests/test_efficient_did_validation.py index c1410f298..286e8881a 100644 --- a/tests/test_efficient_did_validation.py +++ b/tests/test_efficient_did_validation.py @@ -118,9 +118,9 @@ def _run_mc_simulation(n_sims, rho, seed=1000, also_cs=False): first_treat="first_treat", ) edid_estimates.append(_compute_es_avg(res)) - edid_overall_att.append(res.overall_att) - edid_overall_se.append(res.overall_se) - edid_overall_ci.append(res.overall_conf_int) + edid_overall_att.append(res.att) + edid_overall_se.append(res.se) + edid_overall_ci.append(res.conf_int) if also_cs: cs = CallawaySantAnna(control_group="never_treated") diff --git a/tests/test_estimators_vcov_type.py b/tests/test_estimators_vcov_type.py index 8c50da2fe..ff752369d 100644 --- a/tests/test_estimators_vcov_type.py +++ b/tests/test_estimators_vcov_type.py @@ -2208,7 +2208,7 @@ def test_sunab_hc1_autoclusters_so_gate_skips(self): r_cl = SunAbraham(vcov_type="hc1", cluster="unit").fit( df, outcome="y", first_treat="first_treat", time="time", unit="unit" ) - np.testing.assert_allclose(r_def.overall_se, r_cl.overall_se, rtol=1e-12) + np.testing.assert_allclose(r_def.se, r_cl.se, rtol=1e-12) def test_absorbed_fe_vcov_scale_fail_closed(self): """The rescale helper is fail-closed on non-positive full-K residual dof.""" diff --git a/tests/test_imputation.py b/tests/test_imputation.py index 60e8398ef..4ccbbcbca 100644 --- a/tests/test_imputation.py +++ b/tests/test_imputation.py @@ -99,8 +99,8 @@ def test_basic_fit(self): assert est.is_fitted_ assert isinstance(results, ImputationDiDResults) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 assert results.n_treated_obs > 0 assert results.n_untreated_obs > 0 assert results.n_treated_units > 0 @@ -121,9 +121,9 @@ def test_positive_treatment_effect(self): first_treat="first_treat", ) - assert results.overall_att > 0 + assert results.att > 0 # Effect should be close to 3.0 (dynamic effects add some) - assert abs(results.overall_att - 3.0) < 2 * results.overall_se + 1.5 + assert abs(results.att - 3.0) < 2 * results.se + 1.5 def test_zero_treatment_effect(self): """Test with no treatment effect.""" @@ -138,7 +138,7 @@ def test_zero_treatment_effect(self): first_treat="first_treat", ) - assert abs(results.overall_att) < 3 * results.overall_se + 0.5 + assert abs(results.att) < 3 * results.se + 0.5 def test_aggregate_simple(self): """Test that default aggregate computes overall ATT.""" @@ -152,8 +152,8 @@ def test_aggregate_simple(self): first_treat="first_treat", ) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 assert results.event_study_effects is None assert results.group_effects is None @@ -240,8 +240,8 @@ def test_covariates(self): covariates=["x1", "x2"], ) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 def test_anticipation(self): """Test anticipation parameter.""" @@ -511,8 +511,8 @@ def test_unbalanced_panel(self): first_treat="first_treat", ) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 def test_balance_e_checks_pre_treatment_periods(self): """balance_e should drop cohorts missing pre-treatment observations.""" @@ -708,7 +708,7 @@ def test_se_positive(self): first_treat="first_treat", ) - assert results.overall_se > 0 + assert results.se > 0 def test_se_positive_event_study(self): """Test that event study SEs are positive.""" @@ -737,7 +737,7 @@ def test_aux_partition_cohort_horizon(self): time="time", first_treat="first_treat", ) - assert results.overall_se > 0 + assert results.se > 0 def test_aux_partition_cohort(self): """Test cohort partition produces valid SEs.""" @@ -750,7 +750,7 @@ def test_aux_partition_cohort(self): time="time", first_treat="first_treat", ) - assert results.overall_se > 0 + assert results.se > 0 def test_aux_partition_horizon(self): """Test horizon partition produces valid SEs.""" @@ -763,7 +763,7 @@ def test_aux_partition_horizon(self): time="time", first_treat="first_treat", ) - assert results.overall_se > 0 + assert results.se > 0 def test_cohort_partition_coincides_on_balanced_uniform_panel(self): """On a BALANCED panel with uniform weights the cohort partition is an @@ -783,9 +783,7 @@ def test_cohort_partition_coincides_on_balanced_uniform_panel(self): results_coarse = ImputationDiD(aux_partition="cohort").fit(data, **kwargs) # rtol=0: numpy's default rtol=1e-7 would mask the identity pin. - np.testing.assert_allclose( - results_coarse.overall_se, results_fine.overall_se, rtol=0, atol=1e-12 - ) + np.testing.assert_allclose(results_coarse.se, results_fine.se, rtol=0, atol=1e-12) def test_coarser_partition_diverges_on_unbalanced_panel(self): """Companion to the identity test: on an unbalanced subsample the coarse @@ -804,9 +802,9 @@ def test_coarser_partition_diverges_on_unbalanced_panel(self): assert len(sub) < len(data) kwargs = dict(outcome="outcome", unit="unit", time="time", first_treat="first_treat") - se_fine = ImputationDiD(aux_partition="cohort_horizon").fit(sub, **kwargs).overall_se - se_cohort = ImputationDiD(aux_partition="cohort").fit(sub, **kwargs).overall_se - se_horizon = ImputationDiD(aux_partition="horizon").fit(sub, **kwargs).overall_se + se_fine = ImputationDiD(aux_partition="cohort_horizon").fit(sub, **kwargs).se + se_cohort = ImputationDiD(aux_partition="cohort").fit(sub, **kwargs).se + se_horizon = ImputationDiD(aux_partition="horizon").fit(sub, **kwargs).se assert se_cohort > se_fine * 1.01 assert se_horizon > se_fine * 1.002 @@ -832,8 +830,8 @@ def test_sparse_solver_matches_dense(self): covariates=["x1"], ) - assert np.isfinite(results.overall_se) - assert results.overall_se > 0 + assert np.isfinite(results.se) + assert results.se > 0 def test_sparse_solver_lsmr_fallback(self): """Test that the LSMR fallback produces finite SE when the sparse @@ -860,8 +858,8 @@ def test_sparse_solver_lsmr_fallback(self): covariates=["x1"], ) - assert np.isfinite(results.overall_se) - assert results.overall_se > 0 + assert np.isfinite(results.se) + assert results.se > 0 def test_sparse_solver_lsmr_fallback_emits_warning(self): """Silent-failure audit axis C: the sparse-factorization -> LSMR @@ -929,7 +927,7 @@ def test_bootstrap_reproducibility(self, ci_params): est2 = ImputationDiD(n_bootstrap=n_boot, seed=42) r2 = est2.fit(data, outcome="outcome", unit="unit", time="time", first_treat="first_treat") - assert r1.overall_se == r2.overall_se + assert r1.se == r2.se def test_bootstrap_different_seeds(self, ci_params): """Test that different seeds give different results.""" @@ -943,7 +941,7 @@ def test_bootstrap_different_seeds(self, ci_params): r2 = est2.fit(data, outcome="outcome", unit="unit", time="time", first_treat="first_treat") # Results should differ (at least slightly) - assert r1.overall_se != r2.overall_se + assert r1.se != r2.se def test_bootstrap_event_study(self, ci_params): """A bootstrapped fit materializes the percentile event-study surface @@ -1040,7 +1038,7 @@ def test_bootstrap_p_value_significance(self, ci_params): ) # Strong effect should be significant - assert results.overall_p_value < 0.05 + assert results.p_value < 0.05 def test_bootstrap_zero_noise_near_zero_se(self, ci_params): """Bootstrap SE ~ 0 when influence function is zero (constant effect, no noise).""" @@ -1063,7 +1061,7 @@ def test_bootstrap_zero_noise_near_zero_se(self, ci_params): data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert abs(results.overall_att - true_effect) < 1e-8 + assert abs(results.att - true_effect) < 1e-8 assert results.bootstrap_results is not None # With zero noise, influence function sums are ~0, so SE should be ~0 assert results.bootstrap_results.overall_att_se < 0.01 @@ -1225,8 +1223,8 @@ def test_similar_point_estimates_vs_cs(self): ) # Point estimates should be reasonably close - cs_att = cs_results.overall_att - imp_att = imp_results.overall_att + cs_att = cs_results.att + imp_att = imp_results.att assert abs(imp_att - cs_att) < 1.0 def test_similar_point_estimates_vs_sa(self): @@ -1254,7 +1252,7 @@ def test_similar_point_estimates_vs_sa(self): ) # Point estimates should be reasonably close - assert abs(imp_results.overall_att - sa_results.overall_att) < 1.0 + assert abs(imp_results.att - sa_results.att) < 1.0 def test_shorter_cis_under_homogeneous_effects(self): """Monte Carlo: imputation CIs shorter than CS and SA under homogeneous effects. @@ -1300,9 +1298,9 @@ def test_shorter_cis_under_homogeneous_effects(self): first_treat="first_treat", ) - imp_w = imp_r.overall_conf_int[1] - imp_r.overall_conf_int[0] - cs_w = cs_r.overall_conf_int[1] - cs_r.overall_conf_int[0] - sa_w = sa_r.overall_conf_int[1] - sa_r.overall_conf_int[0] + imp_w = imp_r.conf_int[1] - imp_r.conf_int[0] + cs_w = cs_r.conf_int[1] - cs_r.conf_int[0] + sa_w = sa_r.conf_int[1] - sa_r.conf_int[0] imp_vs_cs_ratios.append(imp_w / cs_w) imp_vs_sa_ratios.append(imp_w / sa_w) @@ -1372,8 +1370,8 @@ def test_single_cohort(self): ) assert len(results.groups) == 1 - assert results.overall_se > 0 - assert abs(results.overall_att - 2.0) < 1.0 + assert results.se > 0 + assert abs(results.att - 2.0) < 1.0 def test_no_never_treated(self): """Test with no never-treated units (Proposition 5).""" @@ -1392,8 +1390,8 @@ def test_no_never_treated(self): effects = es_dict(results) # Should still estimate - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 # Proposition 5: long-run horizons should be NaN prop5_nans = [ @@ -1449,7 +1447,7 @@ def test_two_periods(self): first_treat="first_treat", ) - assert abs(results.overall_att - 3.0) < 1.0 + assert abs(results.att - 3.0) < 1.0 def test_rank_deficiency_warn(self): """Test rank_deficient_action='warn' doesn't error.""" @@ -1462,7 +1460,7 @@ def test_rank_deficiency_warn(self): time="time", first_treat="first_treat", ) - assert results.overall_se > 0 + assert results.se > 0 def test_rank_deficiency_error(self): """Test rank_deficient_action='error' works.""" @@ -1476,7 +1474,7 @@ def test_rank_deficiency_error(self): time="time", first_treat="first_treat", ) - assert results.overall_se > 0 + assert results.se > 0 def test_invalid_rank_deficient_action(self): """Test invalid rank_deficient_action raises ValueError.""" @@ -1773,9 +1771,9 @@ def test_overall_se_with_partial_nan_tau_hat(self): # Partial NaN case: SE should be finite (computed from finite-only weights) assert np.isfinite( - results.overall_se + results.se ), f"overall_se should be finite with {n_finite} finite and {n_nan} NaN tau_hat" - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) def test_covariate_delta_matches_full_dummy_ols(self): """Step-A/B covariate coefficients match explicit unit+time dummy OLS. @@ -1923,7 +1921,7 @@ def test_non_constant_first_treat_warning(self): # Behavioral assertion: estimator still produces results (warns, doesn't crash) assert isinstance(results, ImputationDiDResults) - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) # Behavioral assertion: coercion applied — first_treat is now constant per unit fit_df = est._fit_data["df"] @@ -1996,8 +1994,8 @@ def test_rank_deficient_covariates_excluded_from_variance(self): ) # SE should be finite (not blown up by singular design matrix) - assert np.isfinite(results.overall_se), "SE should be finite with rank-deficient covariates" - assert results.overall_se > 0 + assert np.isfinite(results.se), "SE should be finite with rank-deficient covariates" + assert results.se > 0 # Verify kept_cov_mask is stored and has one True + one False mask = est._fit_data["kept_cov_mask"] @@ -2033,7 +2031,7 @@ def failing_precompute(*args, **kwargs): # Behavioral assertion: bootstrap_results is None assert results.bootstrap_results is None # Analytical SE still present - assert results.overall_se > 0 + assert results.se > 0 def test_event_study_empty_after_filtering(self): """Warn when balance_e/horizon_max filter out all treated horizons.""" @@ -2364,8 +2362,8 @@ def test_default_hc1_bit_equal_baseline(self): ) r_default = ImputationDiD().fit(**common) r_explicit = ImputationDiD(vcov_type="hc1").fit(**common) - assert r_default.overall_att == r_explicit.overall_att - assert r_default.overall_se == r_explicit.overall_se + assert r_default.att == r_explicit.att + assert r_default.se == r_explicit.se self._assert_levels_bit_equal(r_default, r_explicit) # ---- Surface 2: cluster path bit-equal -------------------------------- @@ -2381,8 +2379,8 @@ def test_cluster_hc1_bit_equal_baseline(self): ) r_default = ImputationDiD(cluster="state").fit(**common) r_explicit = ImputationDiD(cluster="state", vcov_type="hc1").fit(**common) - assert r_default.overall_att == r_explicit.overall_att - assert r_default.overall_se == r_explicit.overall_se + assert r_default.att == r_explicit.att + assert r_default.se == r_explicit.se self._assert_levels_bit_equal(r_default, r_explicit) # ---- Surface 3: TSL-survey path bit-equal ----------------------------- @@ -2405,8 +2403,8 @@ def test_survey_tsl_hc1_bit_equal_baseline(self): ) r_default = ImputationDiD().fit(**common) r_explicit = ImputationDiD(vcov_type="hc1").fit(**common) - assert r_default.overall_att == r_explicit.overall_att - assert r_default.overall_se == r_explicit.overall_se + assert r_default.att == r_explicit.att + assert r_default.se == r_explicit.se self._assert_levels_bit_equal(r_default, r_explicit) # ---- Surface 4: replicate-survey path bit-equal ----------------------- @@ -2430,8 +2428,8 @@ def test_survey_replicate_hc1_bit_equal_baseline(self, level): ) r_default = ImputationDiD().fit(**common) r_explicit = ImputationDiD(vcov_type="hc1").fit(**common) - assert r_default.overall_att == r_explicit.overall_att - assert r_default.overall_se == r_explicit.overall_se + assert r_default.att == r_explicit.att + assert r_default.se == r_explicit.se # Per-horizon / per-group SE override branches must also agree under # the replicate-weight variance path (post-fit replicate replay). a_default = r_default.aggregate(level) @@ -2822,7 +2820,7 @@ def test_fit_clone_idempotent_on_vcov_type(self): time="time", first_treat="first_treat", ) - assert r1.overall_se == r2.overall_se + assert r1.se == r2.se assert r1.vcov_type == r2.vcov_type @@ -2874,8 +2872,8 @@ def test_replicate_covariates_zero_weight_psus_finite_se(self): messages = [str(w.message) for w in rec] assert not any("replicate refits failed" in m for m in messages) assert not any("did not converge" in m for m in messages) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) and r.overall_se > 0 + assert np.isfinite(r.att) + assert np.isfinite(r.se) and r.se > 0 def test_main_fit_zero_weight_treated_unit_covariates(self): """Main fit with a zero-weight treated unit + covariates. @@ -2927,7 +2925,7 @@ def test_main_fit_zero_weight_treated_unit_covariates(self): groups = group_dict(r) from tests.conftest import assert_nan_inference - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) # Cohort 2 contains only the zero-weight unit: NaN across ALL fields. assert_nan_inference(groups[2]) assert np.isnan(groups[2]["effect"]) @@ -3101,7 +3099,7 @@ def _spy(mat, rhs): data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) assert calls, "fallback path did not route through the LSMR solver" - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) assert any("sparse LSMR" in str(x.message) for x in w) def test_unconverged_lsmr_fails_closed_to_nan(self, monkeypatch): @@ -3142,11 +3140,11 @@ def _fake_lsmr(A, b, **kwargs): res = ImputationDiD().fit( data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert np.isfinite(res.overall_att) # point estimate unaffected - assert np.isnan(res.overall_se) - assert np.isnan(res.overall_t_stat) - assert np.isnan(res.overall_p_value) - assert np.all(np.isnan(np.asarray(res.overall_conf_int, dtype=float))) + assert np.isfinite(res.att) # point estimate unaffected + assert np.isnan(res.se) + assert np.isnan(res.t_stat) + assert np.isnan(res.p_value) + assert np.all(np.isnan(np.asarray(res.conf_int, dtype=float))) def test_machine_precision_istop_accepted(self, monkeypatch): """CI-review P1 regression: istop 4/5 (machine-precision analogues of diff --git a/tests/test_imputation_loo_stata_parity.py b/tests/test_imputation_loo_stata_parity.py index 3026a2558..39fa8cf12 100644 --- a/tests/test_imputation_loo_stata_parity.py +++ b/tests/test_imputation_loo_stata_parity.py @@ -130,7 +130,7 @@ def r_golden() -> dict: def library_fit() -> dict: """Fit the LOO and non-LOO paths once each; return per-horizon + overall + warnings. - The fit stores scalar ``res.overall_att`` / ``res.overall_se``; the per-horizon + The fit stores scalar ``res.att`` / ``res.se``; the per-horizon surface is the post-fit ``res.aggregate("event_study")`` container, read here through the legacy dict shape (``es_dict``: ``{h: {"effect", "se", ...}}``). The LOO warnings of both stages (fit + event-study recompute) are recorded. @@ -161,9 +161,9 @@ def by_h(es): return {h: es[h] for h in HORIZONS} return { - "loo_overall": (float(loo.overall_att), float(loo.overall_se)), + "loo_overall": (float(loo.att), float(loo.se)), "loo_es": {h: (float(v["effect"]), float(v["se"])) for h, v in by_h(loo_es).items()}, - "nonloo_overall_se": float(nonloo.overall_se), + "nonloo_overall_se": float(nonloo.se), "nonloo_es_se": {h: float(v["se"]) for h, v in by_h(nonloo_es).items()}, "loo_warnings": loo_msgs, } @@ -212,8 +212,8 @@ def library_fit_variants() -> dict: out["loo_warnings"] += msgs mode = "loo" if loo else "nonloo" out[f"{part}_{mode}"] = { - "att": float(res.overall_att), - "se": float(res.overall_se), + "att": float(res.att), + "se": float(res.se), "es": {h: (float(es[h]["effect"]), float(es[h]["se"])) for h in HORIZONS}, } return out @@ -243,7 +243,7 @@ def library_fit_unbalanced() -> dict: if loo: out["loo_warnings"] += [str(w.message) for w in caught] mode = "loo" if loo else "nonloo" - out[f"{part}_{mode}"] = (float(res.overall_att), float(res.overall_se)) + out[f"{part}_{mode}"] = (float(res.att), float(res.se)) return out diff --git a/tests/test_linalg.py b/tests/test_linalg.py index ec129e803..429bc57d6 100644 --- a/tests/test_linalg.py +++ b/tests/test_linalg.py @@ -1956,8 +1956,8 @@ def test_sun_abraham_estimator_produces_valid_results(self): result = sa.fit(data, outcome="y", unit="unit", time="time", first_treat="first_treat") # Should produce valid results - assert result.overall_se > 0 - assert np.isfinite(result.overall_att) + assert result.se > 0 + assert np.isfinite(result.att) assert len(result.event_study_effects) > 0 diff --git a/tests/test_methodology_callaway.py b/tests/test_methodology_callaway.py index 864bfdc7c..58a44d4c9 100644 --- a/tests/test_methodology_callaway.py +++ b/tests/test_methodology_callaway.py @@ -301,9 +301,7 @@ def test_doubly_robust_recovers_true_effect(self): # Should recover approximately 2.5 treatment effect # Allow wider tolerance due to dynamic effects and noise - assert ( - abs(results.overall_att - 2.5) < 1.0 - ), f"DR should recover ~2.5 effect, got {results.overall_att}" + assert abs(results.att - 2.5) < 1.0, f"DR should recover ~2.5 effect, got {results.att}" def test_estimation_methods_produce_similar_results(self): """ @@ -324,7 +322,7 @@ def test_estimation_methods_produce_similar_results(self): ) # All methods should produce similar overall ATT - atts = [results[m].overall_att for m in ["reg", "ipw", "dr"]] + atts = [results[m].att for m in ["reg", "ipw", "dr"]] max_diff = max(atts) - min(atts) assert ( max_diff < 0.5 @@ -409,8 +407,8 @@ def test_dr_no_cov_aggregated_se_matches_reg(self): reg = self._fit(data, "reg") dr = self._fit(data, "dr") - np.testing.assert_allclose(dr.overall_att, reg.overall_att, rtol=0, atol=1e-12) - np.testing.assert_allclose(dr.overall_se, reg.overall_se, rtol=0, atol=1e-12) + np.testing.assert_allclose(dr.att, reg.att, rtol=0, atol=1e-12) + np.testing.assert_allclose(dr.se, reg.se, rtol=0, atol=1e-12) reg_es_all = es_dict(reg) dr_es_all = es_dict(dr) @@ -452,8 +450,8 @@ def test_ipw_no_cov_per_cell_identical_to_reg(self): assert compared >= 3, f"expected several finite cells to compare, got {compared}" # Aggregated surfaces inherit the identity (same IF feeds aggregation). - np.testing.assert_allclose(ipw.overall_att, reg.overall_att, rtol=0, atol=1e-12) - np.testing.assert_allclose(ipw.overall_se, reg.overall_se, rtol=0, atol=1e-12) + np.testing.assert_allclose(ipw.att, reg.att, rtol=0, atol=1e-12) + np.testing.assert_allclose(ipw.se, reg.se, rtol=0, atol=1e-12) # ============================================================================= @@ -590,8 +588,8 @@ def test_overall_att_matches_r_dr(self, require_r, benchmark_data): # Compare overall ATT - use 20% tolerance for aggregation differences # The discrepancy is primarily in aggregation weights, not ATT(g,t) values assert np.isclose( - py_results.overall_att, r_results["overall_att"], rtol=0.20 - ), f"ATT mismatch: Python={py_results.overall_att}, R={r_results['overall_att']}" + py_results.att, r_results["overall_att"], rtol=0.20 + ), f"ATT mismatch: Python={py_results.att}, R={r_results['overall_att']}" def test_overall_att_matches_r_reg(self, require_r, benchmark_data): """Test overall ATT matches R with outcome regression. @@ -609,8 +607,8 @@ def test_overall_att_matches_r_reg(self, require_r, benchmark_data): r_results = self._run_r_estimation(csv_path, estimation_method="reg") assert np.isclose( - py_results.overall_att, r_results["overall_att"], rtol=0.20 - ), f"ATT mismatch: Python={py_results.overall_att}, R={r_results['overall_att']}" + py_results.att, r_results["overall_att"], rtol=0.20 + ), f"ATT mismatch: Python={py_results.att}, R={r_results['overall_att']}" def test_group_time_effects_match_r(self, require_r, benchmark_data): """Test individual ATT(g,t) values match R for post-treatment periods. @@ -687,8 +685,8 @@ def test_single_obs_group_produces_valid_result(self): ) # Should produce valid results - assert results.overall_att is not None - assert np.isfinite(results.overall_att) + assert results.att is not None + assert np.isfinite(results.att) def test_no_post_treatment_returns_nan_with_warning(self): """ @@ -734,10 +732,10 @@ def test_no_post_treatment_returns_nan_with_warning(self): ), f"Expected post-treatment warning, got: {warning_messages}" # All overall inference fields should be NaN - assert np.isnan(results.overall_att), "overall_att should be NaN" - assert np.isnan(results.overall_se), "overall_se should be NaN" - assert np.isnan(results.overall_t_stat), "overall_t_stat should be NaN" - assert np.isnan(results.overall_p_value), "overall_p_value should be NaN" + assert np.isnan(results.att), "overall_att should be NaN" + assert np.isnan(results.se), "overall_se should be NaN" + assert np.isnan(results.t_stat), "overall_t_stat should be NaN" + assert np.isnan(results.p_value), "overall_p_value should be NaN" def test_nonfinite_se_produces_nan_tstat(self): """ @@ -788,7 +786,7 @@ def test_anticipation_shifts_reference_period(self): # With anticipation=1, period 4 (= g-1 = 5-1) should be post-treatment # Check that ATT(5, 4) is included in overall aggregation # (This is implicit in the overall_att calculation including t=4) - assert results.overall_att is not None + assert results.att is not None def test_not_yet_treated_excludes_cohort_g(self): """ @@ -813,8 +811,8 @@ def test_not_yet_treated_excludes_cohort_g(self): ) # Both should produce valid results - assert results_nyt.overall_att is not None - assert results_nt.overall_att is not None + assert results_nyt.att is not None + assert results_nt.att is not None # Control group setting should be recorded assert results_nyt.control_group == "not_yet_treated" @@ -933,14 +931,12 @@ def test_analytical_se_close_to_bootstrap_se(self, ci_params): # Check overall ATT SE (wider tolerance when min_n cap reduces # bootstrap iterations in pure Python mode) - if results_boot.overall_se > 0: - rel_diff = ( - abs(results_anal.overall_se - results_boot.overall_se) / results_boot.overall_se - ) + if results_boot.se > 0: + rel_diff = abs(results_anal.se - results_boot.se) / results_boot.se threshold = 0.40 if n_boot < 100 else 0.25 assert rel_diff < threshold, ( - f"Analytical SE ({results_anal.overall_se}) differs from bootstrap SE " - f"({results_boot.overall_se}) by {rel_diff*100:.1f}%" + f"Analytical SE ({results_anal.se}) differs from bootstrap SE " + f"({results_boot.se}) by {rel_diff*100:.1f}%" ) def test_bootstrap_weight_moments_rademacher(self): @@ -1017,9 +1013,9 @@ def test_bootstrap_produces_valid_inference(self, ci_params): assert results.bootstrap_results is not None # Overall statistics should be valid - assert np.isfinite(results.overall_se) - assert 0 <= results.overall_p_value <= 1 - assert results.overall_conf_int[0] < results.overall_conf_int[1] + assert np.isfinite(results.se) + assert 0 <= results.p_value <= 1 + assert results.conf_int[0] < results.conf_int[1] # Group-time p-values should be in [0, 1] for effect in results.group_time_effects.values(): @@ -1045,8 +1041,8 @@ def test_simple_aggregation_weights_by_group_size(self): ) # Overall ATT should be weighted average of post-treatment effects - assert results.overall_att is not None - assert np.isfinite(results.overall_att) + assert results.att is not None + assert np.isfinite(results.att) def test_event_study_aggregation_by_relative_time(self): """ @@ -1188,7 +1184,7 @@ def test_results_summary_contains_key_info(self): assert "Callaway-Sant'Anna" in summary assert "ATT" in summary - assert str(round(results.overall_att, 3))[:4] in summary or "ATT" in summary + assert str(round(results.att, 3))[:4] in summary or "ATT" in summary def test_results_to_dataframe_group_time(self): """Test to_dataframe with level='group_time'.""" @@ -1362,8 +1358,8 @@ def test_overall_att_bootstrap_uses_combined_if(self, ci_params): data, outcome="outcome", unit="unit", time="period", first_treat="first_treat" ) - se_a = results_a.overall_se - se_b = results_b.overall_se + se_a = results_a.se + se_b = results_b.se threshold = 0.40 if n_boot < 100 else 0.20 if np.isfinite(se_a) and se_a > 0 and np.isfinite(se_b) and se_b > 0: @@ -1793,11 +1789,9 @@ def test_mpdta_overall_att_matches_r_strict(self, require_r, tmp_path): ) # Compare overall ATT - strict 1% tolerance for MPDTA - rel_diff = abs(py_results.overall_att - r_results["overall_att"]) / abs( - r_results["overall_att"] - ) + rel_diff = abs(py_results.att - r_results["overall_att"]) / abs(r_results["overall_att"]) assert rel_diff < 0.01, ( - f"MPDTA ATT mismatch: Python={py_results.overall_att:.6f}, " + f"MPDTA ATT mismatch: Python={py_results.att:.6f}, " f"R={r_results['overall_att']:.6f}, diff={rel_diff*100:.2f}%" ) @@ -1814,9 +1808,9 @@ def test_mpdta_overall_se_matches_r_strict(self, require_r, tmp_path): ) # Compare overall SE - strict 1% tolerance for MPDTA - rel_diff = abs(py_results.overall_se - r_results["overall_se"]) / r_results["overall_se"] + rel_diff = abs(py_results.se - r_results["overall_se"]) / r_results["overall_se"] assert rel_diff < 0.01, ( - f"MPDTA SE mismatch: Python={py_results.overall_se:.6f}, " + f"MPDTA SE mismatch: Python={py_results.se:.6f}, " f"R={r_results['overall_se']:.6f}, diff={rel_diff*100:.2f}%" ) @@ -2030,8 +2024,8 @@ def test_or_fit_offset_invariant(self): # New (equilibrated SVD) is invariant to ~1e-11; the prior normal- # equations path drifted ~3.8e-6 at this offset, so 1e-7 cleanly # separates the two. - assert base.overall_att == pytest.approx(shifted.overall_att, abs=1e-7) - assert base.overall_se == pytest.approx(shifted.overall_se, abs=1e-6) + assert base.att == pytest.approx(shifted.att, abs=1e-7) + assert base.se == pytest.approx(shifted.se, abs=1e-6) for gt, eff in base.group_time_effects.items(): if np.isfinite(eff["effect"]) and gt in shifted.group_time_effects: assert eff["effect"] == pytest.approx( @@ -2230,7 +2224,7 @@ def test_reg_if_crosscheck(self): cell = res.group_time_effects[(2, 2)] assert cell["effect"] == pytest.approx(att, rel=1e-9) assert cell["se"] == pytest.approx(se, rel=1e-9) - assert res.overall_se == pytest.approx(se, rel=1e-9) + assert res.se == pytest.approx(se, rel=1e-9) def test_reg_if_crosscheck_not_yet_treated(self): """not_yet_treated forces the per-pair (non-hoisted) bread branch of @@ -2272,7 +2266,7 @@ def test_ipw_if_crosscheck(self): cell = res.group_time_effects[(2, 2)] assert cell["effect"] == pytest.approx(att, rel=1e-8) assert cell["se"] == pytest.approx(se, rel=1e-8) - assert res.overall_se == pytest.approx(se, rel=1e-8) + assert res.se == pytest.approx(se, rel=1e-8) def test_ipw_if_crosscheck_survey(self): """Survey-weighted ipw+cov: the survey branch already carried the PS diff --git a/tests/test_methodology_chaisemartin_dhaultfoeuille.py b/tests/test_methodology_chaisemartin_dhaultfoeuille.py index 4f8db2006..9f18aaf97 100644 --- a/tests/test_methodology_chaisemartin_dhaultfoeuille.py +++ b/tests/test_methodology_chaisemartin_dhaultfoeuille.py @@ -68,7 +68,7 @@ def test_hand_calculable_4group_3period_joiners_and_leavers(self, panel): treatment="treatment", ) # Exact integer/half-integer arithmetic from the plan's worked example - assert results.overall_att == 2.5 + assert results.att == 2.5 assert results.joiners_att == 2.0 assert results.leavers_att == 3.0 @@ -103,14 +103,14 @@ def test_worked_example_se_is_unidentified_with_warning(self, panel): treatment="treatment", ) # Point estimate is still exact - assert results.overall_att == 2.5 + assert results.att == 2.5 # SE is NaN, not 0.0, on the degenerate panel - assert np.isnan(results.overall_se) + assert np.isnan(results.se) # NaN propagates through inference fields - assert np.isnan(results.overall_t_stat) - assert np.isnan(results.overall_p_value) - assert np.isnan(results.overall_conf_int[0]) - assert np.isnan(results.overall_conf_int[1]) + assert np.isnan(results.t_stat) + assert np.isnan(results.p_value) + assert np.isnan(results.conf_int[0]) + assert np.isnan(results.conf_int[1]) # The degenerate-cohort warning fired assert any( "variance is unidentified" in str(wi.message) for wi in w @@ -200,7 +200,7 @@ def test_placebo_zero_under_constant_trends(self): ) # Under constant trends with no treatment effect, both DID_M # and the placebo should be exactly zero. - assert results.overall_att == 0.0 + assert results.att == 0.0 if results.placebo_available: assert results.placebo_effect == 0.0 @@ -302,7 +302,7 @@ def test_cohort_recentering_not_grand_mean(self): time="period", treatment="treatment", ) - cohort_se = results.overall_se + cohort_se = results.se assert np.isfinite(cohort_se) and cohort_se > 0 # Reach into the IF helpers and compute the GRAND-MEAN version on @@ -405,10 +405,10 @@ def test_iid_data_finite_variance(self): time="period", treatment="treatment", ) - assert np.isfinite(results.overall_se) - assert results.overall_se > 0 - assert np.isfinite(results.overall_t_stat) - assert np.isfinite(results.overall_p_value) + assert np.isfinite(results.se) + assert results.se > 0 + assert np.isfinite(results.t_stat) + assert np.isfinite(results.p_value) # ============================================================================= @@ -541,7 +541,7 @@ def test_recovery_single_switch_n200(self): treatment="treatment", ) # With n=200 and homogeneous effect=2.5, the CI should bracket truth - lo, hi = results.overall_conf_int + lo, hi = results.conf_int assert lo <= 2.5 <= hi @pytest.mark.slow @@ -566,7 +566,7 @@ def test_recovery_joiners_only_n200(self): # Use a point-estimate proximity assertion rather than CI # coverage, which is stochastic and can fail on specific seeds # or architectures (the arm64 CI runner hit this with seed 43). - assert abs(results.overall_att - 1.5) < 0.5, ( - f"Large-N recovery failed: overall_att={results.overall_att:.4f}, " + assert abs(results.att - 1.5) < 0.5, ( + f"Large-N recovery failed: overall_att={results.att:.4f}, " f"expected ~1.5 (tolerance 0.5)" ) diff --git a/tests/test_methodology_continuous_did.py b/tests/test_methodology_continuous_did.py index c0f373b3c..03477147b 100644 --- a/tests/test_methodology_continuous_did.py +++ b/tests/test_methodology_continuous_did.py @@ -69,7 +69,7 @@ def test_att_glob_binarized(self, linear_data): est = ContinuousDiD(degree=1, num_knots=0) results = est.fit(linear_data, "outcome", "unit", "period", "first_treat", "dose") - np.testing.assert_allclose(results.overall_att, expected_att_glob, atol=1e-10) + np.testing.assert_allclose(results.att, expected_att_glob, atol=1e-10) def test_acrt_glob_plugin(self, linear_data): """ACRT_glob = mean(ACRT(D_i)) over treated = 2.""" @@ -164,13 +164,13 @@ def test_all_same_dose(self): with pytest.warns(UserWarning, match="[Ii]dentical"): results = est.fit(data, "outcome", "unit", "period", "first_treat", "dose") # ATT_glob should be 5.0 - np.testing.assert_allclose(results.overall_att, 5.0, atol=1e-10) + np.testing.assert_allclose(results.att, 5.0, atol=1e-10) # Dose-response: ATT(d) should be constant = overall_att everywhere. # With all-same dose, only the intercept is identified, which equals # mean(delta_tilde_Y) = att_glob — same quantity by both paths. np.testing.assert_allclose( results.dose_response_att.effects, - results.overall_att, + results.att, atol=1e-10, ) # ACRT(d) should be zero everywhere (no dose variation → zero derivative) @@ -457,7 +457,7 @@ def _compare_with_r( # Compare overall ATT r_overall_att = r_out["overall_att"] - py_overall_att = results.overall_att + py_overall_att = results.att overall_att_diff = abs(py_overall_att - r_overall_att) / (abs(r_overall_att) + 1e-10) assert overall_att_diff < att_tol, ( f"Overall ATT diff: {overall_att_diff:.4f} " @@ -607,12 +607,10 @@ def test_benchmark_4_staggered_dose(self): ) # Overall ATT - att_diff = abs(results.overall_att - r_out["overall_att"]) / ( - abs(r_out["overall_att"]) + 1e-10 - ) + att_diff = abs(results.att - r_out["overall_att"]) / (abs(r_out["overall_att"]) + 1e-10) assert att_diff < 0.01, ( f"Overall ATT diff: {att_diff:.4f} " - f"(R={r_out['overall_att']:.6f}, Py={results.overall_att:.6f})" + f"(R={r_out['overall_att']:.6f}, Py={results.att:.6f})" ) # Overall ACRT @@ -703,12 +701,10 @@ def test_benchmark_5_not_yet_treated(self): "dose", ) - att_diff = abs(results.overall_att - r_out["overall_att"]) / ( - abs(r_out["overall_att"]) + 1e-10 - ) + att_diff = abs(results.att - r_out["overall_att"]) / (abs(r_out["overall_att"]) + 1e-10) assert att_diff < 0.01, ( f"Overall ATT diff: {att_diff:.4f} " - f"(R={r_out['overall_att']:.6f}, Py={results.overall_att:.6f})" + f"(R={r_out['overall_att']:.6f}, Py={results.att:.6f})" ) acrt_diff = abs(results.overall_acrt - r_out["overall_acrt"]) / ( @@ -797,12 +793,10 @@ def test_benchmark_6_event_study(self): results.aggregate("event_study") # Compare overall ATT (binarized) - att_diff = abs(results.overall_att - r_out["overall_att"]) / ( - abs(r_out["overall_att"]) + 1e-10 - ) + att_diff = abs(results.att - r_out["overall_att"]) / (abs(r_out["overall_att"]) + 1e-10) assert att_diff < 0.01, ( f"Overall ATT diff: {att_diff:.4f} " - f"(R={r_out['overall_att']:.6f}, Py={results.overall_att:.6f})" + f"(R={r_out['overall_att']:.6f}, Py={results.att:.6f})" ) finally: os.unlink(tmp_path) @@ -979,8 +973,8 @@ def test_reg_matches_drdid(self): pytest.skip(f"R DRDID failed: {r.stderr[:300]}") ref = json.loads(r.stdout) res = _fit_cov(df, "reg") - assert abs(float(res.overall_att) - ref["att"]) < 1e-8 - assert abs(float(res.overall_att_se) - ref["se"]) < 1e-8 + assert abs(float(res.att) - ref["att"]) < 1e-8 + assert abs(float(res.se) - ref["se"]) < 1e-8 @require_drdid def test_dr_matches_drdid(self): @@ -1010,8 +1004,8 @@ def test_dr_matches_drdid(self): ref = json.loads(r.stdout) res = _fit_cov(df, "dr") # dr att/se match DRDID (~1e-6; Python IRLS vs fastglm method=3) - assert abs(float(res.overall_att) - ref["att"]) < 1e-5 - assert abs(float(res.overall_att_se) - ref["se"]) < 1e-5 + assert abs(float(res.att) - ref["att"]) < 1e-5 + assert abs(float(res.se) - ref["se"]) < 1e-5 def test_dr_reg_numpy_crosscheck_p2(self): """R-free: estimator reg/dr att+se match an independent NumPy @@ -1022,10 +1016,10 @@ def test_dr_reg_numpy_crosscheck_p2(self): ref = _numpy_reg_dr(dY, D, X) reg = _fit_cov(df, "reg") dr = _fit_cov(df, "dr") - assert abs(float(reg.overall_att) - ref["reg_att"]) < 1e-9 - assert abs(float(reg.overall_att_se) - ref["reg_se"]) < 1e-9 - assert abs(float(dr.overall_att) - ref["dr_att"]) < 1e-7 - assert abs(float(dr.overall_att_se) - ref["dr_se"]) < 1e-7 + assert abs(float(reg.att) - ref["reg_att"]) < 1e-9 + assert abs(float(reg.se) - ref["reg_se"]) < 1e-9 + assert abs(float(dr.att) - ref["dr_att"]) < 1e-7 + assert abs(float(dr.se) - ref["dr_se"]) < 1e-7 def test_reg_vs_dr_acrt_identical(self): """reg and dr share the dose-response shape: ACRT(d) point AND SE are @@ -1040,7 +1034,7 @@ def test_reg_vs_dr_acrt_identical(self): np.testing.assert_allclose(reg.dose_response_acrt.se, dr.dose_response_acrt.se, atol=1e-10) att_diff = reg.dose_response_att.effects - dr.dose_response_att.effects assert np.ptp(att_diff) < 1e-9 # constant shift across the grid - assert abs(float(reg.overall_att) - float(dr.overall_att)) > 1e-4 # levels differ + assert abs(float(reg.att) - float(dr.att)) > 1e-4 # levels differ def test_covariate_dgp_recovery(self): """Conditional-PT DGP where unconditional DiD is biased but covariate @@ -1075,10 +1069,10 @@ def test_covariate_dgp_recovery(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") uncond = ContinuousDiD().fit(df, "outcome", "unit", "period", "first_treat", "dose") - assert abs(float(uncond.overall_att) - tau) > 0.5 # unconditional biased + assert abs(float(uncond.att) - tau) > 0.5 # unconditional biased for method in ("reg", "dr"): res = _fit_cov(df, method) - assert abs(float(res.overall_att) - tau) < 0.1 # recovered + assert abs(float(res.att) - tau) < 0.1 # recovered @pytest.mark.slow def test_covariate_coverage(self): @@ -1123,7 +1117,7 @@ def test_covariate_coverage(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") res = _fit_cov(df, method) - lo, hi = res.overall_att_conf_int + lo, hi = res.conf_int cover += int(lo <= tau <= hi) total += 1 rate = cover / total @@ -1274,7 +1268,7 @@ def test_hand_calc_att_acrt_overall(self): assert np.allclose(res.dose_response_att.dose_grid, levels) assert np.allclose(res.dose_response_att.effects, att, atol=1e-12) assert np.allclose(res.dose_response_acrt.effects, acrt, atol=1e-12) - assert np.isclose(res.overall_att, overall, atol=1e-12) + assert np.isclose(res.att, overall, atol=1e-12) def test_hand_calc_analytical_se(self): """Saturated SE reduces exactly to the per-level 2x2 DiD SE (the gate).""" @@ -1308,7 +1302,7 @@ def test_binary_single_dose_acrt_equals_att(self): assert np.isclose( r1.dose_response_acrt.effects[0], r1.dose_response_att.effects[0], atol=1e-12 ) - assert np.isclose(r1.overall_acrt, r1.overall_att, atol=1e-12) + assert np.isclose(r1.overall_acrt, r1.att, atol=1e-12) assert np.isfinite(r1.dose_response_acrt.se[0]) and r1.dose_response_acrt.se[0] > 0 # d_1 = 2.0 -> ACRT = ATT / 2. df2 = _make_discrete_panel({2.0: 3.0}, n_per_level=60, n_control=90, seed=31) @@ -1423,7 +1417,7 @@ def test_hand_calc_att_acrt_overall(self): assert np.allclose(res.dose_response_att.dose_grid, modelled) # d_L excluded assert np.allclose(res.dose_response_att.effects, att, atol=1e-12) assert np.allclose(res.dose_response_acrt.effects, acrt, atol=1e-12) - assert np.isclose(res.overall_att, overall, atol=1e-12) + assert np.isclose(res.att, overall, atol=1e-12) # overall_acrt (plug-in scalar) asserted explicitly, not just the rows. assert np.isclose(res.overall_acrt, overall_acrt, atol=1e-12) diff --git a/tests/test_methodology_dml_did.py b/tests/test_methodology_dml_did.py index 4f10d4468..6e00d7d4b 100644 --- a/tests/test_methodology_dml_did.py +++ b/tests/test_methodology_dml_did.py @@ -261,7 +261,7 @@ def test_coverage_sanity(self, ci_params): with warnings.catch_warnings(): warnings.simplefilter("ignore") res = DMLDiD(seed=0).fit(df, **FIT_KW, covariates=XCOLS) - lo, hi = res.overall_conf_int + lo, hi = res.conf_int hits += int(lo <= theta <= hi) coverage = hits / n_reps assert band[0] <= coverage <= band[1], f"coverage {coverage:.3f} outside {band}" @@ -862,9 +862,9 @@ class TestChangS422Recovery: def test_theta_recovery(self, n, learner, seed, abs_bound): df = chang_s422_kernel_frame(n, seed=seed) res = _fit_s422_expecting_only_a23(DMLDiD(outcome_learner=learner, seed=0, panel=False), df) - err = abs(res.overall_att - CHANG_S422_THETA0) - assert err < 4 * res.overall_se, (res.overall_att, res.overall_se) - assert err < abs_bound, (res.overall_att, res.overall_se) + err = abs(res.att - CHANG_S422_THETA0) + assert err < 4 * res.se, (res.att, res.se) + assert err < abs_bound, (res.att, res.se) confounded_err = abs(_unadjusted_did(df) - CHANG_S422_THETA0) assert err < confounded_err, (err, confounded_err) diff --git a/tests/test_methodology_efficient_did.py b/tests/test_methodology_efficient_did.py index 4454120ef..cb6fd9417 100644 --- a/tests/test_methodology_efficient_did.py +++ b/tests/test_methodology_efficient_did.py @@ -416,7 +416,7 @@ def test_fit_level_twin(self): r_loop = EfficientDiD().fit(df, "y", "unit", "time", "first_treat") finally: ed_mod._omega_star_nocov_gram = orig - assert r_gram.overall_att == pytest.approx(r_loop.overall_att, rel=1e-10) + assert r_gram.att == pytest.approx(r_loop.att, rel=1e-10) for k in r_loop.group_time_effects: np.testing.assert_allclose( ( @@ -1118,8 +1118,8 @@ def test_covariate_dr_path_last_cohort_with_anticipation(self): # anticipation=1, last_g=7 -> effective last cohort at 6 -> time_periods 1..5 assert max(res.time_periods) == 5 assert 7 not in res.groups # last cohort reclassified as pseudo-control - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) @pytest.mark.slow def test_covariate_path_beats_forced_linear_under_nonlinear_nuisance(self, ci_params): @@ -1154,12 +1154,12 @@ def test_covariate_path_beats_forced_linear_under_nonlinear_nuisance(self, ci_pa a_sieve = ( EfficientDiD(pt_assumption="all") .fit(df, "y", "unit", "time", "first_treat", covariates=["x1"]) - .overall_att + .att ) a_linear = ( EfficientDiD(pt_assumption="all", sieve_k_max=1) .fit(df, "y", "unit", "time", "first_treat", covariates=["x1"]) - .overall_att + .att ) if np.isfinite(a_sieve): err_sieve.append(abs(a_sieve - tau)) @@ -1396,16 +1396,14 @@ def fit(df): # single-date fixture is just-identified (H=1 per cell), so # ``compute_per_unit_weights`` short-circuits to 1 and the kernel/bandwidth # path is NOT exercised here — the overidentified test below covers that. - np.testing.assert_allclose( - r_padded.overall_att, r_filtered.overall_att, atol=1e-9, rtol=0.0 - ) + np.testing.assert_allclose(r_padded.att, r_filtered.att, atol=1e-9, rtol=0.0) # The SE is invariant up to a tiny finite-sample DOF correction: the shared # survey sandwich (compute_survey_vcov / _compute_stratified_psu_meat) counts # zero-weight units as PSUs in its n_psu/(n_psu-1)-style correction. The # weighted scores themselves are zero for zero-weight rows, so this is a # second-order (~2e-4 relative) cross-cutting survey-infra effect, NOT the # sieve selection (tracked in TODO.md). - np.testing.assert_allclose(r_padded.overall_se, r_filtered.overall_se, rtol=2e-3, atol=0.0) + np.testing.assert_allclose(r_padded.se, r_filtered.se, rtol=2e-3, atol=0.0) def _overid_panel(self, seed=0): """Staggered panel (cohorts 2 & 3 + never-treated, 4 periods) whose PT-All @@ -1478,8 +1476,6 @@ def fit(df): np.testing.assert_allclose( r_padded.group_time_effects[k]["effect"], info["effect"], atol=1e-9, rtol=0.0 ) - np.testing.assert_allclose( - r_padded.overall_att, r_filtered.overall_att, atol=1e-9, rtol=0.0 - ) + np.testing.assert_allclose(r_padded.att, r_filtered.att, atol=1e-9, rtol=0.0) # SE invariant up to the same second-order survey-vcov DOF correction. - np.testing.assert_allclose(r_padded.overall_se, r_filtered.overall_se, rtol=2e-3, atol=0.0) + np.testing.assert_allclose(r_padded.se, r_filtered.se, rtol=2e-3, atol=0.0) diff --git a/tests/test_methodology_imputation.py b/tests/test_methodology_imputation.py index 15aa3246a..2e21f281e 100644 --- a/tests/test_methodology_imputation.py +++ b/tests/test_methodology_imputation.py @@ -155,7 +155,7 @@ def test_recovers_constant_att(self) -> None: res = ImputationDiD().fit( panel, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert abs(res.overall_att - 2.0) < 0.05 + assert abs(res.att - 2.0) < 0.05 def test_recovers_heterogeneous_event_study(self) -> None: """Horizon-specific effects tau_K = 1 + 0.5*K are recovered per horizon.""" @@ -200,7 +200,7 @@ def test_step1_uses_untreated_only(self) -> None: perturbed, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) # Only the perturbed obs's own tau_hat changes (weight 1/N_1). - assert abs((pert.overall_att - base.overall_att) - delta / n_1) < 1e-6 + assert abs((pert.att - base.att) - delta / n_1) < 1e-6 # ============================================================================= @@ -220,7 +220,7 @@ def test_se_finite_and_positive(self) -> None: res = ImputationDiD().fit( panel, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 def test_event_study_ses_finite(self) -> None: rng = np.random.default_rng(_BASE_SEED_THM3 + 2) @@ -280,7 +280,7 @@ def test_singular_omega0_routes_to_dense_fallback(self) -> None: # Factorize-once: the build-time singular warning fires a single time for # this single-target (overall-only) fit, not once per (g,t). assert len(fallback_warnings) == 1 - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) # ============================================================================= @@ -470,7 +470,7 @@ def test_fit_idempotent_cache_no_leak(self) -> None: est = ImputationDiD() r1 = est.fit(panel, **base, covariates=["x1", "x2"]) r2 = est.fit(panel, **base, covariates=["x1", "x2"]) - assert r1.overall_se == r2.overall_se + assert r1.se == r2.se es1, es2 = r1.aggregate("event_study"), r2.aggregate("event_study") assert len(es1.se) > 0 assert np.array_equal(es1.event_time, es2.event_time) @@ -581,9 +581,9 @@ def test_public_se_regression_pin_cohort_partition(self) -> None: res = ImputationDiD(aux_partition="cohort").fit( panel, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 # Regression pin (value produced by the unit-clustered Eq. 8 code path). - assert res.overall_se == pytest.approx(_EQ8_COHORT_SE_PIN, abs=1e-8) + assert res.se == pytest.approx(_EQ8_COHORT_SE_PIN, abs=1e-8) # Pin value for test_public_se_regression_pin_cohort_partition, produced by the @@ -711,15 +711,15 @@ def test_loo_se_geq_nonloo_at_unit_clustering(self): ) r0 = ImputationDiD(leave_one_out=False).fit(panel, **self._COMMON) r1 = ImputationDiD(leave_one_out=True).fit(panel, **self._COMMON) - np.testing.assert_allclose(r1.overall_att, r0.overall_att, rtol=0, atol=1e-12) - assert r1.overall_se > r0.overall_se + np.testing.assert_allclose(r1.att, r0.att, rtol=0, atol=1e-12) + assert r1.se > r0.se def test_loo_false_is_byte_identical_to_default(self): """leave_one_out=False changes nothing on the default path.""" rng = np.random.default_rng(_BASE_SEED_EQ8 + 12) panel = _make_staggered_panel(rng, cohorts=[3], n_per_cohort=50, n_periods=5) - se_default = ImputationDiD().fit(panel, **self._COMMON).overall_se - se_loo_false = ImputationDiD(leave_one_out=False).fit(panel, **self._COMMON).overall_se + se_default = ImputationDiD().fit(panel, **self._COMMON).se + se_loo_false = ImputationDiD(leave_one_out=False).fit(panel, **self._COMMON).se assert se_default == se_loo_false def test_loo_single_unit_group_warns_and_returns_finite(self): @@ -753,7 +753,7 @@ def test_loo_single_unit_group_warns_and_returns_finite(self): panel = pd.DataFrame(rows) with pytest.warns(UserWarning, match="single positive-weight unit"): res = ImputationDiD(leave_one_out=True).fit(panel, **self._COMMON) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) def test_loo_param_validation_and_roundtrip(self): """leave_one_out is in get/set_params; a non-bool is rejected (TypeError) @@ -773,8 +773,8 @@ def test_loo_fit_is_idempotent_on_config(self): rng = np.random.default_rng(_BASE_SEED_EQ8 + 15) panel = _make_staggered_panel(rng, cohorts=[3, 4], n_per_cohort=25, n_periods=6) est = ImputationDiD(leave_one_out=True) - se_a = est.fit(panel, **self._COMMON).overall_se - se_b = est.fit(panel, **self._COMMON).overall_se + se_a = est.fit(panel, **self._COMMON).se + se_b = est.fit(panel, **self._COMMON).se assert se_a == se_b assert est.leave_one_out is True @@ -786,7 +786,7 @@ def test_loo_composes_with_cluster_and_bootstrap(self): panel = _make_staggered_panel(rng, cohorts=[3, 4], n_per_cohort=30, n_periods=6) panel["state"] = panel["unit"] % 5 # coarser cluster se_cluster = ( - ImputationDiD(leave_one_out=True, cluster="state").fit(panel, **self._COMMON).overall_se + ImputationDiD(leave_one_out=True, cluster="state").fit(panel, **self._COMMON).se ) assert np.isfinite(se_cluster) with warnings.catch_warnings(): @@ -794,7 +794,7 @@ def test_loo_composes_with_cluster_and_bootstrap(self): se_boot = ( ImputationDiD(leave_one_out=True, n_bootstrap=49, seed=7) .fit(panel, **self._COMMON) - .overall_se + .se ) assert np.isfinite(se_boot) @@ -861,8 +861,8 @@ def test_loo_aggregate_all_analytical(self): ) r0 = ImputationDiD(leave_one_out=False).fit(panel, **self._COMMON) r1 = ImputationDiD(leave_one_out=True).fit(panel, **self._COMMON) - np.testing.assert_allclose(r1.overall_att, r0.overall_att, rtol=0, atol=1e-12) - assert r1.overall_se > r0.overall_se + np.testing.assert_allclose(r1.att, r0.att, rtol=0, atol=1e-12) + assert r1.se > r0.se self._assert_inference_consistent(es_dict(r1)) self._assert_inference_consistent(group_dict(r1)) @@ -874,7 +874,7 @@ def test_loo_aggregate_all_bootstrap(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") r = ImputationDiD(leave_one_out=True, n_bootstrap=99, seed=3).fit(panel, **self._COMMON) - assert np.isfinite(r.overall_se) + assert np.isfinite(r.se) self._assert_inference_consistent(r.event_study_effects) self._assert_inference_consistent(r.group_effects) self._assert_inference_consistent(es_dict(r)) @@ -895,7 +895,7 @@ def test_loo_aggregate_all_analytical_survey(self): warnings.simplefilter("ignore") r = ImputationDiD(leave_one_out=True).fit(panel, survey_design=design, **self._COMMON) effects, groups = es_dict(r), group_dict(r) - assert np.isfinite(r.overall_se) + assert np.isfinite(r.se) self._assert_inference_consistent(effects) self._assert_inference_consistent(groups) @@ -921,8 +921,8 @@ def test_loo_coverage_geq_nominal(self): warnings.simplefilter("ignore") r1 = ImputationDiD(leave_one_out=True).fit(panel, **self._COMMON) for r, hit in ((r0, "cov0"), (r1, "cov1")): - lo = r.overall_att - 1.96 * r.overall_se - hi = r.overall_att + 1.96 * r.overall_se + lo = r.att - 1.96 * r.se + hi = r.att + 1.96 * r.se if lo <= true_att <= hi: if hit == "cov0": cov0 += 1 @@ -1038,7 +1038,7 @@ def test_estimate_independent_of_pretrend_request(self) -> None: common = dict(outcome="outcome", unit="unit", time="time", first_treat="first_treat") base = ImputationDiD().fit(panel, **common) with_pre = ImputationDiD(pretrends=True).fit(panel, **common) - assert with_pre.overall_att == pytest.approx(base.overall_att, abs=1e-10) + assert with_pre.att == pytest.approx(base.att, abs=1e-10) # ============================================================================= @@ -1073,7 +1073,7 @@ def test_aux_partition_options_all_run(self) -> None: common = dict(outcome="outcome", unit="unit", time="time", first_treat="first_treat") for partition in ("cohort_horizon", "cohort", "horizon"): res = ImputationDiD(aux_partition=partition).fit(panel, **common) - assert np.isfinite(res.overall_se), partition + assert np.isfinite(res.se), partition # ============================================================================= @@ -1121,13 +1121,13 @@ def test_overall_att_matches_r(self, golden: dict, panel: pd.DataFrame) -> None: res = ImputationDiD().fit( panel, outcome="y", unit="unit", time="time", first_treat="first_treat" ) - assert res.overall_att == pytest.approx(golden["overall"]["att"], abs=1e-6) + assert res.att == pytest.approx(golden["overall"]["att"], abs=1e-6) def test_overall_se_matches_r(self, golden: dict, panel: pd.DataFrame) -> None: res = ImputationDiD().fit( panel, outcome="y", unit="unit", time="time", first_treat="first_treat" ) - assert res.overall_se == pytest.approx(golden["overall"]["se"], abs=1e-7) + assert res.se == pytest.approx(golden["overall"]["se"], abs=1e-7) def test_event_study_atts_match_r(self, golden: dict, panel: pd.DataFrame) -> None: res = ImputationDiD().fit( @@ -1215,11 +1215,11 @@ def _fit(panel: pd.DataFrame, **kwargs): def test_overall_att_matches_r(self, golden: dict, covariate_panel: pd.DataFrame) -> None: res = self._fit(covariate_panel) - assert res.overall_att == pytest.approx(golden["covariate"]["overall"]["att"], abs=1e-6) + assert res.att == pytest.approx(golden["covariate"]["overall"]["att"], abs=1e-6) def test_overall_se_matches_r(self, golden: dict, covariate_panel: pd.DataFrame) -> None: res = self._fit(covariate_panel) - assert res.overall_se == pytest.approx(golden["covariate"]["overall"]["se"], abs=1e-7) + assert res.se == pytest.approx(golden["covariate"]["overall"]["se"], abs=1e-7) def test_event_study_atts_match_r(self, golden: dict, covariate_panel: pd.DataFrame) -> None: effects = es_dict(self._fit(covariate_panel)) diff --git a/tests/test_methodology_stacked_did.py b/tests/test_methodology_stacked_did.py index acc5f29d6..9cb40f72d 100644 --- a/tests/test_methodology_stacked_did.py +++ b/tests/test_methodology_stacked_did.py @@ -230,7 +230,7 @@ def test_hc1_se_matches_clubsandwich_cr1s_unit_cluster(self, goldens, panel): ), ) np.testing.assert_allclose( - res.overall_se, + res.se, r_overall_se, atol=1e-10, rtol=1e-10, @@ -270,7 +270,7 @@ def test_hc2_bm_se_matches_clubsandwich_cr2_unit_cluster(self, goldens, panel): err_msg="StackedDiD hc2_bm event-study SE must match clubSandwich CR2 at atol=1e-10", ) np.testing.assert_allclose( - res.overall_se, + res.se, r_overall_se, atol=1e-10, rtol=1e-10, @@ -321,7 +321,7 @@ def test_hc2_bm_overall_att_dof_matches_wald_test_htz_unit_cluster(self, goldens the post-period-average contrast was missing from the prior fixture).""" r_dof_overall = float(goldens["unit"]["dof_bm_overall"]) res = _fit(panel, vcov_type="hc2_bm", cluster="unit") - att, se, ci_hi = res.overall_att, res.overall_se, res.overall_conf_int[1] + att, se, ci_hi = res.att, res.se, res.conf_int[1] df_python = _recover_dof_from_ci(att, se, ci_hi, res.alpha) np.testing.assert_allclose( df_python, @@ -353,7 +353,7 @@ def test_hc2_bm_se_matches_clubsandwich_cr2_unit_subexp_cluster(self, goldens, p ), ) np.testing.assert_allclose( - res.overall_se, + res.se, r_overall_se, atol=1e-10, rtol=1e-10, @@ -367,7 +367,7 @@ def test_hc2_bm_overall_att_dof_matches_wald_test_htz_unit_subexp_cluster(self, """Symmetric overall ATT DOF parity at the alternate cluster level.""" r_dof_overall = float(goldens["unit_subexp"]["dof_bm_overall"]) res = _fit(panel, vcov_type="hc2_bm", cluster="unit_subexp") - att, se, ci_hi = res.overall_att, res.overall_se, res.overall_conf_int[1] + att, se, ci_hi = res.att, res.se, res.conf_int[1] df_python = _recover_dof_from_ci(att, se, ci_hi, res.alpha) np.testing.assert_allclose( df_python, @@ -440,7 +440,7 @@ def test_population_weighting_hc1_matches_clubsandwich_cr1s(self, goldens, popul err_msg="StackedDiD weighting='population' hc1 event-study SE must match R CR1S", ) np.testing.assert_allclose( - res.overall_se, + res.se, r_overall_se, atol=1e-10, err_msg="StackedDiD weighting='population' hc1 overall_se must match R", @@ -470,7 +470,7 @@ def test_population_weighting_hc2_bm_matches_clubsandwich_cr2(self, goldens, pop err_msg="StackedDiD weighting='population' hc2_bm event-study SE must match R CR2", ) np.testing.assert_allclose( - res.overall_se, + res.se, r_overall_se, atol=1e-10, err_msg="StackedDiD weighting='population' hc2_bm overall_se must match R", @@ -490,9 +490,7 @@ def test_population_weighting_hc2_bm_matches_clubsandwich_cr2(self, goldens, pop rtol=1e-6, err_msg=f"population hc2_bm BM DOF at h={h} must match R", ) - df_overall = _recover_dof_from_ci( - res.overall_att, res.overall_se, res.overall_conf_int[1], res.alpha - ) + df_overall = _recover_dof_from_ci(res.att, res.se, res.conf_int[1], res.alpha) np.testing.assert_allclose( df_overall, r_dof_overall, @@ -527,7 +525,7 @@ def test_anticipation1_hc1_matches_clubsandwich_cr1s(self, goldens, panel): err_msg="StackedDiD anticipation=1 hc1 event-study SE must match R CR1S", ) np.testing.assert_allclose( - res.overall_se, + res.se, r_overall_se, atol=1e-10, err_msg="StackedDiD anticipation=1 hc1 overall_se must match R", @@ -556,7 +554,7 @@ def test_anticipation1_hc2_bm_matches_clubsandwich_cr2(self, goldens, panel): err_msg="StackedDiD anticipation=1 hc2_bm event-study SE must match R CR2", ) np.testing.assert_allclose( - res.overall_se, + res.se, r_overall_se, atol=1e-10, err_msg="StackedDiD anticipation=1 hc2_bm overall_se must match R", @@ -577,9 +575,7 @@ def test_anticipation1_hc2_bm_matches_clubsandwich_cr2(self, goldens, panel): rtol=1e-6, err_msg=f"anticipation1 hc2_bm BM DOF at h={h} must match R", ) - df_overall = _recover_dof_from_ci( - res.overall_att, res.overall_se, res.overall_conf_int[1], res.alpha - ) + df_overall = _recover_dof_from_ci(res.att, res.se, res.conf_int[1], res.alpha) np.testing.assert_allclose( df_overall, r_dof_overall, @@ -614,7 +610,7 @@ def test_sample_share_weighting_hc1_matches_clubsandwich_cr1s(self, goldens, pan err_msg="sample_share hc1 event-study SE must match R CR1S", ) np.testing.assert_allclose( - res.overall_se, + res.se, r_overall_se, atol=1e-10, err_msg="sample_share hc1 overall_se must match R", @@ -646,7 +642,7 @@ def test_sample_share_weighting_hc2_bm_matches_clubsandwich_cr2(self, goldens, p err_msg="sample_share hc2_bm event-study SE must match R CR2", ) np.testing.assert_allclose( - res.overall_se, + res.se, r_overall_se, atol=1e-10, err_msg="sample_share hc2_bm overall_se must match R", @@ -663,9 +659,7 @@ def test_sample_share_weighting_hc2_bm_matches_clubsandwich_cr2(self, goldens, p rtol=1e-6, err_msg=f"sample_share hc2_bm BM DOF at h={h} must match R", ) - df_overall = _recover_dof_from_ci( - res.overall_att, res.overall_se, res.overall_conf_int[1], res.alpha - ) + df_overall = _recover_dof_from_ci(res.att, res.se, res.conf_int[1], res.alpha) np.testing.assert_allclose( df_overall, r_dof_overall, @@ -801,8 +795,8 @@ def test_estimand_integrity_under_heterogeneity(self): treated_x_mean = units.loc[np.isfinite(units["ft"]), "x"].mean() true_att = 2.0 + 1.0 * treated_x_mean # treated-average ATT - cb = self._fit(df, balance="entropy").overall_att - plain = self._fit(df, balance="none").overall_att + cb = self._fit(df, balance="entropy").att + plain = self._fit(df, balance="none").att assert abs(cb - true_att) < 0.15, f"CBWSDID should recover {true_att:.3f}, got {cb:.3f}" assert abs(plain - true_att) > 0.3, f"plain StackedDiD should be biased, got {plain:.3f}" @@ -828,8 +822,8 @@ def test_balanced_covariate_reduces_to_plain_stacked(self): ) rows.append((uid, t, ft if np.isfinite(ft) else 0, x, y)) df = pd.DataFrame(rows, columns=["unit", "time", "first_treat", "x", "y"]) - cb = self._fit(df, balance="entropy").overall_att - plain = self._fit(df, balance="none").overall_att + cb = self._fit(df, balance="entropy").att + plain = self._fit(df, balance="none").att assert abs(cb - plain) < 0.05 assert abs(cb - 2.0) < 0.1 diff --git a/tests/test_methodology_sun_abraham.py b/tests/test_methodology_sun_abraham.py index 66ca6b0a4..8960f2e99 100644 --- a/tests/test_methodology_sun_abraham.py +++ b/tests/test_methodology_sun_abraham.py @@ -225,8 +225,8 @@ def _dof_from_p(t_stat, p_value, lo=2.0, hi=1e6): ) # Overall ATT contrast DOF. - t_all = res.overall_att / res.overall_se - df_all = _dof_from_p(t_all, res.overall_p_value) + t_all = res.att / res.se + df_all = _dof_from_p(t_all, res.p_value) r_df_all = sa_golden["dof_bm_contrast_overall_unit"] assert np.isclose(df_all, r_df_all, atol=1e-6), ( f"SA overall ATT inferred BM DOF {df_all:.6f} does not match " diff --git a/tests/test_methodology_two_stage.py b/tests/test_methodology_two_stage.py index 7d09d198d..570775f3d 100644 --- a/tests/test_methodology_two_stage.py +++ b/tests/test_methodology_two_stage.py @@ -151,7 +151,7 @@ def test_recovers_overall_att_eq4(self) -> None: res = TwoStageDiD().fit( panel, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert abs(res.overall_att - 2.0) < 0.05 + assert abs(res.att - 2.0) < 0.05 def test_recovers_heterogeneous_event_study_eq6(self) -> None: """Horizon-specific effects tau_K = 1 + 0.5*K are recovered per horizon @@ -199,7 +199,7 @@ def test_stage1_uses_untreated_only(self) -> None: perturbed, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) # Only the perturbed obs's own residual changes (Stage-2 weight 1/N_treated). - assert abs((pert.overall_att - base.overall_att) - delta / n_treated) < 1e-6 + assert abs((pert.att - base.att) - delta / n_treated) < 1e-6 def test_coincides_with_imputation_estimand(self) -> None: """Gardner's two-stage estimator coincides in point estimates with the @@ -216,7 +216,7 @@ def test_coincides_with_imputation_estimand(self) -> None: imp = ImputationDiD().fit( panel, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert ts.overall_att == pytest.approx(imp.overall_att, abs=1e-10) + assert ts.att == pytest.approx(imp.att, abs=1e-10) def test_covariates_residualized_in_both_stages(self) -> None: """The live ``covariates=`` path (fn. 9 in-both-stages): a time-varying @@ -252,7 +252,7 @@ def test_covariates_residualized_in_both_stages(self) -> None: att_no_cov = ( TwoStageDiD() .fit(panel, outcome="outcome", unit="unit", time="time", first_treat="first_treat") - .overall_att + .att ) att_cov = ( TwoStageDiD() @@ -264,7 +264,7 @@ def test_covariates_residualized_in_both_stages(self) -> None: first_treat="first_treat", covariates=["x"], ) - .overall_att + .att ) # Without adjustment, the confounded covariate biases the ATT well away # from tau; with adjustment, the true ATT is recovered. @@ -290,7 +290,7 @@ def test_overall_se_finite_and_positive(self) -> None: res = TwoStageDiD().fit( panel, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 def test_event_study_ses_finite(self) -> None: rng = np.random.default_rng(_BASE_SEED_VAR + 2) @@ -333,7 +333,7 @@ def test_first_stage_correction_is_nontrivial(self) -> None: res = TwoStageDiD().fit( panel, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - gmm_se = res.overall_se + gmm_se = res.se # Naive benchmark: y_tilde for the treated obs is exposed as `tau_hat`; # the overall ATT is its (unweighted) mean. The SE of that mean IGNORING @@ -361,8 +361,8 @@ def test_no_fsa_global_inverse_se_pin(self) -> None: res = TwoStageDiD().fit( panel, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 - assert res.overall_se == pytest.approx(_SE_PIN, abs=1e-8) + assert np.isfinite(res.se) and res.se > 0 + assert res.se == pytest.approx(_SE_PIN, abs=1e-8) def test_pretrends_event_study_ses_finite(self) -> None: """With pretrends=True the Stage-2 design adds pre-period leads; their GMM @@ -390,7 +390,7 @@ def test_pretrends_event_study_ses_finite(self) -> None: if np.isfinite(att): assert np.isfinite(se), f"lead h={h} SE non-finite" # Overall ATT is unchanged by requesting pre-period leads. - assert res.overall_att == pytest.approx(base.overall_att, abs=1e-8) + assert res.att == pytest.approx(base.att, abs=1e-8) def test_unit_clustered_default(self) -> None: """cluster=None clusters the meat at the unit level (Appendix B @@ -433,7 +433,7 @@ def test_singular_omega0_warns_and_falls_back(self) -> None: assert any( "falling back to sparse LSMR" in str(w.message) for w in caught ), "expected the sparse-LSMR fallback warning under a singular Omega_0" - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) def test_bootstrap_scores_use_exact_residuals(self) -> None: """The multiplier bootstrap builds its per-cluster GMM scores from the SAME @@ -479,7 +479,7 @@ def test_bootstrap_scores_use_exact_residuals(self) -> None: ) v_boot = bread @ (S.T @ S) @ bread # Identical to the analytical exact SE (would be ~1% off with iterative residuals). - assert np.sqrt(v_boot[0, 0]) == pytest.approx(res.overall_se, abs=1e-9) + assert np.sqrt(v_boot[0, 0]) == pytest.approx(res.se, abs=1e-9) def test_bootstrap_event_study_scores_use_exact_residuals(self) -> None: """Multi-column (event-study) bootstrap GMM scores also use the shared @@ -582,7 +582,7 @@ def test_always_treated_excluded_with_warning(self) -> None: # The 5 always-treated units are dropped, so the treated-unit count falls # by exactly 5, and their obs do not enter Stage 2. assert res.n_treated_units == n_treated_orig - 5 - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) def test_no_never_treated_horizons_nan(self) -> None: """Proposition 5 (Borusyak et al. 2024): with no never-treated units and @@ -762,13 +762,13 @@ def test_overall_att_matches_r(self, golden: dict, panel: pd.DataFrame) -> None: res = TwoStageDiD().fit( panel, outcome="y", unit="unit", time="time", first_treat="first_treat" ) - assert res.overall_att == pytest.approx(golden["overall"]["att"], abs=1e-6) + assert res.att == pytest.approx(golden["overall"]["att"], abs=1e-6) def test_overall_se_matches_r(self, golden: dict, panel: pd.DataFrame) -> None: res = TwoStageDiD().fit( panel, outcome="y", unit="unit", time="time", first_treat="first_treat" ) - assert res.overall_se == pytest.approx(golden["overall"]["se"], abs=1e-7) + assert res.se == pytest.approx(golden["overall"]["se"], abs=1e-7) def test_event_study_atts_match_r(self, golden: dict, panel: pd.DataFrame) -> None: res = TwoStageDiD().fit( diff --git a/tests/test_methodology_wooldridge.py b/tests/test_methodology_wooldridge.py index 04ef987ce..a23e6a79f 100644 --- a/tests/test_methodology_wooldridge.py +++ b/tests/test_methodology_wooldridge.py @@ -431,9 +431,8 @@ def test_overall_att_equals_cell_count_weighted_average_of_gt_atts(self) -> None weights = {k: res._gt_weights.get(k, 0) for k in gt} total_w = sum(weights.values()) manual_avg = sum(weights[k] * gt[k]["att"] for k in gt) / total_w - assert res.overall_att == pytest.approx(manual_avg, abs=1e-12), ( - f"overall_att={res.overall_att:.6f} != manual cell-count-weighted " - f"avg={manual_avg:.6f}" + assert res.att == pytest.approx(manual_avg, abs=1e-12), ( + f"overall_att={res.att:.6f} != manual cell-count-weighted " f"avg={manual_avg:.6f}" ) def test_constant_outcome_panel_returns_nan_inference(self) -> None: @@ -596,7 +595,7 @@ def test_pols_runs_with_never_treated_control_group(self) -> None: # Surface-level invariants: fit completed cleanly + the result # object is well-formed. assert isinstance(res, WooldridgeDiDResults) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) assert res.control_group == "never_treated" treated_finite_atts = [ eff["att"] @@ -621,12 +620,12 @@ def test_simple_aggregate_matches_overall_att_at_fit_time(self) -> None: res = WooldridgeDiD(method="ols").fit( panel, outcome="y", unit="unit", time="time", first_treat="cohort" ) - before_att = res.overall_att - before_se = res.overall_se + before_att = res.att + before_se = res.se res.aggregate("simple") # Simple aggregation is a no-op for these fields - assert res.overall_att == pytest.approx(before_att, abs=1e-15) - assert res.overall_se == pytest.approx(before_se, abs=1e-15) + assert res.att == pytest.approx(before_att, abs=1e-15) + assert res.se == pytest.approx(before_se, abs=1e-15) # ============================================================================= @@ -786,7 +785,7 @@ def test_aggregate_weights_cell_default_matches_jwdid_estat(self) -> None: cell_w = {k: res._gt_weights.get(k, 0) for k in gt} total_w = sum(cell_w.values()) manual_cell_count_att = sum(cell_w[k] * gt[k]["att"] for k in gt) / total_w - assert res.overall_att == pytest.approx(manual_cell_count_att, abs=1e-12) + assert res.att == pytest.approx(manual_cell_count_att, abs=1e-12) def test_aggregate_simple_weights_cohort_share_matches_paper_eq74(self) -> None: """``aggregate("simple", weights="cohort_share")`` matches paper Eq. 7.4 hand-calc. @@ -829,8 +828,8 @@ def test_aggregate_simple_weights_cohort_share_matches_paper_eq74(self) -> None: + n_g[3] * gt[(3, 4)]["att"] ) / (n_g[2] * 3 + n_g[3] * 2) res.aggregate("simple", weights="cohort_share") - assert res.overall_att == pytest.approx(manual_cohort_share_att, abs=1e-12), ( - f"cohort_share overall={res.overall_att:.6f} != " + assert res.att == pytest.approx(manual_cohort_share_att, abs=1e-12), ( + f"cohort_share overall={res.att:.6f} != " f"manual paper Eq. 7.4 ATT={manual_cohort_share_att:.6f}" ) @@ -905,9 +904,9 @@ def test_aggregate_weights_cohort_share_balanced_panel_equals_cell(self) -> None res = WooldridgeDiD(method="ols").fit( panel, outcome="y", unit="unit", time="time", first_treat="cohort" ) - cell_att = res.overall_att + cell_att = res.att res.aggregate("simple", weights="cohort_share") - cohort_share_att = res.overall_att + cohort_share_att = res.att assert cell_att == pytest.approx(cohort_share_att, abs=1e-12), ( f"balanced panel: cell ATT={cell_att:.6f} should equal " f"cohort_share ATT={cohort_share_att:.6f} per paper Section 7.5" @@ -1057,12 +1056,12 @@ def test_aggregate_weights_cohort_share_poisson_path(self) -> None: # simple aggregation with pytest.warns(UserWarning, match=r"cohort_share.*conditional-on-shares"): res.aggregate("simple", weights="cohort_share") - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) and res.overall_se > 0 - assert np.isnan(res.overall_t_stat) - assert np.isnan(res.overall_p_value) - assert np.isnan(res.overall_conf_int[0]) - assert np.isnan(res.overall_conf_int[1]) + assert np.isfinite(res.att) + assert np.isfinite(res.se) and res.se > 0 + assert np.isnan(res.t_stat) + assert np.isnan(res.p_value) + assert np.isnan(res.conf_int[0]) + assert np.isnan(res.conf_int[1]) # event aggregation: k >= 0 only with pytest.warns(UserWarning, match=r"cohort_share.*conditional-on-shares"): res.aggregate("event_study", weights="cohort_share") @@ -1125,12 +1124,12 @@ def test_aggregate_weights_cohort_share_logit_path(self) -> None: # simple aggregation with pytest.warns(UserWarning, match=r"cohort_share.*conditional-on-shares"): res.aggregate("simple", weights="cohort_share") - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) and res.overall_se > 0 - assert np.isnan(res.overall_t_stat) - assert np.isnan(res.overall_p_value) - assert np.isnan(res.overall_conf_int[0]) - assert np.isnan(res.overall_conf_int[1]) + assert np.isfinite(res.att) + assert np.isfinite(res.se) and res.se > 0 + assert np.isnan(res.t_stat) + assert np.isnan(res.p_value) + assert np.isnan(res.conf_int[0]) + assert np.isnan(res.conf_int[1]) # event aggregation: k >= 0 only with pytest.warns(UserWarning, match=r"cohort_share.*conditional-on-shares"): res.aggregate("event_study", weights="cohort_share") @@ -1179,12 +1178,12 @@ def test_aggregate_weights_cohort_share_inference_fail_closed_with_warning( with pytest.warns(UserWarning, match=r"cohort_share.*conditional-on-shares"): res.aggregate("simple", weights="cohort_share") # Point estimate + SE retained for reference; inference fail-closed. - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) and res.overall_se > 0 - assert np.isnan(res.overall_t_stat) - assert np.isnan(res.overall_p_value) - assert np.isnan(res.overall_conf_int[0]) - assert np.isnan(res.overall_conf_int[1]) + assert np.isfinite(res.att) + assert np.isfinite(res.se) and res.se > 0 + assert np.isnan(res.t_stat) + assert np.isnan(res.p_value) + assert np.isnan(res.conf_int[0]) + assert np.isnan(res.conf_int[1]) def test_aggregate_simple_weights_cell_idempotent_with_fit_time_overall_att(self) -> None: """``aggregate("simple", weights="cell")`` reproduces the fit-time overall_att. @@ -1211,11 +1210,11 @@ def test_aggregate_simple_weights_cell_idempotent_with_fit_time_overall_att(self res = WooldridgeDiD(method="ols").fit( panel, outcome="y", unit="unit", time="time", first_treat="cohort" ) - fit_time_att = res.overall_att - fit_time_se = res.overall_se + fit_time_att = res.att + fit_time_se = res.se res.aggregate("simple") # default weights="cell" - assert res.overall_att == pytest.approx(fit_time_att, abs=1e-12) - assert res.overall_se == pytest.approx(fit_time_se, abs=1e-12) + assert res.att == pytest.approx(fit_time_att, abs=1e-12) + assert res.se == pytest.approx(fit_time_se, abs=1e-12) def test_aggregate_group_calendar_use_cell_count_weights(self) -> None: """``aggregate('group'/'calendar')`` use cell-count weights (the only supported scheme). @@ -1306,8 +1305,8 @@ def test_cohort_trends_false_default_matches_pre_pr_baseline(self) -> None: # Bit-equal across both invocations (tolerance handles sub-ULP # float-aggregation-order noise from the Python-level # ``_cell_weight`` helper closure). - assert res_default.overall_att == pytest.approx(res_explicit_off.overall_att, abs=1e-14) - assert res_default.overall_se == pytest.approx(res_explicit_off.overall_se, abs=1e-14) + assert res_default.att == pytest.approx(res_explicit_off.att, abs=1e-14) + assert res_default.se == pytest.approx(res_explicit_off.se, abs=1e-14) for k in res_default.group_time_effects: assert res_default.group_time_effects[k]["att"] == pytest.approx( res_explicit_off.group_time_effects[k]["att"], abs=1e-14 @@ -1482,8 +1481,8 @@ def test_cohort_trends_true_plus_weights_cohort_share_simple_excludes_trend_colu panel, outcome="y", unit="unit", time="time", first_treat="cohort" ) res.aggregate("simple", weights="cohort_share") - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.att) + assert np.isfinite(res.se) and res.se > 0 def test_cohort_trends_true_rejects_never_treated_control_group(self) -> None: """CI R9 P1 fix: ``cohort_trends=True`` + ``control_group="never_treated"`` raises. @@ -1901,9 +1900,9 @@ def test_cohort_trends_true_plus_bootstrap_preserves_bootstrap_se(self) -> None: panel, outcome="y", unit="unit", time="time", first_treat="cohort" ) assert res._bootstrap_used is True - boot_se = res.overall_se + boot_se = res.se res.aggregate("simple", weights="cell") - assert res.overall_se == boot_se + assert res.se == boot_se with pytest.raises(ValueError, match=r"cohort_share.*not supported on bootstrapped fits"): res.aggregate("simple", weights="cohort_share") @@ -2067,7 +2066,7 @@ def test_unbalanced_panel_with_random_missingness_runs_without_error(self) -> No ) # Sanity: at least one treated cell estimated; overall_att finite. assert any(g > 0 for (g, _t) in res.group_time_effects) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) def test_unbalanced_panel_recovers_constant_tau_approximately(self) -> None: """Under constant-TE DGP with random missingness, ETWFE recovers ``tau`` at MC band. @@ -2361,8 +2360,8 @@ def test_anticipation_window_leads_excluded_from_overall_att(self) -> None: manual_post_only_att = ( sum(post_weights[k] * post_cells[k]["att"] for k in post_cells) / total_w ) - assert res.overall_att == pytest.approx(manual_post_only_att, abs=1e-12), ( - f"overall_att={res.overall_att:.6f} != manual post-only " + assert res.att == pytest.approx(manual_post_only_att, abs=1e-12), ( + f"overall_att={res.att:.6f} != manual post-only " f"({manual_post_only_att:.6f}); REGISTRY Deviation #9 says " f"anticipation leads must be excluded from overall_att." ) @@ -2394,16 +2393,16 @@ def test_bootstrap_preserved_under_aggregate_simple_weights_cell(self) -> None: ) # _bootstrap_used was set assert res._bootstrap_used is True - boot_se = res.overall_se - boot_t = res.overall_t_stat - boot_p = res.overall_p_value - boot_ci = res.overall_conf_int + boot_se = res.se + boot_t = res.t_stat + boot_p = res.p_value + boot_ci = res.conf_int # aggregate("simple", weights="cell") is a no-op on bootstrap fits res.aggregate("simple", weights="cell") - assert res.overall_se == boot_se - assert res.overall_t_stat == boot_t - assert res.overall_p_value == boot_p - assert res.overall_conf_int == boot_ci + assert res.se == boot_se + assert res.t_stat == boot_t + assert res.p_value == boot_p + assert res.conf_int == boot_ci def test_bootstrap_rejects_aggregate_simple_weights_cohort_share(self) -> None: """R1 P1 fix: ``aggregate("simple", weights="cohort_share")`` raises on bootstrap fits. @@ -2533,7 +2532,7 @@ def test_hc2_bm_overall_att_se_matches_clubsandwich_cr2( panel, outcome="y", unit="unit", time="time", first_treat="cohort" ) r_se = golden["hc2_bm"]["overall_att_se"] - assert res.overall_se == pytest.approx(r_se, abs=1e-10) + assert res.se == pytest.approx(r_se, abs=1e-10) def test_hc2_bm_overall_att_contrast_dof_matches_wald_test_htz( self, golden: dict, panel: pd.DataFrame @@ -2548,9 +2547,7 @@ def test_hc2_bm_overall_att_contrast_dof_matches_wald_test_htz( res = WooldridgeDiD(method="ols", vcov_type="hc2_bm").fit( panel, outcome="y", unit="unit", time="time", first_treat="cohort" ) - py_dof = _recover_dof_from_ci( - res.overall_att, res.overall_se, res.overall_conf_int[1], res.alpha - ) + py_dof = _recover_dof_from_ci(res.att, res.se, res.conf_int[1], res.alpha) r_dof = golden["hc2_bm"]["overall_att_contrast_dof"] # brentq inversion tolerance + scipy stats roundtrip: 1e-6 is comfortable # for a DOF in the 1.5..1000 range. The underlying clubSandwich CR2 @@ -2567,7 +2564,7 @@ def test_classical_se_matches_lm_summary(self, golden: dict, panel: pd.DataFrame for i, (g, t) in enumerate(r_keys): py_se = res.group_time_effects[(g, t)]["se"] assert py_se == pytest.approx(r_ses[i], abs=1e-10) - assert res.overall_se == pytest.approx(golden["classical"]["overall_att_se"], abs=1e-10) + assert res.se == pytest.approx(golden["classical"]["overall_att_se"], abs=1e-10) def test_hc2_se_matches_sandwich_vcovhc(self, golden: dict, panel: pd.DataFrame) -> None: """`vcov_type='hc2'` (drops auto-cluster) matches `sandwich::vcovHC(type="HC2")` SE.""" @@ -2579,7 +2576,7 @@ def test_hc2_se_matches_sandwich_vcovhc(self, golden: dict, panel: pd.DataFrame) for i, (g, t) in enumerate(r_keys): py_se = res.group_time_effects[(g, t)]["se"] assert py_se == pytest.approx(r_ses[i], abs=1e-10) - assert res.overall_se == pytest.approx(golden["hc2"]["overall_att_se"], abs=1e-10) + assert res.se == pytest.approx(golden["hc2"]["overall_att_se"], abs=1e-10) def test_classical_per_cell_inference_uses_residual_df( self, golden: dict, panel: pd.DataFrame @@ -2751,7 +2748,7 @@ def test_poisson_fit_completes_with_finite_atts( if k[0] > 0 and k[1] >= k[0] and np.isfinite(res.group_time_effects[k]["att"]) ] assert len(finite_atts) >= 1, "Poisson path returned no finite treated-cell ATTs" - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) def test_etwfe_poisson_golden_present_and_well_formed( self, golden: dict, panel: pd.DataFrame diff --git a/tests/test_power.py b/tests/test_power.py index a585843f3..8ecc74963 100644 --- a/tests/test_power.py +++ b/tests/test_power.py @@ -39,7 +39,6 @@ _ddd_fit_kwargs, _ddd_panel_viable_min_n, _extract_simple, - _extract_staggered, _factor_dgp_kwargs, _get_registry, _staggered_dgp_kwargs, @@ -912,36 +911,6 @@ class MockResult: assert p == 0.01 assert ci == (2.0, 4.0) - def test_extract_staggered_analytical(self): - """_extract_staggered handles analytical result objects.""" - - class MockResult: - overall_att = 2.0 - overall_se = 0.3 - overall_p_value = 0.02 - overall_conf_int = (1.4, 2.6) - - att, se, p, ci = _extract_staggered(MockResult()) - assert att == 2.0 - assert se == 0.3 - assert p == 0.02 - assert ci == (1.4, 2.6) - - def test_extract_staggered_bootstrap_fallback(self): - """_extract_staggered falls back to bootstrap attribute names.""" - - class MockBootstrapResult: - overall_att = 2.0 - overall_att_se = 0.4 - overall_att_p_value = 0.03 - overall_att_ci = (1.2, 2.8) - - att, se, p, ci = _extract_staggered(MockBootstrapResult()) - assert att == 2.0 - assert se == 0.4 - assert p == 0.03 - assert ci == (1.2, 2.8) - def test_continuous_did_not_in_registry(self): """ContinuousDiD is not in registry and raises without custom data_generator.""" from diff_diff import ContinuousDiD diff --git a/tests/test_practitioner.py b/tests/test_practitioner.py index 5b4957287..96d1fbfe7 100644 --- a/tests/test_practitioner.py +++ b/tests/test_practitioner.py @@ -516,7 +516,6 @@ def test_bacon_with_high_forbidden_weight(self): from diff_diff.bacon import BaconDecompositionResults r = BaconDecompositionResults.__new__(BaconDecompositionResults) - r.overall_att = 0.5 r.total_weight_later_vs_earlier = 0.4 r.comparisons = [] output = practitioner_next_steps(r, verbose=False) diff --git a/tests/test_prep.py b/tests/test_prep.py index 2df106ca1..b6e144463 100644 --- a/tests/test_prep.py +++ b/tests/test_prep.py @@ -4067,8 +4067,8 @@ def test_pweight_callaway_santanna_integration(self): first_treat="first_treat", survey_design=stage2, ) - assert np.isfinite(result.overall_att), f"ATT not finite: {result.overall_att}" - assert result.overall_se > 0, f"SE not positive: {result.overall_se}" + assert np.isfinite(result.att), f"ATT not finite: {result.att}" + assert result.se > 0, f"SE not positive: {result.se}" def test_pweight_replicate_weight_design(self): """Pweight mode works correctly under replicate-weight survey designs.""" diff --git a/tests/test_pretrends_event_study.py b/tests/test_pretrends_event_study.py index a5c7acd6d..c2479baad 100644 --- a/tests/test_pretrends_event_study.py +++ b/tests/test_pretrends_event_study.py @@ -167,8 +167,8 @@ def test_backward_compatibility(self): first_treat="first_treat", ) - assert results_default.overall_att == results_false.overall_att - assert results_default.overall_se == results_false.overall_se + assert results_default.att == results_false.att + assert results_default.se == results_false.se assert set(es_dict(results_default)) == set(es_dict(results_false)) def test_post_treatment_invariance(self): @@ -191,8 +191,8 @@ def test_post_treatment_invariance(self): ) # Overall ATT unchanged - assert results_on.overall_att == results_off.overall_att - assert results_on.overall_se == results_off.overall_se + assert results_on.att == results_off.att + assert results_on.se == results_off.se # Post-treatment event study effects unchanged es_off, es_on = es_dict(results_off), es_dict(results_on) @@ -359,8 +359,8 @@ def test_post_treatment_invariance(self): ) # Overall ATT unchanged - assert results_on.overall_att == results_off.overall_att - assert results_on.overall_se == results_off.overall_se + assert results_on.att == results_off.att + assert results_on.se == results_off.se # Post-treatment event study effects unchanged es_off, es_on = es_dict(results_off), es_dict(results_on) @@ -483,9 +483,9 @@ def test_imputation_survey_weighted_no_covariates(self): survey_design=sd, ) - assert np.isfinite(results.overall_att) - assert np.isfinite(results.overall_se) - assert results.overall_se > 0 + assert np.isfinite(results.att) + assert np.isfinite(results.se) + assert results.se > 0 def test_imputation_survey_weighted_event_study(self): """ImputationDiD with survey weights and event study aggregation.""" @@ -725,7 +725,7 @@ def test_imputation_pretrends_survey_accepted(self): survey_design=sd, ) assert result.aggregate("event_study") is not None - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_imputation_pretrends_balance_e(self): """balance_e restricts pre-period lead regression to balanced cohorts.""" diff --git a/tests/test_replicate_weight_expansion.py b/tests/test_replicate_weight_expansion.py index f1640e193..e5d199983 100644 --- a/tests/test_replicate_weight_expansion.py +++ b/tests/test_replicate_weight_expansion.py @@ -318,8 +318,8 @@ def test_sun_abraham_brr(self): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) and result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) and result.se > 0 assert result.survey_metadata is not None result.summary() @@ -353,8 +353,8 @@ def test_stacked_jk1(self): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) and result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) and result.se > 0 assert result.survey_metadata is not None result.summary() @@ -370,8 +370,8 @@ def test_stacked_brr(self): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) and result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) and result.se > 0 class TestImputationDiDReplicate: @@ -389,8 +389,8 @@ def test_imputation_jk1(self): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) and result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) and result.se > 0 assert result.survey_metadata is not None result.summary() @@ -408,7 +408,7 @@ def test_imputation_event_study_replicate(self): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_se) and result.overall_se > 0 + assert np.isfinite(result.se) and result.se > 0 es = es_dict(result) # At least some identified periods should have finite SE finite_ses = [ @@ -466,8 +466,8 @@ def test_two_stage_jk1(self): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) and result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) and result.se > 0 assert result.survey_metadata is not None result.summary() @@ -540,21 +540,21 @@ def test_two_stage_always_treated(self): survey_design=sd, ) # ATT comes from the main fit (always finite once always-treated drop runs) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) # SE comes from the replicate refit variance: requires the refit # callback to align replicate weights with the post-drop sample. # Pre-Wave-E.3-parity-fix, replicate refits raised ValueError on # length mismatch, `compute_replicate_refit_variance` swallowed # them, and `overall_se` came out NaN with all replicate-based # inference fields NaN. - assert np.isfinite(result.overall_se), ( + assert np.isfinite(result.se), ( "Replicate SE must be finite under always-treated drop. " "If NaN, the replicate refit callback is failing to align " "weights with the post-drop sample — Wave E.3 parity bug." ) - assert np.isfinite(result.overall_p_value) - assert result.overall_conf_int is not None - assert np.all(np.isfinite(result.overall_conf_int)) + assert np.isfinite(result.p_value) + assert result.conf_int is not None + assert np.all(np.isfinite(result.conf_int)) def test_two_stage_always_treated_event_study_and_group_replicate(self): """Replicate refit covers event-study + group surfaces under @@ -997,8 +997,8 @@ def test_sun_abraham_true_brr(self): "first_treat", survey_design=SurveyDesign(weights="weight"), ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 - self._assert_point_invariance(res.overall_att, base.overall_att) + assert np.isfinite(res.se) and res.se > 0 + self._assert_point_invariance(res.att, base.att) def test_stacked_true_brr(self): data = _make_staggered_panel() @@ -1014,8 +1014,8 @@ def test_stacked_true_brr(self): "first_treat", survey_design=SurveyDesign(weights="weight"), ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 - self._assert_point_invariance(res.overall_att, base.overall_att) + assert np.isfinite(res.se) and res.se > 0 + self._assert_point_invariance(res.att, base.att) def test_imputation_true_brr(self): data = _make_staggered_panel() @@ -1031,8 +1031,8 @@ def test_imputation_true_brr(self): "first_treat", survey_design=SurveyDesign(weights="weight"), ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 - self._assert_point_invariance(res.overall_att, base.overall_att) + assert np.isfinite(res.se) and res.se > 0 + self._assert_point_invariance(res.att, base.att) def test_two_stage_true_brr(self): data = _make_staggered_panel() @@ -1048,5 +1048,5 @@ def test_two_stage_true_brr(self): "first_treat", survey_design=SurveyDesign(weights="weight"), ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 - self._assert_point_invariance(res.overall_att, base.overall_att) + assert np.isfinite(res.se) and res.se > 0 + self._assert_point_invariance(res.att, base.att) diff --git a/tests/test_se_accuracy.py b/tests/test_se_accuracy.py index 3dc6a8c73..f65097084 100644 --- a/tests/test_se_accuracy.py +++ b/tests/test_se_accuracy.py @@ -136,14 +136,10 @@ def test_point_estimate_stability(self, cs_results): results, _ = cs_results # Point estimate should be close to true effect (2.0) - assert ( - 1.5 < results.overall_att < 2.5 - ), f"Overall ATT {results.overall_att} not near true effect 2.0" + assert 1.5 < results.att < 2.5, f"Overall ATT {results.att} not near true effect 2.0" # SE should be positive and reasonable - assert ( - 0 < results.overall_se < 0.5 - ), f"Overall SE {results.overall_se} out of expected range" + assert 0 < results.se < 0.5, f"Overall SE {results.se} out of expected range" def test_individual_att_gt_se_formula(self, benchmark_data): """ @@ -197,7 +193,7 @@ def test_aggregation_se_formula(self, benchmark_data): ) # Aggregated SE should be positive - assert results.overall_se > 0 + assert results.se > 0 # Individual SEs for comparison individual_ses = [data["se"] for data in results.group_time_effects.values()] @@ -206,7 +202,7 @@ def test_aggregation_se_formula(self, benchmark_data): # (due to averaging effect) avg_individual_se = np.mean(individual_ses) assert ( - results.overall_se <= avg_individual_se * 1.5 + results.se <= avg_individual_se * 1.5 ), "Aggregated SE unexpectedly large relative to individual SEs" def test_se_difference_threshold(self, cs_results): @@ -219,8 +215,8 @@ def test_se_difference_threshold(self, cs_results): results, _ = cs_results # SE should be positive and reasonable - assert results.overall_se > 0 - assert results.overall_se < 0.5 + assert results.se > 0 + assert results.se < 0.5 def test_se_vs_r_benchmark(self): """ @@ -253,11 +249,11 @@ def test_se_vs_r_benchmark(self): r_overall_se = 0.063460396019 # ATT should match exactly (< 1e-8) - att_diff = abs(results.overall_att - r_overall_att) - assert att_diff < 1e-8, f"ATT differs from R: {results.overall_att} vs {r_overall_att}" + att_diff = abs(results.att - r_overall_att) + assert att_diff < 1e-8, f"ATT differs from R: {results.att} vs {r_overall_att}" # SE should match R exactly (< 0.01% after wif fix) - se_diff_pct = abs(results.overall_se - r_overall_se) / r_overall_se * 100 + se_diff_pct = abs(results.se - r_overall_se) / r_overall_se * 100 assert se_diff_pct < 0.01, f"SE differs from R by {se_diff_pct:.4f}%, expected <0.01%" @pytest.mark.slow @@ -344,7 +340,7 @@ def test_scaling_consistency(self, benchmark_data): ) # SE ratio should be approximately sqrt(400/100) = 2 - se_ratio = small_results.overall_se / large_results.overall_se + se_ratio = small_results.se / large_results.se expected_ratio = np.sqrt(400 / 100) # Allow 50% tolerance due to different cohort compositions diff --git a/tests/test_stacked_did.py b/tests/test_stacked_did.py index 07f349a56..687379c55 100644 --- a/tests/test_stacked_did.py +++ b/tests/test_stacked_did.py @@ -77,9 +77,9 @@ def test_basic_fit(self, staggered_data): first_treat="first_treat", ) assert isinstance(results, StackedDiDResults) - assert np.isfinite(results.overall_att) - assert np.isfinite(results.overall_se) - assert results.overall_se > 0 + assert np.isfinite(results.att) + assert np.isfinite(results.se) + assert results.se > 0 assert results.n_stacked_obs > 0 assert results.n_sub_experiments > 0 @@ -143,7 +143,7 @@ def test_simple_att(self, staggered_data): time="period", first_treat="first_treat", ) - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) # The event-study surface is ALWAYS materialized on 3.9+ fits. assert results.event_study_effects is not None assert results.group_effects is None @@ -160,8 +160,8 @@ def test_known_constant_effect(self, constant_effect_data): ) # Treatment effect is 5.0; allow generous tolerance assert ( - abs(results.overall_att - 5.0) < 1.5 - ), f"Estimated ATT {results.overall_att:.2f} too far from true effect 5.0" + abs(results.att - 5.0) < 1.5 + ), f"Estimated ATT {results.att:.2f} too far from true effect 5.0" def test_dynamic_effects(self, staggered_data): """With dynamic effects, post-treatment coefficients should increase.""" @@ -452,8 +452,8 @@ def test_unit_clustering(self, staggered_data): time="period", first_treat="first_treat", ) - assert np.isfinite(results.overall_se) - assert results.overall_se > 0 + assert np.isfinite(results.se) + assert results.se > 0 def test_unit_subexp_clustering(self, staggered_data): """unit_subexp clustering produces finite SEs.""" @@ -465,8 +465,8 @@ def test_unit_subexp_clustering(self, staggered_data): time="period", first_treat="first_treat", ) - assert np.isfinite(results.overall_se) - assert results.overall_se > 0 + assert np.isfinite(results.se) + assert results.se > 0 # ============================================================================= @@ -547,7 +547,7 @@ def test_single_cohort(self): first_treat="first_treat", ) assert results.n_sub_experiments == 1 - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) def test_anticipation_reference_period(self): """anticipation=1 shifts reference period to e=-2.""" @@ -583,7 +583,7 @@ def test_anticipation_reference_period(self): # Post-treatment includes anticipation period (-1) # Overall ATT averages h in {-1, 0, 1, 2} - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) def test_unbalanced_panel(self): """Works with missing observations within the window.""" @@ -609,7 +609,7 @@ def test_unbalanced_panel(self): first_treat="first_treat", ) sd = results.stacked_data - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) assert np.all(sd["_Q_weight"] > 0) assert np.all(np.isfinite(sd["_Q_weight"])) @@ -998,8 +998,8 @@ def test_default_bit_equal_to_explicit_hc1(self, staggered_data): kwargs = dict(outcome="outcome", unit="unit", time="period", first_treat="first_treat") res_default = est_default.fit(staggered_data, **kwargs) res_explicit = est_explicit.fit(staggered_data, **kwargs) - np.testing.assert_allclose(res_default.overall_se, res_explicit.overall_se, atol=1e-15) - np.testing.assert_allclose(res_default.overall_att, res_explicit.overall_att, atol=1e-15) + np.testing.assert_allclose(res_default.se, res_explicit.se, atol=1e-15) + np.testing.assert_allclose(res_default.att, res_explicit.att, atol=1e-15) def test_hc1_se_bit_equal_to_pre_pr_baseline(self, baseline_panel): """HC1 SE matches the captured pre-PR baseline at machine precision. @@ -1028,13 +1028,13 @@ def test_hc1_se_bit_equal_to_pre_pr_baseline(self, baseline_panel): baseline_panel, outcome="outcome", unit="unit", time="period", first_treat="first_treat" ) np.testing.assert_allclose( - res.overall_att, + res.att, BASELINE_OVERALL_ATT, atol=1e-13, err_msg="HC1 overall_att drifted from pre-PR baseline", ) np.testing.assert_allclose( - res.overall_se, + res.se, BASELINE_OVERALL_SE, atol=1e-13, err_msg="HC1 overall_se drifted from pre-PR baseline", @@ -1058,12 +1058,12 @@ def test_hc2_bm_finite_and_att_identical_to_hc1(self, staggered_data): staggered_data, **kwargs ) np.testing.assert_allclose( - res_hc1.overall_att, - res_hc2bm.overall_att, + res_hc1.att, + res_hc2bm.att, atol=1e-13, err_msg="ATT must be identical across vcov_type", ) - assert np.isfinite(res_hc2bm.overall_se) and res_hc2bm.overall_se > 0 + assert np.isfinite(res_hc2bm.se) and res_hc2bm.se > 0 # Per-event-time SE also all finite under hc2_bm for h, eff in res_hc2bm.event_study_effects.items(): if h == -1: @@ -1171,7 +1171,7 @@ def test_unit_subexp_cluster_plus_hc2_bm_finite(self, staggered_data): res = est.fit( staggered_data, outcome="outcome", unit="unit", time="period", first_treat="first_treat" ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 assert res.vcov_type == "hc2_bm" def test_replicate_refit_smoke_with_default_hc1(self, staggered_data): @@ -1206,7 +1206,7 @@ def test_replicate_refit_smoke_with_default_hc1(self, staggered_data): first_treat="first_treat", survey_design=design, ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 def test_fit_clone_idempotent_on_vcov_type(self, staggered_data): """Per `feedback_fit_does_not_mutate_config`: fit, clone the @@ -1219,7 +1219,7 @@ def test_fit_clone_idempotent_on_vcov_type(self, staggered_data): est_b = StackedDiD(**est_a.get_params()) assert est_a.vcov_type == est_b.vcov_type == "hc2_bm" res_b = est_b.fit(staggered_data, **kwargs) - np.testing.assert_allclose(res_a.overall_se, res_b.overall_se, atol=1e-15) + np.testing.assert_allclose(res_a.se, res_b.se, atol=1e-15) # And refitting est_a doesn't mutate its config _ = est_a.fit(staggered_data, **kwargs) assert est_a.vcov_type == "hc2_bm" @@ -1318,8 +1318,8 @@ def test_replicate_refit_coef_bit_equal_vs_bake_w_baseline(self, baseline_panel) # used elsewhere in the project. BASELINE_ATT = 2.0807833161293945 BASELINE_SE = 0.14697256517699428 - np.testing.assert_allclose(res.overall_att, BASELINE_ATT, atol=1e-13) - np.testing.assert_allclose(res.overall_se, BASELINE_SE, atol=1e-10) + np.testing.assert_allclose(res.att, BASELINE_ATT, atol=1e-13) + np.testing.assert_allclose(res.se, BASELINE_SE, atol=1e-10) def test_hc2_bm_uses_t_distribution_not_normal_on_well_conditioned_design(self, staggered_data): """Smoke-test on a well-conditioned design: hc2_bm CIs use t(BM DOF), @@ -1362,8 +1362,8 @@ def test_hc2_bm_uses_t_distribution_not_normal_on_well_conditioned_design(self, "hc2_bm CIs should use t(BM DOF), not normal — at least one " "post-treatment event_study CI half-width must exceed z_0.975 * SE" ) - overall_half_width = res.overall_conf_int[1] - res.overall_att - overall_t_crit = overall_half_width / res.overall_se + overall_half_width = res.conf_int[1] - res.att + overall_t_crit = overall_half_width / res.se assert overall_t_crit > z_975 * 1.0001 def test_hc2_bm_rank_deficient_keeps_bm_dof_on_identified_contrasts( @@ -1457,8 +1457,8 @@ def _fake_solve_ols(*args, **kwargs): f"delta_{h} CI must use t(BM DOF) on rank-deficient design; " f"got t_crit={t_crit}" ) # Overall ATT also keeps t-distribution - if np.isfinite(res.overall_se) and res.overall_se > 0: - overall_t_crit = (res.overall_conf_int[1] - res.overall_att) / res.overall_se + if np.isfinite(res.se) and res.se > 0: + overall_t_crit = (res.conf_int[1] - res.att) / res.se assert overall_t_crit > z_975 * 1.0001 def test_hc2_bm_nan_dof_fails_closed_with_all_nan_inference(self, staggered_data, monkeypatch): @@ -1514,12 +1514,10 @@ def _fake_contrast_dof(*args, **kwargs): np.isnan(b) for b in eff["conf_int"] ), f"conf_int at h={h} must be all-NaN; got {eff['conf_int']}" # Overall ATT: same fail-closed expectation - assert np.isfinite(res.overall_att) and np.isfinite(res.overall_se) - assert np.isnan( - res.overall_t_stat - ), f"overall_t_stat must be NaN under NaN DOF; got {res.overall_t_stat}" - assert np.isnan(res.overall_p_value) - assert all(np.isnan(b) for b in res.overall_conf_int) + assert np.isfinite(res.att) and np.isfinite(res.se) + assert np.isnan(res.t_stat), f"overall_t_stat must be NaN under NaN DOF; got {res.t_stat}" + assert np.isnan(res.p_value) + assert all(np.isnan(b) for b in res.conf_int) def test_hc2_bm_helper_raises_fails_closed_with_all_nan_inference( self, staggered_data, monkeypatch @@ -1557,8 +1555,8 @@ def _fake_raises(*args, **kwargs): assert np.isnan(eff["t_stat"]) and np.isnan( eff["p_value"] ), f"event_time h={h} inference must NaN-close, not fall back to normal" - assert np.isnan(res.overall_t_stat), "overall_t_stat must NaN-close on helper failure" - assert np.isnan(res.overall_p_value) + assert np.isnan(res.t_stat), "overall_t_stat must NaN-close on helper failure" + assert np.isnan(res.p_value) def test_anticipation_plus_hc2_bm_threads_bm_dof(self, staggered_data): """Per local codex R7 P1: the BM-DOF code path uses anticipation in @@ -1594,8 +1592,8 @@ def test_anticipation_plus_hc2_bm_threads_bm_dof(self, staggered_data): ) # Overall ATT: includes h=-1 per anticipation extension; verify # the overall CI uses t(BM DOF), not normal. - if np.isfinite(res.overall_se) and res.overall_se > 0: - overall_t_crit = (res.overall_conf_int[1] - res.overall_att) / res.overall_se + if np.isfinite(res.se) and res.se > 0: + overall_t_crit = (res.conf_int[1] - res.att) / res.se assert overall_t_crit > z_975 * 1.0001, ( f"Overall ATT CI under anticipation=1 must use t(BM DOF); " f"got t_crit={overall_t_crit}" @@ -1633,11 +1631,11 @@ def test_population_weighting_plus_hc2_bm_finite_threads_bm_dof(self, staggered_ first_treat="first_treat", population="pop", ) - assert np.isfinite(res.overall_att) and np.isfinite(res.overall_se) - assert res.overall_se > 0 + assert np.isfinite(res.att) and np.isfinite(res.se) + assert res.se > 0 # Verify BM DOF threaded (t-distribution used) - if np.isfinite(res.overall_se) and res.overall_se > 0: - overall_t_crit = (res.overall_conf_int[1] - res.overall_att) / res.overall_se + if np.isfinite(res.se) and res.se > 0: + overall_t_crit = (res.conf_int[1] - res.att) / res.se assert ( overall_t_crit > z_975 * 1.0001 ), "hc2_bm + weighting='population' overall CI must use t(BM DOF)" @@ -1658,10 +1656,10 @@ def test_sample_share_weighting_plus_hc2_bm_finite_threads_bm_dof(self, staggere time="period", first_treat="first_treat", ) - assert np.isfinite(res.overall_att) and np.isfinite(res.overall_se) - assert res.overall_se > 0 - if np.isfinite(res.overall_se) and res.overall_se > 0: - overall_t_crit = (res.overall_conf_int[1] - res.overall_att) / res.overall_se + assert np.isfinite(res.att) and np.isfinite(res.se) + assert res.se > 0 + if np.isfinite(res.se) and res.se > 0: + overall_t_crit = (res.conf_int[1] - res.att) / res.se assert ( overall_t_crit > z_975 * 1.0001 ), "hc2_bm + weighting='sample_share' overall CI must use t(BM DOF)" @@ -1683,7 +1681,7 @@ def test_hc1_vs_hc2_bm_differ_under_anticipation(self, staggered_data): staggered_data, **kwargs ) # ATT identical (vcov-only change); SE differs on event-study - np.testing.assert_allclose(res_hc1.overall_att, res_hc2bm.overall_att, atol=1e-13) + np.testing.assert_allclose(res_hc1.att, res_hc2bm.att, atol=1e-13) hc1_es = { h: e["se"] for h, e in res_hc1.event_study_effects.items() if h != -2 and e["se"] > 0 } @@ -1877,7 +1875,7 @@ def test_covariate_scale_invariance(self): df["x2"] = 2.0 * df["x"] a = _cb_fit(df, balance="entropy", covariates=["x"]) b = _cb_fit(df, balance="entropy", covariates=["x2"]) - assert abs(a.overall_att - b.overall_att) < 1e-8 + assert abs(a.att - b.att) < 1e-8 def test_balance_on_plain_fit(self): # balancing works on a plain fit; since M-024 the surface (and the @@ -1885,7 +1883,7 @@ def test_balance_on_plain_fit(self): # aggregate, so this is the same code path as every _cb_fit case. df = _balance_panel(seed=8) res = _cb_fit(df, balance="entropy", covariates=["x"]) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) assert res.balance == "entropy" assert res.event_study_effects is not None @@ -1907,7 +1905,7 @@ def test_ragged_event_window_raises(self): dfr = df[~drop].reset_index(drop=True) with pytest.raises(ValueError, match="balanced event windows"): _cb_fit(dfr, balance="entropy", covariates=["x"]) - assert np.isfinite(_cb_fit(dfr, balance="none").overall_att) + assert np.isfinite(_cb_fit(dfr, balance="none").att) def test_zero_row_eligible_control_raises(self): # An eligible (never-treated) control with ZERO rows in a cohort's window is @@ -1973,23 +1971,23 @@ def test_residual_default_is_t_with_pinned_df(self): res = StackedDiD(kappa_pre=2, kappa_post=2).fit(self._panel(), **self._kw) dfres = res.inference_df assert dfres is not None and dfres > res.n_clusters - assert res.overall_p_value == pytest.approx(self._t_p(res.overall_t_stat, dfres), rel=1e-12) + assert res.p_value == pytest.approx(self._t_p(res.t_stat, dfres), rel=1e-12) finite = {v for v in res.event_study_df.values() if np.isfinite(v)} assert finite == {dfres} def test_small_n_residual_default_moves_off_z(self): """On a SMALL stacked design the z -> t movement is measurable.""" res = StackedDiD(kappa_pre=1, kappa_post=1).fit(self._panel(n_units=12), **self._kw) - assert np.isfinite(res.overall_p_value) - assert res.overall_p_value != self._z_p(res.overall_t_stat) + assert np.isfinite(res.p_value) + assert res.p_value != self._z_p(res.t_stat) def test_cluster_matches_g_minus_1_effective_count(self): data = self._panel() r0 = StackedDiD(kappa_pre=2, kappa_post=2).fit(data, **self._kw) r1 = StackedDiD(kappa_pre=2, kappa_post=2, df_convention="cluster").fit(data, **self._kw) G = r1.n_clusters - assert r1.overall_att == r0.overall_att and r1.overall_se == r0.overall_se - assert r1.overall_p_value == pytest.approx(self._t_p(r1.overall_t_stat, G - 1), rel=1e-12) + assert r1.att == r0.att and r1.se == r0.se + assert r1.p_value == pytest.approx(self._t_p(r1.t_stat, G - 1), rel=1e-12) assert r1.inference_df == G - 1 def test_cluster_g_uses_effective_count_not_raw_unique(self): @@ -2016,8 +2014,8 @@ def test_normal_reproduces_pre39_z(self): data = self._panel() r0 = StackedDiD(kappa_pre=2, kappa_post=2).fit(data, **self._kw) rn = StackedDiD(kappa_pre=2, kappa_post=2, df_convention="normal").fit(data, **self._kw) - assert rn.overall_att == r0.overall_att and rn.overall_se == r0.overall_se - assert rn.overall_p_value == pytest.approx(self._z_p(rn.overall_t_stat), rel=1e-14) + assert rn.att == r0.att and rn.se == r0.se + assert rn.p_value == pytest.approx(self._z_p(rn.t_stat), rel=1e-14) assert rn.inference_df is None def test_bm_dof_precedence_over_knob(self): @@ -2026,7 +2024,7 @@ def test_bm_dof_precedence_over_knob(self): r_clu = StackedDiD( kappa_pre=2, kappa_post=2, vcov_type="hc2_bm", df_convention="cluster" ).fit(data, **self._kw) - assert r_res.overall_p_value == r_clu.overall_p_value + assert r_res.p_value == r_clu.p_value def test_survey_df_precedence_over_knob(self): """Survey design df wins under EVERY knob value: the three fits are @@ -2048,8 +2046,8 @@ def test_survey_df_precedence_over_knob(self): assert base.inference_df is not None and np.isfinite(base.inference_df) for conv in ("cluster", "normal"): r = fits[conv] - assert r.overall_p_value == base.overall_p_value - assert r.overall_conf_int == base.overall_conf_int + assert r.p_value == base.p_value + assert r.conf_int == base.conf_int assert r.inference_df == base.inference_df def test_validation_and_transactional_set_params(self): @@ -2158,7 +2156,7 @@ def test_partial_reference_support_still_fits(self): assert res.event_study_effects is not None surf = res.aggregate("event_study") assert surf.reference_event_times == (-1,) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) class TestReferenceSupportGuardEffectiveWeights: diff --git a/tests/test_staggered.py b/tests/test_staggered.py index 2cc61734c..2f71fd238 100644 --- a/tests/test_staggered.py +++ b/tests/test_staggered.py @@ -65,8 +65,8 @@ def test_basic_fit(self): assert cs.is_fitted_ assert isinstance(results, CallawaySantAnnaResults) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 assert len(results.group_time_effects) > 0 def test_positive_treatment_effect(self): @@ -79,9 +79,9 @@ def test_positive_treatment_effect(self): ) # Should detect positive effect - assert results.overall_att > 0 + assert results.att > 0 # Effect should be roughly correct (within 2 SE) - assert abs(results.overall_att - 3.0) < 2 * results.overall_se + 1.0 + assert abs(results.att - 3.0) < 2 * results.se + 1.0 def test_zero_treatment_effect(self): """Test with no treatment effect.""" @@ -93,7 +93,7 @@ def test_zero_treatment_effect(self): ) # Effect should be close to zero - assert abs(results.overall_att) < 3 * results.overall_se + assert abs(results.att) < 3 * results.se def test_never_treated_inf_encoding(self): """Test that first_treat=np.inf is handled as never-treated, not as a cohort.""" @@ -115,8 +115,8 @@ def test_never_treated_inf_encoding(self): # Results should be identical assert np.isclose( - results_inf.overall_att, results_zero.overall_att - ), f"ATT differs: inf={results_inf.overall_att}, zero={results_zero.overall_att}" + results_inf.att, results_zero.att + ), f"ATT differs: inf={results_inf.att}, zero={results_zero.att}" def test_event_study_aggregation(self): """Test post-fit event study aggregation.""" @@ -192,7 +192,7 @@ def test_control_group_options(self): assert results1.control_group == "never_treated" assert results2.control_group == "not_yet_treated" # Results should be different - assert results1.overall_att != results2.overall_att + assert results1.att != results2.att def test_estimation_methods(self): """Test different estimation methods.""" @@ -209,7 +209,7 @@ def test_estimation_methods(self): # All methods should produce results for method, res in results.items(): - assert res.overall_att is not None + assert res.att is not None def test_summary_output(self): """Test summary output formatting.""" @@ -441,13 +441,13 @@ def test_covariates_are_used(self): ) # Both should produce valid results - assert results1.overall_att is not None - assert results2.overall_att is not None + assert results1.att is not None + assert results2.att is not None # Results may differ when using covariates # (they don't have to differ significantly for this test) - assert results1.overall_se > 0 - assert results2.overall_se > 0 + assert results1.se > 0 + assert results2.se > 0 def test_outcome_regression_with_covariates(self): """Test outcome regression method with covariates.""" @@ -463,8 +463,8 @@ def test_outcome_regression_with_covariates(self): covariates=["x1", "x2"], ) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 assert len(results.group_time_effects) > 0 def test_ipw_with_covariates(self): @@ -481,8 +481,8 @@ def test_ipw_with_covariates(self): covariates=["x1", "x2"], ) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 assert len(results.group_time_effects) > 0 def test_doubly_robust_with_covariates(self): @@ -499,8 +499,8 @@ def test_doubly_robust_with_covariates(self): covariates=["x1", "x2"], ) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 assert len(results.group_time_effects) > 0 def test_all_methods_with_covariates(self): @@ -523,8 +523,8 @@ def test_all_methods_with_covariates(self): # All methods should produce valid results for method, res in results.items(): - assert res.overall_att is not None, f"{method} failed to produce ATT" - assert res.overall_se > 0, f"{method} failed to produce valid SE" + assert res.att is not None, f"{method} failed to produce ATT" + assert res.se > 0, f"{method} failed to produce valid SE" def test_event_study_with_covariates(self): """Test event study aggregation with covariates.""" @@ -572,8 +572,8 @@ def test_single_covariate(self): covariates=["x1"], ) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 def test_treatment_effect_recovery_with_covariates(self): """Test that we recover approximately correct treatment effect.""" @@ -597,8 +597,8 @@ def test_treatment_effect_recovery_with_covariates(self): # Effect should be roughly correct (within reasonable bounds) # Note: we use a generous bound due to finite sample variance - assert results.overall_att > 0, "ATT should be positive" - assert abs(results.overall_att - 3.0) < 2.0, f"ATT={results.overall_att} too far from 3.0" + assert results.att > 0, "ATT should be positive" + assert abs(results.att - 3.0) < 2.0, f"ATT={results.att} too far from 3.0" def test_extreme_propensity_scores(self): """Test handling of covariates that strongly predict treatment. @@ -651,9 +651,9 @@ def test_extreme_propensity_scores(self): ) # Should produce valid results (not NaN or inf) - assert np.isfinite(results.overall_att), "ATT should be finite" - assert np.isfinite(results.overall_se), "SE should be finite" - assert results.overall_se > 0, "SE should be positive" + assert np.isfinite(results.att), "ATT should be finite" + assert np.isfinite(results.se), "SE should be finite" + assert results.se > 0, "SE should be positive" def test_extreme_weights_warning(self, ci_params): """Test that extreme weights produce warnings and methodology-aligned behavior. @@ -697,10 +697,10 @@ def test_extreme_weights_warning(self, ci_params): ) # ATT point estimate should be finite - assert np.isfinite(results.overall_att), "ATT should be finite" + assert np.isfinite(results.att), "ATT should be finite" # SE is either finite (valid) or NaN (signals invalid inference) - not biased - assert np.isfinite(results.overall_se) or np.isnan( - results.overall_se + assert np.isfinite(results.se) or np.isnan( + results.se ), "SE should be finite or NaN (not inf)" # Test with bootstrap - should drop invalid samples with warning @@ -716,16 +716,16 @@ def test_extreme_weights_warning(self, ci_params): warning_messages = [str(warning.message) for warning in w] # ATT should be finite - assert np.isfinite(boot_results.overall_att), "ATT should be finite" + assert np.isfinite(boot_results.att), "ATT should be finite" # Bootstrap SE based on valid samples - may be finite or NaN assert boot_results.bootstrap_results is not None, "Bootstrap results should exist" - assert np.isfinite(boot_results.overall_se) or np.isnan( - boot_results.overall_se + assert np.isfinite(boot_results.se) or np.isnan( + boot_results.se ), "Bootstrap SE should be finite or NaN (not inf)" # If SE is NaN, verify it's due to validity threshold (should have warning) - if np.isnan(boot_results.overall_se): + if np.isnan(boot_results.se): assert any( "valid" in msg.lower() or "nan" in msg.lower() for msg in warning_messages ), "NaN SE should be accompanied by warning about validity" @@ -782,8 +782,8 @@ def test_validity_threshold_nan_se(self): assert boot_results.bootstrap_results is not None, "Bootstrap results should exist" # SE constraints: finite or NaN (never inf) - assert np.isfinite(boot_results.overall_se) or np.isnan( - boot_results.overall_se + assert np.isfinite(boot_results.se) or np.isnan( + boot_results.se ), "Bootstrap SE should be finite or NaN (not inf)" def test_near_collinear_covariates(self): @@ -808,8 +808,8 @@ def test_near_collinear_covariates(self): ) # Should still produce valid results (noise is above tolerance) - assert results.overall_att is not None - assert np.isfinite(results.overall_att) + assert results.att is not None + assert np.isfinite(results.att) def test_missing_values_in_covariates_warning(self): """Test that missing values trigger fallback warning.""" @@ -832,8 +832,8 @@ def test_missing_values_in_covariates_warning(self): ) # Should still produce valid results (using unconditional estimation) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 def test_dr_covariates_not_yet_treated(self): """Regression test: DR + covariates with not_yet_treated control group. @@ -857,10 +857,8 @@ def test_dr_covariates_not_yet_treated(self): covariates=["x1", "x2"], ) - assert np.isfinite( - results.overall_att - ), f"{method}/not_yet_treated: ATT should be finite" - assert results.overall_se > 0, f"{method}/not_yet_treated: SE should be positive" + assert np.isfinite(results.att), f"{method}/not_yet_treated: ATT should be finite" + assert results.se > 0, f"{method}/not_yet_treated: SE should be positive" assert ( len(results.group_time_effects) > 0 ), f"{method}/not_yet_treated: should have group-time effects" @@ -929,7 +927,7 @@ def test_rank_deficient_action_silent_no_warning(self): # Should still get valid results assert results is not None - assert results.overall_att is not None + assert results.att is not None def test_rank_deficient_action_warn_emits_warning(self): """Test that rank_deficient_action='warn' emits rank-deficiency warning on batched path.""" @@ -969,8 +967,8 @@ def test_rank_deficient_action_warn_emits_warning(self): # Should still produce valid results (lstsq fallback) assert results is not None - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 def test_empty_covariates_list_behaves_like_none(self): """covariates=[] should behave identically to covariates=None.""" @@ -996,8 +994,8 @@ def test_empty_covariates_list_behaves_like_none(self): covariates=[], ) - assert results_none.overall_att == results_empty.overall_att - assert results_none.overall_se == results_empty.overall_se + assert results_none.att == results_empty.att + assert results_none.se == results_empty.se assert len(results_none.group_time_effects) == len(results_empty.group_time_effects) def test_nan_cell_preserved_not_dropped(self): @@ -1070,7 +1068,7 @@ def mock_solve_ols(*args, **kwargs): ), f"NaN cell ({g},{t}) must carry skip_reason='non_finite_regression'" # Overall ATT should still be finite (NaN cells excluded from aggregation) - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) def test_nan_cell_bootstrap_aggregation_excludes_nan(self, ci_params): """Bootstrap aggregation paths must exclude NaN ATT(g,t) cells.""" @@ -1135,10 +1133,10 @@ def mock_solve_ols(*args, **kwargs): ), "Poisoned cell must be post-treatment to exercise overall ATT bootstrap filtering" # Overall ATT bootstrap inference should be finite (NaN cells excluded) - assert np.isfinite(results.overall_att), "overall_att should be finite" - assert np.isfinite(results.overall_se), "overall_se should be finite" - assert np.isfinite(results.overall_p_value), "overall_p_value should be finite" - assert all(np.isfinite(x) for x in results.overall_conf_int), "overall CI should be finite" + assert np.isfinite(results.att), "overall_att should be finite" + assert np.isfinite(results.se), "overall_se should be finite" + assert np.isfinite(results.p_value), "overall_p_value should be finite" + assert all(np.isfinite(x) for x in results.conf_int), "overall CI should be finite" # Event study: valid relative times should have finite bootstrap inference assert es @@ -1403,8 +1401,8 @@ def test_materializes_nan_cell_with_skip_reason(self, method, panel, covariates) # Non-empty dict mixing NaN + finite cells fits without raising, and the # NaN cells are excluded from aggregation (the aggregation invariant). assert n_finite > 0, "expected some estimable cells" - assert np.isfinite(results.overall_att), "NaN cells must be excluded from aggregation" - assert np.isfinite(results.overall_se) + assert np.isfinite(results.att), "NaN cells must be excluded from aggregation" + assert np.isfinite(results.se) def test_estimable_cells_have_skip_reason_none(self): """A well-posed fit carries skip_reason=None on every cell.""" @@ -1475,7 +1473,7 @@ def wrapped(self, precomputed, g, t, covariates, **kw): assert not np.isinf(cell["effect"]) assert cell["skip_reason"] == "non_finite_regression" # Excluded from aggregation -> overall ATT still finite. - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) def test_no_covariate_path_nonfinite_att_materialized_as_nan(self): """No-covariate vectorized path: an inf outcome (passes the NaN-only valid @@ -1505,7 +1503,7 @@ def test_no_covariate_path_nonfinite_att_materialized_as_nan(self): for _, v in nf: assert np.isnan(v["effect"]) and not np.isinf(v["effect"]) assert np.isnan(v["se"]) and np.isnan(v["t_stat"]) and np.isnan(v["p_value"]) - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) def test_event_study_omits_all_nonestimable_relative_time(self): """An event-time bucket whose cells are ALL non-estimable is omitted from @@ -1598,11 +1596,11 @@ def test_constant_covariate_finite_se_matches_drop_one(self, method): ) # Regression guard: previously ~1e13, now finite and modest. - assert np.isfinite(with_const.overall_se) - assert with_const.overall_se < 1.0 + assert np.isfinite(with_const.se) + assert with_const.se < 1.0 # Dropping the redundant covariate is equivalent to never adding it. - np.testing.assert_allclose(with_const.overall_se, drop_one.overall_se, rtol=1e-9) - np.testing.assert_allclose(with_const.overall_att, drop_one.overall_att, rtol=1e-9) + np.testing.assert_allclose(with_const.se, drop_one.se, rtol=1e-9) + np.testing.assert_allclose(with_const.att, drop_one.att, rtol=1e-9) @pytest.mark.parametrize("method", ["reg", "ipw", "dr"]) def test_constant_covariate_emits_single_rank_guard_warning(self, method): @@ -1630,7 +1628,7 @@ def test_well_conditioned_covariates_take_fast_path(self): data, "outcome", "unit", "time", "first_treat", covariates=["x1", "x2"] ) assert not any("rank-guarded inverse" in str(w.message) for w in caught) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) def test_clustered_constant_covariate_finite_se(self): # The clustered SE path reuses the same per-cell influence functions, so @@ -1643,8 +1641,8 @@ def test_clustered_constant_covariate_finite_se(self): res = CallawaySantAnna(estimation_method="dr", cluster="cl").fit( data, "outcome", "unit", "time", "first_treat", covariates=["x1", "xc"] ) - assert np.isfinite(res.overall_se) - assert res.overall_se < 1.0 + assert np.isfinite(res.se) + assert res.se < 1.0 @pytest.mark.parametrize("method", ["ipw", "dr"]) def test_rank0_bread_propagates_nan_not_zero(self, monkeypatch, method): @@ -1678,7 +1676,7 @@ def _all_nan_inv(A, tracker=None): assert np.isfinite(cell["effect"]), "point estimate is bread-independent" assert np.isnan(cell["se"]), "rank-0 bread must give NaN SE, not 0.0" assert_nan_inference(cell) - assert np.isnan(res.overall_se) + assert np.isnan(res.se) @pytest.mark.parametrize("method", ["reg", "ipw", "dr"]) def test_rcs_panel_false_constant_covariate_finite_se(self, method): @@ -1719,9 +1717,9 @@ def test_rcs_panel_false_constant_covariate_finite_se(self, method): "first_treat", covariates=["x1", "xc"], ) - assert np.isfinite(with_const.overall_se) - assert with_const.overall_se < 1.0 - np.testing.assert_allclose(with_const.overall_se, drop_one.overall_se, rtol=1e-9) + assert np.isfinite(with_const.se) + assert with_const.se < 1.0 + np.testing.assert_allclose(with_const.se, drop_one.se, rtol=1e-9) @pytest.mark.parametrize("method", ["reg", "dr"]) def test_control_cell_aliasing_close_to_drop_one(self, method): @@ -1751,8 +1749,8 @@ def test_control_cell_aliasing_close_to_drop_one(self, method): "first_treat", covariates=["x1", "x2_deg"], ) - assert np.isfinite(with_deg.overall_se) and with_deg.overall_se > 0 - np.testing.assert_allclose(with_deg.overall_se, drop_one.overall_se, rtol=5e-2) + assert np.isfinite(with_deg.se) and with_deg.se > 0 + np.testing.assert_allclose(with_deg.se, drop_one.se, rtol=5e-2) @pytest.mark.parametrize("method", ["reg", "ipw", "dr"]) def test_survey_weighted_constant_covariate_finite_se(self, method): @@ -1789,9 +1787,9 @@ def test_survey_weighted_constant_covariate_finite_se(self, method): covariates=["x1", "xc"], survey_design=sd, ) - assert np.isfinite(with_const.overall_se) - assert with_const.overall_se < 1.0 - np.testing.assert_allclose(with_const.overall_se, drop_one.overall_se, rtol=1e-9) + assert np.isfinite(with_const.se) + assert with_const.se < 1.0 + np.testing.assert_allclose(with_const.se, drop_one.se, rtol=1e-9) def test_aggregated_se_wif_contract(self, monkeypatch): # Locks the _compute_aggregated_se_with_wif arity + fail-closed contract @@ -1877,15 +1875,15 @@ def test_exact_duplicate_covariate(self, method): d, "outcome", "unit", "time", "first_treat", covariates=["xbig", "x1"] ) # Well-scaled exact duplicate == dropping it (clean column-drop). - np.testing.assert_allclose(well.overall_se, drop_one.overall_se, rtol=1e-9) + np.testing.assert_allclose(well.se, drop_one.se, rtol=1e-9) # Mixed-scale exact duplicate: finite for every method. - assert np.isfinite(big_ab.overall_se) and big_ab.overall_se > 0 - assert np.isfinite(big_ba.overall_se) and big_ba.overall_se > 0 + assert np.isfinite(big_ab.se) and big_ab.se > 0 + assert np.isfinite(big_ba.se) and big_ba.se > 0 # Order-invariance holds for ALL methods now: ipw/dr via the equilibrated # rank-guarded inverse, and reg via the equilibrated point-estimate OR # solve (post OR scale-equilibration change) — mixed-scale exact-duplicate # columns become identical after equilibration, so the SE is order-invariant. - np.testing.assert_allclose(big_ab.overall_se, big_ba.overall_se, rtol=1e-9) + np.testing.assert_allclose(big_ab.se, big_ba.se, rtol=1e-9) @pytest.mark.parametrize("method", ["reg", "ipw", "dr"]) def test_exact_duplicate_covariate_survey_weighted(self, method): @@ -1945,14 +1943,14 @@ def test_exact_duplicate_covariate_survey_weighted(self, method): survey_design=sd, ) # Well-scaled exact duplicate == dropping it, under survey weighting. - np.testing.assert_allclose(well.overall_se, drop_one.overall_se, rtol=1e-7) + np.testing.assert_allclose(well.se, drop_one.se, rtol=1e-7) # Mixed-scale exact duplicate under survey weighting: finite + order-invariant # for ALL methods. reg's point-estimate OR fit now routes through the # equilibrated solve_ols (post OR scale-equilibration change), so its SE is # order-invariant like ipw/dr — see test_exact_duplicate_covariate. - assert np.isfinite(big_ab.overall_se) and big_ab.overall_se > 0 - assert np.isfinite(big_ba.overall_se) and big_ba.overall_se > 0 - np.testing.assert_allclose(big_ab.overall_se, big_ba.overall_se, rtol=1e-7) + assert np.isfinite(big_ab.se) and big_ab.se > 0 + assert np.isfinite(big_ba.se) and big_ba.se > 0 + np.testing.assert_allclose(big_ab.se, big_ba.se, rtol=1e-7) class TestRegIpwIFBehavior: @@ -2082,7 +2080,7 @@ def test_universal_base_period_anticipation_reg_smoke(self): assert np.isfinite(cell["se"]), f"cell ({g},{t})" # The reference period is now materialized as a zero cell. assert any(c.get("is_reference") for c in res.group_time_effects.values()) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) def test_reg_constant_only_covariate_matches_no_covariate(self): """A constant as the ONLY reg covariate makes the CENTERED @@ -2100,9 +2098,9 @@ def test_reg_constant_only_covariate_matches_no_covariate(self): const_only = CallawaySantAnna(estimation_method="reg").fit( data, "outcome", "unit", "time", "first_treat", covariates=["xc"] ) - assert np.isfinite(const_only.overall_se) - np.testing.assert_allclose(const_only.overall_att, no_cov.overall_att, rtol=1e-12) - np.testing.assert_allclose(const_only.overall_se, no_cov.overall_se, rtol=1e-9) + assert np.isfinite(const_only.se) + np.testing.assert_allclose(const_only.att, no_cov.att, rtol=1e-12) + np.testing.assert_allclose(const_only.se, no_cov.se, rtol=1e-9) for key, cell in const_only.group_time_effects.items(): ref = no_cov.group_time_effects[key] np.testing.assert_allclose(cell["effect"], ref["effect"], rtol=1e-12) @@ -2144,9 +2142,9 @@ def test_reg_constant_only_covariate_matches_no_covariate_survey(self): covariates=["xc"], survey_design=SurveyDesign(weights="weight"), ) - assert np.isfinite(const_only.overall_se) - np.testing.assert_allclose(const_only.overall_att, no_cov.overall_att, rtol=1e-12) - np.testing.assert_allclose(const_only.overall_se, no_cov.overall_se, rtol=1e-9) + assert np.isfinite(const_only.se) + np.testing.assert_allclose(const_only.att, no_cov.att, rtol=1e-12) + np.testing.assert_allclose(const_only.se, no_cov.se, rtol=1e-9) for key, cell in const_only.group_time_effects.items(): ref = no_cov.group_time_effects[key] np.testing.assert_allclose(cell["se"], ref["se"], rtol=1e-9) @@ -2223,7 +2221,7 @@ def test_dr_rank_deficient_action_warn_emits_warning(self): ), "Expected at least one rank-deficiency warning from DR path" assert results is not None - assert results.overall_att is not None + assert results.att is not None def test_reg_nyt_rank_deficient_action_warn(self): """Test that reg+not_yet_treated emits rank-deficiency warning with collinear covariates.""" @@ -2259,8 +2257,8 @@ def test_reg_nyt_rank_deficient_action_warn(self): ), "Expected at least one rank-deficiency warning from reg nyt path" assert results is not None - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 def test_ipw_rank_deficient_action_error_raises(self): """IPW path raises ValueError with rank_deficient_action='error' and collinear covariates.""" @@ -2336,7 +2334,7 @@ def test_bootstrap_single_unit_cohort_handles_gracefully(self, ci_params): results.aggregate("group") assert results is not None - assert results.overall_att is not None + assert results.att is not None # Single-unit cohort (g=5) effects should exist and have finite ATT g5_effects = {(g, t): eff for (g, t), eff in results.group_time_effects.items() if g == 5} assert len(g5_effects) > 0, "Expected group-time effects for cohort g=5" @@ -2360,8 +2358,8 @@ def test_bootstrap_basic(self, ci_params): assert results.bootstrap_results is not None assert results.bootstrap_results.n_bootstrap == n_boot assert results.bootstrap_results.weight_type == "rademacher" - assert results.overall_se > 0 - assert results.overall_conf_int[0] < results.overall_att < results.overall_conf_int[1] + assert results.se > 0 + assert results.conf_int[0] < results.att < results.conf_int[1] def test_bootstrap_weight_types(self, ci_params): """Test different bootstrap weight types.""" @@ -2378,7 +2376,7 @@ def test_bootstrap_weight_types(self, ci_params): assert results.bootstrap_results is not None assert results.bootstrap_results.weight_type == wt - assert results.overall_se > 0 + assert results.se > 0 def test_bootstrap_event_study(self, ci_params): """Test bootstrap with event study aggregation.""" @@ -2463,8 +2461,8 @@ def test_bootstrap_reproducibility(self, ci_params): ) # Results should be identical with same seed - assert results1.overall_se == results2.overall_se - assert results1.overall_conf_int == results2.overall_conf_int + assert results1.se == results2.se + assert results1.conf_int == results2.conf_int def test_bootstrap_different_seeds(self, ci_params): """Test that different seeds give different results.""" @@ -2482,7 +2480,7 @@ def test_bootstrap_different_seeds(self, ci_params): ) # Results should differ with different seeds - assert results1.overall_se != results2.overall_se + assert results1.se != results2.se def test_bootstrap_p_value_significance(self, ci_params): """Test that strong effect has significant p-value with bootstrap.""" @@ -2495,7 +2493,7 @@ def test_bootstrap_p_value_significance(self, ci_params): ) # Strong effect should be significant - assert results.overall_p_value < 0.05 + assert results.p_value < 0.05 assert results.is_significant def test_bootstrap_zero_effect_not_significant(self, ci_params): @@ -2510,7 +2508,7 @@ def test_bootstrap_zero_effect_not_significant(self, ci_params): # Zero effect should not be significant at 0.01 level # (using 0.01 to be more conservative with finite sample) - assert results.overall_p_value > 0.01 or abs(results.overall_att) < 2 * results.overall_se + assert results.p_value > 0.01 or abs(results.att) < 2 * results.se def test_bootstrap_distribution_stored(self, ci_params): """Test that bootstrap distribution is stored in results.""" @@ -2541,7 +2539,7 @@ def test_bootstrap_with_covariates(self, ci_params): ) assert results.bootstrap_results is not None - assert results.overall_se > 0 + assert results.se > 0 def test_bootstrap_group_time_effects(self, ci_params): """Test that bootstrap updates group-time effect SEs.""" @@ -2595,7 +2593,7 @@ def test_bootstrap_with_not_yet_treated(self, ci_params): ) assert results.bootstrap_results is not None - assert results.overall_se > 0 + assert results.se > 0 def test_bootstrap_estimation_methods(self, ci_params): """Test bootstrap with different estimation methods.""" @@ -2611,7 +2609,7 @@ def test_bootstrap_estimation_methods(self, ci_params): ) assert results.bootstrap_results is not None - assert results.overall_se > 0, f"Failed for method {method}" + assert results.se > 0, f"Failed for method {method}" def test_bootstrap_with_balanced_event_study(self, ci_params): """Test bootstrap with balanced event study aggregation.""" @@ -2763,9 +2761,9 @@ def test_single_cohort_basic(self): results = cs.fit(df, outcome="outcome", unit="unit", time="time", first_treat="first_treat") # Should produce valid results - assert results.overall_att is not None - assert np.isfinite(results.overall_att) - assert results.overall_se > 0 + assert results.att is not None + assert np.isfinite(results.att) + assert results.se > 0 # Should have effects for single group only groups = set(g for g, t in results.group_time_effects.keys()) @@ -2773,7 +2771,7 @@ def test_single_cohort_basic(self): assert treatment_period in groups # ATT should be roughly correct - assert abs(results.overall_att - 2.5) < 1.5 + assert abs(results.att - 2.5) < 1.5 def test_single_cohort_event_study(self): """Test event study aggregation with single cohort.""" @@ -2927,12 +2925,12 @@ def test_single_cohort_not_yet_treated_control(self): ) # Both should produce valid results - assert np.isfinite(results_never.overall_att) - assert np.isfinite(results_not_yet.overall_att) + assert np.isfinite(results_never.att) + assert np.isfinite(results_not_yet.att) # Results may differ slightly due to different comparison groups # but should be in similar range - assert abs(results_never.overall_att - results_not_yet.overall_att) < 1.0 + assert abs(results_never.att - results_not_yet.att) < 1.0 class TestCallawaySantAnnaAnalyticalSE: @@ -2964,18 +2962,15 @@ def test_analytical_se_vs_bootstrap_se(self, ci_params): ) # Point estimates should match exactly - assert abs(results_analytical.overall_att - results_bootstrap.overall_att) < 1e-10 + assert abs(results_analytical.att - results_bootstrap.att) < 1e-10 # SEs should be similar (within 15% with enough bootstrap iterations, # wider tolerance when min_n cap reduces iterations in pure Python mode) - rel_diff = ( - abs(results_analytical.overall_se - results_bootstrap.overall_se) - / results_bootstrap.overall_se - ) + rel_diff = abs(results_analytical.se - results_bootstrap.se) / results_bootstrap.se threshold = 0.40 if n_boot < 100 else 0.15 assert rel_diff < threshold, ( - f"Analytical SE ({results_analytical.overall_se:.4f}) differs from " - f"bootstrap SE ({results_bootstrap.overall_se:.4f}) by {rel_diff:.1%}" + f"Analytical SE ({results_analytical.se:.4f}) differs from " + f"bootstrap SE ({results_bootstrap.se:.4f}) by {rel_diff:.1%}" ) def test_analytical_se_accounts_for_covariance(self): @@ -3001,7 +2996,7 @@ def test_analytical_se_accounts_for_covariance(self): ) # The SE should be non-zero and positive - assert results.overall_se > 0 + assert results.se > 0 # Compute what the "independence" SE would be (sum of weighted variances) gt_effects = results.group_time_effects @@ -3023,7 +3018,7 @@ def test_analytical_se_accounts_for_covariance(self): # because covariances from shared control units are typically positive # Note: May not always be true but should be for typical staggered designs # We test that both are positive and finite - assert np.isfinite(results.overall_se) + assert np.isfinite(results.se) assert np.isfinite(independence_se) def test_analytical_se_single_gt_pair(self): @@ -3067,7 +3062,7 @@ def test_analytical_se_single_gt_pair(self): gt_key = list(results.group_time_effects.keys())[0] individual_se = results.group_time_effects[gt_key]["se"] # Should be close (may not be exact due to normalization) - assert abs(results.overall_se - individual_se) < individual_se * 0.01 + assert abs(results.se - individual_se) < individual_se * 0.01 def test_event_study_analytical_se(self, ci_params): """Event study SEs should also use influence function aggregation.""" @@ -3178,11 +3173,11 @@ def test_non_standard_first_treat_name(self): data, outcome="y", unit="id", time="period", first_treat="treatment_cohort" ) - assert results.overall_att is not None - assert np.isfinite(results.overall_att) - assert results.overall_se > 0 + assert results.att is not None + assert np.isfinite(results.att) + assert results.se > 0 # Treatment effect should be approximately 2.5 - assert abs(results.overall_att - 2.5) < 1.5 + assert abs(results.att - 2.5) < 1.5 def test_non_standard_all_column_names(self): """Test with all non-standard column names.""" @@ -3202,9 +3197,9 @@ def test_non_standard_all_column_names(self): first_treat="treatment_timing", ) - assert results.overall_att is not None - assert np.isfinite(results.overall_att) - assert results.overall_se > 0 + assert results.att is not None + assert np.isfinite(results.att) + assert results.se > 0 def test_non_standard_names_with_bootstrap(self, ci_params): """Test non-standard column names with bootstrap inference.""" @@ -3217,8 +3212,8 @@ def test_non_standard_names_with_bootstrap(self, ci_params): results = cs.fit(data, outcome="y", unit="id", time="period", first_treat="g") assert results.bootstrap_results is not None - assert results.overall_se > 0 - assert results.overall_conf_int[0] < results.overall_att < results.overall_conf_int[1] + assert results.se > 0 + assert results.conf_int[0] < results.att < results.conf_int[1] def test_non_standard_names_with_event_study(self): """Test non-standard column names with event study aggregation.""" @@ -3253,8 +3248,8 @@ def test_non_standard_names_with_covariates(self): covariates=["covariate_x", "covariate_z"], ) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 def test_non_standard_names_with_not_yet_treated(self): """Test non-standard column names with not_yet_treated control group.""" @@ -3263,7 +3258,7 @@ def test_non_standard_names_with_not_yet_treated(self): cs = CallawaySantAnna(control_group="not_yet_treated") results = cs.fit(data, outcome="y", unit="id", time="period", first_treat="adoption_period") - assert results.overall_att is not None + assert results.att is not None assert results.control_group == "not_yet_treated" def test_non_standard_names_matches_standard_names(self): @@ -3293,8 +3288,8 @@ def test_non_standard_names_matches_standard_names(self): results2 = cs2.fit(data_custom, outcome="y", unit="entity", time="t", first_treat="g") # Results should be identical - assert abs(results1.overall_att - results2.overall_att) < 1e-10 - assert abs(results1.overall_se - results2.overall_se) < 1e-10 + assert abs(results1.att - results2.att) < 1e-10 + assert abs(results1.se - results2.se) < 1e-10 def test_column_name_with_spaces(self): """Test column names containing spaces.""" @@ -3315,8 +3310,8 @@ def test_column_name_with_spaces(self): first_treat="treatment period", ) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 def test_column_name_with_special_characters(self): """Test column names with underscores and numbers.""" @@ -3332,7 +3327,7 @@ def test_column_name_with_special_characters(self): data, outcome="y", unit="id", time="period", first_treat="first_treat_2024" ) - assert results.overall_att is not None + assert results.att is not None class TestCallawaySantAnnaPreTreatment: @@ -3577,20 +3572,14 @@ def test_no_post_treatment_effects_returns_nan_with_warning(self): ), f"Expected warning about no post-treatment effects, got: {warning_messages}" # Overall ATT should be NaN - assert np.isnan(results.overall_att), ( + assert np.isnan(results.att), ( f"Expected NaN for overall_att when no post-treatment effects exist, " - f"got {results.overall_att}" + f"got {results.att}" ) # All inference fields should also be NaN - assert np.isnan( - results.overall_se - ), f"Expected NaN for overall_se, got {results.overall_se}" - assert np.isnan( - results.overall_t_stat - ), f"Expected NaN for overall_t_stat, got {results.overall_t_stat}" - assert np.isnan( - results.overall_p_value - ), f"Expected NaN for overall_p_value, got {results.overall_p_value}" + assert np.isnan(results.se), f"Expected NaN for overall_se, got {results.se}" + assert np.isnan(results.t_stat), f"Expected NaN for overall_t_stat, got {results.t_stat}" + assert np.isnan(results.p_value), f"Expected NaN for overall_p_value, got {results.p_value}" def test_no_post_treatment_effects_bootstrap_returns_nan(self, ci_params): """Bootstrap returns NaN inference when no post-treatment effects exist.""" @@ -3628,12 +3617,12 @@ def test_no_post_treatment_effects_bootstrap_returns_nan(self, ci_params): assert has_warning, f"Expected warning, got: {warning_messages}" # All overall inference fields should be NaN - assert np.isnan(results.overall_att), "overall_att should be NaN" - assert np.isnan(results.overall_se), "overall_se should be NaN" - assert np.isnan(results.overall_t_stat), "overall_t_stat should be NaN" - assert np.isnan(results.overall_p_value), "overall_p_value should be NaN" - assert np.isnan(results.overall_conf_int[0]), "CI lower should be NaN" - assert np.isnan(results.overall_conf_int[1]), "CI upper should be NaN" + assert np.isnan(results.att), "overall_att should be NaN" + assert np.isnan(results.se), "overall_se should be NaN" + assert np.isnan(results.t_stat), "overall_t_stat should be NaN" + assert np.isnan(results.p_value), "overall_p_value should be NaN" + assert np.isnan(results.conf_int[0]), "CI lower should be NaN" + assert np.isnan(results.conf_int[1]), "CI upper should be NaN" # Bootstrap results should also have NaN assert results.bootstrap_results is not None @@ -3684,8 +3673,8 @@ def test_bootstrap_runs_for_pretreatment_effects(self, ci_params): assert has_warning, "Expected warning about no post-treatment effects" # Verify overall ATT is NaN - assert np.isnan(results.overall_att), "overall_att should be NaN" - assert np.isnan(results.overall_se), "overall_se should be NaN" + assert np.isnan(results.att), "overall_att should be NaN" + assert np.isnan(results.se), "overall_se should be NaN" # Verify we have pre-treatment effects pre_treatment_effects = [(g, t) for (g, t) in results.group_time_effects.keys() if t < g] @@ -3952,10 +3941,8 @@ def test_invalid_se_produces_nan_tstat_overall(self, ci_params): ) # Overall t_stat should be NaN when SE is invalid - if np.isnan(results.overall_se) or results.overall_se == 0: - assert np.isnan( - results.overall_t_stat - ), "overall_t_stat should be NaN when SE is invalid" + if np.isnan(results.se) or results.se == 0: + assert np.isnan(results.t_stat), "overall_t_stat should be NaN when SE is invalid" def test_per_effect_tstat_consistency(self, ci_params): """Per-effect t_stat uses same NaN logic as overall t_stat. @@ -4394,7 +4381,7 @@ def test_nondefault_pscore_trim_ipw(self): first_treat="first_treat", covariates=["x"], ) - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) assert results.pscore_trim == 0.1 def test_nondefault_pscore_trim_dr(self): @@ -4428,7 +4415,7 @@ def test_nondefault_pscore_trim_dr(self): first_treat="first_treat", covariates=["x"], ) - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) assert results.pscore_trim == 0.1 @@ -4491,8 +4478,8 @@ def test_near_separation_warning_ipw(self): ] assert len(pscore_warns) > 0, "Expected propensity score warnings" # ATT should still be reasonable (not wildly inflated) - assert results.overall_att is not None - assert np.isfinite(results.overall_att) + assert results.att is not None + assert np.isfinite(results.att) def test_near_separation_att_not_inflated(self): """IRLS produces reasonable ATT even with near-separation covariates. @@ -4548,10 +4535,10 @@ def test_near_separation_att_not_inflated(self): ) # ATT should be in a reasonable range around the true effect - assert results.overall_att is not None + assert results.att is not None assert ( - abs(results.overall_att - true_effect) < 3.0 - ), f"ATT={results.overall_att} too far from true effect {true_effect}" + abs(results.att - true_effect) < 3.0 + ), f"ATT={results.att} too far from true effect {true_effect}" def test_dr_fallback_warning(self): """DR path emits warning when propensity estimation fails.""" @@ -4577,7 +4564,7 @@ def test_dr_fallback_warning(self): fallback_warns = [x for x in w if "unconditional propensity" in str(x.message)] assert len(fallback_warns) > 0, "Expected fallback warning in DR path" - assert results.overall_att is not None + assert results.att is not None def test_large_scale_covariate_stability(self): """IRLS handles large-scale covariates without wild ATT inflation. @@ -4630,12 +4617,12 @@ def test_large_scale_covariate_stability(self): covariates=["x_large"], ) - assert results.overall_att is not None - assert np.isfinite(results.overall_att) + assert results.att is not None + assert np.isfinite(results.att) # ATT should be in a plausible range assert ( - abs(results.overall_att - true_effect) < 5.0 - ), f"ATT={results.overall_att} too far from true effect {true_effect}" + abs(results.att - true_effect) < 5.0 + ), f"ATT={results.att} too far from true effect {true_effect}" class TestEPVDiagnostics: @@ -4770,7 +4757,7 @@ def test_cs_pscore_fallback_unconditional_opt_in(self): ) fallback_warns = [x for x in w if "unconditional propensity" in str(x.message)] assert len(fallback_warns) > 0 - assert results.overall_att is not None + assert results.att is not None def test_cs_diagnose_propensity(self): """diagnose_propensity() returns DataFrame with EPV per cohort.""" @@ -4936,7 +4923,7 @@ def test_cs_cached_rank_deficient_pscore_no_nan(self): assert np.isfinite( eff["effect"] ), f"ATT({g},{t}) is {eff['effect']} — NaN cache poisoning" - assert np.isfinite(results.overall_att) + assert np.isfinite(results.att) def test_cs_strict_mode_not_swallowed_by_unconditional_fallback(self): """rank_deficient_action='error' raises even with pscore_fallback='unconditional'. @@ -5380,12 +5367,12 @@ def test_cluster_robust_ses_differ_from_unit_level(self): first_treat="first_treat", ) - assert np.isfinite(res_unit.overall_se) and res_unit.overall_se > 0 - assert np.isfinite(res_cluster.overall_se) and res_cluster.overall_se > 0 - assert abs(res_unit.overall_se - res_cluster.overall_se) > 1e-6, ( - f"cluster=state SE ({res_cluster.overall_se:.6f}) is " + assert np.isfinite(res_unit.se) and res_unit.se > 0 + assert np.isfinite(res_cluster.se) and res_cluster.se > 0 + assert abs(res_unit.se - res_cluster.se) > 1e-6, ( + f"cluster=state SE ({res_cluster.se:.6f}) is " f"effectively identical to cluster=None SE " - f"({res_unit.overall_se:.6f}) — the cluster= parameter " + f"({res_unit.se:.6f}) — the cluster= parameter " "may not be wired through to the variance machinery." ) @@ -5474,7 +5461,7 @@ def test_cluster_none_path_unchanged(self): assert res.cluster_name is None assert res.n_clusters is None assert res.vcov_type == "hc1" - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 def test_invalid_cluster_column_raises(self): """cluster= raises ValueError with column name.""" @@ -5556,9 +5543,9 @@ def test_bare_cluster_works_with_panel_false_rcs(self): time="time", first_treat="first_treat", ) - assert np.isfinite(res_unit.overall_se) and res_unit.overall_se > 0 - assert np.isfinite(res_cluster.overall_se) and res_cluster.overall_se > 0 - assert abs(res_unit.overall_se - res_cluster.overall_se) > 1e-6, ( + assert np.isfinite(res_unit.se) and res_unit.se > 0 + assert np.isfinite(res_cluster.se) and res_cluster.se > 0 + assert abs(res_unit.se - res_cluster.se) > 1e-6, ( "RCS path: cluster=state SE not measurably different from " "cluster=None SE — cluster wiring may not reach the RCS code path." ) @@ -5640,7 +5627,7 @@ def test_fit_clone_idempotent_on_vcov_type(self): time="time", first_treat="first_treat", ) - assert res1.overall_se == pytest.approx(res2.overall_se, rel=0, abs=0) + assert res1.se == pytest.approx(res2.se, rel=0, abs=0) assert res1.vcov_type == res2.vcov_type == "hc1" assert res1.cluster_name == res2.cluster_name == "state" @@ -5878,12 +5865,12 @@ def test_bare_cluster_bootstrap_se_differs_from_unit_level(self): time="time", first_treat="first_treat", ) - assert np.isfinite(res_unit.overall_se) and res_unit.overall_se > 0 - assert np.isfinite(res_cluster.overall_se) and res_cluster.overall_se > 0 - assert abs(res_unit.overall_se - res_cluster.overall_se) > 1e-6, ( - f"Bootstrap path: cluster=state SE ({res_cluster.overall_se:.6f}) " + assert np.isfinite(res_unit.se) and res_unit.se > 0 + assert np.isfinite(res_cluster.se) and res_cluster.se > 0 + assert abs(res_unit.se - res_cluster.se) > 1e-6, ( + f"Bootstrap path: cluster=state SE ({res_cluster.se:.6f}) " f"is effectively identical to cluster=None SE " - f"({res_unit.overall_se:.6f}) — the cluster= parameter may " + f"({res_unit.se:.6f}) — the cluster= parameter may " "not be reaching the bootstrap multiplier-weights routing." ) @@ -6113,12 +6100,12 @@ def test_bare_cluster_bootstrap_propagates_nan_when_g_less_than_2(self): es = es_dict(res) # Overall bootstrap inference must be NaN-consistent - assert not np.isfinite(res.overall_se), ( - f"Bootstrap overall_se should be NaN under G=1 cluster, " f"got {res.overall_se}." + assert not np.isfinite(res.se), ( + f"Bootstrap overall_se should be NaN under G=1 cluster, " f"got {res.se}." ) - assert np.isnan(res.overall_t_stat) - assert np.isnan(res.overall_p_value) - assert np.isnan(res.overall_conf_int[0]) and np.isnan(res.overall_conf_int[1]) + assert np.isnan(res.t_stat) + assert np.isnan(res.p_value) + assert np.isnan(res.conf_int[0]) and np.isnan(res.conf_int[1]) # At least one (g, t) cell must have NaN inference (undefined # clustered variance propagating through either the bootstrap or @@ -6273,10 +6260,10 @@ def test_survey_design_psu_wins_under_bootstrap(self): ) # PSU wins under bootstrap too — SE must match the reference # (explicit-PSU-only) fit at the same seed - assert res_conflict.overall_se == pytest.approx(res_ref.overall_se, rel=0, abs=0), ( + assert res_conflict.se == pytest.approx(res_ref.se, rel=0, abs=0), ( f"Bootstrap precedence: with seed={cs_conflict.seed}, conflict " - f"fit SE ({res_conflict.overall_se}) must match explicit-PSU-only " - f"reference SE ({res_ref.overall_se}) — both bootstraps must " + f"fit SE ({res_conflict.se}) must match explicit-PSU-only " + f"reference SE ({res_ref.se}) — both bootstraps must " "draw at the same effective PSU level." ) diff --git a/tests/test_staggered_rc.py b/tests/test_staggered_rc.py index cb67a474e..c517b2c7f 100644 --- a/tests/test_staggered_rc.py +++ b/tests/test_staggered_rc.py @@ -56,9 +56,9 @@ def test_basic_reg(self, rc_data): "period", "first_treat", ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 @pytest.mark.parametrize("method", ["reg", "ipw", "dr"]) def test_all_methods(self, rc_data, method): @@ -69,9 +69,9 @@ def test_all_methods(self, rc_data, method): "period", "first_treat", ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_panel_param_in_get_params(self): cs = CallawaySantAnna(panel=False) @@ -97,7 +97,7 @@ def test_no_covariate_methods_agree(self, rc_data): "period", "first_treat", ) - results[method] = r.overall_att + results[method] = r.att np.testing.assert_allclose(results["reg"], results["ipw"], atol=1e-10) np.testing.assert_allclose(results["reg"], results["dr"], atol=1e-10) @@ -120,8 +120,8 @@ def test_positive_effect(self, rc_data): "first_treat", ) # DGP has treatment_effect=2.0 by default - assert result.overall_att > 0 - assert abs(result.overall_att - 2.0) < 2.0 # within 2 SE roughly + assert result.att > 0 + assert abs(result.att - 2.0) < 2.0 # within 2 SE roughly # ============================================================================= @@ -140,7 +140,7 @@ def test_aggregation(self, rc_data): "period", "first_treat", ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) # the fit never populates the fit-time surface; aggregation is post-fit assert result.event_study_effects is None assert result.group_effects is None @@ -171,9 +171,9 @@ def test_with_covariates(self, rc_data_with_covariates, method): "first_treat", covariates=["x1"], ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 # ============================================================================= @@ -194,8 +194,8 @@ def test_survey_weights_pweight(self, rc_data_with_survey): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_survey_covariates_dr(self, rc_data_with_survey): @@ -210,9 +210,9 @@ def test_survey_covariates_dr(self, rc_data_with_survey): covariates=["x1"], survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 # ============================================================================= @@ -233,7 +233,7 @@ def test_not_yet_treated(self, rc_data): "period", "first_treat", ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_never_treated(self, rc_data): result = CallawaySantAnna( @@ -245,7 +245,7 @@ def test_never_treated(self, rc_data): "period", "first_treat", ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) # ============================================================================= @@ -266,7 +266,7 @@ def test_universal(self, rc_data): "period", "first_treat", ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_varying(self, rc_data): result = CallawaySantAnna(estimation_method="reg", panel=False, base_period="varying").fit( @@ -276,7 +276,7 @@ def test_varying(self, rc_data): "period", "first_treat", ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) # ============================================================================= @@ -298,7 +298,7 @@ def test_bootstrap_reg(self, rc_data): "first_treat", ) assert result.bootstrap_results is not None - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) # ============================================================================= @@ -379,7 +379,7 @@ def test_dr_se_differs_from_reg_rc(self, rc_data_with_covariates): covariates=["x1"], ) # SEs should differ (DR has nuisance IF corrections) - assert r_reg.overall_se != r_dr.overall_se + assert r_reg.se != r_dr.se def test_panel_field_on_results(self, rc_data): """panel=False should be reflected on CallawaySantAnnaResults.""" @@ -435,12 +435,12 @@ def test_reg_se_matches_bootstrap(self, rc_data_with_covariates): covariates=["x1"], ) # ATTs should match (bootstrap doesn't change point estimate) - np.testing.assert_allclose(r_analytical.overall_att, r_bootstrap.overall_att, atol=1e-10) + np.testing.assert_allclose(r_analytical.att, r_bootstrap.att, atol=1e-10) # SEs should be within 10% (proves IF scaling is correct) - ratio = r_analytical.overall_se / r_bootstrap.overall_se + ratio = r_analytical.se / r_bootstrap.se assert 0.9 < ratio < 1.1, ( f"Analytical/bootstrap SE ratio {ratio:.3f} outside [0.9, 1.1] — " - f"analytical={r_analytical.overall_se:.4f}, bootstrap={r_bootstrap.overall_se:.4f}" + f"analytical={r_analytical.se:.4f}, bootstrap={r_bootstrap.se:.4f}" ) @@ -487,9 +487,9 @@ def test_finite_results_unequal(self, unequal_rc_data, method): "period", "first_treat", ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_dr_covariates_unequal(self, unequal_rc_data): """DR with covariates under unequal cohort counts.""" @@ -503,8 +503,8 @@ def test_dr_covariates_unequal(self, unequal_rc_data): "first_treat", covariates=["x1"], ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) def test_bootstrap_unequal(self, unequal_rc_data): """Bootstrap with unequal cohort counts.""" @@ -518,4 +518,4 @@ def test_bootstrap_unequal(self, unequal_rc_data): "first_treat", ) assert result.bootstrap_results is not None - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) diff --git a/tests/test_sun_abraham.py b/tests/test_sun_abraham.py index c6fbb4470..30c3ef82a 100644 --- a/tests/test_sun_abraham.py +++ b/tests/test_sun_abraham.py @@ -93,8 +93,8 @@ def test_basic_fit(self): assert sa.is_fitted_ assert isinstance(results, SunAbrahamResults) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 assert len(results.event_study_effects) > 0 def test_positive_treatment_effect(self): @@ -111,9 +111,9 @@ def test_positive_treatment_effect(self): ) # Should detect positive effect - assert results.overall_att > 0 + assert results.att > 0 # Effect should be roughly correct (within reasonable bounds) - assert abs(results.overall_att - 3.0) < 2 * results.overall_se + 1.5 + assert abs(results.att - 3.0) < 2 * results.se + 1.5 def test_zero_treatment_effect(self): """Test with no treatment effect.""" @@ -129,7 +129,7 @@ def test_zero_treatment_effect(self): ) # Effect should be close to zero - assert abs(results.overall_att) < 3 * results.overall_se + 0.5 + assert abs(results.att) < 3 * results.se + 0.5 def test_event_study_effects(self): """Test event study effects structure.""" @@ -206,8 +206,8 @@ def test_control_group_options(self): assert results2.control_group == "not_yet_treated" # Results may be different # (they don't have to be for this test to pass) - assert results1.overall_att is not None - assert results2.overall_att is not None + assert results1.att is not None + assert results2.att is not None def test_summary_output(self): """Test summary output formatting.""" @@ -363,8 +363,8 @@ def test_bootstrap_basic(self, ci_params): assert results.bootstrap_results is not None assert results.bootstrap_results.n_bootstrap == n_boot assert results.bootstrap_results.weight_type == "pairs" - assert results.overall_se > 0 - assert results.overall_conf_int[0] < results.overall_att < results.overall_conf_int[1] + assert results.se > 0 + assert results.conf_int[0] < results.att < results.conf_int[1] def test_bootstrap_reproducibility(self, ci_params): """Test that bootstrap is reproducible with same seed.""" @@ -390,8 +390,8 @@ def test_bootstrap_reproducibility(self, ci_params): ) # Results should be identical with same seed - assert results1.overall_se == results2.overall_se - assert results1.overall_conf_int == results2.overall_conf_int + assert results1.se == results2.se + assert results1.conf_int == results2.conf_int def test_bootstrap_different_seeds(self, ci_params): """Test that different seeds give different results.""" @@ -417,7 +417,7 @@ def test_bootstrap_different_seeds(self, ci_params): ) # Results should differ with different seeds - assert results1.overall_se != results2.overall_se + assert results1.se != results2.se def test_bootstrap_p_value_significance(self, ci_params): """Test that strong effect has significant p-value with bootstrap.""" @@ -434,7 +434,7 @@ def test_bootstrap_p_value_significance(self, ci_params): ) # Strong effect should be significant - assert results.overall_p_value < 0.05 + assert results.p_value < 0.05 assert results.is_significant def test_bootstrap_distribution_stored(self, ci_params): @@ -524,12 +524,12 @@ def test_both_recover_treatment_effect(self): ) # Both should detect positive effect - assert sa_results.overall_att > 0 - assert cs_results.overall_att > 0 + assert sa_results.att > 0 + assert cs_results.att > 0 # Both should be reasonably close to true effect - assert abs(sa_results.overall_att - 3.0) < 2.0 - assert abs(cs_results.overall_att - 3.0) < 2.0 + assert abs(sa_results.att - 3.0) < 2.0 + assert abs(cs_results.att - 3.0) < 2.0 def test_pre_period_difference_expected_between_cs_sa(self): """Pre-periods differ between CS (varying) and SA; post-periods match. @@ -688,11 +688,10 @@ def test_agreement_under_homogeneous_effects(self): # Under homogeneous effects, SA and CS should give similar results # Allow for some sampling variation - diff = abs(sa_results.overall_att - cs_results.overall_att) - max_se = max(sa_results.overall_se, cs_results.overall_se) + diff = abs(sa_results.att - cs_results.att) + max_se = max(sa_results.se, cs_results.se) assert diff < 3 * max_se, ( - f"SA ATT={sa_results.overall_att:.3f}, " - f"CS ATT={cs_results.overall_att:.3f}, diff={diff:.3f}" + f"SA ATT={sa_results.att:.3f}, " f"CS ATT={cs_results.att:.3f}, diff={diff:.3f}" ) @@ -736,7 +735,7 @@ def test_single_cohort(self): first_treat="first_treat", ) - assert results.overall_att is not None + assert results.att is not None assert len(results.groups) == 1 def test_many_cohorts(self): @@ -752,7 +751,7 @@ def test_many_cohorts(self): first_treat="first_treat", ) - assert results.overall_att is not None + assert results.att is not None assert len(results.groups) > 1 def test_unbalanced_panel(self): @@ -773,7 +772,7 @@ def test_unbalanced_panel(self): first_treat="first_treat", ) - assert results.overall_att is not None + assert results.att is not None def test_anticipation_periods(self): """Test with anticipation periods.""" @@ -788,7 +787,7 @@ def test_anticipation_periods(self): first_treat="first_treat", ) - assert results.overall_att is not None + assert results.att is not None assert sa.anticipation == 1 def test_with_covariates(self): @@ -810,8 +809,8 @@ def test_with_covariates(self): covariates=["covariate1", "covariate2"], ) - assert results.overall_att is not None - assert results.overall_se > 0 + assert results.att is not None + assert results.se > 0 def test_cohort_level_dataframe(self): """Test cohort-level DataFrame output.""" @@ -887,7 +886,7 @@ def test_rank_deficient_action_silent_no_warning(self): # Should still get valid results assert results is not None - assert results.overall_att is not None + assert results.att is not None class TestSunAbrahamTStatNaN: @@ -945,21 +944,21 @@ def test_overall_tstat_nan_when_se_invalid(self): first_treat="first_treat", ) - se = results.overall_se - t_stat = results.overall_t_stat + se = results.se + t_stat = results.t_stat if not np.isfinite(se) or se == 0: assert np.isnan(t_stat), f"overall_t_stat should be NaN when SE={se}, got {t_stat}" - assert np.isnan(results.overall_p_value), ( + assert np.isnan(results.p_value), ( f"overall_p_value should be NaN when SE={se} (analytical inference), " - f"got {results.overall_p_value}" + f"got {results.p_value}" ) - ci = results.overall_conf_int + ci = results.conf_int assert np.isnan(ci[0]) and np.isnan( ci[1] ), f"overall_conf_int should be (NaN, NaN) when SE={se}, got {ci}" else: - expected = results.overall_att / se + expected = results.att / se assert np.isclose( t_stat, expected ), f"overall_t_stat should be ATT/SE, expected {expected}, got {t_stat}" @@ -985,8 +984,8 @@ def test_bootstrap_tstat_nan_when_se_invalid(self, ci_params): ) # Check overall - se = results.overall_se - t_stat = results.overall_t_stat + se = results.se + t_stat = results.t_stat if not np.isfinite(se) or se == 0: assert np.isnan( @@ -1104,18 +1103,14 @@ def test_no_post_effects_returns_nan(self): ) # Overall ATT and SE should be NaN - assert np.isnan(results.overall_att), f"Expected NaN overall_att, got {results.overall_att}" - assert np.isnan(results.overall_se), f"Expected NaN overall_se, got {results.overall_se}" + assert np.isnan(results.att), f"Expected NaN overall_att, got {results.att}" + assert np.isnan(results.se), f"Expected NaN overall_se, got {results.se}" # Downstream inference should propagate NaN - assert np.isnan( - results.overall_t_stat - ), f"Expected NaN overall_t_stat, got {results.overall_t_stat}" - assert np.isnan( - results.overall_p_value - ), f"Expected NaN overall_p_value, got {results.overall_p_value}" - assert np.isnan(results.overall_conf_int[0]) and np.isnan( - results.overall_conf_int[1] - ), f"Expected (NaN, NaN) overall_conf_int, got {results.overall_conf_int}" + assert np.isnan(results.t_stat), f"Expected NaN overall_t_stat, got {results.t_stat}" + assert np.isnan(results.p_value), f"Expected NaN overall_p_value, got {results.p_value}" + assert np.isnan(results.conf_int[0]) and np.isnan( + results.conf_int[1] + ), f"Expected (NaN, NaN) overall_conf_int, got {results.conf_int}" def test_no_post_effects_bootstrap_returns_nan(self, ci_params): """Test that no post-treatment effects returns NaN even with bootstrap. @@ -1160,17 +1155,15 @@ def test_no_post_effects_bootstrap_returns_nan(self, ci_params): ) # All overall inference fields should be NaN - assert np.isnan(results.overall_att), f"Expected NaN overall_att, got {results.overall_att}" - assert np.isnan(results.overall_se), f"Expected NaN overall_se, got {results.overall_se}" + assert np.isnan(results.att), f"Expected NaN overall_att, got {results.att}" + assert np.isnan(results.se), f"Expected NaN overall_se, got {results.se}" + assert np.isnan(results.t_stat), f"Expected NaN overall_t_stat, got {results.t_stat}" assert np.isnan( - results.overall_t_stat - ), f"Expected NaN overall_t_stat, got {results.overall_t_stat}" - assert np.isnan( - results.overall_p_value - ), f"Expected NaN overall_p_value with bootstrap, got {results.overall_p_value}" - assert np.isnan(results.overall_conf_int[0]) and np.isnan( - results.overall_conf_int[1] - ), f"Expected (NaN, NaN) overall_conf_int, got {results.overall_conf_int}" + results.p_value + ), f"Expected NaN overall_p_value with bootstrap, got {results.p_value}" + assert np.isnan(results.conf_int[0]) and np.isnan( + results.conf_int[1] + ), f"Expected (NaN, NaN) overall_conf_int, got {results.conf_int}" def test_event_time_no_truncation(self): """Test that event times beyond ±20 are estimated (Step 5d). @@ -1318,9 +1311,7 @@ def patched_compute_overall_att( ), "Expected warning about simplified variance fallback" # The result should still have a positive SE (simplified variance) - assert ( - results.overall_se > 0 - ), f"Expected positive SE from fallback, got {results.overall_se}" + assert results.se > 0, f"Expected positive SE from fallback, got {results.se}" def test_iw_weights_match_cohort_shares(self): """Test that IW weights match event-time sample shares. @@ -1437,11 +1428,11 @@ def test_never_treated_inf_encoding(self): # Results should be identical to first_treat=0 encoding assert np.isclose( - results_inf.overall_att, results_zero.overall_att - ), f"ATT differs: inf={results_inf.overall_att}, zero={results_zero.overall_att}" + results_inf.att, results_zero.att + ), f"ATT differs: inf={results_inf.att}, zero={results_zero.att}" assert np.isclose( - results_inf.overall_se, results_zero.overall_se - ), f"SE differs: inf={results_inf.overall_se}, zero={results_zero.overall_se}" + results_inf.se, results_zero.se + ), f"SE differs: inf={results_inf.se}, zero={results_zero.se}" def test_removed_params_raise_typeerror(self): """Removed min_pre_periods/min_post_periods raise TypeError.""" @@ -1494,21 +1485,21 @@ def test_default_matches_hc1_explicit(self): res_explicit = SunAbraham(vcov_type="hc1").fit(data, **kwargs) # Both paths execute the same code under the hood (default vcov_type # is "hc1"); any divergence is float64 representation noise. - assert np.isclose(res_default.overall_att, res_explicit.overall_att, atol=1e-14) - assert np.isclose(res_default.overall_se, res_explicit.overall_se, atol=1e-14) - assert np.isclose(res_default.overall_p_value, res_explicit.overall_p_value, atol=1e-12) + assert np.isclose(res_default.att, res_explicit.att, atol=1e-14) + assert np.isclose(res_default.se, res_explicit.se, atol=1e-14) + assert np.isclose(res_default.p_value, res_explicit.p_value, atol=1e-12) def test_classical_finite(self): """SA(vcov_type='classical') produces finite ATT/SE; drops auto-cluster.""" data = self._panel() kwargs = dict(outcome="outcome", unit="unit", time="time", first_treat="first_treat") res = SunAbraham(vcov_type="classical").fit(data, **kwargs) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) - assert np.isfinite(res.overall_p_value) + assert np.isfinite(res.att) + assert np.isfinite(res.se) + assert np.isfinite(res.p_value) # Classical differs from HC1 cluster-at-unit in heteroscedastic / panel data res_hc1 = SunAbraham(vcov_type="hc1").fit(data, **kwargs) - assert res.overall_se != res_hc1.overall_se + assert res.se != res_hc1.se def test_hc2_finite_no_auto_cluster(self): """SA(vcov_type='hc2') runs without explicit cluster (linalg validator @@ -1516,22 +1507,22 @@ def test_hc2_finite_no_auto_cluster(self): data = self._panel() kwargs = dict(outcome="outcome", unit="unit", time="time", first_treat="first_treat") res = SunAbraham(vcov_type="hc2").fit(data, **kwargs) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) # HC2 differs from cluster-at-unit HC1 on this panel res_hc1 = SunAbraham(vcov_type="hc1").fit(data, **kwargs) - assert res.overall_se != res_hc1.overall_se + assert res.se != res_hc1.se def test_hc2_bm_finite_with_auto_cluster(self): """SA(vcov_type='hc2_bm') auto-clusters at unit by default; routes to CR2-BM.""" data = self._panel() kwargs = dict(outcome="outcome", unit="unit", time="time", first_treat="first_treat") res = SunAbraham(vcov_type="hc2_bm").fit(data, **kwargs) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) # HC2-BM applies BM Satterthwaite DOF; should differ from cluster-at-unit HC1 res_hc1 = SunAbraham(vcov_type="hc1").fit(data, **kwargs) - assert res.overall_se != res_hc1.overall_se + assert res.se != res_hc1.se def test_hc2_bm_nan_contrast_dof_regression_pin(self, monkeypatch): """NaN BM contrast DOF -> all-NaN row inference + NaN df provenance. @@ -1604,11 +1595,11 @@ def test_hc2_bm_explicit_cluster_works(self): data["firm"] = (data["unit"] // 5).astype(int) kwargs = dict(outcome="outcome", unit="unit", time="time", first_treat="first_treat") res = SunAbraham(cluster="firm", vcov_type="hc2_bm").fit(data, **kwargs) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) # Differs from cluster-at-unit (different cluster grouping) res_unit = SunAbraham(vcov_type="hc2_bm").fit(data, **kwargs) - assert res.overall_se != res_unit.overall_se + assert res.se != res_unit.se def test_hc2_rejects_explicit_cluster(self): """SA(cluster=..., vcov_type='hc2') is rejected by the linalg validator.""" @@ -1676,9 +1667,9 @@ def test_hc1_replicate_weights_path_unchanged(self): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) # Replicate path → bootstrap-style SE; should be finite - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) def test_get_params_includes_vcov_type(self): """get_params() returns dict with the new 'vcov_type' key.""" @@ -1713,8 +1704,8 @@ def test_clone_repeat_fit_idempotent(self): sa2 = SunAbraham() sa2.set_params(**sa1.get_params()) res2 = sa2.fit(data, **kwargs) - assert res1.overall_att == res2.overall_att - assert res1.overall_se == res2.overall_se + assert res1.att == res2.att + assert res1.se == res2.se assert sa2.vcov_type == "hc2_bm" assert sa2._vcov_type_explicit is True @@ -1781,8 +1772,8 @@ def test_hc1_survey_design_preserves_unit_psu_injection(self): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) # Survey metadata should record unit-level PSUs (n_psu == n_units), # NOT per-observation PSUs (n_psu == n_obs). On the panel: 40 # units × 8 periods = 320 obs. @@ -1831,8 +1822,8 @@ def test_hc2_bm_routes_through_full_dummy_path(self): res_hc2 = SunAbraham(vcov_type="hc2").fit(data, **kwargs) res_hc2_bm = SunAbraham(vcov_type="hc2_bm").fit(data, **kwargs) # ATT should match across all vcov families (FWL) - assert np.isclose(res_hc1.overall_att, res_hc2.overall_att, atol=1e-12) - assert np.isclose(res_hc1.overall_att, res_hc2_bm.overall_att, atol=1e-12) + assert np.isclose(res_hc1.att, res_hc2.att, atol=1e-12) + assert np.isclose(res_hc1.att, res_hc2_bm.att, atol=1e-12) def test_hc2_robustness_value_differs_from_within_transform(self): """Confirms hc2 routes through full-dummy: the SE differs from a manual @@ -1905,9 +1896,9 @@ def test_point_estimates_invariant_across_vcov_type_with_covariates(self): results[vt] = SunAbraham(vcov_type=vt).fit(data, **kwargs) # Overall ATT and event_study effects should match across vcov_type for vt in ("classical", "hc2", "hc2_bm"): - assert np.isclose(results["hc1"].overall_att, results[vt].overall_att, atol=1e-10), ( - f"overall_att diverges between hc1 ({results['hc1'].overall_att}) " - f"and {vt} ({results[vt].overall_att}); covariate threading " + assert np.isclose(results["hc1"].att, results[vt].att, atol=1e-10), ( + f"overall_att diverges between hc1 ({results['hc1'].att}) " + f"and {vt} ({results[vt].att}); covariate threading " "regression in the full-dummy design-builder." ) for e in results["hc1"].event_study_effects: @@ -1934,9 +1925,9 @@ def test_point_estimates_invariant_across_vcov_type_not_yet_treated(self): data, **kwargs ) for vt in ("classical", "hc2", "hc2_bm"): - assert np.isclose(results["hc1"].overall_att, results[vt].overall_att, atol=1e-10), ( - f"overall_att diverges between hc1 ({results['hc1'].overall_att}) " - f"and {vt} ({results[vt].overall_att}) under " + assert np.isclose(results["hc1"].att, results[vt].att, atol=1e-10), ( + f"overall_att diverges between hc1 ({results['hc1'].att}) " + f"and {vt} ({results[vt].att}) under " "control_group='not_yet_treated'; sample-composition regression " "in the full-dummy branch." ) @@ -2019,17 +2010,15 @@ def test_bootstrap_finite_and_point_estimates_invariant(self, ci_params): results[vt] = SunAbraham(vcov_type=vt, n_bootstrap=n_boot, seed=42).fit(data, **kwargs) # Bootstrap outputs must be finite for each family assert np.isfinite( - results[vt].overall_att + results[vt].att ), f"overall_att non-finite for vcov_type={vt} under bootstrap" - assert np.isfinite( - results[vt].overall_se - ), f"bootstrap SE non-finite for vcov_type={vt}" + assert np.isfinite(results[vt].se), f"bootstrap SE non-finite for vcov_type={vt}" # Point estimates must match across vcov_type (FWL preserves them # across within-transform vs full-dummy refits). for vt in ("classical", "hc2", "hc2_bm"): - assert np.isclose(results["hc1"].overall_att, results[vt].overall_att, atol=1e-10), ( - f"Bootstrap overall_att diverges: hc1={results['hc1'].overall_att}, " - f"{vt}={results[vt].overall_att}. Bootstrap refits in the new " + assert np.isclose(results["hc1"].att, results[vt].att, atol=1e-10), ( + f"Bootstrap overall_att diverges: hc1={results['hc1'].att}, " + f"{vt}={results[vt].att}. Bootstrap refits in the new " "full-dummy branch are not FWL-invariant." ) @@ -2108,13 +2097,11 @@ def _panel(seed=3): def test_residual_default_is_t_not_z(self): """The D4/defect fix: aggregates use t(saturated residual df), not z.""" res = SunAbraham().fit(self._panel(), **self._kw) - p_z = 2 * stats.norm.sf(abs(res.overall_t_stat)) - assert res.overall_p_value != p_z + p_z = 2 * stats.norm.sf(abs(res.t_stat)) + assert res.p_value != p_z dfres = res.inference_df assert dfres is not None and dfres > 0 - assert res.overall_p_value == pytest.approx( - 2 * stats.t.sf(abs(res.overall_t_stat), dfres), rel=1e-12 - ) + assert res.p_value == pytest.approx(2 * stats.t.sf(abs(res.t_stat), dfres), rel=1e-12) # every ES row shares the same finite residual df (one df source) assert set(res.event_study_df.values()) == {dfres} @@ -2123,15 +2110,13 @@ def test_cluster_matches_hand_reconstructed_g_minus_1(self): r0 = SunAbraham().fit(data, **self._kw) r1 = SunAbraham(df_convention="cluster").fit(data, **self._kw) G = data["unit"].nunique() - assert r1.overall_att == r0.overall_att and r1.overall_se == r0.overall_se - assert r1.overall_t_stat == r0.overall_t_stat - assert r1.overall_p_value == pytest.approx( - 2 * stats.t.sf(abs(r1.overall_t_stat), G - 1), rel=1e-12 - ) - lo, hi = r1.overall_conf_int + assert r1.att == r0.att and r1.se == r0.se + assert r1.t_stat == r0.t_stat + assert r1.p_value == pytest.approx(2 * stats.t.sf(abs(r1.t_stat), G - 1), rel=1e-12) + lo, hi = r1.conf_int crit = stats.t.ppf(0.975, G - 1) - assert lo == pytest.approx(r1.overall_att - crit * r1.overall_se, rel=1e-12) - assert hi == pytest.approx(r1.overall_att + crit * r1.overall_se, rel=1e-12) + assert lo == pytest.approx(r1.att - crit * r1.se, rel=1e-12) + assert hi == pytest.approx(r1.att + crit * r1.se, rel=1e-12) assert r1.inference_df == G - 1 assert set(r1.event_study_df.values()) == {float(G - 1)} @@ -2140,10 +2125,8 @@ def test_normal_reproduces_z_on_aggregates(self): data = self._panel() r0 = SunAbraham().fit(data, **self._kw) rn = SunAbraham(df_convention="normal").fit(data, **self._kw) - assert rn.overall_att == r0.overall_att and rn.overall_se == r0.overall_se - assert rn.overall_p_value == pytest.approx( - 2 * stats.norm.sf(abs(rn.overall_t_stat)), rel=1e-14 - ) + assert rn.att == r0.att and rn.se == r0.se + assert rn.p_value == pytest.approx(2 * stats.norm.sf(abs(rn.t_stat)), rel=1e-14) assert rn.inference_df is None # provenance: z rows record NaN assert all(np.isnan(v) for v in rn.event_study_df.values()) @@ -2153,8 +2136,8 @@ def test_bm_dof_precedence_over_knob(self): data = self._panel() r_res = SunAbraham(vcov_type="hc2_bm").fit(data, **self._kw) r_clu = SunAbraham(vcov_type="hc2_bm", df_convention="cluster").fit(data, **self._kw) - assert r_res.overall_p_value == r_clu.overall_p_value - assert r_res.overall_conf_int == r_clu.overall_conf_int + assert r_res.p_value == r_clu.p_value + assert r_res.conf_int == r_clu.conf_int def test_survey_df_precedence_over_knob(self): """Survey design df wins under EVERY knob value: the three fits are @@ -2174,8 +2157,8 @@ def test_survey_df_precedence_over_knob(self): assert base.inference_df is not None and np.isfinite(base.inference_df) for conv in ("cluster", "normal"): r = fits[conv] - assert r.overall_p_value == base.overall_p_value - assert r.overall_conf_int == base.overall_conf_int + assert r.p_value == base.p_value + assert r.conf_int == base.conf_int assert r.inference_df == base.inference_df assert r.event_study_df == base.event_study_df @@ -2205,9 +2188,9 @@ def test_get_params_roundtrip_and_repeat_fit(self): clone = SunAbraham(**est.get_params()) data = self._panel() r1 = clone.fit(data, **self._kw) - p1, d1 = r1.overall_p_value, r1.inference_df + p1, d1 = r1.p_value, r1.inference_df r2 = clone.fit(data, **self._kw) - assert r2.overall_p_value == p1 and r2.inference_df == d1 + assert r2.p_value == p1 and r2.inference_df == d1 assert clone.df_convention == "cluster" diff --git a/tests/test_survey_dcdh.py b/tests/test_survey_dcdh.py index ab668d5f2..44f55f166 100644 --- a/tests/test_survey_dcdh.py +++ b/tests/test_survey_dcdh.py @@ -56,8 +56,8 @@ def test_no_survey_unchanged(self, base_data): treatment="treatment", survey_design=None, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) or result.overall_se != result.overall_se + assert np.isfinite(result.att) + assert np.isfinite(result.se) or result.se != result.se assert result.survey_metadata is None @@ -88,7 +88,7 @@ def test_uniform_weights_match_unweighted(self, base_data): survey_design=sd, ) # Point estimates should match exactly (uniform weights = unweighted mean) - assert result_plain.overall_att == pytest.approx(result_survey.overall_att, abs=1e-10) + assert result_plain.att == pytest.approx(result_survey.att, abs=1e-10) # ── Test: Non-uniform weights change estimate ─────────────────────── @@ -144,7 +144,7 @@ def test_nonuniform_weights_change_att(self, base_data): # weight). The ATTs should match. This confirms the equal-cell # contract: survey weights don't change the cross-group aggregation. # The SE will differ because the survey variance accounts for design. - assert result_plain.overall_att == pytest.approx(result_survey.overall_att, abs=1e-8) + assert result_plain.att == pytest.approx(result_survey.att, abs=1e-8) # But the SEs should differ when strata/PSU are present # (tested separately in TestSurveySE) @@ -177,8 +177,8 @@ def test_weight_scale_invariance(self, data_with_survey): treatment="treatment", survey_design=sd2, ) - assert r1.overall_att == pytest.approx(r2.overall_att, abs=1e-10) - assert r1.overall_se == pytest.approx(r2.overall_se, rel=1e-6) + assert r1.att == pytest.approx(r2.att, abs=1e-10) + assert r1.se == pytest.approx(r2.se, rel=1e-6) # ── Test: Survey SE differs from analytical SE ────────────────────── @@ -208,10 +208,10 @@ def test_strata_psu_changes_se(self, data_with_survey): survey_design=sd_full, ) # Point estimates should match (same weights) - assert r_w.overall_att == pytest.approx(r_full.overall_att, abs=1e-10) + assert r_w.att == pytest.approx(r_full.att, abs=1e-10) # SEs should differ (strata/PSU affects variance) - if np.isfinite(r_w.overall_se) and np.isfinite(r_full.overall_se): - assert r_w.overall_se != pytest.approx(r_full.overall_se, rel=0.01) + if np.isfinite(r_w.se) and np.isfinite(r_full.se): + assert r_w.se != pytest.approx(r_full.se, rel=0.01) def test_survey_metadata_populated(self, data_with_survey): sd = SurveyDesign(weights="pw", strata="stratum", psu="cluster", nest=True) @@ -279,7 +279,7 @@ def test_varying_weights_within_group_accepted(self, base_data): treatment="treatment", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_varying_weights_change_att(self, base_data): """With multi-obs cells and varying weights, ATT differs from unweighted. @@ -314,7 +314,7 @@ def test_varying_weights_change_att(self, base_data): ) # Weighted cell means with time-varying noise produce different # first differences -> different ATT - assert result_plain.overall_att != pytest.approx(result_survey.overall_att, abs=0.01) + assert result_plain.att != pytest.approx(result_survey.att, abs=0.01) def test_rejects_replicate_weights_with_bootstrap(self, base_data): """Replicate weights combined with n_bootstrap > 0 is rejected. @@ -440,10 +440,10 @@ def test_uniform_survey_se_matches_plugin(self, base_data): # With PSU=group and uniform weights, survey SE should be # close to plug-in SE (both assume group-level independence). # Small-sample corrections (n/(n-1)) cause minor differences. - if np.isfinite(r_plain.overall_se) and np.isfinite(r_survey.overall_se): - assert r_plain.overall_se == pytest.approx(r_survey.overall_se, rel=0.15), ( - f"Survey SE ({r_survey.overall_se:.6f}) should be close to " - f"plug-in SE ({r_plain.overall_se:.6f}) with uniform weights " + if np.isfinite(r_plain.se) and np.isfinite(r_survey.se): + assert r_plain.se == pytest.approx(r_survey.se, rel=0.15), ( + f"Survey SE ({r_survey.se:.6f}) should be close to " + f"plug-in SE ({r_plain.se:.6f}) with uniform weights " f"and PSU=group" ) @@ -473,7 +473,7 @@ def test_zero_weight_cell_excluded(self, base_data): treatment="treatment", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) # ── Test: Delta overall surface threads survey df ─────────────────── @@ -497,17 +497,17 @@ def test_survey_delta_uses_survey_df(self, data_with_survey): L_max=2, survey_design=sd, ) - if not (np.isfinite(r.overall_se) and r.overall_se > 0): + if not (np.isfinite(r.se) and r.se > 0): pytest.skip("delta not estimable on this fixture") assert r.survey_metadata is not None df_s = r.survey_metadata.df_survey assert df_s is not None and df_s > 0, f"expected positive df_survey, got {df_s}" - t_stat = r.overall_att / r.overall_se + t_stat = r.att / r.se p_t = 2.0 * (1.0 - stats.t.cdf(abs(t_stat), df=df_s)) # Reported p-value must match t-based (proving _df_survey was threaded) - assert r.overall_p_value == pytest.approx(p_t, abs=1e-10) + assert r.p_value == pytest.approx(p_t, abs=1e-10) def test_survey_delta_t_differs_from_z(self, base_data): """With a small-df design (df~4), survey-t p-value must differ @@ -533,7 +533,7 @@ def test_survey_delta_t_differs_from_z(self, base_data): L_max=2, survey_design=sd, ) - if not (np.isfinite(r.overall_se) and r.overall_se > 0): + if not (np.isfinite(r.se) and r.se > 0): pytest.skip("delta not estimable on this fixture") assert r.survey_metadata is not None df_s = r.survey_metadata.df_survey @@ -541,13 +541,13 @@ def test_survey_delta_t_differs_from_z(self, base_data): df_s is not None and df_s < 30 ), f"expected small df_survey for t-vs-z gap, got {df_s}" - t_stat = r.overall_att / r.overall_se + t_stat = r.att / r.se p_t = 2.0 * (1.0 - stats.t.cdf(abs(t_stat), df=df_s)) p_z = 2.0 * (1.0 - stats.norm.cdf(abs(t_stat))) # Threaded p-value must match t, not z - assert r.overall_p_value == pytest.approx(p_t, abs=1e-10) + assert r.p_value == pytest.approx(p_t, abs=1e-10) assert ( - abs(r.overall_p_value - p_z) > 1e-6 + abs(r.p_value - p_z) > 1e-6 ), "overall_p_value must differ from z-inference when df_survey is small" @@ -573,7 +573,7 @@ def test_survey_plus_controls_runs(self, data_with_survey): L_max=1, survey_design=sd, ) - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) assert r.survey_metadata is not None @@ -906,7 +906,7 @@ def test_mixed_zero_weight_row_excluded_from_validation(self, base_data): treatment="treatment", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_zero_weight_row_with_nan_outcome(self, base_data): """A zero-weight row with NaN outcome must not trip the outcome @@ -927,7 +927,7 @@ def test_zero_weight_row_with_nan_outcome(self, base_data): treatment="treatment", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_zero_weight_row_with_nan_group_id(self, base_data): """A zero-weight row with NaN group id must not crash the SE @@ -950,7 +950,7 @@ def test_zero_weight_row_with_nan_group_id(self, base_data): treatment="treatment", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_zero_weight_row_with_nan_control(self, base_data): """A zero-weight row with NaN in a control column must not abort @@ -976,7 +976,7 @@ def test_zero_weight_row_with_nan_control(self, base_data): covariates=["x"], survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_zero_weight_row_with_nan_heterogeneity(self, base_data): """A zero-weight row with NaN in the heterogeneity column must @@ -1157,8 +1157,8 @@ def test_accepts_varying_psu_within_group(self, base_data): survey_design=sd, L_max=2, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) # And the SE differs from a within-group-constant-PSU baseline, # because the cell allocator now honors the extra PSU structure. df_const = base_data.copy() @@ -1175,7 +1175,7 @@ def test_accepts_varying_psu_within_group(self, base_data): survey_design=sd_const, L_max=2, ) - assert result.overall_se != r_const.overall_se, ( + assert result.se != r_const.se, ( "Cell-period allocator must produce a different SE when PSU " "actually varies across cells vs. constant-within-group." ) @@ -1202,8 +1202,8 @@ def test_accepts_varying_strata_within_group(self, base_data): survey_design=sd, L_max=2, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) def test_heterogeneity_with_varying_psu_succeeds(self, base_data): """heterogeneity= is supported under within-group-varying PSU @@ -1317,10 +1317,10 @@ def test_auto_inject_with_varying_strata_nest_true_succeeds(self, base_data): survey_design=sd_explicit, L_max=2, ) - assert np.isfinite(r_auto.overall_att) - assert np.isfinite(r_auto.overall_se) - if np.isfinite(r_auto.overall_se) and np.isfinite(r_explicit.overall_se): - assert r_auto.overall_se == pytest.approx(r_explicit.overall_se, rel=1e-6) + assert np.isfinite(r_auto.att) + assert np.isfinite(r_auto.se) + if np.isfinite(r_auto.se) and np.isfinite(r_explicit.se): + assert r_auto.se == pytest.approx(r_explicit.se, rel=1e-6) assert r_auto.survey_metadata is not None assert r_explicit.survey_metadata is not None assert r_auto.survey_metadata.df_survey == r_explicit.survey_metadata.df_survey @@ -1471,7 +1471,7 @@ def test_accepts_varying_weights_within_group(self, base_data): treatment="treatment", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_auto_inject_psu_matches_explicit_group_psu(self, base_data): """SurveyDesign(weights='pw') (no PSU) must yield the same SE and @@ -1495,9 +1495,9 @@ def test_auto_inject_psu_matches_explicit_group_psu(self, base_data): treatment="treatment", survey_design=SurveyDesign(weights="pw", psu="group"), ) - assert r_no_psu.overall_att == pytest.approx(r_explicit.overall_att, abs=1e-10) - if np.isfinite(r_no_psu.overall_se) and np.isfinite(r_explicit.overall_se): - assert r_no_psu.overall_se == pytest.approx(r_explicit.overall_se, rel=1e-6) + assert r_no_psu.att == pytest.approx(r_explicit.att, abs=1e-10) + if np.isfinite(r_no_psu.se) and np.isfinite(r_explicit.se): + assert r_no_psu.se == pytest.approx(r_explicit.se, rel=1e-6) assert r_no_psu.survey_metadata is not None assert r_explicit.survey_metadata is not None assert r_no_psu.survey_metadata.df_survey == r_explicit.survey_metadata.df_survey @@ -1542,8 +1542,8 @@ def test_degenerate_cohort_survey_se_is_nan(self): ) # overall_se must be NaN on degenerate cohorts (not 0.0) - assert np.isnan(result.overall_se), ( - f"Degenerate-cohort survey overall_se must be NaN, " f"got {result.overall_se}" + assert np.isnan(result.se), ( + f"Degenerate-cohort survey overall_se must be NaN, " f"got {result.se}" ) # Degenerate-cohort warning must fire assert any( @@ -1628,10 +1628,10 @@ def test_off_horizon_row_duplication_does_not_change_se(self, base_data): L_max=2, survey_design=sd, ) - if np.isfinite(r_base.overall_se) and np.isfinite(r_dup.overall_se): - assert r_base.overall_se == pytest.approx(r_dup.overall_se, rel=1e-6), ( - f"Row duplication changed SE ({r_base.overall_se} vs " - f"{r_dup.overall_se}) — auto-inject psu=group is not active." + if np.isfinite(r_base.se) and np.isfinite(r_dup.se): + assert r_base.se == pytest.approx(r_dup.se, rel=1e-6), ( + f"Row duplication changed SE ({r_base.se} vs " + f"{r_dup.se}) — auto-inject psu=group is not active." ) def test_cell_allocator_row_sum_identity(self): @@ -1746,7 +1746,7 @@ def test_within_cell_check_excludes_zero_weight_rows(self, base_data): treatment="treatment", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) class TestHeterogeneityCellPeriod: @@ -2425,11 +2425,11 @@ def test_fit_succeeds_on_terminal_missingness_with_psu_group(self): survey_design=sd, L_max=1, ) - assert np.isfinite(res.overall_att), ( + assert np.isfinite(res.att), ( f"n_bootstrap={n_boot}: overall_att must be finite " f"under PSU=group + terminal missingness." ) - assert np.isfinite(res.overall_se) and res.overall_se >= 0.0, ( + assert np.isfinite(res.se) and res.se >= 0.0, ( f"n_bootstrap={n_boot}: overall_se must be finite " f"under PSU=group + terminal missingness." ) diff --git a/tests/test_survey_dcdh_replicate_psu.py b/tests/test_survey_dcdh_replicate_psu.py index f0696acdd..00a0b1065 100644 --- a/tests/test_survey_dcdh_replicate_psu.py +++ b/tests/test_survey_dcdh_replicate_psu.py @@ -165,9 +165,9 @@ def test_overall_se_finite(self, base_panel, replicate_design, method): treatment="treatment", survey_design=sd, ) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) - assert res.overall_se > 0 + assert np.isfinite(res.att) + assert np.isfinite(res.se) + assert res.se > 0 def test_att_raises_on_terminal_missingness_replicate_path(self): """Replicate ATT + terminal missingness: the Class A replicate @@ -283,7 +283,7 @@ def _spy(psi, resolved): treatment="treatment", survey_design=sd, ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 assert captured_psi, ( "Spy captured no psi_obs — the fit did not route through " "compute_replicate_if_variance. Did the replicate " @@ -322,7 +322,7 @@ def _spy(psi, resolved): # from the captured psi_obs. Trivial sanity check — the fit # computed it from this exact psi_obs via the spy pass-through. var_cell_recompute, _ = original_repvar(psi_obs_actual, resolved) - assert float(res.overall_se) ** 2 == pytest.approx( + assert float(res.se) ** 2 == pytest.approx( var_cell_recompute, rel=1e-12, ), ( @@ -365,9 +365,9 @@ def test_inference_fields_finite(self, base_panel, replicate_design, method): treatment="treatment", survey_design=sd, ) - assert np.isfinite(res.overall_t_stat) - assert np.isfinite(res.overall_p_value) - assert np.all(np.isfinite(res.overall_conf_int)) + assert np.isfinite(res.t_stat) + assert np.isfinite(res.p_value) + assert np.all(np.isfinite(res.conf_int)) @pytest.mark.parametrize("method", ["BRR", "JK1"]) def test_df_survey_reflects_n_valid(self, base_panel, replicate_design, method): @@ -415,12 +415,12 @@ def test_jk1_converges_to_tsl(self, replicate_design): survey_design=sd_tsl, ) # Same point estimate (both unweighted) - assert res_rep.overall_att == pytest.approx(res_tsl.overall_att, rel=1e-6) + assert res_rep.att == pytest.approx(res_tsl.att, rel=1e-6) # SEs should be within 50% of each other (broad envelope) - ratio = res_rep.overall_se / res_tsl.overall_se + ratio = res_rep.se / res_tsl.se assert 0.5 <= ratio <= 2.0, ( - f"Replicate SE ({res_rep.overall_se:.4f}) and TSL SE " - f"({res_tsl.overall_se:.4f}) differ by more than 2x." + f"Replicate SE ({res_rep.se:.4f}) and TSL SE " + f"({res_tsl.se:.4f}) differ by more than 2x." ) @pytest.mark.parametrize("method", ["BRR", "JK1"]) @@ -498,8 +498,8 @@ def test_did_x_replicate(self, base_panel, replicate_design): covariates=["cov1"], L_max=1, ) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.att) + assert np.isfinite(res.se) and res.se > 0 # ── 2. Class B replicate (heterogeneity + twowayfeweights) ────────── @@ -594,7 +594,7 @@ def test_auto_inject_bit_identical_to_group_level(self, base_panel): treatment="treatment", survey_design=sd_explicit, ) - assert r_auto.overall_se == pytest.approx(r_explicit.overall_se, rel=1e-10) + assert r_auto.se == pytest.approx(r_explicit.se, rel=1e-10) def test_coarser_psu_produces_finite_se(self): """Under a strictly coarser PSU, the Hall-Mammen PSU bootstrap @@ -612,8 +612,8 @@ def test_coarser_psu_produces_finite_se(self): treatment="treatment", survey_design=sd, ) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.att) + assert np.isfinite(res.se) and res.se > 0 def test_no_warning_under_auto_inject(self, base_panel): """No UserWarning about PSU-level bootstrap should fire under @@ -671,7 +671,7 @@ def test_all_weight_types_under_psu(self, weight_type): treatment="treatment", survey_design=sd, ) - assert np.isfinite(res.overall_se) and res.overall_se > 0 + assert np.isfinite(res.se) and res.se > 0 def test_multi_horizon_psu_bootstrap(self): """Event study bootstrap inherits PSU broadcasting.""" @@ -778,7 +778,7 @@ def test_non_survey_unchanged(self, base_panel): time="period", treatment="treatment", ) - assert r1.overall_se == pytest.approx(r2.overall_se, rel=1e-10) + assert r1.se == pytest.approx(r2.se, rel=1e-10) def test_map_for_target_id_lookup(self): """`_map_for_target` builds the PSU map from group IDs via dict @@ -1033,9 +1033,9 @@ def test_rank_1_replicate_forces_nan_inference(self, base_panel): # Main dCDH inference fields are NaN via the _inference_df # coercion (None → 0 under replicate → safe_inference NaN # branch). - assert np.isnan(res.overall_t_stat) - assert np.isnan(res.overall_p_value) - assert not np.all(np.isfinite(res.overall_conf_int)) + assert np.isnan(res.t_stat) + assert np.isnan(res.p_value) + assert not np.all(np.isfinite(res.conf_int)) # Event study horizon 1 is also NaN. if 1 in res.event_study_effects: info = res.event_study_effects[1] @@ -1090,9 +1090,9 @@ def test_heterogeneity_replicate_cross_surface_df_consistency( # Verify overall inference was computed with df=final_df by # checking p-value against t(final_df) — would disagree if # df=R-1 or df=None was used. - t_stat = res.overall_att / res.overall_se + t_stat = res.att / res.se expected_p = 2 * _stats.t.sf(abs(t_stat), df=final_df) - assert res.overall_p_value == pytest.approx(expected_p, rel=1e-6) + assert res.p_value == pytest.approx(expected_p, rel=1e-6) # Same check for heterogeneity surface. if res.heterogeneity_effects: het_info = res.heterogeneity_effects[1] @@ -1200,9 +1200,9 @@ def reduce_after_main(psi, resolved_arg): ) # Overall surface: p-value must use the reduced df. - t_stat = res.overall_att / res.overall_se + t_stat = res.att / res.se expected_p_overall = 2 * _stats.t.sf(abs(t_stat), df=expected_df) - assert res.overall_p_value == pytest.approx( + assert res.p_value == pytest.approx( expected_p_overall, rel=1e-6, ), ( @@ -1355,9 +1355,9 @@ def reduce_to_one(psi, resolved_arg): # Final effective df is None (reduced_df = 0 < 1 guard). assert res.survey_metadata.df_survey is None # Every public inference surface must be NaN. - assert np.isnan(res.overall_t_stat) - assert np.isnan(res.overall_p_value) - assert not np.all(np.isfinite(res.overall_conf_int)) + assert np.isnan(res.t_stat) + assert np.isnan(res.p_value) + assert not np.all(np.isfinite(res.conf_int)) if res.event_study_effects: for info in res.event_study_effects.values(): assert np.isnan(info["t_stat"]) @@ -1415,10 +1415,10 @@ def test_phase1_event_study_row_matches_refreshed_overall(self, base_panel): row = res.event_study_effects[1] # The stored copy agrees with the refreshed overall inference... - assert row["t_stat"] == res.overall_t_stat - assert row["p_value"] == res.overall_p_value + assert row["t_stat"] == res.t_stat + assert row["p_value"] == res.p_value # ...and that inference is the FINAL df's, not an intermediate one. - t_stat = res.overall_att / res.overall_se + t_stat = res.att / res.se expected_p = 2 * _stats.t.sf(abs(t_stat), df=final_df) assert row["p_value"] == pytest.approx(expected_p, rel=1e-6) diff --git a/tests/test_survey_dml.py b/tests/test_survey_dml.py index 866fd3acd..4377a64dc 100644 --- a/tests/test_survey_dml.py +++ b/tests/test_survey_dml.py @@ -270,8 +270,8 @@ def test_uniform_weights_match_unweighted( # numerical route than the unweighted one). res_w = _fit(df, panel=panel, survey=SurveyDesign(weights="w1")) base = panel_plain if panel else rcs_plain - np.testing.assert_allclose(res_w.overall_att, base.overall_att, rtol=0, atol=1e-12) - np.testing.assert_allclose(res_w.overall_se, base.overall_se, rtol=0, atol=1e-12) + np.testing.assert_allclose(res_w.att, base.att, rtol=0, atol=1e-12) + np.testing.assert_allclose(res_w.se, base.se, rtol=0, atol=1e-12) @pytest.mark.parametrize("panel", [True, False]) def test_weight_scale_invariance(self, panel, panel_df, rcs_df): @@ -279,21 +279,21 @@ def test_weight_scale_invariance(self, panel, panel_df, rcs_df): df["w3"] = df["w"] * 3.0 r1 = _fit(df, panel=panel, survey=_DESIGN) r3 = _fit(df, panel=panel, survey=SurveyDesign(weights="w3", strata="stratum", psu="psu")) - np.testing.assert_allclose(r3.overall_att, r1.overall_att, rtol=1e-10) - np.testing.assert_allclose(r3.overall_se, r1.overall_se, rtol=1e-10) + np.testing.assert_allclose(r3.att, r1.att, rtol=1e-10) + np.testing.assert_allclose(r3.se, r1.se, rtol=1e-10) @pytest.mark.parametrize("panel", [True, False]) def test_weights_move_the_point_estimate( self, panel, panel_survey, rcs_survey, panel_plain, rcs_plain ): res, base = (panel_survey, panel_plain) if panel else (rcs_survey, rcs_plain) - assert np.isfinite(res.overall_att) - assert res.overall_att != base.overall_att + assert np.isfinite(res.att) + assert res.att != base.att @pytest.mark.parametrize("panel", [True, False]) def test_full_design_smoke(self, panel, panel_survey, rcs_survey): res = panel_survey if panel else rcs_survey - assert np.isfinite(res.overall_att) and np.isfinite(res.overall_se) + assert np.isfinite(res.att) and np.isfinite(res.se) assert res.survey_metadata is not None assert res.survey_metadata.n_strata == 2 assert res.survey_metadata.n_psu == 20 @@ -308,7 +308,7 @@ def test_fpc_shrinks_se(self, panel_df): df["fpc"] = 11 r_nofpc = _fit(df, survey=_DESIGN) r_fpc = _fit(df, survey=SurveyDesign(weights="w", strata="stratum", psu="psu", fpc="fpc")) - assert r_fpc.overall_se < r_nofpc.overall_se + assert r_fpc.se < r_nofpc.se # --------------------------------------------------------------------------- @@ -366,7 +366,7 @@ def test_single_psu_retained_cells_nan_inference(self, panel_df): df["one_psu"] = 0 with pytest.warns(UserWarning, match="PSU"): res = _fit(df, survey=SurveyDesign(weights="w", psu="one_psu"), ignore_warnings=False) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) found = False for data in res.group_time_effects.values(): if data.get("is_reference") or data.get("skip_reason") is not None: @@ -413,20 +413,20 @@ def test_bare_cluster_synthesizes_design(self, panel_df): def test_bare_cluster_matches_explicit_psu_design(self, panel_df): r_cluster = _fit(panel_df, cluster="psu") r_design = _fit(panel_df, survey=SurveyDesign(psu="psu")) - np.testing.assert_allclose(r_cluster.overall_att, r_design.overall_att, rtol=1e-12) - np.testing.assert_allclose(r_cluster.overall_se, r_design.overall_se, rtol=1e-12) + np.testing.assert_allclose(r_cluster.att, r_design.att, rtol=1e-12) + np.testing.assert_allclose(r_cluster.se, r_design.se, rtol=1e-12) def test_identity_psu_bare_cluster_bit_identical_on_complete_panel(self, panel_df, panel_plain): # Identity PSU (cluster == unit) on a fully complete panel: folds # stay stratified (predicate's coarser-than-unit conjunct), kernels # stay unweighted, aggregation masses coincide -> bit-identical. res = _fit(panel_df, cluster="unit") - assert res.overall_att == panel_plain.overall_att + assert res.att == panel_plain.att # The SE deliberately moves: identity-PSU CR1 carries the # per-PSU centering + Bessel factor the plain sqrt(sum(if^2)) # does not (that IS the clustering request). - assert res.overall_se != panel_plain.overall_se - assert np.isfinite(res.overall_se) + assert res.se != panel_plain.se + assert np.isfinite(res.se) def test_incomplete_panel_bare_cluster_moves_overall_att(self): # CS-parity divergence (staggered_results.py bare-cluster note): the @@ -436,12 +436,12 @@ def test_incomplete_panel_bare_cluster_moves_overall_att(self): df = _make_panel(drop_one=True) base = _fit(df) res = _fit(df, cluster="unit") - assert np.isfinite(res.overall_att) - assert res.overall_att != base.overall_att + assert np.isfinite(res.att) + assert res.att != base.att def test_xy_only_learner_works_under_bare_cluster(self, panel_df): res = _fit(panel_df, cluster="psu", outcome_learner=_XYOnlyRegressor()) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) def test_design_psu_wins_over_cluster_with_warning(self, panel_df): df = panel_df.copy() @@ -603,8 +603,8 @@ def test_few_psus_reduce_fold_count_keep_cohesion(self, panel, panel_df, rcs_df) n_folds=5, ignore_warnings=False, ) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) diags = [d for d in res.cross_fit_diagnostics.values() if d.get("propensity")] assert diags and all(d["psu_folds"] is True for d in diags) # Effective fold count == n_psu (3), visible in the per-fold counts. @@ -623,8 +623,8 @@ def test_exact_psu_fold_match_uses_psu_folds_unreduced(self, panel, panel_df, rc df = (panel_df if panel else rcs_df).copy() df["psu3"] = df["unit"] % 3 res = _fit(df, panel=panel, survey=SurveyDesign(weights="w", psu="psu3"), n_folds=3) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) diags = [d for d in res.cross_fit_diagnostics.values() if d.get("propensity")] assert diags and all(d["psu_folds"] is True for d in diags) assert all(len(d["propensity"]["n_fit_per_fold"]) == 3 for d in diags) @@ -747,8 +747,8 @@ class TestSurveyBootstrap: def test_survey_bootstrap_smoke(self, panel, panel_df, rcs_df, ci_params): df = panel_df if panel else rcs_df res = _fit(df, panel=panel, survey=_DESIGN, n_bootstrap=ci_params.bootstrap(49)) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) def test_single_psu_bootstrap_nan_with_dml_label(self, panel_df, ci_params): df = panel_df.copy() @@ -760,7 +760,7 @@ def test_single_psu_bootstrap_nan_with_dml_label(self, panel_df, ci_params): n_bootstrap=ci_params.bootstrap(29), ignore_warnings=False, ) - assert np.isnan(res.overall_se) + assert np.isnan(res.se) def test_replay_label_and_legacy_kit_fallback(self, panel_df, ci_params): df = panel_df.copy() @@ -784,7 +784,7 @@ def test_replay_label_and_legacy_kit_fallback(self, panel_df, ci_params): def test_survey_bootstrap_replay_matches_fit(self, panel_df, ci_params): res = _fit(panel_df, survey=_DESIGN, n_bootstrap=ci_params.bootstrap(49)) agg = res.aggregate("simple") - np.testing.assert_allclose(float(agg.att[0]), res.overall_att, rtol=1e-12) + np.testing.assert_allclose(float(agg.att[0]), res.att, rtol=1e-12) # --------------------------------------------------------------------------- @@ -836,9 +836,9 @@ def test_xy_only_learner_rejected_up_front_under_survey(self, panel_df): def test_weighted_and_kwargs_learners_accepted_under_survey(self, panel_df): r1 = _fit(panel_df, survey=_DESIGN, outcome_learner=_WeightedRegressor()) - assert np.isfinite(r1.overall_att) + assert np.isfinite(r1.att) r2 = _fit(panel_df, survey=_DESIGN, outcome_learner=_KwargsRegressor()) - assert np.isfinite(r2.overall_att) + assert np.isfinite(r2.att) # --------------------------------------------------------------------------- @@ -958,7 +958,7 @@ class TestReplicateDesigns: @pytest.mark.parametrize("fixture", ["panel_replicate", "rcs_replicate"]) def test_acceptance_both_lanes(self, fixture, request): res = request.getfixturevalue(fixture) - assert np.isfinite(res.overall_att) and np.isfinite(res.overall_se) + assert np.isfinite(res.att) and np.isfinite(res.se) assert res.survey_metadata is not None # Full-rank JK1 over 20 PSUs: QR-rank df = R - 1. assert res.survey_metadata.df_survey == 19 @@ -976,7 +976,7 @@ def test_brr_method_accepted(self, panel_df): df, survey=SurveyDesign(weights="w", replicate_weights=rep_cols, replicate_method="BRR"), ) - assert np.isfinite(res.overall_att) and np.isfinite(res.overall_se) + assert np.isfinite(res.att) and np.isfinite(res.se) assert res.survey_metadata is not None def test_combined_weights_false_branch(self, panel_replicate_df, panel_replicate): @@ -986,9 +986,9 @@ def test_combined_weights_false_branch(self, panel_replicate_df, panel_replicate # routed it through the combined branch would reproduce it exactly). df, rep_cols = panel_replicate_df res = _fit(df, survey=_jk1_design(rep_cols, combined_weights=False)) - assert np.isfinite(res.overall_att) and np.isfinite(res.overall_se) - np.testing.assert_allclose(res.overall_att, panel_replicate.overall_att, rtol=1e-12) - assert not np.isclose(res.overall_se, panel_replicate.overall_se, rtol=1e-6) + assert np.isfinite(res.att) and np.isfinite(res.se) + np.testing.assert_allclose(res.att, panel_replicate.att, rtol=1e-12) + assert not np.isclose(res.se, panel_replicate.se, rtol=1e-6) @pytest.mark.parametrize("fixture", ["panel_replicate", "rcs_replicate"]) def test_per_cell_se_matches_compute_replicate_if_variance(self, fixture, request): @@ -1018,8 +1018,8 @@ def test_scale_invariance_linear_sieve(self, panel_replicate_df, learner): for c in ["w"] + rep_cols: df_s[c] = df_s[c] * 100.0 scaled = _fit(df_s, survey=_jk1_design(rep_cols), outcome_learner=learner) - np.testing.assert_allclose(scaled.overall_att, base.overall_att, rtol=1e-14) - np.testing.assert_allclose(scaled.overall_se, base.overall_se, rtol=1e-14) + np.testing.assert_allclose(scaled.att, base.att, rtol=1e-14) + np.testing.assert_allclose(scaled.se, base.se, rtol=1e-14) def test_ridge_scale_dependence_bounded(self, panel_replicate_df): # RidgeLearner is weight-SCALE-sensitive by documented solve_ridge @@ -1033,8 +1033,8 @@ def test_ridge_scale_dependence_bounded(self, panel_replicate_df): for c in ["w"] + rep_cols: df_s[c] = df_s[c] * 100.0 scaled = _fit(df_s, survey=_jk1_design(rep_cols), outcome_learner=RidgeLearner(alpha=1.0)) - assert np.isfinite(scaled.overall_att) - assert abs(scaled.overall_att - base.overall_att) < 1e-2 + assert np.isfinite(scaled.att) + assert abs(scaled.att - base.att) < 1e-2 def test_df_tightening_spy_reaches_all_surfaces(self, panel_replicate_df, monkeypatch): # dCDH two-pass convention: reduce n_valid on EVERY call; the @@ -1057,8 +1057,8 @@ def reduce_n_valid(psi, resolved_arg): expected_df = reduced_n_valid - 1 assert res.survey_metadata.df_survey == expected_df # Overall: min-cap picked the reduced value (19 -> 6). - t_overall = res.overall_att / res.overall_se - assert res.overall_p_value == pytest.approx( + t_overall = res.att / res.se + assert res.p_value == pytest.approx( 2 * _stats.t.sf(abs(t_overall), df=expected_df), rel=1e-10 ) # Per-cell: every finite cell's p-value uses the reduced df. @@ -1105,10 +1105,8 @@ def reduce_n_valid(psi, resolved_arg): monkeypatch.setattr(_survey_mod, "compute_replicate_if_variance", reduce_n_valid) res = _fit(df, survey=design) assert res.survey_metadata.df_survey == 4 - t_overall = res.overall_att / res.overall_se - assert res.overall_p_value == pytest.approx( - 2 * _stats.t.sf(abs(t_overall), df=4), rel=1e-10 - ) + t_overall = res.att / res.se + assert res.p_value == pytest.approx(2 * _stats.t.sf(abs(t_overall), df=4), rel=1e-10) def test_rank_one_replicate_matrix_nan_inference(self, panel_df): # All replicate columns identical to the full weights: QR rank 1 -> @@ -1134,10 +1132,10 @@ def test_rank_one_replicate_matrix_nan_inference(self, panel_df): assert checked > 0 assert_nan_inference( { - "se": res.overall_se, - "t_stat": res.overall_t_stat, - "p_value": res.overall_p_value, - "conf_int": res.overall_conf_int, + "se": res.se, + "t_stat": res.t_stat, + "p_value": res.p_value, + "conf_int": res.conf_int, } ) diff --git a/tests/test_survey_estimator_validation.py b/tests/test_survey_estimator_validation.py index ba533345d..b030f9da5 100644 --- a/tests/test_survey_estimator_validation.py +++ b/tests/test_survey_estimator_validation.py @@ -191,9 +191,9 @@ def test_s1_imputation_end_to_end(self, golden): covariates=["x1", "x2"], survey_design=sd, ) - assert np.isfinite(result.overall_att), "ImputationDiD ATT should be finite" - assert np.isfinite(result.overall_se), "ImputationDiD SE should be finite" - assert result.overall_se > 0, "ImputationDiD SE should be positive" + assert np.isfinite(result.att), "ImputationDiD ATT should be finite" + assert np.isfinite(result.se), "ImputationDiD SE should be finite" + assert result.se > 0, "ImputationDiD SE should be positive" # ------------------------------------------------------------------ # S2: StackedDiD — stacked WLS with Q-weight x survey weight @@ -220,8 +220,8 @@ def test_s2_stacked_did(self, golden): survey_design=sd, ) - _assert_close(result.overall_att, r["att"], COEF_RTOL, COEF_ATOL, "S2 ATT") - _assert_close(result.overall_se, r["se"], SE_RTOL, SE_ATOL, "S2 SE") + _assert_close(result.att, r["att"], COEF_RTOL, COEF_ATOL, "S2 ATT") + _assert_close(result.se, r["se"], SE_RTOL, SE_ATOL, "S2 SE") # Compare individual event-study coefficients and SEs if "event_study" in r: @@ -265,8 +265,8 @@ def test_s3_sun_abraham(self, golden): ) # Compare overall IW-aggregated ATT - _assert_close(result.overall_att, r["att"], COEF_RTOL, COEF_ATOL, "S3 ATT") - _assert_close(result.overall_se, r["se"], SE_RTOL, SE_ATOL, "S3 SE") + _assert_close(result.att, r["att"], COEF_RTOL, COEF_ATOL, "S3 ATT") + _assert_close(result.se, r["se"], SE_RTOL, SE_ATOL, "S3 SE") # Compare individual cohort x rel-time effects and SEs (post-treatment only) if "cohort_effects" in r: diff --git a/tests/test_survey_metadata_raw_scale.py b/tests/test_survey_metadata_raw_scale.py index 63e15486f..375f99060 100644 --- a/tests/test_survey_metadata_raw_scale.py +++ b/tests/test_survey_metadata_raw_scale.py @@ -240,7 +240,7 @@ def test_bare_cluster_never_reaches_metadata_recompute(self, panel_df): "time", "first_treat", ) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) assert res.survey_metadata is None rc = _make_rc_data() rc["cluster_col"] = rc["unit"] // 20 @@ -252,7 +252,7 @@ def test_bare_cluster_never_reaches_metadata_recompute(self, panel_df): "time", "first_treat", ) - assert np.isfinite(res_rc.overall_att) + assert np.isfinite(res_rc.att) assert res_rc.survey_metadata is None @@ -313,8 +313,8 @@ def fit(col): ) a, b = fit(""), fit("weight") - assert a.overall_att == b.overall_att - assert a.overall_se == b.overall_se + assert a.att == b.att + assert a.se == b.se for e in a.event_study_effects: assert a.event_study_effects[e]["effect"] == b.event_study_effects[e]["effect"] assert a.event_study_effects[e]["se"] == b.event_study_effects[e]["se"] @@ -359,7 +359,7 @@ def outcome_of(col): "first_treat", survey_design=SurveyDesign(weights=col), ) - return ("fit", r.overall_att, r.overall_se) + return ("fit", r.att, r.se) except (ValueError, NotImplementedError) as exc: # Normalize only the quoted column name, so messages that # embed it still compare equal across the two aliases. @@ -533,7 +533,7 @@ def test_empty_post_cells_metadata_is_unit_level(self): "dose", survey_design=SurveyDesign(weights="weight"), ) - assert np.isnan(res.overall_att) + assert np.isnan(res.att) _assert_raw_scale(res.survey_metadata, df.groupby("unit")["weight"].first()) def test_weight_column_aliasing_dose_reports_original_values(self): @@ -600,8 +600,8 @@ def fit_with(wcol): ) r_alias, r_dup = fit_with("dose"), fit_with("w_dup") - assert r_alias.overall_att == r_dup.overall_att - assert r_alias.overall_att_se == r_dup.overall_att_se + assert r_alias.att == r_dup.att + assert r_alias.se == r_dup.se ma, md_ = r_alias.survey_metadata, r_dup.survey_metadata for field in ( "sum_weights", diff --git a/tests/test_survey_phase3.py b/tests/test_survey_phase3.py index 8618febd7..d00e19206 100644 --- a/tests/test_survey_phase3.py +++ b/tests/test_survey_phase3.py @@ -130,8 +130,8 @@ def test_smoke_weights_only(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_uniform_weights_match_unweighted(self, staggered_survey_data): @@ -150,7 +150,7 @@ def test_uniform_weights_match_unweighted(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert abs(r_unw.overall_att - r_w.overall_att) < 1e-10 + assert abs(r_unw.att - r_w.att) < 1e-10 def test_survey_metadata_fields(self, staggered_survey_data): """survey_metadata has correct fields with full design.""" @@ -196,9 +196,9 @@ def test_se_differs_with_design(self, staggered_survey_data): survey_design=sd_full, ) # ATTs should be the same (same weights) - assert abs(r_w.overall_att - r_full.overall_att) < 1e-10 + assert abs(r_w.att - r_full.att) < 1e-10 # SEs should differ due to different variance estimators - assert r_w.overall_se != r_full.overall_se + assert r_w.se != r_full.se def test_bootstrap_weights_only_uses_pairs(self, staggered_survey_data): """Bootstrap + weights-only survey uses pairs bootstrap (no Rao-Wu).""" @@ -216,8 +216,8 @@ def test_bootstrap_weights_only_uses_pairs(self, staggered_survey_data): assert result.bootstrap_results is not None assert result.bootstrap_results.weight_type == "pairs" assert result.bootstrap_results.n_bootstrap == 99 - assert np.isfinite(result.overall_se) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.se) + assert np.isfinite(result.att) def test_bootstrap_survey_strata_uses_rao_wu(self, staggered_survey_data): """Bootstrap + survey with strata/PSU uses Rao-Wu rescaled bootstrap.""" @@ -235,8 +235,8 @@ def test_bootstrap_survey_strata_uses_rao_wu(self, staggered_survey_data): assert result.bootstrap_results is not None assert result.bootstrap_results.weight_type == "rao_wu" assert result.bootstrap_results.n_bootstrap == 99 - assert np.isfinite(result.overall_se) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.se) + assert np.isfinite(result.att) # Event study effects should also have finite bootstrap SEs for e, eff in result.event_study_effects.items(): assert np.isfinite(eff["se"]), f"Event study e={e} has non-finite SE" @@ -287,8 +287,8 @@ def test_smoke_weights_only(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_survey_metadata_present(self, staggered_survey_data): @@ -322,7 +322,7 @@ def test_q_weight_composition(self, staggered_survey_data): survey_design=sd, ) # ATT should differ (non-uniform weights) - assert r_unw.overall_att != r_w.overall_att + assert r_unw.att != r_w.att def test_summary_includes_survey(self, staggered_survey_data): """Summary includes survey design section.""" @@ -621,7 +621,7 @@ def test_smoke_weights_only(self, continuous_survey_data): "dose", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) assert result.survey_metadata is not None def test_bootstrap_survey_supported(self, continuous_survey_data): @@ -638,8 +638,8 @@ def test_bootstrap_survey_supported(self, continuous_survey_data): "dose", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_att_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) def test_summary_includes_survey(self, continuous_survey_data): """Summary includes survey design section.""" @@ -679,8 +679,8 @@ def test_smoke_weights_only(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_bootstrap_survey_supported(self, staggered_survey_data): @@ -696,8 +696,8 @@ def test_bootstrap_survey_supported(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) def test_covariates_survey_works(self, staggered_survey_data): """Covariates + survey should produce finite results via DR path.""" @@ -717,9 +717,9 @@ def test_covariates_survey_works(self, staggered_survey_data): covariates=["x1"], survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 assert result.estimation_path == "dr" assert result.survey_metadata is not None @@ -819,8 +819,8 @@ def test_survey_all_aggregation(self, staggered_survey_data): ) assert result.aggregate("event_study") is not None assert result.aggregate("group") is not None - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) # ============================================================================= @@ -901,7 +901,7 @@ def test_uniform_weight_equivalence(self, cov_survey_data): ) assert result_survey.estimation_path == "dr" assert result_nosurv.estimation_path == "dr" - np.testing.assert_allclose(result_survey.overall_att, result_nosurv.overall_att, atol=1e-8) + np.testing.assert_allclose(result_survey.att, result_nosurv.att, atol=1e-8) def test_scale_invariance(self, cov_survey_data): """Multiplying all weights by a constant doesn't change ATT.""" @@ -929,7 +929,7 @@ def test_scale_invariance(self, cov_survey_data): covariates=["x1"], survey_design=sd2, ) - np.testing.assert_allclose(result1.overall_att, result2.overall_att, atol=1e-8) + np.testing.assert_allclose(result1.att, result2.att, atol=1e-8) def test_nontrivial_weight_effect(self, cov_survey_data): """Heterogeneous weights produce different ATT from unweighted.""" @@ -954,7 +954,7 @@ def test_nontrivial_weight_effect(self, cov_survey_data): covariates=["x1"], ) # Non-uniform weights (1.0 + 0.3*stratum) should produce different ATT - assert abs(result_survey.overall_att - result_nosurv.overall_att) > 1e-6 + assert abs(result_survey.att - result_nosurv.att) > 1e-6 def test_full_design_smoke(self, cov_survey_data): """Strata + PSU + FPC + covariates produces finite results.""" @@ -976,9 +976,9 @@ def test_full_design_smoke(self, cov_survey_data): covariates=["x1"], survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 assert result.estimation_path == "dr" def test_aggregation_with_survey(self, cov_survey_data): @@ -1020,9 +1020,9 @@ def test_bootstrap_covariates_survey(self, cov_survey_data): covariates=["x1"], survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_analytical_se_differs_from_unweighted(self, cov_survey_data): """Survey analytical SE should differ from unweighted SE.""" @@ -1047,9 +1047,9 @@ def test_analytical_se_differs_from_unweighted(self, cov_survey_data): covariates=["x1"], ) # Non-uniform weights (1.0 + 0.3*stratum) should produce different SEs - assert result_survey.overall_se != result_nosurv.overall_se - assert np.isfinite(result_survey.overall_se) - assert result_survey.overall_se > 0 + assert result_survey.se != result_nosurv.se + assert np.isfinite(result_survey.se) + assert result_survey.se > 0 def test_bootstrap_se_in_ballpark_of_analytical(self, cov_survey_data): """Bootstrap SE should be in same ballpark as analytical SE.""" @@ -1074,7 +1074,7 @@ def test_bootstrap_se_in_ballpark_of_analytical(self, cov_survey_data): covariates=["x1"], survey_design=sd, ) - ratio = result_boot.overall_se / result_analytical.overall_se + ratio = result_boot.se / result_analytical.se assert 0.3 < ratio < 3.0, f"Bootstrap/analytical SE ratio {ratio:.2f} outside [0.3, 3.0]" def test_zero_weight_cohort_skipped(self, cov_survey_data): @@ -1095,8 +1095,8 @@ def test_zero_weight_cohort_skipped(self, cov_survey_data): covariates=["x1"], survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) def test_zero_weight_never_treated_raises(self, cov_survey_data): """Zero-weight never-treated group should raise ValueError (DR path).""" @@ -1186,9 +1186,9 @@ def test_replicate_weight_aggregation(self): covariates=["x1"], survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 es = result.aggregate("event_study") assert es is not None for att, se in zip(es.att, es.se): @@ -1229,8 +1229,8 @@ def test_sun_abraham_scale_invariance(self, staggered_survey_data): survey_design=sd2, ) - assert abs(r1.overall_att - r2.overall_att) < 1e-10 - assert abs(r1.overall_se - r2.overall_se) < 1e-8 + assert abs(r1.att - r2.att) < 1e-10 + assert abs(r1.se - r2.se) < 1e-8 def test_efficient_did_scale_invariance(self, staggered_survey_data): from diff_diff import EfficientDiD @@ -1256,8 +1256,8 @@ def test_efficient_did_scale_invariance(self, staggered_survey_data): survey_design=sd2, ) - assert abs(r1.overall_att - r2.overall_att) < 1e-10 - assert abs(r1.overall_se - r2.overall_se) < 1e-8 + assert abs(r1.att - r2.att) < 1e-10 + assert abs(r1.se - r2.se) < 1e-8 def test_triple_diff_scale_invariance(self, ddd_survey_data): from diff_diff import TripleDifference @@ -1310,8 +1310,8 @@ def test_sun_abraham_sub_unit_weight_scale_invariance(self, staggered_survey_dat survey_design=sd2, ) - assert abs(r1.overall_att - r2.overall_att) < 1e-10 - assert abs(r1.overall_se - r2.overall_se) < 1e-8 + assert abs(r1.att - r2.att) < 1e-10 + assert abs(r1.se - r2.se) < 1e-8 # ============================================================================= @@ -1335,7 +1335,7 @@ def test_stacked_did_no_weight_survey(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) assert result.survey_metadata is not None def test_triple_diff_survey_df(self, ddd_survey_data): @@ -1371,9 +1371,9 @@ def test_efficient_did_weights_only_se(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(r.overall_se) - assert r.overall_se > 0 - assert r.overall_se > 0.01 # Not artificially tiny + assert np.isfinite(r.se) + assert r.se > 0 + assert r.se > 0.01 # Not artificially tiny def test_continuous_did_eventstudy_survey(self, continuous_survey_data): """The post-fit event-study aggregation works with a survey design @@ -1495,13 +1495,11 @@ def test_sun_abraham_survey_df_regression(self, staggered_survey_data): # Recompute with normal approx and verify they differ from diff_diff.utils import safe_inference - _, p_normal, _ = safe_inference(result.overall_att, result.overall_se, alpha=0.05, df=None) - _, p_survey, _ = safe_inference( - result.overall_att, result.overall_se, alpha=0.05, df=sm.df_survey - ) + _, p_normal, _ = safe_inference(result.att, result.se, alpha=0.05, df=None) + _, p_survey, _ = safe_inference(result.att, result.se, alpha=0.05, df=sm.df_survey) # With small survey df, t-dist p-value > normal p-value - if np.isfinite(result.overall_p_value) and np.isfinite(p_normal): - assert result.overall_p_value == pytest.approx(p_survey, rel=1e-10) + if np.isfinite(result.p_value) and np.isfinite(p_normal): + assert result.p_value == pytest.approx(p_survey, rel=1e-10) def test_continuous_did_dose_response_survey_pvalue(self, continuous_survey_data): """DoseResponseCurve.to_dataframe() p-values should use survey df.""" @@ -1566,7 +1564,7 @@ def test_fpc_census_zero_variance_stacked(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert result.overall_se == pytest.approx(0.0, abs=1e-10) + assert result.se == pytest.approx(0.0, abs=1e-10) def test_single_psu_lonely_remove_nan_se(self, staggered_survey_data): """All strata with 1 PSU + lonely_psu='remove' -> NaN SE.""" @@ -1592,7 +1590,7 @@ def test_single_psu_lonely_remove_nan_se(self, staggered_survey_data): survey_design=sd, ) # All strata removed -> NaN SE - assert np.isnan(result.overall_se) + assert np.isnan(result.se) def test_full_design_bootstrap_continuous_did(self, staggered_survey_data): """Bootstrap with strata survey design works for ContinuousDiD.""" @@ -1612,8 +1610,8 @@ def test_full_design_bootstrap_continuous_did(self, staggered_survey_data): "dose", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_att_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_full_design_bootstrap_efficient_did(self, staggered_survey_data): @@ -1629,6 +1627,6 @@ def test_full_design_bootstrap_efficient_did(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None diff --git a/tests/test_survey_phase4.py b/tests/test_survey_phase4.py index 58b63c5dc..984a00c44 100644 --- a/tests/test_survey_phase4.py +++ b/tests/test_survey_phase4.py @@ -233,8 +233,8 @@ def test_smoke_weights_only(self, staggered_survey_data, survey_design_weights_o "first_treat", survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_uniform_weights_match_unweighted(self, staggered_survey_data): @@ -253,7 +253,7 @@ def test_uniform_weights_match_unweighted(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert abs(r_unw.overall_att - r_w.overall_att) < 1e-10 + assert abs(r_unw.att - r_w.att) < 1e-10 def test_survey_metadata_fields(self, staggered_survey_data, survey_design_full): """survey_metadata has correct fields with full design.""" @@ -295,14 +295,14 @@ def test_se_differs_with_design(self, staggered_survey_data): survey_design=sd_full, ) # ATTs should be the same (same weights) - assert abs(r_w.overall_att - r_full.overall_att) < 1e-10 + assert abs(r_w.att - r_full.att) < 1e-10 # Full design should carry survey df (strata/PSU structure) assert r_full.survey_metadata is not None assert r_full.survey_metadata.n_strata is not None assert r_full.survey_metadata.n_psu is not None # P-values should differ due to t-distribution with survey df - if np.isfinite(r_w.overall_p_value) and np.isfinite(r_full.overall_p_value): - assert r_w.overall_p_value != r_full.overall_p_value + if np.isfinite(r_w.p_value) and np.isfinite(r_full.p_value): + assert r_w.p_value != r_full.p_value def test_weighted_att_differs(self, staggered_survey_data, survey_design_weights_only): """Non-uniform survey weights should change the overall ATT.""" @@ -318,7 +318,7 @@ def test_weighted_att_differs(self, staggered_survey_data, survey_design_weights survey_design=survey_design_weights_only, ) # ATT should differ because non-uniform weights change aggregation - assert r_unw.overall_att != r_w.overall_att + assert r_unw.att != r_w.att def test_event_study_with_survey(self, staggered_survey_data, survey_design_weights_only): """Event study effects exist when using survey design.""" @@ -348,8 +348,8 @@ def test_bootstrap_survey_supported(self, staggered_survey_data, survey_design_w ) assert result.bootstrap_results is not None assert result.bootstrap_results.n_bootstrap == 99 - assert np.isfinite(result.overall_se) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.se) + assert np.isfinite(result.att) def test_bootstrap_survey_full_design(self, staggered_survey_data, survey_design_full): """Bootstrap + full survey (strata+PSU) uses survey-aware multiplier weights.""" @@ -362,8 +362,8 @@ def test_bootstrap_survey_full_design(self, staggered_survey_data, survey_design survey_design=survey_design_full, ) assert result.bootstrap_results is not None - assert np.isfinite(result.overall_se) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.se) + assert np.isfinite(result.att) def test_summary_includes_survey(self, staggered_survey_data, survey_design_weights_only): """Summary output should include survey design section.""" @@ -401,10 +401,10 @@ def test_se_scale_invariance_fe_only(self, staggered_survey_data): "first_treat", survey_design=sd2, ) - assert np.isclose(r1.overall_att, r2.overall_att, atol=1e-8) + assert np.isclose(r1.att, r2.att, atol=1e-8) assert np.isclose( - r1.overall_se, r2.overall_se, atol=1e-8 - ), f"SE not scale-invariant (FE-only): {r1.overall_se} vs {r2.overall_se}" + r1.se, r2.se, atol=1e-8 + ), f"SE not scale-invariant (FE-only): {r1.se} vs {r2.se}" def test_se_scale_invariance_with_covariates(self, staggered_survey_data): """SE must be invariant to weight rescaling (with covariates).""" @@ -431,10 +431,10 @@ def test_se_scale_invariance_with_covariates(self, staggered_survey_data): covariates=["x1"], survey_design=sd2, ) - assert np.isclose(r1.overall_att, r2.overall_att, atol=1e-8) + assert np.isclose(r1.att, r2.att, atol=1e-8) assert np.isclose( - r1.overall_se, r2.overall_se, atol=1e-8 - ), f"SE not scale-invariant (covariates): {r1.overall_se} vs {r2.overall_se}" + r1.se, r2.se, atol=1e-8 + ), f"SE not scale-invariant (covariates): {r1.se} vs {r2.se}" def test_aggregate_group_with_survey(self, staggered_survey_data, survey_design_weights_only): """Post-fit group aggregation works with survey design.""" @@ -460,7 +460,7 @@ def test_aggregate_all_with_survey(self, staggered_survey_data, survey_design_we "first_treat", survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) assert len(result.aggregate("event_study").event_time) > 0 assert len(result.aggregate("group").label) > 0 @@ -488,9 +488,9 @@ def test_fpc_accepted(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 # ============================================================================= @@ -511,8 +511,8 @@ def test_smoke_weights_only(self, staggered_survey_data, survey_design_weights_o "first_treat", survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_uniform_weights_match_unweighted(self, staggered_survey_data): @@ -529,7 +529,7 @@ def test_uniform_weights_match_unweighted(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert abs(r_unw.overall_att - r_w.overall_att) < 1e-10 + assert abs(r_unw.att - r_w.att) < 1e-10 def test_survey_metadata_fields(self, staggered_survey_data, survey_design_full): """survey_metadata has correct fields with full design.""" @@ -571,14 +571,14 @@ def test_se_differs_with_design(self, staggered_survey_data): survey_design=sd_full, ) # ATTs should be the same (same weights) - assert abs(r_w.overall_att - r_full.overall_att) < 1e-10 + assert abs(r_w.att - r_full.att) < 1e-10 # Full design should carry survey df (strata/PSU structure) assert r_full.survey_metadata is not None assert r_full.survey_metadata.n_strata is not None assert r_full.survey_metadata.n_psu is not None # P-values should differ due to t-distribution with survey df - if np.isfinite(r_w.overall_p_value) and np.isfinite(r_full.overall_p_value): - assert r_w.overall_p_value != r_full.overall_p_value + if np.isfinite(r_w.p_value) and np.isfinite(r_full.p_value): + assert r_w.p_value != r_full.p_value def test_weighted_gmm_variance(self, staggered_survey_data, survey_design_weights_only): """GMM SE should differ from unweighted (weights affect sandwich).""" @@ -592,7 +592,7 @@ def test_weighted_gmm_variance(self, staggered_survey_data, survey_design_weight survey_design=survey_design_weights_only, ) # SE magnitude should differ (not just sign) - assert abs(r_unw.overall_se - r_w.overall_se) > 1e-6 + assert abs(r_unw.se - r_w.se) > 1e-6 def test_bootstrap_survey_works(self, staggered_survey_data, survey_design_weights_only): """Bootstrap + survey should succeed via PSU-level multiplier bootstrap.""" @@ -605,9 +605,9 @@ def test_bootstrap_survey_works(self, staggered_survey_data, survey_design_weigh survey_design=survey_design_weights_only, ) assert result is not None - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_summary_includes_survey(self, staggered_survey_data, survey_design_weights_only): """Summary output should include survey design section.""" @@ -646,7 +646,7 @@ def test_aggregate_all_with_survey(self, staggered_survey_data, survey_design_we "first_treat", survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) assert len(result.aggregate("event_study").event_time) > 0 assert len(result.aggregate("group").label) > 0 @@ -668,8 +668,8 @@ def test_always_treated_with_survey(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_aweight_raises(self, staggered_survey_data): @@ -696,9 +696,9 @@ def test_fpc_accepted(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 # ============================================================================= @@ -734,8 +734,8 @@ def test_smoke_reg_weights_only(self, staggered_survey_data, survey_design_weigh "first_treat", survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_smoke_ipw_weights_only(self, staggered_survey_data, survey_design_weights_only): @@ -748,8 +748,8 @@ def test_smoke_ipw_weights_only(self, staggered_survey_data, survey_design_weigh "first_treat", survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_smoke_dr_weights_only(self, staggered_survey_data, survey_design_weights_only): @@ -762,8 +762,8 @@ def test_smoke_dr_weights_only(self, staggered_survey_data, survey_design_weight "first_treat", survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_uniform_weights_match_unweighted(self, staggered_survey_data): @@ -783,7 +783,7 @@ def test_uniform_weights_match_unweighted(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert abs(r_unw.overall_att - r_w.overall_att) < 1e-8, f"method={method}: ATT mismatch" + assert abs(r_unw.att - r_w.att) < 1e-8, f"method={method}: ATT mismatch" def test_survey_metadata_fields(self, staggered_survey_data, survey_design_weights_only): """survey_metadata has correct fields with weights-only design.""" @@ -812,8 +812,8 @@ def test_strata_psu_fpc_supported(self, staggered_survey_data): "first_treat", survey_design=sd_full, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_aggregate_group_with_survey(self, staggered_survey_data, survey_design_weights_only): @@ -855,7 +855,7 @@ def test_aggregate_all_with_survey(self, staggered_survey_data, survey_design_we "first_treat", survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) assert len(result.aggregate("event_study").event_time) > 0 assert len(result.aggregate("group").label) > 0 @@ -869,8 +869,8 @@ def test_bootstrap_survey_supported(self, staggered_survey_data, survey_design_w "first_treat", survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) def test_ipw_covariates_survey_works(self, staggered_survey_data, survey_design_weights_only): """IPW + covariates + survey works (Phase 7a: nuisance IF corrections).""" @@ -885,9 +885,9 @@ def test_ipw_covariates_survey_works(self, staggered_survey_data, survey_design_ covariates=["x1"], survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_dr_covariates_survey_works(self, staggered_survey_data, survey_design_weights_only): """DR + covariates + survey works (Phase 7a: nuisance IF corrections).""" @@ -902,9 +902,9 @@ def test_dr_covariates_survey_works(self, staggered_survey_data, survey_design_w covariates=["x1"], survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_reg_covariates_survey_works(self, staggered_survey_data, survey_design_weights_only): """Regression + covariates + survey should work (has nuisance IF correction).""" @@ -919,7 +919,7 @@ def test_reg_covariates_survey_works(self, staggered_survey_data, survey_design_ covariates=["x1"], survey_design=survey_design_weights_only, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_reg_covariates_rank_deficient_survey(self, staggered_survey_data): """Collinear covariates with survey should produce finite results.""" @@ -940,8 +940,8 @@ def test_reg_covariates_rank_deficient_survey(self, staggered_survey_data): covariates=["x1", "x2"], survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) def test_reg_covariates_survey_se_scale_invariance(self, staggered_survey_data): """SE for reg + covariates + survey must be invariant to weight rescaling.""" @@ -969,12 +969,10 @@ def test_reg_covariates_survey_se_scale_invariance(self, staggered_survey_data): covariates=["x1"], survey_design=sd2, ) + assert np.isclose(r1.att, r2.att, atol=1e-8), "ATT not scale-invariant for reg+cov+survey" assert np.isclose( - r1.overall_att, r2.overall_att, atol=1e-8 - ), "ATT not scale-invariant for reg+cov+survey" - assert np.isclose( - r1.overall_se, r2.overall_se, atol=1e-8 - ), f"SE not scale-invariant for reg+cov+survey: {r1.overall_se} vs {r2.overall_se}" + r1.se, r2.se, atol=1e-8 + ), f"SE not scale-invariant for reg+cov+survey: {r1.se} vs {r2.se}" def test_weighted_logit(self, staggered_survey_data, survey_design_weights_only): """Propensity scores should change with survey weights (IPW path).""" @@ -991,7 +989,7 @@ def test_weighted_logit(self, staggered_survey_data, survey_design_weights_only) ) # Non-uniform weights should produce different ATT # (propensity scores change with survey weights) - assert r_unw.overall_att != r_w.overall_att + assert r_unw.att != r_w.att def test_ipw_survey_weight_composition(self, staggered_survey_data, survey_design_weights_only): """w_survey x w_ipw should compose — ATT differs from unweighted IPW.""" @@ -1007,7 +1005,7 @@ def test_ipw_survey_weight_composition(self, staggered_survey_data, survey_desig survey_design=survey_design_weights_only, ) # Weighted IPW should produce different ATT than unweighted - assert abs(r_unw.overall_att - r_w.overall_att) > 1e-6 + assert abs(r_unw.att - r_w.att) > 1e-6 def test_aggregation_with_survey(self, staggered_survey_data, survey_design_weights_only): """Simple aggregation should use survey weights.""" @@ -1027,7 +1025,7 @@ def test_aggregation_with_survey(self, staggered_survey_data, survey_design_weig survey_design=survey_design_weights_only, ) # Event study ATTs should differ with non-uniform weights - assert r_unw.overall_att != r_w.overall_att + assert r_unw.att != r_w.att # Event study effects should exist on the post-fit route es_w = r_w.aggregate("event_study") assert len(es_w.event_time) > 0 @@ -1282,12 +1280,10 @@ def test_unbalanced_panel_scale_invariance(self, staggered_survey_data): "first_treat", survey_design=sd2, ) + assert np.isclose(r1.att, r2.att, atol=1e-8), "ATT not scale-invariant on unbalanced panel" assert np.isclose( - r1.overall_att, r2.overall_att, atol=1e-8 - ), "ATT not scale-invariant on unbalanced panel" - assert np.isclose( - r1.overall_se, r2.overall_se, atol=1e-8 - ), f"SE not scale-invariant on unbalanced panel: {r1.overall_se} vs {r2.overall_se}" + r1.se, r2.se, atol=1e-8 + ), f"SE not scale-invariant on unbalanced panel: {r1.se} vs {r2.se}" def test_se_scale_invariance_all_methods(self, staggered_survey_data): """SE should be invariant under weight rescaling for all methods.""" @@ -1315,12 +1311,8 @@ def test_se_scale_invariance_all_methods(self, staggered_survey_data): first_treat="first_treat", survey_design=sd2, ) - assert np.isclose( - r1.overall_att, r2.overall_att, atol=1e-8 - ), f"{method}: ATT not scale-invariant" - assert np.isclose( - r1.overall_se, r2.overall_se, atol=1e-8 - ), f"{method}: SE not scale-invariant" + assert np.isclose(r1.att, r2.att, atol=1e-8), f"{method}: ATT not scale-invariant" + assert np.isclose(r1.se, r2.se, atol=1e-8), f"{method}: SE not scale-invariant" def test_survey_weights_change_per_cell_att(self, staggered_survey_data): """Non-uniform survey weights should change per-cell ATT(g,t).""" @@ -1360,8 +1352,8 @@ def test_strata_psu_fpc_works_inference(self, staggered_survey_data): "first_treat", survey_design=sd_full, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) # ============================================================================= @@ -1395,8 +1387,8 @@ def test_weight_scale_invariance_imputation(self, staggered_survey_data): survey_design=sd2, ) - assert abs(r1.overall_att - r2.overall_att) < 1e-10 - assert abs(r1.overall_se - r2.overall_se) < 1e-8 + assert abs(r1.att - r2.att) < 1e-10 + assert abs(r1.se - r2.se) < 1e-8 def test_weight_scale_invariance_two_stage(self, staggered_survey_data): """TwoStageDiD: 2*w gives same ATT as w.""" @@ -1421,8 +1413,8 @@ def test_weight_scale_invariance_two_stage(self, staggered_survey_data): survey_design=sd2, ) - assert abs(r1.overall_att - r2.overall_att) < 1e-10 - assert abs(r1.overall_se - r2.overall_se) < 1e-8 + assert abs(r1.att - r2.att) < 1e-10 + assert abs(r1.se - r2.se) < 1e-8 def test_weight_scale_invariance_callaway_santanna(self, staggered_survey_data): """CallawaySantAnna: 2*w gives same ATT as w.""" @@ -1447,8 +1439,8 @@ def test_weight_scale_invariance_callaway_santanna(self, staggered_survey_data): survey_design=sd2, ) - assert abs(r1.overall_att - r2.overall_att) < 1e-10 - assert abs(r1.overall_se - r2.overall_se) < 1e-8 + assert abs(r1.att - r2.att) < 1e-10 + assert abs(r1.se - r2.se) < 1e-8 class TestCallawaySantAnnaFullDesignBootstrap: @@ -1474,7 +1466,7 @@ def test_bootstrap_full_design_cs(self, staggered_survey_data): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None assert result.survey_metadata.n_strata is not None diff --git a/tests/test_survey_phase6.py b/tests/test_survey_phase6.py index c6cd7e532..aed6edab3 100644 --- a/tests/test_survey_phase6.py +++ b/tests/test_survey_phase6.py @@ -1350,8 +1350,8 @@ def test_callaway_santanna_replicate(self): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_efficient_did_replicate(self): @@ -1372,8 +1372,8 @@ def test_efficient_did_replicate(self): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None def test_subpopulation_preserves_replicate_metadata(self): @@ -1487,7 +1487,7 @@ def test_sun_abraham_replicate_accepted(self): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) class TestSubpopulationMaskValidation: @@ -1627,7 +1627,7 @@ def test_stacked_did_replicate_accepted(self): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_invalid_replicate_scale_rejected(self): """Negative or zero replicate_scale should be rejected.""" @@ -1701,7 +1701,7 @@ def test_imputation_did_replicate_accepted(self): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_two_stage_did_replicate_accepted(self): """TwoStageDiD now supports replicate-weight designs.""" @@ -1716,7 +1716,7 @@ def test_two_stage_did_replicate_accepted(self): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_bacon_replicate_rejected(self): """BaconDecomposition rejects replicate-weight designs.""" @@ -1797,8 +1797,8 @@ def test_cs_replicate_ipw_dr_no_covariates(self, method): "first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) assert result.survey_metadata is not None @@ -1890,7 +1890,7 @@ def test_continuous_did_positive_weight_gate(self): assert len(weight_warnings) > 0, "Expected warning about insufficient positive-weight units" # Result should still be finite (from the valid cohort-3 cells) assert result is not None - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_triple_diff_positive_weight_gate(self): """TripleDifference falls back to weighted mean when n_eff < n_cols.""" diff --git a/tests/test_survey_phase7a.py b/tests/test_survey_phase7a.py index 2df4e6a9b..d888734b0 100644 --- a/tests/test_survey_phase7a.py +++ b/tests/test_survey_phase7a.py @@ -68,9 +68,9 @@ def test_finite_results(self, staggered_data_with_covariates, survey_weights_onl covariates=["x1"], survey_design=survey_weights_only, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 @pytest.mark.parametrize("method", ["ipw", "dr"]) def test_event_study(self, staggered_data_with_covariates, survey_weights_only, method): @@ -105,9 +105,9 @@ def test_full_design(self, staggered_data_with_covariates, survey_full_design, m covariates=["x1"], survey_design=survey_full_design, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 # ============================================================================= @@ -147,7 +147,7 @@ def test_double_weights_same_att(self, staggered_data_with_covariates, method): survey_design=sd2, ) - np.testing.assert_allclose(r1.overall_att, r2.overall_att, atol=1e-10) + np.testing.assert_allclose(r1.att, r2.att, atol=1e-10) # ============================================================================= @@ -188,7 +188,7 @@ def test_uniform_weights(self, method): ) # ATTs should be very close (not exact due to weight normalization) - np.testing.assert_allclose(r_no_survey.overall_att, r_survey.overall_att, rtol=1e-6) + np.testing.assert_allclose(r_no_survey.att, r_survey.att, rtol=1e-6) # ============================================================================= @@ -220,7 +220,7 @@ def test_dr_se_differs_from_reg(self, staggered_data_with_covariates, survey_wei survey_design=survey_weights_only, ) # SEs should differ because DR has additional IF corrections - assert r_reg.overall_se != r_dr.overall_se + assert r_reg.se != r_dr.se # ============================================================================= @@ -242,7 +242,7 @@ def test_no_covariate_methods_agree(self, staggered_data_with_covariates, survey "first_treat", survey_design=survey_weights_only, ) - results[method] = r.overall_att + results[method] = r.att np.testing.assert_allclose(results["reg"], results["ipw"], atol=1e-10) np.testing.assert_allclose(results["reg"], results["dr"], atol=1e-10) @@ -267,10 +267,10 @@ def test_aggregation(self, staggered_data_with_covariates, survey_weights_only, covariates=["x1"], survey_design=survey_weights_only, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) agg_result = result.aggregate(agg) if agg == "simple": - assert float(agg_result.att[0]) == pytest.approx(result.overall_att) + assert float(agg_result.att[0]) == pytest.approx(result.att) else: assert len(agg_result.att) > 0 assert np.isfinite(agg_result.att).any() @@ -295,7 +295,7 @@ def test_bootstrap_runs(self, staggered_data_with_covariates, survey_weights_onl covariates=["x1"], survey_design=survey_weights_only, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) assert result.bootstrap_results is not None @@ -328,7 +328,7 @@ def test_single_covariate(self): covariates=["binary_x"], survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_not_yet_treated_control(self, staggered_data_with_covariates, survey_weights_only): """control_group='not_yet_treated' with DR + covariates + survey.""" @@ -341,7 +341,7 @@ def test_not_yet_treated_control(self, staggered_data_with_covariates, survey_we covariates=["x1"], survey_design=survey_weights_only, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) def test_universal_base_period(self, staggered_data_with_covariates, survey_weights_only): """base_period='universal' with DR + covariates + survey.""" @@ -354,7 +354,7 @@ def test_universal_base_period(self, staggered_data_with_covariates, survey_weig covariates=["x1"], survey_design=survey_weights_only, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) # ============================================================================= @@ -388,6 +388,6 @@ def test_dr_se_differs_from_reg_no_survey(self): covariates=["x1"], ) # SEs should differ (DR has nuisance IF corrections) - assert r_reg.overall_se != r_dr.overall_se + assert r_reg.se != r_dr.se # But ATTs should be similar (both consistent under correct specification) - assert abs(r_reg.overall_att - r_dr.overall_att) < 1.0 + assert abs(r_reg.att - r_dr.att) < 1.0 diff --git a/tests/test_survey_phase8.py b/tests/test_survey_phase8.py index 541649924..fc7df0528 100644 --- a/tests/test_survey_phase8.py +++ b/tests/test_survey_phase8.py @@ -294,9 +294,9 @@ def test_sdr_end_to_end_callaway_santanna(self): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 # =========================================================================== @@ -331,9 +331,9 @@ def test_imputation_fpc_no_raise(self, staggered_data): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_imputation_fpc_reduces_se(self, staggered_data): """FPC SE should be <= no-FPC SE (by definition).""" @@ -368,7 +368,7 @@ def test_imputation_fpc_reduces_se(self, staggered_data): first_treat="first_treat", survey_design=sd_fpc, ) - assert result_fpc.overall_se <= result_no_fpc.overall_se + 1e-10 + assert result_fpc.se <= result_no_fpc.se + 1e-10 def test_imputation_fpc_event_study(self, staggered_data): """FPC works with the post-fit event-study aggregation.""" @@ -449,9 +449,9 @@ def test_two_stage_fpc_no_raise(self, staggered_data): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_two_stage_fpc_reduces_se(self, staggered_data): """FPC SE should be <= no-FPC SE.""" @@ -485,7 +485,7 @@ def test_two_stage_fpc_reduces_se(self, staggered_data): first_treat="first_treat", survey_design=sd_fpc, ) - assert result_fpc.overall_se <= result_no_fpc.overall_se + 1e-10 + assert result_fpc.se <= result_no_fpc.se + 1e-10 def test_two_stage_fpc_event_study(self, staggered_data): """FPC works with the post-fit event-study aggregation.""" @@ -561,9 +561,9 @@ def test_imputation_unstratified_fpc(self, staggered_data): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_two_stage_unstratified_fpc(self, staggered_data): """TwoStageDiD with weights + psu + fpc (no strata) runs.""" @@ -582,9 +582,9 @@ def test_two_stage_unstratified_fpc(self, staggered_data): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_two_stage_unstratified_fpc_event_study(self, staggered_data): """TwoStageDiD unstratified FPC with event study.""" @@ -651,7 +651,7 @@ def test_two_stage_single_psu_nan_se(self): survey_design=sd, ) # Single PSU = variance unidentified → NaN SE - assert np.isnan(result.overall_se) + assert np.isnan(result.se) class TestPSUMeatHelper: @@ -804,9 +804,9 @@ def test_multiplier_adjust_no_raise(self): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 def test_adjust_nonzero_variance(self): """With 'adjust', singleton strata contribute nonzero bootstrap variance.""" @@ -904,9 +904,9 @@ def test_adjust_all_singleton(self): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) - assert result.overall_se > 0 + assert np.isfinite(result.att) + assert np.isfinite(result.se) + assert result.se > 0 class TestSingleSingletonAdjust: @@ -1133,7 +1133,7 @@ def test_coef_var_overall_att_pattern(self): ) cv = result.coef_var assert np.isfinite(cv) - assert cv == pytest.approx(result.overall_se / abs(result.overall_att)) + assert cv == pytest.approx(result.se / abs(result.att)) # =========================================================================== @@ -1377,7 +1377,7 @@ def test_pretrends_survey_always_treated_psu(self): ) # Should produce finite results even though stratum 0 has # mostly treated PSUs (sparse untreated domain) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) es = result.aggregate("event_study") pre = es.event_time < -1 assert pre.any() @@ -1418,7 +1418,7 @@ def test_pretrends_replicate_raises(self, staggered_data): first_treat="first_treat", survey_design=sd, ) - assert np.isfinite(result.overall_att) + assert np.isfinite(result.att) with pytest.raises(NotImplementedError, match="replicate"): result.aggregate("event_study") diff --git a/tests/test_survey_r_crossvalidation.py b/tests/test_survey_r_crossvalidation.py index 42932192e..467f67c32 100644 --- a/tests/test_survey_r_crossvalidation.py +++ b/tests/test_survey_r_crossvalidation.py @@ -319,7 +319,7 @@ def test_overall_att_matches(self, r_results, key): result, r = self._fit_scenario(r_results, key) _assert_close( - result.overall_att, + result.att, r["overall_att"], rtol=1e-3, atol=0.05, @@ -333,8 +333,8 @@ def test_se_is_design_based(self, r_results, key): pytest.skip(f"Scenario {key} not in R results") result, _ = self._fit_scenario(r_results, key) - assert np.isfinite(result.overall_se), "Overall SE should be finite" - assert result.overall_se > 0, "Overall SE should be positive" + assert np.isfinite(result.se), "Overall SE should be finite" + assert result.se > 0, "Overall SE should be positive" @pytest.mark.parametrize("key", CS_SCENARIOS) def test_design_se_differs_from_naive(self, r_results, key): @@ -380,9 +380,9 @@ def test_design_se_differs_from_naive(self, r_results, key): covariates=covariates, ) - assert result_survey.overall_se != result_naive.overall_se, ( - f"{key}: Design-based SE ({result_survey.overall_se:.6f}) should " - f"differ from naive SE ({result_naive.overall_se:.6f})" + assert result_survey.se != result_naive.se, ( + f"{key}: Design-based SE ({result_survey.se:.6f}) should " + f"differ from naive SE ({result_naive.se:.6f})" ) diff --git a/tests/test_target_parameter.py b/tests/test_target_parameter.py index 2e99d9c72..0760287dc 100644 --- a/tests/test_target_parameter.py +++ b/tests/test_target_parameter.py @@ -474,9 +474,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 500 stub.n_treated = 100 @@ -501,9 +505,13 @@ class CallawaySantAnnaResults: stub = CallawaySantAnnaResults() stub.overall_att = 1.0 + stub.att = 1.0 stub.overall_se = 0.2 + stub.se = 0.2 stub.overall_p_value = 0.001 + stub.p_value = 0.001 stub.overall_conf_int = (0.6, 1.4) + stub.conf_int = (0.6, 1.4) stub.alpha = 0.05 stub.n_obs = 500 stub.n_treated = 100 @@ -581,7 +589,7 @@ def test_twfe_fit_returns_did_results_branch(self): assert "TWFE" in tp["name"] or "TWFE" in tp["definition"] def test_stacked_did_fit_headline_attribute_matches_real_estimand(self): - """``StackedDiDResults.overall_att`` is the average of + """``StackedDiDResults.att`` is the average of post-treatment event-study coefficients ``delta_h`` with delta-method SE (``stacked_did.py`` around line 541). Real-fit regression against the reviewer's R1 P1 wording catch. @@ -630,7 +638,7 @@ def _dcdh_reversible_panel(self, seed): return pd.DataFrame(rows) def test_dcdh_did_m_fit_headline_attribute_is_overall_att(self): - """``ChaisemartinDHaultfoeuilleResults.overall_att`` holds the + """``ChaisemartinDHaultfoeuilleResults.att`` holds the DID_M headline scalar (``chaisemartin_dhaultfoeuille_results.py`` line ~357). R1 P1: previously ``headline_attribute="att"`` pointed at a non-existent attribute. @@ -683,7 +691,7 @@ def test_dcdh_did_1_fit_overall_att_real(self): assert tp["aggregation"] == "DID_1" assert "DID_1" in tp["name"] assert tp["headline_attribute"] == "overall_att" - assert hasattr(fit, "overall_att") + assert hasattr(fit, "att") def test_dcdh_delta_fit_real(self): """Real ``L_max >= 2`` fit: headline is the cost-benefit @@ -707,7 +715,7 @@ def test_dcdh_delta_fit_real(self): assert tp["aggregation"] == "delta" assert "delta" in tp["name"] assert tp["headline_attribute"] == "overall_att" - assert hasattr(fit, "overall_att") + assert hasattr(fit, "att") def test_dcdh_trends_linear_no_scalar_propagates_through_br(self): """PR #347 R4 P1 end-to-end: on the dCDH no-scalar @@ -1232,7 +1240,7 @@ def test_dcdh_trends_linear_with_l_max_geq_2_fit_real(self): trends_linear=True, ) # Real fit must intentionally produce NaN overall_att. - assert math.isnan(fit.overall_att), ( + assert math.isnan(fit.att), ( "trends_linear + L_max>=2 must suppress overall_att (NaN by " "design). If this test fails, the library contract changed — " "update the ``no_scalar_headline`` branch of " diff --git a/tests/test_two_stage.py b/tests/test_two_stage.py index e236ab96e..c40e6a665 100644 --- a/tests/test_two_stage.py +++ b/tests/test_two_stage.py @@ -112,7 +112,7 @@ def test_att_accuracy(self): ) # Should recover ~2.0 with reasonable tolerance - assert abs(results.overall_att - 2.0) < 0.3 + assert abs(results.att - 2.0) < 0.3 def test_se_positive_finite(self): """Test that SEs are positive and finite.""" @@ -121,8 +121,8 @@ def test_se_positive_finite(self): data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert results.overall_se > 0 - assert np.isfinite(results.overall_se) + assert results.se > 0 + assert np.isfinite(results.se) def test_ci_contains_point_estimate(self): """Test that confidence interval contains the point estimate.""" @@ -131,8 +131,8 @@ def test_ci_contains_point_estimate(self): data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert results.overall_conf_int[0] <= results.overall_att - assert results.overall_att <= results.overall_conf_int[1] + assert results.conf_int[0] <= results.att + assert results.att <= results.conf_int[1] def test_t_stat_and_p_value(self): """Test that t-stat and p-value are consistent.""" @@ -141,12 +141,12 @@ def test_t_stat_and_p_value(self): data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert np.isfinite(results.overall_t_stat) - assert 0 <= results.overall_p_value <= 1 + assert np.isfinite(results.t_stat) + assert 0 <= results.p_value <= 1 # t-stat should equal ATT / SE - expected_t = results.overall_att / results.overall_se - assert abs(results.overall_t_stat - expected_t) < 1e-10 + expected_t = results.att / results.se + assert abs(results.t_stat - expected_t) < 1e-10 def test_event_study(self): """Test event study specification.""" @@ -378,7 +378,7 @@ def test_overall_att_matches_imputation(self): data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert abs(ts_results.overall_att - imp_results.overall_att) < 1e-10 + assert abs(ts_results.att - imp_results.att) < 1e-10 def test_event_study_effects_match_imputation(self): """Event study point estimates should match ImputationDiD.""" @@ -461,7 +461,7 @@ def test_ses_differ_from_imputation(self): ) # SEs should differ (different variance estimators) - assert abs(ts_results.overall_se - imp_results.overall_se) > 1e-6 + assert abs(ts_results.se - imp_results.se) > 1e-6 # ============================================================================= @@ -480,8 +480,8 @@ def test_gmm_se_differs_from_naive(self): ) # The GMM SE accounts for first-stage estimation uncertainty - assert results.overall_se > 0 - assert np.isfinite(results.overall_se) + assert results.se > 0 + assert np.isfinite(results.se) def test_event_study_se_positive(self): """Event study SEs should all be positive.""" @@ -528,8 +528,8 @@ def test_sparse_factorized_dense_fallback_emits_warning(self): ) # LSMR fallback must still produce a usable SE. - assert np.isfinite(results.overall_se) - assert results.overall_se > 0 + assert np.isfinite(results.se) + assert results.se > 0 def test_lsmr_fallback_matches_dense_lstsq_oracle(self): """Consumer-level parity on a genuinely singular Stage-1 Gram: any @@ -587,7 +587,7 @@ def _no_lstsq(*a, **k): time="time", first_treat="first_treat", ) - assert np.isfinite(results.overall_se) + assert np.isfinite(results.se) def test_uncertified_lsmr_fails_closed_to_nan(self): """A finite-but-uncertified LSMR result (istop outside {0,1,2,4,5} @@ -615,8 +615,8 @@ def _fake_lsmr(A, b, **kwargs): time="time", first_treat="first_treat", ) - assert np.isnan(results.overall_se) - assert np.isnan(results.overall_p_value) + assert np.isnan(results.se) + assert np.isnan(results.p_value) def test_weighted_survey_lsmr_fallback_no_densify(self): """The weighted/survey GMM-sandwich site (X'_10 W X_10) takes the @@ -655,7 +655,7 @@ def _no_lstsq(*a, **k): first_treat="first_treat", survey_design=SurveyDesign(weights="w"), ) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) def test_forced_fallback_fit_level_lsmr_vs_dense_oracle(self): """Fit-level parity on the FORCED fallback path (the fixture itself @@ -702,8 +702,8 @@ def _dense_oracle(gram_csc, rhs): warnings.simplefilter("ignore") res_dense = _fit() - np.testing.assert_allclose(res_lsmr.overall_att, res_dense.overall_att, rtol=1e-8) - np.testing.assert_allclose(res_lsmr.overall_se, res_dense.overall_se, rtol=1e-6) + np.testing.assert_allclose(res_lsmr.att, res_dense.att, rtol=1e-8) + np.testing.assert_allclose(res_lsmr.se, res_dense.se, rtol=1e-6) def test_sparse_factorized_bootstrap_dense_fallback_emits_warning(self): """Silent-failure audit axis C: the TwoStage bootstrap path has the @@ -731,8 +731,8 @@ def test_sparse_factorized_bootstrap_dense_fallback_emits_warning(self): ) # Bootstrap LSMR fallback must still produce a usable SE. - assert np.isfinite(results.overall_se) - assert results.overall_se > 0 + assert np.isfinite(results.se) + assert results.se > 0 # ============================================================================= @@ -783,8 +783,8 @@ def test_no_never_treated_works(self): data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert results.overall_att > 0 - assert results.overall_se > 0 + assert results.att > 0 + assert results.se > 0 def test_single_cohort(self): """Should work with a single treatment cohort.""" @@ -812,7 +812,7 @@ def test_single_cohort(self): data, outcome="outcome", unit="unit", time="time", first_treat="first_treat" ) - assert abs(results.overall_att - 2.0) < 0.5 + assert abs(results.att - 2.0) < 0.5 assert len(results.groups) == 1 def test_anticipation_shifts_timing(self): @@ -828,7 +828,7 @@ def test_anticipation_shifts_timing(self): # With anticipation, more obs are treated -> different ATT assert results_with_ant.n_treated_obs > results_no_ant.n_treated_obs - assert abs(results_no_ant.overall_att - results_with_ant.overall_att) > 0.01 + assert abs(results_no_ant.att - results_with_ant.att) > 0.01 def test_rank_deficiency_warning(self): """Rank deficiency should emit warning in 'warn' mode.""" @@ -918,8 +918,8 @@ def test_nan_propagation(self): assert nan_horizons_found > 0, "Should have at least one Prop 5 NaN horizon" # Normal results should have finite values - assert np.isfinite(results.overall_t_stat) - assert np.isfinite(results.overall_p_value) + assert np.isfinite(results.t_stat) + assert np.isfinite(results.p_value) def test_covariates(self): """Estimation with covariates should work.""" @@ -937,9 +937,9 @@ def test_covariates(self): covariates=["x1", "x2"], ) - assert results.overall_att > 0 - assert results.overall_se > 0 - assert np.isfinite(results.overall_se) + assert results.att > 0 + assert results.se > 0 + assert np.isfinite(results.se) def test_missing_column_error(self): """Missing required columns should raise ValueError.""" @@ -1034,8 +1034,8 @@ def test_bootstrap_with_nan_y_tilde(self, ci_params): first_treat="first_treat", ) - assert np.isfinite(results.overall_att) - assert results.overall_se > 0 + assert np.isfinite(results.att) + assert results.se > 0 def test_balance_e_empty_cohorts_warns(self): """Unreasonably large balance_e should warn when no cohorts qualify.""" @@ -1202,9 +1202,9 @@ def test_cluster_changes_ses(self): ) # Point estimates should be the same - assert abs(results_unit.overall_att - results_cluster.overall_att) < 1e-10 + assert abs(results_unit.att - results_cluster.att) < 1e-10 # SEs should differ - assert abs(results_unit.overall_se - results_cluster.overall_se) > 1e-6 + assert abs(results_unit.se - results_cluster.se) > 1e-6 def test_horizon_max_limits_horizons(self): """horizon_max should limit event study horizons.""" @@ -1278,9 +1278,9 @@ def test_bootstrap_updates_inference(self, ci_params): ) # Point estimates should match - assert abs(results_analytical.overall_att - results_bootstrap.overall_att) < 1e-10 + assert abs(results_analytical.att - results_bootstrap.att) < 1e-10 # SEs should differ (analytical GMM vs bootstrap) - assert abs(results_analytical.overall_se - results_bootstrap.overall_se) > 1e-6 + assert abs(results_analytical.se - results_bootstrap.se) > 1e-6 def test_bootstrap_event_study(self, ci_params): """Bootstrap should work with event study specification.""" @@ -1425,8 +1425,8 @@ def test_sparse_fallback_path(self): finally: ts_mod._SPARSE_DENSE_THRESHOLD = orig - np.testing.assert_allclose(result_dense.overall_att, result_sparse.overall_att, rtol=1e-10) - np.testing.assert_allclose(result_dense.overall_se, result_sparse.overall_se, rtol=1e-10) + np.testing.assert_allclose(result_dense.att, result_sparse.att, rtol=1e-10) + np.testing.assert_allclose(result_dense.se, result_sparse.se, rtol=1e-10) class TestSilentWarningAudit: @@ -1650,7 +1650,7 @@ def force_drop(A, **kwargs): assert "rank-reducing" in msg assert "X_2'WX_2" in msg # rank-reduced bread must still produce a finite SE. - assert np.isfinite(result.overall_se) + assert np.isfinite(result.se) def test_bootstrap_bread_rank_reduces_and_warns(self): """Same contract for the multiplier-bootstrap bread path (the @@ -1904,8 +1904,8 @@ def test_a_no_always_treated_baseline_survey_path(self): assert result.survey_metadata.n_psu == n_psu_data assert result.survey_metadata.df_survey == n_psu_data - n_strata_data # ATT + SE finite - assert np.isfinite(result.overall_att) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.att) + assert np.isfinite(result.se) def test_b_full_domain_df_survey_under_always_treated_drop(self): """Wave E.3 parity contract: when the always-treated drop removes a @@ -2125,12 +2125,8 @@ def test_g_no_survey_path_unchanged_under_always_treated(self): # Always-treated drop equivalence on the unweighted path: fitting # with the always-treated unit (which gets dropped internally) is # equivalent to fitting on the pre-filtered dataset. - np.testing.assert_allclose( - result_with_at.overall_att, result_without_at.overall_att, rtol=1e-10 - ) - np.testing.assert_allclose( - result_with_at.overall_se, result_without_at.overall_se, rtol=1e-10 - ) + np.testing.assert_allclose(result_with_at.att, result_without_at.att, rtol=1e-10) + np.testing.assert_allclose(result_with_at.se, result_without_at.se, rtol=1e-10) def test_h_psu_entirely_always_treated_unidentified_gate(self): """Optional sharper case: a PSU containing ONLY always-treated units @@ -2154,7 +2150,7 @@ def test_h_psu_entirely_always_treated_unidentified_gate(self): first_treat="g", survey_design=design, ) - assert np.isfinite(result.overall_se) + assert np.isfinite(result.se) assert result.survey_metadata is not None assert result.survey_metadata.n_psu == 6 @@ -2218,8 +2214,8 @@ def _assert_results_bit_equal(r0, r1): def _eq(a, b): return a == b or (np.isnan(a) and np.isnan(b)) - assert _eq(r0.overall_att, r1.overall_att) - assert _eq(r0.overall_se, r1.overall_se) + assert _eq(r0.att, r1.att) + assert _eq(r0.se, r1.se) assert (r0.bootstrap_results is None) == (r1.bootstrap_results is None) if r0.bootstrap_results is not None: for attr in ("event_study_effects", "group_effects"): @@ -2254,10 +2250,10 @@ def _assert_full_bootstrap_nan(r): inference field is NaN, not just the SE (REGISTRY NaN-inference contract). """ # Overall inference fields - assert np.isnan(r.overall_se) - assert np.isnan(r.overall_t_stat) - assert np.isnan(r.overall_p_value) - assert all(np.isnan(x) for x in r.overall_conf_int) + assert np.isnan(r.se) + assert np.isnan(r.t_stat) + assert np.isnan(r.p_value) + assert all(np.isnan(x) for x in r.conf_int) assert np.isnan(r.coef_var) # Bootstrap payload b = r.bootstrap_results @@ -2326,8 +2322,8 @@ def test_fit_clone_idempotence(self): clone = TwoStageDiD(**est.get_params()) r2 = _fit_ts(clone, data) assert clone.vcov_type == "hc1" - assert r1.overall_att == pytest.approx(r2.overall_att) - assert r1.overall_se == pytest.approx(r2.overall_se) + assert r1.att == pytest.approx(r2.att) + assert r1.se == pytest.approx(r2.se) # ---- rejections ---- @@ -2584,8 +2580,8 @@ def test_replicate_covariates_zero_weight_psus_finite_se(self): # Main-fit weights are all positive here, and the per-replicate # nan-ytilde warning is suppressed (warn_nan=False): zero copies. assert not any("non-finite imputed outcomes" in m for m in messages) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) and r.overall_se > 0 + assert np.isfinite(r.att) + assert np.isfinite(r.se) and r.se > 0 def test_main_fit_zero_weight_unit_warns_once_and_fits(self): """Main-fit zero-weight treated unit: nan-ytilde warning UNCHANGED. @@ -2615,8 +2611,8 @@ def test_main_fit_zero_weight_unit_warns_once_and_fits(self): m for m in (str(w.message) for w in rec) if "non-finite imputed outcomes" in m ] assert len(nan_ytilde) >= 1 # main-fit warning still fires - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) and r.overall_se > 0 + assert np.isfinite(r.att) + assert np.isfinite(r.se) and r.se > 0 class TestEventStudyVcovPersistence: diff --git a/tests/test_v4_merge_ddd.py b/tests/test_v4_merge_ddd.py index b28b40ce0..77e5dafed 100644 --- a/tests/test_v4_merge_ddd.py +++ b/tests/test_v4_merge_ddd.py @@ -564,8 +564,8 @@ class TestGateBPrimeStaggeredOracle: def test_base_config_matches_oracle(self, stag): exp = ORACLE_STAGGERED["stag_dr_all"] r = _fit_new(stag, aggregate="all") - assert_allclose(r.overall_att, exp["overall"]["att"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) - assert_allclose(r.overall_se, exp["overall"]["se"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) + assert_allclose(r.att, exp["overall"]["att"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) + assert_allclose(r.se, exp["overall"]["se"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) assert_allclose(r.overall_att_es, exp["overall_att_es"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) assert r.n_obs == exp["n_obs"] assert r.n_never_enabled == exp["n_never_enabled"] @@ -578,8 +578,8 @@ def test_base_config_matches_oracle(self, stag): def test_bootstrap_config_matches_oracle(self, stag): exp = ORACLE_STAGGERED["stag_dr_all_boot49"] r = _fit_new(stag, aggregate="all", ctor=dict(n_bootstrap=49, seed=7, cband=True)) - assert_allclose(r.overall_att, exp["overall"]["att"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) - assert_allclose(r.overall_se, exp["overall"]["se"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) + assert_allclose(r.att, exp["overall"]["att"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) + assert_allclose(r.se, exp["overall"]["se"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) assert_allclose( r.cband_crit_value, exp["cband_crit_value"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL ) @@ -634,8 +634,8 @@ def test_methods_converge_without_covariates(self, stag): for m in ("dr", "ipw", "reg") ] for r in fits[1:]: - assert_allclose(r.overall_att, fits[0].overall_att, rtol=ORACLE_RTOL, atol=ORACLE_ATOL) - assert_allclose(r.overall_se, fits[0].overall_se, rtol=ORACLE_RTOL, atol=ORACLE_ATOL) + assert_allclose(r.att, fits[0].att, rtol=ORACLE_RTOL, atol=ORACLE_ATOL) + assert_allclose(r.se, fits[0].se, rtol=ORACLE_RTOL, atol=ORACLE_ATOL) def test_survey_pweight_matches_oracle(self, stag_survey): exp = ORACLE_STAGGERED["stag_dr_survey_pweight"] @@ -646,8 +646,8 @@ def test_survey_pweight_matches_oracle(self, stag_survey): @staticmethod def _assert_against(r, exp): - assert_allclose(r.overall_att, exp["overall"]["att"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) - assert_allclose(r.overall_se, exp["overall"]["se"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) + assert_allclose(r.att, exp["overall"]["att"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) + assert_allclose(r.se, exp["overall"]["se"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) assert_allclose(r.overall_att_es, exp["overall_att_es"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) got = {f"{g}|{t}": v for (g, t), v in sorted(r.group_time_effects.items())} assert set(got) == set(exp["group_time"]) @@ -683,9 +683,7 @@ def _assert_staggered_parity(old, new): assert a is b, f"{f}: {a!r} vs {b!r}" else: _eq_with_nans(a, b) - _eq_with_nans( - np.asarray(old.overall_conf_int, dtype=float), np.asarray(new.overall_conf_int, dtype=float) - ) + _eq_with_nans(np.asarray(old.conf_int, dtype=float), np.asarray(new.conf_int, dtype=float)) _eq_dicts_with_nans(old.group_time_effects, new.group_time_effects) for attr in ("event_study_effects", "group_effects"): a, b = getattr(old, attr), getattr(new, attr) @@ -717,7 +715,7 @@ def test_control_group_values_are_SEMANTICALLY_different(self, stag): warnings.simplefilter("ignore") nyt = _fit_new(stag, ctor=dict(control_group="not_yet_treated"), aggregate="all") nev = _fit_new(stag, ctor=dict(control_group="never_treated"), aggregate="all") - assert nyt.overall_att != nev.overall_att, ( + assert nyt.att != nev.att, ( "not_yet_treated and never_treated produced identical ATTs - the " "comparison-group branch is not reading control_group" ) @@ -866,7 +864,7 @@ def test_staggered_time_is_the_calendar_column(self, stag): with warnings.catch_warnings(): warnings.simplefilter("ignore") r = _fit_new(stag, aggregate="simple") - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) def test_2x2x2_both_post_and_time_raises(self, cross): with pytest.raises(ValueError, match="pass only post="): @@ -1091,14 +1089,14 @@ def test_positive_inf_still_recodes_with_a_warning(self, stag): with pytest.warns(UserWarning, match="recoding to 0"): r = _fit_new(df, aggregate="simple") plain = _fit_new(stag, aggregate="simple") - _eq_with_nans([r.overall_att, r.overall_se], [plain.overall_att, plain.overall_se]) + _eq_with_nans([r.att, r.se], [plain.att, plain.se]) assert r.n_never_enabled == plain.n_never_enabled def test_valid_cohorts_are_untouched_by_the_guard(self, stag): """Regression pin: the guard changes no accepted fit.""" r = _fit_new(stag, aggregate="all") exp = ORACLE_STAGGERED["stag_dr_all"] - assert_allclose(r.overall_att, exp["overall"]["att"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) + assert_allclose(r.att, exp["overall"]["att"], rtol=ORACLE_RTOL, atol=ORACLE_ATOL) def test_anticipation_guard_survives_attribute_mutation(self, stag): """The engine-level guard also covers the __init__/set_params bypass.""" @@ -1511,7 +1509,7 @@ def test_r_golden_through_the_merged_surface(self): # carries that suite's documented looser envelope (a weighting deviation # on individual values - see TestStaggeredDDDAggregation's docstring). assert_allclose( - r.overall_att, + r.att, scenario["overall_att_simple"], rtol=TestStaggeredDDDAggregation.AGG_RTOL, atol=TestStaggeredDDDAggregation.AGG_ATOL, diff --git a/tests/test_v4_rename_mechanical.py b/tests/test_v4_rename_mechanical.py index 9bc82f5e9..04c9cd0c6 100644 --- a/tests/test_v4_rename_mechanical.py +++ b/tests/test_v4_rename_mechanical.py @@ -234,7 +234,7 @@ def test_canonical_and_positional_silent(self, dcdh_panel): ) r_pos = ChaisemartinDHaultfoeuille().fit(dcdh_panel, "y", "g", "t", "d") _assert_no_future_warning(record) - assert r_kw.overall_att == r_pos.overall_att + assert r_kw.att == r_pos.att def test_group_raises_typeerror(self, dcdh_panel): from diff_diff import ChaisemartinDHaultfoeuille @@ -256,7 +256,7 @@ def test_controls_raises_typeerror(self, dcdh_panel): r_new = ChaisemartinDHaultfoeuille().fit( df, "y", "g", "t", "d", L_max=1, covariates=["x"] ) - assert np.isfinite(r_new.overall_att) + assert np.isfinite(r_new.att) with pytest.raises(TypeError, match=_unexpected("controls")): ChaisemartinDHaultfoeuille().fit(df, "y", "g", "t", "d", L_max=1, controls=["x"]) diff --git a/tests/test_variance_conventions.py b/tests/test_variance_conventions.py index 22050da6f..fa2703b3a 100644 --- a/tests/test_variance_conventions.py +++ b/tests/test_variance_conventions.py @@ -1071,7 +1071,7 @@ def spy(self, X, y, **kw): assert ( captured and captured[-1] == 54 ), f"SA df_ {captured} != 54 (the old sum(levels-1) count gives 53)" - assert np.isfinite(r.overall_se) + assert np.isfinite(r.se) def test_nan_group_key_raises_actionable_error(self): """A NaN absorb key must name the offending column, not surface as diff --git a/tests/test_wooldridge.py b/tests/test_wooldridge.py index eda64b755..950c913fc 100644 --- a/tests/test_wooldridge.py +++ b/tests/test_wooldridge.py @@ -306,9 +306,9 @@ def test_overall_att_finite(self, mpdta): r = est.fit( mpdta, outcome="lemp", unit="countyreal", time="year", first_treat="first_treat" ) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) - assert r.overall_se > 0 + assert np.isfinite(r.att) + assert np.isfinite(r.se) + assert r.se > 0 def test_group_time_effects_populated(self, mpdta): est = WooldridgeDiD() @@ -326,9 +326,9 @@ def test_all_inference_fields_finite(self, mpdta): r = est.fit( mpdta, outcome="lemp", unit="countyreal", time="year", first_treat="first_treat" ) - assert np.isfinite(r.overall_t_stat) - assert np.isfinite(r.overall_p_value) - assert all(np.isfinite(c) for c in r.overall_conf_int) + assert np.isfinite(r.t_stat) + assert np.isfinite(r.p_value) + assert all(np.isfinite(c) for c in r.conf_int) def test_never_treated_control_group(self, mpdta): est = WooldridgeDiD(control_group="never_treated") @@ -366,7 +366,7 @@ def test_simple_matches_manual_weighted_average(self, fitted): post_keys = [(g, t) for (g, t) in w if t >= g] w_total = sum(w[k] for k in post_keys) manual_att = sum(w[k] * gt[k]["att"] for k in post_keys) / w_total - assert abs(fitted.overall_att - manual_att) < 1e-10 + assert abs(fitted.att - manual_att) < 1e-10 def test_aggregate_group_keys_match_cohorts(self, fitted): fitted.aggregate("group") @@ -424,12 +424,12 @@ def test_logit_att_sign(self, binary_panel): """ATT should be positive (treatment increases binary outcome).""" est = WooldridgeDiD(method="logit") r = est.fit(binary_panel, outcome="y", unit="unit", time="time", first_treat="cohort") - assert r.overall_att > 0 + assert r.att > 0 def test_logit_se_positive(self, binary_panel): est = WooldridgeDiD(method="logit") r = est.fit(binary_panel, outcome="y", unit="unit", time="time", first_treat="cohort") - assert r.overall_se > 0 + assert r.se > 0 def test_logit_method_stored(self, binary_panel): est = WooldridgeDiD(method="logit") @@ -460,12 +460,12 @@ def test_poisson_fit_runs(self, count_panel): def test_poisson_att_sign(self, count_panel): est = WooldridgeDiD(method="poisson") r = est.fit(count_panel, outcome="y", unit="unit", time="time", first_treat="cohort") - assert r.overall_att > 0 + assert r.att > 0 def test_poisson_se_positive(self, count_panel): est = WooldridgeDiD(method="poisson") r = est.fit(count_panel, outcome="y", unit="unit", time="time", first_treat="cohort") - assert r.overall_se > 0 + assert r.se > 0 class TestBootstrap: @@ -478,7 +478,7 @@ def test_multiplier_bootstrap_ols(self, ci_params): n_boot = ci_params.bootstrap(50, min_n=19) est = WooldridgeDiD(n_bootstrap=n_boot, seed=42) r = est.fit(df, outcome="lemp", unit="countyreal", time="year", first_treat="first_treat") - assert abs(r.overall_se - r.overall_att) / max(abs(r.overall_att), 1e-8) < 10 + assert abs(r.se - r.att) / max(abs(r.att), 1e-8) < 10 def test_bootstrap_zero_disables(self): from diff_diff.datasets import load_mpdta @@ -486,7 +486,7 @@ def test_bootstrap_zero_disables(self): df = load_mpdta() est = WooldridgeDiD(n_bootstrap=0) r = est.fit(df, outcome="lemp", unit="countyreal", time="year", first_treat="first_treat") - assert np.isfinite(r.overall_se) + assert np.isfinite(r.se) class TestMethodologyCorrectness: @@ -505,7 +505,7 @@ def test_ols_att_sign_direction(self): r = est.fit( df, outcome="lemp", unit="countyreal", time="year", first_treat="first_treat" ) - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) rank = [w for w in caught if "Rank-deficient" in str(w.message)] assert rank == [], f"unexpected rank deficiency: {[str(w.message)[:100] for w in rank]}" @@ -542,7 +542,7 @@ def test_single_cohort_degenerates_to_simple_did(self): df = pd.DataFrame(rows) r = WooldridgeDiD().fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") assert len(r.group_time_effects) == 1 - assert abs(r.overall_att - 1.0) < 0.5 + assert abs(r.att - 1.0) < 0.5 def test_aggregation_weights_sum_to_one(self): """Simple aggregation weights should sum to 1.""" @@ -655,7 +655,7 @@ def test_anticipation_aggregate_semantics(self): est = WooldridgeDiD(anticipation=1) r = est.fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") r.aggregate("event_study").aggregate("group").aggregate("simple") - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) assert r.event_study_effects is not None assert r.group_effects is not None # Anticipation cells (t < g but t >= g - anticipation) should be in @@ -670,7 +670,7 @@ def test_anticipation_aggregate_semantics(self): w = r._gt_weights w_total = sum(w.get(k, 0) for k in post_keys) manual_att = sum(w.get(k, 0) * gt[k]["att"] for k in post_keys) / w_total - assert abs(r.overall_att - manual_att) < 1e-10 + assert abs(r.att - manual_att) < 1e-10 class TestXgvarCovariates: @@ -687,9 +687,9 @@ def test_xgvar_fit_runs(self): df = pd.DataFrame(rows) est = WooldridgeDiD() r = est.fit(df, outcome="y", unit="unit", time="time", first_treat="cohort", xgvar=["x1"]) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) - assert r.overall_se > 0 + assert np.isfinite(r.att) + assert np.isfinite(r.se) + assert r.se > 0 class TestAllEventuallyTreated: @@ -752,7 +752,7 @@ def test_no_never_treated_not_yet_treated_control(self): # Scaled by each cell's own SE rather than a fixed band -- per-cell SEs # here are ~0.17-0.21, so a fixed tolerance is either flaky or vacuous. # The pre-fix values this guards against sat ~9 SE away (-0.0096 etc.). - assert res.overall_att == pytest.approx(1.5502, abs=1e-3) + assert res.att == pytest.approx(1.5502, abs=1e-3) for key, eff in res.group_time_effects.items(): assert ( abs(eff["att"] - 1.5) < 3 * eff["se"] @@ -797,7 +797,7 @@ def test_sparse_panel_no_crash(self): df = pd.DataFrame(rows) est = WooldridgeDiD() r = est.fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) r.aggregate("event_study") assert r.event_study_effects is not None @@ -817,9 +817,9 @@ def test_logit_on_mpdta(self, mpdta): r = est.fit( df, outcome="lemp_bin", unit="countyreal", time="year", first_treat="first_treat" ) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) - assert r.overall_se > 0 + assert np.isfinite(r.att) + assert np.isfinite(r.se) + assert r.se > 0 r.aggregate("event_study") assert r.event_study_effects is not None @@ -829,11 +829,11 @@ def test_poisson_on_mpdta(self, mpdta): df["emp"] = np.exp(df["lemp"]) est = WooldridgeDiD(method="poisson") r = est.fit(df, outcome="emp", unit="countyreal", time="year", first_treat="first_treat") - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) - assert r.overall_se > 0 + assert np.isfinite(r.att) + assert np.isfinite(r.se) + assert r.se > 0 r.aggregate("simple") - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) class TestControlGroupDistinction: @@ -851,12 +851,12 @@ def test_never_treated_differs_from_not_yet_treated(self): r_nt = WooldridgeDiD(control_group="never_treated").fit( df, outcome="lemp", unit="countyreal", time="year", first_treat="first_treat" ) - assert np.isfinite(r_nyt.overall_att) - assert np.isfinite(r_nt.overall_att) + assert np.isfinite(r_nyt.att) + assert np.isfinite(r_nt.att) # They must differ — if they don't, control_group is a no-op - assert r_nyt.overall_att != r_nt.overall_att, ( - f"never_treated ATT ({r_nt.overall_att:.6f}) == not_yet_treated ATT " - f"({r_nyt.overall_att:.6f}); control_group has no effect" + assert r_nyt.att != r_nt.att, ( + f"never_treated ATT ({r_nt.att:.6f}) == not_yet_treated ATT " + f"({r_nyt.att:.6f}); control_group has no effect" ) def test_never_treated_more_interaction_terms(self): @@ -943,10 +943,10 @@ def test_bootstrap_with_coarser_cluster(self): r_region = WooldridgeDiD(n_bootstrap=99, seed=0, cluster="region").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert np.isfinite(r_unit.overall_se) - assert np.isfinite(r_region.overall_se) + assert np.isfinite(r_unit.se) + assert np.isfinite(r_region.se) # Coarser clustering with fewer clusters should produce different SE - assert r_unit.overall_se != r_region.overall_se + assert r_unit.se != r_region.se class TestNonlinearRankDeficiency: @@ -1013,10 +1013,10 @@ def test_logit_with_covariates(self): r_nocov = WooldridgeDiD(method="logit").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert np.isfinite(r_cov.overall_att) - assert np.isfinite(r_cov.overall_se) - assert r_cov.overall_se > 0 - assert r_cov.overall_att != r_nocov.overall_att, "Covariates should affect ATT" + assert np.isfinite(r_cov.att) + assert np.isfinite(r_cov.se) + assert r_cov.se > 0 + assert r_cov.att != r_nocov.att, "Covariates should affect ATT" def test_poisson_with_covariates(self): """Poisson with covariates should produce finite ATT/SE and differ from @@ -1038,10 +1038,10 @@ def test_poisson_with_covariates(self): r_nocov = WooldridgeDiD(method="poisson").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert np.isfinite(r_cov.overall_att) - assert np.isfinite(r_cov.overall_se) - assert r_cov.overall_se > 0 - assert r_cov.overall_att != r_nocov.overall_att, "Covariates should affect ATT" + assert np.isfinite(r_cov.att) + assert np.isfinite(r_cov.se) + assert r_cov.se > 0 + assert r_cov.att != r_nocov.att, "Covariates should affect ATT" class TestCohortTimeInvariance: @@ -1181,8 +1181,8 @@ def test_never_treated_unbalanced_finite(self, unbalanced_data): r = WooldridgeDiD(control_group="never_treated").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) + assert np.isfinite(r.att) + assert np.isfinite(r.se) # Should have pre-treatment placebo cells (OLS never_treated) pre_cells = [(g, t) for (g, t) in r.group_time_effects if t < g] assert len(pre_cells) > 0 @@ -1230,7 +1230,7 @@ def test_logit_never_treated_post_treatment_only(self, binary_data): # All expected post-treatment cells present expected = {(g, t) for g in [3, 4] for t in range(1, 6) if t >= g} assert set(r.group_time_effects.keys()) == expected - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) def test_poisson_never_treated_post_treatment_only(self, count_data): r = WooldridgeDiD(method="poisson", control_group="never_treated").fit( @@ -1240,7 +1240,7 @@ def test_poisson_never_treated_post_treatment_only(self, count_data): assert t >= g, f"Pre-treatment cell ({g},{t}) in nonlinear never_treated" expected = {(g, t) for g in [3, 4] for t in range(1, 6) if t >= g} assert set(r.group_time_effects.keys()) == expected - assert np.isfinite(r.overall_att) + assert np.isfinite(r.att) def test_interaction_matrix_fewer_cols_for_nonlinear(self): """For never_treated, nonlinear methods get fewer interaction columns @@ -1368,9 +1368,7 @@ def test_covariates_affect_ols_att(self, cov_data): r_nocov = WooldridgeDiD().fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert ( - r_cov.overall_att != r_nocov.overall_att - ), "Covariate-adjusted ATT should differ from unadjusted" + assert r_cov.att != r_nocov.att, "Covariate-adjusted ATT should differ from unadjusted" class TestWooldridgeSurvey: @@ -1422,9 +1420,9 @@ def test_ols_survey_runs(self, survey_panel): first_treat="cohort", survey_design=sd, ) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) - assert r.overall_se > 0 + assert np.isfinite(r.att) + assert np.isfinite(r.se) + assert r.se > 0 def test_ols_survey_se_differs_from_naive(self, survey_panel): """Survey SE should differ from naive (unweighted) SE.""" @@ -1446,7 +1444,7 @@ def test_ols_survey_se_differs_from_naive(self, survey_panel): time="time", first_treat="cohort", ) - assert r_survey.overall_se != r_naive.overall_se + assert r_survey.se != r_naive.se def test_logit_survey_runs(self, survey_panel): """Logit with survey design completes.""" @@ -1461,9 +1459,9 @@ def test_logit_survey_runs(self, survey_panel): first_treat="cohort", survey_design=sd, ) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) - assert r.overall_se > 0 + assert np.isfinite(r.att) + assert np.isfinite(r.se) + assert r.se > 0 def test_poisson_survey_runs(self, survey_panel): """Poisson with survey design completes.""" @@ -1478,9 +1476,9 @@ def test_poisson_survey_runs(self, survey_panel): first_treat="cohort", survey_design=sd, ) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) - assert r.overall_se > 0 + assert np.isfinite(r.att) + assert np.isfinite(r.se) + assert r.se > 0 def test_bootstrap_survey_rejected(self, survey_panel): """n_bootstrap > 0 with survey_design raises ValueError.""" @@ -1512,8 +1510,8 @@ def test_weights_only_survey(self, survey_panel): first_treat="cohort", survey_design=sd, ) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) + assert np.isfinite(r.att) + assert np.isfinite(r.se) assert r.survey_metadata is not None def test_survey_metadata_present(self, survey_panel): @@ -1587,7 +1585,7 @@ def test_weights_only_plus_cluster(self, survey_panel): first_treat="cohort", survey_design=sd, ) - assert r.overall_se != r_no_cluster.overall_se + assert r.se != r_no_cluster.se def test_survey_gt_weights_are_counts(self, survey_panel): """Survey aggregation uses cell counts, not survey-weight sums.""" @@ -1673,8 +1671,8 @@ def test_poisson_zero_weight_cell(self, survey_panel): first_treat="cohort", survey_design=sd, ) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) + assert np.isfinite(r.att) + assert np.isfinite(r.se) def test_ols_survey_all_treated_refuses_period_filtering(self, survey_panel): """Comparison-support filtering under `survey_design=` is REFUSED. @@ -1725,7 +1723,7 @@ def test_survey_refusal_is_conditional_on_rows_actually_dropping(self, survey_pa first_treat="cohort", survey_design=sd, ) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) def test_ols_survey_zero_weight_unit_rejected(self, survey_panel): """Zero-weight unit raises ValueError before within_transform.""" @@ -1766,8 +1764,8 @@ def test_logit_survey_zero_weight_cell(self, survey_panel): first_treat="cohort", survey_design=sd, ) - assert np.isfinite(r.overall_att) - assert np.isfinite(r.overall_se) + assert np.isfinite(r.att) + assert np.isfinite(r.se) def test_ols_survey_non_range_index(self, survey_panel): """OLS survey zero-weight guard works with non-RangeIndex DataFrames.""" @@ -1920,8 +1918,8 @@ def test_hc1_se_bit_equal_to_pre_pr_baseline(self): res = WooldridgeDiD(method="ols", vcov_type="hc1").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert res.overall_att == pytest.approx(0.9178849934516233, abs=1e-14) - assert res.overall_se == pytest.approx(0.031906603167527435, abs=1e-14) + assert res.att == pytest.approx(0.9178849934516233, abs=1e-14) + assert res.se == pytest.approx(0.031906603167527435, abs=1e-14) def test_hc2_bm_finite_and_inflates_over_hc1(self): df = _make_vcov_panel() @@ -1933,10 +1931,10 @@ def test_hc2_bm_finite_and_inflates_over_hc1(self): ) for k, eff in res_bm.group_time_effects.items(): assert np.isfinite(eff["se"]) - assert np.isfinite(res_bm.overall_se) - assert res_bm.overall_se > res_hc1.overall_se + assert np.isfinite(res_bm.se) + assert res_bm.se > res_hc1.se # ATT identity across vcov branches (only SE differs) - assert res_bm.overall_att == pytest.approx(res_hc1.overall_att, abs=1e-10) + assert res_bm.att == pytest.approx(res_hc1.att, abs=1e-10) def test_atts_identical_across_vcov_branches(self): """Per-cell ATT estimates must be identical across all 4 vcov branches @@ -1949,7 +1947,7 @@ def test_atts_identical_across_vcov_branches(self): ) ref = results["hc1"] for vt in ("hc2_bm", "hc2", "classical"): - assert results[vt].overall_att == pytest.approx(ref.overall_att, abs=1e-10) + assert results[vt].att == pytest.approx(ref.att, abs=1e-10) for k in ref.group_time_effects: assert results[vt].group_time_effects[k]["att"] == pytest.approx( ref.group_time_effects[k]["att"], abs=1e-10 @@ -1966,7 +1964,7 @@ def test_classical_drops_auto_cluster(self): res = WooldridgeDiD(method="ols", vcov_type="classical").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) assert res.cluster_name is None assert res.n_clusters is None @@ -1975,7 +1973,7 @@ def test_hc2_drops_auto_cluster(self): res = WooldridgeDiD(method="ols", vcov_type="hc2").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.se) assert res.cluster_name is None assert res.n_clusters is None @@ -2079,14 +2077,14 @@ def test_hc2_bm_plus_bootstrap_finite_inference(self): df, outcome="y", unit="unit", time="time", first_treat="cohort" ) # ATT is unchanged by the bootstrap (only SE is overridden) - assert res_boot.overall_att == pytest.approx(res_analytical.overall_att, abs=1e-10) + assert res_boot.att == pytest.approx(res_analytical.att, abs=1e-10) # SE finite + sensible (positive, smaller than the panel SD of y) - assert np.isfinite(res_boot.overall_se) - assert res_boot.overall_se > 0 - assert res_boot.overall_se < df["y"].std() + assert np.isfinite(res_boot.se) + assert res_boot.se > 0 + assert res_boot.se < df["y"].std() # Bootstrap overrides analytical inference for overall ATT - assert np.isfinite(res_boot.overall_t_stat) - assert np.isfinite(res_boot.overall_p_value) + assert np.isfinite(res_boot.t_stat) + assert np.isfinite(res_boot.p_value) # Per-cell SEs still come from the analytical full-dummy CR2-BM path # (bootstrap only overrides overall_*); locks the coef_offset # bootstrap indexing didn't regress the per-cell analytical path. @@ -2148,9 +2146,9 @@ def test_hc2_bm_plus_bootstrap_rank_deficient(self): first_treat="cohort", exovar=["xc", "xc_dup"], ) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) - assert res.overall_se > 0 + assert np.isfinite(res.att) + assert np.isfinite(res.se) + assert res.se > 0 def test_get_params_includes_vcov_type(self): est = WooldridgeDiD(vcov_type="hc2_bm") @@ -2268,8 +2266,8 @@ def test_fit_clone_idempotent_on_vcov_type(self): res1 = est.fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") est2 = WooldridgeDiD(**est.get_params()) res2 = est2.fit(df, outcome="y", unit="unit", time="time", first_treat="cohort") - assert res1.overall_se == pytest.approx(res2.overall_se, abs=1e-14) - assert res1.overall_att == pytest.approx(res2.overall_att, abs=1e-14) + assert res1.se == pytest.approx(res2.se, abs=1e-14) + assert res1.att == pytest.approx(res2.att, abs=1e-14) def test_bm_dof_nan_fails_closed(self, monkeypatch): """When ``_compute_cr2_bm_contrast_dof`` returns NaN, BOTH per-cell @@ -2288,12 +2286,12 @@ def _fake_dof(X, cluster_ids, bread, contrasts): df, outcome="y", unit="unit", time="time", first_treat="cohort" ) # Overall: att + se preserved (sandwich is finite); inference NaN - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) - assert np.isnan(res.overall_t_stat) - assert np.isnan(res.overall_p_value) - assert np.isnan(res.overall_conf_int[0]) - assert np.isnan(res.overall_conf_int[1]) + assert np.isfinite(res.att) + assert np.isfinite(res.se) + assert np.isnan(res.t_stat) + assert np.isnan(res.p_value) + assert np.isnan(res.conf_int[0]) + assert np.isnan(res.conf_int[1]) # Per-cell: same pattern (att + se preserved, inference NaN) for (g, t), eff in res.group_time_effects.items(): assert np.isfinite(eff["att"]), f"cell ({g},{t}) att should be finite" @@ -2415,8 +2413,8 @@ def test_hc2_bm_handles_rank_deficient_reduced_design(self): assert np.isfinite(eff["conf_int"][0]) assert np.isfinite(eff["conf_int"][1]) # Overall ATT inference: finite end-to-end. - assert np.isfinite(res.overall_t_stat) - assert np.isfinite(res.overall_p_value) + assert np.isfinite(res.t_stat) + assert np.isfinite(res.p_value) # All three aggregations (group/calendar/event) must produce finite # inference on identified contrasts under the reduced-design BM path. for agg_type in ("group", "calendar", "event_study"): @@ -2469,8 +2467,8 @@ def test_hc2_bm_handles_rank_deficient_with_unit_invariant_exovar(self): eff["t_stat"] ), f"({k}) t_stat NaN under rank-deficient exovar — BM DOF not threaded" assert np.isfinite(eff["p_value"]) - assert np.isfinite(res.overall_t_stat) - assert np.isfinite(res.overall_p_value) + assert np.isfinite(res.t_stat) + assert np.isfinite(res.p_value) # Group + calendar + event aggregates should all produce finite # inference under the reduced-design BM path. for agg_type in ("group", "calendar", "event_study"): @@ -2571,7 +2569,7 @@ def test_ols_binary_warns_logit(self): df = self._binary_panel() with pytest.warns(UserWarning, match=r"method='logit'.*more appropriate"): res = self._fit(df, method="ols") - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) def test_ols_count_warns_poisson(self): df = self._count_panel() @@ -2604,7 +2602,7 @@ def test_suppression_via_filterwarnings(self): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) res = self._fit(df, method="ols") # must not raise - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) def test_framing_paper_faithful(self): # Lock the paper-faithful framing (Wooldridge 2023: LPT vs IPT + the @@ -2669,8 +2667,8 @@ def test_unit_constant_exovar_snaps_with_cause_warning(self): first_treat="cohort", exovar=["xc"], ) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) class TestReferencePeriodNormalization: @@ -3060,8 +3058,8 @@ def test_valid_fit_is_unaffected(self): res = WooldridgeDiD(method="ols", control_group="never_treated").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) class TestInvalidWeightsCannotHideBehindExclusion: @@ -3167,7 +3165,7 @@ def test_valid_weights_still_fit(self): first_treat="cohort", survey_design=SurveyDesign(weights="w"), ) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) assert set(res.groups) == {3, 4}, "no cohort should be excluded on valid weights" def test_zero_weights_still_shape_support_without_excluding(self): @@ -3212,7 +3210,7 @@ def test_zero_weights_still_shape_support_without_excluding(self): first_treat="cohort", survey_design=SurveyDesign(weights="w"), ) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) assert (4, 3) not in res.group_time_effects, "zero-weight cell should be unsupported" assert [w for w in caught if "no pre-treatment period" in str(w.message)] == [] @@ -3468,7 +3466,7 @@ def test_warn_still_fits_and_warns(self): res = WooldridgeDiD( method="ols", control_group="never_treated", rank_deficient_action="warn" ).fit(self._gapped(), outcome="y", unit="unit", time="time", first_treat="cohort") - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) assert [w for w in caught if "Skipped 1 cohort-time" in str(w.message)] != [] def test_silent_fits_without_warning(self): @@ -3477,7 +3475,7 @@ def test_silent_fits_without_warning(self): res = WooldridgeDiD( method="ols", control_group="never_treated", rank_deficient_action="silent" ).fit(self._gapped(), outcome="y", unit="unit", time="time", first_treat="cohort") - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) assert [w for w in caught if "Skipped" in str(w.message)] == [] @staticmethod @@ -3601,7 +3599,7 @@ def test_connected_unbalanced_panel_is_not_flagged(self): res = WooldridgeDiD(method="ols", control_group="never_treated").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) assert (4, 5) in res.group_time_effects @pytest.mark.parametrize("method", ["logit", "poisson"]) @@ -3638,7 +3636,7 @@ def test_nonlinear_paths_are_not_subject_to_the_unit_fe_condition(self, method): df, outcome="y", unit="unit", time="time", first_treat="cohort" ) assert {k for k in res.group_time_effects if k[1] >= k[0]} == {(4, 4), (4, 5)} - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) def test_ols_still_refuses_the_same_panel(self): """The OLS half of the contrast: the same data IS unidentified there, @@ -3774,7 +3772,7 @@ def test_the_same_panel_fits_once_the_bad_period_is_removed(self): res = WooldridgeDiD(method="ols", control_group="never_treated").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) # True ATT(3,3)=ATT(3,4)=1.0, ATT(4,4)=3.0 -- recovered, not contrasts. assert res.group_time_effects[(3, 3)]["att"] == pytest.approx(1.0, abs=0.1) assert res.group_time_effects[(4, 4)]["att"] == pytest.approx(3.0, abs=0.1) @@ -3957,7 +3955,7 @@ def test_branch_one_reference_move_warns_by_name(self): res = WooldridgeDiD(method="ols", control_group="never_treated").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert res.overall_att == pytest.approx(1.0171, abs=5e-3) + assert res.att == pytest.approx(1.0171, abs=5e-3) def test_branch_one_no_move_when_never_treated_covers_the_period(self): """Companion to the above, on a frame truncated to t <= 5. @@ -3996,7 +3994,7 @@ def test_branch_one_no_move_when_never_treated_covers_the_period(self): df, outcome="y", unit="unit", time="time", first_treat="cohort" ) assert not [m for m in w if "reference period moved" in str(m.message)] - assert res.overall_att == pytest.approx(0.9951, abs=5e-3) + assert res.att == pytest.approx(0.9951, abs=5e-3) def test_branch_two_references_are_invariant(self): """`not_yet_treated`: references provably cannot move, so no warning. @@ -4062,7 +4060,7 @@ def test_nonlinear_all_treated_paths_filter_identically(self, kind): assert sorted(int(g) for g in res.groups) == [3, 5] # 2 of 9 periods removed -> 7/9 of the rows survive. assert res.n_obs == len(df) * 7 // 9 - assert np.isfinite(res.overall_att) and np.isfinite(res.overall_se) + assert np.isfinite(res.att) and np.isfinite(res.se) # Both warnings fire here, exactly as on OLS. assert [m for m in w if "no eligible comparison group" in str(m.message)] @@ -4244,7 +4242,7 @@ def add(u, c, ts, eff): with pytest.raises(ValueError, match="comparison-support filtering removed"): res.aggregate(type="simple", weights="cohort_share") # The default weighting is untouched. - assert np.isfinite(res.aggregate(type="simple", weights="cell").overall_att) + assert np.isfinite(res.aggregate(type="simple", weights="cell").att) def test_every_aggregation_surface_reads_the_filtered_cell_set(self): """`group` / `calendar` / `event` all consume the REDUCED cell set. @@ -4280,7 +4278,7 @@ def test_every_aggregation_surface_reads_the_filtered_cell_set(self): assert sorted(int(k) for k in evt.event_study_effects) == [0, 1, 2, 3, 4] for agg in (grp, cal, evt): - assert np.isfinite(agg.overall_att) and np.isfinite(agg.overall_se) + assert np.isfinite(agg.att) and np.isfinite(agg.se) def test_cohort_share_still_works_when_no_unit_is_lost(self): """The refusal must not fire on the balanced deliverable. @@ -4297,7 +4295,7 @@ def test_cohort_share_still_works_when_no_unit_is_lost(self): ) assert res._cohort_units_dropped == {} agg = res.aggregate(type="simple", weights="cohort_share") - assert np.isfinite(agg.overall_att) + assert np.isfinite(agg.att) def test_cells_derived_groups_did_not_leak_into_the_design(self): """`results.groups` is cells-derived; the DESIGN keeps present cohorts. @@ -4327,7 +4325,7 @@ def test_cells_derived_groups_did_not_leak_into_the_design(self): res = WooldridgeDiD(control_group="not_yet_treated").fit( df, outcome="y", unit="unit", time="time", first_treat="cohort", xtvar=["x"] ) - assert np.isfinite(res.overall_att) + assert np.isfinite(res.att) def test_bootstrap_runs_on_a_filtered_fit(self): """Bootstrap consumes the FILTERED sample's residuals and cluster ids. @@ -4341,8 +4339,8 @@ def test_bootstrap_runs_on_a_filtered_fit(self): res = WooldridgeDiD(control_group="not_yet_treated", n_bootstrap=49, seed=42).fit( df, outcome="y", unit="unit", time="time", first_treat="cohort" ) - assert np.isfinite(res.overall_att) - assert np.isfinite(res.overall_se) + assert np.isfinite(res.att) + assert np.isfinite(res.se) class TestUnsupportedPeriodAction: @@ -4667,14 +4665,10 @@ def _z_p(t_stat): def test_default_hc1_is_t_residual_not_z(self): """The defect fix: the auto-clustered hc1 arm moved z -> t(residual).""" res = WooldridgeDiD().fit(self._panel(), **self._kw) - assert res.overall_p_value != self._z_p(res.overall_t_stat) + assert res.p_value != self._z_p(res.t_stat) # residual df = n - k_kept - absorbed [unit, time] rank; recover it n = 300 - match = [ - d - for d in range(2, n) - if abs(res.overall_p_value - self._t_p(res.overall_t_stat, d)) < 1e-13 - ] + match = [d for d in range(2, n) if abs(res.p_value - self._t_p(res.t_stat, d)) < 1e-13] assert len(match) == 1, match # per-cell inference shares the same df _, eff = next(iter(res.group_time_effects.items())) @@ -4685,15 +4679,15 @@ def test_cluster_matches_g_minus_1(self): r0 = WooldridgeDiD().fit(data, **self._kw) r1 = WooldridgeDiD(df_convention="cluster").fit(data, **self._kw) G = data["unit"].nunique() - assert r1.overall_att == r0.overall_att and r1.overall_se == r0.overall_se - assert r1.overall_p_value == pytest.approx(self._t_p(r1.overall_t_stat, G - 1), rel=1e-12) + assert r1.att == r0.att and r1.se == r0.se + assert r1.p_value == pytest.approx(self._t_p(r1.t_stat, G - 1), rel=1e-12) def test_normal_reproduces_pre39_z(self): data = self._panel() r0 = WooldridgeDiD().fit(data, **self._kw) rn = WooldridgeDiD(df_convention="normal").fit(data, **self._kw) - assert rn.overall_att == r0.overall_att and rn.overall_se == r0.overall_se - assert rn.overall_p_value == pytest.approx(self._z_p(rn.overall_t_stat), rel=1e-14) + assert rn.att == r0.att and rn.se == r0.se + assert rn.p_value == pytest.approx(self._z_p(rn.t_stat), rel=1e-14) def test_classical_and_hc2_bit_identical_under_default(self): """classical/hc2 already used t(n - rank(X)); the ONE resolved @@ -4738,18 +4732,12 @@ def test_postfit_aggregate_reproduces_fit_time_convention(self): """aggregate() consumes the stored resolved fallback on every OLS arm.""" res = WooldridgeDiD().fit(self._panel(), **self._kw) n = 300 - match = [ - d - for d in range(2, n) - if abs(res.overall_p_value - self._t_p(res.overall_t_stat, d)) < 1e-13 - ] + match = [d for d in range(2, n) if abs(res.p_value - self._t_p(res.t_stat, d)) < 1e-13] agg = res.aggregate("group") ge = next(iter(agg.group_effects.values())) assert ge["p_value"] == pytest.approx(self._t_p(ge["t_stat"], match[0]), rel=1e-12) simple = res.aggregate("simple") - assert simple.overall_p_value == pytest.approx( - self._t_p(simple.overall_t_stat, match[0]), rel=1e-12 - ) + assert simple.p_value == pytest.approx(self._t_p(simple.t_stat, match[0]), rel=1e-12) def test_bootstrap_aggregate_group_stays_analytical_t(self): """On a bootstrap fit only the overall p/CI are percentile-overridden; @@ -4788,7 +4776,7 @@ def test_pickle_setstate_migrates_df_one_way(self): assert not hasattr(revived, "_df_one_way") or "_df_one_way" not in revived.__dict__ assert revived.df_convention == "residual" agg = revived.aggregate("simple") - assert np.isfinite(agg.overall_p_value) + assert np.isfinite(agg.p_value) def test_survey_df_precedence_over_knob(self): """Survey design df wins under EVERY knob value: the three fits are @@ -4806,11 +4794,11 @@ def test_survey_df_precedence_over_knob(self): } base = fits["residual"] # the survey df is finite -> t inference even under "normal" - assert base.overall_p_value != self._z_p(base.overall_t_stat) + assert base.p_value != self._z_p(base.t_stat) for conv in ("cluster", "normal"): r = fits[conv] - assert r.overall_p_value == base.overall_p_value - assert r.overall_conf_int == base.overall_conf_int + assert r.p_value == base.p_value + assert r.conf_int == base.conf_int for k, eff in base.group_time_effects.items(): assert r.group_time_effects[k]["p_value"] == eff["p_value"]