Skip to content

Commit 2d81262

Browse files
authored
Merge pull request #339 from bashtage/sur-breusch-pagan
ENH: Add Breusch-Pagan test
2 parents 74ddb9a + 8a5ae3c commit 2d81262

File tree

3 files changed

+75
-3
lines changed

3 files changed

+75
-3
lines changed

linearmodels/system/results.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import linearmodels
1313
from linearmodels.shared.base import _SummaryStr
14-
from linearmodels.shared.hypotheses import WaldTestStatistic
14+
from linearmodels.shared.hypotheses import InvalidTestStatistic, WaldTestStatistic
1515
from linearmodels.shared.io import _str, format_wide, param_table, pval_format
1616
from linearmodels.shared.utility import AttrDict
1717
from linearmodels.typing import ArrayLike, NDArray
@@ -516,6 +516,51 @@ def summary(self) -> Summary:
516516

517517
return smry
518518

519+
def breusch_pagan(self) -> Union[WaldTestStatistic, InvalidTestStatistic]:
520+
r"""
521+
Breusch-Pagan LM test for no cross-correlation
522+
523+
Returns
524+
-------
525+
WaldTestStatistic
526+
Test statistic for null all correlationsare zero.
527+
528+
Notes
529+
-----
530+
The null hypothesis is that the shock correlations are all 0, and
531+
so there are no gains to using GLS estimation in the system estimator.
532+
When the null is rejected, there should be efficiency gains to using
533+
GLS as long the regressors are not common to all models.
534+
535+
The Breusch-Pagan test statistic is defined as
536+
537+
.. math::
538+
539+
LM = n \sum_{i=1}^k \sum_{j=i+1}^k \hat{\rho}_{ij}^2
540+
541+
where :math:`\hat{\rho}_{ij}` is the sample residual correlation
542+
between series i and j. n is the sample size. It has an asymptotic
543+
:math:`\chi^2_{k(k-1)/2}` distribution.
544+
"""
545+
name = "Breusch-Pagan LM Test"
546+
resids = self.resids
547+
if resids.shape[1] == 1:
548+
return InvalidTestStatistic(
549+
"Cannot test correlation when the system contains a single "
550+
"dependent variable.",
551+
name=name,
552+
)
553+
r = np.corrcoef(resids.T)
554+
k = r.shape[0]
555+
distinct_corr = np.tril(r, -1)
556+
stat = self.resids.shape[0] * (distinct_corr ** 2).sum()
557+
return WaldTestStatistic(
558+
stat,
559+
"Residuals are uncorrelated",
560+
k * (k - 1) // 2,
561+
name=name,
562+
)
563+
519564

520565
class SystemEquationResult(_CommonResults):
521566
"""

linearmodels/tests/panel/test_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import pickle
33

44
import numpy as np
5-
from numpy.testing import assert_allclose, assert_equal
5+
from numpy.testing import assert_allclose
66
import pandas as pd
77
import pytest
88

linearmodels/tests/system/test_sur.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
from pandas import DataFrame, Series, concat
88
from pandas.testing import assert_frame_equal, assert_series_equal
99
import pytest
10+
import scipy.stats
1011

1112
from linearmodels.iv.model import _OLS as OLS
12-
from linearmodels.shared.hypotheses import InvalidTestStatistic
13+
from linearmodels.shared.hypotheses import InvalidTestStatistic, WaldTestStatistic
1314
from linearmodels.shared.utility import AttrDict
1415
from linearmodels.system._utility import (
1516
blocked_column_product,
@@ -851,3 +852,29 @@ def test_tvalues_homogeneity(method, cov_type):
851852
if cov_type == "robust" and method == "gls":
852853
assert_allclose(res0.tstats, base_tstat)
853854
assert_allclose(res1.tstats, base_100_tstat)
855+
856+
857+
@pytest.mark.parametrize("k", [1, 3])
858+
def test_brequsch_pagan(k):
859+
eqns = generate_data(k=k)
860+
mod = SUR(eqns)
861+
res = mod.fit()
862+
stat = res.breusch_pagan()
863+
if k == 1:
864+
assert isinstance(stat, InvalidTestStatistic)
865+
assert "Breusch-Pagan" in str(stat)
866+
assert np.isnan(stat.stat)
867+
return
868+
rho = np.asarray(res.resids.corr())
869+
nobs = res.resids.shape[0]
870+
direct = 0.0
871+
for i in range(3):
872+
for j in range(i + 1, 3):
873+
direct += rho[i, j] ** 2
874+
direct *= nobs
875+
assert isinstance(stat, WaldTestStatistic)
876+
assert_allclose(stat.stat, direct)
877+
assert stat.df == 3
878+
assert_allclose(stat.pval, 1.0 - scipy.stats.chi2(3).cdf(direct))
879+
assert "Residuals are uncorrelated" in stat.null
880+
assert "Breusch-Pagan" in str(stat)

0 commit comments

Comments
 (0)