panelbox 0.2.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 +67 -0
- panelbox/__version__.py +14 -0
- panelbox/cli/__init__.py +0 -0
- panelbox/cli/{commands}/__init__.py +0 -0
- panelbox/core/__init__.py +0 -0
- panelbox/core/base_model.py +164 -0
- panelbox/core/formula_parser.py +318 -0
- panelbox/core/panel_data.py +387 -0
- panelbox/core/results.py +366 -0
- panelbox/datasets/__init__.py +0 -0
- panelbox/datasets/{data}/__init__.py +0 -0
- panelbox/gmm/__init__.py +65 -0
- panelbox/gmm/difference_gmm.py +645 -0
- panelbox/gmm/estimator.py +562 -0
- panelbox/gmm/instruments.py +580 -0
- panelbox/gmm/results.py +550 -0
- panelbox/gmm/system_gmm.py +621 -0
- panelbox/gmm/tests.py +535 -0
- panelbox/models/__init__.py +11 -0
- panelbox/models/dynamic/__init__.py +0 -0
- panelbox/models/iv/__init__.py +0 -0
- panelbox/models/static/__init__.py +13 -0
- panelbox/models/static/fixed_effects.py +516 -0
- panelbox/models/static/pooled_ols.py +298 -0
- panelbox/models/static/random_effects.py +512 -0
- panelbox/report/__init__.py +61 -0
- panelbox/report/asset_manager.py +410 -0
- panelbox/report/css_manager.py +472 -0
- panelbox/report/exporters/__init__.py +15 -0
- panelbox/report/exporters/html_exporter.py +440 -0
- panelbox/report/exporters/latex_exporter.py +510 -0
- panelbox/report/exporters/markdown_exporter.py +446 -0
- panelbox/report/renderers/__init__.py +11 -0
- panelbox/report/renderers/static/__init__.py +0 -0
- panelbox/report/renderers/static_validation_renderer.py +341 -0
- panelbox/report/report_manager.py +502 -0
- panelbox/report/template_manager.py +337 -0
- panelbox/report/transformers/__init__.py +0 -0
- panelbox/report/transformers/static/__init__.py +0 -0
- panelbox/report/validation_transformer.py +449 -0
- panelbox/standard_errors/__init__.py +0 -0
- panelbox/templates/__init__.py +0 -0
- panelbox/templates/assets/css/base_styles.css +382 -0
- panelbox/templates/assets/css/report_components.css +747 -0
- panelbox/templates/assets/js/tab-navigation.js +161 -0
- panelbox/templates/assets/js/utils.js +276 -0
- panelbox/templates/common/footer.html +24 -0
- panelbox/templates/common/header.html +44 -0
- panelbox/templates/common/meta.html +5 -0
- panelbox/templates/validation/interactive/index.html +272 -0
- panelbox/templates/validation/interactive/partials/charts.html +58 -0
- panelbox/templates/validation/interactive/partials/methodology.html +201 -0
- panelbox/templates/validation/interactive/partials/overview.html +146 -0
- panelbox/templates/validation/interactive/partials/recommendations.html +101 -0
- panelbox/templates/validation/interactive/partials/test_results.html +231 -0
- panelbox/utils/__init__.py +0 -0
- panelbox/utils/formatting.py +172 -0
- panelbox/utils/matrix_ops.py +233 -0
- panelbox/utils/statistical.py +173 -0
- panelbox/validation/__init__.py +58 -0
- panelbox/validation/base.py +175 -0
- panelbox/validation/cointegration/__init__.py +0 -0
- panelbox/validation/cross_sectional_dependence/__init__.py +13 -0
- panelbox/validation/cross_sectional_dependence/breusch_pagan_lm.py +222 -0
- panelbox/validation/cross_sectional_dependence/frees.py +297 -0
- panelbox/validation/cross_sectional_dependence/pesaran_cd.py +188 -0
- panelbox/validation/heteroskedasticity/__init__.py +13 -0
- panelbox/validation/heteroskedasticity/breusch_pagan.py +222 -0
- panelbox/validation/heteroskedasticity/modified_wald.py +172 -0
- panelbox/validation/heteroskedasticity/white.py +208 -0
- panelbox/validation/instruments/__init__.py +0 -0
- panelbox/validation/robustness/__init__.py +0 -0
- panelbox/validation/serial_correlation/__init__.py +13 -0
- panelbox/validation/serial_correlation/baltagi_wu.py +220 -0
- panelbox/validation/serial_correlation/breusch_godfrey.py +260 -0
- panelbox/validation/serial_correlation/wooldridge_ar.py +200 -0
- panelbox/validation/specification/__init__.py +16 -0
- panelbox/validation/specification/chow.py +273 -0
- panelbox/validation/specification/hausman.py +264 -0
- panelbox/validation/specification/mundlak.py +331 -0
- panelbox/validation/specification/reset.py +273 -0
- panelbox/validation/unit_root/__init__.py +0 -0
- panelbox/validation/validation_report.py +257 -0
- panelbox/validation/validation_suite.py +401 -0
- panelbox-0.2.0.dist-info/METADATA +337 -0
- panelbox-0.2.0.dist-info/RECORD +90 -0
- panelbox-0.2.0.dist-info/WHEEL +5 -0
- panelbox-0.2.0.dist-info/entry_points.txt +2 -0
- panelbox-0.2.0.dist-info/licenses/LICENSE +21 -0
- panelbox-0.2.0.dist-info/top_level.txt +1 -0
panelbox/gmm/tests.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GMM Specification Tests
|
|
3
|
+
========================
|
|
4
|
+
|
|
5
|
+
Specification tests for GMM models including Hansen J-test, Sargan test,
|
|
6
|
+
Arellano-Bond autocorrelation tests, and Difference-in-Hansen test.
|
|
7
|
+
|
|
8
|
+
Classes
|
|
9
|
+
-------
|
|
10
|
+
GMMTests : Specification tests for GMM models
|
|
11
|
+
|
|
12
|
+
References
|
|
13
|
+
----------
|
|
14
|
+
.. [1] Hansen, L. P. (1982). "Large Sample Properties of Generalized Method
|
|
15
|
+
of Moments Estimators." Econometrica, 50(4), 1029-1054.
|
|
16
|
+
|
|
17
|
+
.. [2] Sargan, J. D. (1958). "The Estimation of Economic Relationships using
|
|
18
|
+
Instrumental Variables." Econometrica, 26(3), 393-415.
|
|
19
|
+
|
|
20
|
+
.. [3] Arellano, M., & Bond, S. (1991). "Some Tests of Specification for Panel
|
|
21
|
+
Data: Monte Carlo Evidence and an Application to Employment Equations."
|
|
22
|
+
Review of Economic Studies, 58(2), 277-297.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import Tuple, Optional
|
|
26
|
+
import numpy as np
|
|
27
|
+
from scipy import stats
|
|
28
|
+
from panelbox.gmm.results import TestResult
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GMMTests:
|
|
32
|
+
"""
|
|
33
|
+
Specification tests for GMM models.
|
|
34
|
+
|
|
35
|
+
Implements:
|
|
36
|
+
- Hansen J-test of overidentifying restrictions
|
|
37
|
+
- Sargan test (non-robust version)
|
|
38
|
+
- Arellano-Bond AR(1) and AR(2) tests
|
|
39
|
+
- Difference-in-Hansen test for instrument subsets
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
>>> tester = GMMTests()
|
|
44
|
+
>>> hansen = tester.hansen_j_test(residuals, Z, W, n_params)
|
|
45
|
+
>>> ar2 = tester.arellano_bond_ar_test(residuals_diff, order=2)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def hansen_j_test(self,
|
|
49
|
+
residuals: np.ndarray,
|
|
50
|
+
Z: np.ndarray,
|
|
51
|
+
W: np.ndarray,
|
|
52
|
+
n_params: int) -> TestResult:
|
|
53
|
+
"""
|
|
54
|
+
Hansen (1982) J-test of overidentifying restrictions.
|
|
55
|
+
|
|
56
|
+
Tests the validity of the moment conditions (instrument validity).
|
|
57
|
+
Under the null hypothesis that instruments are valid, the J-statistic
|
|
58
|
+
follows a chi-square distribution.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
residuals : np.ndarray
|
|
63
|
+
Model residuals (n x 1)
|
|
64
|
+
Z : np.ndarray
|
|
65
|
+
Instrument matrix (n x n_instruments)
|
|
66
|
+
W : np.ndarray
|
|
67
|
+
GMM weight matrix (n_instruments x n_instruments)
|
|
68
|
+
n_params : int
|
|
69
|
+
Number of parameters estimated
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
TestResult
|
|
74
|
+
Hansen J-test result
|
|
75
|
+
|
|
76
|
+
Notes
|
|
77
|
+
-----
|
|
78
|
+
H0: All instruments are valid (E[Z'ε] = 0)
|
|
79
|
+
Test statistic: J = n * (Z'ε)' W (Z'ε) ~ χ²(n_instruments - n_params)
|
|
80
|
+
|
|
81
|
+
Interpretation:
|
|
82
|
+
- p-value < 0.10: Reject H0 (instruments invalid)
|
|
83
|
+
- p-value > 0.25: Suspicious (may indicate weak instruments)
|
|
84
|
+
- 0.10 < p-value < 0.25: Good (instruments valid)
|
|
85
|
+
|
|
86
|
+
References
|
|
87
|
+
----------
|
|
88
|
+
Hansen, L. P. (1982). Econometrica, 50(4), 1029-1054.
|
|
89
|
+
"""
|
|
90
|
+
# Remove missing values
|
|
91
|
+
residuals = residuals.flatten() if residuals.ndim > 1 else residuals
|
|
92
|
+
valid_mask = ~np.isnan(residuals)
|
|
93
|
+
resid_clean = residuals[valid_mask]
|
|
94
|
+
Z_clean = Z[valid_mask, :]
|
|
95
|
+
|
|
96
|
+
n = len(resid_clean)
|
|
97
|
+
n_instruments = Z_clean.shape[1]
|
|
98
|
+
|
|
99
|
+
# Compute moment conditions: g_n = (1/n) Z'ε
|
|
100
|
+
g_n = (Z_clean.T @ resid_clean) / n
|
|
101
|
+
|
|
102
|
+
# Compute J statistic: J = n * g_n' W g_n
|
|
103
|
+
J_stat = n * (g_n.T @ W @ g_n)
|
|
104
|
+
|
|
105
|
+
# Degrees of freedom
|
|
106
|
+
df = n_instruments - n_params
|
|
107
|
+
|
|
108
|
+
if df <= 0:
|
|
109
|
+
# Exactly identified or under-identified
|
|
110
|
+
return TestResult(
|
|
111
|
+
name='Hansen J-test',
|
|
112
|
+
statistic=np.nan,
|
|
113
|
+
pvalue=np.nan,
|
|
114
|
+
df=df,
|
|
115
|
+
distribution='chi2',
|
|
116
|
+
null_hypothesis='All instruments are valid',
|
|
117
|
+
conclusion='N/A (exactly/under-identified)',
|
|
118
|
+
details={'message': 'Test not applicable: model is exactly or under-identified'}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# P-value from chi-square distribution
|
|
122
|
+
pvalue = 1 - stats.chi2.cdf(J_stat, df)
|
|
123
|
+
|
|
124
|
+
return TestResult(
|
|
125
|
+
name='Hansen J-test',
|
|
126
|
+
statistic=J_stat,
|
|
127
|
+
pvalue=pvalue,
|
|
128
|
+
df=df,
|
|
129
|
+
distribution='chi2',
|
|
130
|
+
null_hypothesis='All instruments are valid (overid restrictions hold)',
|
|
131
|
+
details={
|
|
132
|
+
'n_instruments': n_instruments,
|
|
133
|
+
'n_params': n_params,
|
|
134
|
+
'overid_restrictions': df
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def sargan_test(self,
|
|
139
|
+
residuals: np.ndarray,
|
|
140
|
+
Z: np.ndarray,
|
|
141
|
+
n_params: int) -> TestResult:
|
|
142
|
+
"""
|
|
143
|
+
Sargan (1958) test of overidentifying restrictions.
|
|
144
|
+
|
|
145
|
+
Non-robust version of Hansen J-test. Only valid under homoskedasticity,
|
|
146
|
+
but reported for compatibility with Stata xtabond2.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
residuals : np.ndarray
|
|
151
|
+
Model residuals (n x 1)
|
|
152
|
+
Z : np.ndarray
|
|
153
|
+
Instrument matrix (n x n_instruments)
|
|
154
|
+
n_params : int
|
|
155
|
+
Number of parameters estimated
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
TestResult
|
|
160
|
+
Sargan test result
|
|
161
|
+
|
|
162
|
+
Notes
|
|
163
|
+
-----
|
|
164
|
+
H0: All instruments are valid (under homoskedasticity)
|
|
165
|
+
Test statistic: S = (Z'ε)' (Z'Z)^{-1} (Z'ε) ~ χ²(n_instruments - n_params)
|
|
166
|
+
|
|
167
|
+
This is equivalent to Hansen J-test with W = (Z'Z)^{-1}.
|
|
168
|
+
Not robust to heteroskedasticity - use Hansen J-test for robustness.
|
|
169
|
+
|
|
170
|
+
References
|
|
171
|
+
----------
|
|
172
|
+
Sargan, J. D. (1958). Econometrica, 26(3), 393-415.
|
|
173
|
+
"""
|
|
174
|
+
# Remove missing values
|
|
175
|
+
residuals = residuals.flatten() if residuals.ndim > 1 else residuals
|
|
176
|
+
valid_mask = ~np.isnan(residuals)
|
|
177
|
+
resid_clean = residuals[valid_mask]
|
|
178
|
+
Z_clean = Z[valid_mask, :]
|
|
179
|
+
|
|
180
|
+
n = len(resid_clean)
|
|
181
|
+
n_instruments = Z_clean.shape[1]
|
|
182
|
+
|
|
183
|
+
# Degrees of freedom
|
|
184
|
+
df = n_instruments - n_params
|
|
185
|
+
|
|
186
|
+
if df <= 0:
|
|
187
|
+
return TestResult(
|
|
188
|
+
name='Sargan test',
|
|
189
|
+
statistic=np.nan,
|
|
190
|
+
pvalue=np.nan,
|
|
191
|
+
df=df,
|
|
192
|
+
distribution='chi2',
|
|
193
|
+
null_hypothesis='All instruments are valid (homoskedasticity)',
|
|
194
|
+
conclusion='N/A (exactly/under-identified)',
|
|
195
|
+
details={'message': 'Test not applicable: model is exactly or under-identified'}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Compute Sargan statistic
|
|
199
|
+
# S = (Z'ε)' (Z'Z)^{-1} (Z'ε)
|
|
200
|
+
Zte = Z_clean.T @ resid_clean
|
|
201
|
+
ZtZ = Z_clean.T @ Z_clean
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
ZtZ_inv = np.linalg.inv(ZtZ)
|
|
205
|
+
except np.linalg.LinAlgError:
|
|
206
|
+
ZtZ_inv = np.linalg.pinv(ZtZ)
|
|
207
|
+
|
|
208
|
+
S_stat = Zte.T @ ZtZ_inv @ Zte
|
|
209
|
+
|
|
210
|
+
# P-value from chi-square distribution
|
|
211
|
+
pvalue = 1 - stats.chi2.cdf(S_stat, df)
|
|
212
|
+
|
|
213
|
+
return TestResult(
|
|
214
|
+
name='Sargan test',
|
|
215
|
+
statistic=S_stat,
|
|
216
|
+
pvalue=pvalue,
|
|
217
|
+
df=df,
|
|
218
|
+
distribution='chi2',
|
|
219
|
+
null_hypothesis='All instruments valid (assumes homoskedasticity)',
|
|
220
|
+
details={
|
|
221
|
+
'n_instruments': n_instruments,
|
|
222
|
+
'n_params': n_params,
|
|
223
|
+
'note': 'Not robust to heteroskedasticity. Use Hansen J-test for robustness.'
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def arellano_bond_ar_test(self,
|
|
228
|
+
residuals_diff: np.ndarray,
|
|
229
|
+
ids: np.ndarray,
|
|
230
|
+
order: int = 1) -> TestResult:
|
|
231
|
+
"""
|
|
232
|
+
Arellano-Bond (1991) test for autocorrelation in residuals.
|
|
233
|
+
|
|
234
|
+
Tests for serial correlation in the differenced residuals.
|
|
235
|
+
Critical for validating moment conditions in GMM.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
residuals_diff : np.ndarray
|
|
240
|
+
First-differenced residuals (Δε_it)
|
|
241
|
+
ids : np.ndarray
|
|
242
|
+
Cross-sectional unit identifiers
|
|
243
|
+
order : int
|
|
244
|
+
Order of autocorrelation to test (1 or 2)
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
TestResult
|
|
249
|
+
AR test result
|
|
250
|
+
|
|
251
|
+
Notes
|
|
252
|
+
-----
|
|
253
|
+
H0: No autocorrelation of order `order` in differenced residuals
|
|
254
|
+
|
|
255
|
+
AR(1) test:
|
|
256
|
+
- Expected to REJECT (first-differencing induces MA(1))
|
|
257
|
+
- Reported for completeness, not a failure if rejected
|
|
258
|
+
|
|
259
|
+
AR(2) test:
|
|
260
|
+
- Should NOT reject (critical test)
|
|
261
|
+
- If AR(2) is rejected, moment conditions are invalid
|
|
262
|
+
- This invalidates lagged levels as instruments
|
|
263
|
+
|
|
264
|
+
Test statistic is asymptotically N(0,1).
|
|
265
|
+
|
|
266
|
+
References
|
|
267
|
+
----------
|
|
268
|
+
Arellano, M., & Bond, S. (1991). Review of Economic Studies, 58(2), 277-297.
|
|
269
|
+
"""
|
|
270
|
+
# Remove missing values
|
|
271
|
+
valid_mask = ~np.isnan(residuals_diff)
|
|
272
|
+
resid_clean = residuals_diff[valid_mask]
|
|
273
|
+
ids_clean = ids[valid_mask]
|
|
274
|
+
|
|
275
|
+
# Compute lagged residuals by group
|
|
276
|
+
unique_ids = np.unique(ids_clean)
|
|
277
|
+
n_groups = len(unique_ids)
|
|
278
|
+
|
|
279
|
+
# Store products of residuals and their lags
|
|
280
|
+
products = []
|
|
281
|
+
|
|
282
|
+
for group_id in unique_ids:
|
|
283
|
+
# Get residuals for this group
|
|
284
|
+
mask = ids_clean == group_id
|
|
285
|
+
group_resid = resid_clean[mask]
|
|
286
|
+
|
|
287
|
+
# Compute products: Δε_it * Δε_{i,t-order}
|
|
288
|
+
for t in range(order, len(group_resid)):
|
|
289
|
+
product = group_resid[t] * group_resid[t - order]
|
|
290
|
+
products.append(product)
|
|
291
|
+
|
|
292
|
+
if len(products) == 0:
|
|
293
|
+
return TestResult(
|
|
294
|
+
name=f'AR({order}) test',
|
|
295
|
+
statistic=np.nan,
|
|
296
|
+
pvalue=np.nan,
|
|
297
|
+
df=None,
|
|
298
|
+
distribution='normal',
|
|
299
|
+
null_hypothesis=f'No AR({order}) in differenced residuals',
|
|
300
|
+
conclusion='N/A (insufficient data)',
|
|
301
|
+
details={'message': 'Insufficient observations for AR test'}
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
products = np.array(products)
|
|
305
|
+
|
|
306
|
+
# Compute test statistic
|
|
307
|
+
# Under H0, E[Δε_it * Δε_{i,t-k}] = 0
|
|
308
|
+
mean_product = np.mean(products)
|
|
309
|
+
var_product = np.var(products, ddof=1)
|
|
310
|
+
|
|
311
|
+
if var_product == 0:
|
|
312
|
+
return TestResult(
|
|
313
|
+
name=f'AR({order}) test',
|
|
314
|
+
statistic=np.nan,
|
|
315
|
+
pvalue=np.nan,
|
|
316
|
+
df=None,
|
|
317
|
+
distribution='normal',
|
|
318
|
+
null_hypothesis=f'No AR({order}) in differenced residuals',
|
|
319
|
+
conclusion='N/A (zero variance)',
|
|
320
|
+
details={'message': 'Zero variance in products'}
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Normalize by standard error
|
|
324
|
+
se_product = np.sqrt(var_product / len(products))
|
|
325
|
+
z_stat = mean_product / se_product
|
|
326
|
+
|
|
327
|
+
# P-value from standard normal (two-sided test)
|
|
328
|
+
pvalue = 2 * (1 - stats.norm.cdf(np.abs(z_stat)))
|
|
329
|
+
|
|
330
|
+
# Determine null hypothesis and expected result
|
|
331
|
+
if order == 1:
|
|
332
|
+
null_hyp = 'No AR(1) in differenced residuals'
|
|
333
|
+
details = {
|
|
334
|
+
'note': 'AR(1) rejection is EXPECTED due to MA(1) induced by differencing',
|
|
335
|
+
'n_products': len(products)
|
|
336
|
+
}
|
|
337
|
+
else:
|
|
338
|
+
null_hyp = f'No AR({order}) in differenced residuals'
|
|
339
|
+
details = {
|
|
340
|
+
'note': f'AR({order}) rejection indicates INVALID moment conditions',
|
|
341
|
+
'n_products': len(products)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return TestResult(
|
|
345
|
+
name=f'AR({order}) test',
|
|
346
|
+
statistic=z_stat,
|
|
347
|
+
pvalue=pvalue,
|
|
348
|
+
df=None,
|
|
349
|
+
distribution='normal',
|
|
350
|
+
null_hypothesis=null_hyp,
|
|
351
|
+
details=details
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def difference_in_hansen(self,
|
|
355
|
+
residuals: np.ndarray,
|
|
356
|
+
Z_full: np.ndarray,
|
|
357
|
+
Z_subset: np.ndarray,
|
|
358
|
+
W_full: np.ndarray,
|
|
359
|
+
W_subset: np.ndarray,
|
|
360
|
+
n_params: int,
|
|
361
|
+
subset_name: str = 'subset') -> TestResult:
|
|
362
|
+
"""
|
|
363
|
+
Difference-in-Hansen test for instrument subsets.
|
|
364
|
+
|
|
365
|
+
Tests the validity of a specific subset of instruments by comparing
|
|
366
|
+
Hansen J statistics with and without the subset.
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
residuals : np.ndarray
|
|
371
|
+
Model residuals (n x 1)
|
|
372
|
+
Z_full : np.ndarray
|
|
373
|
+
Full instrument matrix (n x n_instruments_full)
|
|
374
|
+
Z_subset : np.ndarray
|
|
375
|
+
Subset instrument matrix to test (n x n_instruments_subset)
|
|
376
|
+
W_full : np.ndarray
|
|
377
|
+
Weight matrix for full model
|
|
378
|
+
W_subset : np.ndarray
|
|
379
|
+
Weight matrix for model without subset
|
|
380
|
+
n_params : int
|
|
381
|
+
Number of parameters
|
|
382
|
+
subset_name : str
|
|
383
|
+
Name of instrument subset being tested
|
|
384
|
+
|
|
385
|
+
Returns
|
|
386
|
+
-------
|
|
387
|
+
TestResult
|
|
388
|
+
Difference-in-Hansen test result
|
|
389
|
+
|
|
390
|
+
Notes
|
|
391
|
+
-----
|
|
392
|
+
H0: Subset of instruments is valid
|
|
393
|
+
|
|
394
|
+
Test statistic: D = J_subset - J_full ~ χ²(df_subset - df_full)
|
|
395
|
+
|
|
396
|
+
Common uses in System GMM:
|
|
397
|
+
- Test validity of level instruments
|
|
398
|
+
- Test validity of specific GMM-type instruments
|
|
399
|
+
- Compare collapsed vs. uncollapsed instruments
|
|
400
|
+
|
|
401
|
+
References
|
|
402
|
+
----------
|
|
403
|
+
Roodman, D. (2009). Stata Journal, 9(1), 86-136.
|
|
404
|
+
"""
|
|
405
|
+
# Remove missing values
|
|
406
|
+
residuals = residuals.flatten() if residuals.ndim > 1 else residuals
|
|
407
|
+
valid_mask = ~np.isnan(residuals)
|
|
408
|
+
resid_clean = residuals[valid_mask]
|
|
409
|
+
Z_full_clean = Z_full[valid_mask, :] # 2D indexing
|
|
410
|
+
Z_subset_clean = Z_subset[valid_mask, :] # 2D indexing
|
|
411
|
+
|
|
412
|
+
n = len(resid_clean)
|
|
413
|
+
|
|
414
|
+
# Compute Hansen J for full model
|
|
415
|
+
n_instruments_full = Z_full_clean.shape[1]
|
|
416
|
+
df_full = n_instruments_full - n_params
|
|
417
|
+
|
|
418
|
+
g_n_full = (Z_full_clean.T @ resid_clean) / n
|
|
419
|
+
J_full = n * (g_n_full.T @ W_full @ g_n_full)
|
|
420
|
+
|
|
421
|
+
# Compute Hansen J for model without subset
|
|
422
|
+
n_instruments_subset = Z_subset_clean.shape[1]
|
|
423
|
+
df_subset = n_instruments_subset - n_params
|
|
424
|
+
|
|
425
|
+
g_n_subset = (Z_subset_clean.T @ resid_clean) / n
|
|
426
|
+
J_subset = n * (g_n_subset.T @ W_subset @ g_n_subset)
|
|
427
|
+
|
|
428
|
+
# Difference-in-Hansen statistic
|
|
429
|
+
D_stat = J_subset - J_full
|
|
430
|
+
df_diff = df_subset - df_full
|
|
431
|
+
|
|
432
|
+
if df_diff <= 0:
|
|
433
|
+
return TestResult(
|
|
434
|
+
name=f'Diff-in-Hansen ({subset_name})',
|
|
435
|
+
statistic=np.nan,
|
|
436
|
+
pvalue=np.nan,
|
|
437
|
+
df=df_diff,
|
|
438
|
+
distribution='chi2',
|
|
439
|
+
null_hypothesis=f'{subset_name} instruments are valid',
|
|
440
|
+
conclusion='N/A (invalid df)',
|
|
441
|
+
details={'message': 'Invalid degrees of freedom for difference test'}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# P-value from chi-square distribution
|
|
445
|
+
pvalue = 1 - stats.chi2.cdf(D_stat, df_diff)
|
|
446
|
+
|
|
447
|
+
return TestResult(
|
|
448
|
+
name=f'Diff-in-Hansen ({subset_name})',
|
|
449
|
+
statistic=D_stat,
|
|
450
|
+
pvalue=pvalue,
|
|
451
|
+
df=df_diff,
|
|
452
|
+
distribution='chi2',
|
|
453
|
+
null_hypothesis=f'{subset_name} instruments are valid',
|
|
454
|
+
details={
|
|
455
|
+
'J_full': J_full,
|
|
456
|
+
'J_subset': J_subset,
|
|
457
|
+
'n_instruments_tested': df_diff
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def weak_instruments_test(self,
|
|
462
|
+
X: np.ndarray,
|
|
463
|
+
Z: np.ndarray) -> Tuple[float, bool]:
|
|
464
|
+
"""
|
|
465
|
+
Simple weak instruments diagnostic.
|
|
466
|
+
|
|
467
|
+
Computes the F-statistic from the first-stage regression of each
|
|
468
|
+
endogenous variable on the instruments.
|
|
469
|
+
|
|
470
|
+
Parameters
|
|
471
|
+
----------
|
|
472
|
+
X : np.ndarray
|
|
473
|
+
Endogenous regressors (n x k)
|
|
474
|
+
Z : np.ndarray
|
|
475
|
+
Instruments (n x n_instruments)
|
|
476
|
+
|
|
477
|
+
Returns
|
|
478
|
+
-------
|
|
479
|
+
f_stat : float
|
|
480
|
+
Minimum F-statistic across first-stage regressions
|
|
481
|
+
weak : bool
|
|
482
|
+
True if instruments may be weak (F < 10)
|
|
483
|
+
|
|
484
|
+
Notes
|
|
485
|
+
-----
|
|
486
|
+
Rule of thumb: F-statistic > 10 suggests instruments are not weak.
|
|
487
|
+
|
|
488
|
+
For more sophisticated weak instruments tests, see:
|
|
489
|
+
- Stock & Yogo (2005) critical values
|
|
490
|
+
- Montiel Olea & Pflueger (2013) effective F-statistic
|
|
491
|
+
"""
|
|
492
|
+
# Remove missing values
|
|
493
|
+
valid_mask = ~np.isnan(X).any(axis=1) & ~np.isnan(Z).any(axis=1)
|
|
494
|
+
X_clean = X[valid_mask]
|
|
495
|
+
Z_clean = Z[valid_mask]
|
|
496
|
+
|
|
497
|
+
n, k = X_clean.shape
|
|
498
|
+
n_instruments = Z_clean.shape[1]
|
|
499
|
+
|
|
500
|
+
# Compute F-statistics for each endogenous variable
|
|
501
|
+
f_stats = []
|
|
502
|
+
|
|
503
|
+
for j in range(k):
|
|
504
|
+
x_j = X_clean[:, j]
|
|
505
|
+
|
|
506
|
+
# First-stage regression: x_j = Z * π + u
|
|
507
|
+
ZtZ = Z_clean.T @ Z_clean
|
|
508
|
+
Ztx = Z_clean.T @ x_j
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
ZtZ_inv = np.linalg.inv(ZtZ)
|
|
512
|
+
except np.linalg.LinAlgError:
|
|
513
|
+
ZtZ_inv = np.linalg.pinv(ZtZ)
|
|
514
|
+
|
|
515
|
+
pi = ZtZ_inv @ Ztx
|
|
516
|
+
x_j_fitted = Z_clean @ pi
|
|
517
|
+
resid = x_j - x_j_fitted
|
|
518
|
+
|
|
519
|
+
# Compute F-statistic
|
|
520
|
+
# F = (R² / k) / ((1 - R²) / (n - k - 1))
|
|
521
|
+
ss_total = np.sum((x_j - np.mean(x_j)) ** 2)
|
|
522
|
+
ss_resid = np.sum(resid ** 2)
|
|
523
|
+
r_squared = 1 - (ss_resid / ss_total)
|
|
524
|
+
|
|
525
|
+
if r_squared >= 1.0:
|
|
526
|
+
f_stat = np.inf
|
|
527
|
+
else:
|
|
528
|
+
f_stat = (r_squared / n_instruments) / ((1 - r_squared) / (n - n_instruments - 1))
|
|
529
|
+
|
|
530
|
+
f_stats.append(f_stat)
|
|
531
|
+
|
|
532
|
+
min_f_stat = np.min(f_stats)
|
|
533
|
+
weak = min_f_stat < 10.0
|
|
534
|
+
|
|
535
|
+
return min_f_stat, weak
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Static panel models.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from panelbox.models.static.pooled_ols import PooledOLS
|
|
6
|
+
from panelbox.models.static.fixed_effects import FixedEffects
|
|
7
|
+
from panelbox.models.static.random_effects import RandomEffects
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
'PooledOLS',
|
|
11
|
+
'FixedEffects',
|
|
12
|
+
'RandomEffects',
|
|
13
|
+
]
|