systemgmmkit 0.5.2__tar.gz → 0.5.4__tar.gz

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 (55) hide show
  1. {systemgmmkit-0.5.2/src/systemgmmkit.egg-info → systemgmmkit-0.5.4}/PKG-INFO +4 -1
  2. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/README.md +3 -0
  3. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/pyproject.toml +1 -1
  4. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/__init__.py +1 -1
  5. systemgmmkit-0.5.4/src/systemgmmkit/estimators/first_difference.py +219 -0
  6. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/fixed_effects.py +11 -8
  7. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/native_gmm.py +462 -308
  8. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/parity.py +50 -11
  9. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4/src/systemgmmkit.egg-info}/PKG-INFO +4 -1
  10. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit.egg-info/SOURCES.txt +1 -0
  11. systemgmmkit-0.5.4/tests/test_first_difference_reporting.py +55 -0
  12. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_parity_native_gmm.py +1 -1
  13. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_decomposition_controls_certification.py +9 -4
  14. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_no_controls_certification.py +4 -4
  15. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_parity.py +18 -15
  16. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_three_way_controls_certification.py +9 -4
  17. systemgmmkit-0.5.2/src/systemgmmkit/estimators/first_difference.py +0 -82
  18. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/LICENSE +0 -0
  19. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/setup.cfg +0 -0
  20. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/cli.py +0 -0
  21. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/diagnostics/__init__.py +0 -0
  22. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/diagnostics/gmm.py +0 -0
  23. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/diagnostics/panel.py +0 -0
  24. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/diagnostics.py +0 -0
  25. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/dynamic_panel.py +0 -0
  26. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/gmm_parity_policy.py +0 -0
  27. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/panel_iv.py +0 -0
  28. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/presets.py +0 -0
  29. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/pydynpd_backend.py +0 -0
  30. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/pydynpd_output_parser.py +0 -0
  31. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/random_effects.py +0 -0
  32. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/reporting/__init__.py +0 -0
  33. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/reporting/parity.py +0 -0
  34. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/reporting.py +0 -0
  35. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/spec.py +0 -0
  36. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/suite.py +0 -0
  37. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/tables.py +0 -0
  38. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/validation.py +0 -0
  39. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit.egg-info/dependency_links.txt +0 -0
  40. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit.egg-info/entry_points.txt +0 -0
  41. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit.egg-info/requires.txt +0 -0
  42. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit.egg-info/top_level.txt +0 -0
  43. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_apply_gmm_parity_policy_script.py +0 -0
  44. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_fixed_effects.py +0 -0
  45. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_generic_presets.py +0 -0
  46. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_gmm_parity_policy.py +0 -0
  47. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_pydynpd_backend.py +0 -0
  48. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_pydynpd_output_parser.py +0 -0
  49. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_random_effects_iv_tables.py +0 -0
  50. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_spec_command.py +0 -0
  51. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_validation_diagnostics.py +0 -0
  52. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_decomposition_controls_scaffold.py +0 -0
  53. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_no_controls_scaffold.py +0 -0
  54. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_three_way_controls_scaffold.py +0 -0
  55. {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_uncorrected_se_baseline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: systemgmmkit
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Generic panel-data econometrics workflow helpers for FE, RE, IV/2SLS, and Difference/System GMM in Python.
5
5
  Author: Oluwajuwon Mayomi Akanbi
6
6
  License-Expression: MIT
@@ -48,6 +48,9 @@ Dynamic: license-file
48
48
  [![CI](https://github.com/Akanom/systemgmmkit/actions/workflows/ci.yml/badge.svg)](https://github.com/Akanom/systemgmmkit/actions/workflows/ci.yml)
49
49
  [![Publish](https://github.com/Akanom/systemgmmkit/actions/workflows/publish.yml/badge.svg)](https://github.com/Akanom/systemgmmkit/actions/workflows/publish.yml)
50
50
  [![Downloads](https://img.shields.io/pepy/dm/systemgmmkit)](https://pepy.tech/project/systemgmmkit)
51
+
52
+ ---
53
+
51
54
  `systemgmmkit` is a Python workflow package for panel-data econometrics.
52
55
 
53
56
  It supports reusable model specification, panel validation, static panel estimation, dynamic-panel GMM estimation, backend routing, diagnostics interpretation, reproducible reporting, and regression-table export.
@@ -6,6 +6,9 @@
6
6
  [![CI](https://github.com/Akanom/systemgmmkit/actions/workflows/ci.yml/badge.svg)](https://github.com/Akanom/systemgmmkit/actions/workflows/ci.yml)
7
7
  [![Publish](https://github.com/Akanom/systemgmmkit/actions/workflows/publish.yml/badge.svg)](https://github.com/Akanom/systemgmmkit/actions/workflows/publish.yml)
8
8
  [![Downloads](https://img.shields.io/pepy/dm/systemgmmkit)](https://pepy.tech/project/systemgmmkit)
9
+
10
+ ---
11
+
9
12
  `systemgmmkit` is a Python workflow package for panel-data econometrics.
10
13
 
11
14
  It supports reusable model specification, panel validation, static panel estimation, dynamic-panel GMM estimation, backend routing, diagnostics interpretation, reproducible reporting, and regression-table export.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "systemgmmkit"
7
- version = "0.5.2"
7
+ version = "0.5.4"
8
8
  description = "Generic panel-data econometrics workflow helpers for FE, RE, IV/2SLS, and Difference/System GMM in Python."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -75,7 +75,7 @@ __all__ = [
75
75
  "first_difference",
76
76
  ]
77
77
 
78
- __version__ = "0.5.2"
78
+ __version__ = "0.5.3"
79
79
 
80
80
  import contextlib
81
81
 
@@ -0,0 +1,219 @@
1
+ """First-difference panel estimator.
2
+
3
+ This module provides a lightweight first-difference OLS estimator for panel data.
4
+ The result object exposes ``summary_frame()`` so it is compatible with
5
+ systemgmmkit reporting helpers:
6
+
7
+ - result_to_frame()
8
+ - combine_result_frames()
9
+ - export_regression_table()
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from math import erf, sqrt
16
+ from typing import Iterable
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+
21
+
22
+ def _normal_two_sided_pvalue(z: float) -> float:
23
+ """Two-sided normal-approximation p-value."""
24
+ if not np.isfinite(z):
25
+ return float("nan")
26
+ cdf = 0.5 * (1.0 + erf(abs(float(z)) / sqrt(2.0)))
27
+ return float(2.0 * (1.0 - cdf))
28
+
29
+
30
+ @dataclass
31
+ class FirstDifferenceResult:
32
+ """Result container for first-difference OLS."""
33
+
34
+ params: dict[str, float]
35
+ residuals: pd.Series
36
+ fitted_values: pd.Series
37
+ y: str
38
+ x: list[str]
39
+ entity: str
40
+ time: str
41
+ nobs: int
42
+ method: str = "first_difference_ols"
43
+ std_errors: dict[str, float] = field(default_factory=dict)
44
+ t_stats: dict[str, float] = field(default_factory=dict)
45
+ p_values: dict[str, float] = field(default_factory=dict)
46
+ r2: float | None = None
47
+ rss: float | None = None
48
+ df_resid: int | None = None
49
+ rank: int | None = None
50
+
51
+ def summary_frame(self) -> pd.DataFrame:
52
+ """Return coefficient table compatible with systemgmmkit reporting.
53
+
54
+ Contract:
55
+ systemgmmkit.tables.result_to_frame() inserts the ``term`` column
56
+ from the summary frame index. Therefore this method must return
57
+ terms as the DataFrame index, not as a pre-existing ``term`` column.
58
+ """
59
+ rows: list[dict[str, float]] = []
60
+ terms = list(self.params.keys())
61
+
62
+ for term in terms:
63
+ coef = self.params.get(term, float("nan"))
64
+ se = self.std_errors.get(term, float("nan"))
65
+ stat = self.t_stats.get(term, float("nan"))
66
+ pval = self.p_values.get(term, float("nan"))
67
+
68
+ rows.append(
69
+ {
70
+ "coef": coef,
71
+ "std_err": se,
72
+ "statistic": stat,
73
+ "p_value": pval,
74
+ }
75
+ )
76
+
77
+ return pd.DataFrame(rows, index=pd.Index(terms, name="term"))
78
+
79
+
80
+ def first_difference(
81
+ data: pd.DataFrame,
82
+ y: str,
83
+ x: Iterable[str],
84
+ entity: str,
85
+ time: str,
86
+ drop_missing: bool = True,
87
+ ) -> FirstDifferenceResult:
88
+ """Estimate a first-difference OLS model.
89
+
90
+ Parameters
91
+ ----------
92
+ data:
93
+ Panel dataframe.
94
+ y:
95
+ Dependent variable.
96
+ x:
97
+ Regressors.
98
+ entity:
99
+ Entity identifier.
100
+ time:
101
+ Time identifier.
102
+ drop_missing:
103
+ Whether to drop rows with missing differenced values.
104
+
105
+ Returns
106
+ -------
107
+ FirstDifferenceResult
108
+ Result object with coefficients, residuals, fitted values,
109
+ standard errors, test statistics, p-values, and summary_frame().
110
+ """
111
+ x_list = list(x)
112
+ required = [entity, time, y] + x_list
113
+
114
+ missing = [col for col in required if col not in data.columns]
115
+ if missing:
116
+ raise ValueError(f"Missing required columns: {missing}")
117
+
118
+ df = data[required].copy()
119
+ df = df.sort_values([entity, time]).reset_index(drop=False)
120
+
121
+ numeric_cols = [y] + x_list
122
+ for col in numeric_cols:
123
+ df[col] = pd.to_numeric(df[col], errors="coerce")
124
+
125
+ group = df.groupby(entity, sort=False)
126
+
127
+ diff_cols: dict[str, pd.Series] = {}
128
+ diff_cols[f"d_{y}"] = group[y].diff()
129
+
130
+ for col in x_list:
131
+ diff_cols[f"d_{col}"] = group[col].diff()
132
+
133
+ diff_df = pd.DataFrame(diff_cols, index=df.index)
134
+ diff_df["_original_index"] = df["index"].to_numpy()
135
+
136
+ if drop_missing:
137
+ diff_df = diff_df.dropna().copy()
138
+
139
+ if diff_df.empty:
140
+ raise ValueError("No observations remain after first differencing.")
141
+
142
+ y_vec = diff_df[f"d_{y}"].to_numpy(dtype=float)
143
+ x_mat = diff_df[[f"d_{col}" for col in x_list]].to_numpy(dtype=float)
144
+
145
+ finite_mask = np.isfinite(y_vec) & np.all(np.isfinite(x_mat), axis=1)
146
+ y_vec = y_vec[finite_mask]
147
+ x_mat = x_mat[finite_mask]
148
+ used_index = diff_df.loc[finite_mask, "_original_index"].to_numpy()
149
+
150
+ if y_vec.size == 0:
151
+ raise ValueError("No finite observations remain after differencing.")
152
+
153
+ if x_mat.shape[1] == 0:
154
+ raise ValueError("At least one regressor is required.")
155
+
156
+ beta, _, rank, _ = np.linalg.lstsq(x_mat, y_vec, rcond=None)
157
+
158
+ fitted = x_mat @ beta
159
+ resid = y_vec - fitted
160
+
161
+ nobs = int(y_vec.shape[0])
162
+ k = int(x_mat.shape[1])
163
+ df_resid = max(nobs - int(rank), 0)
164
+ rss = float(resid @ resid)
165
+
166
+ if nobs > 0:
167
+ tss = float(((y_vec - y_vec.mean()) @ (y_vec - y_vec.mean())))
168
+ r2 = float(1.0 - rss / tss) if tss > 0 else float("nan")
169
+ else:
170
+ r2 = float("nan")
171
+
172
+ xtx = x_mat.T @ x_mat
173
+ xtx_inv = np.linalg.pinv(xtx)
174
+
175
+ if df_resid > 0:
176
+ sigma2 = rss / df_resid
177
+ cov = sigma2 * xtx_inv
178
+ se_arr = np.sqrt(np.maximum(np.diag(cov), 0.0))
179
+ else:
180
+ se_arr = np.full(k, np.nan)
181
+
182
+ params = {name: float(value) for name, value in zip(x_list, beta)}
183
+ std_errors = {name: float(value) for name, value in zip(x_list, se_arr)}
184
+
185
+ t_stats: dict[str, float] = {}
186
+ p_values: dict[str, float] = {}
187
+
188
+ for name in x_list:
189
+ coef = params[name]
190
+ se = std_errors[name]
191
+
192
+ if np.isfinite(se) and se > 0:
193
+ stat = float(coef / se)
194
+ else:
195
+ stat = float("nan")
196
+
197
+ t_stats[name] = stat
198
+ p_values[name] = _normal_two_sided_pvalue(stat)
199
+
200
+ residuals = pd.Series(resid, index=used_index, name="residual")
201
+ fitted_values = pd.Series(fitted, index=used_index, name="fitted")
202
+
203
+ return FirstDifferenceResult(
204
+ params=params,
205
+ std_errors=std_errors,
206
+ t_stats=t_stats,
207
+ p_values=p_values,
208
+ residuals=residuals,
209
+ fitted_values=fitted_values,
210
+ y=y,
211
+ x=x_list,
212
+ entity=entity,
213
+ time=time,
214
+ nobs=nobs,
215
+ r2=r2,
216
+ rss=rss,
217
+ df_resid=df_resid,
218
+ rank=int(rank),
219
+ )
@@ -119,7 +119,17 @@ def _build_lsdv_design(
119
119
  X_parts: list[pd.DataFrame] = []
120
120
  notes: list[str] = []
121
121
 
122
- # Structural regressors first; output is restricted to these coefficients.
122
+ # Include an intercept before fixed-effect dummies. With drop_first=True
123
+ # for dummy variables, constant + N-1 dummies is the standard LSDV
124
+ # parameterisation and is slope-equivalent to the within estimator used by
125
+ # Stata xtreg, fe. Omitting the constant while also dropping a dummy
126
+ # incorrectly constrains the base group intercept to zero and changes slopes.
127
+ X_parts.append(
128
+ pd.DataFrame({"const": np.ones(len(work), dtype=float)}, index=work.index)
129
+ )
130
+
131
+ # Structural regressors follow; output is restricted to these coefficients
132
+ # plus the constant when it survives collinearity checks.
123
133
  X_reg = work[spec.regressors].astype(float)
124
134
  X_parts.append(X_reg)
125
135
 
@@ -139,13 +149,6 @@ def _build_lsdv_design(
139
149
  X_parts.append(d_time)
140
150
  notes.append("Time fixed effects included via LSDV dummies.")
141
151
 
142
- # Constant is included only when no FE are requested. With FE dummies, a
143
- # constant plus dropped categories yields the same slopes but adds clutter.
144
- if not spec.entity_effects and not spec.time_effects:
145
- X_parts.insert(
146
- 0, pd.DataFrame({"const": np.ones(len(work), dtype=float)}, index=work.index)
147
- )
148
-
149
152
  X = pd.concat(X_parts, axis=1)
150
153
 
151
154
  # Drop exactly collinear columns if requested. This protects against absorbed