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..b202a40558 --- /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]) + # 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 + )