panelbox 0.2.0__py3-none-any.whl → 0.4.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.
- panelbox/__init__.py +41 -0
- panelbox/__version__.py +13 -1
- panelbox/core/formula_parser.py +9 -2
- panelbox/core/panel_data.py +1 -1
- panelbox/datasets/__init__.py +39 -0
- panelbox/datasets/load.py +334 -0
- panelbox/gmm/difference_gmm.py +63 -15
- panelbox/gmm/estimator.py +46 -5
- panelbox/gmm/system_gmm.py +136 -21
- panelbox/models/static/__init__.py +4 -0
- panelbox/models/static/between.py +434 -0
- panelbox/models/static/first_difference.py +494 -0
- panelbox/models/static/fixed_effects.py +80 -11
- panelbox/models/static/pooled_ols.py +80 -11
- panelbox/models/static/random_effects.py +52 -10
- panelbox/standard_errors/__init__.py +119 -0
- panelbox/standard_errors/clustered.py +386 -0
- panelbox/standard_errors/comparison.py +528 -0
- panelbox/standard_errors/driscoll_kraay.py +386 -0
- panelbox/standard_errors/newey_west.py +324 -0
- panelbox/standard_errors/pcse.py +358 -0
- panelbox/standard_errors/robust.py +324 -0
- panelbox/standard_errors/utils.py +390 -0
- panelbox/validation/__init__.py +6 -0
- panelbox/validation/robustness/__init__.py +51 -0
- panelbox/validation/robustness/bootstrap.py +933 -0
- panelbox/validation/robustness/checks.py +143 -0
- panelbox/validation/robustness/cross_validation.py +538 -0
- panelbox/validation/robustness/influence.py +364 -0
- panelbox/validation/robustness/jackknife.py +457 -0
- panelbox/validation/robustness/outliers.py +529 -0
- panelbox/validation/robustness/sensitivity.py +809 -0
- {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/METADATA +32 -3
- {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/RECORD +38 -21
- {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/WHEEL +1 -1
- {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/entry_points.txt +0 -0
- {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Panel-Corrected Standard Errors (PCSE).
|
|
3
|
+
|
|
4
|
+
PCSE (Beck & Katz 1995) are designed for panel data with cross-sectional
|
|
5
|
+
dependence. They estimate the full cross-sectional covariance matrix of
|
|
6
|
+
the errors and use FGLS to obtain efficient standard errors.
|
|
7
|
+
|
|
8
|
+
PCSE requires T > N (more time periods than entities).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from .utils import compute_bread
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PCSEResult:
|
|
21
|
+
"""
|
|
22
|
+
Result of PCSE estimation.
|
|
23
|
+
|
|
24
|
+
Attributes
|
|
25
|
+
----------
|
|
26
|
+
cov_matrix : np.ndarray
|
|
27
|
+
PCSE covariance matrix (k x k)
|
|
28
|
+
std_errors : np.ndarray
|
|
29
|
+
PCSE standard errors (k,)
|
|
30
|
+
sigma_matrix : np.ndarray
|
|
31
|
+
Estimated cross-sectional error covariance matrix (N x N)
|
|
32
|
+
n_obs : int
|
|
33
|
+
Number of observations
|
|
34
|
+
n_params : int
|
|
35
|
+
Number of parameters
|
|
36
|
+
n_entities : int
|
|
37
|
+
Number of entities
|
|
38
|
+
n_periods : int
|
|
39
|
+
Number of time periods
|
|
40
|
+
"""
|
|
41
|
+
cov_matrix: np.ndarray
|
|
42
|
+
std_errors: np.ndarray
|
|
43
|
+
sigma_matrix: np.ndarray
|
|
44
|
+
n_obs: int
|
|
45
|
+
n_params: int
|
|
46
|
+
n_entities: int
|
|
47
|
+
n_periods: int
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PanelCorrectedStandardErrors:
|
|
51
|
+
"""
|
|
52
|
+
Panel-Corrected Standard Errors (PCSE).
|
|
53
|
+
|
|
54
|
+
Beck & Katz (1995) estimator for panel data with contemporaneous
|
|
55
|
+
cross-sectional correlation. Estimates the full N×N contemporaneous
|
|
56
|
+
covariance matrix and uses FGLS.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
X : np.ndarray
|
|
61
|
+
Design matrix (n x k)
|
|
62
|
+
resid : np.ndarray
|
|
63
|
+
Residuals (n,)
|
|
64
|
+
entity_ids : np.ndarray
|
|
65
|
+
Entity identifiers (n,)
|
|
66
|
+
time_ids : np.ndarray
|
|
67
|
+
Time period identifiers (n,)
|
|
68
|
+
|
|
69
|
+
Attributes
|
|
70
|
+
----------
|
|
71
|
+
X : np.ndarray
|
|
72
|
+
Design matrix
|
|
73
|
+
resid : np.ndarray
|
|
74
|
+
Residuals
|
|
75
|
+
entity_ids : np.ndarray
|
|
76
|
+
Entity identifiers
|
|
77
|
+
time_ids : np.ndarray
|
|
78
|
+
Time identifiers
|
|
79
|
+
n_obs : int
|
|
80
|
+
Number of observations
|
|
81
|
+
n_params : int
|
|
82
|
+
Number of parameters
|
|
83
|
+
n_entities : int
|
|
84
|
+
Number of entities
|
|
85
|
+
n_periods : int
|
|
86
|
+
Number of time periods
|
|
87
|
+
|
|
88
|
+
Examples
|
|
89
|
+
--------
|
|
90
|
+
>>> # Panel with T > N
|
|
91
|
+
>>> pcse = PanelCorrectedStandardErrors(X, resid, entity_ids, time_ids)
|
|
92
|
+
>>> result = pcse.compute()
|
|
93
|
+
>>> print(result.std_errors)
|
|
94
|
+
|
|
95
|
+
Notes
|
|
96
|
+
-----
|
|
97
|
+
PCSE requires T > N. If T < N, the estimated Σ matrix will be singular.
|
|
98
|
+
|
|
99
|
+
References
|
|
100
|
+
----------
|
|
101
|
+
Beck, N., & Katz, J. N. (1995). What to do (and not to do) with
|
|
102
|
+
time-series cross-section data. American Political Science Review,
|
|
103
|
+
89(3), 634-647.
|
|
104
|
+
|
|
105
|
+
Bailey, D., & Katz, J. N. (2011). Implementing panel corrected standard
|
|
106
|
+
errors in R: The pcse package. Journal of Statistical Software,
|
|
107
|
+
42(CS1), 1-11.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
X: np.ndarray,
|
|
113
|
+
resid: np.ndarray,
|
|
114
|
+
entity_ids: np.ndarray,
|
|
115
|
+
time_ids: np.ndarray
|
|
116
|
+
):
|
|
117
|
+
self.X = X
|
|
118
|
+
self.resid = resid
|
|
119
|
+
self.entity_ids = np.asarray(entity_ids)
|
|
120
|
+
self.time_ids = np.asarray(time_ids)
|
|
121
|
+
|
|
122
|
+
self.n_obs, self.n_params = X.shape
|
|
123
|
+
|
|
124
|
+
# Validate dimensions
|
|
125
|
+
if len(self.entity_ids) != self.n_obs:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"entity_ids dimension mismatch: expected {self.n_obs}, "
|
|
128
|
+
f"got {len(self.entity_ids)}"
|
|
129
|
+
)
|
|
130
|
+
if len(self.time_ids) != self.n_obs:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"time_ids dimension mismatch: expected {self.n_obs}, "
|
|
133
|
+
f"got {len(self.time_ids)}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Get unique entities and periods
|
|
137
|
+
self.unique_entities = np.unique(self.entity_ids)
|
|
138
|
+
self.unique_times = np.unique(self.time_ids)
|
|
139
|
+
self.n_entities = len(self.unique_entities)
|
|
140
|
+
self.n_periods = len(self.unique_times)
|
|
141
|
+
|
|
142
|
+
# Check T > N requirement
|
|
143
|
+
if self.n_periods <= self.n_entities:
|
|
144
|
+
import warnings
|
|
145
|
+
warnings.warn(
|
|
146
|
+
f"PCSE requires T > N. Got T={self.n_periods}, N={self.n_entities}. "
|
|
147
|
+
f"The estimated Σ matrix may be singular or poorly estimated. "
|
|
148
|
+
f"Consider using cluster-robust or Driscoll-Kraay SEs instead.",
|
|
149
|
+
UserWarning
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _reshape_panel(self) -> np.ndarray:
|
|
153
|
+
"""
|
|
154
|
+
Reshape residuals to (N x T) matrix.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
resid_matrix : np.ndarray
|
|
159
|
+
Residuals reshaped to (N x T)
|
|
160
|
+
"""
|
|
161
|
+
# Create mapping from entity to row index
|
|
162
|
+
entity_map = {e: i for i, e in enumerate(self.unique_entities)}
|
|
163
|
+
time_map = {t: j for j, t in enumerate(self.unique_times)}
|
|
164
|
+
|
|
165
|
+
# Initialize with NaN for unbalanced panels
|
|
166
|
+
resid_matrix = np.full((self.n_entities, self.n_periods), np.nan)
|
|
167
|
+
|
|
168
|
+
# Fill in observed values
|
|
169
|
+
for i in range(self.n_obs):
|
|
170
|
+
entity_idx = entity_map[self.entity_ids[i]]
|
|
171
|
+
time_idx = time_map[self.time_ids[i]]
|
|
172
|
+
resid_matrix[entity_idx, time_idx] = self.resid[i]
|
|
173
|
+
|
|
174
|
+
return resid_matrix
|
|
175
|
+
|
|
176
|
+
def _estimate_sigma(self) -> np.ndarray:
|
|
177
|
+
"""
|
|
178
|
+
Estimate contemporaneous covariance matrix Σ (N x N).
|
|
179
|
+
|
|
180
|
+
Σ̂_ij = (1/T) Σ_t ε_it ε_jt
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
sigma : np.ndarray
|
|
185
|
+
Estimated contemporaneous covariance matrix (N x N)
|
|
186
|
+
"""
|
|
187
|
+
resid_matrix = self._reshape_panel() # (N x T)
|
|
188
|
+
|
|
189
|
+
# For balanced panels: Σ = (1/T) E E'
|
|
190
|
+
# where E is the (N x T) residual matrix
|
|
191
|
+
sigma = (resid_matrix @ resid_matrix.T) / self.n_periods
|
|
192
|
+
|
|
193
|
+
# For unbalanced panels, need pairwise estimation
|
|
194
|
+
# For now, we use simple approach with available data
|
|
195
|
+
# More sophisticated: pairwise covariance with available pairs
|
|
196
|
+
|
|
197
|
+
return sigma
|
|
198
|
+
|
|
199
|
+
def compute(self) -> PCSEResult:
|
|
200
|
+
"""
|
|
201
|
+
Compute PCSE covariance matrix.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
result : PCSEResult
|
|
206
|
+
PCSE covariance and standard errors
|
|
207
|
+
|
|
208
|
+
Notes
|
|
209
|
+
-----
|
|
210
|
+
The PCSE estimator uses FGLS with estimated Σ:
|
|
211
|
+
|
|
212
|
+
V_PCSE = (X' (Σ̂^{-1} ⊗ I_T) X)^{-1}
|
|
213
|
+
|
|
214
|
+
where Σ̂ is the estimated N×N contemporaneous covariance matrix.
|
|
215
|
+
|
|
216
|
+
For balanced panels:
|
|
217
|
+
- Reshape residuals to (N x T) matrix E
|
|
218
|
+
- Estimate Σ̂ = (1/T) E E'
|
|
219
|
+
- Compute Ω = Σ̂ ⊗ I_T
|
|
220
|
+
- V = (X' Ω^{-1} X)^{-1}
|
|
221
|
+
"""
|
|
222
|
+
# Estimate Σ
|
|
223
|
+
sigma = self._estimate_sigma() # (N x N)
|
|
224
|
+
|
|
225
|
+
# For large N, inverting Σ can be problematic
|
|
226
|
+
# We use Moore-Penrose pseudoinverse for robustness
|
|
227
|
+
try:
|
|
228
|
+
sigma_inv = np.linalg.inv(sigma)
|
|
229
|
+
except np.linalg.LinAlgError:
|
|
230
|
+
import warnings
|
|
231
|
+
warnings.warn(
|
|
232
|
+
"Σ matrix is singular. Using pseudoinverse. "
|
|
233
|
+
"Results may be unreliable.",
|
|
234
|
+
UserWarning
|
|
235
|
+
)
|
|
236
|
+
sigma_inv = np.linalg.pinv(sigma)
|
|
237
|
+
|
|
238
|
+
# Create Ω = Σ^{-1} ⊗ I_T
|
|
239
|
+
# For efficiency, we don't explicitly form the full Ω matrix
|
|
240
|
+
# Instead, we compute X' Ω^{-1} X directly
|
|
241
|
+
|
|
242
|
+
# Create entity mapping
|
|
243
|
+
entity_map = {e: i for i, e in enumerate(self.unique_entities)}
|
|
244
|
+
entity_indices = np.array([entity_map[e] for e in self.entity_ids])
|
|
245
|
+
|
|
246
|
+
# Compute weighted X: X_tilde = sqrt(Σ^{-1}) ⊗ I_T applied to X
|
|
247
|
+
# This is done row-by-row based on entity
|
|
248
|
+
k = self.n_params
|
|
249
|
+
|
|
250
|
+
# Method: For each observation, weight by corresponding Σ^{-1} element
|
|
251
|
+
# V = (Σ X'_i X_j)^{-1} where sum is over all pairs weighted by Σ^{-1}
|
|
252
|
+
|
|
253
|
+
# More direct approach: X' Ω^{-1} X where Ω^{-1} = Σ^{-1} ⊗ I_T
|
|
254
|
+
XtOmegaX = np.zeros((k, k))
|
|
255
|
+
|
|
256
|
+
for i in range(self.n_obs):
|
|
257
|
+
for j in range(self.n_obs):
|
|
258
|
+
entity_i = entity_indices[i]
|
|
259
|
+
entity_j = entity_indices[j]
|
|
260
|
+
|
|
261
|
+
# Weight by Σ^{-1}[entity_i, entity_j]
|
|
262
|
+
weight = sigma_inv[entity_i, entity_j]
|
|
263
|
+
|
|
264
|
+
# Add contribution
|
|
265
|
+
XtOmegaX += weight * np.outer(self.X[i], self.X[j])
|
|
266
|
+
|
|
267
|
+
# Invert to get covariance
|
|
268
|
+
try:
|
|
269
|
+
cov_matrix = np.linalg.inv(XtOmegaX)
|
|
270
|
+
except np.linalg.LinAlgError:
|
|
271
|
+
import warnings
|
|
272
|
+
warnings.warn(
|
|
273
|
+
"X'ΩX matrix is singular. Using pseudoinverse.",
|
|
274
|
+
UserWarning
|
|
275
|
+
)
|
|
276
|
+
cov_matrix = np.linalg.pinv(XtOmegaX)
|
|
277
|
+
|
|
278
|
+
std_errors = np.sqrt(np.diag(cov_matrix))
|
|
279
|
+
|
|
280
|
+
return PCSEResult(
|
|
281
|
+
cov_matrix=cov_matrix,
|
|
282
|
+
std_errors=std_errors,
|
|
283
|
+
sigma_matrix=sigma,
|
|
284
|
+
n_obs=self.n_obs,
|
|
285
|
+
n_params=self.n_params,
|
|
286
|
+
n_entities=self.n_entities,
|
|
287
|
+
n_periods=self.n_periods
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def diagnostic_summary(self) -> str:
|
|
291
|
+
"""
|
|
292
|
+
Generate diagnostic summary.
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
summary : str
|
|
297
|
+
Diagnostic information
|
|
298
|
+
"""
|
|
299
|
+
lines = []
|
|
300
|
+
lines.append("Panel-Corrected Standard Errors Diagnostics")
|
|
301
|
+
lines.append("=" * 50)
|
|
302
|
+
lines.append(f"Number of observations: {self.n_obs}")
|
|
303
|
+
lines.append(f"Number of entities (N): {self.n_entities}")
|
|
304
|
+
lines.append(f"Number of time periods (T): {self.n_periods}")
|
|
305
|
+
lines.append(f"Average obs per entity: {self.n_obs / self.n_entities:.1f}")
|
|
306
|
+
lines.append("")
|
|
307
|
+
|
|
308
|
+
# Check requirements
|
|
309
|
+
if self.n_periods <= self.n_entities:
|
|
310
|
+
lines.append("⚠ CRITICAL: T ≤ N")
|
|
311
|
+
lines.append(f" PCSE requires T > N")
|
|
312
|
+
lines.append(f" T={self.n_periods}, N={self.n_entities}")
|
|
313
|
+
lines.append(" Σ matrix will be poorly estimated or singular")
|
|
314
|
+
lines.append(" Consider cluster-robust or Driscoll-Kraay SEs")
|
|
315
|
+
elif self.n_periods < 2 * self.n_entities:
|
|
316
|
+
lines.append("⚠ WARNING: T < 2N")
|
|
317
|
+
lines.append(f" T={self.n_periods}, N={self.n_entities}")
|
|
318
|
+
lines.append(" PCSE may be unreliable with T/N < 2")
|
|
319
|
+
else:
|
|
320
|
+
lines.append(f"✓ T/N ratio: {self.n_periods / self.n_entities:.2f}")
|
|
321
|
+
lines.append(" Sufficient for PCSE estimation")
|
|
322
|
+
|
|
323
|
+
return "\n".join(lines)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def pcse(
|
|
327
|
+
X: np.ndarray,
|
|
328
|
+
resid: np.ndarray,
|
|
329
|
+
entity_ids: np.ndarray,
|
|
330
|
+
time_ids: np.ndarray
|
|
331
|
+
) -> PCSEResult:
|
|
332
|
+
"""
|
|
333
|
+
Convenience function for Panel-Corrected Standard Errors.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
X : np.ndarray
|
|
338
|
+
Design matrix (n x k)
|
|
339
|
+
resid : np.ndarray
|
|
340
|
+
Residuals (n,)
|
|
341
|
+
entity_ids : np.ndarray
|
|
342
|
+
Entity identifiers (n,)
|
|
343
|
+
time_ids : np.ndarray
|
|
344
|
+
Time period identifiers (n,)
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
result : PCSEResult
|
|
349
|
+
PCSE covariance and standard errors
|
|
350
|
+
|
|
351
|
+
Examples
|
|
352
|
+
--------
|
|
353
|
+
>>> from panelbox.standard_errors import pcse
|
|
354
|
+
>>> result = pcse(X, resid, entity_ids, time_ids)
|
|
355
|
+
>>> print(result.std_errors)
|
|
356
|
+
"""
|
|
357
|
+
pcse_est = PanelCorrectedStandardErrors(X, resid, entity_ids, time_ids)
|
|
358
|
+
return pcse_est.compute()
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Heteroskedasticity-robust standard errors (HC0, HC1, HC2, HC3).
|
|
3
|
+
|
|
4
|
+
This module implements White's heteroskedasticity-robust covariance
|
|
5
|
+
estimators and their finite-sample improvements.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional, Literal
|
|
9
|
+
import numpy as np
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from .utils import (
|
|
13
|
+
compute_leverage,
|
|
14
|
+
compute_bread,
|
|
15
|
+
compute_meat_hc,
|
|
16
|
+
sandwich_covariance,
|
|
17
|
+
hc_covariance
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
HC_TYPES = Literal['HC0', 'HC1', 'HC2', 'HC3']
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class RobustCovarianceResult:
|
|
26
|
+
"""
|
|
27
|
+
Result of robust covariance estimation.
|
|
28
|
+
|
|
29
|
+
Attributes
|
|
30
|
+
----------
|
|
31
|
+
cov_matrix : np.ndarray
|
|
32
|
+
Robust covariance matrix (k x k)
|
|
33
|
+
std_errors : np.ndarray
|
|
34
|
+
Robust standard errors (k,)
|
|
35
|
+
method : str
|
|
36
|
+
Method used ('HC0', 'HC1', 'HC2', 'HC3')
|
|
37
|
+
n_obs : int
|
|
38
|
+
Number of observations
|
|
39
|
+
n_params : int
|
|
40
|
+
Number of parameters
|
|
41
|
+
leverage : np.ndarray, optional
|
|
42
|
+
Leverage values (for HC2, HC3)
|
|
43
|
+
"""
|
|
44
|
+
cov_matrix: np.ndarray
|
|
45
|
+
std_errors: np.ndarray
|
|
46
|
+
method: str
|
|
47
|
+
n_obs: int
|
|
48
|
+
n_params: int
|
|
49
|
+
leverage: Optional[np.ndarray] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class RobustStandardErrors:
|
|
53
|
+
"""
|
|
54
|
+
Heteroskedasticity-robust standard errors.
|
|
55
|
+
|
|
56
|
+
Implements White (1980) and improved finite-sample variants.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
X : np.ndarray
|
|
61
|
+
Design matrix (n x k)
|
|
62
|
+
resid : np.ndarray
|
|
63
|
+
Residuals (n,)
|
|
64
|
+
|
|
65
|
+
Attributes
|
|
66
|
+
----------
|
|
67
|
+
X : np.ndarray
|
|
68
|
+
Design matrix
|
|
69
|
+
resid : np.ndarray
|
|
70
|
+
Residuals
|
|
71
|
+
n_obs : int
|
|
72
|
+
Number of observations
|
|
73
|
+
n_params : int
|
|
74
|
+
Number of parameters
|
|
75
|
+
|
|
76
|
+
Methods
|
|
77
|
+
-------
|
|
78
|
+
hc0()
|
|
79
|
+
White (1980) heteroskedasticity-robust SE
|
|
80
|
+
hc1()
|
|
81
|
+
Degrees of freedom corrected
|
|
82
|
+
hc2()
|
|
83
|
+
Leverage-adjusted
|
|
84
|
+
hc3()
|
|
85
|
+
MacKinnon-White (1985)
|
|
86
|
+
|
|
87
|
+
Examples
|
|
88
|
+
--------
|
|
89
|
+
>>> from panelbox.standard_errors import RobustStandardErrors
|
|
90
|
+
>>> robust = RobustStandardErrors(X, resid)
|
|
91
|
+
>>> result_hc1 = robust.hc1()
|
|
92
|
+
>>> print(result_hc1.std_errors)
|
|
93
|
+
|
|
94
|
+
References
|
|
95
|
+
----------
|
|
96
|
+
White, H. (1980). A heteroskedasticity-consistent covariance matrix
|
|
97
|
+
estimator and a direct test for heteroskedasticity. Econometrica,
|
|
98
|
+
48(4), 817-838.
|
|
99
|
+
|
|
100
|
+
MacKinnon, J. G., & White, H. (1985). Some heteroskedasticity-consistent
|
|
101
|
+
covariance matrix estimators with improved finite sample properties.
|
|
102
|
+
Journal of Econometrics, 29(3), 305-325.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, X: np.ndarray, resid: np.ndarray):
|
|
106
|
+
self.X = X
|
|
107
|
+
self.resid = resid
|
|
108
|
+
self.n_obs, self.n_params = X.shape
|
|
109
|
+
|
|
110
|
+
# Cache for efficiency
|
|
111
|
+
self._leverage = None
|
|
112
|
+
self._bread = None
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def leverage(self) -> np.ndarray:
|
|
116
|
+
"""Compute and cache leverage values."""
|
|
117
|
+
if self._leverage is None:
|
|
118
|
+
self._leverage = compute_leverage(self.X)
|
|
119
|
+
return self._leverage
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def bread(self) -> np.ndarray:
|
|
123
|
+
"""Compute and cache bread matrix."""
|
|
124
|
+
if self._bread is None:
|
|
125
|
+
self._bread = compute_bread(self.X)
|
|
126
|
+
return self._bread
|
|
127
|
+
|
|
128
|
+
def hc0(self) -> RobustCovarianceResult:
|
|
129
|
+
"""
|
|
130
|
+
HC0: White's heteroskedasticity-robust covariance.
|
|
131
|
+
|
|
132
|
+
V_HC0 = (X'X)^{-1} X' Ω̂ X (X'X)^{-1}
|
|
133
|
+
|
|
134
|
+
where Ω̂ = diag(ε̂²)
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
result : RobustCovarianceResult
|
|
139
|
+
Covariance matrix and standard errors
|
|
140
|
+
|
|
141
|
+
Notes
|
|
142
|
+
-----
|
|
143
|
+
HC0 is the original White (1980) estimator. It can be biased
|
|
144
|
+
downward in finite samples. Consider using HC1, HC2, or HC3
|
|
145
|
+
for better finite-sample properties.
|
|
146
|
+
"""
|
|
147
|
+
meat = compute_meat_hc(self.X, self.resid, method='HC0')
|
|
148
|
+
cov_matrix = sandwich_covariance(self.bread, meat)
|
|
149
|
+
std_errors = np.sqrt(np.diag(cov_matrix))
|
|
150
|
+
|
|
151
|
+
return RobustCovarianceResult(
|
|
152
|
+
cov_matrix=cov_matrix,
|
|
153
|
+
std_errors=std_errors,
|
|
154
|
+
method='HC0',
|
|
155
|
+
n_obs=self.n_obs,
|
|
156
|
+
n_params=self.n_params
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def hc1(self) -> RobustCovarianceResult:
|
|
160
|
+
"""
|
|
161
|
+
HC1: Degrees of freedom corrected.
|
|
162
|
+
|
|
163
|
+
V_HC1 = [n/(n-k)] × V_HC0
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
result : RobustCovarianceResult
|
|
168
|
+
Covariance matrix and standard errors
|
|
169
|
+
|
|
170
|
+
Notes
|
|
171
|
+
-----
|
|
172
|
+
HC1 is the most commonly used robust SE in practice.
|
|
173
|
+
It provides better finite-sample properties than HC0.
|
|
174
|
+
|
|
175
|
+
This is the default in Stata's "robust" option.
|
|
176
|
+
"""
|
|
177
|
+
meat = compute_meat_hc(self.X, self.resid, method='HC1')
|
|
178
|
+
cov_matrix = sandwich_covariance(self.bread, meat)
|
|
179
|
+
std_errors = np.sqrt(np.diag(cov_matrix))
|
|
180
|
+
|
|
181
|
+
return RobustCovarianceResult(
|
|
182
|
+
cov_matrix=cov_matrix,
|
|
183
|
+
std_errors=std_errors,
|
|
184
|
+
method='HC1',
|
|
185
|
+
n_obs=self.n_obs,
|
|
186
|
+
n_params=self.n_params
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def hc2(self) -> RobustCovarianceResult:
|
|
190
|
+
"""
|
|
191
|
+
HC2: Leverage-adjusted.
|
|
192
|
+
|
|
193
|
+
V_HC2 = (X'X)^{-1} X' Ω̂_2 X (X'X)^{-1}
|
|
194
|
+
|
|
195
|
+
where Ω̂_2 = diag(ε̂²/(1-h_i))
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
result : RobustCovarianceResult
|
|
200
|
+
Covariance matrix and standard errors
|
|
201
|
+
|
|
202
|
+
Notes
|
|
203
|
+
-----
|
|
204
|
+
HC2 adjusts for leverage (hat values). Observations with
|
|
205
|
+
high leverage receive more weight. Generally performs
|
|
206
|
+
better than HC0 and HC1 in finite samples.
|
|
207
|
+
"""
|
|
208
|
+
leverage = self.leverage
|
|
209
|
+
meat = compute_meat_hc(self.X, self.resid, method='HC2', leverage=leverage)
|
|
210
|
+
cov_matrix = sandwich_covariance(self.bread, meat)
|
|
211
|
+
std_errors = np.sqrt(np.diag(cov_matrix))
|
|
212
|
+
|
|
213
|
+
return RobustCovarianceResult(
|
|
214
|
+
cov_matrix=cov_matrix,
|
|
215
|
+
std_errors=std_errors,
|
|
216
|
+
method='HC2',
|
|
217
|
+
n_obs=self.n_obs,
|
|
218
|
+
n_params=self.n_params,
|
|
219
|
+
leverage=leverage
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def hc3(self) -> RobustCovarianceResult:
|
|
223
|
+
"""
|
|
224
|
+
HC3: MacKinnon-White leverage-adjusted.
|
|
225
|
+
|
|
226
|
+
V_HC3 = (X'X)^{-1} X' Ω̂_3 X (X'X)^{-1}
|
|
227
|
+
|
|
228
|
+
where Ω̂_3 = diag(ε̂²/(1-h_i)²)
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
result : RobustCovarianceResult
|
|
233
|
+
Covariance matrix and standard errors
|
|
234
|
+
|
|
235
|
+
Notes
|
|
236
|
+
-----
|
|
237
|
+
HC3 provides the most aggressive leverage adjustment.
|
|
238
|
+
Often recommended for small samples or when there are
|
|
239
|
+
high leverage points.
|
|
240
|
+
|
|
241
|
+
MacKinnon & White (1985) found HC3 to have good properties
|
|
242
|
+
in simulation studies.
|
|
243
|
+
"""
|
|
244
|
+
leverage = self.leverage
|
|
245
|
+
meat = compute_meat_hc(self.X, self.resid, method='HC3', leverage=leverage)
|
|
246
|
+
cov_matrix = sandwich_covariance(self.bread, meat)
|
|
247
|
+
std_errors = np.sqrt(np.diag(cov_matrix))
|
|
248
|
+
|
|
249
|
+
return RobustCovarianceResult(
|
|
250
|
+
cov_matrix=cov_matrix,
|
|
251
|
+
std_errors=std_errors,
|
|
252
|
+
method='HC3',
|
|
253
|
+
n_obs=self.n_obs,
|
|
254
|
+
n_params=self.n_params,
|
|
255
|
+
leverage=leverage
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def compute(self, method: HC_TYPES = 'HC1') -> RobustCovarianceResult:
|
|
259
|
+
"""
|
|
260
|
+
Compute robust covariance with specified method.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
method : {'HC0', 'HC1', 'HC2', 'HC3'}, default='HC1'
|
|
265
|
+
Type of heteroskedasticity-robust covariance
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
result : RobustCovarianceResult
|
|
270
|
+
Covariance matrix and standard errors
|
|
271
|
+
|
|
272
|
+
Examples
|
|
273
|
+
--------
|
|
274
|
+
>>> robust = RobustStandardErrors(X, resid)
|
|
275
|
+
>>> result = robust.compute('HC1')
|
|
276
|
+
>>> print(result.std_errors)
|
|
277
|
+
"""
|
|
278
|
+
method_upper = method.upper()
|
|
279
|
+
|
|
280
|
+
if method_upper == 'HC0':
|
|
281
|
+
return self.hc0()
|
|
282
|
+
elif method_upper == 'HC1':
|
|
283
|
+
return self.hc1()
|
|
284
|
+
elif method_upper == 'HC2':
|
|
285
|
+
return self.hc2()
|
|
286
|
+
elif method_upper == 'HC3':
|
|
287
|
+
return self.hc3()
|
|
288
|
+
else:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
f"Unknown HC method: {method}. "
|
|
291
|
+
f"Must be one of: 'HC0', 'HC1', 'HC2', 'HC3'"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def robust_covariance(
|
|
296
|
+
X: np.ndarray,
|
|
297
|
+
resid: np.ndarray,
|
|
298
|
+
method: HC_TYPES = 'HC1'
|
|
299
|
+
) -> RobustCovarianceResult:
|
|
300
|
+
"""
|
|
301
|
+
Convenience function for computing robust covariance.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
X : np.ndarray
|
|
306
|
+
Design matrix (n x k)
|
|
307
|
+
resid : np.ndarray
|
|
308
|
+
Residuals (n,)
|
|
309
|
+
method : {'HC0', 'HC1', 'HC2', 'HC3'}, default='HC1'
|
|
310
|
+
Type of heteroskedasticity-robust covariance
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
result : RobustCovarianceResult
|
|
315
|
+
Covariance matrix and standard errors
|
|
316
|
+
|
|
317
|
+
Examples
|
|
318
|
+
--------
|
|
319
|
+
>>> from panelbox.standard_errors import robust_covariance
|
|
320
|
+
>>> result = robust_covariance(X, resid, method='HC1')
|
|
321
|
+
>>> print(result.std_errors)
|
|
322
|
+
"""
|
|
323
|
+
robust = RobustStandardErrors(X, resid)
|
|
324
|
+
return robust.compute(method)
|