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.
- {systemgmmkit-0.5.2/src/systemgmmkit.egg-info → systemgmmkit-0.5.4}/PKG-INFO +4 -1
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/README.md +3 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/pyproject.toml +1 -1
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/__init__.py +1 -1
- systemgmmkit-0.5.4/src/systemgmmkit/estimators/first_difference.py +219 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/fixed_effects.py +11 -8
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/native_gmm.py +462 -308
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/parity.py +50 -11
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4/src/systemgmmkit.egg-info}/PKG-INFO +4 -1
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit.egg-info/SOURCES.txt +1 -0
- systemgmmkit-0.5.4/tests/test_first_difference_reporting.py +55 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_parity_native_gmm.py +1 -1
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_decomposition_controls_certification.py +9 -4
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_no_controls_certification.py +4 -4
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_parity.py +18 -15
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_three_way_controls_certification.py +9 -4
- systemgmmkit-0.5.2/src/systemgmmkit/estimators/first_difference.py +0 -82
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/LICENSE +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/setup.cfg +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/cli.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/diagnostics/__init__.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/diagnostics/gmm.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/diagnostics/panel.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/diagnostics.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/dynamic_panel.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/gmm_parity_policy.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/panel_iv.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/presets.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/pydynpd_backend.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/pydynpd_output_parser.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/random_effects.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/reporting/__init__.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/reporting/parity.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/reporting.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/spec.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/suite.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/tables.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit/validation.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit.egg-info/dependency_links.txt +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit.egg-info/entry_points.txt +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit.egg-info/requires.txt +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/src/systemgmmkit.egg-info/top_level.txt +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_apply_gmm_parity_policy_script.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_fixed_effects.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_generic_presets.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_gmm_parity_policy.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_pydynpd_backend.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_pydynpd_output_parser.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_random_effects_iv_tables.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_spec_command.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_validation_diagnostics.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_decomposition_controls_scaffold.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_no_controls_scaffold.py +0 -0
- {systemgmmkit-0.5.2 → systemgmmkit-0.5.4}/tests/test_xtabond2_system_gmm_three_way_controls_scaffold.py +0 -0
- {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.
|
|
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
|
[](https://github.com/Akanom/systemgmmkit/actions/workflows/ci.yml)
|
|
49
49
|
[](https://github.com/Akanom/systemgmmkit/actions/workflows/publish.yml)
|
|
50
50
|
[](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
|
[](https://github.com/Akanom/systemgmmkit/actions/workflows/ci.yml)
|
|
7
7
|
[](https://github.com/Akanom/systemgmmkit/actions/workflows/publish.yml)
|
|
8
8
|
[](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.
|
|
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"
|
|
@@ -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
|
-
#
|
|
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
|