Causal Inference V(b) — Dynamic Panels¶
Lagged dependent variables, Nickell bias, and GMM — on Arellano & Bond's own employment data¶
When last period's outcome drives this period's — employment adjusts slowly, debt begets debt, health persists — the panel model gains a lagged dependent variable: $$y_{it}=\rho\,y_{i,t-1}+\beta'X_{it}+\alpha_i+\varepsilon_{it}.$$ This one term breaks the standard panel estimators. Pooled OLS leaves the firm effect $\alpha_i$ in the error, which correlates with $y_{i,t-1}$, biasing $\hat\rho$ upward. The within (fixed-effects) transformation removes $\alpha_i$ but induces a mechanical correlation between the demeaned lag and the demeaned error — the Nickell bias, which is $O(1/T)$ and pushes $\hat\rho$ downward in short panels. So the two familiar workhorses bracket the truth from opposite sides, and neither is consistent.
The fix is Arellano & Bond's (1991) first-difference GMM: difference away $\alpha_i$, then instrument the differenced lag with lagged levels. When the series is persistent those instruments weaken and difference GMM is biased downward, motivating Blundell & Bond's (1998) system GMM, which adds lagged-difference instruments for the levels equation.
We use Arellano & Bond's original dataset — a panel of 140 UK firms, 1976–1984 (unbalanced, 7–9 years each) — modeling log employment. With real data there is no known $\rho$, so we use Bond's (2002) diagnostic bracket: a consistent estimate must lie between upward-biased pooled OLS and downward-biased FE. Python leads (from-scratch OLS/FE/Anderson-Hsiao/Arellano-Bond/system GMM for an unbalanced panel); the R companion uses plm::pgmm, whose documentation example is this dataset. Cross-links the panel/fixed-effects notebook (5a) and the weak-instrument theme of IV (subsection 3).
1. The lagged dependent variable and the OLS–FE bracket¶
Employment is sticky: a firm's headcount this year is largely last year's. We fit the dynamic equation $$\log\text{emp}_{it}=\rho\log\text{emp}_{i,t-1}+\beta_1\log\text{wage}_{it}+\beta_2\log\text{capital}_{it}+\beta_3\log\text{output}_{it}+\alpha_i+\varepsilon_{it}$$ by pooled OLS and by fixed effects. Pooled OLS ignores $\alpha_i$ and over-states persistence; FE over-corrects via the Nickell bias. Bond's rule of thumb: the consistent estimate of $\rho$ lives between them — here between roughly 0.51 and 0.93. That interval is our real-data stand-in for the (unknown) truth.
import numpy as np, pandas as pd, matplotlib.pyplot as plt, statsmodels.formula.api as smf, warnings
warnings.filterwarnings("ignore")
BLUE="#2b6cb0"; RED="#c53030"; GREEN="#2f855a"; ORANGE="#dd6b20"; GREY="#a0aec0"; PURP="#6b46c1"
d=pd.read_csv("emplUK.csv").sort_values(["firm","year"]).reset_index(drop=True)
for c in ["emp","wage","capital","output"]: d["l"+c]=np.log(d[c])
d["lemp_l1"]=d.groupby("firm")["lemp"].shift(1)
print(f"Arellano-Bond employment panel: {d.firm.nunique()} UK firms, {d.year.min()}-{d.year.max()}, {len(d)} firm-years (unbalanced, {d.groupby('firm').size().min()}-{d.groupby('firm').size().max()} yrs each)")
dd=d.dropna(subset=["lemp_l1"]).copy()
ols=smf.ols("lemp~lemp_l1+lwage+lcapital+loutput", dd).fit()
for c in ["lemp","lemp_l1","lwage","lcapital","loutput"]: dd[c+"_w"]=dd[c]-dd.groupby("firm")[c].transform("mean")
fe=smf.ols("lemp_w~lemp_l1_w+lwage_w+lcapital_w+loutput_w-1", dd).fit()
rho_ols=ols.params['lemp_l1']; rho_fe=fe.params['lemp_l1_w']
print(f"\n persistence rho (coef on lagged log-employment):")
print(f" pooled OLS = {rho_ols:.3f} biased UP (firm effect left in error correlates with the lag)")
print(f" FE within = {rho_fe:.3f} biased DOWN (Nickell bias, O(1/T); here T is only ~7-9)")
print(f" => Bond bracket: a consistent estimate should lie in [{rho_fe:.2f}, {rho_ols:.2f}]")
fig,ax=plt.subplots(1,2,figsize=(13,4.4))
ax[0].bar(["pooled OLS\n(up)","FE within\n(down)"],[rho_ols,rho_fe],color=[RED,ORANGE])
ax[0].axhspan(rho_fe,rho_ols,color=GREEN,alpha=.12); ax[0].set_ylabel("persistence rho"); ax[0].set_title("The two workhorses bracket the truth")
for i,v in enumerate([rho_ols,rho_fe]): ax[0].text(i,v+0.02,f"{v:.2f}",ha="center")
ex=d[d.firm.isin(d.firm.unique()[:6])]
for f,g in ex.groupby("firm"): ax[1].plot(g.year,g.lemp,marker="o",ms=3,alpha=.7)
ax[1].set_xlabel("year"); ax[1].set_ylabel("log employment"); ax[1].set_title("Employment is persistent within firms (6 example firms)")
plt.tight_layout(); plt.show()
print("Persistence is exactly what makes the lag matter -- and what will later weaken the difference-GMM instruments.")
Arellano-Bond employment panel: 140 UK firms, 1976-1984, 1031 firm-years (unbalanced, 7-9 yrs each)
persistence rho (coef on lagged log-employment):
pooled OLS = 0.932 biased UP (firm effect left in error correlates with the lag)
FE within = 0.514 biased DOWN (Nickell bias, O(1/T); here T is only ~7-9)
=> Bond bracket: a consistent estimate should lie in [0.51, 0.93]
Persistence is exactly what makes the lag matter -- and what will later weaken the difference-GMM instruments.
2. First-difference the fixed effect away, then instrument with lagged levels¶
First-differencing removes $\alpha_i$: $$\Delta y_{it}=\rho\,\Delta y_{i,t-1}+\beta'\Delta X_{it}+\Delta\varepsilon_{it}.$$ Now $\Delta y_{i,t-1}=y_{i,t-1}-y_{i,t-2}$ is correlated with $\Delta\varepsilon_{it}=\varepsilon_{it}-\varepsilon_{i,t-1}$ (both contain $\varepsilon_{i,t-1}$), so OLS on differences is still biased — but lagged levels $y_{i,t-2},y_{i,t-3},\dots$ are valid instruments (correlated with $\Delta y_{i,t-1}$, uncorrelated with $\Delta\varepsilon_{it}$ under no serial correlation).
- Anderson–Hsiao: use a single instrument ($y_{i,t-2}$) in 2SLS — consistent but inefficient.
- Arellano–Bond difference GMM: use all available lagged levels at each period as instruments, stacked in a firm-specific matrix and weighted by the first-difference (MA(1)) structure — consistent and more efficient.
Both are built from scratch for this unbalanced panel. Anderson–Hsiao lands inside the Bond bracket; but Arellano–Bond falls below the FE estimate — a red flag we resolve in the next section.
firms=[g for _,g in d.groupby("firm")]
def fa(g):
g=g.sort_values("year"); return g["lemp"].values,g["lwage"].values,g["lcapital"].values,g["loutput"].values
maxT=max(len(fa(g)[0]) for g in firms)
def anderson_hsiao(firms): # 2SLS: dY_t ~ [dY_{t-1},dX], instrument dY_{t-1} by level y_{t-2}
Z,X,Y=[],[],[]
for g in firms:
n,w,k,ys=fa(g); T=len(n)
for t in range(2,T):
dw,dk,dys=w[t]-w[t-1],k[t]-k[t-1],ys[t]-ys[t-1]
Z.append([n[t-2],dw,dk,dys]); X.append([n[t-1]-n[t-2],dw,dk,dys]); Y.append(n[t]-n[t-1])
Z,X,Y=np.array(Z),np.array(X),np.array(Y); Pz=Z@np.linalg.pinv(Z.T@Z)@Z.T
return (np.linalg.pinv(X.T@Pz@X)@(X.T@Pz@Y))[0]
def arellano_bond(firms,maxT): # one-step difference GMM, all lagged levels as instruments
per=list(range(2,maxT)); m=len(per); lag_cols=sum(t-1 for t in per); ncol=lag_cols+3
H=2*np.eye(m)-np.eye(m,k=1)-np.eye(m,k=-1); cs={}; c=0
for t in per: cs[t]=c; c+=t-1
SZZ=np.zeros((ncol,ncol)); SZX=np.zeros((ncol,4)); SZY=np.zeros(ncol)
for g in firms:
n,w,k,ys=fa(g); T=len(n); Zi=np.zeros((m,ncol)); Xi=np.zeros((m,4)); Yi=np.zeros(m)
for r,t in enumerate(per):
if t>=T: continue
lags=n[0:t-1]; Zi[r,cs[t]:cs[t]+len(lags)]=lags
dw,dk,dys=w[t]-w[t-1],k[t]-k[t-1],ys[t]-ys[t-1]; Zi[r,lag_cols:]=[dw,dk,dys]
Xi[r]=[n[t-1]-n[t-2],dw,dk,dys]; Yi[r]=n[t]-n[t-1]
SZZ+=Zi.T@H@Zi; SZX+=Zi.T@Xi; SZY+=Zi.T@Yi
W=np.linalg.pinv(SZZ); return (np.linalg.solve(SZX.T@W@SZX, SZX.T@W@SZY))[0]
rho_ah=anderson_hsiao(firms); rho_ab=arellano_bond(firms,maxT)
print(f"persistence rho, four ways:")
print(f" pooled OLS = {rho_ols:.3f} (up)")
print(f" FE within = {rho_fe:.3f} (down, Nickell)")
print(f" Anderson-Hsiao IV = {rho_ah:.3f} (consistent, single lag instrument -- inside the bracket)")
print(f" Arellano-Bond diff-GMM = {rho_ab:.3f} (all lags; falls BELOW FE -- a warning sign)")
fig,ax=plt.subplots(figsize=(8.7,4.2))
nm=["pooled\nOLS","FE\n(Nickell)","Anderson-\nHsiao","Arellano-Bond\ndiff-GMM"]; vals=[rho_ols,rho_fe,rho_ah,rho_ab]
ax.bar(nm,vals,color=[RED,ORANGE,BLUE,PURP]); ax.axhspan(rho_fe,rho_ols,color=GREEN,alpha=.12,label="Bond bracket")
for i,v in enumerate(vals): ax.text(i,v+0.015,f"{v:.2f}",ha="center")
ax.set_ylabel("persistence rho"); ax.set_title("Anderson-Hsiao lands in the bracket; diff-GMM drops below FE"); ax.legend(fontsize=8)
plt.tight_layout(); plt.show()
print("Difference GMM sitting below the downward-biased FE estimate is the classic symptom of WEAK instruments on a persistent")
print("series: lagged levels barely move the differenced lag. The fix is system GMM.")
persistence rho, four ways: pooled OLS = 0.932 (up) FE within = 0.514 (down, Nickell) Anderson-Hsiao IV = 0.584 (consistent, single lag instrument -- inside the bracket) Arellano-Bond diff-GMM = 0.336 (all lags; falls BELOW FE -- a warning sign)
Difference GMM sitting below the downward-biased FE estimate is the classic symptom of WEAK instruments on a persistent series: lagged levels barely move the differenced lag. The fix is system GMM.
3. Persistence, weak instruments, and system GMM¶
When $\rho$ is near 1 the series is close to a random walk, and lagged levels become weak instruments for the differences ($\Delta y_{i,t-1}$ barely depends on distant $y_{i,t-2}$). Difference GMM then inherits a downward bias — exactly why our estimate fell below FE. Blundell & Bond (1998) system GMM augments the moment set: it stacks the differenced equation (levels instrumenting differences) with the levels equation (lagged differences $\Delta y_{i,t-1}$ instrumenting levels), valid under a mild stationarity condition on the initial observations. The extra moments are informative even when $\rho\to1$, restoring the estimate to inside the Bond bracket.
We build one-step system GMM from scratch (a levels intercept is included, as system GMM requires) and report the standard diagnostics: the AR(2) test for second-order serial correlation in the differenced residuals (should not reject — first-order is expected, second-order would invalidate the lagged-level instruments), the instrument count, and the Sargan over-identification test, which is computed in the R companion.
The last of those is the one worth bracing for. AR(2) passes comfortably, but Sargan rejects at $p=0.003$ — and because the specification requests every available lag, the instrument set is large enough that Sargan is known to over-reject. The test therefore fails and cannot be trusted to have failed for the right reason, which is why the instrument count belongs in the output beside it.
def system_gmm(firms,maxT):
dper=list(range(2,maxT)); lper=list(range(2,maxT))
dcols=sum(t-1 for t in dper)+3; lcols=len(lper)+3+1; ncol=dcols+lcols
globals()["N_INSTR"]=ncol # exposed so the diagnostics below can report it
md_=len(dper); ml=len(lper); m=md_+ml
H=np.zeros((m,m)); H[:md_,:md_]=2*np.eye(md_)-np.eye(md_,k=1)-np.eye(md_,k=-1); H[md_:,md_:]=np.eye(ml)
cs={}; c=0
for t in dper: cs[t]=c; c+=t-1
P=5; SZZ=np.zeros((ncol,ncol)); SZX=np.zeros((ncol,P)); SZY=np.zeros(ncol)
for g in firms:
n,w,k,ys=fa(g); T=len(n); Zi=np.zeros((m,ncol)); Xi=np.zeros((m,P)); Yi=np.zeros(m)
for r,t in enumerate(dper): # differenced equations
if t>=T: continue
lags=n[0:t-1]; Zi[r,cs[t]:cs[t]+len(lags)]=lags
dw,dk,dys=w[t]-w[t-1],k[t]-k[t-1],ys[t]-ys[t-1]; Zi[r,dcols-3:dcols]=[dw,dk,dys]
Xi[r]=[n[t-1]-n[t-2],dw,dk,dys,0.0]; Yi[r]=n[t]-n[t-1]
for r,t in enumerate(lper): # levels equations (BB instrument = dy_{t-1})
row=md_+r
if t>=T: continue
Zi[row,dcols+r]=n[t-1]-n[t-2]; Zi[row,dcols+ml:dcols+ml+3]=[w[t],k[t],ys[t]]; Zi[row,ncol-1]=1.0
Xi[row]=[n[t-1],w[t],k[t],ys[t],1.0]; Yi[row]=n[t]
SZZ+=Zi.T@H@Zi; SZX+=Zi.T@Xi; SZY+=Zi.T@Yi
W=np.linalg.pinv(SZZ); return np.linalg.solve(SZX.T@W@SZX, SZX.T@W@SZY)
theta=system_gmm(firms,maxT); rho_sys=theta[0]
# Arellano-Bond AR(2) test: second-order serial correlation in the differenced residuals
res={}
for g in firms:
n,w,k,ys=fa(g); T=len(n)
for t in range(2,T): res[(id(g),t)]=(n[t]-n[t-1]) - rho_ab*(n[t-1]-n[t-2])
pairs=[]
for g in firms:
n,w,k,ys=fa(g); T=len(n)
for t in range(4,T):
a=res.get((id(g),t)); b=res.get((id(g),t-2))
if a is not None and b is not None: pairs.append((a,b))
pa=np.array(pairs); r2=np.corrcoef(pa[:,0],pa[:,1])[0,1]; z2=r2*np.sqrt(len(pa)); from scipy.stats import norm
print(f"persistence rho:")
print(f" Arellano-Bond diff-GMM = {rho_ab:.3f} (downward biased by persistence)")
print(f" system GMM (Blundell-Bond) = {rho_sys:.3f} <-- restored INTO the Bond bracket [{rho_fe:.2f},{rho_ols:.2f}]")
print(f" AR(2) test on differenced residuals: z={z2:.2f}, p={2*(1-norm.cdf(abs(z2))):.3f} (no 2nd-order corr -> lagged-level instruments valid)")
fig,ax=plt.subplots(figsize=(8.7,4.2))
nm=["pooled\nOLS","FE","Anderson-\nHsiao","diff-GMM","system\nGMM"]; vals=[rho_ols,rho_fe,rho_ah,rho_ab,rho_sys]
ax.bar(nm,vals,color=[RED,ORANGE,BLUE,PURP,GREEN]); ax.axhspan(rho_fe,rho_ols,color=GREEN,alpha=.12,label="Bond bracket")
for i,v in enumerate(vals): ax.text(i,v+0.015,f"{v:.2f}",ha="center")
ax.set_ylabel("persistence rho"); ax.set_title("System GMM restores a sensible, in-bracket persistence estimate"); ax.legend(fontsize=8)
plt.tight_layout(); plt.show()
print(f" instruments used by the system-GMM moment set: {N_INSTR} (against {d.firm.nunique()} firms)")
print()
print("Now the diagnostic this notebook told you to always report, and which it is easy to skip because")
print("it does not cooperate. The R companion runs the same one-step system GMM through plm::pgmm and")
print("gets rho = 0.658 against 0.64 here, an AR(2) p of 0.670 -- and a SARGAN OVER-IDENTIFICATION TEST")
print("THAT REJECTS AT p = 0.003.")
print()
print("Two readings, and the honest position is that neither rescues the other. Taken at face value the")
print("rejection says the moment conditions are not all valid, which would undercut the estimate this")
print("section just restored. But Sargan is known to over-reject badly when instruments proliferate, and")
print(f"the R specification asks for every available lag (2:99) while this one uses {N_INSTR} moment columns --")
print("so the test is simultaneously failing AND unreliable, which is the least useful combination.")
print()
print("What that leaves is a weaker claim than 'system GMM fixes it': the estimate is back inside the Bond")
print("bracket and the AR(2) test is clean, so the lagged-level instruments are not obviously invalid --")
print("but the over-identifying restrictions are not confirmed, and with this many instruments they")
print("cannot be. The standard remedy is to collapse the instrument set or cap the lag depth and check")
print("that rho survives; that is the honest next step rather than a footnote.")
print()
print("System GMM's lagged-difference instruments stay informative under persistence, pulling the estimate back into [FE, OLS].")
persistence rho: Arellano-Bond diff-GMM = 0.336 (downward biased by persistence) system GMM (Blundell-Bond) = 0.635 <-- restored INTO the Bond bracket [0.51,0.93] AR(2) test on differenced residuals: z=-0.49, p=0.623 (no 2nd-order corr -> lagged-level instruments valid)
instruments used by the system-GMM moment set: 42 (against 140 firms) Now the diagnostic this notebook told you to always report, and which it is easy to skip because it does not cooperate. The R companion runs the same one-step system GMM through plm::pgmm and gets rho = 0.658 against 0.64 here, an AR(2) p of 0.670 -- and a SARGAN OVER-IDENTIFICATION TEST THAT REJECTS AT p = 0.003. Two readings, and the honest position is that neither rescues the other. Taken at face value the rejection says the moment conditions are not all valid, which would undercut the estimate this section just restored. But Sargan is known to over-reject badly when instruments proliferate, and the R specification asks for every available lag (2:99) while this one uses 42 moment columns -- so the test is simultaneously failing AND unreliable, which is the least useful combination. What that leaves is a weaker claim than 'system GMM fixes it': the estimate is back inside the Bond bracket and the AR(2) test is clean, so the lagged-level instruments are not obviously invalid -- but the over-identifying restrictions are not confirmed, and with this many instruments they cannot be. The standard remedy is to collapse the instrument set or cap the lag depth and check that rho survives; that is the honest next step rather than a footnote. System GMM's lagged-difference instruments stay informative under persistence, pulling the estimate back into [FE, OLS].
4. Summary¶
A lagged dependent variable turns an ordinary panel into a dynamic one and defeats the standard estimators — on Arellano & Bond's own UK employment data:
- Pooled OLS over-stated persistence ($\hat\rho=0.93$) by leaving the firm effect in the error; fixed effects under-stated it ($\hat\rho=0.51$) via the $O(1/T)$ Nickell bias. With no known truth, the two form Bond's bracket — a consistent estimate must lie between them.
- Difference GMM (Arellano–Bond) first-differences $\alpha_i$ away and instruments the differenced lag with lagged levels; Anderson–Hsiao (single instrument, $\hat\rho=0.58$) landed inside the bracket, but the full difference-GMM estimate ($\hat\rho=0.34$) fell below FE — the signature of weak instruments when employment is persistent.
- System GMM (Blundell–Bond) added lagged-difference instruments for the levels equation, restoring $\hat\rho=0.64$ inside the bracket, and the from-scratch estimates matched
plm::pgmm(diff-GMM 0.34, system 0.66). The AR(2) test is clean ($p=0.62$ here, $0.67$ in R) — but the Sargan over-identification test rejects at $p=0.003$. That is not a clean bill of health, and it should not be reported as one: either some moment conditions are invalid, or the instrument set is large enough that Sargan over-rejects and cannot tell us. With every available lag requested, the second is likely and the first is not ruled out.
Guidance: for short, persistent dynamic panels use system GMM, not FE or difference GMM; always report the AR(2) test, the over-identification test, and the instrument count — and follow that guidance even when, as here, the over-identification test does not cooperate. Instrument proliferation weakens Sargan/Hansen and can over-fit the endogenous regressors, so the standard remedy is to collapse the instrument set or cap the lag depth and check that $\hat\rho$ survives. On this panel that check is the honest next step, not an appendix: the estimate is back inside the bracket and AR(2) is clean, but the over-identifying restrictions are unconfirmed. Cross-links: the weak-instrument pathology is the panel-data face of the IV weak-instrument problem (subsection 3); the within transformation and clustered inference come from the panel/fixed-effects notebook (5a); the many-moment GMM machinery connects to the orthogonality conditions behind Double ML (subsection 9). The R companion runs the canonical plm::pgmm difference and system GMM — this dataset is pgmm's own documentation example.