Skip to content

TST: Use eigh for the symmetric matrix in test_against_ols - #762

Merged
bashtage merged 5 commits into
mainfrom
lm71-bug6-hygiene
Oct 11, 2026
Merged

bashtage merged 5 commits into
mainfrom
lm71-bug6-hygiene

Conversation

@bashtage

Copy link
Copy Markdown
Owner

test_against_ols builds an orthogonal basis for the absorbed regressors from
the eigenvectors of the symmetric matrix absorb.T @ absorb. np.linalg.eig is
the solver for general matrices, and when the matrix is singular, which it is
in 32 of the 72 configurations, it returns the whole array of eigenvectors as
complex. Each of the 32 tests then emitted "ComplexWarning: Casting complex
values to real discards the imaginary part" when IVData converted the
regressors to float (iv/data.py:82).

Use np.linalg.eigh. It is the solver for symmetric matrices and its
eigenvectors are real. It orders the eigenvalues from the smallest, which the
test already reverses with argsort(a)[::-1]. Only the span of the leading
eigenvectors enters the OLS regression, so the test checks the same thing.
Comparing the eig and eigh versions of every configuration:

  • The imaginary part of the eigenvectors that eig returned and the test used
    is exactly 0 in all 32 configurations.
  • The projection matrices on the two bases differ by at most 2e-16.
  • The OLS parameters, standard errors, covariance, residuals, R2 and s2 differ
    by at most 6e-15.

Running test_against_ols with ComplexWarning turned into an error: 32 of 72
fail before this change, and all 72 pass after it. This is also needed before
IVData rejects complex input instead of discarding the imaginary part, which
would turn these warnings into errors.

test_against_ols builds an orthogonal basis for the absorbed regressors from
the eigenvectors of the symmetric matrix absorb.T @ absorb. np.linalg.eig is
the solver for general matrices, and when the matrix is singular, which it is
in 32 of the 72 configurations, it returns the whole array of eigenvectors as
complex. Each of the 32 tests then emitted "ComplexWarning: Casting complex
values to real discards the imaginary part" when IVData converted the
regressors to float (iv/data.py:82).

Use np.linalg.eigh. It is the solver for symmetric matrices and its
eigenvectors are real. It orders the eigenvalues from the smallest, which the
test already reverses with argsort(a)[::-1]. Only the span of the leading
eigenvectors enters the OLS regression, so the test checks the same thing.
Comparing the eig and eigh versions of every configuration:

- The imaginary part of the eigenvectors that eig returned and the test used
  is exactly 0 in all 32 configurations.
- The projection matrices on the two bases differ by at most 2e-16.
- The OLS parameters, standard errors, covariance, residuals, R2 and s2 differ
  by at most 6e-15.

Running test_against_ols with ComplexWarning turned into an error: 32 of 72
fail before this change, and all 72 pass after it. This is also needed before
IVData rejects complex input instead of discarding the imaginary part, which
would turn these warnings into errors.
Two deprecated features have no removal date. Say that they will be removed in
linearmodels 8.0.

AbsorbingLS.fit(lsmr_options=...) has been deprecated since 4.17 in favor of
absorb_options. The FutureWarning and the ".. deprecated:: 4.17" note in the
docstring now say that lsmr_options will be removed in linearmodels 8.0. The
docstring also had a typo, "Options to ass to".

linearmodels.__future__.ordering has been empty since 5.0, when the variable
ordering that importing it enabled in 4.31 became the default. Importing it did
nothing and said nothing. It now emits

    FutureWarning: linearmodels.__future__.ordering has no effect and will be
    removed in linearmodels 8.0

FutureWarning is shown by default. stacklevel=2 attributes the warning to the
statement that imports the module, for `import linearmodels.__future__.ordering`
and for `from linearmodels.__future__ import ordering`, because the warnings
module skips the frames of the import machinery. Nothing in linearmodels or the
examples imports the module. linearmodels/__future__/__init__.py is still empty,
so importing the package does not warn.

The new tests in tests/test_deprecations.py check that

- both warnings have the text above and are attributed to the caller: the file
  that calls fit, and the file and line of the import statement;
- the lsmr_options docstring says that it is deprecated and will be removed in
  8.0;
- lsmr_options is still passed on to the absorption, by comparing a fit with
  maxiter=1, which does not converge, to the same fit using absorb_options.

The ordering module runs only the first time that it is imported, which may
have been earlier in the test session. The tests remove it from sys.modules, run
the import in a code object with a file name and a line number to see where the
warning points, and restore the state afterwards. One more test imports it in a
new interpreter, which is what a user sees. It takes a few seconds, because
importing linearmodels does.

Six of the seven new tests fail on main. The seventh checks that lsmr_options
still works, which it also does on main.
test_examples.py switches asyncio to the Windows selector event loop policy so
that zmq, which the notebook kernel client uses, does not warn about the default
Proactor loop. WindowsSelectorEventLoopPolicy, get_event_loop_policy and
set_event_loop_policy are deprecated in Python 3.14 and will be removed in 3.16.
Every xdist worker emitted four DeprecationWarnings when it imported the test
module, 16 in a run with -n 4.

Only select the policy before Python 3.14. The (3, 8) lower bound is below the
minimum Python version, so drop it.

I checked that the notebooks run without the policy on Windows 11 with Python
3.14.6, pyzmq 27.1.0, jupyter_client 8.9.1, ipykernel 7.3.0, nbclient 0.11.0 and
tornado 6.5.7, using python -m pytest linearmodels/tests/test_examples.py -n 4:

| Case | Result |
|---|---|
| Policy selected (before this change) | 12 passed, 16 warnings (DeprecationWarning) |
| Policy not selected | 12 passed, 4 warnings (pyzmq RuntimeWarning) |
| Policy not selected, pyzmq warning ignored | 12 passed, 0 warnings |

Without the policy pyzmq warns once per worker that the Proactor loop has no
add_reader, that it registers a selector thread instead, and recommends the
deprecated function. It only describes how zmq works on this loop, so ignore it
in test_notebook.
test_c_stat_against_r compared p-values with rtol=1e-5, atol=1e-12 and a
comment that the p-value is 1 - cdf, so it cannot resolve probabilities below
about 1e-16. That stopped being true in #742: p-values are survival functions,
which are accurate in the far tail. The absolute tolerance only hides errors in
small p-values. Four of the 23 reference p-values are below 1e-12 (the smallest
is 2e-29), so any computed value between 0 and 1e-12 passed for them.

Use rtol=1e-8 and no atol, and replace the stale comment. The largest relative
difference between a p-value and the R reference in the table is 7e-13, so the
tolerance leaves a margin of four orders of magnitude.
The HDFE path sets absorb_options["drop_singletons"] = False before it calls
pyhdfe.create. It did this on the dict that the caller passed, so after fit the
caller's dict had a new key. That key is not an option of LSMR, so reusing the
dict for a model that is fitted with method="lsmr" failed:

    options = {}
    AbsorbingLS(y, x, absorb=cat).fit(absorb_options=options)
    # options is now {"drop_singletons": False}
    AbsorbingLS(y, x, absorb=cat).fit(absorb_options=options, method="lsmr")
    # TypeError: lsmr() got an unexpected keyword argument 'drop_singletons'

Copy the dict in _first_time_fit. The deprecated lsmr_options were already
copied.

The new test checks both symptoms. On main it fails on the first assertion,
because the options that were passed in have gained a key, and the LSMR fit
that follows raises the TypeError above. The LSMR fit does not use the cache,
which could otherwise hold the result of an earlier fit and skip the call that
fails.

This is a different issue from the lsmr_options deprecation and is in its own
commit.
@bashtage
bashtage merged commit 5c33d0d into main Oct 11, 2026
6 of 18 checks passed
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