Skip to content

Commit a50944a

Browse files
committed
BUG: Fix predict when exog or endog is None
1 parent 28af72e commit a50944a

File tree

4 files changed

+76
-6
lines changed

4 files changed

+76
-6
lines changed

linearmodels/iv/model.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
OneWayClusteredWeightMatrix,
5151
)
5252
from linearmodels.iv.results import IVGMMResults, IVResults, OLSResults
53+
from linearmodels.panel.utility import InvalidFormulaError
5354
from linearmodels.shared.exceptions import IndexWarning, missing_warning
5455
from linearmodels.shared.hypotheses import InvalidTestStatistic, WaldTestStatistic
5556
from linearmodels.shared.linalg import has_constant, inv_sqrth
@@ -291,15 +292,34 @@ def predict(
291292
"Predictions can only be constructed using one "
292293
"of exog/endog or data, but not both."
293294
)
294-
if exog is not None or endog is not None:
295+
if exog is not None:
295296
exog = IVData(exog).pandas
297+
if endog is not None:
296298
endog = IVData(endog).pandas
297-
elif data is not None:
299+
if data is not None:
300+
if endog is not None or exog is not None:
301+
raise ValueError(
302+
"Predictions can be constructed using either exog and endog "
303+
"or data, but not both."
304+
)
298305
parser = IVFormulaParser(self.formula, data, eval_env=eval_env)
299306
exog = parser.exog
300307
endog = parser.endog
301-
else:
302-
raise ValueError("exog and endog or data must be provided.")
308+
if exog is None and exog is None:
309+
raise InvalidFormulaError(
310+
f"Parsed formula ({self.formula}) has no exog and no endog. One "
311+
f"of these must be included in the formula to make a prediction."
312+
)
313+
if all(a is None for a in (exog, endog, data)):
314+
raise ValueError("At least one of exog, endog, or data must be provided.")
315+
if exog is None:
316+
assert endog is not None
317+
exog = IVData(exog, nobs=endog.shape[0]).pandas
318+
exog.index = endog.index
319+
if endog is None:
320+
assert exog is not None
321+
endog = IVData(endog, nobs=exog.shape[0]).pandas
322+
endog.index = exog.index
303323
assert exog is not None
304324
assert endog is not None
305325
if exog.shape[0] != endog.shape[0]:

linearmodels/panel/utility.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
__all__ = [
2525
"AbsorbingEffectError",
2626
"AbsorbingEffectWarning",
27+
"InvalidFormulaError",
2728
"_drop_singletons",
2829
"_py_drop_singletons",
2930
"_remove_node",
@@ -688,3 +689,7 @@ def generate_panel_data(
688689
clusters = concat([vc1_df, vc2_df], sort=False)
689690
data = concat([y_df, x_df], axis=1, sort=False)
690691
return PanelModelData(data, w_df, other_eff, clusters)
692+
693+
694+
class InvalidFormulaError(Exception):
695+
pass

linearmodels/tests/iv/test_formulas.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,9 @@ def test_predict_formula(data, model_and_func, formula):
210210
assert_frame_equal(pred, pred2)
211211
assert_allclose(res.fitted_values, pred)
212212

213-
with pytest.raises(ValueError, match=r"exog and endog or data must be provided"):
213+
with pytest.raises(
214+
ValueError, match=r"At least one of exog, endog, or data must be provided"
215+
):
214216
mod.predict(res.params)
215217

216218

@@ -404,3 +406,13 @@ def test_formula_categorical_equiv(data, model_and_func, dtype):
404406
"x2",
405407
"x3",
406408
]
409+
410+
411+
def test_predict_no_rhs(data, model_and_func):
412+
model, _ = model_and_func
413+
mod = model.from_formula("y ~", data)
414+
res = mod.fit()
415+
pred0 = res.predict()
416+
pred1 = res.predict(data=data)
417+
pred1.columns = pred0.columns
418+
assert_frame_equal(pred0, pred1)

linearmodels/tests/iv/test_results.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from numpy import asarray
22
from numpy.testing import assert_allclose
33
from pandas import DataFrame
4-
from pandas.testing import assert_series_equal
4+
from pandas.testing import assert_frame_equal, assert_series_equal
55
import pytest
66

77
from linearmodels.iv.data import IVData
@@ -96,3 +96,36 @@ def test_predict_no_selection(data, model):
9696
res = mod.fit()
9797
with pytest.raises(ValueError, match=r"At least one output must be selected"):
9898
res.predict(fitted=False, idiosyncratic=False, missing=True)
99+
100+
101+
@pytest.mark.parametrize("include_vars", ["exog", "both", "endog"])
102+
def test_fitted_predict_combinations(data, model, include_vars):
103+
args = (data.dep,)
104+
if include_vars in ("exog", "both"):
105+
args += (data.exog,)
106+
else:
107+
args += (None,)
108+
if include_vars in ("endog", "both"):
109+
args += (data.endog, data.instr)
110+
else:
111+
args += (None, None)
112+
113+
mod = model(*args)
114+
res = mod.fit()
115+
assert_series_equal(res.idiosyncratic, res.resids)
116+
y = mod.dependent.pandas
117+
expected = asarray(y) - asarray(res.resids)[:, None]
118+
expected = DataFrame(expected, y.index, ["fitted_values"])
119+
assert_frame_similar(expected, res.fitted_values)
120+
assert_allclose(expected, res.fitted_values)
121+
pred = res.predict()
122+
pred2 = res.predict(exog=args[1], endog=args[2])
123+
pred2.columns = pred.columns
124+
assert_frame_equal(pred, pred2)
125+
nobs = res.resids.shape[0]
126+
assert isinstance(pred, DataFrame)
127+
assert pred.shape == (nobs, 1)
128+
pred = res.predict(idiosyncratic=True, missing=True)
129+
nobs = IVData(data.dep).pandas.shape[0]
130+
assert pred.shape == (nobs, 2)
131+
assert list(pred.columns) == ["fitted_values", "residual"]

0 commit comments

Comments
 (0)