Quickstart¶
Download the script: quickstart.py
The panel has 20,000 units and 8 periods, with \(\rho = 0.5\), \(\beta = 1\) and \(\gamma = 0.5\) (see the simulated data):
from dpd_stream import dpd_stream, etable
from dpd_stream.simulate import simulate_dynamic
df = simulate_dynamic(20_000, 8, rho=(0.5,), seed=1)
df.write_parquet("panel.parquet")
Difference GMM¶
The formula has three parts: the model, time effects, and instruments.
Model:
l(y, 1)is y’s first lag, taken by the time variable’s value.Time effects:
| yearabsorbs a period effect. It is estimated as a dummy per year, each its own instrument (as xtabond2’syr*in the regressors and iniv()), so every statistic is the dummies’ exactly, but the dummies are not reported. For reported dummies, write them among the regressors and instruments yourself.Instruments:
gmm(y, 2:4)instruments the differenced equation with y’s lags 2 to 4, one column per lag and period, as xtabond2’sgmm(y, lag(2 4)). x is predetermined, so its lags 1 to 3 are valid instruments; w is its own instrument.
diff = dpd_stream("y ~ l(y, 1) + x + w | year | gmm(y, 2:4) + gmm(x, 1:3) + iv(w)",
"panel.parquet", panel=("id", "year"))
diff.summary()
Dynamic panel-data estimation, two-step difference GMM (dpd_stream) vcov: robust (Windmeijer-corrected)
observations: 120,000 groups: 20,000 obs per group: min 6, avg 6.00, max 6 instruments: 39
absorbed fixed effect: year (6 effects, as dummies)
Wald test of all coefficients: chi2(3) = 53969.59, p = 0.0000
Arellano-Bond test for AR(1) in first differences: z = -84.99, p = 0.0000
Arellano-Bond test for AR(2) in first differences: z = -0.29, p = 0.7696
Sargan test of overid. restrictions: chi2(30) = 22.63, p = 0.8300 (not robust)
Hansen test of overid. restrictions: chi2(30) = 22.69, p = 0.8278 (robust)
Difference-in-Hansen tests of exogeneity of instrument subsets:
iv(w)
excluding group: chi2(29) = 22.69, p = 0.7905 difference: chi2(1) = 0.00, p = 0.9918
...
Underidentification (Kleibergen-Paap rk LM, robust): chi2(31) = 7428.63, p = 0.0000 weak identification (KP rk Wald F): 372.21
Sanderson-Windmeijer, l(y, 1): chi2(31) = 7197.96, p = 0.0000
Sanderson-Windmeijer, x: chi2(31) = 8242.24, p = 0.0000
┌─────────────┬──────────┬────────────┬────────────┬──────────┐
│ Coefficient ┆ Estimate ┆ Std. Error ┆ t value ┆ Pr(>|t|) │
╞═════════════╪══════════╪════════════╪════════════╪══════════╡
│ l(y, 1) ┆ 0.501335 ┆ 0.003398 ┆ 147.518876 ┆ 0.0 │
│ x ┆ 0.995548 ┆ 0.005092 ┆ 195.507327 ┆ 0.0 │
│ w ┆ 0.496752 ┆ 0.003504 ┆ 141.777769 ┆ 0.0 │
└─────────────┴──────────┴────────────┴────────────┴──────────┘
How to read the tests:
AR(1) rejects, as it must: first differences are serially correlated by construction.
AR(2) does not reject, which supports lags 2 and deeper of y as instruments.
The Hansen test does not reject the overidentifying restrictions.
The difference-in-Hansen tests check each instrument group in turn.
The identification tests (Kleibergen-Paap, Sanderson-Windmeijer) reject underidentification, and the weak-identification F is large; see identification tests.
System GMM¶
System GMM adds the levels equation, instrumented by lagged differences.
collapse=True keeps the instrument count small (16 here). w is correlated
with the fixed effect, so it instruments the differenced equation only
(eq='diff').
sys_ = dpd_stream("y ~ l(y, 1) + x + w | year | gmm(y, 2:4, collapse=True) "
"+ gmm(x, 1:3, collapse=True) + iv(w, eq='diff')",
"panel.parquet", panel=("id", "year"), system=True)
Dynamic panel-data estimation, two-step system GMM (dpd_stream) vcov: robust (Windmeijer-corrected)
observations: 140,000 groups: 20,000 obs per group: min 7, avg 7.00, max 7 instruments: 16
...
Hansen test of overid. restrictions: chi2(6) = 2.60, p = 0.8576 (robust)
Difference-in-Hansen tests of exogeneity of instrument subsets:
...
GMM instruments for levels
excluding group: chi2(4) = 1.44, p = 0.8371 difference: chi2(2) = 1.15, p = 0.5613
Underidentification (Kleibergen-Paap rk LM, robust): chi2(7) = 12370.65, p = 0.0000 weak identification (KP rk Wald F): 4385.58
│ l(y, 1) ┆ 0.497039 ┆ 0.002883 ┆ ...
│ x ┆ 0.989955 ┆ 0.004088 ┆ ...
│ w ┆ 0.496112 ┆ 0.003444 ┆ ...
Reporting¶
Results have hdfe-stream’s reporting methods:
tidy(),confint(),wald_test(),summary_json();etable, through pyfixest;onestep, which every two-step fit carries: the one-step estimates, at no extra cost.
etable([diff.onestep, diff, sys_], keep=["l\\(y", "^x$", "^w$"])
sys_.long_run("x") # beta / (1 - rho), delta-method SE
{'variable': 'x', 'estimate': 1.968, 'se': 0.0157, 'z': 125.4, 'p': 0.0,
'terms': ['x'], 'ldv_terms': ['l(y, 1)']}
The script
"""Quickstart: difference and system GMM on a simulated dynamic panel.
python examples/quickstart.py
The panel is y_it = 0.5 y_i,t-1 + x_it + 0.5 w_it + alpha_i + lambda_t + e_it,
with x predetermined (it responds to past shocks) and w strictly exogenous
but correlated with the fixed effect. OLS overstates the lagged dependent
variable's coefficient and within-groups understates it; GMM recovers it.
"""
from pathlib import Path
from dpd_stream import dpd_stream, etable
from dpd_stream.simulate import simulate_dynamic, truth_dynamic
OUT = Path(__file__).resolve().parent / "output"
OUT.mkdir(exist_ok=True)
df = simulate_dynamic(20_000, 8, rho=(0.5,), seed=1)
df.write_parquet(OUT / "panel.parquet")
print(df.head(3))
print("truth:", truth_dynamic(rho=(0.5,)))
# ---- difference GMM (Arellano-Bond): two-step, Windmeijer-corrected SEs.
# The year effects are absorbed; y's lags 2-4 instrument the differenced
# equation, x's lags 1-3 (x is predetermined), w is its own instrument.
diff = dpd_stream("y ~ l(y, 1) + x + w | year | gmm(y, 2:4) + gmm(x, 1:3) + iv(w)",
OUT / "panel.parquet", panel=("id", "year"), verbose=False)
diff.summary()
# ---- system GMM (Blundell-Bond): the levels equation is added, instrumented
# by differences. w is correlated with the fixed effect, so it instruments
# the differenced equation only; the year effects become dummies.
sys_ = dpd_stream("y ~ l(y, 1) + x + w | year | gmm(y, 2:4, collapse=True) "
"+ gmm(x, 1:3, collapse=True) + iv(w, eq='diff')",
OUT / "panel.parquet", panel=("id", "year"), system=True, verbose=False)
sys_.summary()
# ---- one table; the one-step estimates come with every two-step fit
print(etable([diff.onestep, diff, sys_], keep=["l\\(y", "^x$", "^w$"], type="df"))
print("long-run effect of x:", sys_.long_run("x"))