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.
- diff_diff/__init__.py +254 -0
- diff_diff/_backend.py +112 -0
- diff_diff/_rust_backend.cp313-win_amd64.pyd +0 -0
- diff_diff/bacon.py +979 -0
- diff_diff/datasets.py +708 -0
- diff_diff/diagnostics.py +927 -0
- diff_diff/estimators.py +1161 -0
- diff_diff/honest_did.py +1511 -0
- diff_diff/imputation.py +2480 -0
- diff_diff/linalg.py +1537 -0
- diff_diff/power.py +1350 -0
- diff_diff/prep.py +1241 -0
- diff_diff/prep_dgp.py +777 -0
- diff_diff/pretrends.py +1104 -0
- diff_diff/results.py +794 -0
- diff_diff/staggered.py +1120 -0
- diff_diff/staggered_aggregation.py +492 -0
- diff_diff/staggered_bootstrap.py +753 -0
- diff_diff/staggered_results.py +296 -0
- diff_diff/sun_abraham.py +1227 -0
- diff_diff/synthetic_did.py +858 -0
- diff_diff/triple_diff.py +1322 -0
- diff_diff/trop.py +2904 -0
- diff_diff/twfe.py +428 -0
- diff_diff/utils.py +1845 -0
- diff_diff/visualization.py +1676 -0
- diff_diff-2.3.2.dist-info/METADATA +2646 -0
- diff_diff-2.3.2.dist-info/RECORD +30 -0
- diff_diff-2.3.2.dist-info/WHEEL +4 -0
- diff_diff-2.3.2.dist-info/sboms/diff_diff_rust.cyclonedx.json +5952 -0
|
@@ -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)
|