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.
Files changed (38) hide show
  1. panelbox/__init__.py +41 -0
  2. panelbox/__version__.py +13 -1
  3. panelbox/core/formula_parser.py +9 -2
  4. panelbox/core/panel_data.py +1 -1
  5. panelbox/datasets/__init__.py +39 -0
  6. panelbox/datasets/load.py +334 -0
  7. panelbox/gmm/difference_gmm.py +63 -15
  8. panelbox/gmm/estimator.py +46 -5
  9. panelbox/gmm/system_gmm.py +136 -21
  10. panelbox/models/static/__init__.py +4 -0
  11. panelbox/models/static/between.py +434 -0
  12. panelbox/models/static/first_difference.py +494 -0
  13. panelbox/models/static/fixed_effects.py +80 -11
  14. panelbox/models/static/pooled_ols.py +80 -11
  15. panelbox/models/static/random_effects.py +52 -10
  16. panelbox/standard_errors/__init__.py +119 -0
  17. panelbox/standard_errors/clustered.py +386 -0
  18. panelbox/standard_errors/comparison.py +528 -0
  19. panelbox/standard_errors/driscoll_kraay.py +386 -0
  20. panelbox/standard_errors/newey_west.py +324 -0
  21. panelbox/standard_errors/pcse.py +358 -0
  22. panelbox/standard_errors/robust.py +324 -0
  23. panelbox/standard_errors/utils.py +390 -0
  24. panelbox/validation/__init__.py +6 -0
  25. panelbox/validation/robustness/__init__.py +51 -0
  26. panelbox/validation/robustness/bootstrap.py +933 -0
  27. panelbox/validation/robustness/checks.py +143 -0
  28. panelbox/validation/robustness/cross_validation.py +538 -0
  29. panelbox/validation/robustness/influence.py +364 -0
  30. panelbox/validation/robustness/jackknife.py +457 -0
  31. panelbox/validation/robustness/outliers.py +529 -0
  32. panelbox/validation/robustness/sensitivity.py +809 -0
  33. {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/METADATA +32 -3
  34. {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/RECORD +38 -21
  35. {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/WHEEL +1 -1
  36. {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/entry_points.txt +0 -0
  37. {panelbox-0.2.0.dist-info → panelbox-0.4.0.dist-info}/licenses/LICENSE +0 -0
  38. {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)