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,528 @@
1
+ """
2
+ Standard Error Comparison Tools
3
+
4
+ This module provides tools for comparing different types of standard errors
5
+ for the same model specification. It allows researchers to assess the impact
6
+ of different SE assumptions on inference.
7
+
8
+ Classes
9
+ -------
10
+ StandardErrorComparison
11
+ Compare multiple standard error types for a given model
12
+
13
+ Examples
14
+ --------
15
+ >>> import panelbox as pb
16
+ >>> import pandas as pd
17
+ >>>
18
+ >>> # Fit model
19
+ >>> fe = pb.FixedEffects("y ~ x1 + x2", data, "entity", "time")
20
+ >>> results = fe.fit()
21
+ >>>
22
+ >>> # Compare all SE types
23
+ >>> comparison = pb.StandardErrorComparison(results)
24
+ >>> comp_df = comparison.compare_all()
25
+ >>> print(comp_df)
26
+ >>>
27
+ >>> # Plot comparison
28
+ >>> comparison.plot_comparison()
29
+
30
+ References
31
+ ----------
32
+ - Petersen, M. A. (2009). Estimating standard errors in finance panel data sets:
33
+ Comparing approaches. Review of Financial Studies, 22(1), 435-480.
34
+ - Thompson, S. B. (2011). Simple formulas for standard errors that cluster by
35
+ both firm and time. Journal of Financial Economics, 99(1), 1-10.
36
+ """
37
+
38
+ import numpy as np
39
+ import pandas as pd
40
+ from typing import Dict, List, Optional, Union, Any
41
+ from dataclasses import dataclass
42
+
43
+
44
+ @dataclass
45
+ class ComparisonResult:
46
+ """
47
+ Results from comparing multiple standard error types.
48
+
49
+ Attributes
50
+ ----------
51
+ se_comparison : pd.DataFrame
52
+ DataFrame with columns for each SE type and rows for each coefficient
53
+ se_ratios : pd.DataFrame
54
+ Ratios of each SE type relative to nonrobust (baseline)
55
+ t_stats : pd.DataFrame
56
+ t-statistics for each coefficient under each SE type
57
+ p_values : pd.DataFrame
58
+ p-values for each coefficient under each SE type
59
+ ci_lower : pd.DataFrame
60
+ Lower bounds of 95% confidence intervals
61
+ ci_upper : pd.DataFrame
62
+ Upper bounds of 95% confidence intervals
63
+ significance : pd.DataFrame
64
+ Significance indicators (*, **, ***) for each SE type
65
+ summary_stats : pd.DataFrame
66
+ Summary statistics across SE types
67
+ """
68
+ se_comparison: pd.DataFrame
69
+ se_ratios: pd.DataFrame
70
+ t_stats: pd.DataFrame
71
+ p_values: pd.DataFrame
72
+ ci_lower: pd.DataFrame
73
+ ci_upper: pd.DataFrame
74
+ significance: pd.DataFrame
75
+ summary_stats: pd.DataFrame
76
+
77
+
78
+ class StandardErrorComparison:
79
+ """
80
+ Compare multiple standard error types for a panel data model.
81
+
82
+ This class facilitates comparison of different robust standard error
83
+ estimators to assess the sensitivity of inference to SE assumptions.
84
+
85
+ Parameters
86
+ ----------
87
+ model_results : PanelResults
88
+ Fitted model results object
89
+
90
+ Attributes
91
+ ----------
92
+ model_results : PanelResults
93
+ Original model results
94
+ coef_names : list
95
+ Coefficient names
96
+ coefficients : np.ndarray
97
+ Point estimates (same across all SE types)
98
+ df_resid : int
99
+ Residual degrees of freedom
100
+
101
+ Methods
102
+ -------
103
+ compare_all(se_types=None, **kwargs)
104
+ Compare all specified SE types
105
+ compare_pair(se_type1, se_type2, **kwargs)
106
+ Compare two specific SE types
107
+ plot_comparison(result=None, alpha=0.05)
108
+ Plot comparison of standard errors
109
+ summary(result=None)
110
+ Print summary of comparison
111
+
112
+ Examples
113
+ --------
114
+ Compare all SE types:
115
+
116
+ >>> fe = FixedEffects("y ~ x1 + x2", data, "entity", "time")
117
+ >>> results = fe.fit()
118
+ >>> comparison = StandardErrorComparison(results)
119
+ >>> comp = comparison.compare_all()
120
+ >>> print(comp.se_comparison)
121
+
122
+ Compare specific pair:
123
+
124
+ >>> comp = comparison.compare_pair('robust', 'clustered')
125
+ >>> print(f"Max difference: {comp.se_ratios.max().max():.3f}")
126
+
127
+ Plot comparison:
128
+
129
+ >>> comparison.plot_comparison()
130
+
131
+ References
132
+ ----------
133
+ - Petersen, M. A. (2009). Review of Financial Studies, 22(1), 435-480.
134
+ - Thompson, S. B. (2011). Journal of Financial Economics, 99(1), 1-10.
135
+ """
136
+
137
+ def __init__(self, model_results):
138
+ """
139
+ Initialize comparison with fitted model results.
140
+
141
+ Parameters
142
+ ----------
143
+ model_results : PanelResults
144
+ Fitted model results from FixedEffects, RandomEffects, or PooledOLS
145
+ """
146
+ self.model_results = model_results
147
+ self.coef_names = model_results.params.index.tolist()
148
+ self.coefficients = model_results.params.values
149
+ self.df_resid = model_results.df_resid
150
+
151
+ # Extract model info for computing SEs
152
+ self._extract_model_info()
153
+
154
+ def _extract_model_info(self):
155
+ """Extract model information for computing different SEs."""
156
+ # Store original model object if available
157
+ # Check multiple possible attribute names
158
+ self.model = getattr(self.model_results, 'model', None)
159
+
160
+ # If model not available, store what we need from results
161
+ if self.model is None:
162
+ # Store residuals and fitted values
163
+ self.resid = self.model_results.resid
164
+ self.fittedvalues = self.model_results.fittedvalues
165
+
166
+ # Try to reconstruct design matrix from model_info
167
+ # This is a fallback if we can't refit the model
168
+ self._has_model = False
169
+ else:
170
+ self._has_model = True
171
+
172
+ def compare_all(
173
+ self,
174
+ se_types: Optional[List[str]] = None,
175
+ **kwargs
176
+ ) -> ComparisonResult:
177
+ """
178
+ Compare all specified standard error types.
179
+
180
+ Parameters
181
+ ----------
182
+ se_types : list of str, optional
183
+ List of SE types to compare. If None, uses default list:
184
+ ['nonrobust', 'robust', 'hc3', 'clustered', 'twoway', 'driscoll_kraay']
185
+ **kwargs : dict
186
+ Additional parameters for specific SE types:
187
+ - max_lags : int, for driscoll_kraay and newey_west
188
+ - kernel : str, for driscoll_kraay and newey_west
189
+
190
+ Returns
191
+ -------
192
+ ComparisonResult
193
+ Object containing all comparison results
194
+
195
+ Examples
196
+ --------
197
+ >>> comparison = StandardErrorComparison(results)
198
+ >>> comp = comparison.compare_all()
199
+ >>> print(comp.se_comparison)
200
+
201
+ >>> # Custom SE types
202
+ >>> comp = comparison.compare_all(['nonrobust', 'robust', 'clustered'])
203
+
204
+ >>> # With parameters
205
+ >>> comp = comparison.compare_all(
206
+ ... se_types=['driscoll_kraay', 'newey_west'],
207
+ ... max_lags=3
208
+ ... )
209
+ """
210
+ if se_types is None:
211
+ # Default list of SE types to compare
212
+ se_types = ['nonrobust', 'robust', 'hc3', 'clustered']
213
+
214
+ # Add advanced types if T is large enough
215
+ if hasattr(self.model_results, 'nobs') and self.model_results.nobs > 100:
216
+ se_types.extend(['driscoll_kraay', 'newey_west'])
217
+
218
+ # Store standard errors for each type
219
+ se_dict = {}
220
+
221
+ for se_type in se_types:
222
+ try:
223
+ # Refit model with specific SE type
224
+ if self.model is not None:
225
+ # Get SE-specific kwargs
226
+ se_kwargs = self._get_se_kwargs(se_type, **kwargs)
227
+ results = self.model.fit(cov_type=se_type, **se_kwargs)
228
+ se_dict[se_type] = results.std_errors.values
229
+ else:
230
+ # Can't refit, skip this SE type
231
+ print(f"Warning: Cannot refit model for {se_type}")
232
+ continue
233
+ except Exception as e:
234
+ print(f"Warning: Failed to compute {se_type} SEs: {str(e)}")
235
+ continue
236
+
237
+ if not se_dict:
238
+ raise ValueError("No SE types could be computed successfully")
239
+
240
+ # Create comparison DataFrame
241
+ se_comparison = pd.DataFrame(se_dict, index=self.coef_names)
242
+
243
+ # Compute ratios relative to nonrobust (if available)
244
+ if 'nonrobust' in se_dict:
245
+ se_ratios = se_comparison.div(se_comparison['nonrobust'], axis=0)
246
+ else:
247
+ # Use first SE type as baseline
248
+ baseline = list(se_dict.keys())[0]
249
+ se_ratios = se_comparison.div(se_comparison[baseline], axis=0)
250
+
251
+ # Compute t-statistics
252
+ t_stats = pd.DataFrame(
253
+ {se_type: self.coefficients / se_dict[se_type]
254
+ for se_type in se_dict.keys()},
255
+ index=self.coef_names
256
+ )
257
+
258
+ # Compute p-values (two-tailed)
259
+ from scipy import stats
260
+ p_values = pd.DataFrame(
261
+ {se_type: 2 * (1 - stats.t.cdf(np.abs(t_stats[se_type]), self.df_resid))
262
+ for se_type in se_dict.keys()},
263
+ index=self.coef_names
264
+ )
265
+
266
+ # Compute 95% confidence intervals
267
+ t_crit = stats.t.ppf(0.975, self.df_resid)
268
+ ci_lower = pd.DataFrame(
269
+ {se_type: self.coefficients - t_crit * se_dict[se_type]
270
+ for se_type in se_dict.keys()},
271
+ index=self.coef_names
272
+ )
273
+ ci_upper = pd.DataFrame(
274
+ {se_type: self.coefficients + t_crit * se_dict[se_type]
275
+ for se_type in se_dict.keys()},
276
+ index=self.coef_names
277
+ )
278
+
279
+ # Significance indicators
280
+ significance = p_values.copy()
281
+ significance = significance.applymap(self._significance_stars)
282
+
283
+ # Summary statistics
284
+ summary_stats = pd.DataFrame({
285
+ 'mean_se': se_comparison.mean(axis=1),
286
+ 'std_se': se_comparison.std(axis=1),
287
+ 'min_se': se_comparison.min(axis=1),
288
+ 'max_se': se_comparison.max(axis=1),
289
+ 'range_se': se_comparison.max(axis=1) - se_comparison.min(axis=1),
290
+ 'cv_se': se_comparison.std(axis=1) / se_comparison.mean(axis=1) # Coefficient of variation
291
+ })
292
+
293
+ return ComparisonResult(
294
+ se_comparison=se_comparison,
295
+ se_ratios=se_ratios,
296
+ t_stats=t_stats,
297
+ p_values=p_values,
298
+ ci_lower=ci_lower,
299
+ ci_upper=ci_upper,
300
+ significance=significance,
301
+ summary_stats=summary_stats
302
+ )
303
+
304
+ def compare_pair(
305
+ self,
306
+ se_type1: str,
307
+ se_type2: str,
308
+ **kwargs
309
+ ) -> ComparisonResult:
310
+ """
311
+ Compare two specific standard error types.
312
+
313
+ Parameters
314
+ ----------
315
+ se_type1 : str
316
+ First SE type (e.g., 'nonrobust')
317
+ se_type2 : str
318
+ Second SE type (e.g., 'clustered')
319
+ **kwargs : dict
320
+ Additional parameters for SE types
321
+
322
+ Returns
323
+ -------
324
+ ComparisonResult
325
+ Comparison results for the two SE types
326
+
327
+ Examples
328
+ --------
329
+ >>> comp = comparison.compare_pair('robust', 'clustered')
330
+ >>> print(comp.se_ratios)
331
+ """
332
+ return self.compare_all(se_types=[se_type1, se_type2], **kwargs)
333
+
334
+ def plot_comparison(
335
+ self,
336
+ result: Optional[ComparisonResult] = None,
337
+ alpha: float = 0.05,
338
+ figsize: tuple = (12, 8)
339
+ ):
340
+ """
341
+ Plot comparison of standard errors and confidence intervals.
342
+
343
+ Parameters
344
+ ----------
345
+ result : ComparisonResult, optional
346
+ Pre-computed comparison result. If None, computes comparison.
347
+ alpha : float, default=0.05
348
+ Significance level for confidence intervals
349
+ figsize : tuple, default=(12, 8)
350
+ Figure size (width, height)
351
+
352
+ Returns
353
+ -------
354
+ fig : matplotlib.figure.Figure
355
+ The figure object
356
+
357
+ Examples
358
+ --------
359
+ >>> comparison.plot_comparison()
360
+ >>>
361
+ >>> # Custom figure size
362
+ >>> comparison.plot_comparison(figsize=(14, 10))
363
+
364
+ Notes
365
+ -----
366
+ Requires matplotlib to be installed.
367
+ """
368
+ try:
369
+ import matplotlib.pyplot as plt
370
+ except ImportError:
371
+ raise ImportError(
372
+ "Matplotlib is required for plotting. "
373
+ "Install it with: pip install matplotlib"
374
+ )
375
+
376
+ if result is None:
377
+ result = self.compare_all()
378
+
379
+ n_coefs = len(self.coef_names)
380
+ n_se_types = len(result.se_comparison.columns)
381
+
382
+ # Create figure with subplots
383
+ fig, axes = plt.subplots(2, 1, figsize=figsize)
384
+
385
+ # Plot 1: Standard Errors Comparison
386
+ ax1 = axes[0]
387
+ result.se_comparison.plot(kind='bar', ax=ax1)
388
+ ax1.set_title('Standard Errors Comparison', fontsize=14, fontweight='bold')
389
+ ax1.set_xlabel('Coefficient', fontsize=12)
390
+ ax1.set_ylabel('Standard Error', fontsize=12)
391
+ ax1.legend(title='SE Type', bbox_to_anchor=(1.05, 1), loc='upper left')
392
+ ax1.grid(axis='y', alpha=0.3)
393
+
394
+ # Plot 2: Coefficient Estimates with Confidence Intervals
395
+ ax2 = axes[1]
396
+ x = np.arange(n_coefs)
397
+ width = 0.8 / n_se_types
398
+
399
+ for i, se_type in enumerate(result.se_comparison.columns):
400
+ offset = (i - n_se_types/2 + 0.5) * width
401
+ ax2.errorbar(
402
+ x + offset,
403
+ self.coefficients,
404
+ yerr=[
405
+ self.coefficients - result.ci_lower[se_type].values,
406
+ result.ci_upper[se_type].values - self.coefficients
407
+ ],
408
+ fmt='o',
409
+ label=se_type,
410
+ capsize=5,
411
+ capthick=2
412
+ )
413
+
414
+ ax2.axhline(y=0, color='black', linestyle='--', alpha=0.3)
415
+ ax2.set_title('Coefficient Estimates with 95% Confidence Intervals',
416
+ fontsize=14, fontweight='bold')
417
+ ax2.set_xlabel('Coefficient', fontsize=12)
418
+ ax2.set_ylabel('Estimate', fontsize=12)
419
+ ax2.set_xticks(x)
420
+ ax2.set_xticklabels(self.coef_names, rotation=45, ha='right')
421
+ ax2.legend(title='SE Type', bbox_to_anchor=(1.05, 1), loc='upper left')
422
+ ax2.grid(axis='y', alpha=0.3)
423
+
424
+ plt.tight_layout()
425
+ return fig
426
+
427
+ def summary(self, result: Optional[ComparisonResult] = None):
428
+ """
429
+ Print summary of standard error comparison.
430
+
431
+ Parameters
432
+ ----------
433
+ result : ComparisonResult, optional
434
+ Pre-computed comparison result. If None, computes comparison.
435
+
436
+ Examples
437
+ --------
438
+ >>> comparison.summary()
439
+ """
440
+ if result is None:
441
+ result = self.compare_all()
442
+
443
+ print("=" * 80)
444
+ print("STANDARD ERROR COMPARISON SUMMARY")
445
+ print("=" * 80)
446
+ print()
447
+
448
+ print("Standard Errors by Type:")
449
+ print("-" * 80)
450
+ print(result.se_comparison.to_string())
451
+ print()
452
+
453
+ print("Standard Error Ratios (relative to baseline):")
454
+ print("-" * 80)
455
+ print(result.se_ratios.to_string(float_format=lambda x: f"{x:.3f}"))
456
+ print()
457
+
458
+ print("Significance Levels (* p<0.10, ** p<0.05, *** p<0.01):")
459
+ print("-" * 80)
460
+
461
+ # Combine coefficients with significance
462
+ sig_table = pd.DataFrame({
463
+ 'Coefficient': self.coefficients
464
+ })
465
+ for col in result.significance.columns:
466
+ sig_table[col] = result.significance[col]
467
+ print(sig_table.to_string(float_format=lambda x: f"{x:.4f}" if isinstance(x, float) else str(x)))
468
+ print()
469
+
470
+ print("Summary Statistics Across SE Types:")
471
+ print("-" * 80)
472
+ print(result.summary_stats.to_string(float_format=lambda x: f"{x:.4f}"))
473
+ print()
474
+
475
+ # Inference sensitivity analysis
476
+ print("Inference Sensitivity:")
477
+ print("-" * 80)
478
+
479
+ # Count significant coefficients by SE type
480
+ sig_counts = (result.p_values < 0.05).sum()
481
+ print(f"Coefficients significant at 5% level:")
482
+ for se_type, count in sig_counts.items():
483
+ print(f" {se_type:20s}: {count}/{len(self.coef_names)}")
484
+ print()
485
+
486
+ # Identify coefficients with inconsistent inference
487
+ sig_matrix = result.p_values < 0.05
488
+ inconsistent = sig_matrix.sum(axis=1)
489
+ inconsistent = inconsistent[(inconsistent > 0) & (inconsistent < len(result.p_values.columns))]
490
+
491
+ if len(inconsistent) > 0:
492
+ print("⚠️ Coefficients with inconsistent inference across SE types:")
493
+ for coef in inconsistent.index:
494
+ sig_types = sig_matrix.loc[coef]
495
+ sig_list = [st for st, is_sig in sig_types.items() if is_sig]
496
+ nonsig_list = [st for st, is_sig in sig_types.items() if not is_sig]
497
+ print(f" {coef}:")
498
+ print(f" Significant with: {', '.join(sig_list)}")
499
+ print(f" Not significant with: {', '.join(nonsig_list)}")
500
+ else:
501
+ print("✓ Inference is consistent across all SE types")
502
+
503
+ print()
504
+ print("=" * 80)
505
+
506
+ def _get_se_kwargs(self, se_type: str, **kwargs) -> Dict[str, Any]:
507
+ """Get SE-specific keyword arguments."""
508
+ se_kwargs = {}
509
+
510
+ if se_type in ['driscoll_kraay', 'newey_west']:
511
+ if 'max_lags' in kwargs:
512
+ se_kwargs['max_lags'] = kwargs['max_lags']
513
+ if 'kernel' in kwargs:
514
+ se_kwargs['kernel'] = kwargs['kernel']
515
+
516
+ return se_kwargs
517
+
518
+ @staticmethod
519
+ def _significance_stars(p_value: float) -> str:
520
+ """Convert p-value to significance stars."""
521
+ if p_value < 0.01:
522
+ return '***'
523
+ elif p_value < 0.05:
524
+ return '**'
525
+ elif p_value < 0.10:
526
+ return '*'
527
+ else:
528
+ return ''