From 4dc1c618d709fa462d1d98e917a1a91b55e1b199 Mon Sep 17 00:00:00 2001 From: Kevin Sheppard Date: Mon, 12 Oct 2026 21:35:54 +0100 Subject: [PATCH 1/2] BUG: Accept endog and instruments without exog in IV3SLS.multivariate_iv The documentation of IV3SLS.multivariate_iv requires at least one of exog and endog, but exog was converted without the number of observations. With endog and instruments and no exog, exog is None and IVData has no array to take the number of observations from: IV3SLS.multivariate_iv(dependent, endog=endog, instruments=instruments) ValueError: nobs required when x is None The number of observations of dependent is now given for exog, as it is for endog and instruments. Specifications with exog are not changed. test_multivariate_iv.py checks that the model with endog and instruments, with arrays and with data frames, has the estimates of the 2SLS regression of each dependent variable, which the 3SLS estimator reduces to when the equations have the same regressors and instruments, that exog alone and all three work as before, and the errors for a wrong number of observations. --- linearmodels/system/model.py | 2 +- .../tests/system/test_multivariate_iv.py | 76 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 linearmodels/tests/system/test_multivariate_iv.py diff --git a/linearmodels/system/model.py b/linearmodels/system/model.py index ca59e5697a..5c765b5806 100644 --- a/linearmodels/system/model.py +++ b/linearmodels/system/model.py @@ -1653,8 +1653,8 @@ def multivariate_iv( dependent_ivd = IVData(dependent, var_name="dependent") if exog is None and endog is None: raise ValueError("At least one of exog or endog must be provided") - exog_ivd = IVData(exog, var_name="exog") nobs = dependent_ivd.shape[0] + exog_ivd = IVData(exog, var_name="exog", nobs=nobs) endog_ivd = IVData(endog, var_name="endog", nobs=nobs) instr_ivd = IVData(instruments, var_name="instruments", nobs=nobs) for col in dependent_ivd.pandas: diff --git a/linearmodels/tests/system/test_multivariate_iv.py b/linearmodels/tests/system/test_multivariate_iv.py new file mode 100644 index 0000000000..2de63db657 --- /dev/null +++ b/linearmodels/tests/system/test_multivariate_iv.py @@ -0,0 +1,76 @@ +""" +Tests of IV3SLS.multivariate_iv with only some of the exogenous, endogenous and +instrumental variables + +The documentation requires at least one of exog and endog, so endog and instruments +without exog is a valid specification. +""" + +import numpy as np +from numpy.testing import assert_allclose +import pandas as pd +import pytest + +from linearmodels.iv import IV2SLS +from linearmodels.system import IV3SLS + +NOBS = 200 +NDEP = 2 + + +@pytest.fixture(scope="module") +def data(): + rng = np.random.default_rng(0) + instruments = rng.standard_normal((NOBS, 3)) + shocks = rng.standard_normal((NOBS, NDEP + 1)) + endog = ( + instruments @ np.array([[1.0, 0.5], [0.5, 1.0], [0.2, 0.2]]) + + 0.5 * shocks[:, [0]] + + rng.standard_normal((NOBS, 2)) + ) + dependent = endog @ rng.standard_normal((2, NDEP)) + shocks[:, 1:] + exog = rng.standard_normal((NOBS, 2)) + return dependent, exog, endog, instruments + + +@pytest.mark.parametrize("kind", ["array", "frame"]) +def test_endog_and_instruments_without_exog(data, kind): + dependent, _, endog, instruments = data + if kind == "frame": + index = pd.date_range("2000-1-1", periods=NOBS) + dependent = pd.DataFrame(dependent, columns=["y0", "y1"], index=index) + endog = pd.DataFrame(endog, columns=["x0", "x1"], index=index) + instruments = pd.DataFrame(instruments, columns=["z0", "z1", "z2"], index=index) + mod = IV3SLS.multivariate_iv(dependent, endog=endog, instruments=instruments) + res = mod.fit() + assert len(res.equations) == NDEP + # The equations share the regressors and the instruments, so 3SLS is 2SLS + # in each equation + dep = np.asarray(dependent) + for i, label in enumerate(res.equation_labels): + expected = IV2SLS(dep[:, i], None, np.asarray(endog), np.asarray(instruments)) + assert_allclose( + res.equations[label].params, expected.fit().params, rtol=1e-8, atol=1e-10 + ) + assert len(res.equations[label].params) == 2 + + +def test_exog_only_and_with_exog_are_unchanged(data): + dependent, exog, endog, instruments = data + only_exog = IV3SLS.multivariate_iv(dependent, exog=exog) + assert only_exog.fit().params.shape[0] == NDEP * exog.shape[1] + full = IV3SLS.multivariate_iv(dependent, exog, endog, instruments).fit() + assert full.params.shape[0] == NDEP * (exog.shape[1] + endog.shape[1]) + # exog and endog without instruments: nothing identifies the endogenous variables + with pytest.raises(ValueError, match=r"endogenous variables"): + IV3SLS.multivariate_iv(dependent, endog=endog) + + +def test_wrong_number_of_observations(data): + dependent, _, endog, instruments = data + with pytest.raises(ValueError, match=r"observations"): + IV3SLS.multivariate_iv(dependent, endog=endog[:-1], instruments=instruments) + with pytest.raises(ValueError, match=r"observations"): + IV3SLS.multivariate_iv( + dependent, exog=instruments[:-5], endog=endog, instruments=instruments + ) From 5f233cd3a96eb52e9a80536e3e75396c5dc1dd11 Mon Sep 17 00:00:00 2001 From: Kevin Sheppard Date: Mon, 12 Oct 2026 21:42:45 +0100 Subject: [PATCH 2/2] TST: Correct the comment on endog without instruments in the multivariate_iv test The call passes endog only, so the comment no longer mentions exog. --- linearmodels/tests/system/test_multivariate_iv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linearmodels/tests/system/test_multivariate_iv.py b/linearmodels/tests/system/test_multivariate_iv.py index 2de63db657..b202a40558 100644 --- a/linearmodels/tests/system/test_multivariate_iv.py +++ b/linearmodels/tests/system/test_multivariate_iv.py @@ -61,7 +61,7 @@ def test_exog_only_and_with_exog_are_unchanged(data): assert only_exog.fit().params.shape[0] == NDEP * exog.shape[1] full = IV3SLS.multivariate_iv(dependent, exog, endog, instruments).fit() assert full.params.shape[0] == NDEP * (exog.shape[1] + endog.shape[1]) - # exog and endog without instruments: nothing identifies the endogenous variables + # endog without instruments: nothing identifies the endogenous variables with pytest.raises(ValueError, match=r"endogenous variables"): IV3SLS.multivariate_iv(dependent, endog=endog)