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/results.py
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GMM Results Classes
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
Data structures for GMM estimation results and specification tests.
|
|
6
|
+
|
|
7
|
+
Classes
|
|
8
|
+
-------
|
|
9
|
+
TestResult : Results from a single specification test
|
|
10
|
+
GMMResults : Complete results from GMM estimation
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Optional, Dict, List
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
from scipy import stats
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TestResult:
|
|
22
|
+
"""
|
|
23
|
+
Results from a specification test.
|
|
24
|
+
|
|
25
|
+
Attributes
|
|
26
|
+
----------
|
|
27
|
+
name : str
|
|
28
|
+
Name of the test (e.g., 'Hansen J-test', 'AR(2) test')
|
|
29
|
+
statistic : float
|
|
30
|
+
Test statistic value
|
|
31
|
+
pvalue : float
|
|
32
|
+
P-value for the test
|
|
33
|
+
df : Optional[int]
|
|
34
|
+
Degrees of freedom (for chi-square tests)
|
|
35
|
+
distribution : str
|
|
36
|
+
Distribution of test statistic ('chi2' or 'normal')
|
|
37
|
+
null_hypothesis : str
|
|
38
|
+
Description of the null hypothesis
|
|
39
|
+
conclusion : str
|
|
40
|
+
Human-readable conclusion ('PASS', 'REJECT', 'WARNING')
|
|
41
|
+
details : Dict
|
|
42
|
+
Additional test-specific details
|
|
43
|
+
|
|
44
|
+
Examples
|
|
45
|
+
--------
|
|
46
|
+
>>> test = TestResult(
|
|
47
|
+
... name='Hansen J-test',
|
|
48
|
+
... statistic=12.5,
|
|
49
|
+
... pvalue=0.185,
|
|
50
|
+
... df=10,
|
|
51
|
+
... distribution='chi2',
|
|
52
|
+
... null_hypothesis='Instruments are valid',
|
|
53
|
+
... conclusion='PASS'
|
|
54
|
+
... )
|
|
55
|
+
>>> print(test)
|
|
56
|
+
Hansen J-test: statistic=12.500, p-value=0.185 [PASS]
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
name: str
|
|
60
|
+
statistic: float
|
|
61
|
+
pvalue: float
|
|
62
|
+
df: Optional[int] = None
|
|
63
|
+
distribution: str = 'chi2'
|
|
64
|
+
null_hypothesis: str = ''
|
|
65
|
+
conclusion: str = ''
|
|
66
|
+
details: Dict = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
def __post_init__(self):
|
|
69
|
+
"""Auto-determine conclusion if not provided."""
|
|
70
|
+
if not self.conclusion:
|
|
71
|
+
self.conclusion = self._determine_conclusion()
|
|
72
|
+
|
|
73
|
+
def _determine_conclusion(self) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Determine test conclusion based on p-value.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
str
|
|
80
|
+
'PASS', 'REJECT', or 'WARNING'
|
|
81
|
+
"""
|
|
82
|
+
if self.name.startswith('Hansen') or self.name.startswith('Sargan'):
|
|
83
|
+
# For overid tests: reject if p-value too low OR too high
|
|
84
|
+
if self.pvalue < 0.10:
|
|
85
|
+
return 'REJECT'
|
|
86
|
+
elif self.pvalue > 0.25:
|
|
87
|
+
return 'WARNING'
|
|
88
|
+
else:
|
|
89
|
+
return 'PASS'
|
|
90
|
+
elif 'AR(2)' in self.name:
|
|
91
|
+
# For AR(2): should NOT reject (p-value > 0.10)
|
|
92
|
+
if self.pvalue < 0.10:
|
|
93
|
+
return 'REJECT'
|
|
94
|
+
else:
|
|
95
|
+
return 'PASS'
|
|
96
|
+
elif 'AR(1)' in self.name:
|
|
97
|
+
# For AR(1): expected to reject (informational only)
|
|
98
|
+
if self.pvalue < 0.10:
|
|
99
|
+
return 'EXPECTED'
|
|
100
|
+
else:
|
|
101
|
+
return 'PASS'
|
|
102
|
+
else:
|
|
103
|
+
# Generic: reject if p-value < 0.05
|
|
104
|
+
if self.pvalue < 0.05:
|
|
105
|
+
return 'REJECT'
|
|
106
|
+
else:
|
|
107
|
+
return 'PASS'
|
|
108
|
+
|
|
109
|
+
def __str__(self) -> str:
|
|
110
|
+
"""String representation."""
|
|
111
|
+
if self.df is not None:
|
|
112
|
+
return (f"{self.name}: statistic={self.statistic:.3f}, "
|
|
113
|
+
f"p-value={self.pvalue:.4f}, df={self.df} [{self.conclusion}]")
|
|
114
|
+
else:
|
|
115
|
+
return (f"{self.name}: statistic={self.statistic:.3f}, "
|
|
116
|
+
f"p-value={self.pvalue:.4f} [{self.conclusion}]")
|
|
117
|
+
|
|
118
|
+
def to_dict(self) -> Dict:
|
|
119
|
+
"""Convert to dictionary."""
|
|
120
|
+
return {
|
|
121
|
+
'name': self.name,
|
|
122
|
+
'statistic': self.statistic,
|
|
123
|
+
'pvalue': self.pvalue,
|
|
124
|
+
'df': self.df,
|
|
125
|
+
'conclusion': self.conclusion
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class GMMResults:
|
|
131
|
+
"""
|
|
132
|
+
Results from GMM estimation.
|
|
133
|
+
|
|
134
|
+
Attributes
|
|
135
|
+
----------
|
|
136
|
+
params : pd.Series
|
|
137
|
+
Estimated coefficients
|
|
138
|
+
std_errors : pd.Series
|
|
139
|
+
Standard errors
|
|
140
|
+
tvalues : pd.Series
|
|
141
|
+
T-statistics
|
|
142
|
+
pvalues : pd.Series
|
|
143
|
+
P-values for coefficients
|
|
144
|
+
nobs : int
|
|
145
|
+
Number of observations used
|
|
146
|
+
n_groups : int
|
|
147
|
+
Number of cross-sectional units
|
|
148
|
+
n_instruments : int
|
|
149
|
+
Number of instruments
|
|
150
|
+
n_params : int
|
|
151
|
+
Number of parameters estimated
|
|
152
|
+
hansen_j : TestResult
|
|
153
|
+
Hansen J-test of overidentifying restrictions
|
|
154
|
+
sargan : TestResult
|
|
155
|
+
Sargan test (non-robust version)
|
|
156
|
+
ar1_test : TestResult
|
|
157
|
+
Arellano-Bond AR(1) test
|
|
158
|
+
ar2_test : TestResult
|
|
159
|
+
Arellano-Bond AR(2) test
|
|
160
|
+
diff_hansen : Optional[TestResult]
|
|
161
|
+
Difference-in-Hansen test (System GMM only)
|
|
162
|
+
vcov : np.ndarray
|
|
163
|
+
Variance-covariance matrix of parameters
|
|
164
|
+
weight_matrix : np.ndarray
|
|
165
|
+
GMM weight matrix
|
|
166
|
+
converged : bool
|
|
167
|
+
Whether estimation converged
|
|
168
|
+
two_step : bool
|
|
169
|
+
Whether two-step GMM was used
|
|
170
|
+
windmeijer_corrected : bool
|
|
171
|
+
Whether Windmeijer correction was applied
|
|
172
|
+
model_type : str
|
|
173
|
+
Type of model ('difference' or 'system')
|
|
174
|
+
transformation : str
|
|
175
|
+
Transformation used ('fd' or 'fod')
|
|
176
|
+
residuals : Optional[np.ndarray]
|
|
177
|
+
Model residuals
|
|
178
|
+
fitted_values : Optional[np.ndarray]
|
|
179
|
+
Fitted values
|
|
180
|
+
|
|
181
|
+
Examples
|
|
182
|
+
--------
|
|
183
|
+
**Accessing estimation results:**
|
|
184
|
+
|
|
185
|
+
>>> from panelbox.gmm import DifferenceGMM
|
|
186
|
+
>>>
|
|
187
|
+
>>> model = DifferenceGMM(data=data, dep_var='y', lags=1, exog_vars=['x1', 'x2'])
|
|
188
|
+
>>> results = model.fit()
|
|
189
|
+
>>>
|
|
190
|
+
>>> # Print formatted summary
|
|
191
|
+
>>> print(results.summary())
|
|
192
|
+
>>>
|
|
193
|
+
>>> # Access coefficients
|
|
194
|
+
>>> print(results.params)
|
|
195
|
+
>>> print(f"Persistence: {results.params['L1.y']:.3f}")
|
|
196
|
+
>>>
|
|
197
|
+
>>> # Access standard errors and p-values
|
|
198
|
+
>>> print(results.std_errors)
|
|
199
|
+
>>> print(results.pvalues)
|
|
200
|
+
>>>
|
|
201
|
+
>>> # Confidence intervals
|
|
202
|
+
>>> ci = results.conf_int(alpha=0.05)
|
|
203
|
+
>>> print(ci)
|
|
204
|
+
|
|
205
|
+
**Checking diagnostic tests:**
|
|
206
|
+
|
|
207
|
+
>>> # Hansen J-test (instruments validity)
|
|
208
|
+
>>> if 0.10 < results.hansen_j.pvalue < 0.25:
|
|
209
|
+
... print("✓ Instruments appear valid")
|
|
210
|
+
... elif results.hansen_j.pvalue < 0.10:
|
|
211
|
+
... print("✗ Instruments rejected")
|
|
212
|
+
... else:
|
|
213
|
+
... print("⚠ Warning: p-value very high (possible weak instruments)")
|
|
214
|
+
>>>
|
|
215
|
+
>>> # AR(2) test (moment conditions)
|
|
216
|
+
>>> if results.ar2_test.pvalue > 0.10:
|
|
217
|
+
... print("✓ Moment conditions valid")
|
|
218
|
+
... else:
|
|
219
|
+
... print("✗ AR(2) rejected - moment conditions invalid!")
|
|
220
|
+
>>>
|
|
221
|
+
>>> # Instrument ratio (rule of thumb: < 1.0)
|
|
222
|
+
>>> if results.instrument_ratio < 1.0:
|
|
223
|
+
... print(f"✓ Instrument ratio acceptable: {results.instrument_ratio:.3f}")
|
|
224
|
+
... else:
|
|
225
|
+
... print(f"⚠ Too many instruments: {results.instrument_ratio:.3f}")
|
|
226
|
+
|
|
227
|
+
**Exporting results:**
|
|
228
|
+
|
|
229
|
+
>>> # LaTeX table for papers
|
|
230
|
+
>>> latex = results.to_latex()
|
|
231
|
+
>>> with open('gmm_results.tex', 'w') as f:
|
|
232
|
+
... f.write(latex)
|
|
233
|
+
>>>
|
|
234
|
+
>>> # DataFrame for further analysis
|
|
235
|
+
>>> df = results.to_dataframe()
|
|
236
|
+
>>> print(df)
|
|
237
|
+
|
|
238
|
+
**Comparing models:**
|
|
239
|
+
|
|
240
|
+
>>> # Estimate both Difference and System GMM
|
|
241
|
+
>>> diff_results = DifferenceGMM(...).fit()
|
|
242
|
+
>>> sys_results = SystemGMM(...).fit()
|
|
243
|
+
>>>
|
|
244
|
+
>>> # Compare coefficient estimates
|
|
245
|
+
>>> print(f"Difference GMM: {diff_results.params['L1.y']:.3f}")
|
|
246
|
+
>>> print(f"System GMM: {sys_results.params['L1.y']:.3f}")
|
|
247
|
+
>>>
|
|
248
|
+
>>> # Compare efficiency (standard errors)
|
|
249
|
+
>>> diff_se = diff_results.std_errors['L1.y']
|
|
250
|
+
>>> sys_se = sys_results.std_errors['L1.y']
|
|
251
|
+
>>> print(f"Efficiency gain: {(diff_se - sys_se)/diff_se*100:.1f}%")
|
|
252
|
+
>>>
|
|
253
|
+
>>> # Check which model is valid
|
|
254
|
+
>>> diff_valid = (diff_results.ar2_test.pvalue > 0.10 and
|
|
255
|
+
... 0.10 < diff_results.hansen_j.pvalue < 0.50)
|
|
256
|
+
>>> sys_valid = (sys_results.ar2_test.pvalue > 0.10 and
|
|
257
|
+
... 0.10 < sys_results.hansen_j.pvalue < 0.50)
|
|
258
|
+
>>>
|
|
259
|
+
>>> if diff_valid and sys_valid:
|
|
260
|
+
... if sys_se < diff_se * 0.9:
|
|
261
|
+
... print("Prefer System GMM (more efficient)")
|
|
262
|
+
... else:
|
|
263
|
+
... print("Both valid, similar efficiency")
|
|
264
|
+
|
|
265
|
+
**Accessing model diagnostics:**
|
|
266
|
+
|
|
267
|
+
>>> print(f"Observations: {results.nobs}")
|
|
268
|
+
>>> print(f"Groups: {results.n_groups}")
|
|
269
|
+
>>> print(f"Instruments: {results.n_instruments}")
|
|
270
|
+
>>> print(f"Parameters: {results.n_params}")
|
|
271
|
+
>>> print(f"Model type: {results.model_type}")
|
|
272
|
+
>>> print(f"Two-step: {results.two_step}")
|
|
273
|
+
>>> print(f"Windmeijer corrected: {results.windmeijer_corrected}")
|
|
274
|
+
|
|
275
|
+
See Also
|
|
276
|
+
--------
|
|
277
|
+
DifferenceGMM : Arellano-Bond Difference GMM estimator
|
|
278
|
+
SystemGMM : Blundell-Bond System GMM estimator
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
# Coefficients
|
|
282
|
+
params: pd.Series
|
|
283
|
+
std_errors: pd.Series
|
|
284
|
+
tvalues: pd.Series
|
|
285
|
+
pvalues: pd.Series
|
|
286
|
+
|
|
287
|
+
# Model info
|
|
288
|
+
nobs: int
|
|
289
|
+
n_groups: int
|
|
290
|
+
n_instruments: int
|
|
291
|
+
n_params: int
|
|
292
|
+
|
|
293
|
+
# Tests
|
|
294
|
+
hansen_j: TestResult
|
|
295
|
+
sargan: TestResult
|
|
296
|
+
ar1_test: TestResult
|
|
297
|
+
ar2_test: TestResult
|
|
298
|
+
diff_hansen: Optional[TestResult] = None
|
|
299
|
+
|
|
300
|
+
# Matrices
|
|
301
|
+
vcov: np.ndarray = None
|
|
302
|
+
weight_matrix: np.ndarray = None
|
|
303
|
+
|
|
304
|
+
# Flags
|
|
305
|
+
converged: bool = True
|
|
306
|
+
two_step: bool = True
|
|
307
|
+
windmeijer_corrected: bool = False
|
|
308
|
+
|
|
309
|
+
# Model metadata
|
|
310
|
+
model_type: str = 'difference'
|
|
311
|
+
transformation: str = 'fd'
|
|
312
|
+
|
|
313
|
+
# Additional data
|
|
314
|
+
residuals: Optional[np.ndarray] = None
|
|
315
|
+
fitted_values: Optional[np.ndarray] = None
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def instrument_ratio(self) -> float:
|
|
319
|
+
"""
|
|
320
|
+
Ratio of instruments to groups.
|
|
321
|
+
|
|
322
|
+
Rule of thumb: Should be <= 1.0 to avoid instrument proliferation.
|
|
323
|
+
|
|
324
|
+
Returns
|
|
325
|
+
-------
|
|
326
|
+
float
|
|
327
|
+
n_instruments / n_groups
|
|
328
|
+
"""
|
|
329
|
+
return self.n_instruments / self.n_groups
|
|
330
|
+
|
|
331
|
+
def conf_int(self, alpha: float = 0.05) -> pd.DataFrame:
|
|
332
|
+
"""
|
|
333
|
+
Confidence intervals for parameters.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
alpha : float
|
|
338
|
+
Significance level (default 0.05 for 95% CI)
|
|
339
|
+
|
|
340
|
+
Returns
|
|
341
|
+
-------
|
|
342
|
+
pd.DataFrame
|
|
343
|
+
DataFrame with columns ['lower', 'upper']
|
|
344
|
+
"""
|
|
345
|
+
z_crit = stats.norm.ppf(1 - alpha / 2)
|
|
346
|
+
lower = self.params - z_crit * self.std_errors
|
|
347
|
+
upper = self.params + z_crit * self.std_errors
|
|
348
|
+
|
|
349
|
+
return pd.DataFrame({
|
|
350
|
+
'lower': lower,
|
|
351
|
+
'upper': upper
|
|
352
|
+
}, index=self.params.index)
|
|
353
|
+
|
|
354
|
+
def summary(self, title: Optional[str] = None) -> str:
|
|
355
|
+
"""
|
|
356
|
+
Generate Stata-style regression table.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
title : str, optional
|
|
361
|
+
Custom title for the table
|
|
362
|
+
|
|
363
|
+
Returns
|
|
364
|
+
-------
|
|
365
|
+
str
|
|
366
|
+
Formatted regression table
|
|
367
|
+
"""
|
|
368
|
+
if title is None:
|
|
369
|
+
title = f"{self.model_type.capitalize()} GMM"
|
|
370
|
+
if self.transformation == 'fod':
|
|
371
|
+
title += " (FOD)"
|
|
372
|
+
|
|
373
|
+
# Build table
|
|
374
|
+
lines = []
|
|
375
|
+
lines.append("=" * 78)
|
|
376
|
+
lines.append(f"{title:^78}")
|
|
377
|
+
lines.append("=" * 78)
|
|
378
|
+
|
|
379
|
+
# Model info
|
|
380
|
+
lines.append(f"Number of observations: {self.nobs:>10,}")
|
|
381
|
+
lines.append(f"Number of groups: {self.n_groups:>10,}")
|
|
382
|
+
lines.append(f"Number of instruments: {self.n_instruments:>10}")
|
|
383
|
+
lines.append(f"Instrument ratio: {self.instrument_ratio:>10.3f}")
|
|
384
|
+
|
|
385
|
+
gmm_type = "Two-step" if self.two_step else "One-step"
|
|
386
|
+
if self.windmeijer_corrected:
|
|
387
|
+
gmm_type += " (Windmeijer)"
|
|
388
|
+
lines.append(f"GMM type: {gmm_type:>10}")
|
|
389
|
+
lines.append("-" * 78)
|
|
390
|
+
|
|
391
|
+
# Coefficients table
|
|
392
|
+
lines.append(f"{'Variable':<20} {'Coef.':>12} {'Std.Err.':>12} "
|
|
393
|
+
f"{'z':>8} {'P>|z|':>8} {'[95% Conf. Int.]':>20}")
|
|
394
|
+
lines.append("-" * 78)
|
|
395
|
+
|
|
396
|
+
ci = self.conf_int()
|
|
397
|
+
for var in self.params.index:
|
|
398
|
+
coef = self.params[var]
|
|
399
|
+
se = self.std_errors[var]
|
|
400
|
+
t = self.tvalues[var]
|
|
401
|
+
p = self.pvalues[var]
|
|
402
|
+
ci_lower = ci.loc[var, 'lower']
|
|
403
|
+
ci_upper = ci.loc[var, 'upper']
|
|
404
|
+
|
|
405
|
+
# Significance stars
|
|
406
|
+
stars = ''
|
|
407
|
+
if p < 0.001:
|
|
408
|
+
stars = '***'
|
|
409
|
+
elif p < 0.01:
|
|
410
|
+
stars = '**'
|
|
411
|
+
elif p < 0.05:
|
|
412
|
+
stars = '*'
|
|
413
|
+
|
|
414
|
+
lines.append(f"{var:<20} {coef:>12.6f} {se:>12.6f} "
|
|
415
|
+
f"{t:>8.2f} {p:>8.4f} "
|
|
416
|
+
f"[{ci_lower:>9.6f}, {ci_upper:>9.6f}] {stars}")
|
|
417
|
+
|
|
418
|
+
lines.append("=" * 78)
|
|
419
|
+
|
|
420
|
+
# Specification tests
|
|
421
|
+
lines.append("Specification Tests:")
|
|
422
|
+
lines.append("-" * 78)
|
|
423
|
+
lines.append(str(self.hansen_j))
|
|
424
|
+
lines.append(str(self.sargan))
|
|
425
|
+
lines.append(str(self.ar1_test))
|
|
426
|
+
lines.append(str(self.ar2_test))
|
|
427
|
+
if self.diff_hansen:
|
|
428
|
+
lines.append(str(self.diff_hansen))
|
|
429
|
+
lines.append("=" * 78)
|
|
430
|
+
|
|
431
|
+
# Legend
|
|
432
|
+
lines.append("Significance: * p<0.05, ** p<0.01, *** p<0.001")
|
|
433
|
+
|
|
434
|
+
if self.model_type == 'difference':
|
|
435
|
+
lines.append("Transformation: First-differences")
|
|
436
|
+
elif self.transformation == 'fod':
|
|
437
|
+
lines.append("Transformation: Forward Orthogonal Deviations")
|
|
438
|
+
|
|
439
|
+
if self.windmeijer_corrected:
|
|
440
|
+
lines.append("Standard errors: Windmeijer (2005) corrected")
|
|
441
|
+
else:
|
|
442
|
+
lines.append("Standard errors: Robust")
|
|
443
|
+
|
|
444
|
+
lines.append("=" * 78)
|
|
445
|
+
|
|
446
|
+
return '\n'.join(lines)
|
|
447
|
+
|
|
448
|
+
def to_latex(self,
|
|
449
|
+
caption: str = "GMM Estimation Results",
|
|
450
|
+
label: str = "tab:gmm") -> str:
|
|
451
|
+
"""
|
|
452
|
+
Generate LaTeX table.
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
caption : str
|
|
457
|
+
Table caption
|
|
458
|
+
label : str
|
|
459
|
+
LaTeX label for cross-referencing
|
|
460
|
+
|
|
461
|
+
Returns
|
|
462
|
+
-------
|
|
463
|
+
str
|
|
464
|
+
LaTeX table code
|
|
465
|
+
"""
|
|
466
|
+
lines = []
|
|
467
|
+
lines.append(r"\begin{table}[htbp]")
|
|
468
|
+
lines.append(r" \centering")
|
|
469
|
+
lines.append(f" \\caption{{{caption}}}")
|
|
470
|
+
lines.append(f" \\label{{{label}}}")
|
|
471
|
+
lines.append(r" \begin{tabular}{lcccc}")
|
|
472
|
+
lines.append(r" \toprule")
|
|
473
|
+
lines.append(r" Variable & Coefficient & Std. Error & z-stat & P-value \\")
|
|
474
|
+
lines.append(r" \midrule")
|
|
475
|
+
|
|
476
|
+
for var in self.params.index:
|
|
477
|
+
coef = self.params[var]
|
|
478
|
+
se = self.std_errors[var]
|
|
479
|
+
t = self.tvalues[var]
|
|
480
|
+
p = self.pvalues[var]
|
|
481
|
+
|
|
482
|
+
# Significance stars
|
|
483
|
+
stars = ''
|
|
484
|
+
if p < 0.001:
|
|
485
|
+
stars = r'^{***}'
|
|
486
|
+
elif p < 0.01:
|
|
487
|
+
stars = r'^{**}'
|
|
488
|
+
elif p < 0.05:
|
|
489
|
+
stars = r'^{*}'
|
|
490
|
+
|
|
491
|
+
# Escape underscores in variable names
|
|
492
|
+
var_escaped = var.replace('_', r'\_')
|
|
493
|
+
|
|
494
|
+
lines.append(f" {var_escaped} & "
|
|
495
|
+
f"{coef:.4f}{stars} & "
|
|
496
|
+
f"{se:.4f} & "
|
|
497
|
+
f"{t:.2f} & "
|
|
498
|
+
f"{p:.4f} \\\\")
|
|
499
|
+
|
|
500
|
+
lines.append(r" \midrule")
|
|
501
|
+
lines.append(f" Observations & \\multicolumn{{4}}{{c}}{{{self.nobs:,}}} \\\\")
|
|
502
|
+
lines.append(f" Groups & \\multicolumn{{4}}{{c}}{{{self.n_groups:,}}} \\\\")
|
|
503
|
+
lines.append(f" Instruments & \\multicolumn{{4}}{{c}}{{{self.n_instruments}}} \\\\")
|
|
504
|
+
lines.append(f" Hansen J (p-val) & \\multicolumn{{4}}{{c}}{{{self.hansen_j.pvalue:.4f}}} \\\\")
|
|
505
|
+
lines.append(f" AR(2) (p-val) & \\multicolumn{{4}}{{c}}{{{self.ar2_test.pvalue:.4f}}} \\\\")
|
|
506
|
+
lines.append(r" \bottomrule")
|
|
507
|
+
lines.append(r" \end{tabular}")
|
|
508
|
+
lines.append(r" \begin{tablenotes}")
|
|
509
|
+
lines.append(r" \small")
|
|
510
|
+
lines.append(r" \item Significance: * $p<0.05$, ** $p<0.01$, *** $p<0.001$")
|
|
511
|
+
|
|
512
|
+
gmm_type = "Two-step" if self.two_step else "One-step"
|
|
513
|
+
if self.windmeijer_corrected:
|
|
514
|
+
gmm_type += " (Windmeijer corrected)"
|
|
515
|
+
lines.append(f" \\item GMM type: {gmm_type}")
|
|
516
|
+
|
|
517
|
+
lines.append(r" \end{tablenotes}")
|
|
518
|
+
lines.append(r"\end{table}")
|
|
519
|
+
|
|
520
|
+
return '\n'.join(lines)
|
|
521
|
+
|
|
522
|
+
def to_dict(self) -> Dict:
|
|
523
|
+
"""
|
|
524
|
+
Convert results to dictionary.
|
|
525
|
+
|
|
526
|
+
Returns
|
|
527
|
+
-------
|
|
528
|
+
dict
|
|
529
|
+
Dictionary with all results
|
|
530
|
+
"""
|
|
531
|
+
return {
|
|
532
|
+
'params': self.params.to_dict(),
|
|
533
|
+
'std_errors': self.std_errors.to_dict(),
|
|
534
|
+
'pvalues': self.pvalues.to_dict(),
|
|
535
|
+
'nobs': self.nobs,
|
|
536
|
+
'n_groups': self.n_groups,
|
|
537
|
+
'n_instruments': self.n_instruments,
|
|
538
|
+
'hansen_j': self.hansen_j.to_dict(),
|
|
539
|
+
'sargan': self.sargan.to_dict(),
|
|
540
|
+
'ar1_test': self.ar1_test.to_dict(),
|
|
541
|
+
'ar2_test': self.ar2_test.to_dict(),
|
|
542
|
+
'instrument_ratio': self.instrument_ratio,
|
|
543
|
+
'converged': self.converged
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
def __repr__(self) -> str:
|
|
547
|
+
"""Repr showing key info."""
|
|
548
|
+
return (f"GMMResults(model='{self.model_type}', "
|
|
549
|
+
f"nobs={self.nobs}, n_groups={self.n_groups}, "
|
|
550
|
+
f"n_instruments={self.n_instruments})")
|