Skip to content

LWDiD event-study output needs the shared results contract #732

Description

@shawcharles

Summary

LWDiD computes event-study effects, simultaneous confidence-band metadata, and event-time counts, but its result object does not expose that output through the shared result contract. As a result, generic consumers cannot serialise or tabulate the event-study result.

Current behaviour

The estimator stores event_study_effects and confidence-band fields on LWDiDResults, but:

  • to_dataframe() only emits scalar, common-timing, or cohort rows;
  • to_dict() omits event-study effects and confidence-band metadata; and
  • LWDiDResults does not inherit BaseResults or use the repository's EventStudyResults surface.

Relevant implementation:

  • event-study construction:
    event_study_effects = {}
  • to_dataframe():
    def to_dataframe(self) -> pd.DataFrame:
    """Convert results to a pandas DataFrame.
    Returns
    -------
    pd.DataFrame
    For common timing: a single-row DataFrame (plus period rows if available).
    For staggered: one row per cohort plus an "Overall" row.
    """
    if not self.is_staggered:
    rows: List[Dict[str, Any]] = [
    {
    "term": "ATT",
    "att": self.att,
    "se": self.se,
    "t_stat": self.t_stat,
    "p_value": self.p_value,
    "ci_lower": self.conf_int[0],
    "ci_upper": self.conf_int[1],
    "n_obs": self.n_obs,
    "n_treated": self.n_treated,
    "n_control": self.n_control,
    "rolling": self.rolling,
    "estimator": self.estimator,
    "vce_type": self.vce_type,
    }
    ]
    if self.has_period_effects:
    for period, eff in sorted(self.period_effects.items()): # type: ignore[union-attr]
    ci = eff.get("conf_int", (np.nan, np.nan))
    rows.append(
    {
    "term": f"Period {period}",
    "att": eff.get("att", np.nan),
    "se": eff.get("se", np.nan),
    "t_stat": eff.get("t_stat", np.nan),
    "p_value": eff.get("p_value", np.nan),
    "ci_lower": ci[0] if ci else np.nan,
    "ci_upper": ci[1] if ci else np.nan,
    "n_obs": eff.get("n_obs", 0),
    "n_treated": None,
    "n_control": None,
    "rolling": self.rolling,
    "estimator": self.estimator,
    "vce_type": self.vce_type,
    }
    )
    return pd.DataFrame(rows)
    rows_stag: List[Dict[str, Any]] = []
    for cohort, eff in self.cohort_effects.items(): # type: ignore[union-attr]
    ci = eff.get("conf_int", (np.nan, np.nan))
    n_t = eff.get("n_treated", 0)
    n_c = eff.get("n_control", 0)
    rows_stag.append(
    {
    "cohort": cohort,
    "att": eff.get("att", np.nan),
    "se": eff.get("se", np.nan),
    "t_stat": eff.get("t_stat", np.nan),
    "p_value": eff.get("p_value", np.nan),
    "ci_lower": ci[0] if ci else np.nan,
    "ci_upper": ci[1] if ci else np.nan,
    "n_treated": n_t,
    "n_control": n_c,
    }
    )
    # Append overall row
    rows_stag.append(
    {
    "cohort": "Overall",
    "att": self.att,
    "se": self.se,
    "t_stat": self.t_stat,
    "p_value": self.p_value,
    "ci_lower": self.conf_int[0],
    "ci_upper": self.conf_int[1],
    "n_treated": self.n_treated,
    "n_control": self.n_control,
    }
    )
    return pd.DataFrame(rows_stag)
  • to_dict():
    def to_dict(self) -> Dict[str, Any]:
    """Convert results to a JSON-serializable dictionary.
    Returns
    -------
    dict
    All scalar results and metadata. Arrays are converted to lists.
    """
    result: Dict[str, Any] = {
    "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": self.n_treated,
    "n_control": self.n_control,
    "rolling": self.rolling,
    "estimator": self.estimator,
    "vce_type": self.vce_type,
    "alpha": self.alpha,
    }
    if self.cluster_name is not None:
    result["cluster_name"] = self.cluster_name
    if self.n_clusters is not None:
    result["n_clusters"] = self.n_clusters
    if self.cohort_effects is not None:
    result["cohort_effects"] = {str(k): v for k, v in self.cohort_effects.items()}
    if self.overall_att is not None:
    result["overall_att"] = self.overall_att
    if self.params is not None:
    result["params"] = self.params.tolist()
    if self.bse is not None:
    result["bse"] = self.bse.tolist()
    if self.period_effects is not None:
    result["period_effects"] = {str(k): v for k, v in self.period_effects.items()}
    return result

Current main has a shared BaseResults / EventStudyResults contract used by estimator result objects and serialisation tests. LWDiD should integrate with it as part of its rebase.

Expected behaviour

LWDiD should provide a public, serialisable event-study result compatible with the package-wide API, preferably by making LWDiDResults a BaseResults implementation and exposing aggregate("event_study") -> EventStudyResults.

A different public surface is possible, but it should provide equivalent dataframe, dictionary, plotting, and confidence-band behaviour rather than leaving the information as an internal dictionary.

Acceptance criteria

  • Event-study estimates, confidence intervals/bands, reference period, and sample counts are available through the public result API.
  • They round-trip through the standard dataframe and dictionary serialisation paths.
  • LWDiD passes the shared result-object contract tests after rebasing onto current main.
  • Documentation points users to the supported event-study result surface.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions