diff-diff 2.3.2__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
1
+ """
2
+ Result container classes for Callaway-Sant'Anna estimator.
3
+
4
+ This module provides dataclass containers for storing and presenting
5
+ group-time average treatment effects and their aggregations.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ from diff_diff.results import _get_significance_stars
15
+
16
+ if TYPE_CHECKING:
17
+ from diff_diff.staggered_bootstrap import CSBootstrapResults
18
+
19
+
20
+ @dataclass
21
+ class GroupTimeEffect:
22
+ """
23
+ Treatment effect for a specific group-time combination.
24
+
25
+ Attributes
26
+ ----------
27
+ group : any
28
+ The treatment cohort (first treatment period).
29
+ time : any
30
+ The time period.
31
+ effect : float
32
+ The ATT(g,t) estimate.
33
+ se : float
34
+ Standard error.
35
+ n_treated : int
36
+ Number of treated observations.
37
+ n_control : int
38
+ Number of control observations.
39
+ """
40
+ group: Any
41
+ time: Any
42
+ effect: float
43
+ se: float
44
+ t_stat: float
45
+ p_value: float
46
+ conf_int: Tuple[float, float]
47
+ n_treated: int
48
+ n_control: int
49
+
50
+ @property
51
+ def is_significant(self) -> bool:
52
+ """Check if effect is significant at 0.05 level."""
53
+ return bool(self.p_value < 0.05)
54
+
55
+ @property
56
+ def significance_stars(self) -> str:
57
+ """Return significance stars based on p-value."""
58
+ return _get_significance_stars(self.p_value)
59
+
60
+
61
+ @dataclass
62
+ class CallawaySantAnnaResults:
63
+ """
64
+ Results from Callaway-Sant'Anna (2021) staggered DiD estimation.
65
+
66
+ This class stores group-time average treatment effects ATT(g,t) and
67
+ provides methods for aggregation into summary measures.
68
+
69
+ Attributes
70
+ ----------
71
+ group_time_effects : dict
72
+ Dictionary mapping (group, time) tuples to effect dictionaries.
73
+ overall_att : float
74
+ Overall average treatment effect (weighted average of ATT(g,t)).
75
+ overall_se : float
76
+ Standard error of overall ATT.
77
+ overall_p_value : float
78
+ P-value for overall ATT.
79
+ overall_conf_int : tuple
80
+ Confidence interval for overall ATT.
81
+ groups : list
82
+ List of treatment cohorts (first treatment periods).
83
+ time_periods : list
84
+ List of all time periods.
85
+ n_obs : int
86
+ Total number of observations.
87
+ n_treated_units : int
88
+ Number of ever-treated units.
89
+ n_control_units : int
90
+ Number of never-treated units.
91
+ event_study_effects : dict, optional
92
+ Effects aggregated by relative time (event study).
93
+ group_effects : dict, optional
94
+ Effects aggregated by treatment cohort.
95
+ """
96
+ group_time_effects: Dict[Tuple[Any, Any], Dict[str, Any]]
97
+ overall_att: float
98
+ overall_se: float
99
+ overall_t_stat: float
100
+ overall_p_value: float
101
+ overall_conf_int: Tuple[float, float]
102
+ groups: List[Any]
103
+ time_periods: List[Any]
104
+ n_obs: int
105
+ n_treated_units: int
106
+ n_control_units: int
107
+ alpha: float = 0.05
108
+ control_group: str = "never_treated"
109
+ base_period: str = "varying"
110
+ event_study_effects: Optional[Dict[int, Dict[str, Any]]] = field(default=None)
111
+ group_effects: Optional[Dict[Any, Dict[str, Any]]] = field(default=None)
112
+ influence_functions: Optional["np.ndarray"] = field(default=None, repr=False)
113
+ bootstrap_results: Optional["CSBootstrapResults"] = field(default=None, repr=False)
114
+
115
+ def __repr__(self) -> str:
116
+ """Concise string representation."""
117
+ sig = _get_significance_stars(self.overall_p_value)
118
+ return (
119
+ f"CallawaySantAnnaResults(ATT={self.overall_att:.4f}{sig}, "
120
+ f"SE={self.overall_se:.4f}, "
121
+ f"n_groups={len(self.groups)}, "
122
+ f"n_periods={len(self.time_periods)})"
123
+ )
124
+
125
+ def summary(self, alpha: Optional[float] = None) -> str:
126
+ """
127
+ Generate formatted summary of estimation results.
128
+
129
+ Parameters
130
+ ----------
131
+ alpha : float, optional
132
+ Significance level. Defaults to alpha used in estimation.
133
+
134
+ Returns
135
+ -------
136
+ str
137
+ Formatted summary.
138
+ """
139
+ alpha = alpha or self.alpha
140
+ conf_level = int((1 - alpha) * 100)
141
+
142
+ lines = [
143
+ "=" * 85,
144
+ "Callaway-Sant'Anna Staggered Difference-in-Differences Results".center(85),
145
+ "=" * 85,
146
+ "",
147
+ f"{'Total observations:':<30} {self.n_obs:>10}",
148
+ f"{'Treated units:':<30} {self.n_treated_units:>10}",
149
+ f"{'Control units:':<30} {self.n_control_units:>10}",
150
+ f"{'Treatment cohorts:':<30} {len(self.groups):>10}",
151
+ f"{'Time periods:':<30} {len(self.time_periods):>10}",
152
+ f"{'Control group:':<30} {self.control_group:>10}",
153
+ f"{'Base period:':<30} {self.base_period:>10}",
154
+ "",
155
+ ]
156
+
157
+ # Overall ATT
158
+ lines.extend([
159
+ "-" * 85,
160
+ "Overall Average Treatment Effect on the Treated".center(85),
161
+ "-" * 85,
162
+ f"{'Parameter':<15} {'Estimate':>12} {'Std. Err.':>12} {'t-stat':>10} {'P>|t|':>10} {'Sig.':>6}",
163
+ "-" * 85,
164
+ f"{'ATT':<15} {self.overall_att:>12.4f} {self.overall_se:>12.4f} "
165
+ f"{self.overall_t_stat:>10.3f} {self.overall_p_value:>10.4f} "
166
+ f"{_get_significance_stars(self.overall_p_value):>6}",
167
+ "-" * 85,
168
+ "",
169
+ f"{conf_level}% Confidence Interval: [{self.overall_conf_int[0]:.4f}, {self.overall_conf_int[1]:.4f}]",
170
+ "",
171
+ ])
172
+
173
+ # Event study effects if available
174
+ if self.event_study_effects:
175
+ lines.extend([
176
+ "-" * 85,
177
+ "Event Study (Dynamic) Effects".center(85),
178
+ "-" * 85,
179
+ f"{'Rel. Period':<15} {'Estimate':>12} {'Std. Err.':>12} {'t-stat':>10} {'P>|t|':>10} {'Sig.':>6}",
180
+ "-" * 85,
181
+ ])
182
+
183
+ for rel_t in sorted(self.event_study_effects.keys()):
184
+ eff = self.event_study_effects[rel_t]
185
+ sig = _get_significance_stars(eff['p_value'])
186
+ lines.append(
187
+ f"{rel_t:<15} {eff['effect']:>12.4f} {eff['se']:>12.4f} "
188
+ f"{eff['t_stat']:>10.3f} {eff['p_value']:>10.4f} {sig:>6}"
189
+ )
190
+
191
+ lines.extend(["-" * 85, ""])
192
+
193
+ # Group effects if available
194
+ if self.group_effects:
195
+ lines.extend([
196
+ "-" * 85,
197
+ "Effects by Treatment Cohort".center(85),
198
+ "-" * 85,
199
+ f"{'Cohort':<15} {'Estimate':>12} {'Std. Err.':>12} {'t-stat':>10} {'P>|t|':>10} {'Sig.':>6}",
200
+ "-" * 85,
201
+ ])
202
+
203
+ for group in sorted(self.group_effects.keys()):
204
+ eff = self.group_effects[group]
205
+ sig = _get_significance_stars(eff['p_value'])
206
+ lines.append(
207
+ f"{group:<15} {eff['effect']:>12.4f} {eff['se']:>12.4f} "
208
+ f"{eff['t_stat']:>10.3f} {eff['p_value']:>10.4f} {sig:>6}"
209
+ )
210
+
211
+ lines.extend(["-" * 85, ""])
212
+
213
+ lines.extend([
214
+ "Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1",
215
+ "=" * 85,
216
+ ])
217
+
218
+ return "\n".join(lines)
219
+
220
+ def print_summary(self, alpha: Optional[float] = None) -> None:
221
+ """Print summary to stdout."""
222
+ print(self.summary(alpha))
223
+
224
+ def to_dataframe(self, level: str = "group_time") -> pd.DataFrame:
225
+ """
226
+ Convert results to DataFrame.
227
+
228
+ Parameters
229
+ ----------
230
+ level : str, default="group_time"
231
+ Level of aggregation: "group_time", "event_study", or "group".
232
+
233
+ Returns
234
+ -------
235
+ pd.DataFrame
236
+ Results as DataFrame.
237
+ """
238
+ if level == "group_time":
239
+ rows = []
240
+ for (g, t), data in self.group_time_effects.items():
241
+ rows.append({
242
+ 'group': g,
243
+ 'time': t,
244
+ 'effect': data['effect'],
245
+ 'se': data['se'],
246
+ 't_stat': data['t_stat'],
247
+ 'p_value': data['p_value'],
248
+ 'conf_int_lower': data['conf_int'][0],
249
+ 'conf_int_upper': data['conf_int'][1],
250
+ })
251
+ return pd.DataFrame(rows)
252
+
253
+ elif level == "event_study":
254
+ if self.event_study_effects is None:
255
+ raise ValueError("Event study effects not computed. Use aggregate='event_study'.")
256
+ rows = []
257
+ for rel_t, data in sorted(self.event_study_effects.items()):
258
+ rows.append({
259
+ 'relative_period': rel_t,
260
+ 'effect': data['effect'],
261
+ 'se': data['se'],
262
+ 't_stat': data['t_stat'],
263
+ 'p_value': data['p_value'],
264
+ 'conf_int_lower': data['conf_int'][0],
265
+ 'conf_int_upper': data['conf_int'][1],
266
+ })
267
+ return pd.DataFrame(rows)
268
+
269
+ elif level == "group":
270
+ if self.group_effects is None:
271
+ raise ValueError("Group effects not computed. Use aggregate='group'.")
272
+ rows = []
273
+ for group, data in sorted(self.group_effects.items()):
274
+ rows.append({
275
+ 'group': group,
276
+ 'effect': data['effect'],
277
+ 'se': data['se'],
278
+ 't_stat': data['t_stat'],
279
+ 'p_value': data['p_value'],
280
+ 'conf_int_lower': data['conf_int'][0],
281
+ 'conf_int_upper': data['conf_int'][1],
282
+ })
283
+ return pd.DataFrame(rows)
284
+
285
+ else:
286
+ raise ValueError(f"Unknown level: {level}. Use 'group_time', 'event_study', or 'group'.")
287
+
288
+ @property
289
+ def is_significant(self) -> bool:
290
+ """Check if overall ATT is significant."""
291
+ return bool(self.overall_p_value < self.alpha)
292
+
293
+ @property
294
+ def significance_stars(self) -> str:
295
+ """Significance stars for overall ATT."""
296
+ return _get_significance_stars(self.overall_p_value)