Skip to content
Merged
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
169 changes: 128 additions & 41 deletions linearmodels/iv/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,58 @@ def _j_statistic(
null = "Expected moment conditions are equal to 0"
return WaldTestStatistic(stat, null, ninstr - nvar)

def _raise_if_singular(
self,
moment_cov: linearmodels.typing.data.Float64Array,
consequence: str,
) -> None:
"""
Raise a ValueError if the covariance of the moment conditions is singular

Parameters
----------
moment_cov : ndarray
Estimate of the covariance of the moment conditions (ninstr by
ninstr).
consequence : str
Sentence that says why the estimator needs the inverse of the
covariance. It ends with the period.

Raises
------
ValueError
If the covariance is singular. The message says that there are too
few clusters or observations and how many are needed.
"""
if not _is_singular(moment_cov):
return
nobs, nmoment = self._wz.shape
if isinstance(self._weight, OneWayClusteredWeightMatrix):
nclusters = len(unique(asarray(self._weight_config["clusters"])))
centered = self._weight_config["center"]
rank = nclusters - 1 if centered else nclusters
rank_reason = (
"one less than the number of clusters, since the cluster sums of "
"centered moment conditions add to zero"
if centered
else "the number of clusters"
)
msg = (
f"The clustered covariance of the {nmoment} moment conditions is "
f"singular because there are too few clusters ({nclusters} "
f"clusters). Its rank is at most {rank}, {rank_reason}. "
f"{consequence} Use more clusters than moment conditions, or a "
"weight_type that does not use clusters."
)
else:
msg = (
f"The covariance of the {nmoment} moment conditions is singular, "
f"which occurs if there are too few observations ({nobs}) for the "
f"number of moment conditions. {consequence} Use more "
"observations or fewer moment conditions."
)
raise ValueError(msg)


class IVGMM(_IVGMMBase):
r"""
Expand Down Expand Up @@ -1251,6 +1303,20 @@ class IVGMM(_IVGMMBase):
paired by position. The same rule applies to ``clusters`` passed to
``fit`` or in ``weight_config``.

The weight matrix is the inverse of an estimate of the covariance of the
moment conditions, so that the estimate has to be nonsingular. A clustered
estimate from G clusters has rank at most G, and at most G - 1 if the
moment conditions are centered (``center`` is ``True``), since the
centered cluster sums add to zero. There have to be at least as many
clusters as moment conditions, and more if they are centered. The first
step moment conditions of an exactly identified model add to zero as well,
so that it needs more clusters than moment conditions with every value of
``center``. The rank of a kernel or robust estimate is bounded in the same
way by the number of observations. ``fit`` raises a ``ValueError`` that
says so if the estimate is singular, since the estimates would be
numerical noise. The estimate is not used, and so it is not tested, if
``iter_limit`` is 1.

.. todo::

* VCV: bootstrap
Expand Down Expand Up @@ -1425,6 +1491,16 @@ def fit(
IVGMMResults
Results container

Raises
------
ValueError
If the estimate of the covariance of the moment conditions is
singular, which occurs if there are too few clusters or
observations. A clustered estimate needs at least as many clusters
as moment conditions, and more if the moment conditions are
centered or the model is exactly identified. The estimate is not
used if ``iter_limit`` is 1.

See also
--------
linearmodels.iv.gmm.IVGMMCovariance
Expand All @@ -1448,6 +1524,11 @@ def fit(
while iters < iter_limit and norm > tol:
eps = wy - wx @ params
moment_cov = weight_matrix(wx, wz, eps)
self._raise_if_singular(
moment_cov,
"The weight matrix of the GMM estimator is the inverse of the "
"covariance, so it is not defined.",
)
wmat = inv(moment_cov)
params = self.estimate_parameters(wx, wy, wz, wmat)
delta = params - _params
Expand Down Expand Up @@ -1532,7 +1613,12 @@ class IVGMMCUE(_IVGMMBase):
The covariance of the moment conditions has to be nonsingular. A clustered
covariance that is estimated from G clusters has rank at most G, and at
most G - 1 if the moment conditions are centered, so that there have to be
more clusters than moment conditions if ``center`` is ``True``.
more clusters than moment conditions if ``center`` is ``True``. If
``center`` is ``False`` and there are as many clusters as moment
conditions, the covariance is nonsingular but the objective function is
the same for every value of the parameters, so that there is nothing to
optimize. There have to be more clusters than moment conditions whether or
not the moment conditions are centered.

.. math::

Expand Down Expand Up @@ -1770,47 +1856,47 @@ def _callback(params: ndarray[tuple[int], dtype[float64]]) -> None:

def _check_moment_covariance(self) -> None:
"""
Raise a ValueError if the covariance of the moment conditions is singular

The objective function uses the inverse of the covariance, so it is not
defined if the covariance is singular. The rank of the covariance does
not depend on the parameters, other than for special values such as
residuals that are all 0, and so it is tested at the first step
estimates of two-step GMM.
Raise a ValueError if the objective function is not defined or constant

The objective function uses the inverse of the covariance of the moment
conditions, so it is not defined if the covariance is singular. The rank
of the covariance does not depend on the parameters, other than for
special values such as residuals that are all 0, and so it is tested at
the first step estimates of two-step GMM.

If the covariance is clustered, the moment conditions are not centered
and there are as many clusters as moment conditions, the covariance is
not singular but the objective function is the same for all values of
the parameters. The cluster sums of the moment conditions are the rows
of a square matrix, Xi, the average of the moment conditions is
Xi'1 / n, and the covariance is Xi'Xi / n, so that the objective function
is 1'Xi(Xi'Xi)^{-1}Xi'1, which is the number of clusters whenever Xi is
nonsingular (it is divided by the small sample adjustment if debiased).
"""
wy, wx, wz = self._wy, self._wx, self._wz
nobs, nmoment = wz.shape
params = IVGMM.estimate_parameters(wx, wy, wz, inv(wz.T @ wz / nobs))
if not _is_singular(self._weight.weight_matrix(wx, wz, wy - wx @ params)):
self._raise_if_singular(
self._weight.weight_matrix(wx, wz, wy - wx @ params),
"The objective function of the continuously updating GMM estimator "
"uses the inverse of the covariance, so it is not defined.",
)
if not isinstance(self._weight, OneWayClusteredWeightMatrix):
return
if isinstance(self._weight, OneWayClusteredWeightMatrix):
nclusters = len(unique(asarray(self._weight_config["clusters"])))
centered = self._weight_config["center"]
rank = nclusters - 1 if centered else nclusters
rank_reason = (
"one less than the number of clusters, since the cluster sums of "
"centered moment conditions add to zero"
if centered
else "the number of clusters"
)
msg = (
f"The clustered covariance of the {nmoment} moment conditions is "
f"singular because there are too few clusters ({nclusters} "
f"clusters). Its rank is at most {rank}, {rank_reason}. The "
"objective function of the continuously updating GMM estimator "
"uses the inverse of the covariance, so it is not defined. Use "
"more clusters than moment conditions, or a weight_type that does "
"not use clusters."
)
else:
msg = (
f"The covariance of the {nmoment} moment conditions is singular, "
f"which occurs if there are too few observations ({nobs}) for the "
"number of moment conditions. The objective function of the "
"continuously updating GMM estimator uses the inverse of the "
"covariance, so it is not defined. Use more observations or fewer "
"moment conditions."
)
nclusters = len(unique(asarray(self._weight_config["clusters"])))
# Fewer clusters, or centered moment conditions, give a singular covariance
if nclusters > nmoment:
return
msg = (
f"The clustered covariance of the {nmoment} moment conditions is "
f"estimated from as many clusters ({nclusters} clusters) as there "
"are moment conditions, and the moment conditions are not centered. "
"The objective function of the continuously updating GMM estimator "
"is then the same for every value of the parameters, so that there is "
"nothing to optimize and the estimates would be the starting values. "
"Use more clusters than moment conditions, or a weight_type that "
"does not use clusters."
)
raise ValueError(msg)

def fit(
Expand Down Expand Up @@ -1859,10 +1945,11 @@ def fit(
------
ValueError
If the covariance of the moment conditions is singular, which
occurs if there are too few clusters or observations. The
number of clusters has to be larger than the number of moment
conditions if the moment conditions are centered, which is the
default.
occurs if there are too few clusters or observations, or if the
moment conditions are not centered and there are as many clusters
as moment conditions, so that the objective function is constant.
The number of clusters has to be larger than the number of moment
conditions.

Notes
-----
Expand Down
17 changes: 14 additions & 3 deletions linearmodels/iv/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -1671,7 +1671,9 @@ def j_stat(self) -> InvalidTestStatistic | WaldTestStatistic:
J statistic test of overidentifying restrictions.
An InvalidTestStatistic is returned if the estimate of the
covariance of the moment conditions that was inverted to compute
the weight matrix is singular, which can happen if there are too
the weight matrix is singular. This can only happen at the final
estimates of IVGMMCUE, since IVGMM and IVGMMCUE raise a
ValueError if it is singular, which is the case if there are too
few clusters.

Notes
Expand Down Expand Up @@ -1700,6 +1702,13 @@ def j_stat(self) -> InvalidTestStatistic | WaldTestStatistic:
same way by the number of observations. Whether the estimate is
singular does not depend on the units in which the variables are
measured.

``IVGMM.fit`` raises a ``ValueError`` if the estimate is singular in
any step, and ``IVGMMCUE.fit`` raises one if it is singular at the
first step estimates, since the weight matrix and the objective
function are not defined. The statistic is invalid only if the
estimate of ``IVGMMCUE`` is singular at its final estimates and not at
the first step estimates.
"""
return self._j_stat

Expand Down Expand Up @@ -1895,8 +1904,10 @@ def c_stat(
)
try:
res_e = mod.fit(cov_type=self.cov_type, **self.cov_config)
except LinAlgError:
# The covariance of the moment conditions is exactly singular
except (LinAlgError, ValueError):
# The covariance of the moment conditions is singular: fit raises
# a ValueError, and NumPy raises a LinAlgError if it is exactly
# singular. LinAlgError is not a ValueError in NumPy 1.x
return InvalidTestStatistic(reason, name="C-statistic")
assert isinstance(res_e, IVGMMResults)
if isinstance(res_e.j_stat, InvalidTestStatistic):
Expand Down
Loading
Loading