"""Result containers for ETWFE and EMFX output."""
from typing import NamedTuple
import numpy as np
from scipy import stats
from moderndid.core.maketables import (
build_coef_table_with_ci,
make_effect_names,
make_group_time_names,
)
[docs]
class EtwfeResult(NamedTuple):
"""Container for ETWFE regression output.
This class implements the ``maketables`` plug-in interface for
publication-quality tables. See :ref:`publication_tables`.
Returned by :func:`~moderndid.etwfe.estimator.etwfe`. Stores the saturated
TWFE regression coefficients and variance-covariance matrix needed by
:func:`~moderndid.etwfe.emfx.emfx` for aggregation.
Attributes
----------
coefficients : ndarray
Coefficient estimates for each cohort x time interaction term.
std_errors : ndarray
Standard errors for each coefficient.
vcov : ndarray
Variance-covariance matrix of the interaction coefficients.
coef_names : list[str]
Names for each coefficient (from pyfixest).
gt_pairs : list[tuple[float, float]]
(group, time) pair for each coefficient.
n_obs : int
Number of observations used in estimation.
n_units : int
Number of unique cross-sectional units.
r_squared : float or None
R-squared of the regression.
adj_r_squared : float or None
Adjusted R-squared of the regression.
data : object or None
Preprocessed DataFrame (used by emfx for cell counts).
config : object or None
EtwfeConfig used for estimation.
estimation_params : dict
Additional estimation parameters.
"""
#: Coefficient estimates for each cohort x time interaction term.
coefficients: np.ndarray
#: Standard errors for each coefficient.
std_errors: np.ndarray
#: Variance-covariance matrix of the interaction coefficients.
vcov: np.ndarray
#: Names for each coefficient from pyfixest.
coef_names: list
#: (group, time) pair for each coefficient.
gt_pairs: list
#: Number of observations used in estimation.
n_obs: int
#: Number of unique cross-sectional units.
n_units: int
#: R-squared of the regression.
r_squared: float | None = None
#: Adjusted R-squared of the regression.
adj_r_squared: float | None = None
#: Preprocessed DataFrame (used by emfx for aggregation).
data: object = None
#: EtwfeConfig used for estimation.
config: object = None
#: Estimation parameters (yname, cgroup, formula, etc.).
estimation_params: dict = {}
@property
def __maketables_coef_table__(self):
"""Return canonical coefficient table for maketables."""
alpha = float(self.estimation_params.get("alpha", 0.05))
names = make_group_time_names(
[g for g, _ in self.gt_pairs],
[t for _, t in self.gt_pairs],
prefix="ATT",
)
return build_coef_table_with_ci(names, self.coefficients, self.std_errors, alpha=alpha)
def __maketables_stat__(self, key: str) -> int | float | str | None:
"""Return model-level statistics for maketables."""
if key == "N":
return self.n_obs
if key == "n_units":
return self.n_units
if key == "R2":
return self.r_squared
if key == "R2_adj":
return self.adj_r_squared
if key == "se_type":
return self.estimation_params.get("vcov_type", "CRV1")
return None
@property
def __maketables_depvar__(self) -> str:
"""Return dependent variable label for maketables."""
return str(self.estimation_params.get("yname", "ETWFE"))
@property
def __maketables_fixef_string__(self) -> str | None:
"""Return fixed-effects formula string."""
return self.estimation_params.get("fe_spec")
@property
def __maketables_vcov_info__(self) -> dict[str, str | None]:
"""Return variance-covariance metadata."""
return {
"vcov_type": self.estimation_params.get("vcov_type", "CRV1"),
"clustervar": self.estimation_params.get("clustervar"),
}
@property
def __maketables_stat_labels__(self) -> dict[str, str]:
"""Return custom labels for model-level statistics."""
return {"n_units": "Units", "R2": "R-squared", "R2_adj": "Adj. R-squared"}
@property
def __maketables_default_stat_keys__(self) -> list[str]:
"""Default model-level stats to display in ETable."""
keys = ["N", "n_units", "se_type"]
if self.r_squared is not None:
keys.append("R2")
return keys
[docs]
class EmfxResult(NamedTuple):
"""Container for aggregated ETWFE marginal effects.
This class implements the ``maketables`` plug-in interface for
publication-quality tables. See :ref:`publication_tables`.
Returned by :func:`~moderndid.etwfe.emfx.emfx`.
Attributes
----------
overall_att : float
Overall average treatment effect on the treated.
overall_se : float
Standard error for the overall ATT.
aggregation_type : str
Type of aggregation: ``"simple"``, ``"group"``, ``"calendar"``,
or ``"event"``.
event_times : ndarray or None
Event times, groups, or calendar times for non-simple aggregations.
att_by_event : ndarray or None
ATT estimates for each aggregation level.
se_by_event : ndarray or None
Standard errors for each aggregation level.
ci_lower : ndarray or None
Lower confidence interval bounds.
ci_upper : ndarray or None
Upper confidence interval bounds.
critical_value : float
Critical value used for confidence intervals.
n_obs : int
Number of observations in the original regression.
estimation_params : dict
Additional estimation parameters.
"""
#: Overall average treatment effect on the treated.
overall_att: float
#: Standard error for the overall ATT.
overall_se: float
#: Type of aggregation: "simple", "group", "calendar", or "event".
aggregation_type: str
#: Event times, groups, or calendar times for non-simple aggregations.
event_times: np.ndarray | None = None
#: ATT estimates for each aggregation level.
att_by_event: np.ndarray | None = None
#: Standard errors for each aggregation level.
se_by_event: np.ndarray | None = None
#: Lower confidence interval bounds.
ci_lower: np.ndarray | None = None
#: Upper confidence interval bounds.
ci_upper: np.ndarray | None = None
#: Critical value used for confidence intervals.
critical_value: float = 1.96
#: Number of observations in the original regression.
n_obs: int = 0
#: Estimation parameters (alpha, vcov_type, etc.).
estimation_params: dict = {}
@property
def __maketables_coef_table__(self):
"""Return canonical coefficient table for maketables."""
alpha = float(self.estimation_params.get("alpha", 0.05))
z_crit = stats.norm.ppf(1 - alpha / 2)
names = ["Overall ATT"]
estimates = [self.overall_att]
se = [self.overall_se]
if self.event_times is not None and self.att_by_event is not None and self.se_by_event is not None:
prefix = {
"event": "Event",
"group": "Group",
"calendar": "Time",
}.get(self.aggregation_type, "Effect")
names.extend(make_effect_names(self.event_times, prefix=prefix))
estimates.extend(np.asarray(self.att_by_event, dtype=float).tolist())
se.extend(np.asarray(self.se_by_event, dtype=float).tolist())
crit = np.full(len(names), z_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":
return self.n_obs
if key == "aggregation":
return self.aggregation_type
if key == "se_type":
return self.estimation_params.get("vcov_type", "Delta method")
return None
@property
def __maketables_depvar__(self) -> str:
"""Return dependent variable label for maketables."""
return str(self.estimation_params.get("yname", "EMFX"))
@property
def __maketables_fixef_string__(self) -> str | None:
"""EMFX output does not report fixed-effects formulas."""
return None
@property
def __maketables_vcov_info__(self) -> dict[str, str | None]:
"""Return variance-covariance metadata."""
return {
"vcov_type": "Delta method",
"clustervar": self.estimation_params.get("clustervar"),
}
@property
def __maketables_stat_labels__(self) -> dict[str, str]:
"""Return custom labels for model-level statistics."""
return {"aggregation": "Aggregation"}
@property
def __maketables_default_stat_keys__(self) -> list[str]:
"""Default model-level stats to display in ETable."""
keys = ["N", "aggregation", "se_type"]
return keys