Source code for moderndid.didtriple.container

"""Result containers for triple difference-in-differences estimators."""

from typing import Literal, NamedTuple

import numpy as np
from scipy import stats

from moderndid.core.maketables import (
    build_coef_table,
    build_coef_table_with_ci,
    build_single_coef_table,
    ci_from_se,
    control_group_label,
    make_effect_names,
    make_group_time_names,
    n_from_first_dim,
    se_type_label,
    vcov_info_from_bootstrap,
)
from moderndid.core.result import extract_vcov_info


[docs] class DDDAggResult(NamedTuple): """Container for aggregated DDD treatment effect parameters. This class implements the ``maketables`` plug-in interface for publication-quality tables. See :ref:`publication_tables`. Attributes ---------- overall_att : float The estimated overall average treatment effect on the treated. overall_se : float Standard error for overall ATT. aggregation_type : {'simple', 'eventstudy', 'group', 'calendar'} Type of aggregation performed. egt : ndarray, optional Event times, groups, or calendar times depending on aggregation type. att_egt : ndarray, optional ATT estimates for each element in egt. se_egt : ndarray, optional Standard errors for each element in egt. crit_val : float Critical value for confidence intervals. inf_func : ndarray, optional Influence function matrix for disaggregated effects. inf_func_overall : ndarray, optional Influence function for the overall ATT. args : dict Arguments used for aggregation. """ #: Estimated overall average treatment effect on the treated. overall_att: float #: Standard error for overall ATT. overall_se: float #: Type of aggregation performed. aggregation_type: Literal["simple", "eventstudy", "group", "calendar"] #: Event times, groups, or calendar times depending on aggregation type. egt: np.ndarray | None = None #: ATT estimates for each element in egt. att_egt: np.ndarray | None = None #: Standard errors for each element in egt. se_egt: np.ndarray | None = None #: Critical value for confidence intervals. crit_val: float = 1.96 #: Influence function matrix for disaggregated effects. inf_func: np.ndarray | None = None #: Influence function for the overall ATT. inf_func_overall: np.ndarray | None = None #: Arguments used for aggregation. args: dict = {} @property def __maketables_coef_table__(self): """Return canonical coefficient table for maketables.""" alpha = float(self.args.get("alpha", 0.05)) names = ["Overall ATT"] estimates = [self.overall_att] se = [self.overall_se] crit = None if self.egt is not None and self.att_egt is not None and self.se_egt is not None: z_crit = stats.norm.ppf(1 - alpha / 2) prefix = {"eventstudy": "Event", "group": "Group", "calendar": "Time"}.get( self.aggregation_type, "Effect", ) names.extend(make_effect_names(self.egt, prefix=prefix)) estimates.extend(np.asarray(self.att_egt, dtype=float).tolist()) se.extend(np.asarray(self.se_egt, dtype=float).tolist()) event_crit = np.full(len(self.egt), self.crit_val) crit = np.concatenate([[z_crit], event_crit]) return build_coef_table_with_ci(names, estimates, se, alpha=alpha, critical_values=crit) def __maketables_stat__(self, key: str) -> int | float | str | None: """Return model-level statistics for maketables.""" if key == "N": n_obs = n_from_first_dim(self.inf_func_overall) if n_obs is not None: return n_obs return n_from_first_dim(self.inf_func) if key == "aggregation": return self.aggregation_type if key == "se_type": return se_type_label(bool(self.args.get("boot", False))) if key == "control_group": return control_group_label(self.args.get("control_group")) if key == "est_method": return self.args.get("est_method") return None @property def __maketables_depvar__(self) -> str: """Return dependent variable label for maketables.""" return str(self.args.get("yname", "Aggregated DDD ATT")) @property def __maketables_fixef_string__(self) -> str | None: """DDD aggregation output does not report fixed-effects formulas.""" return None @property def __maketables_vcov_info__(self) -> dict[str, str | None]: """Return variance-covariance metadata.""" return extract_vcov_info(self.args, bootstrap_key="boot") @property def __maketables_stat_labels__(self) -> dict[str, str]: """Return custom labels for model-level statistics.""" return { "aggregation": "Aggregation", "control_group": "Control Group", "est_method": "Estimation Method", } @property def __maketables_default_stat_keys__(self) -> list[str]: """Default model-level stats to display in ETable.""" keys = ["aggregation", "se_type", "control_group"] if self.__maketables_stat__("N") is not None: keys.insert(0, "N") if self.args.get("est_method") is not None: keys.append("est_method") return keys
[docs] class DDDPanelResult(NamedTuple): """Container for DDD panel estimation results. This class implements the ``maketables`` plug-in interface for publication-quality tables. See :ref:`publication_tables`. Attributes ---------- att : float The DDD point estimate for the ATT. se : float Standard error of the ATT estimate. uci : float Upper bound of the 95% confidence interval. lci : float Lower bound of the 95% confidence interval. boots : ndarray or None Bootstrap draws if bootstrap inference was used. att_inf_func : ndarray or None Influence function if requested. did_atts : dict Individual DiD ATT estimates for each comparison. subgroup_counts : dict Number of units in each subgroup. args : dict Arguments used for estimation. """ #: DDD point estimate for the ATT. att: float #: Standard error of the ATT estimate. se: float #: Upper bound of the 95% confidence interval. uci: float #: Lower bound of the 95% confidence interval. lci: float #: Bootstrap draws if bootstrap inference was used. boots: np.ndarray | None #: Influence function if requested. att_inf_func: np.ndarray | None #: Individual DiD ATT estimates for each comparison. did_atts: dict #: Number of units in each subgroup. subgroup_counts: dict #: Arguments used for estimation. args: dict @property def __maketables_coef_table__(self): """Return canonical coefficient table for maketables.""" return build_single_coef_table("ATT", self.att, self.se, ci95l=self.lci, ci95u=self.uci) def __maketables_stat__(self, key: str) -> int | float | str | None: """Return model-level statistics for maketables.""" if key == "N": return int(sum(self.subgroup_counts.values())) if key == "se_type": return se_type_label(bool(self.args.get("boot", False))) if key == "est_method": return self.args.get("est_method") return None @property def __maketables_depvar__(self) -> str: """Return dependent variable label for maketables.""" return str(self.args.get("yname", "DDD ATT")) @property def __maketables_fixef_string__(self) -> str | None: """DDD results do not report fixed-effects formulas.""" return None @property def __maketables_vcov_info__(self) -> dict[str, str | None]: """Return variance-covariance metadata.""" return vcov_info_from_bootstrap(is_bootstrap=bool(self.args.get("boot", False))) @property def __maketables_stat_labels__(self) -> dict[str, str]: """Return custom labels for model-level statistics.""" return {"est_method": "Estimation Method"} @property def __maketables_default_stat_keys__(self) -> list[str]: """Default model-level stats to display in ETable.""" return ["N", "se_type", "est_method"]
[docs] class DDDRCResult(NamedTuple): """Container for DDD repeated cross-section estimation results. This class implements the ``maketables`` plug-in interface for publication-quality tables. See :ref:`publication_tables`. Attributes ---------- att : float The DDD point estimate for the ATT. se : float Standard error of the ATT estimate. uci : float Upper bound of the 95% confidence interval. lci : float Lower bound of the 95% confidence interval. boots : ndarray or None Bootstrap draws if bootstrap inference was used. att_inf_func : ndarray or None Influence function if requested. did_atts : dict Individual DiD ATT estimates for each comparison. subgroup_counts : dict Number of observations in each subgroup. args : dict Arguments used for estimation. """ #: DDD point estimate for the ATT. att: float #: Standard error of the ATT estimate. se: float #: Upper bound of the 95% confidence interval. uci: float #: Lower bound of the 95% confidence interval. lci: float #: Bootstrap draws if bootstrap inference was used. boots: np.ndarray | None #: Influence function if requested. att_inf_func: np.ndarray | None #: Individual DiD ATT estimates for each comparison. did_atts: dict #: Number of observations in each subgroup. subgroup_counts: dict #: Arguments used for estimation. args: dict @property def __maketables_coef_table__(self): """Return canonical coefficient table for maketables.""" return build_single_coef_table("ATT", self.att, self.se, ci95l=self.lci, ci95u=self.uci) def __maketables_stat__(self, key: str) -> int | float | str | None: """Return model-level statistics for maketables.""" if key == "N": return int(sum(self.subgroup_counts.values())) if key == "se_type": return se_type_label(bool(self.args.get("boot", False))) if key == "est_method": return self.args.get("est_method") return None @property def __maketables_depvar__(self) -> str: """Return dependent variable label for maketables.""" return str(self.args.get("yname", "DDD ATT")) @property def __maketables_fixef_string__(self) -> str | None: """DDD results do not report fixed-effects formulas.""" return None @property def __maketables_vcov_info__(self) -> dict[str, str | None]: """Return variance-covariance metadata.""" return vcov_info_from_bootstrap(is_bootstrap=bool(self.args.get("boot", False))) @property def __maketables_stat_labels__(self) -> dict[str, str]: """Return custom labels for model-level statistics.""" return {"est_method": "Estimation Method"} @property def __maketables_default_stat_keys__(self) -> list[str]: """Default model-level stats to display in ETable.""" return ["N", "se_type", "est_method"]
class ATTgtResult(NamedTuple): """Container for a single (g,t) cell from the multi-period DDD estimator.""" #: DDD point estimate for this (g,t) cell. att: float #: Group identifier (first treatment period). group: int #: Time period. time: int #: Whether this is a post-treatment period (1) or not (0). post: int
[docs] class DDDMultiPeriodResult(NamedTuple): """Container for multi-period DDD estimation results. This class implements the ``maketables`` plug-in interface for publication-quality tables. See :ref:`publication_tables`. Attributes ---------- att : ndarray Array of ATT(g,t) point estimates. se : ndarray Array of standard errors for each ATT(g,t). uci : ndarray Array of upper confidence interval bounds. lci : ndarray Array of lower confidence interval bounds. groups : ndarray Array of treatment cohort identifiers for each estimate. times : ndarray Array of time period identifiers for each estimate. glist : ndarray Unique treatment cohorts. tlist : ndarray Unique time periods. inf_func_mat : ndarray Matrix of influence functions (n_units x n_estimates). n : int Number of units. args : dict Arguments used for estimation. unit_groups : ndarray Array of treatment group for each unit (length n). """ #: Array of ATT(g,t) point estimates. att: np.ndarray #: Array of standard errors for each ATT(g,t). se: np.ndarray #: Array of upper confidence interval bounds. uci: np.ndarray #: Array of lower confidence interval bounds. lci: np.ndarray #: Array of treatment cohort identifiers for each estimate. groups: np.ndarray #: Array of time period identifiers for each estimate. times: np.ndarray #: Unique treatment cohorts. glist: np.ndarray #: Unique time periods. tlist: np.ndarray #: Matrix of influence functions (n_units x n_estimates). inf_func_mat: np.ndarray #: Number of units. n: int #: Arguments used for estimation. args: dict #: Array of treatment group for each unit. unit_groups: np.ndarray @property def __maketables_coef_table__(self): """Return canonical coefficient table for maketables.""" names = make_group_time_names(self.groups, self.times, prefix="ATT") ci90l, ci90u = ci_from_se(self.att, self.se, alpha=0.10) return build_coef_table(names, self.att, self.se, ci95l=self.lci, ci95u=self.uci, ci90l=ci90l, ci90u=ci90u) def __maketables_stat__(self, key: str) -> int | float | str | None: """Return model-level statistics for maketables.""" if key == "N": return int(self.n) if key == "n_cohorts": return len(self.glist) if key == "n_periods": return len(self.tlist) if key == "se_type": return se_type_label(bool(self.args.get("boot", False))) if key == "control_group": return control_group_label(self.args.get("control_group")) if key == "base_period": return self.args.get("base_period") if key == "est_method": return self.args.get("est_method") return None @property def __maketables_depvar__(self) -> str: """Return dependent variable label for maketables.""" return str(self.args.get("yname", "DDD ATT(g,t)")) @property def __maketables_fixef_string__(self) -> str | None: """DDD group-time outputs do not report fixed-effects formulas.""" return None @property def __maketables_vcov_info__(self) -> dict[str, str | None]: """Return variance-covariance metadata.""" return extract_vcov_info(self.args, bootstrap_key="boot") @property def __maketables_stat_labels__(self) -> dict[str, str]: """Return custom labels for model-level statistics.""" return { "n_cohorts": "Treatment Cohorts", "n_periods": "Time Periods", "control_group": "Control Group", "base_period": "Base Period", "est_method": "Estimation Method", } @property def __maketables_default_stat_keys__(self) -> list[str]: """Default model-level stats to display in ETable.""" return ["N", "n_cohorts", "n_periods", "se_type", "control_group", "base_period", "est_method"]
class ATTgtRCResult(NamedTuple): """Container for a single (g,t) cell from the multi-period DDD RCS estimator.""" #: DDD point estimate for this (g,t) cell. att: float #: Group identifier (first treatment period). group: int #: Time period. time: int #: Whether this is a post-treatment period (1) or not (0). post: int
[docs] class DDDMultiPeriodRCResult(NamedTuple): """Container for multi-period DDD repeated cross-section estimation results. This class implements the ``maketables`` plug-in interface for publication-quality tables. See :ref:`publication_tables`. Attributes ---------- att : ndarray Array of ATT(g,t) point estimates. se : ndarray Array of standard errors for each ATT(g,t). uci : ndarray Array of upper confidence interval bounds. lci : ndarray Array of lower confidence interval bounds. groups : ndarray Array of treatment cohort identifiers for each estimate. times : ndarray Array of time period identifiers for each estimate. glist : ndarray Unique treatment cohorts. tlist : ndarray Unique time periods. inf_func_mat : ndarray Matrix of influence functions (n_obs x n_estimates). n : int Number of observations (not units, since this is RCS). args : dict Arguments used for estimation. unit_groups : ndarray Array of treatment group for each observation (length n). """ #: Array of ATT(g,t) point estimates. att: np.ndarray #: Array of standard errors for each ATT(g,t). se: np.ndarray #: Array of upper confidence interval bounds. uci: np.ndarray #: Array of lower confidence interval bounds. lci: np.ndarray #: Array of treatment cohort identifiers for each estimate. groups: np.ndarray #: Array of time period identifiers for each estimate. times: np.ndarray #: Unique treatment cohorts. glist: np.ndarray #: Unique time periods. tlist: np.ndarray #: Matrix of influence functions (n_obs x n_estimates). inf_func_mat: np.ndarray #: Number of observations. n: int #: Arguments used for estimation. args: dict #: Array of treatment group for each observation. unit_groups: np.ndarray @property def __maketables_coef_table__(self): """Return canonical coefficient table for maketables.""" names = make_group_time_names(self.groups, self.times, prefix="ATT") ci90l, ci90u = ci_from_se(self.att, self.se, alpha=0.10) return build_coef_table(names, self.att, self.se, ci95l=self.lci, ci95u=self.uci, ci90l=ci90l, ci90u=ci90u) def __maketables_stat__(self, key: str) -> int | float | str | None: """Return model-level statistics for maketables.""" if key == "N": return int(self.n) if key == "n_cohorts": return len(self.glist) if key == "n_periods": return len(self.tlist) if key == "se_type": return se_type_label(bool(self.args.get("boot", False))) if key == "control_group": return control_group_label(self.args.get("control_group")) if key == "base_period": return self.args.get("base_period") if key == "est_method": return self.args.get("est_method") return None @property def __maketables_depvar__(self) -> str: """Return dependent variable label for maketables.""" return str(self.args.get("yname", "DDD ATT(g,t)")) @property def __maketables_fixef_string__(self) -> str | None: """DDD group-time outputs do not report fixed-effects formulas.""" return None @property def __maketables_vcov_info__(self) -> dict[str, str | None]: """Return variance-covariance metadata.""" return extract_vcov_info(self.args, bootstrap_key="boot") @property def __maketables_stat_labels__(self) -> dict[str, str]: """Return custom labels for model-level statistics.""" return { "n_cohorts": "Treatment Cohorts", "n_periods": "Time Periods", "control_group": "Control Group", "base_period": "Base Period", "est_method": "Estimation Method", } @property def __maketables_default_stat_keys__(self) -> list[str]: """Default model-level stats to display in ETable.""" return ["N", "n_cohorts", "n_periods", "se_type", "control_group", "base_period", "est_method"]