Source code for moderndid.didinter.container

"""Result containers for intertemporal DiD estimators."""

from typing import NamedTuple

import numpy as np

from moderndid.core.maketables import build_coef_table, se_type_label, vcov_info_from_bootstrap


[docs] class EffectsResult(NamedTuple): """Container for treatment effects at each horizon. Attributes ---------- horizons : ndarray Event horizons (1, 2, ..., effects). estimates : ndarray Point estimates at each horizon. std_errors : ndarray Standard errors at each horizon. ci_lower : ndarray Lower confidence interval bounds. ci_upper : ndarray Upper confidence interval bounds. n_switchers : ndarray Number of switchers at each horizon. n_observations : ndarray Number of observations at each horizon. """ #: Event horizons (1, 2, ..., effects). horizons: np.ndarray #: Point estimates at each horizon. estimates: np.ndarray #: Standard errors at each horizon. std_errors: np.ndarray #: Lower confidence interval bounds. ci_lower: np.ndarray #: Upper confidence interval bounds. ci_upper: np.ndarray #: Number of switchers at each horizon. n_switchers: np.ndarray #: Number of observations at each horizon. n_observations: np.ndarray
[docs] class PlacebosResult(NamedTuple): """Container for placebo effects at each pre-treatment horizon. Attributes ---------- horizons : ndarray Pre-treatment horizons (-1, -2, ..., -placebo). estimates : ndarray Point estimates at each horizon. std_errors : ndarray Standard errors at each horizon. ci_lower : ndarray Lower confidence interval bounds. ci_upper : ndarray Upper confidence interval bounds. n_switchers : ndarray Number of switchers at each horizon. n_observations : ndarray Number of observations at each horizon. """ #: Pre-treatment horizons (-1, -2, ..., -placebo). horizons: np.ndarray #: Point estimates at each horizon. estimates: np.ndarray #: Standard errors at each horizon. std_errors: np.ndarray #: Lower confidence interval bounds. ci_lower: np.ndarray #: Upper confidence interval bounds. ci_upper: np.ndarray #: Number of switchers at each horizon. n_switchers: np.ndarray #: Number of observations at each horizon. n_observations: np.ndarray
[docs] class ATEResult(NamedTuple): """Container for average total effect. Attributes ---------- estimate : float Point estimate of the average total effect. std_error : float Standard error of the estimate. ci_lower : float Lower confidence interval bound. ci_upper : float Upper confidence interval bound. n_observations : float Total observations contributing to the ATE. n_switchers : float Total switchers contributing to the ATE. """ #: Point estimate of the average total effect. estimate: float #: Standard error of the estimate. std_error: float #: Lower confidence interval bound. ci_lower: float #: Upper confidence interval bound. ci_upper: float #: Total observations contributing to the ATE. n_observations: float = 0.0 #: Total switchers contributing to the ATE. n_switchers: float = 0.0
[docs] class HeterogeneityResult(NamedTuple): """Container for heterogeneous effects analysis. Attributes ---------- horizon : int Effect horizon analyzed. covariates : list[str] Covariate names. estimates : np.ndarray Coefficient estimates for each covariate. std_errors : np.ndarray Standard errors for each coefficient. t_stats : np.ndarray T-statistics for each coefficient. ci_lower : np.ndarray Lower confidence interval bounds. ci_upper : np.ndarray Upper confidence interval bounds. n_obs : int Number of observations in the regression. f_pvalue : float P-value from joint F-test that all covariate coefficients are zero. """ #: Effect horizon analyzed. horizon: int #: Covariate names. covariates: list[str] #: Coefficient estimates for each covariate. estimates: np.ndarray #: Standard errors for each coefficient. std_errors: np.ndarray #: T-statistics for each coefficient. t_stats: np.ndarray #: Lower confidence interval bounds. ci_lower: np.ndarray #: Upper confidence interval bounds. ci_upper: np.ndarray #: Number of observations in the regression. n_obs: int #: P-value from joint F-test that all covariate coefficients are zero. f_pvalue: float
[docs] class DIDInterResult(NamedTuple): """Container for DIDInter estimation results. This class implements the ``maketables`` plug-in interface for publication-quality tables. See :ref:`publication_tables`. Attributes ---------- effects : EffectsResult Treatment effects for each post-treatment horizon. placebos : PlacebosResult, optional Placebo effects for each pre-treatment horizon. ate : ATEResult, optional Average total effect across all horizons. n_units : int Total number of units in the sample. n_switchers : int Number of switchers in the sample. n_never_switchers : int Number of never-switchers in the sample. ci_level : float Confidence level used for intervals (e.g., 95.0). effects_equal_test : dict, optional Test for equality of effects across horizons. placebo_joint_test : dict, optional Joint test that all placebo effects are zero. influence_effects : ndarray, optional Influence function for effects. influence_placebos : ndarray, optional Influence function for placebos. heterogeneity : list[HeterogeneityResult], optional Heterogeneous effects analysis results for each horizon. estimation_params : dict Parameters used for estimation. vcov_warnings : list Variance-covariance warnings. """ #: Treatment effects for each post-treatment horizon. effects: EffectsResult #: Placebo effects for each pre-treatment horizon. placebos: PlacebosResult | None = None #: Average total effect across all horizons. ate: ATEResult | None = None #: Total number of units in the sample. n_units: int = 0 #: Number of switchers in the sample. n_switchers: int = 0 #: Number of never-switchers in the sample. n_never_switchers: int = 0 #: Confidence level used for intervals. ci_level: float = 95.0 #: Test for equality of effects across horizons. effects_equal_test: dict | None = None #: Joint test that all placebo effects are zero. placebo_joint_test: dict | None = None #: Influence function for effects. influence_effects: np.ndarray | None = None #: Influence function for placebos. influence_placebos: np.ndarray | None = None #: Heterogeneous effects analysis results. heterogeneity: list | None = None #: Parameters used for estimation. estimation_params: dict = {} #: Variance-covariance warnings. vcov_warnings: list = [] @property def __maketables_coef_table__(self): """Return canonical coefficient table for maketables.""" names: list[str] = [] estimates: list[float] = [] se: list[float] = [] ci95l: list[float] = [] ci95u: list[float] = [] if self.ate is not None: names.append("ATE") estimates.append(float(self.ate.estimate)) se.append(float(self.ate.std_error)) ci95l.append(float(self.ate.ci_lower)) ci95u.append(float(self.ate.ci_upper)) for horizon, estimate, std_error, lower, upper in zip( self.effects.horizons, self.effects.estimates, self.effects.std_errors, self.effects.ci_lower, self.effects.ci_upper, strict=False, ): names.append(f"Effect h={int(horizon)}") estimates.append(float(estimate)) se.append(float(std_error)) ci95l.append(float(lower)) ci95u.append(float(upper)) if self.placebos is not None: for horizon, estimate, std_error, lower, upper in zip( self.placebos.horizons, self.placebos.estimates, self.placebos.std_errors, self.placebos.ci_lower, self.placebos.ci_upper, strict=False, ): names.append(f"Placebo h={int(horizon)}") estimates.append(float(estimate)) se.append(float(std_error)) ci95l.append(float(lower)) ci95u.append(float(upper)) return build_coef_table(names, estimates, se, ci95l=ci95l, ci95u=ci95u) def __maketables_stat__(self, key: str) -> int | float | str | None: """Return model-level statistics for maketables.""" if key == "N": if self.n_units > 0: return int(self.n_units) if len(self.effects.n_observations) > 0: return int(np.nanmax(self.effects.n_observations)) return None if key == "n_switchers": if self.n_switchers > 0: return int(self.n_switchers) if len(self.effects.n_switchers) > 0: return int(np.nanmax(self.effects.n_switchers)) return None if key == "n_never_switchers": return int(self.n_never_switchers) if self.n_never_switchers > 0 else None if key == "se_type": cluster = self.estimation_params.get("cluster") return "Clustered" if cluster else se_type_label(False) if key == "placebo_joint_pvalue": if self.placebo_joint_test is None: return None return self.placebo_joint_test.get("p_value") if key == "effects_equal_pvalue": if self.effects_equal_test is None: return None return self.effects_equal_test.get("p_value") return None @property def __maketables_depvar__(self) -> str: """Return dependent variable label for maketables.""" return str(self.estimation_params.get("yname", "Intertemporal ATT")) @property def __maketables_fixef_string__(self) -> str | None: """Intertemporal DiD output does not report fixed-effects formulas.""" return None @property def __maketables_vcov_info__(self) -> dict[str, str | None]: """Return variance-covariance metadata.""" cluster = self.estimation_params.get("cluster") return vcov_info_from_bootstrap( is_bootstrap=False, cluster=cluster, clustered_label="clustered", ) @property def __maketables_stat_labels__(self) -> dict[str, str]: """Return custom labels for model-level statistics.""" return { "n_switchers": "Switchers", "n_never_switchers": "Never-switchers", "placebo_joint_pvalue": "Joint placebo p-value", "effects_equal_pvalue": "Equal effects p-value", } @property def __maketables_default_stat_keys__(self) -> list[str]: """Default model-level stats to display in ETable.""" keys = ["N", "n_switchers", "n_never_switchers", "se_type"] if self.placebo_joint_test is not None: keys.append("placebo_joint_pvalue") if self.effects_equal_test is not None: keys.append("effects_equal_pvalue") return keys