diff-diff 0.1.0__py3-none-any.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/results.py ADDED
@@ -0,0 +1,170 @@
1
+ """
2
+ Results classes for difference-in-differences estimation.
3
+
4
+ Provides statsmodels-style output with a more Pythonic interface.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Optional
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+
13
+
14
+ @dataclass
15
+ class DiDResults:
16
+ """
17
+ Results from a Difference-in-Differences estimation.
18
+
19
+ Provides easy access to coefficients, standard errors, confidence intervals,
20
+ and summary statistics in a Pythonic way.
21
+
22
+ Attributes
23
+ ----------
24
+ att : float
25
+ Average Treatment effect on the Treated (ATT).
26
+ se : float
27
+ Standard error of the ATT estimate.
28
+ t_stat : float
29
+ T-statistic for the ATT estimate.
30
+ p_value : float
31
+ P-value for the null hypothesis that ATT = 0.
32
+ conf_int : tuple[float, float]
33
+ Confidence interval for the ATT.
34
+ n_obs : int
35
+ Number of observations used in estimation.
36
+ n_treated : int
37
+ Number of treated units.
38
+ n_control : int
39
+ Number of control units.
40
+ """
41
+
42
+ att: float
43
+ se: float
44
+ t_stat: float
45
+ p_value: float
46
+ conf_int: tuple
47
+ n_obs: int
48
+ n_treated: int
49
+ n_control: int
50
+ alpha: float = 0.05
51
+ coefficients: Optional[dict] = field(default=None)
52
+ vcov: Optional[np.ndarray] = field(default=None)
53
+ residuals: Optional[np.ndarray] = field(default=None)
54
+ fitted_values: Optional[np.ndarray] = field(default=None)
55
+ r_squared: Optional[float] = field(default=None)
56
+
57
+ def __repr__(self) -> str:
58
+ """Concise string representation."""
59
+ sig = "***" if self.p_value < 0.001 else "**" if self.p_value < 0.01 else "*" if self.p_value < 0.05 else ""
60
+ return (
61
+ f"DiDResults(ATT={self.att:.4f}{sig}, "
62
+ f"SE={self.se:.4f}, "
63
+ f"p={self.p_value:.4f})"
64
+ )
65
+
66
+ def summary(self, alpha: Optional[float] = None) -> str:
67
+ """
68
+ Generate a formatted summary of the estimation results.
69
+
70
+ Parameters
71
+ ----------
72
+ alpha : float, optional
73
+ Significance level for confidence intervals. Defaults to the
74
+ alpha used during estimation.
75
+
76
+ Returns
77
+ -------
78
+ str
79
+ Formatted summary table.
80
+ """
81
+ alpha = alpha or self.alpha
82
+ conf_level = int((1 - alpha) * 100)
83
+
84
+ lines = [
85
+ "=" * 70,
86
+ "Difference-in-Differences Estimation Results".center(70),
87
+ "=" * 70,
88
+ "",
89
+ f"{'Observations:':<25} {self.n_obs:>10}",
90
+ f"{'Treated units:':<25} {self.n_treated:>10}",
91
+ f"{'Control units:':<25} {self.n_control:>10}",
92
+ ]
93
+
94
+ if self.r_squared is not None:
95
+ lines.append(f"{'R-squared:':<25} {self.r_squared:>10.4f}")
96
+
97
+ lines.extend([
98
+ "",
99
+ "-" * 70,
100
+ f"{'Parameter':<15} {'Estimate':>12} {'Std. Err.':>12} {'t-stat':>10} {'P>|t|':>10}",
101
+ "-" * 70,
102
+ f"{'ATT':<15} {self.att:>12.4f} {self.se:>12.4f} {self.t_stat:>10.3f} {self.p_value:>10.4f}",
103
+ "-" * 70,
104
+ "",
105
+ f"{conf_level}% Confidence Interval: [{self.conf_int[0]:.4f}, {self.conf_int[1]:.4f}]",
106
+ ])
107
+
108
+ # Add significance codes
109
+ lines.extend([
110
+ "",
111
+ "Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1",
112
+ "=" * 70,
113
+ ])
114
+
115
+ return "\n".join(lines)
116
+
117
+ def print_summary(self, alpha: Optional[float] = None) -> None:
118
+ """Print the summary to stdout."""
119
+ print(self.summary(alpha))
120
+
121
+ def to_dict(self) -> dict:
122
+ """
123
+ Convert results to a dictionary.
124
+
125
+ Returns
126
+ -------
127
+ dict
128
+ Dictionary containing all estimation results.
129
+ """
130
+ return {
131
+ "att": self.att,
132
+ "se": self.se,
133
+ "t_stat": self.t_stat,
134
+ "p_value": self.p_value,
135
+ "conf_int_lower": self.conf_int[0],
136
+ "conf_int_upper": self.conf_int[1],
137
+ "n_obs": self.n_obs,
138
+ "n_treated": self.n_treated,
139
+ "n_control": self.n_control,
140
+ "r_squared": self.r_squared,
141
+ }
142
+
143
+ def to_dataframe(self) -> pd.DataFrame:
144
+ """
145
+ Convert results to a pandas DataFrame.
146
+
147
+ Returns
148
+ -------
149
+ pd.DataFrame
150
+ DataFrame with estimation results.
151
+ """
152
+ return pd.DataFrame([self.to_dict()])
153
+
154
+ @property
155
+ def is_significant(self) -> bool:
156
+ """Check if the ATT is statistically significant at the alpha level."""
157
+ return bool(self.p_value < self.alpha)
158
+
159
+ @property
160
+ def significance_stars(self) -> str:
161
+ """Return significance stars based on p-value."""
162
+ if self.p_value < 0.001:
163
+ return "***"
164
+ elif self.p_value < 0.01:
165
+ return "**"
166
+ elif self.p_value < 0.05:
167
+ return "*"
168
+ elif self.p_value < 0.1:
169
+ return "."
170
+ return ""