systemgmmkit 0.5.8.dev0__tar.gz → 0.5.9__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 (57) hide show
  1. {systemgmmkit-0.5.8.dev0/src/systemgmmkit.egg-info → systemgmmkit-0.5.9}/PKG-INFO +1 -1
  2. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/pyproject.toml +2 -4
  3. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/__init__.py +3 -1
  4. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/native_gmm.py +42 -681
  5. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9/src/systemgmmkit.egg-info}/PKG-INFO +1 -1
  6. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/LICENSE +0 -0
  7. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/README.md +0 -0
  8. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/setup.cfg +0 -0
  9. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/cli.py +0 -0
  10. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/diagnostics/__init__.py +0 -0
  11. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/diagnostics/gmm.py +0 -0
  12. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/diagnostics/panel.py +0 -0
  13. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/diagnostics.py +0 -0
  14. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/dynamic_panel.py +0 -0
  15. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/estimators/first_difference.py +0 -0
  16. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/fixed_effects.py +0 -0
  17. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/gmm_parity_policy.py +0 -0
  18. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/linear.py +0 -0
  19. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/panel_iv.py +0 -0
  20. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/parity.py +0 -0
  21. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/postestimation.py +0 -0
  22. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/presets.py +0 -0
  23. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/pydynpd_backend.py +0 -0
  24. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/pydynpd_output_parser.py +0 -0
  25. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/random_effects.py +0 -0
  26. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/reporting/__init__.py +0 -0
  27. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/reporting/parity.py +0 -0
  28. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/reporting.py +0 -0
  29. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/spec.py +0 -0
  30. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/suite.py +0 -0
  31. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/tables.py +0 -0
  32. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit/validation.py +0 -0
  33. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit.egg-info/SOURCES.txt +0 -0
  34. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit.egg-info/dependency_links.txt +0 -0
  35. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit.egg-info/entry_points.txt +0 -0
  36. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit.egg-info/requires.txt +0 -0
  37. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/src/systemgmmkit.egg-info/top_level.txt +0 -0
  38. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_apply_gmm_parity_policy_script.py +0 -0
  39. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_first_difference_reporting.py +0 -0
  40. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_fixed_effects.py +0 -0
  41. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_generic_presets.py +0 -0
  42. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_gmm_parity_policy.py +0 -0
  43. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_ols_postestimation.py +0 -0
  44. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_parity_native_gmm.py +0 -0
  45. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_pydynpd_backend.py +0 -0
  46. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_pydynpd_output_parser.py +0 -0
  47. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_random_effects_iv_tables.py +0 -0
  48. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_spec_command.py +0 -0
  49. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_validation_diagnostics.py +0 -0
  50. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_xtabond2_system_gmm_decomposition_controls_certification.py +0 -0
  51. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_xtabond2_system_gmm_decomposition_controls_scaffold.py +0 -0
  52. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_xtabond2_system_gmm_no_controls_certification.py +0 -0
  53. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_xtabond2_system_gmm_no_controls_scaffold.py +0 -0
  54. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_xtabond2_system_gmm_parity.py +0 -0
  55. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_xtabond2_system_gmm_three_way_controls_certification.py +0 -0
  56. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/tests/test_xtabond2_system_gmm_three_way_controls_scaffold.py +0 -0
  57. {systemgmmkit-0.5.8.dev0 → systemgmmkit-0.5.9}/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.8.dev0
3
+ Version: 0.5.9
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "systemgmmkit"
7
- version = "0.5.8.dev0"
7
+ version = "0.5.9"
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"
@@ -51,7 +51,7 @@ where = ["src"]
51
51
 
52
52
  [tool.ruff]
53
53
  line-length = 100
54
- target-version = "0.5.8.dev0"
54
+ target-version = "0.5.9"
55
55
 
56
56
  [tool.ruff.lint]
57
57
  select = ["E", "F", "I", "B", "UP", "SIM"]
@@ -72,5 +72,3 @@ line-ending = "auto"
72
72
 
73
73
 
74
74
 
75
-
76
-
@@ -75,7 +75,7 @@ __all__ = [
75
75
  "first_difference",
76
76
  ]
77
77
 
78
- __version__ = "0.5.8"
78
+ __version__ = "0.5.9"
79
79
 
80
80
  import contextlib
81
81
 
@@ -107,3 +107,5 @@ from .postestimation import (
107
107
  )
108
108
 
109
109
 
110
+
111
+
@@ -39,10 +39,6 @@ class NativeGMMResult:
39
39
  sargan_p: float | None = None
40
40
  ar1_p: float | None = None
41
41
  ar2_p: float | None = None
42
- ar1_z: float | None = None
43
- ar2_z: float | None = None
44
- ar1: float | None = None
45
- ar2: float | None = None
46
42
  z_shape: tuple[int, int] | None = None
47
43
  w_shape: tuple[int, int] | None = None
48
44
  j_stat: float | None = None
@@ -53,16 +49,6 @@ class NativeGMMResult:
53
49
  overid_df: int | None = None
54
50
  hansen_j_error: str | None = None
55
51
 
56
- # Stata/xtabond2-style diagnostic aliases for export/comparison layers.
57
- hansen: float | None = None
58
- hansenp: float | None = None
59
- sargan: float | None = None
60
- sarganp: float | None = None
61
- n_g: int | None = None
62
- df_m: int | None = None
63
- df_r: int | None = None
64
- j_p: float | None = None
65
-
66
52
  def summary_frame(self) -> pd.DataFrame:
67
53
  return pd.DataFrame(
68
54
  {
@@ -1253,461 +1239,66 @@ def _native_overid_diagnostics(
1253
1239
  return pvalue, pvalue
1254
1240
 
1255
1241
 
1256
-
1257
- def _native_export_ar_debug_inputs(
1258
- *,
1259
- spec: DynamicPanelSpec,
1260
- data: pd.DataFrame,
1261
- entity: str,
1262
- time: str | None,
1263
- row_index: np.ndarray,
1264
- residual_vec: np.ndarray,
1265
- row_meta: object | None = None,
1266
- ) -> None:
1267
- """Export residual-level inputs for AR diagnostic parity research.
1268
-
1269
- Diagnostic-only. Activated only when SYSTEMGMMKIT_AR_DEBUG_DIR is set.
1270
- """
1271
-
1272
- import json as _json
1273
- import os as _os
1274
- from pathlib import Path as _Path
1275
-
1276
- debug_dir_raw = _os.getenv("SYSTEMGMMKIT_AR_DEBUG_DIR")
1277
- if not debug_dir_raw:
1278
- return
1279
-
1280
- debug_dir = _Path(debug_dir_raw)
1281
- debug_dir.mkdir(parents=True, exist_ok=True)
1282
-
1283
- safe_name = "".join(
1284
- ch if ch.isalnum() or ch in "-_." else "_"
1285
- for ch in str(getattr(spec, "name", "native_gmm"))
1286
- )
1287
-
1288
- resid = np.asarray(residual_vec, dtype=float).reshape(-1)
1289
- idx = np.asarray(row_index).reshape(-1)
1290
-
1291
- base = data.copy()
1292
- if time is not None and time in base.columns:
1293
- base = base.sort_values([entity, time]).copy()
1294
- else:
1295
- base = base.sort_values([entity]).copy()
1296
-
1297
- rows = None
1298
-
1299
- try:
1300
- idx_int = idx.astype(int)
1301
- if (
1302
- idx_int.size >= resid.size
1303
- and idx_int.min(initial=0) >= 0
1304
- and idx_int.max(initial=-1) < len(base)
1305
- ):
1306
- rows = base.iloc[idx_int[: resid.size]].copy()
1307
- except Exception:
1308
- rows = None
1309
-
1310
- if rows is None:
1311
- try:
1312
- if idx.size >= resid.size:
1313
- rows = data.loc[idx[: resid.size]].copy()
1314
- except Exception:
1315
- rows = None
1316
-
1317
- if rows is None or len(rows) != resid.size:
1318
- rows = base.iloc[: min(len(base), resid.size)].copy()
1319
- resid = resid[: len(rows)]
1320
-
1321
- out = pd.DataFrame(
1322
- {
1323
- "spec_name": str(getattr(spec, "name", "")),
1324
- "system": bool(getattr(spec, "system", False)),
1325
- "transformation": str(getattr(spec, "transformation", "")),
1326
- "row_number": np.arange(len(rows), dtype=int),
1327
- "row_index": idx[: len(rows)] if idx.size >= len(rows) else np.arange(len(rows)),
1328
- "entity": rows[entity].to_numpy() if entity in rows.columns else np.nan,
1329
- "time": rows[time].to_numpy() if time is not None and time in rows.columns else np.nan,
1330
- "residual": resid,
1331
- }
1332
- )
1333
-
1334
- try:
1335
- meta = list(row_meta) if row_meta is not None else []
1336
- if len(meta) >= len(out):
1337
- eq_values = []
1338
- raw_values = []
1339
- for item in meta[: len(out)]:
1340
- raw_values.append(_json.dumps(item, default=str))
1341
- eq = None
1342
- if isinstance(item, dict):
1343
- eq = item.get("eq") or item.get("equation") or item.get("block")
1344
- else:
1345
- eq = (
1346
- getattr(item, "eq", None)
1347
- or getattr(item, "equation", None)
1348
- or getattr(item, "block", None)
1349
- )
1350
- eq_values.append(eq)
1351
- out["equation"] = eq_values
1352
- out["row_meta"] = raw_values
1353
- except Exception:
1354
- pass
1355
-
1356
- out_path = debug_dir / f"{safe_name}_ar_debug_inputs.csv"
1357
- out.to_csv(out_path, index=False)
1358
-
1359
- meta_path = debug_dir / f"{safe_name}_ar_debug_inputs_meta.json"
1360
- meta_path.write_text(
1361
- _json.dumps(
1362
- {
1363
- "spec_name": str(getattr(spec, "name", "")),
1364
- "system": bool(getattr(spec, "system", False)),
1365
- "transformation": str(getattr(spec, "transformation", "")),
1366
- "n_rows": int(len(out)),
1367
- "source": "systemgmmkit native AR debug export",
1368
- },
1369
- indent=2,
1370
- ),
1371
- encoding="utf-8",
1372
- )
1373
-
1374
-
1375
-
1376
- def _native_xtabond2_ab_serial_correlation_tests(
1242
+ def _native_ab_serial_correlation_pvalues(
1377
1243
  *,
1378
1244
  data: pd.DataFrame,
1379
1245
  entity: str,
1380
- time: str | None,
1246
+ time: str,
1381
1247
  row_index: np.ndarray,
1382
- residual_vec: np.ndarray,
1248
+ residuals: np.ndarray,
1383
1249
  X: np.ndarray,
1384
- Z: np.ndarray,
1385
- W: np.ndarray,
1386
- bread: np.ndarray,
1387
- cov: np.ndarray,
1388
1250
  coef_names: list[str],
1389
- spec: DynamicPanelSpec | None = None,
1390
- cov_correction: float = 1.0,
1391
- windmeijer_xtabond2_small_correction: float = 1.0,
1392
- ) -> dict[str, float | None]:
1393
- """Compute xtabond2-compatible Arellano-Bond AR(1)/AR(2) tests.
1394
-
1395
- This follows xtabond2's robust/two-step specification-test denominator:
1396
-
1397
- m2VZXA = -2 * e(V_raw) * X'Z * A
1398
- d = wHw + Xw * (m2VZXA * ZHw' + V_robust * Xw')
1399
- ar_l = ew_l / sqrt(d)
1400
-
1401
- where V_raw is the uncorrected GMM bread matrix and V_robust is the
1402
- robust/Windmeijer base covariance before xtabond2's final small-sample
1403
- reposting correction. In this implementation that is recovered as:
1404
-
1405
- V_robust = cov / (cov_correction * windmeijer_xtabond2_small_correction)
1251
+ system: bool,
1252
+ ) -> tuple[float | None, float | None]:
1253
+ """Experimental Arellano-Bond AR(1)/AR(2)-style residual tests.
1406
1254
 
1407
- This diagnostic is post-estimation only and does not affect coefficients
1408
- or standard errors.
1255
+ Uses residuals from differenced-equation rows. For System GMM, level rows
1256
+ are excluded using the level-only `_con` marker.
1409
1257
  """
1410
- resid = np.asarray(residual_vec, dtype=float).reshape(-1)
1411
- X = np.asarray(X, dtype=float)
1412
- Z = np.asarray(Z, dtype=float)
1413
- W = np.asarray(W, dtype=float)
1414
- bread = np.asarray(bread, dtype=float)
1415
- cov = np.asarray(cov, dtype=float)
1416
-
1417
- if resid.size == 0:
1418
- return {"ar1_z": None, "ar1_p": None, "ar2_z": None, "ar2_p": None}
1419
-
1420
- if X.shape[0] != resid.size or Z.shape[0] != resid.size:
1421
- return {"ar1_z": None, "ar1_p": None, "ar2_z": None, "ar2_p": None}
1422
-
1423
- base = data.copy()
1424
- if time is not None and time in base.columns:
1425
- base = base.sort_values([entity, time]).copy()
1426
- else:
1427
- base = base.sort_values([entity]).copy()
1428
-
1429
- idx = np.asarray(row_index).reshape(-1)
1430
-
1431
1258
  try:
1432
- idx_int = idx.astype(int)
1433
- if (
1434
- idx_int.size == resid.size
1435
- and idx_int.min(initial=0) >= 0
1436
- and idx_int.max(initial=-1) < len(base)
1437
- ):
1438
- rows = base.iloc[idx_int].copy()
1439
- elif idx.size == resid.size:
1440
- rows = data.loc[idx].copy()
1441
- else:
1442
- raise ValueError("row_index length does not match residual length")
1443
- except Exception:
1444
- # Conservative fallback matching the transformed-equation row order.
1445
- parts = []
1446
- for _, g in base.groupby(entity, sort=False):
1447
- if len(g) > 1:
1448
- parts.append(g.iloc[1:].copy())
1449
- rows = pd.concat(parts, axis=0) if parts else base.iloc[:0].copy()
1450
- rows = rows.iloc[: resid.size].copy()
1451
-
1452
- if len(rows) != resid.size:
1453
- n_common = min(len(rows), resid.size, X.shape[0], Z.shape[0])
1454
- rows = rows.iloc[:n_common].copy()
1455
- resid = resid[:n_common]
1456
- X = X[:n_common, :]
1457
- Z = Z[:n_common, :]
1458
-
1459
- row_df = pd.DataFrame(
1460
- {
1461
- "row_pos": np.arange(resid.size, dtype=int),
1462
- "entity": rows[entity].to_numpy() if entity in rows.columns else np.arange(resid.size),
1463
- "resid": resid,
1464
- }
1465
- )
1466
-
1467
- if time is not None and time in rows.columns:
1468
- row_df["time"] = pd.to_numeric(rows[time].to_numpy(), errors="coerce")
1469
- else:
1470
- row_df["time"] = row_df["row_pos"].astype(float)
1471
-
1472
- row_df["entity_str"] = row_df["entity"].astype(str)
1473
-
1474
- if spec is not None and bool(getattr(spec, "system", False)) and "_con" in coef_names:
1475
- con_idx = coef_names.index("_con")
1476
- # Current stacked System-GMM design: _con == 0 for transformed/difference
1477
- # rows and _con == 1 for level-equation rows.
1478
- ar_equation_mask = np.isclose(X[:, con_idx], 0.0)
1479
- else:
1480
- ar_equation_mask = np.ones(resid.size, dtype=bool)
1481
-
1482
- denom_scale = float(cov_correction) * float(windmeijer_xtabond2_small_correction)
1483
- if not np.isfinite(denom_scale) or denom_scale <= 0:
1484
- denom_scale = 1.0
1485
-
1486
- v_robust_for_ar = cov / denom_scale
1487
-
1488
- ZX = X.T @ Z
1489
- m2VZXA = -2.0 * bread @ ZX @ W
1490
-
1491
- def _compute_lag(lag: int) -> tuple[float | None, float | None]:
1492
- df = row_df.copy()
1493
- df["w"] = 0.0
1494
- df["is_ar_equation"] = ar_equation_mask
1495
-
1496
- ar_df = df[df["is_ar_equation"]].copy()
1497
- ar_df = ar_df.sort_values(["entity_str", "time", "row_pos"]).copy()
1498
-
1499
- lagged_rows: list[tuple[int, float]] = []
1500
-
1501
- for _, g in ar_df.groupby("entity_str", sort=False):
1502
- g = g.sort_values(["time", "row_pos"]).copy()
1503
- r = g["resid"].to_numpy(dtype=float)
1504
- pos = g["row_pos"].to_numpy(dtype=int)
1505
-
1506
- if len(r) <= lag:
1507
- continue
1508
-
1509
- for jj in range(lag, len(r)):
1510
- lagged_rows.append((int(pos[jj]), float(r[jj - lag])))
1511
-
1512
- if lagged_rows:
1513
- lagged = pd.DataFrame(lagged_rows, columns=["row_pos", "w_value"])
1514
- df = df.merge(lagged, on="row_pos", how="left")
1515
- df["w"] = df["w_value"].fillna(0.0)
1516
- df = df.drop(columns=["w_value"])
1517
-
1518
- df["ewvar"] = np.where(df["is_ar_equation"], df["w"] * df["resid"], np.nan)
1519
-
1520
- ew = float(np.nansum(df["ewvar"].to_numpy(dtype=float)))
1521
- if not np.isfinite(ew) or ew == 0:
1522
- return None, None
1523
-
1524
- group_ew = (
1525
- df.groupby("entity_str", sort=False)["ewvar"]
1526
- .sum(min_count=1)
1527
- .fillna(0.0)
1528
- .rename("group_ew")
1529
- )
1530
- df = df.join(group_ew, on="entity_str")
1531
-
1532
- w = df["w"].to_numpy(dtype=float)
1533
- group_ew_row = df["group_ew"].to_numpy(dtype=float)
1534
-
1535
- # xtabond2 robust branch:
1536
- # by id: ewi = sum(ewvar)
1537
- # etmp = sign(ewi) * e
1538
- # iweight = abs(ewi)
1539
- # weighted etmp contribution is therefore ewi * e.
1540
- weighted_e = group_ew_row * resid
1541
-
1542
- ZHw = weighted_e.reshape(1, -1) @ Z
1543
- wHw = float(np.asarray(weighted_e.reshape(1, -1) @ w.reshape(-1, 1)).squeeze())
1544
-
1545
- mask = df["is_ar_equation"].to_numpy(dtype=bool)
1546
- Xw = w[mask].reshape(1, -1) @ X[mask, :]
1547
-
1548
- d = float(
1549
- np.asarray(
1550
- wHw + Xw @ (m2VZXA @ ZHw.T + v_robust_for_ar @ Xw.T)
1551
- ).squeeze()
1552
- )
1553
-
1554
- if not np.isfinite(d) or d <= 0:
1555
- return None, None
1556
-
1557
- z = float(ew / np.sqrt(d))
1558
- p = float(2.0 * stats.norm.sf(abs(z)))
1559
- return z, p
1560
-
1561
- ar1_z, ar1_p = _compute_lag(1)
1562
- ar2_z, ar2_p = _compute_lag(2)
1563
-
1564
- return {
1565
- "ar1_z": ar1_z,
1566
- "ar1_p": ar1_p,
1567
- "ar2_z": ar2_z,
1568
- "ar2_p": ar2_p,
1569
- }
1570
-
1259
+ resid = np.asarray(residuals, dtype=float).reshape(-1)
1260
+ idx = np.asarray(row_index)
1571
1261
 
1572
- def _native_ab_serial_correlation_tests(
1573
- *,
1574
- data: pd.DataFrame,
1575
- entity: str,
1576
- time: str | None,
1577
- row_index: np.ndarray,
1578
- residual_vec: np.ndarray,
1579
- spec: DynamicPanelSpec | None = None,
1580
- **_: object,
1581
- ) -> dict[str, float | None]:
1582
- """Compute experimental Arellano-Bond-style AR(1)/AR(2) diagnostics.
1583
-
1584
- WARNING:
1585
- This diagnostic is not yet xtabond2-certified. It is exposed for
1586
- post-estimation development only and must not be described as parity-certified
1587
- until the AR candidate harness reproduces Stata's AR(1)/AR(2) z-statistics.
1588
-
1589
- This post-estimation diagnostic intentionally does not affect coefficient
1590
- or covariance estimation.
1591
-
1592
- Implementation policy:
1593
- - Use entity-level differenced-equation residuals.
1594
- - For System GMM, ignore the level-equation residual block and use only the
1595
- differenced-equation residual block.
1596
- - Report signed z-statistics and two-sided normal p-values.
1597
- - Use entity-cluster moment aggregation: for lag m, compute each entity's
1598
- residual product sum and standardize the cross-entity moment.
1599
-
1600
- This is closer to xtabond2's m-test logic than the previous simple pooled
1601
- residual correlation heuristic, and it exposes z-statistics for parity
1602
- comparison.
1603
- """
1604
-
1605
- from scipy import stats as _stats
1606
-
1607
- resid = np.asarray(residual_vec, dtype=float).reshape(-1)
1608
- idx = np.asarray(row_index).reshape(-1)
1609
-
1610
- if resid.size == 0:
1611
- return {"ar1_z": None, "ar1_p": None, "ar2_z": None, "ar2_p": None}
1612
-
1613
- # Sort data deterministically by entity/time. Use row_index to map residuals
1614
- # back to entity/time rows whenever possible.
1615
- base = data.copy()
1616
- if time is not None and time in base.columns:
1617
- base = base.sort_values([entity, time]).copy()
1618
- else:
1619
- base = base.sort_values([entity]).copy()
1620
-
1621
- # For System GMM, residual_vec usually stacks:
1622
- # [differenced-equation residuals, level-equation residuals]
1623
- # AR tests should use only the differenced-equation block.
1624
- # In the maintained FD001 benchmark this corresponds to sum(T_i - 1).
1625
- if spec is not None and bool(getattr(spec, "system", False)):
1626
- diff_n = int(
1627
- sum(max(len(g) - 1, 0) for _, g in base.groupby(entity, sort=False))
1628
- )
1629
- if 0 < diff_n <= resid.size:
1630
- resid = resid[:diff_n]
1631
- idx = idx[: min(idx.size, diff_n)]
1632
-
1633
- # Build residual frame. Prefer positional row mapping because native matrix
1634
- # construction uses sorted working rows.
1635
- try:
1636
- idx_int = idx.astype(int)
1637
- if idx_int.size == resid.size and idx_int.min(initial=0) >= 0 and idx_int.max(initial=-1) < len(base):
1638
- rows = base.iloc[idx_int].copy()
1639
- elif idx.size == resid.size:
1640
- rows = data.loc[idx].copy()
1262
+ if system and "_con" in coef_names:
1263
+ con_col = coef_names.index("_con")
1264
+ diff_mask = ~np.isclose(np.asarray(X[:, con_col], dtype=float), 1.0)
1641
1265
  else:
1642
- raise ValueError("row_index length does not match residual length")
1643
- except Exception:
1644
- # Fallback: take the final len(resid) sorted differenced rows by entity.
1645
- parts = []
1646
- for _, g in base.groupby(entity, sort=False):
1647
- if len(g) > 1:
1648
- parts.append(g.iloc[1:].copy())
1649
- rows = pd.concat(parts, axis=0) if parts else base.iloc[:0].copy()
1650
- rows = rows.iloc[: resid.size].copy()
1651
-
1652
- if len(rows) != resid.size:
1653
- n = min(len(rows), resid.size)
1654
- rows = rows.iloc[:n].copy()
1655
- resid = resid[:n]
1656
-
1657
- rows["_native_ab_resid"] = resid
1658
-
1659
- if time is not None and time in rows.columns:
1660
- rows = rows.sort_values([entity, time]).copy()
1661
- else:
1662
- rows = rows.sort_values([entity]).copy()
1266
+ diff_mask = np.ones_like(resid, dtype=bool)
1663
1267
 
1664
- def one_lag(lag: int) -> tuple[float | None, float | None]:
1665
- cluster_moments: list[float] = []
1666
-
1667
- for _, g in rows.groupby(entity, sort=False):
1668
- u = np.asarray(g["_native_ab_resid"], dtype=float)
1669
- u = u[np.isfinite(u)]
1670
-
1671
- if u.size <= lag:
1672
- continue
1268
+ idx = idx[diff_mask]
1269
+ resid = resid[diff_mask]
1673
1270
 
1674
- moment = float(np.sum(u[lag:] * u[:-lag]))
1675
- if np.isfinite(moment):
1676
- cluster_moments.append(moment)
1271
+ panel_keys = data.loc[idx, [entity, time]].copy()
1272
+ panel_keys["_resid"] = resid
1273
+ panel_keys = panel_keys.sort_values([entity, time])
1677
1274
 
1678
- if len(cluster_moments) < 2:
1679
- return None, None
1275
+ def _lag_test(lag: int) -> float | None:
1276
+ d = panel_keys.copy()
1277
+ d["_lag"] = d.groupby(entity)["_resid"].shift(lag)
1278
+ d = d.dropna(subset=["_resid", "_lag"])
1680
1279
 
1681
- m = np.asarray(cluster_moments, dtype=float)
1682
- numerator = float(np.sum(m))
1683
- denominator = float(np.sqrt(np.sum(m * m)))
1280
+ if len(d) < 5:
1281
+ return None
1684
1282
 
1685
- if not np.isfinite(denominator) or denominator <= 0:
1686
- return None, None
1283
+ a = d["_resid"].to_numpy(dtype=float)
1284
+ b = d["_lag"].to_numpy(dtype=float)
1687
1285
 
1688
- z = numerator / denominator
1689
- p = float(2.0 * _stats.norm.sf(abs(z)))
1286
+ denom = float(np.sqrt(np.sum(a * a) * np.sum(b * b)))
1287
+ if denom <= 0:
1288
+ return None
1690
1289
 
1691
- return float(z), p
1290
+ corr = float(np.sum(a * b) / denom)
1291
+ z = corr * np.sqrt(len(d))
1692
1292
 
1693
- ar1_z, ar1_p = one_lag(1)
1694
- ar2_z, ar2_p = one_lag(2)
1293
+ if not np.isfinite(z):
1294
+ return None
1695
1295
 
1696
- return {
1697
- "ar1_z": ar1_z,
1698
- "ar1_p": ar1_p,
1699
- "ar2_z": ar2_z,
1700
- "ar2_p": ar2_p,
1701
- }
1296
+ return float(2.0 * stats.norm.sf(abs(z)))
1702
1297
 
1298
+ return _lag_test(1), _lag_test(2)
1703
1299
 
1704
- def _native_ab_serial_correlation_pvalues(
1705
- **kwargs: object,
1706
- ) -> tuple[float | None, float | None]:
1707
- """Backward-compatible wrapper returning only AR p-values."""
1708
-
1709
- tests = _native_ab_serial_correlation_tests(**kwargs)
1710
- return tests.get("ar1_p"), tests.get("ar2_p")
1300
+ except Exception:
1301
+ return None, None
1711
1302
 
1712
1303
 
1713
1304
  def _native_fod_difference_windmeijer_covariance(
@@ -2159,155 +1750,16 @@ def run_native_dynamic_panel_gmm(
2159
1750
  n_params=len(names),
2160
1751
  )
2161
1752
 
2162
- # Optional late-stage matrix dump for xtabond2 AR-test parity.
2163
- #
2164
- # This dump is intentionally placed after final W, bread, residuals, and
2165
- # covariance are available. The earlier SYSTEMGMMKIT_NATIVE_MATRIX_DUMP_DIR
2166
- # dump occurs before final System-GMM Z/W/covariance construction and is
2167
- # therefore not sufficient for reproducing xtabond2's AR denominator:
2168
- # d = wHw + Xw * (m2VZXA * ZHw' + Vrobust * Xw')
2169
- import os as _native_ar_formula_os
2170
-
2171
- native_ar_formula_dump_dir = _native_ar_formula_os.getenv(
2172
- "SYSTEMGMMKIT_NATIVE_AR_FORMULA_DUMP_DIR"
2173
- )
2174
- if native_ar_formula_dump_dir:
2175
- import json as _native_ar_formula_json
2176
- from pathlib import Path as _NativeArFormulaDumpPath
2177
-
2178
- dump_dir = _NativeArFormulaDumpPath(native_ar_formula_dump_dir)
2179
- dump_dir.mkdir(parents=True, exist_ok=True)
2180
-
2181
- safe_name = "".join(
2182
- ch if ch.isalnum() or ch in "-_." else "_"
2183
- for ch in str(getattr(spec, "name", "native_gmm"))
2184
- )
2185
-
2186
- npz_path = dump_dir / f"{safe_name}_ar_formula_inputs.npz"
2187
- json_path = dump_dir / f"{safe_name}_ar_formula_inputs.json"
2188
- row_meta_path = dump_dir / f"{safe_name}_row_meta.json"
2189
-
2190
- def _object_array_from_list(values):
2191
- arr = np.empty(len(values), dtype=object)
2192
- for ii, vv in enumerate(values):
2193
- arr[ii] = np.asarray(vv)
2194
- return arr
2195
-
2196
- _group_indices_obj = _object_array_from_list(group_indices_for_cov)
2197
-
2198
- _maybe_windmeijer_base_cov = locals().get("windmeijer_base_cov", None)
2199
- _maybe_vcov_step1 = locals().get("vcov_step1", None)
2200
- _maybe_s_group = locals().get("s_group", None)
2201
- _maybe_s_group_diag = locals().get("_s_group_diag", None)
2202
-
2203
- np.savez_compressed(
2204
- npz_path,
2205
- y=np.asarray(y, dtype=float),
2206
- X=np.asarray(X, dtype=float),
2207
- Z=np.asarray(Z, dtype=float),
2208
- beta=np.asarray(beta, dtype=float),
2209
- beta1=np.asarray(beta1, dtype=float),
2210
- residuals=np.asarray(residuals, dtype=float),
2211
- residuals1=np.asarray(residuals1, dtype=float),
2212
- residual_vec=np.asarray(residual_vec, dtype=float),
2213
- W1=np.asarray(W1, dtype=float),
2214
- W=np.asarray(W, dtype=float),
2215
- bread=np.asarray(bread, dtype=float),
2216
- D=np.asarray(D, dtype=float),
2217
- ZX=np.asarray(D, dtype=float),
2218
- cov=np.asarray(cov, dtype=float),
2219
- windmeijer_base_cov=(
2220
- np.asarray(_maybe_windmeijer_base_cov, dtype=float)
2221
- if _maybe_windmeijer_base_cov is not None
2222
- else np.asarray([], dtype=float)
2223
- ),
2224
- vcov_step1=(
2225
- np.asarray(_maybe_vcov_step1, dtype=float)
2226
- if _maybe_vcov_step1 is not None
2227
- else np.asarray([], dtype=float)
2228
- ),
2229
- s_group=(
2230
- np.asarray(_maybe_s_group, dtype=float)
2231
- if _maybe_s_group is not None
2232
- else np.asarray([], dtype=float)
2233
- ),
2234
- s_group_diag=(
2235
- np.asarray(_maybe_s_group_diag, dtype=float)
2236
- if _maybe_s_group_diag is not None
2237
- else np.asarray([], dtype=float)
2238
- ),
2239
- row_index=np.asarray([str(v) for v in row_index], dtype=object),
2240
- row_entities_for_cov=np.asarray([str(v) for v in row_entities_for_cov], dtype=object),
2241
- group_indices=_group_indices_obj,
2242
- coef_names=np.asarray(names, dtype=object),
2243
- instrument_names=np.asarray(instrument_names, dtype=object),
2244
- )
2245
-
2246
- with contextlib.suppress(Exception):
2247
- row_meta_path.write_text(
2248
- _native_ar_formula_json.dumps(row_meta, indent=2, default=str),
2249
- encoding="utf-8",
2250
- )
2251
-
2252
- json_path.write_text(
2253
- _native_ar_formula_json.dumps(
2254
- {
2255
- "spec": str(getattr(spec, "name", "")),
2256
- "system": bool(getattr(spec, "system", False)),
2257
- "transformation": str(getattr(spec, "transformation", "")),
2258
- "steps": str(getattr(spec, "steps", "")),
2259
- "windmeijer_requested": bool(windmeijer_requested),
2260
- "use_twostep": bool(use_twostep),
2261
- "nobs_effective": int(nobs_effective),
2262
- "nobs_reported": int(nobs_reported),
2263
- "n_groups_for_cov": int(n_groups_for_cov),
2264
- "n": int(n),
2265
- "k": int(k),
2266
- "j": int(Z.shape[1]),
2267
- "x_shape": list(X.shape),
2268
- "z_shape": list(Z.shape),
2269
- "w1_shape": list(W1.shape),
2270
- "w_shape": list(W.shape),
2271
- "bread_shape": list(bread.shape),
2272
- "cov_shape": list(cov.shape),
2273
- "coef_names": list(names),
2274
- "instrument_names": list(instrument_names),
2275
- "cov_correction": float(locals().get("cov_correction", 1.0)),
2276
- "windmeijer_xtabond2_small_correction": float(
2277
- locals().get("windmeijer_xtabond2_small_correction", 1.0)
2278
- ),
2279
- "has_windmeijer_base_cov": _maybe_windmeijer_base_cov is not None,
2280
- "has_vcov_step1": _maybe_vcov_step1 is not None,
2281
- "has_s_group": _maybe_s_group is not None,
2282
- "has_s_group_diag": _maybe_s_group_diag is not None,
2283
- },
2284
- indent=2,
2285
- ),
2286
- encoding="utf-8",
2287
- )
2288
-
2289
- _ar_serial = _native_xtabond2_ab_serial_correlation_tests(
1753
+ ar1_p, ar2_p = _native_ab_serial_correlation_pvalues(
2290
1754
  data=data,
2291
1755
  entity=entity,
2292
1756
  time=time,
2293
1757
  row_index=np.asarray(row_index),
2294
- residual_vec=residual_vec,
1758
+ residuals=residual_vec,
2295
1759
  X=X,
2296
- Z=Z,
2297
- W=W,
2298
- bread=bread,
2299
- cov=cov,
2300
1760
  coef_names=names,
2301
- spec=spec,
2302
- cov_correction=float(locals().get("cov_correction", 1.0)),
2303
- windmeijer_xtabond2_small_correction=float(
2304
- locals().get("windmeijer_xtabond2_small_correction", 1.0)
2305
- ),
1761
+ system=spec.system,
2306
1762
  )
2307
- ar1_z = _ar_serial.get("ar1_z")
2308
- ar1_p = _ar_serial.get("ar1_p")
2309
- ar2_z = _ar_serial.get("ar2_z")
2310
- ar2_p = _ar_serial.get("ar2_p")
2311
1763
 
2312
1764
  _u_col = residual_vec.reshape(-1, 1)
2313
1765
  _ztu = Z.T @ _u_col
@@ -2385,70 +1837,6 @@ def run_native_dynamic_panel_gmm(
2385
1837
  # reporting a misleading pseudo-Hansen p-value.
2386
1838
  hansen_p = None
2387
1839
 
2388
- # xtabond2-compatible overidentification diagnostics for the maintained
2389
- # first-difference two-step path.
2390
- #
2391
- # xtabond2 computes:
2392
- # Sargan = e1' Z A1 e1, with A1 rescaled by 1/sig2
2393
- # Hansen = e2' Z A2 e2, with robust two-step moment covariance scaling
2394
- #
2395
- # In the native matrix normalization used here, the FD001 parity harness
2396
- # identifies the equivalent formulas as:
2397
- # Hansen = (u2' Z W2 Z' u2) / n_groups
2398
- # Sargan = (u1' Z W1 Z' u1) / sig2
2399
- # sig2 = sum(e1_diff^2) / nobs_effective / 2
2400
- #
2401
- # This override is deliberately restricted to FD two-step style estimation
2402
- # to avoid changing non-FD/FOD behavior before separate certification.
2403
- try:
2404
- _is_fd_transformation_for_overid = (
2405
- str(getattr(spec, "transformation", "")).strip().lower() == "fd"
2406
- )
2407
-
2408
- if use_twostep and _is_fd_transformation_for_overid:
2409
- _u2_overid = np.asarray(residual_vec, dtype=float).reshape(-1, 1)
2410
- _u1_overid = np.asarray(residuals1, dtype=float).reshape(-1, 1)
2411
-
2412
- _ztu2 = Z.T @ _u2_overid
2413
- _hansen_raw_xtabond2 = float(np.asarray(_ztu2.T @ W @ _ztu2).squeeze())
2414
- _hansen_j_stat = _hansen_raw_xtabond2 / max(float(n_groups_for_cov), 1.0)
2415
-
2416
- if _overid_df > 0 and np.isfinite(_hansen_j_stat) and _hansen_j_stat >= 0:
2417
- _hansen_p_candidate = float(stats.chi2.sf(_hansen_j_stat, _overid_df))
2418
- else:
2419
- _hansen_p_candidate = None
2420
-
2421
- if spec.system and "_con" in names:
2422
- _con_col_for_sargan_sig2 = names.index("_con")
2423
- _sargan_sig2_mask = np.isclose(X[:, _con_col_for_sargan_sig2], 0.0)
2424
- else:
2425
- _sargan_sig2_mask = np.ones(np.asarray(residuals1).reshape(-1).shape[0], dtype=bool)
2426
-
2427
- _e1_for_sig2 = np.asarray(residuals1, dtype=float).reshape(-1)[_sargan_sig2_mask]
2428
- _e1_for_sig2 = _e1_for_sig2[np.isfinite(_e1_for_sig2)]
2429
-
2430
- if _e1_for_sig2.size:
2431
- _sig2_xtabond2_sargan = (
2432
- float(np.sum(_e1_for_sig2 ** 2))
2433
- / max(float(nobs_effective), 1.0)
2434
- / 2.0
2435
- )
2436
-
2437
- if np.isfinite(_sig2_xtabond2_sargan) and _sig2_xtabond2_sargan > 0:
2438
- _ztu1 = Z.T @ _u1_overid
2439
- _sargan_unscaled_xtabond2 = float(
2440
- np.asarray(_ztu1.T @ W1 @ _ztu1).squeeze()
2441
- )
2442
- _sargan_stat = _sargan_unscaled_xtabond2 / _sig2_xtabond2_sargan
2443
-
2444
- if _overid_df > 0 and np.isfinite(_sargan_stat) and _sargan_stat >= 0:
2445
- sargan_p = float(stats.chi2.sf(_sargan_stat, _overid_df))
2446
- else:
2447
- sargan_p = None
2448
-
2449
- except Exception as exc:
2450
- _hansen_j_error = f"{type(exc).__name__}: {exc}"
2451
-
2452
1840
 
2453
1841
  _ztu_norm = float(np.linalg.norm(_ztu))
2454
1842
  _w_norm = float(np.linalg.norm(W))
@@ -2550,11 +1938,6 @@ def run_native_dynamic_panel_gmm(
2550
1938
  index=False,
2551
1939
  )
2552
1940
 
2553
- _df_m_export = int(len(names) - (1 if "_con" in names else 0))
2554
- _df_r_export = int(n_groups_for_cov - (1 if bool(spec.system) else 0))
2555
- _j_p_export = None
2556
-
2557
-
2558
1941
  return NativeGMMResult(
2559
1942
  spec=spec,
2560
1943
  nobs=int(nobs_reported),
@@ -2585,18 +1968,10 @@ def run_native_dynamic_panel_gmm(
2585
1968
  ),
2586
1969
  ],
2587
1970
  instrument_names=list(instrument_names),
2588
- # Use the robust Hansen p-value candidate computed from the
2589
- # group-moment robust covariance when available. The earlier
2590
- # hansen_p variable is intentionally left undefined for two-step
2591
- # robust/Windmeijer paths to avoid reporting the old pseudo-Hansen.
2592
- hansen_p=_hansen_p_candidate,
1971
+ hansen_p=hansen_p,
2593
1972
  sargan_p=sargan_p,
2594
1973
  ar1_p=ar1_p,
2595
1974
  ar2_p=ar2_p,
2596
- ar1_z=ar1_z,
2597
- ar2_z=ar2_z,
2598
- ar1=ar1_z,
2599
- ar2=ar2_z,
2600
1975
  z_shape=tuple(Z.shape),
2601
1976
  w_shape=tuple(W.shape),
2602
1977
  j_stat=_j_stat,
@@ -2606,20 +1981,6 @@ def run_native_dynamic_panel_gmm(
2606
1981
  sargan_j_stat=_sargan_stat,
2607
1982
  overid_df=_overid_df,
2608
1983
  hansen_j_error=_hansen_j_error,
2609
-
2610
- # Export aliases used by Stata/xtabond2 comparison workflows.
2611
- hansen=_hansen_j_stat,
2612
- hansenp=_hansen_p_candidate,
2613
- sargan=_sargan_stat,
2614
- sarganp=(
2615
- float(stats.chi2.sf(_sargan_stat, _overid_df))
2616
- if _sargan_stat is not None and _overid_df is not None and _overid_df > 0
2617
- else None
2618
- ),
2619
- n_g=int(data[entity].nunique()) if entity in data.columns else None,
2620
- df_m=_df_m_export,
2621
- df_r=_df_r_export,
2622
- j_p=_j_p_export,
2623
1984
  )
2624
1985
 
2625
1986
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: systemgmmkit
3
- Version: 0.5.8.dev0
3
+ Version: 0.5.9
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
File without changes