Skip to content

BUG: Do not treat badly scaled moment covariances as singular in cent… - #754

Merged
bashtage merged 3 commits into
mainfrom
lm71-fix-gmm-singular
Oct 12, 2026
Merged

bashtage merged 3 commits into
mainfrom
lm71-fix-gmm-singular

Conversation

@bashtage

Copy link
Copy Markdown
Owner

…ered_j_stat

IVGMMResults.centered_j_stat returned an InvalidTestStatistic, saying that the
covariance of the moment conditions is singular, for models in which it is not.
It used matrix_rank on the covariance, and the tolerance of matrix_rank is
relative to the largest eigenvalue. When the variances of two moment conditions
differ by a factor of about 1e16, which is the case if two instruments are
measured in units that differ by a factor of 1e8, the smaller one is below the
tolerance and the covariance is reported as singular.

For a model with four instruments, one of which is multiplied by a constant,
the J statistic is 7.6196 at every scale. The centered statistic is 7.906 for
constants up to 1e7 and is invalid from 1e8 on, and the same for constants
down to 1e-8. This occurred for the unadjusted, robust, kernel and clustered
estimators, with and without weights, and when the scale was that of an
exogenous regressor, which is also an instrument.

The rank is now that of the correlation matrix of the moment conditions, which
does not depend on the units. A covariance whose variances are not positive or
that is not finite is also singular. The statistic is computed as before, and
all of the existing reference values are unchanged. A covariance that is
singular is still found, e.g., when there are too few clusters, with any scale.

The tests that are added check the definition of the statistic for IVGMM and
IVGMMCUE, without using the weight matrix classes, with weights, one-way
clusters, a kernel and the small sample adjustment. The reference values from R
do not cover continuously updating GMM with weights or a kernel, or weights
with clusters. They also check continuously updating GMM with and without
centering, and that the statistic is invalid when there are as many clusters as
moment conditions. 32 tests of the units of the data fail without the change.

@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.10298% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.58%. Comparing base (121d869) to head (f4f8a09).

Files with missing lines Patch % Lines
...earmodels/tests/iv/test_gmm_singular_covariance.py 98.47% 4 Missing and 1 partial ⚠️
linearmodels/iv/model.py 93.93% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #754      +/-   ##
==========================================
- Coverage   99.60%   99.58%   -0.02%     
==========================================
  Files         127      128       +1     
  Lines       23832    24188     +356     
  Branches     1783     1809      +26     
==========================================
+ Hits        23737    24087     +350     
- Misses         35       40       +5     
- Partials       60       61       +1     
Flag Coverage Δ
adder 99.56% <97.83%> (-0.03%) ⬇️
subtractor 99.56% <97.83%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…singular

Stacked on lm71-bug3b-iv-features (BUG3b), which has to be merged first. That
branch adds the scale-free rank check of the covariance of the moment
conditions to IVGMMResults.centered_j_stat. This commit moves the check to a
function and also applies it to j_stat and c_stat.

IVGMMResults.c_stat and IVGMMResults.j_stat gave meaningless numbers if the
estimate of the covariance of the moment conditions was singular, which is the
case if there are too few clusters. The weight matrix is the inverse of that
estimate, so it is numerical noise, and so are the statistics. c_stat uses the
weight type of the model since #744, and so it uses the clustered covariance.

Reproduction: n = 120, a constant and an exogenous regressor, one endogenous
regressor and two instruments, which are 4 moment conditions, and
clusters = arange(n) % G. The model is IVGMM(..., weight_type="clustered",
clusters=clusters).fit(cov_type="clustered", clusters=clusters).

    G   c_stat() before   c_stat() now
    3   -3.73e15          invalid
    4   -7.42e16          invalid
    5   0.8760            0.8760

The model of the C statistic treats the endogenous regressor as exogenous, so
it has 5 moment conditions, and its covariance is singular unless there are at
least 5 clusters. 7.0 gave -0.049 for every G, since it ignored the clusters,
which is not valid either. c_stat also raised a LinAlgError when NumPy found
that the covariance is exactly singular, e.g., with 2 centered clusters.

j_stat is affected in the same way, in 7.0 and in main. J of the same model,
for the default of IVGMM (center=False) and for center=True, which is the
default of IVGMMCUE:

    G   center=False   center=True   now
    2   -3.19e15       1.41          invalid, invalid
    3   -6.87e15       1.73e14       invalid, invalid
    4   0.764          1.52e15       valid, invalid
    5   0.285          0.303         valid, valid

A covariance that is estimated from G clusters is a sum of G outer products,
so its rank is at most G. The centered cluster sums add to zero, so the rank
is at most G - 1 if the moment conditions are centered. With center=True and
G = 2 the statistic looks plausible, 1.41, but it is as meaningless as the
others. The rank of a kernel or a robust covariance is bounded by the number of
observations in the same way, and so these have singular cases too, e.g., 4
observations and 4 moment conditions with center=True gave J = 4.2e6 with a
kernel and 6.96e9 with the robust estimator.

The fix:

* The check of centered_j_stat is a function, _is_singular. Its rank is that of
  the correlation matrix, so it does not depend on the units of the data.
* IVGMM.fit and IVGMMCUE.fit pass the covariance that they inverted to the
  post-estimation, and j_stat is an InvalidTestStatistic if it is singular. It
  is not checked if no covariance is estimated, which is the case if
  iter_limit=1, since the weight matrix is then the inverse of Z'Z / n.
* c_stat is an InvalidTestStatistic with a reason if the J statistic of the
  model that treats the tested variables as exogenous is invalid, or if
  fitting it raises a LinAlgError. The C statistic is the difference of two J
  statistics that use the covariance of this model.

The covariance is tested and not its inverse. In 48,000 simulated clustered
covariances, with the units of the instruments between 1e-3 and 1e3, 14,809 of
which are singular by construction, the test of the covariance found all of them
and had no false alarms. The same test of the inverse missed 780 of them (5%).

The estimates are not changed. A GMM model with a singular covariance still
uses the inverse of noise, so its parameters and standard errors are not
meaningful, or it raises a LinAlgError. The statistics of every model that is
not singular are unchanged: the reference tests of the J, centered J and C
statistics are not modified.

The new tests compute the expected statistics from the definitions, without the
weight matrix classes, for the cases above and for models with two endogenous
regressors and tested subsets, weights, continuously updating GMM, kernel and
robust covariances with 4 to 20 observations, and instruments in units that
differ by 1e16. They also check that the statistics are valid with exactly as
many clusters as moment conditions, that j_stat is not invalid if no covariance
is estimated, and the LinAlgError of c_stat. 54 of the 118 tests fail without
the change.
…clusters

IVGMMCUE with weight_type="clustered" raised numpy.linalg.LinAlgError:
Singular matrix, from inside the objective function that scipy minimizes, if
there were not more clusters than moment conditions. The message does not say
what the problem is or what to do. It happens since #746, which makes the
moment conditions centered by default, as the documentation says they should
be. 7.0 fit a model with as many clusters as moment conditions, since it did
not center them.

Reproduction (reg1/repro_leads.py): n = 120, a constant and an exogenous
regressor, one endogenous regressor and two instruments, so 4 moment
conditions, and clusters = arange(n) % G.

    G   center=True (the default)   center=False
    2   LinAlgError                 LinAlgError
    3   LinAlgError                 LinAlgError
    4   LinAlgError                 fits
    5   fits                        fits

The objective is a function of the inverse of the covariance of the moment
conditions. The covariance that is estimated from G clusters is a sum of G outer
products, and so it has rank at most G. The cluster sums of centered moment
conditions add to zero, so its rank is at most G - 1. With 4 moment conditions
it is singular for G = 4 and less if they are centered, and for G = 3 and less if
they are not. Robust and kernel covariances have the same problem if there are
too few observations, e.g., 4 observations and 4 moment conditions.

fit now tests the rank of the covariance before it does anything else, with the
function that the J and C statistics use, and raises a ValueError. For example,
for G = 4:

    The clustered covariance of the 4 moment conditions is singular because
    there are too few clusters (4 clusters). Its rank is at most 3, one less
    than the number of clusters, since the cluster sums of centered moment
    conditions add to zero. 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.

The covariance is estimated at the first step estimates of two-step GMM, which
are 2SLS. Whether it is singular does not depend on the parameters, except for
special values such as residuals that are all 0. The check is before the
starting values are estimated, since that also inverts the covariance, and
before the optimization starts, so there is no work to do before the error. The
LinAlgError was raised after the optimizer started. A model with enough clusters
is not changed, and its extra cost is one estimate of the covariance. The
class and fit documentation say that the covariance has to be nonsingular.

Not changed:

* IVGMM, the two-step model, does not fail in the same way, and its estimation
  is not changed. With too few clusters it uses the inverse of a singular
  covariance, which is noise, and returns meaningless parameters, or it raises
  a LinAlgError if the covariance is exactly singular. For the model above and
  G = 3 the coefficient on the endogenous regressor is 1.70, and the true value
  is 0.5 and the estimate with G = 5 is 0.48. The J and C statistics of the
  previous commit are invalid.
* A model with center=False and as many clusters as moment conditions still
  fits, as the report says. The covariance is not singular. The objective is
  then constant, and equal to the number of clusters, for every value of the
  parameters, so the estimates are the starting values.

The new tests check the exact type of the error, since LinAlgError is a
subclass of ValueError, the numbers of clusters and moment conditions in the
message, the error with starting values, that the optimizer is not started,
that models with enough clusters fit and have the objective of the definition
as their J statistic, and the kernel and robust covariances with 4, 5 and 8
observations. 10 of the 20 new tests fail without the change.
The two Python 3.10 Azure jobs, which use NumPy 1.x, failed in
test_c_stat_with_too_few_clusters_is_invalid[3] with "'NoneType' object has
no attribute 'c_stat'". With 3 clusters the covariance of the model's own 4
moments is singular as well, and there NumPy raised LinAlgError when the model
was estimated, which the fit helper of the test turns into None. The other
tests in the file already return early in that case; this one did not.

Checked by replacing inv() with one that raises for an ill-conditioned matrix,
as NumPy did on those jobs: the test failed with the same AttributeError
before this change and passes after it. It also passes with the real inv(),
and the whole file passes (138 tests).
@bashtage
bashtage force-pushed the lm71-fix-gmm-singular branch from b0331c6 to f4f8a09 Compare October 12, 2026 08:44
@bashtage
bashtage merged commit 9f1c895 into main Oct 12, 2026
21 of 24 checks passed
@bashtage
bashtage deleted the lm71-fix-gmm-singular branch October 12, 2026 09:20
shaneraphel pushed a commit to shaneraphel/linearmodels that referenced this pull request Oct 13, 2026
origin/main now has bashtage#754 (a ValueError for IVGMMCUE with too few clusters), which
changed the Raises and Notes sections of IVGMMCUE.fit. The Raises section comes
first, then See Also and the Notes of this branch.
shaneraphel pushed a commit to shaneraphel/linearmodels that referenced this pull request Oct 13, 2026
The weight matrix of two-step GMM is the inverse of an estimate of the
covariance of the moment conditions. If there are fewer clusters than moment
conditions (or too few observations) the estimate is singular, the inverse is
numerical noise, and so are the estimates. bashtage#754 made j_stat and c_stat
invalid and made IVGMMCUE raise a clear ValueError in this case, and left the
estimates of IVGMM as they were:

* In the model of the bug report with 3 clusters, the coefficient on x is
  1.697 when it is 0.5, and its standard error is NaN (and 1.95e6 for the
  constant); it is a LinAlgError instead if NumPy finds that the matrix is
  exactly singular, which depends on rounding and so on the platform.
* An exactly identified model does not need its weight matrix, but the noise
  in it is not harmless: with 3 clusters and 3 moment conditions the
  estimates are 9.7 away from the IV estimates, and the standard errors are
  not numbers. The first-step moment conditions of an exactly identified model
  add to zero, so that the covariance of 3 moment conditions is singular with
  3 clusters even if they are not centered.

IVGMM.fit now raises the same ValueError as IVGMMCUE.fit, with the message
built by the same function: it says that the covariance is singular, that
there are too few clusters (with their number and the rank that they allow) or
too few observations, and to use more clusters than moment conditions. It is
checked at every step before the covariance is inverted. It is not checked
with iter_limit=1, which does not use a covariance. IVGMMResults.c_stat
estimates a model with a moment condition for each tested variable, and
returns an invalid statistic instead of an error if that model cannot be
estimated, as it did for a LinAlgError. j_stat of an IVGMM is never invalid
now, since the model is not estimated.

The fits that are valid are not changed. Params, cov, std_errors, j_stat,
centered_j_stat and c_stat of 600 random models (IVGMM and IVGMMCUE, the four
weight types, observation weights, three covariances, units of the
instruments from 1e-2 to 1e2, and cluster counts around the number of moment
conditions) are the same to the last bit on origin/main and here for the 561
that are estimated on both. The 39 that raise here are clustered models with
no more clusters than moment conditions: 19 raised on origin/main too, and the
20 that did not had an invalid J statistic, and NaN or enormous standard
errors.

Tests: the tests of bashtage#754 for the J and C statistics with too few clusters
(and the small samples) accepted a result or a LinAlgError from fit, and
returned if there was none. They test the ValueError and its message now, and
the values of the statistics for the models that can be estimated. New tests
check that the message is that of IVGMMCUE except for the sentence that says
why the inverse is needed; that exactly identified models raise with 2 and 3
clusters and are the IV estimates with 4 or more; that every step is checked,
and the initial weight does not avoid the check; that iter_limit=1 is not
changed; and that the params, std errors, J, centered J and C of seven valid
models are those of origin/main (and the params and J are the definitions).
shaneraphel pushed a commit to shaneraphel/linearmodels that referenced this pull request Oct 13, 2026
…nditions

IVGMMCUE with weight_type="clustered" and center=False fitted a model with as
many clusters as moment conditions without an error, but the objective
function is constant. The cluster sums of the moment conditions are the rows
of a square matrix Xi (G by K), 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 G whenever Xi is nonsingular, for every value of
the parameters (divided by the small sample adjustment if the covariance is
debiased). In the model of the bug report with 4 clusters the objective is
3.99999999997 at every parameter value that was tried, the optimizer stops
after one iteration, and the "estimates" are the starting values, the
estimates of IVGMM, with a J statistic equal to the number of clusters.

bashtage#754 made IVGMMCUE raise if the covariance is singular, which needs more
clusters than moment conditions if they are centered (the default) and at
least as many if they are not. Now more clusters than moment conditions are
needed with every value of center. The check is made at the same place,
before the starting values are estimated and before the objective is
optimized, and also if starting values are given. The message says that there
are as many clusters as moment conditions, that the moment conditions are not
centered, that the objective function is the same for every value of the
parameters so that there is nothing to optimize, and to use more clusters
than moment conditions or a weight_type that does not use clusters.

With as many clusters as moment conditions a model with centered moment
conditions, and an exactly identified model, are singular and raise the
message of bashtage#754. Nothing else changes: of the 600 random models of the
previous commit, the only one that is affected is the one with 4 clusters and
4 uncentered moment conditions, and its J statistic was 3.9999999999999654.

Tests: the objective function of the model with 4 clusters is 4 at five
random parameter vectors (and 4 / (the adjustment) if debiased), computed by
IVGMMCUE.j and from the definition without the package's classes; fit raises
before it optimizes, with and without starting values; the message; and a test
of 2 to 8 clusters with both values of center, which raises for 4 or fewer
clusters in IVGMMCUE, and for IVGMM exactly when the covariance is singular.
The test of bashtage#754 that fitted a model with 4 uncentered clusters expects an
error now.
shaneraphel pushed a commit to shaneraphel/linearmodels that referenced this pull request Oct 13, 2026
…sing IVGMM

test_centered_j_cue_with_too_few_clusters fitted an IVGMMCUE with
center=False and 5 clusters for 5 moment conditions to check that the
centered J statistic is invalid when the uncentered fit is valid. That model
cannot be estimated any more: the objective function of the CUE is constant
with as many clusters as moment conditions, and the CUE needs 6 clusters,
for which the centered covariance is not singular either.

The two-step estimator can still be estimated with 5 uncentered clusters, and
it is the case where the J statistic is valid and the centered J statistic is
not, so the test checks it with IVGMM. It checks that the CUE raises with 5
clusters, and that the CUE with 6 clusters has a valid centered J statistic.
The test of the centered J statistic of the CUE with a singular covariance of
bashtage#754 is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant