Standard errors

Download the script: inference.py

Why the two-step errors need a correction. Two-step GMM is asymptotically efficient, but its conventional standard errors are badly biased downward in finite samples. Windmeijer (2005) derived a correction for the uncertainty in the first-step estimate that the weight matrix uses. It is the default, vcov="robust": clustered by panel, and Windmeijer-corrected for two-step. with_vcov("iid") gives the uncorrected two-step errors, and every two-step fit carries its one-step results.

two = dpd_stream(F, df, panel=("id", "year"))
two.se                    # Windmeijer-corrected
two.with_vcov("iid").se   # uncorrected
two.onestep.se            # one-step, robust
two-step, Windmeijer-corrected: [0.0095 0.0145 0.0095]
two-step, uncorrected:         [0.0092 0.014  0.0093]
one-step, robust:              [0.0093 0.0143 0.0095]

Clustering

vcov={"CRV1": "cluster"} clusters by any variable. As with xtabond2’s cluster(), the clusters also define the two-step weight matrix, so they change the two-step estimates, not only the standard errors.

Several variables, as in {"CRV1": "firm+year"}, cluster by every combination with inclusion and exclusion (Cameron, Gelbach and Miller 2011).

clustered by cluster: [0.0123 0.0193 0.0084] {'cluster': 60}
one-step, two-way (cluster, year): [0.0332 0.0469 0.0117] (l(y, 1), x, w)

Two-way clustering with few clusters. With few clusters in one dimension (years, typically), multi-way clustering is fragile, in xtabond2 as here:

  • Its part of the moment covariance has the rank of its cluster count. So a two-step weight matrix built on it is singular, and the estimates unreliable.

  • The inclusion–exclusion variance can be negative for coefficients that vary only in that dimension, here the (absorbed) year effects.

Clustering a one-step fit avoids the first problem.

Small-sample statistics

small=True gives t and F statistics, with xtabond2’s degrees of freedom and small-sample factors:

│ l(y, 1)     ┆ 0.49296   ┆ 0.009475   ┆ 52.027339 ┆ 0.0         │
...
{'statistic': 3315.6, 'pvalue': 0.0, 'df1': 2, 'df2': 3000, 'distribution': 'F'}
The script
"""Standard errors: one-step and two-step, Windmeijer's correction,
clustering (one- and two-way), small-sample statistics.

    python examples/inference.py
"""

from dpd_stream import dpd_stream
from dpd_stream.simulate import simulate_dynamic

df = simulate_dynamic(3_000, 8, rho=(0.5,), n_clusters=60, heteroskedastic=True, seed=4)
P = ("id", "year")
F = "y ~ l(y, 1) + x + w | year | gmm(y, 2:4) + gmm(x, 1:3) + iv(w)"

two = dpd_stream(F, df, panel=P, verbose=False)
print("two-step, Windmeijer-corrected:", two.se.round(4))
print("two-step, uncorrected:        ", two.with_vcov("iid").se.round(4))
print("one-step, robust:             ", two.onestep.se.round(4))

# clustering defines the two-step weight matrix too (as xtabond2's cluster())
cl = dpd_stream(F, df, panel=P, vcov={"CRV1": "cluster"}, verbose=False)
print("clustered by cluster:", cl.se.round(4), cl.n_clusters)
# two-way clustering: with few clusters in one dimension (8 years here) the
# two-step weight matrix it defines is singular, so cluster a one-step fit,
# whose weight does not depend on it
tw = dpd_stream(F, df, panel=P, steps=1, vcov={"CRV1": "cluster+year"}, verbose=False)
print("one-step, two-way (cluster, year):", tw.se[:3].round(4), "(l(y, 1), x, w)")

sm = dpd_stream(F, df, panel=P, small=True, verbose=False)
print(sm.tidy())
print(sm.wald_test(keep="x|w"))