Skip to content

Commit

Permalink
ENH: Add wald_test to panel model results
Browse files Browse the repository at this point in the history
Add wald_test as a convenience function for panel models
  • Loading branch information
bashtage committed Dec 19, 2018
1 parent b2b6eaa commit 22e7b3f
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 24 deletions.
32 changes: 10 additions & 22 deletions linearmodels/iv/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@

import scipy.stats as stats
from cached_property import cached_property
from numpy import array, asarray, c_, diag, empty, log, ones, sqrt, zeros
from numpy import array, c_, diag, empty, log, ones, sqrt, zeros
from numpy.linalg import inv, pinv
from pandas import DataFrame, Series, concat, to_numeric
from patsy import DesignInfo
from statsmodels.iolib.summary import SimpleTable, fmt_2cols, fmt_params
from statsmodels.iolib.table import default_txt_fmt

from linearmodels.compat.statsmodels import Summary
from linearmodels.iv._utility import annihilate, proj
from linearmodels.utility import (InvalidTestStatistic, WaldTestStatistic, _ModelComparison,
_SummaryStr, _str, pval_format)
_SummaryStr, _str, pval_format, quadratic_form_test)


def stub_concat(lists, sep='='):
Expand Down Expand Up @@ -411,7 +410,7 @@ def summary(self):

return smry

def test_linear_constraint(self, restriction=None, value=None, *, formula=None):
def wald_test(self, restriction=None, value=None, *, formula=None):
r"""
Test linear equality constraints using a Wald test
Expand Down Expand Up @@ -461,25 +460,14 @@ def test_linear_constraint(self, restriction=None, value=None, *, formula=None):
>>> formula = 'exper = I(exper**2) = 0'
>>> res.test_linear_constraint(formula=formula)
"""
if formula is not None and restriction is not None:
raise ValueError('restriction and formula cannot be used'
'simultaneously.')
if formula is not None:
di = DesignInfo(list(self.params.index))
lc = di.linear_constraint(formula)
restriction, value = lc.coefs, lc.constants
restriction = asarray(restriction)
if value is None:
value = zeros(restriction.shape[0])
value = asarray(value).ravel()[:, None]
diff = restriction @ self.params.values[:, None] - value
rcov = restriction @ self.cov @ restriction.T
stat = float(diff.T @ inv(rcov) @ diff)
df = restriction.shape[0]
null = 'Linear equality constraint is valid'
name = 'Linear Equality Hypothesis Test'
return quadratic_form_test(self._params, self.cov, restriction=restriction,
value=value, formula=formula)

return WaldTestStatistic(stat, null, df, name=name)
def test_linear_constraint(self, restriction=None, value=None, *, formula=None):
import warnings
warnings.warn('test_linear_constraint is deprecated. Use wald_test '
'instead.', DeprecationWarning)
return self.wald_test(restriction, value, formula=formula)


class _CommonIVResults(OLSResults):
Expand Down
70 changes: 69 additions & 1 deletion linearmodels/panel/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

from linearmodels.compat.statsmodels import Summary
from linearmodels.iv.results import default_txt_fmt, stub_concat, table_concat
from linearmodels.utility import (_ModelComparison, _SummaryStr, _str, pval_format)
from linearmodels.utility import (_ModelComparison, _SummaryStr, _str, pval_format,
quadratic_form_test)

__all__ = ['PanelResults', 'PanelEffectsResults', 'RandomEffectsResults']

Expand Down Expand Up @@ -513,6 +514,73 @@ def loglik(self):
"""Log-likelihood of model"""
return self._loglik

def wald_test(self, restriction=None, value=None, *, formula=None):
r"""
Test linear equality constraints using a Wald test
Parameters
----------
restriction : {ndarray, DataFrame}, optional
q by nvar array containing linear weights to apply to parameters
when forming the restrictions. It is not possible to use both
restriction and formula.
value : {ndarray, Series}, optional
q element array containing the restricted values.
formula : Union[str, list[str]], optional
patsy linear constrains. The simplest formats are one of:
* A single comma-separated string such as 'x1=0, x2+x3=1'
* A list of strings where each element is a single constraint
such as ['x1=0', 'x2+x3=1']
* A single string without commas to test simple constraints such
as 'x1=x2=x3=0'
It is not possible to use both restriction and formula.
Returns
-------
t: WaldTestStatistic
Test statistic for null that restrictions are valid.
Notes
-----
Hypothesis test examines whether :math:`H_0:C\theta=v` where the
matrix C is ``restriction`` and v is ``value``. The test statistic
has a :math:`\chi^2_q` distribution where q is the number of rows in C.
Examples
--------
>>> from linearmodels.datasets import wage_panel
>>> import statsmodels.api as sm
>>> import numpy as np
>>> import pandas as pd
>>> data = wage_panel.load()
>>> year = pd.Categorical(data.year)
>>> data = data.set_index(['nr', 'year'])
>>> data['year'] = year
>>> from linearmodels.panel import PanelOLS
>>> exog_vars = ['expersq', 'union', 'married', 'year']
>>> exog = sm.add_constant(data[exog_vars])
>>> mod = PanelOLS(data.lwage, exog, entity_effects=True)
>>> fe_res = mod.fit()
Test the restriction that union and married have 0 coefficients
>>> restriction = np.zeros((2, 11))
>>> restriction[0, 2] = 1
>>> restriction[1, 3] = 1
>>> value = np.array([0, 0])
>>> fe_res.wald_test(restriction, value)
The same test using formulas
>>> formula = 'union = married = 0'
>>> fe_res.wald_test(formula=formula)
"""
return quadratic_form_test(self.params, self.cov, restriction=restriction,
value=value, formula=formula)


class PanelEffectsResults(PanelResults):
"""
Expand Down
6 changes: 5 additions & 1 deletion linearmodels/tests/iv/test_postestimation.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,13 @@ def test_linear_restriction(data):
res = IV2SLS(data.dep, data.exog, data.endog, data.instr).fit(cov_type='robust')
nvar = len(res.params)
q = np.eye(nvar)
ts = res.test_linear_constraint(q, np.zeros(nvar))
ts = res.wald_test(q, np.zeros(nvar))
p = res.params.values[:, None]
c = res.cov.values
stat = float(p.T @ np.linalg.inv(c) @ p)
assert_allclose(stat, ts.stat)
assert ts.df == nvar

formula = ' = '.join(res.params.index) + ' = 0'
ts2 = res.wald_test(formula=formula)
assert_allclose(ts.stat, ts2.stat)
23 changes: 23 additions & 0 deletions linearmodels/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import numpy as np
from pandas import DataFrame, Series, MultiIndex
from patsy.design_info import DesignInfo
from scipy.stats import chi2, f
from statsmodels.iolib.summary import SimpleTable, fmt_params

Expand Down Expand Up @@ -575,3 +576,25 @@ def panel_to_frame(x, items, major_axis, minor_axis, swap=False):
df.index.set_levels(final_levels, [0, 1], inplace=True)
df.index.names = ['major', 'minor']
return df


def quadratic_form_test(params, cov, restriction=None, value=None, formula=None):
if formula is not None and restriction is not None:
raise ValueError('restriction and formula cannot be used'
'simultaneously.')
if formula is not None:
di = DesignInfo(list(params.index))
lc = di.linear_constraint(formula)
restriction, value = lc.coefs, lc.constants
restriction = np.asarray(restriction)
if value is None:
value = np.zeros(restriction.shape[0])
value = np.asarray(value).ravel()[:, None]
diff = restriction @ params.values[:, None] - value
rcov = restriction @ cov @ restriction.T
stat = float(diff.T @ np.linalg.inv(rcov) @ diff)
df = restriction.shape[0]
null = 'Linear equality constraint is valid'
name = 'Linear Equality Hypothesis Test'

return WaldTestStatistic(stat, null, df, name=name)

0 comments on commit 22e7b3f

Please sign in to comment.