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.
Files changed (90) hide show
  1. panelbox/__init__.py +67 -0
  2. panelbox/__version__.py +14 -0
  3. panelbox/cli/__init__.py +0 -0
  4. panelbox/cli/{commands}/__init__.py +0 -0
  5. panelbox/core/__init__.py +0 -0
  6. panelbox/core/base_model.py +164 -0
  7. panelbox/core/formula_parser.py +318 -0
  8. panelbox/core/panel_data.py +387 -0
  9. panelbox/core/results.py +366 -0
  10. panelbox/datasets/__init__.py +0 -0
  11. panelbox/datasets/{data}/__init__.py +0 -0
  12. panelbox/gmm/__init__.py +65 -0
  13. panelbox/gmm/difference_gmm.py +645 -0
  14. panelbox/gmm/estimator.py +562 -0
  15. panelbox/gmm/instruments.py +580 -0
  16. panelbox/gmm/results.py +550 -0
  17. panelbox/gmm/system_gmm.py +621 -0
  18. panelbox/gmm/tests.py +535 -0
  19. panelbox/models/__init__.py +11 -0
  20. panelbox/models/dynamic/__init__.py +0 -0
  21. panelbox/models/iv/__init__.py +0 -0
  22. panelbox/models/static/__init__.py +13 -0
  23. panelbox/models/static/fixed_effects.py +516 -0
  24. panelbox/models/static/pooled_ols.py +298 -0
  25. panelbox/models/static/random_effects.py +512 -0
  26. panelbox/report/__init__.py +61 -0
  27. panelbox/report/asset_manager.py +410 -0
  28. panelbox/report/css_manager.py +472 -0
  29. panelbox/report/exporters/__init__.py +15 -0
  30. panelbox/report/exporters/html_exporter.py +440 -0
  31. panelbox/report/exporters/latex_exporter.py +510 -0
  32. panelbox/report/exporters/markdown_exporter.py +446 -0
  33. panelbox/report/renderers/__init__.py +11 -0
  34. panelbox/report/renderers/static/__init__.py +0 -0
  35. panelbox/report/renderers/static_validation_renderer.py +341 -0
  36. panelbox/report/report_manager.py +502 -0
  37. panelbox/report/template_manager.py +337 -0
  38. panelbox/report/transformers/__init__.py +0 -0
  39. panelbox/report/transformers/static/__init__.py +0 -0
  40. panelbox/report/validation_transformer.py +449 -0
  41. panelbox/standard_errors/__init__.py +0 -0
  42. panelbox/templates/__init__.py +0 -0
  43. panelbox/templates/assets/css/base_styles.css +382 -0
  44. panelbox/templates/assets/css/report_components.css +747 -0
  45. panelbox/templates/assets/js/tab-navigation.js +161 -0
  46. panelbox/templates/assets/js/utils.js +276 -0
  47. panelbox/templates/common/footer.html +24 -0
  48. panelbox/templates/common/header.html +44 -0
  49. panelbox/templates/common/meta.html +5 -0
  50. panelbox/templates/validation/interactive/index.html +272 -0
  51. panelbox/templates/validation/interactive/partials/charts.html +58 -0
  52. panelbox/templates/validation/interactive/partials/methodology.html +201 -0
  53. panelbox/templates/validation/interactive/partials/overview.html +146 -0
  54. panelbox/templates/validation/interactive/partials/recommendations.html +101 -0
  55. panelbox/templates/validation/interactive/partials/test_results.html +231 -0
  56. panelbox/utils/__init__.py +0 -0
  57. panelbox/utils/formatting.py +172 -0
  58. panelbox/utils/matrix_ops.py +233 -0
  59. panelbox/utils/statistical.py +173 -0
  60. panelbox/validation/__init__.py +58 -0
  61. panelbox/validation/base.py +175 -0
  62. panelbox/validation/cointegration/__init__.py +0 -0
  63. panelbox/validation/cross_sectional_dependence/__init__.py +13 -0
  64. panelbox/validation/cross_sectional_dependence/breusch_pagan_lm.py +222 -0
  65. panelbox/validation/cross_sectional_dependence/frees.py +297 -0
  66. panelbox/validation/cross_sectional_dependence/pesaran_cd.py +188 -0
  67. panelbox/validation/heteroskedasticity/__init__.py +13 -0
  68. panelbox/validation/heteroskedasticity/breusch_pagan.py +222 -0
  69. panelbox/validation/heteroskedasticity/modified_wald.py +172 -0
  70. panelbox/validation/heteroskedasticity/white.py +208 -0
  71. panelbox/validation/instruments/__init__.py +0 -0
  72. panelbox/validation/robustness/__init__.py +0 -0
  73. panelbox/validation/serial_correlation/__init__.py +13 -0
  74. panelbox/validation/serial_correlation/baltagi_wu.py +220 -0
  75. panelbox/validation/serial_correlation/breusch_godfrey.py +260 -0
  76. panelbox/validation/serial_correlation/wooldridge_ar.py +200 -0
  77. panelbox/validation/specification/__init__.py +16 -0
  78. panelbox/validation/specification/chow.py +273 -0
  79. panelbox/validation/specification/hausman.py +264 -0
  80. panelbox/validation/specification/mundlak.py +331 -0
  81. panelbox/validation/specification/reset.py +273 -0
  82. panelbox/validation/unit_root/__init__.py +0 -0
  83. panelbox/validation/validation_report.py +257 -0
  84. panelbox/validation/validation_suite.py +401 -0
  85. panelbox-0.2.0.dist-info/METADATA +337 -0
  86. panelbox-0.2.0.dist-info/RECORD +90 -0
  87. panelbox-0.2.0.dist-info/WHEEL +5 -0
  88. panelbox-0.2.0.dist-info/entry_points.txt +2 -0
  89. panelbox-0.2.0.dist-info/licenses/LICENSE +21 -0
  90. panelbox-0.2.0.dist-info/top_level.txt +1 -0
@@ -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})")