Skip to content

Commit b136d28

Browse files
committed
ENH: Add typing information
Add types to IV models Change the return type of an invalid test to InvalidTest Silence useless warnings All optimization options to be passed to IVGMMCUE
1 parent 528d7b9 commit b136d28

File tree

9 files changed

+87
-27
lines changed

9 files changed

+87
-27
lines changed

linearmodels/iv/model.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
KernelWeightMatrix,
1919
OneWayClusteredWeightMatrix)
2020
from linearmodels.iv.results import IVGMMResults, IVResults, OLSResults
21+
from linearmodels.typing import Numeric, OptionalNumeric
22+
from linearmodels.typing.iv import ArrayLike, OptionalArrayLike
2123
from linearmodels.utility import (WaldTestStatistic, has_constant, inv_sqrth,
2224
missing_warning)
2325

@@ -105,11 +107,13 @@ class IVLIML(object):
105107
IV2SLS, IVGMM, IVGMMCUE
106108
"""
107109

108-
def __init__(self, dependent, exog, endog, instruments, *, weights=None,
109-
fuller=0, kappa=None):
110+
def __init__(self, dependent: ArrayLike, exog: OptionalArrayLike,
111+
endog: OptionalArrayLike, instruments: OptionalArrayLike, *,
112+
weights: OptionalArrayLike = None, fuller: Numeric = 0,
113+
kappa: OptionalNumeric = None):
110114

111115
self.dependent = IVData(dependent, var_name='dependent')
112-
nobs = self.dependent.shape[0]
116+
nobs = self.dependent.shape[0] # type: int
113117
self.exog = IVData(exog, var_name='exog', nobs=nobs)
114118
self.endog = IVData(endog, var_name='endog', nobs=nobs)
115119
self.instruments = IVData(instruments, var_name='instruments', nobs=nobs)
@@ -573,7 +577,9 @@ class IV2SLS(IVLIML):
573577
IVLIML, IVGMM, IVGMMCUE
574578
"""
575579

576-
def __init__(self, dependent, exog, endog, instruments, *, weights=None):
580+
def __init__(self, dependent: ArrayLike, exog: OptionalArrayLike,
581+
endog: OptionalArrayLike, instruments: OptionalArrayLike, *,
582+
weights: OptionalArrayLike = None):
577583
self._method = 'IV-2SLS'
578584
super(IV2SLS, self).__init__(dependent, exog, endog, instruments,
579585
weights=weights, fuller=0, kappa=1)
@@ -675,8 +681,10 @@ class IVGMM(IVLIML):
675681
IV2SLS, IVLIML, IVGMMCUE
676682
"""
677683

678-
def __init__(self, dependent, exog, endog, instruments, *, weights=None,
679-
weight_type='robust', **weight_config):
684+
def __init__(self, dependent: ArrayLike, exog: OptionalArrayLike,
685+
endog: OptionalArrayLike, instruments: OptionalArrayLike, *,
686+
weights: OptionalArrayLike = None,
687+
weight_type: str = 'robust', **weight_config):
680688
self._method = 'IV-GMM'
681689
self._result_container = IVGMMResults
682690
super(IVGMM, self).__init__(dependent, exog, endog, instruments, weights=weights)
@@ -914,8 +922,10 @@ class IVGMMCUE(IVGMM):
914922
IV2SLS, IVLIML, IVGMM
915923
"""
916924

917-
def __init__(self, dependent, exog, endog, instruments, *, weights=None,
918-
weight_type='robust', **weight_config):
925+
def __init__(self, dependent: ArrayLike, exog: OptionalArrayLike,
926+
endog: OptionalArrayLike, instruments: OptionalArrayLike, *,
927+
weights: OptionalArrayLike = None,
928+
weight_type: str = 'robust', **weight_config):
919929
self._method = 'IV-GMM-CUE'
920930
super(IVGMMCUE, self).__init__(dependent, exog, endog, instruments, weights=weights,
921931
weight_type=weight_type, **weight_config)
@@ -1017,7 +1027,7 @@ def j(self, params, x, y, z):
10171027
g_bar = (z * eps).mean(0)
10181028
return nobs * g_bar.T @ w @ g_bar.T
10191029

1020-
def estimate_parameters(self, starting, x, y, z, display=False):
1030+
def estimate_parameters(self, starting, x, y, z, display=False, opt_options=None):
10211031
r"""
10221032
Parameters
10231033
----------
@@ -1031,6 +1041,9 @@ def estimate_parameters(self, starting, x, y, z, display=False):
10311041
Instrument matrix (nobs by ninstr)
10321042
display : bool
10331043
Flag indicating whether to display iterative optimizer output
1044+
opt_options : dict, optional
1045+
Dictionary containing additional keyword arguments to pass to
1046+
scipy.optimize.minimize.
10341047
10351048
Returns
10361049
-------
@@ -1047,11 +1060,18 @@ def estimate_parameters(self, starting, x, y, z, display=False):
10471060
scipy.optimize.minimize
10481061
"""
10491062
args = (x, y, z)
1050-
res = minimize(self.j, starting, args=args, options={'disp': display})
1063+
opt_options = {} if opt_options is None else opt_options
1064+
options = {'disp': display}
1065+
if 'options' in opt_options:
1066+
opt_options = opt_options.copy()
1067+
options.update(opt_options.pop('options'))
1068+
1069+
res = minimize(self.j, starting, args=args, options=options, **opt_options)
10511070

10521071
return res.x[:, None], res.nit
10531072

1054-
def fit(self, *, starting=None, display=False, cov_type='robust', **cov_config):
1073+
def fit(self, *, starting=None, display=False, cov_type='robust', opt_options=None,
1074+
**cov_config):
10551075
r"""
10561076
Estimate model parameters
10571077
@@ -1064,6 +1084,10 @@ def fit(self, *, starting=None, display=False, cov_type='robust', **cov_config):
10641084
Flag indicating whether to display optimization output
10651085
cov_type : str, optional
10661086
Name of covariance estimator to use
1087+
opt_options : dict, optional
1088+
Additional options to pass to scipy.optimize.minimize when
1089+
optimizing the objective function. If not provided, defers to
1090+
scipy to choose an appropriate optimizer.
10671091
**cov_config
10681092
Additional parameters to pass to covariance estimator
10691093
@@ -1080,10 +1104,6 @@ def fit(self, *, starting=None, display=False, cov_type='robust', **cov_config):
10801104
is provided.
10811105
10821106
Starting values are computed by IVGMM.
1083-
1084-
.. todo::
1085-
1086-
* Expose method to pass optimization options
10871107
"""
10881108

10891109
wy, wx, wz = self._wy, self._wx, self._wz
@@ -1103,7 +1123,8 @@ def fit(self, *, starting=None, display=False, cov_type='robust', **cov_config):
11031123
if len(starting) != self.exog.shape[1] + self.endog.shape[1]:
11041124
raise ValueError('starting does not have the correct number '
11051125
'of values')
1106-
params, iters = self.estimate_parameters(starting, wx, wy, wz, display)
1126+
params, iters = self.estimate_parameters(starting, wx, wy, wz, display,
1127+
opt_options=opt_options)
11071128
eps = wy - wx @ params
11081129
wmat = inv(weight_matrix(wx, wz, eps))
11091130

@@ -1140,6 +1161,7 @@ class _OLS(IVLIML):
11401161
statsmodels.regression.linear_model.GLS
11411162
"""
11421163

1143-
def __init__(self, dependent, exog, *, weights=None):
1164+
def __init__(self, dependent: ArrayLike, exog: OptionalArrayLike, *,
1165+
weights: OptionalArrayLike = None):
11441166
super(_OLS, self).__init__(dependent, exog, None, None, weights=weights, kappa=0.0)
11451167
self._result_container = OLSResults

linearmodels/iv/results.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,13 @@ def cov_type(self):
220220
"""Covariance estimator used"""
221221
return self._cov_type
222222

223-
@property
223+
@cached_property
224224
def std_errors(self):
225225
"""Estimated parameter standard errors"""
226226
std_errors = sqrt(diag(self.cov))
227227
return Series(std_errors, index=self._vars, name='stderr')
228228

229-
@property
229+
@cached_property
230230
def tstats(self):
231231
"""Parameter t-statistics"""
232232
return Series(self._params / self.std_errors, name='tstat')
@@ -834,12 +834,10 @@ def wooldridge_overid(self):
834834
instruments = self.model.instruments
835835
nobs, nendog = endog.shape
836836
ninstr = instruments.shape[1]
837+
name = 'Wooldridge\'s score test of overidentification'
837838
if ninstr - nendog == 0:
838-
import warnings
839-
warnings.warn('Test requires more instruments than '
840-
'endogenous variables',
841-
UserWarning)
842-
return WaldTestStatistic(0, 'Test is not feasible.', 1, name='Infeasible test.')
839+
return InvalidTestStatistic('Test requires more instruments than '
840+
'endogenous variables.', name=name)
843841

844842
endog_hat = proj(endog.ndarray, c_[exog.ndarray, instruments.ndarray])
845843
q = instruments.ndarray[:, :(ninstr - nendog)]
@@ -850,7 +848,6 @@ def wooldridge_overid(self):
850848
stat = res.nobs * res.rsquared
851849
df = ninstr - nendog
852850
null = 'Model is not overidentified.'
853-
name = 'Wooldridge\'s score test of overidentification'
854851
return WaldTestStatistic(stat, null, df, name=name)
855852

856853
@cached_property

linearmodels/tests/iv/test_model.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,21 @@ def test_gmm_str(data):
342342
str(mod.fit(cov_type='robust'))
343343
str(mod.fit(cov_type='clustered', clusters=data.clusters))
344344
str(mod.fit(cov_type='kernel'))
345+
346+
347+
def test_gmm_cue_optimization_options(data):
348+
mod = IVGMMCUE(data.dep, data.exog, data.endog, data.instr)
349+
res_none = mod.fit(display=False)
350+
opt_options = dict(method='BFGS', options={'disp': False})
351+
res_bfgs = mod.fit(display=False, opt_options=opt_options)
352+
opt_options = dict(method='L-BFGS-B', options={'disp': False})
353+
res_lbfgsb = mod.fit(display=False, opt_options=opt_options)
354+
assert res_none.iterations > 2
355+
assert res_bfgs.iterations > 2
356+
assert res_lbfgsb.iterations > 2
357+
358+
mod2 = IVGMM(data.dep, data.exog, data.endog, data.instr)
359+
res2 = mod2.fit()
360+
assert res_none.j_stat.stat <= res2.j_stat.stat
361+
assert res_bfgs.j_stat.stat <= res2.j_stat.stat
362+
assert res_lbfgsb.j_stat.stat <= res2.j_stat.stat

linearmodels/tests/panel/test_panel_ols.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def data(request):
3131
@pytest.fixture(params=perms, ids=ids)
3232
def large_data(request):
3333
missing, datatype, const = request.param
34-
return generate_data(missing, datatype, const=const, ntk=(51, 30, 5), other_effects=2)
34+
return generate_data(missing, datatype, const=const, ntk=(51, 71, 5), other_effects=2)
3535

3636

3737
perms = list(product(missing, datatypes))

linearmodels/tests/panel/test_results.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def test_incorrect_type(data):
9191
compare(dict(model1=res, model2=res2))
9292

9393

94+
@pytest.mark.filterwarnings('ignore::linearmodels.utility.MissingValueWarning')
9495
def test_predict(generated_data):
9596
mod = PanelOLS(generated_data.y, generated_data.x, entity_effects=True)
9697
res = mod.fit()
@@ -124,6 +125,7 @@ def test_predict(generated_data):
124125
assert pred.shape == (PanelData(generated_data.y).dataframe.shape[0], 3)
125126

126127

128+
@pytest.mark.filterwarnings('ignore::linearmodels.utility.MissingValueWarning')
127129
def test_predict_no_selection(generated_data):
128130
mod = PanelOLS(generated_data.y, generated_data.x, entity_effects=True)
129131
res = mod.fit()

linearmodels/tests/system/test_sur.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,7 @@ def test_fitted(data):
637637
assert_frame_equal(expected, res.fitted_values)
638638

639639

640+
@pytest.mark.filterwarnings('ignore::linearmodels.utility.MissingValueWarning')
640641
def test_predict(missing_data):
641642
mod = SUR(missing_data)
642643
res = mod.fit()
@@ -674,6 +675,7 @@ def test_predict(missing_data):
674675
assert pred[key].shape[0] == nobs
675676

676677

678+
@pytest.mark.filterwarnings('ignore::linearmodels.utility.MissingValueWarning')
677679
def test_predict_error(missing_data):
678680
mod = SUR(missing_data)
679681
res = mod.fit()

linearmodels/typing/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from typing import Union
2+
3+
Numeric = Union[int, float]
4+
OptionalNumeric = Union[int, float, None]

linearmodels/typing/iv.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Union
2+
3+
import numpy as np
4+
import pandas as pd
5+
6+
base_data_types = [np.ndarray, pd.DataFrame, pd.Series]
7+
try:
8+
import xarray as xr
9+
10+
ArrayLike = Union[np.ndarray, xr.DataArray, pd.DataFrame, pd.Series]
11+
12+
except ImportError:
13+
ArrayLike = Union[np.ndarray, pd.DataFrame, pd.Series]
14+
15+
OptionalArrayLike = Union[ArrayLike, None]

linearmodels/utility.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ def __init__(self, results):
439439
def _get_series_property(self, name):
440440
out = ([(k, getattr(v, name)) for k, v in self._results.items()])
441441
cols = [v[0] for v in out]
442-
values = concat([v[1] for v in out], 1)
442+
values = concat([v[1] for v in out], 1, sort=True)
443443
values.columns = cols
444444
return values
445445

0 commit comments

Comments
 (0)