Skip to content

Commit 9247a1f

Browse files
authored
Merge pull request #140 from bashtage/fix-coverage
ENH: Allow env to be set for SystemFormulas
2 parents ab001a0 + 47a1214 commit 9247a1f

File tree

3 files changed

+146
-2
lines changed

3 files changed

+146
-2
lines changed

linearmodels/system/model.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,22 @@ def formula(self):
201201
"""Cleaned version of formula"""
202202
return self._clean_formula
203203

204+
@property
205+
def eval_env(self):
206+
"""Set or get the eval env depth"""
207+
return self._eval_env
208+
209+
@eval_env.setter
210+
def eval_env(self, value):
211+
self._eval_env = value
212+
# Update parsers for new level
213+
parsers = self._parsers
214+
new_parsers = OrderedDict()
215+
for key in parsers:
216+
parser = parsers[key]
217+
new_parsers[key] = IVFormulaParser(parser._formula, parser._data, self._eval_env)
218+
self._parsers = new_parsers
219+
204220
@property
205221
def equation_labels(self):
206222
return list(self._parsers.keys())
@@ -532,6 +548,47 @@ def __str__(self):
532548
return out
533549

534550
def predict(self, params, *, equations=None, data=None, eval_env=8):
551+
"""
552+
Predict values for additional data
553+
554+
Parameters
555+
----------
556+
params : array-like
557+
Model parameters (nvar by 1)
558+
equations : dict
559+
Dictionary-like structure containing exogenous and endogenous
560+
variables. Each key is an equations label and must
561+
match the labels used to fir the model. Each value must be either a tuple
562+
of the form (exog, endog) or a dictionary with keys 'exog' and 'endog'.
563+
If predictions are not required for one of more of the model equations,
564+
these keys can be omitted.
565+
data : DataFrame
566+
Values to use when making predictions from a model constructed
567+
from a formula
568+
eval_env : int
569+
Depth of use when evaluating formulas using Patsy.
570+
571+
Returns
572+
-------
573+
predictions : DataFrame
574+
Fitted values from supplied data and parameters
575+
576+
Notes
577+
-----
578+
If `data` is not none, then `equations` must be none.
579+
Predictions from models constructed using formulas can
580+
be computed using either `equations`, which will treat these are
581+
arrays of values corresponding to the formula-process data, or using
582+
`data` which will be processed using the formula used to construct the
583+
values corresponding to the original model specification.
584+
585+
When using `exog` and `endog`, the regressor array for a particular
586+
equation is assembled as
587+
`[equations[eqn]['exog'], equations[eqn]['endog']]` where `eqn` is
588+
an equation label. These must correspond to the columns in the
589+
estimated model.
590+
"""
591+
535592
if data is not None:
536593
parser = SystemFormulaParser(self.formula, data=data, eval_env=eval_env)
537594
equations = parser.data
@@ -543,19 +600,22 @@ def predict(self, params, *, equations=None, data=None, eval_env=8):
543600
for i, label in enumerate(self._eq_labels):
544601
kx = self._x[i].shape[1]
545602
if label in equations:
603+
b = params[loc:loc + kx]
546604
eqn = equations[label] # type: dict
547605
exog = eqn.get('exog', None)
548606
endog = eqn.get('endog', None)
549607
if exog is None and endog is None:
608+
loc += kx
550609
continue
610+
551611
if exog is not None:
552612
exog_endog = IVData(exog).pandas
553613
if endog is not None:
554614
endog = IVData(endog)
555615
exog_endog = concat([exog_endog, endog.pandas], 1)
556616
else:
557617
exog_endog = IVData(endog).pandas
558-
b = params[loc:loc + kx]
618+
559619
fitted = exog_endog.values @ b
560620
fitted = DataFrame(fitted, index=exog_endog.index, columns=[label])
561621
out[label] = fitted

linearmodels/tests/panel/test_formula.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import pandas as pd
55
import pytest
66

7+
from linearmodels.compat.pandas import assert_frame_equal
78
from linearmodels.formula import (between_ols, first_difference_ols, panel_ols,
89
pooled_ols, random_effects, fama_macbeth)
910
from linearmodels.panel.model import (BetweenOLS, FirstDifferenceOLS, PanelOLS,
10-
PooledOLS, RandomEffects, FamaMacBeth)
11+
PooledOLS, RandomEffects, FamaMacBeth,
12+
PanelFormulaParser)
1113
from linearmodels.tests.panel._utility import generate_data, datatypes
1214

1315
pytestmark = pytest.mark.filterwarnings('ignore::linearmodels.utility.MissingValueWarning')
@@ -40,6 +42,11 @@ def models(request):
4042
return request.param
4143

4244

45+
@pytest.fixture(params=[True, False])
46+
def effects(request):
47+
return request.param
48+
49+
4350
def sigmoid(v):
4451
return np.exp(v) / (1 + np.exp(v))
4552

@@ -216,3 +223,30 @@ def test_formulas_predict_error(data, models, formula):
216223
res = model(data.y, x[vars]).fit()
217224
with pytest.raises(ValueError):
218225
res.predict(data=joined)
226+
227+
228+
def test_parser(data, formula, effects):
229+
if not isinstance(data.y, pd.DataFrame):
230+
return
231+
if effects:
232+
formula += ' + EntityEffects + TimeEffects'
233+
joined = data.x
234+
joined['y'] = data.y
235+
parser = PanelFormulaParser(formula, joined)
236+
dep, exog = parser.data
237+
assert_frame_equal(parser.dependent, dep)
238+
assert_frame_equal(parser.exog, exog)
239+
parser.eval_env = 3
240+
assert parser.eval_env == 3
241+
parser.eval_env = 2
242+
assert parser.eval_env == 2
243+
assert parser.entity_effect == ('EntityEffects' in formula)
244+
assert parser.time_effect == ('TimeEffects' in formula)
245+
246+
formula += ' + FixedEffects '
247+
if effects:
248+
with pytest.raises(ValueError):
249+
PanelFormulaParser(formula, joined)
250+
else:
251+
parser = PanelFormulaParser(formula, joined)
252+
assert parser.entity_effect

linearmodels/tests/system/test_formulas.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import OrderedDict
12
from itertools import product
23

34
import numpy as np
@@ -7,6 +8,7 @@
78
from linearmodels import SUR, IVSystemGMM, IV3SLS
89
from linearmodels.compat.pandas import assert_series_equal, assert_frame_equal
910
from linearmodels.formula import sur, iv_system_gmm, iv_3sls
11+
from linearmodels.system.model import SystemFormulaParser
1012
from linearmodels.tests.system._utility import generate_3sls_data_v2
1113
from linearmodels.utility import AttrDict
1214

@@ -100,6 +102,20 @@ def test_predict_partial(config):
100102
pred2 = res.predict(data=joined, dataframe=True)
101103
assert_frame_equal(pred2[pred.columns], pred)
102104

105+
eqns = AttrDict()
106+
for key in list(mod._equations.keys())[1:]:
107+
eqns[key] = mod._equations[key]
108+
final = list(mod._equations.keys())[0]
109+
eqns[final] = {'exog': None, 'endog': None}
110+
pred3 = res.predict(equations=eqns, dataframe=True)
111+
assert_frame_equal(pred2[pred3.columns], pred3)
112+
113+
eqns = AttrDict()
114+
for key in mod._equations:
115+
eqns[key] = {k: v for k, v in mod._equations[key].items() if v.shape[1] > 0}
116+
pred4 = res.predict(equations=eqns, dataframe=True)
117+
assert_frame_equal(pred2, pred4)
118+
103119

104120
def test_invalid_predict(config):
105121
fmla, model, interface = config
@@ -110,3 +126,37 @@ def test_invalid_predict(config):
110126
res = mod.fit()
111127
with pytest.raises(ValueError):
112128
res.predict(data=joined, equations=mod._equations)
129+
130+
131+
def test_parser(config):
132+
fmla, model, interface = config
133+
parser = SystemFormulaParser(fmla, joined, eval_env=5)
134+
orig_data = parser.data
135+
assert isinstance(orig_data, OrderedDict)
136+
assert parser.eval_env == 5
137+
138+
parser.eval_env = 4
139+
assert parser.eval_env == 4
140+
exog = parser.exog
141+
dep = parser.dependent
142+
endog = parser.endog
143+
instr = parser.instruments
144+
for key in orig_data:
145+
eq = orig_data[key]
146+
assert_frame_equal(exog[key], eq['exog'])
147+
assert_frame_equal(dep[key], eq['dependent'])
148+
assert_frame_equal(endog[key], eq['endog'])
149+
assert_frame_equal(instr[key], eq['instruments'])
150+
151+
labels = parser.equation_labels
152+
for label in labels:
153+
assert label in orig_data
154+
new_parser = SystemFormulaParser(parser.formula, joined, eval_env=5)
155+
156+
new_data = new_parser.data
157+
for key in orig_data:
158+
eq1 = orig_data[key]
159+
eq2 = new_data[key]
160+
for key in eq1:
161+
if eq1[key] is not None:
162+
assert_frame_equal(eq1[key], eq2[key])

0 commit comments

Comments
 (0)