Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions linearmodels/iv/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,8 @@ def _gmm_post_estimation(
params: linearmodels.typing.data.Float64Array,
weight_mat: linearmodels.typing.data.Float64Array,
iters: int,
cov_type: str = "robust",
cov_config: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""GMM-specific post-estimation results"""
instr = self._instr_columns
Expand All @@ -1040,10 +1042,93 @@ def _gmm_post_estimation(
"weight_config": self._weight_type,
"iterations": iters,
"j_stat": self._j_statistic(params, weight_mat),
"robust_j_stat": self._hansen_lee_j_statistic(
params, cov_type, cov_config or {}
),
}

return gmm_specific

def _hansen_lee_j_statistic(
self,
params: linearmodels.typing.data.Float64Array,
cov_type: str,
cov_config: dict[str, Any],
) -> WaldTestStatistic:
r"""
Hansen-Lee (2021) misspecification-robust J-test of overidentifying
restrictions.

Unlike the standard J-test, which uses the uncentered moment covariance
as the weight matrix, this test uses the *centered* moment covariance

.. math::

\hat{S}_c = n^{-1}\sum_{i=1}^{n}
(g_i - \bar{g})(g_i - \bar{g})'

which consistently estimates :math:`\text{Var}(g_i)` even when the
model is misspecified (i.e. :math:`E[g_i] \neq 0`). The statistic is

.. math::

J^* = n\,\bar{g}'\hat{S}_c^{-1}\bar{g} \sim \chi^2_q

where :math:`q = n_{\text{instr}} - n_{\text{var}}`.

Parameters
----------
params : ndarray
Estimated model parameters.
cov_type : str
Covariance type used for the main estimation. The same type is
used to compute :math:`\hat{S}_c` (heteroskedastic, clustered,
kernel, or homoskedastic).
cov_config : dict
Configuration for the covariance estimator (e.g. ``clusters``,
``bandwidth``, ``kernel``).

Returns
-------
WaldTestStatistic

References
----------
Hansen, B. E. & Lee, S. (2021). Inference for iterated GMM under
misspecification. *Econometrica*, 89(3), 1419–1447.
"""
y, x, z = self._wy, self._wx, self._wz
nobs, nvar, ninstr = y.shape[0], x.shape[1], z.shape[1]
eps = y - x @ params
g_bar = (z * eps).mean(0)

debiased = bool(cov_config.get("debiased", False))
if cov_type in ("robust", "heteroskedastic"):
score_cov: HomoskedasticWeightMatrix = HeteroskedasticWeightMatrix(
center=True, debiased=debiased
)
elif cov_type in ("unadjusted", "homoskedastic"):
score_cov = HomoskedasticWeightMatrix(center=True, debiased=debiased)
elif cov_type == "clustered":
score_cov = OneWayClusteredWeightMatrix(
clusters=cov_config["clusters"], center=True, debiased=debiased
)
elif cov_type == "kernel":
score_cov = KernelWeightMatrix(
kernel=str(cov_config.get("kernel", "bartlett")),
bandwidth=cov_config.get("bandwidth", None),
center=True,
debiased=debiased,
)
else:
score_cov = HeteroskedasticWeightMatrix(center=True, debiased=debiased)

s_c = score_cov.weight_matrix(x, z, eps)
stat = float(nobs * g_bar @ pinv(s_c) @ g_bar)
null = "Expected moment conditions are equal to 0"
name = "Hansen-Lee misspecification-robust J-test"
return WaldTestStatistic(stat, null, ninstr - nvar, name=name)

def _j_statistic(
self,
params: linearmodels.typing.data.Float64Array,
Expand Down Expand Up @@ -1310,7 +1395,7 @@ def fit(
)

results = self._post_estimation(params, cov_estimator, cov_type)
gmm_pe = self._gmm_post_estimation(params, wmat, iters)
gmm_pe = self._gmm_post_estimation(params, wmat, iters, cov_type, cov_config)

results.update(gmm_pe)

Expand All @@ -1321,6 +1406,8 @@ def _gmm_post_estimation(
params: linearmodels.typing.data.Float64Array,
weight_mat: linearmodels.typing.data.Float64Array,
iters: int,
cov_type: str = "robust",
cov_config: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""GMM-specific post-estimation results"""
instr = self._instr_columns
Expand All @@ -1330,6 +1417,9 @@ def _gmm_post_estimation(
"weight_config": self._weight_type,
"iterations": iters,
"j_stat": self._j_statistic(params, weight_mat),
"robust_j_stat": self._hansen_lee_j_statistic(
params, cov_type, cov_config or {}
),
}

return gmm_specific
Expand Down Expand Up @@ -1673,7 +1763,7 @@ def fit(
wx, wy, wz, params, wmat, cov_type, **cov_config
)
results = self._post_estimation(params, cov_estimator, cov_type)
gmm_pe = self._gmm_post_estimation(params, wmat, iters)
gmm_pe = self._gmm_post_estimation(params, wmat, iters, cov_type, cov_config)
results.update(gmm_pe)

return IVGMMResults(results, self)
Expand Down
67 changes: 67 additions & 0 deletions linearmodels/iv/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,7 @@ def __init__(
self._weight_config = results["weight_config"]
self._iterations = results["iterations"]
self._j_stat = results["j_stat"]
self._robust_j_stat = results["robust_j_stat"]

@property
def weight_matrix(self) -> linearmodels.typing.data.Float64Array:
Expand Down Expand Up @@ -1460,6 +1461,72 @@ def j_stat(self) -> InvalidTestStatistic | WaldTestStatistic:
"""
return self._j_stat

@property
def robust_j_stat(self) -> WaldTestStatistic:
r"""
Hansen-Lee (2021) misspecification-robust J-test of overidentifying
restrictions

Returns
-------
WaldTestStatistic
Robust J statistic test of overidentifying restrictions

Notes
-----
Unlike the standard J-statistic, which uses the uncentered moment
covariance as the weighting matrix, this test uses the *centered*
moment covariance

.. math ::

\hat{S}_c = n^{-1}\sum_{i=1}^{n}(g_i - \bar{g})(g_i - \bar{g})'

which is consistent even when the model is misspecified
(:math:`E[g_i] \neq 0`). The statistic is

.. math ::

J^* = n\,\bar{g}'\hat{S}_c^{-1}\bar{g} \sim \chi^2_q

where :math:`q = n_{\text{instr}} - n_{\text{var}}` is the degree of
overidentification.

Under correct specification :math:`J^*` has the same :math:`\chi^2_q`
null distribution as the standard J-test. Under misspecification
:math:`J^*` diverges at rate :math:`n`, making it a consistent test
for model misspecification.

References
----------
Hansen, B. E. & Lee, S. (2021). Inference for iterated GMM under
misspecification. *Econometrica*, 89(3), 1419–1447.
"""
return self._robust_j_stat

def _top_right(self) -> list[tuple[str, str]]:
j = self.j_stat
rj = self.robust_j_stat
return [
("J-statistic:", _str(j.stat)),
("P-value (J-stat):", pval_format(j.pval)),
("Distribution (J-stat):", str(j.dist_name)),
("HL J-statistic:", _str(rj.stat)),
("P-value (HL J-stat):", pval_format(rj.pval)),
("Distribution (HL J-stat):", str(rj.dist_name)),
("Iterations:", str(self.iterations)),
]

def _update_extra_text(self, extra_text: list[str]) -> list[str]:
instruments = self.model.instruments
if instruments.shape[1] > 0:
endog = self.model.endog
extra_text.append("Endogenous: " + ", ".join(endog.cols))
extra_text.append("Instruments: " + ", ".join(instruments.cols))
cov_descr = str(self._cov_estimator)
extra_text.extend(list(cov_descr.split("\n")))
return extra_text

def c_stat(self, variables: list[str] | str | None = None) -> WaldTestStatistic:
r"""
C-test of endogeneity
Expand Down
Loading
Loading