|
18 | 18 | NBER Working Paper 29873. |
19 | 19 | """ |
20 | 20 |
|
| 21 | +from __future__ import annotations |
| 22 | + |
21 | 23 | from dataclasses import dataclass, field |
22 | | -from typing import Any, Dict, List, Optional, Tuple |
| 24 | +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple |
| 25 | + |
| 26 | +if TYPE_CHECKING: |
| 27 | + from diff_diff.honest_did import HonestDiDResults |
23 | 28 |
|
24 | 29 | import numpy as np |
25 | 30 | import pandas as pd |
@@ -331,8 +336,11 @@ class ChaisemartinDHaultfoeuilleResults: |
331 | 336 | design2_effects : dict, optional |
332 | 337 | Design-2 switch-in/switch-out descriptive summary. Populated |
333 | 338 | when ``design2=True``. |
334 | | - honest_did_results : Any, optional |
335 | | - Reserved for HonestDiD integration on placebos. |
| 339 | + honest_did_results : HonestDiDResults, optional |
| 340 | + HonestDiD sensitivity analysis bounds (Rambachan & Roth 2023). |
| 341 | + Populated when ``honest_did=True`` in ``fit()`` or by calling |
| 342 | + ``compute_honest_did(results)`` post-hoc. Contains identified |
| 343 | + set bounds, robust confidence intervals, and breakdown analysis. |
336 | 344 | survey_metadata : Any, optional |
337 | 345 | Always ``None`` in Phase 1 — survey integration is deferred to a |
338 | 346 | separate effort after all phases ship. |
@@ -415,7 +423,7 @@ class ChaisemartinDHaultfoeuilleResults: |
415 | 423 | linear_trends_effects: Optional[Dict[int, Dict[str, Any]]] = field(default=None, repr=False) |
416 | 424 | heterogeneity_effects: Optional[Dict[int, Dict[str, Any]]] = field(default=None, repr=False) |
417 | 425 | design2_effects: Optional[Dict[str, Any]] = field(default=None, repr=False) |
418 | | - honest_did_results: Optional[Any] = field(default=None, repr=False) |
| 426 | + honest_did_results: Optional["HonestDiDResults"] = field(default=None, repr=False) |
419 | 427 |
|
420 | 428 | # --- Repr-suppressed metadata --- |
421 | 429 | survey_metadata: Optional[Any] = field(default=None, repr=False) |
@@ -798,6 +806,13 @@ def summary(self, alpha: Optional[float] = None) -> str: |
798 | 806 |
|
799 | 807 | lines.extend([""]) |
800 | 808 |
|
| 809 | + # --- Phase 3 extension blocks (factored into helpers) --- |
| 810 | + self._render_covariate_section(lines, width, thin) |
| 811 | + self._render_linear_trends_section(lines, width, thin, header_row) |
| 812 | + self._render_heterogeneity_section(lines, width, thin) |
| 813 | + self._render_design2_section(lines, width, thin) |
| 814 | + self._render_honest_did_section(lines, width, thin) |
| 815 | + |
801 | 816 | # --- TWFE diagnostic --- |
802 | 817 | if self.twfe_beta_fe is not None: |
803 | 818 | lines.extend( |
@@ -842,6 +857,161 @@ def print_summary(self, alpha: Optional[float] = None) -> None: |
842 | 857 | """Print the formatted summary to stdout.""" |
843 | 858 | print(self.summary(alpha)) |
844 | 859 |
|
| 860 | + # ------------------------------------------------------------------ |
| 861 | + # Summary section helpers (Phase 3 blocks) |
| 862 | + # ------------------------------------------------------------------ |
| 863 | + |
| 864 | + def _render_covariate_section( |
| 865 | + self, lines: List[str], width: int, thin: str |
| 866 | + ) -> None: |
| 867 | + if self.covariate_residuals is None: |
| 868 | + return |
| 869 | + cov_df = self.covariate_residuals |
| 870 | + control_names = sorted(cov_df["covariate"].unique()) |
| 871 | + n_baselines = cov_df["baseline_treatment"].nunique() |
| 872 | + failed = int( |
| 873 | + (cov_df.groupby("baseline_treatment")["theta_hat"].first().isna()).sum() |
| 874 | + ) |
| 875 | + lines.extend( |
| 876 | + [ |
| 877 | + thin, |
| 878 | + "Covariate Adjustment (DID^X) Diagnostics".center(width), |
| 879 | + thin, |
| 880 | + f"{'Controls:':<35} {', '.join(control_names):>10}", |
| 881 | + f"{'Baselines residualized:':<35} {n_baselines:>10}", |
| 882 | + f"{'Failed strata:':<35} {failed:>10}", |
| 883 | + thin, |
| 884 | + "", |
| 885 | + ] |
| 886 | + ) |
| 887 | + |
| 888 | + def _render_linear_trends_section( |
| 889 | + self, lines: List[str], width: int, thin: str, header_row: str |
| 890 | + ) -> None: |
| 891 | + if self.linear_trends_effects is None: |
| 892 | + return |
| 893 | + lines.extend( |
| 894 | + [ |
| 895 | + thin, |
| 896 | + "Cumulated Level Effects (DID^{fd}, trends_linear)".center(width), |
| 897 | + thin, |
| 898 | + header_row, |
| 899 | + thin, |
| 900 | + ] |
| 901 | + ) |
| 902 | + for l_h in sorted(self.linear_trends_effects.keys()): |
| 903 | + entry = self.linear_trends_effects[l_h] |
| 904 | + lines.append( |
| 905 | + _format_inference_row( |
| 906 | + f"Level_{l_h}", |
| 907 | + entry["effect"], |
| 908 | + entry["se"], |
| 909 | + entry["t_stat"], |
| 910 | + entry["p_value"], |
| 911 | + ) |
| 912 | + ) |
| 913 | + lines.extend([thin, ""]) |
| 914 | + |
| 915 | + def _render_heterogeneity_section( |
| 916 | + self, lines: List[str], width: int, thin: str |
| 917 | + ) -> None: |
| 918 | + if self.heterogeneity_effects is None: |
| 919 | + return |
| 920 | + lines.extend( |
| 921 | + [ |
| 922 | + thin, |
| 923 | + "Heterogeneity Test (Section 1.5, partial)".center(width), |
| 924 | + thin, |
| 925 | + f"{'Horizon':<15} {'beta^het':>12} {'Std. Err.':>12} " |
| 926 | + f"{'t-stat':>10} {'P>|t|':>10} {'Sig.':>6}", |
| 927 | + thin, |
| 928 | + ] |
| 929 | + ) |
| 930 | + for l_h in sorted(self.heterogeneity_effects.keys()): |
| 931 | + entry = self.heterogeneity_effects[l_h] |
| 932 | + lines.append( |
| 933 | + _format_inference_row( |
| 934 | + f"l={l_h}", |
| 935 | + entry["beta"], |
| 936 | + entry["se"], |
| 937 | + entry["t_stat"], |
| 938 | + entry["p_value"], |
| 939 | + ) |
| 940 | + ) |
| 941 | + lines.extend( |
| 942 | + [ |
| 943 | + thin, |
| 944 | + "Note: Post-treatment regressions only (no placebo/joint test).", |
| 945 | + "", |
| 946 | + ] |
| 947 | + ) |
| 948 | + |
| 949 | + def _render_design2_section( |
| 950 | + self, lines: List[str], width: int, thin: str |
| 951 | + ) -> None: |
| 952 | + if self.design2_effects is None: |
| 953 | + return |
| 954 | + d2 = self.design2_effects |
| 955 | + si = d2.get("switch_in", {}) |
| 956 | + so = d2.get("switch_out", {}) |
| 957 | + lines.extend( |
| 958 | + [ |
| 959 | + thin, |
| 960 | + "Design-2: Switch-In / Switch-Out (Section 1.6)".center(width), |
| 961 | + thin, |
| 962 | + f"{'Join-then-leave groups:':<35} {d2.get('n_design2_groups', 0):>10}", |
| 963 | + f"{'Switch-in effect (mean):':<35} " |
| 964 | + f"{_fmt_float(si.get('mean_effect', float('nan'))):>10}" |
| 965 | + f" (N={si.get('n_groups', 0)})", |
| 966 | + f"{'Switch-out effect (mean):':<35} " |
| 967 | + f"{_fmt_float(so.get('mean_effect', float('nan'))):>10}" |
| 968 | + f" (N={so.get('n_groups', 0)})", |
| 969 | + thin, |
| 970 | + "", |
| 971 | + ] |
| 972 | + ) |
| 973 | + |
| 974 | + def _render_honest_did_section( |
| 975 | + self, lines: List[str], width: int, thin: str |
| 976 | + ) -> None: |
| 977 | + if self.honest_did_results is None: |
| 978 | + return |
| 979 | + hd = self.honest_did_results |
| 980 | + method_label = hd.method.replace("_", " ").title() |
| 981 | + m_val = hd.M |
| 982 | + sig_label = "Yes" if hd.is_significant else "No" |
| 983 | + conf_pct = int((1 - hd.alpha) * 100) |
| 984 | + lines.extend( |
| 985 | + [ |
| 986 | + thin, |
| 987 | + "HonestDiD Sensitivity (Rambachan-Roth 2023)".center(width), |
| 988 | + thin, |
| 989 | + f"{'Method:':<35} {method_label} (M={_fmt_float(m_val)})", |
| 990 | + f"{'Target:':<35} {hd.target_label}", |
| 991 | + ] |
| 992 | + ) |
| 993 | + if hd.post_periods_used is not None: |
| 994 | + lines.append( |
| 995 | + f"{'Post horizons used:':<35} {hd.post_periods_used}" |
| 996 | + ) |
| 997 | + if hd.pre_periods_used is not None: |
| 998 | + lines.append( |
| 999 | + f"{'Pre horizons used:':<35} {hd.pre_periods_used}" |
| 1000 | + ) |
| 1001 | + lines.extend( |
| 1002 | + [ |
| 1003 | + f"{'Original estimate:':<35} {_fmt_float(hd.original_estimate):>10}", |
| 1004 | + f"{'Identified set:':<35} " |
| 1005 | + f"[{_fmt_float(hd.lb)}, {_fmt_float(hd.ub)}]", |
| 1006 | + f"{'Robust ' + str(conf_pct) + '% CI:':<35} " |
| 1007 | + f"[{_fmt_float(hd.ci_lb)}, {_fmt_float(hd.ci_ub)}]", |
| 1008 | + f"{'Significant at ' + str(int(hd.alpha * 100)) + '%:':<35} " |
| 1009 | + f"{sig_label:>10}", |
| 1010 | + thin, |
| 1011 | + "", |
| 1012 | + ] |
| 1013 | + ) |
| 1014 | + |
845 | 1015 | # ------------------------------------------------------------------ |
846 | 1016 | # to_dataframe |
847 | 1017 | # ------------------------------------------------------------------ |
|
0 commit comments