Repository: statsmodels/statsmodels

Issue:
OLS fitting fails if method='qr' and cov_type='HC0' are used at the same time
#### Describe the bug

In OLS regression, specifying method='qr' and cov_type='HC0' in the fit method results in an error. The error message is:

 AttributeError: 'OLS' object has no attribute 'pinv_wexog'

#### Code Sample, a copy-pastable example if possible
```
import numpy as np
import statsmodels.api as sm


y = np.random.randn(100)

x = np.random.randn(100, 5)

res = sm.OLS(y, x, missing='drop').fit(method='qr', cov_type='HC0')
```


If the issue has not been resolved, please file it in the issue tracker.

#### Expected Output

The above-mentioned error does not occur.

#### Output of ``import statsmodels.api as sm; sm.show_versions()``

<details>

INSTALLED VERSIONS
------------------
Python: 3.9.2.final.0
OS: Linux 5.10.0-10-amd64 #1 SMP Debian 5.10.84-1 (2021-12-08) x86_64
byteorder: little
LC_ALL: None
LANG: en_US.utf8

statsmodels
===========

Installed: 0.12.2 (/usr/lib/python3/dist-packages/statsmodels)

Required Dependencies
=====================

cython: 0.29.21 (/usr/lib/python3/dist-packages/Cython)
numpy: 1.19.5 (/usr/lib/python3/dist-packages/numpy)
scipy: 1.6.0 (/usr/lib/python3/dist-packages/scipy)
pandas: 1.1.5 (/usr/lib/python3/dist-packages/pandas)
    dateutil: 2.8.1 (/usr/lib/python3/dist-packages/dateutil)
patsy: 0.5.1 (/usr/lib/python3/dist-packages/patsy)

Optional Dependencies
=====================

matplotlib: 3.3.4 (/usr/lib/python3/dist-packages/matplotlib)
    backend: TkAgg 
cvxopt: 1.2.5 (/usr/lib/python3/dist-packages/cvxopt)**Note**: As you can see, there are many issues on our GitHub tracker, so it is very possible that your issue has been posted before. Please check first before submitting so that we do not have to handle and close duplicates.

**Note**: Please be sure you are using the latest released version of `statsmodels`, or a recent build of `main`. If your problem has been fixed in an unreleased version, you might be able to use `main` until a new release occurs. 

**Note**: If you are using a released version, have you verified that the bug exists in the main branch of this repository? It helps the limited resources if we know problems exist in the current main branch so that they do not need to check whether the code sample produces a bug in the next release.

joblib: 0.17.0 (/usr/lib/python3/dist-packages/joblib)

Developer Tools
================

IPython: 7.20.0 (/usr/lib/python3/dist-packages/IPython)
    jinja2: 2.11.3 (/usr/lib/python3/dist-packages/jinja2)
sphinx: Not installed
    pygments: 2.7.1 (/usr/lib/python3/dist-packages/pygments)
pytest: 6.0.2 (/usr/lib/python3/dist-packages/pytest)
virtualenv: 20.4.0+ds (/usr/lib/python3/dist-packages/virtualenv)


</details>



Discussion:

Resolving patch:
diff --git a/statsmodels/regression/linear_model.py b/statsmodels/regression/linear_model.py
--- a/statsmodels/regression/linear_model.py
+++ b/statsmodels/regression/linear_model.py
@@ -35,7 +35,7 @@
 from statsmodels.compat.pandas import Appender
 from statsmodels.compat.python import lrange, lzip
 
-from typing import Sequence
+from typing import Literal, Sequence
 import warnings
 
 import numpy as np
@@ -50,6 +50,7 @@
 from statsmodels.tools.decorators import cache_readonly, cache_writable
 from statsmodels.tools.sm_exceptions import InvalidTestWarning, ValueWarning
 from statsmodels.tools.tools import pinv_extended
+from statsmodels.tools.typing import Float64Array
 from statsmodels.tools.validation import bool_like, float_like, string_like
 
 from . import _prediction as pred
@@ -189,6 +190,7 @@ class RegressionModel(base.LikelihoodModel):
     """
     def __init__(self, endog, exog, **kwargs):
         super(RegressionModel, self).__init__(endog, exog, **kwargs)
+        self.pinv_wexog: Float64Array | None = None
         self._data_attr.extend(['pinv_wexog', 'wendog', 'wexog', 'weights'])
 
     def initialize(self):
@@ -250,8 +252,24 @@ def whiten(self, x):
         """
         raise NotImplementedError("Subclasses must implement.")
 
-    def fit(self, method="pinv", cov_type='nonrobust', cov_kwds=None,
-            use_t=None, **kwargs):
+    def fit(
+            self,
+            method: Literal["pinv", "qr"] = "pinv",
+            cov_type: Literal[
+                "nonrobust",
+                "fixed scale",
+                "HC0",
+                "HC1",
+                "HC2",
+                "HC3",
+                "hac-panel",
+                "hac-groupsum",
+                "cluster",
+            ] = "nonrobust",
+            cov_kwds=None,
+            use_t: bool | None = None,
+            **kwargs
+    ):
         """
         Full fit of the model.
 
@@ -327,7 +345,8 @@ def fit(self, method="pinv", cov_type='nonrobust', cov_kwds=None,
                 self.rank = np.linalg.matrix_rank(R)
             else:
                 Q, R = self.exog_Q, self.exog_R
-
+            # Needed for some covariance estimators, see GH #8157
+            self.pinv_wexog = np.linalg.pinv(self.wexog)
             # used in ANOVA
             self.effects = effects = np.dot(Q.T, self.wendog)
             beta = np.linalg.solve(R, effects)


Test patch:
diff --git a/statsmodels/regression/tests/test_robustcov.py b/statsmodels/regression/tests/test_robustcov.py
--- a/statsmodels/regression/tests/test_robustcov.py
+++ b/statsmodels/regression/tests/test_robustcov.py
@@ -6,31 +6,35 @@
 Author: Josef Perktold
 """
 
-import pytest
 import numpy as np
+from numpy.testing import (
+    assert_allclose,
+    assert_equal,
+    assert_raises,
+    assert_warns,
+)
+import pytest
 from scipy import stats
 
-from numpy.testing import (assert_allclose, assert_equal, assert_warns,
-                           assert_raises)
-
-
+from statsmodels.datasets import macrodata
 from statsmodels.regression.linear_model import OLS, WLS
 import statsmodels.stats.sandwich_covariance as sw
-from statsmodels.tools.tools import add_constant
-from statsmodels.datasets import macrodata
 from statsmodels.tools.sm_exceptions import InvalidTestWarning
+from statsmodels.tools.tools import add_constant
+
+from .results import (
+    results_grunfeld_ols_robust_cluster as res2,
+    results_macro_ols_robust as res,
+)
 
-from .results import results_macro_ols_robust as res
-from .results import results_grunfeld_ols_robust_cluster as res2
 # TODO: implement test_hac_simple
 
 
 class CheckOLSRobust(object):
-
     def test_basic(self):
         res1 = self.res1
         res2 = self.res2
-        rtol = getattr(self, 'rtol', 1e-10)
+        rtol = getattr(self, "rtol", 1e-10)
         assert_allclose(res1.params, res2.params, rtol=rtol)
         assert_allclose(self.bse_robust, res2.bse, rtol=rtol)
         assert_allclose(self.cov_robust, res2.cov, rtol=rtol)
@@ -65,8 +69,8 @@ def test_tests(self):  # TODO: break into well-scoped tests
         # F statistic has no ddof correction in large, but uses F distribution (?)
         res1 = self.res1
         res2 = self.res2
-        rtol = getattr(self, 'rtol', 1e-10)
-        rtolh = getattr(self, 'rtolh', 1e-12)
+        rtol = getattr(self, "rtol", 1e-10)
+        rtolh = getattr(self, "rtolh", 1e-12)
         mat = np.eye(len(res1.params))
         tt = res1.t_test(mat, cov_p=self.cov_robust)
         # has 'effect', 'pvalue', 'sd', 'tvalue'
@@ -85,14 +89,14 @@ def test_tests(self):  # TODO: break into well-scoped tests
             #'df_denom', 'df_num', 'fvalue', 'pvalue'
             assert_allclose(ft.fvalue, res2.F, rtol=rtol)
             # f-pvalue is not directly available in Stata results, but is in ivreg2
-            if hasattr(res2, 'Fp'):
+            if hasattr(res2, "Fp"):
                 assert_allclose(ft.pvalue, res2.Fp, rtol=rtol)
         else:
-            if not getattr(self, 'skip_f', False):
-                dof_corr = res1.df_resid * 1. / res1.nobs
+            if not getattr(self, "skip_f", False):
+                dof_corr = res1.df_resid * 1.0 / res1.nobs
                 assert_allclose(ft.fvalue * dof_corr, res2.F, rtol=rtol)
 
-        if hasattr(res2, 'df_r'):
+        if hasattr(res2, "df_r"):
             assert_equal(ft.df_num, res2.df_m)
             assert_equal(ft.df_denom, res2.df_r)
         else:
@@ -114,11 +118,18 @@ def setup(self):
     @classmethod
     def setup_class(cls):
         d2 = macrodata.load_pandas().data
-        g_gdp = 400*np.diff(np.log(d2['realgdp'].values))
-        g_inv = 400*np.diff(np.log(d2['realinv'].values))
-        exogg = add_constant(np.c_[g_gdp, d2['realint'][:-1].values], prepend=False)
+        g_gdp = 400 * np.diff(np.log(d2["realgdp"].values))
+        g_inv = 400 * np.diff(np.log(d2["realinv"].values))
+        exogg = add_constant(
+            np.c_[g_gdp, d2["realint"][:-1].values], prepend=False
+        )
 
-        cls.res1 = res_ols = OLS(g_inv, exogg).fit()
+        cls.res1 = OLS(g_inv, exogg).fit()
+
+    def test_qr_equiv(self):
+        # GH8157
+        res2 = self.res1.model.fit(method="qr")
+        assert_allclose(self.res1.HC0_se, res2.HC0_se)
 
 
 class TestOLSRobust2(TestOLSRobust1):
@@ -133,7 +144,6 @@ def setup(self):
         self.res2 = res.results_ivhc0_small
 
 
-
 class TestOLSRobust3(TestOLSRobust1):
     # compare with ivreg robust   (not small)
 
@@ -152,7 +162,7 @@ class TestOLSRobustHacSmall(TestOLSRobust1):
     def setup(self):
         res_ols = self.res1
         cov1 = sw.cov_hac_simple(res_ols, nlags=4, use_correction=True)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust = se1
         self.cov_robust = cov1
         self.small = True
@@ -160,14 +170,13 @@ def setup(self):
         self.res2 = res.results_ivhac4_small
 
 
-
 class TestOLSRobustHacLarge(TestOLSRobust1):
     # compare with ivreg robust (not small)
 
     def setup(self):
         res_ols = self.res1
         cov1 = sw.cov_hac_simple(res_ols, nlags=4, use_correction=False)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust = se1
         self.cov_robust = cov1
         self.small = False
@@ -179,24 +188,24 @@ class CheckOLSRobustNewMixin(object):
     # This uses the robust covariance as default covariance
 
     def test_compare(self):
-        rtol = getattr(self, 'rtol', 1e-10)
+        rtol = getattr(self, "rtol", 1e-10)
         assert_allclose(self.cov_robust, self.cov_robust2, rtol=rtol)
         assert_allclose(self.bse_robust, self.bse_robust2, rtol=rtol)
 
     def test_fvalue(self):
-        if not getattr(self, 'skip_f', False):
-            rtol = getattr(self, 'rtol', 1e-10)
+        if not getattr(self, "skip_f", False):
+            rtol = getattr(self, "rtol", 1e-10)
             assert_allclose(self.res1.fvalue, self.res2.F, rtol=rtol)
-            if hasattr(self.res2, 'Fp'):
+            if hasattr(self.res2, "Fp"):
                 # only available with ivreg2
                 assert_allclose(self.res1.f_pvalue, self.res2.Fp, rtol=rtol)
         else:
             raise pytest.skip("TODO: document why this test is skipped")
 
     def test_confint(self):
-        rtol = getattr(self, 'rtol', 1e-10)
+        rtol = getattr(self, "rtol", 1e-10)
         ci1 = self.res1.conf_int()
-        ci2 = self.res2.params_table[:,4:6]
+        ci2 = self.res2.params_table[:, 4:6]
         assert_allclose(ci1, ci2, rtol=rtol)
 
         # check critical value
@@ -204,12 +213,11 @@ def test_confint(self):
         crit2 = np.diff(ci1, 1).ravel() / 2 / self.res1.bse
         assert_allclose(crit1, crit2, rtol=12)
 
-
     def test_ttest(self):
         res1 = self.res1
         res2 = self.res2
-        rtol = getattr(self, 'rtol', 1e-10)
-        rtolh = getattr(self, 'rtol', 1e-12)
+        rtol = getattr(self, "rtol", 1e-10)
+        rtolh = getattr(self, "rtol", 1e-12)
 
         mat = np.eye(len(res1.params))
         tt = res1.t_test(mat, cov_p=self.cov_robust)
@@ -220,21 +228,20 @@ def test_ttest(self):
         assert_allclose(tt.tvalue, res2.tvalues, rtol=rtolh)
         assert_allclose(tt.pvalue, res2.pvalues, rtol=5 * rtol)
         ci1 = tt.conf_int()
-        ci2 = self.res2.params_table[:,4:6]
+        ci2 = self.res2.params_table[:, 4:6]
         assert_allclose(ci1, ci2, rtol=rtol)
 
-
     def test_scale(self):
         res1 = self.res1
         res2 = self.res2
         rtol = 1e-5
         # Note we always use df_resid for scale
         # Stata uses nobs or df_resid for rmse, not always available in Stata
-        #assert_allclose(res1.scale, res2.rmse**2 * res2.N / (res2.N - res2.df_m - 1), rtol=rtol)
+        # assert_allclose(res1.scale, res2.rmse**2 * res2.N / (res2.N - res2.df_m - 1), rtol=rtol)
         skip = False
-        if hasattr(res2, 'rss'):
+        if hasattr(res2, "rss"):
             scale = res2.rss / (res2.N - res2.df_m - 1)
-        elif hasattr(res2, 'rmse'):
+        elif hasattr(res2, "rmse"):
             scale = res2.rmse**2
         else:
             skip = True
@@ -246,12 +253,11 @@ def test_scale(self):
         if not skip:
             assert_allclose(res1.scale, scale, rtol=rtol)
 
-        if not res2.vcetype == 'Newey-West':
+        if not res2.vcetype == "Newey-West":
             # no rsquared in Stata
-            r2 = res2.r2 if hasattr(res2, 'r2') else res2.r2c
+            r2 = res2.r2 if hasattr(res2, "r2") else res2.r2c
             assert_allclose(res1.rsquared, r2, rtol=rtol, err_msg=str(skip))
 
-
         # consistency checks, not against Stata
         df_resid = res1.nobs - res1.df_model - 1
         assert_equal(res1.df_resid, df_resid)
@@ -268,7 +274,7 @@ class TestOLSRobust2SmallNew(TestOLSRobust1, CheckOLSRobustNewMixin):
     # compare with ivreg robust small
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('HC1', use_t=True)
+        res_ols = self.res1.get_robustcov_results("HC1", use_t=True)
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
@@ -279,17 +285,18 @@ def setup(self):
         self.res2 = res.results_ivhc0_small
 
     def test_compare(self):
-        #check that we get a warning using the nested compare methods
+        # check that we get a warning using the nested compare methods
         res1 = self.res1
         endog = res1.model.endog
         exog = res1.model.exog[:, [0, 2]]  # drop one variable
         res_ols2 = OLS(endog, exog).fit()
         # results from Stata
-        r_pval =  .0307306938402991
-        r_chi2 =  4.667944083588736
-        r_df =  1
+        r_pval = 0.0307306938402991
+        r_chi2 = 4.667944083588736
+        r_df = 1
         assert_warns(InvalidTestWarning, res1.compare_lr_test, res_ols2)
         import warnings
+
         with warnings.catch_warnings():
             warnings.simplefilter("ignore")
             chi2, pval, df = res1.compare_lr_test(res_ols2)
@@ -298,23 +305,22 @@ def test_compare(self):
         assert_equal(df, r_df)
 
         assert_warns(InvalidTestWarning, res1.compare_f_test, res_ols2)
-        #fva, pval, df = res1.compare_f_test(res_ols2)
-
+        # fva, pval, df = res1.compare_f_test(res_ols2)
 
 
 class TestOLSRobustHACSmallNew(TestOLSRobust1, CheckOLSRobustNewMixin):
     # compare with ivreg robust small
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('HAC', maxlags=4,
-                                                  use_correction=True,
-                                                  use_t=True)
+        res_ols = self.res1.get_robustcov_results(
+            "HAC", maxlags=4, use_correction=True, use_t=True
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
         cov1 = sw.cov_hac_simple(res_ols, nlags=4, use_correction=True)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
@@ -325,7 +331,7 @@ class TestOLSRobust2LargeNew(TestOLSRobust1, CheckOLSRobustNewMixin):
     # compare with ivreg robust small
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('HC0')
+        res_ols = self.res1.get_robustcov_results("HC0")
         res_ols.use_t = False
         self.res3 = self.res1
         self.res1 = res_ols
@@ -353,46 +359,44 @@ def test_confint(self):
 class CheckOLSRobustCluster(CheckOLSRobust):
     # compare with regress robust
 
-
     @classmethod
     def setup_class(cls):
-        #import pandas as pa
+        # import pandas as pa
         from statsmodels.datasets import grunfeld
 
         dtapa = grunfeld.data.load_pandas()
-        #Stata example/data seems to miss last firm
+        # Stata example/data seems to miss last firm
         dtapa_endog = dtapa.endog[:200]
         dtapa_exog = dtapa.exog[:200]
-        exog = add_constant(dtapa_exog[['value', 'capital']], prepend=False)
-        #asserts do not work for pandas
+        exog = add_constant(dtapa_exog[["value", "capital"]], prepend=False)
+        # asserts do not work for pandas
         cls.res1 = OLS(dtapa_endog, exog).fit()
 
-        firm_names, firm_id = np.unique(np.asarray(dtapa_exog[['firm']], 'S20'),
-                                        return_inverse=True)
+        firm_names, firm_id = np.unique(
+            np.asarray(dtapa_exog[["firm"]], "S20"), return_inverse=True
+        )
         cls.groups = firm_id
-        #time indicator in range(max Ti)
-        time = np.asarray(dtapa_exog[['year']])
+        # time indicator in range(max Ti)
+        time = np.asarray(dtapa_exog[["year"]])
         time -= time.min()
         cls.time = np.squeeze(time).astype(int)
         # nw_panel function requires interval bounds
-        cls.tidx = [(i*20, 20*(i+1)) for i in range(10)]
-
+        cls.tidx = [(i * 20, 20 * (i + 1)) for i in range(10)]
 
 
 class TestOLSRobustCluster2(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('cluster',
-                                                  groups=self.groups,
-                                                  use_correction=True,
-                                                  use_t=True)
+        res_ols = self.res1.get_robustcov_results(
+            "cluster", groups=self.groups, use_correction=True, use_t=True
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
         cov1 = sw.cov_cluster(self.res1, self.groups, use_correction=True)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
@@ -402,24 +406,26 @@ def setup(self):
         self.rtolh = 1e-10
 
 
-class TestOLSRobustCluster2Input(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
+class TestOLSRobustCluster2Input(
+    CheckOLSRobustCluster, CheckOLSRobustNewMixin
+):
     # compare with `reg cluster`
 
     def setup(self):
         import pandas as pd
+
         fat_array = self.groups.reshape(-1, 1)
         fat_groups = pd.DataFrame(fat_array)
 
-        res_ols = self.res1.get_robustcov_results('cluster',
-                                                  groups=fat_groups,
-                                                  use_correction=True,
-                                                  use_t=True)
+        res_ols = self.res1.get_robustcov_results(
+            "cluster", groups=fat_groups, use_correction=True, use_t=True
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
         cov1 = sw.cov_cluster(self.res1, self.groups, use_correction=True)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
@@ -431,31 +437,40 @@ def setup(self):
     def test_too_many_groups(self):
         long_groups = self.groups.reshape(-1, 1)
         groups3 = np.hstack((long_groups, long_groups, long_groups))
-        assert_raises(ValueError, self.res1.get_robustcov_results,'cluster',
-                      groups=groups3, use_correction=True, use_t=True)
+        assert_raises(
+            ValueError,
+            self.res1.get_robustcov_results,
+            "cluster",
+            groups=groups3,
+            use_correction=True,
+            use_t=True,
+        )
 
     def test_2way_dataframe(self):
         import pandas as pd
+
         long_groups = self.groups.reshape(-1, 1)
         groups2 = pd.DataFrame(np.hstack((long_groups, long_groups)))
         res = self.res1.get_robustcov_results(
-            'cluster', groups=groups2, use_correction=True, use_t=True)
+            "cluster", groups=groups2, use_correction=True, use_t=True
+        )
+
 
 class TestOLSRobustCluster2Fit(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
     # copy, past uses fit method
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.model.fit(cov_type='cluster',
-                                      cov_kwds=dict(groups=self.groups,
-                                                    use_correction=True,
-                                                    use_t=True))
+        res_ols = self.res1.model.fit(
+            cov_type="cluster",
+            cov_kwds=dict(groups=self.groups, use_correction=True, use_t=True),
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
         cov1 = sw.cov_cluster(self.res1, self.groups, use_correction=True)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
@@ -464,7 +479,6 @@ def setup(self):
         self.rtol = 1e-6
         self.rtolh = 1e-10
 
-
     def test_basic_inference(self):
         res1 = self.res1
         res2 = self.res2
@@ -476,21 +490,25 @@ def test_basic_inference(self):
         assert_allclose(res1.conf_int(), ci, rtol=5e-7, atol=1e-20)
 
 
-class TestOLSRobustCluster2Large(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
+class TestOLSRobustCluster2Large(
+    CheckOLSRobustCluster, CheckOLSRobustNewMixin
+):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('cluster',
-                                                  groups=self.groups,
-                                                  use_correction=False,
-                                                  use_t=False,
-                                                  df_correction=True)
+        res_ols = self.res1.get_robustcov_results(
+            "cluster",
+            groups=self.groups,
+            use_correction=False,
+            use_t=False,
+            df_correction=True,
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
         cov1 = sw.cov_cluster(self.res1, self.groups, use_correction=False)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = False
@@ -505,23 +523,29 @@ def test_f_value(self):
         super(TestOLSRobustCluster2Large, self).test_fvalue()
 
 
-class TestOLSRobustCluster2LargeFit(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
+class TestOLSRobustCluster2LargeFit(
+    CheckOLSRobustCluster, CheckOLSRobustNewMixin
+):
     # compare with `reg cluster`
 
     def setup(self):
         model = OLS(self.res1.model.endog, self.res1.model.exog)
-        #res_ols = self.res1.model.fit(cov_type='cluster',
-        res_ols = model.fit(cov_type='cluster',
-                            cov_kwds=dict(groups=self.groups,
-                                          use_correction=False,
-                                          use_t=False,
-                                          df_correction=True))
+        # res_ols = self.res1.model.fit(cov_type='cluster',
+        res_ols = model.fit(
+            cov_type="cluster",
+            cov_kwds=dict(
+                groups=self.groups,
+                use_correction=False,
+                use_t=False,
+                df_correction=True,
+            ),
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
         cov1 = sw.cov_cluster(self.res1, self.groups, use_correction=False)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = False
@@ -540,17 +564,21 @@ class TestOLSRobustClusterGS(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('nw-groupsum',
-                                                  time=self.time,
-                                                  maxlags=4,
-                                                  use_correction=False,
-                                                  use_t=True)
+        res_ols = self.res1.get_robustcov_results(
+            "nw-groupsum",
+            time=self.time,
+            maxlags=4,
+            use_correction=False,
+            use_t=True,
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
-        cov1 = sw.cov_nw_groupsum(self.res1, 4, self.time, use_correction=False)
-        se1 =  sw.se_cov(cov1)
+        cov1 = sw.cov_nw_groupsum(
+            self.res1, 4, self.time, use_correction=False
+        )
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
@@ -565,17 +593,20 @@ class TestOLSRobustClusterGSFit(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.model.fit(cov_type='nw-groupsum',
-                                      cov_kwds=dict(time=self.time,
-                                                    maxlags=4,
-                                                    use_correction=False,
-                                                    use_t=True))
+        res_ols = self.res1.model.fit(
+            cov_type="nw-groupsum",
+            cov_kwds=dict(
+                time=self.time, maxlags=4, use_correction=False, use_t=True
+            ),
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
-        cov1 = sw.cov_nw_groupsum(self.res1, 4, self.time, use_correction=False)
-        se1 =  sw.se_cov(cov1)
+        cov1 = sw.cov_nw_groupsum(
+            self.res1, 4, self.time, use_correction=False
+        )
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
@@ -590,18 +621,20 @@ class TestOLSRobustClusterNWP(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('nw-panel',
-                                                  time=self.time,
-                                                  maxlags=4,
-                                                  use_correction='hac',
-                                                  use_t=True,
-                                                  df_correction=False)
+        res_ols = self.res1.get_robustcov_results(
+            "nw-panel",
+            time=self.time,
+            maxlags=4,
+            use_correction="hac",
+            use_t=True,
+            df_correction=False,
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
         cov1 = sw.cov_nw_panel(self.res1, 4, self.tidx)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
@@ -611,34 +644,41 @@ def setup(self):
         self.rtol = 1e-6
         self.rtolh = 1e-10
 
-
     def test_keyword(self):
         # check corrected keyword
-        res_ols = self.res1.get_robustcov_results('hac-panel',
-                                                  time=self.time,
-                                                  maxlags=4,
-                                                  use_correction='hac',
-                                                  use_t=True,
-                                                  df_correction=False)
+        res_ols = self.res1.get_robustcov_results(
+            "hac-panel",
+            time=self.time,
+            maxlags=4,
+            use_correction="hac",
+            use_t=True,
+            df_correction=False,
+        )
         assert_allclose(res_ols.bse, self.res1.bse, rtol=1e-12)
 
 
-class TestOLSRobustClusterNWPGroupsFit(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
+class TestOLSRobustClusterNWPGroupsFit(
+    CheckOLSRobustCluster, CheckOLSRobustNewMixin
+):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.model.fit(cov_type='nw-panel',
-                                      cov_kwds = dict(groups=self.groups,
-                                                      maxlags=4,
-                                                      use_correction='hac',
-                                                      use_t=True,
-                                                      df_correction=False))
+        res_ols = self.res1.model.fit(
+            cov_type="nw-panel",
+            cov_kwds=dict(
+                groups=self.groups,
+                maxlags=4,
+                use_correction="hac",
+                use_t=True,
+                df_correction=False,
+            ),
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
         cov1 = sw.cov_nw_panel(self.res1, 4, self.tidx)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
@@ -654,42 +694,51 @@ class TestOLSRobustCluster2G(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('cluster',
-                                                  groups=(self.groups, self.time),
-                                                  use_correction=True,
-                                                  use_t=True)
+        res_ols = self.res1.get_robustcov_results(
+            "cluster",
+            groups=(self.groups, self.time),
+            use_correction=True,
+            use_t=True,
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
-        cov1 = sw.cov_cluster_2groups(self.res1, self.groups, group2=self.time,
-                                      use_correction=True)[0]
-        se1 =  sw.se_cov(cov1)
+        cov1 = sw.cov_cluster_2groups(
+            self.res1, self.groups, group2=self.time, use_correction=True
+        )[0]
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
         self.res2 = res2.results_cluster_2groups_small
 
-        self.rtol = 0.35 # only f_pvalue and confint for constant differ >rtol=0.05
+        self.rtol = (
+            0.35  # only f_pvalue and confint for constant differ >rtol=0.05
+        )
         self.rtolh = 1e-10
 
 
-
-class TestOLSRobustCluster2GLarge(CheckOLSRobustCluster, CheckOLSRobustNewMixin):
+class TestOLSRobustCluster2GLarge(
+    CheckOLSRobustCluster, CheckOLSRobustNewMixin
+):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('cluster',
-                                                  groups=(self.groups, self.time),
-                                                  use_correction=False, #True,
-                                                  use_t=False)
+        res_ols = self.res1.get_robustcov_results(
+            "cluster",
+            groups=(self.groups, self.time),
+            use_correction=False,  # True,
+            use_t=False,
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
-        cov1 = sw.cov_cluster_2groups(self.res1, self.groups, group2=self.time,
-                                      use_correction=False)[0]
-        se1 =  sw.se_cov(cov1)
+        cov1 = sw.cov_cluster_2groups(
+            self.res1, self.groups, group2=self.time, use_correction=False
+        )[0]
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = False
@@ -704,32 +753,35 @@ def setup(self):
 #                 WLS
 ######################################
 
+
 class CheckWLSRobustCluster(CheckOLSRobust):
     # compare with regress robust
 
-
     @classmethod
     def setup_class(cls):
-        #import pandas as pa
+        # import pandas as pa
         from statsmodels.datasets import grunfeld
 
         dtapa = grunfeld.data.load_pandas()
-        #Stata example/data seems to miss last firm
+        # Stata example/data seems to miss last firm
         dtapa_endog = dtapa.endog[:200]
         dtapa_exog = dtapa.exog[:200]
-        exog = add_constant(dtapa_exog[['value', 'capital']], prepend=False)
-        #asserts do not work for pandas
-        cls.res1 = WLS(dtapa_endog, exog, weights=1/dtapa_exog['value']).fit()
+        exog = add_constant(dtapa_exog[["value", "capital"]], prepend=False)
+        # asserts do not work for pandas
+        cls.res1 = WLS(
+            dtapa_endog, exog, weights=1 / dtapa_exog["value"]
+        ).fit()
 
-        firm_names, firm_id = np.unique(np.asarray(dtapa_exog[['firm']], 'S20'),
-                                        return_inverse=True)
+        firm_names, firm_id = np.unique(
+            np.asarray(dtapa_exog[["firm"]], "S20"), return_inverse=True
+        )
         cls.groups = firm_id
-        #time indicator in range(max Ti)
-        time = np.asarray(dtapa_exog[['year']])
+        # time indicator in range(max Ti)
+        time = np.asarray(dtapa_exog[["year"]])
         time -= time.min()
         cls.time = np.squeeze(time).astype(int)
         # nw_panel function requires interval bounds
-        cls.tidx = [(i*20, 20*(i+1)) for i in range(10)]
+        cls.tidx = [(i * 20, 20 * (i + 1)) for i in range(10)]
 
 
 # not available yet for WLS
@@ -737,16 +789,15 @@ class TestWLSRobustCluster2(CheckWLSRobustCluster, CheckOLSRobustNewMixin):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('cluster',
-                                                  groups=self.groups,
-                                                  use_correction=True,
-                                                  use_t=True)
+        res_ols = self.res1.get_robustcov_results(
+            "cluster", groups=self.groups, use_correction=True, use_t=True
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
         cov1 = sw.cov_cluster(self.res1, self.groups, use_correction=True)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
@@ -757,21 +808,25 @@ def setup(self):
 
 
 # not available yet for WLS
-class TestWLSRobustCluster2Large(CheckWLSRobustCluster, CheckOLSRobustNewMixin):
+class TestWLSRobustCluster2Large(
+    CheckWLSRobustCluster, CheckOLSRobustNewMixin
+):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('cluster',
-                                                  groups=self.groups,
-                                                  use_correction=False,
-                                                  use_t=False,
-                                                  df_correction=True)
+        res_ols = self.res1.get_robustcov_results(
+            "cluster",
+            groups=self.groups,
+            use_correction=False,
+            use_t=False,
+            df_correction=True,
+        )
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
         cov1 = sw.cov_cluster(self.res1, self.groups, use_correction=False)
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = False
@@ -781,20 +836,20 @@ def setup(self):
         self.rtol = 1e-6
         self.rtolh = 1e-10
 
+
 class TestWLSRobustSmall(CheckWLSRobustCluster, CheckOLSRobustNewMixin):
     # compare with `reg cluster`
 
     def setup(self):
-        res_ols = self.res1.get_robustcov_results('HC1',
-                                                  use_t=True)
+        res_ols = self.res1.get_robustcov_results("HC1", use_t=True)
         self.res3 = self.res1
         self.res1 = res_ols
         self.bse_robust = res_ols.bse
         self.cov_robust = res_ols.cov_params()
-        #TODO: check standalone function
-        #cov1 = sw.cov_cluster(self.res1, self.groups, use_correction=False)
+        # TODO: check standalone function
+        # cov1 = sw.cov_cluster(self.res1, self.groups, use_correction=False)
         cov1 = res_ols.cov_HC1
-        se1 =  sw.se_cov(cov1)
+        se1 = sw.se_cov(cov1)
         self.bse_robust2 = se1
         self.cov_robust2 = cov1
         self.small = True
@@ -806,7 +861,6 @@ def setup(self):
 
 
 class TestWLSOLSRobustSmall(object):
-
     @classmethod
     def setup_class(cls):
         # import pandas as pa
@@ -816,29 +870,32 @@ def setup_class(cls):
         # Stata example/data seems to miss last firm
         dtapa_endog = dtapa.endog[:200]
         dtapa_exog = dtapa.exog[:200]
-        exog = add_constant(dtapa_exog[['value', 'capital']], prepend=False)
+        exog = add_constant(dtapa_exog[["value", "capital"]], prepend=False)
         # asserts do not work for pandas
-        cls.res_wls = WLS(dtapa_endog, exog,
-                          weights=1/dtapa_exog['value']).fit()
-        w_sqrt = 1 / np.sqrt(np.asarray(dtapa_exog['value']))
-        cls.res_ols = OLS(dtapa_endog * w_sqrt,
-                          np.asarray(exog) * w_sqrt[:, None]).fit()
-        ids = np.asarray(dtapa_exog[['firm']], 'S20')
+        cls.res_wls = WLS(
+            dtapa_endog, exog, weights=1 / dtapa_exog["value"]
+        ).fit()
+        w_sqrt = 1 / np.sqrt(np.asarray(dtapa_exog["value"]))
+        cls.res_ols = OLS(
+            dtapa_endog * w_sqrt, np.asarray(exog) * w_sqrt[:, None]
+        ).fit()
+        ids = np.asarray(dtapa_exog[["firm"]], "S20")
         firm_names, firm_id = np.unique(ids, return_inverse=True)
         cls.groups = firm_id
         # time indicator in range(max Ti)
-        time = np.asarray(dtapa_exog[['year']])
+        time = np.asarray(dtapa_exog[["year"]])
         time -= time.min()
         cls.time = np.squeeze(time).astype(int)
         # nw_panel function requires interval bounds
-        cls.tidx = [(i*20, 20*(i+1)) for i in range(10)]
-
+        cls.tidx = [(i * 20, 20 * (i + 1)) for i in range(10)]
 
     def test_all(self):
-        all_cov = [('HC0', dict(use_t=True)),
-                   ('HC1', dict(use_t=True)),
-                   ('HC2', dict(use_t=True)),
-                   ('HC3', dict(use_t=True))]
+        all_cov = [
+            ("HC0", dict(use_t=True)),
+            ("HC1", dict(use_t=True)),
+            ("HC2", dict(use_t=True)),
+            ("HC3", dict(use_t=True)),
+        ]
 
         for cov_type, kwds in all_cov:
             res1 = self.res_ols.get_robustcov_results(cov_type, **kwds)
@@ -854,7 +911,7 @@ def test_all(self):
             assert_allclose(ft1.pvalue, ft2.pvalue, rtol=5e-11)
 
     def test_fixed_scale(self):
-        cov_type = 'fixed_scale'
+        cov_type = "fixed_scale"
         kwds = {}
         res1 = self.res_ols.get_robustcov_results(cov_type, **kwds)
         res2 = self.res_wls.get_robustcov_results(cov_type, **kwds)
@@ -863,10 +920,12 @@ def test_fixed_scale(self):
         assert_allclose(res1.bse, res2.bse, rtol=1e-13)
         assert_allclose(res1.pvalues, res2.pvalues, rtol=1e-12)
 
-        tt = res2.t_test(np.eye(len(res2.params)),
-                         cov_p=res2.normalized_cov_params)
-        assert_allclose(res2.cov_params(), res2.normalized_cov_params,
-                        rtol=1e-13)
+        tt = res2.t_test(
+            np.eye(len(res2.params)), cov_p=res2.normalized_cov_params
+        )
+        assert_allclose(
+            res2.cov_params(), res2.normalized_cov_params, rtol=1e-13
+        )
         assert_allclose(res2.bse, tt.sd, rtol=1e-13)
         assert_allclose(res2.pvalues, tt.pvalue, rtol=1e-13)
         assert_allclose(res2.tvalues, tt.tvalue, rtol=1e-13)
@@ -875,10 +934,12 @@ def test_fixed_scale(self):
         mod = self.res_wls.model
         mod3 = WLS(mod.endog, mod.exog, weights=mod.weights)
         res3 = mod3.fit(cov_type=cov_type, cov_kwds=kwds)
-        tt = res3.t_test(np.eye(len(res3.params)),
-                         cov_p=res3.normalized_cov_params)
-        assert_allclose(res3.cov_params(), res3.normalized_cov_params,
-                        rtol=1e-13)
+        tt = res3.t_test(
+            np.eye(len(res3.params)), cov_p=res3.normalized_cov_params
+        )
+        assert_allclose(
+            res3.cov_params(), res3.normalized_cov_params, rtol=1e-13
+        )
         assert_allclose(res3.bse, tt.sd, rtol=1e-13)
         assert_allclose(res3.pvalues, tt.pvalue, rtol=1e-13)
         assert_allclose(res3.tvalues, tt.tvalue, rtol=1e-13)
@@ -891,7 +952,7 @@ def test_cov_type_fixed_scale():
     sigma = np.array([1, 2, 1, 2, 1, 2])
 
     xdata = np.column_stack((xdata, np.ones(len(xdata))))
-    weights = 1. / sigma**2
+    weights = 1.0 / sigma**2
 
     res = WLS(ydata, xdata, weights=weights).fit()
     assert_allclose(res.bse, [0.20659803, 0.57204404], rtol=1e-3)
@@ -899,12 +960,37 @@ def test_cov_type_fixed_scale():
     res = WLS(ydata, xdata, weights=weights).fit()
     assert_allclose(res.bse, [0.20659803, 0.57204404], rtol=1e-3)
 
-    res = WLS(ydata, xdata, weights=weights).fit(cov_type='fixed scale')
+    res = WLS(ydata, xdata, weights=weights).fit(cov_type="fixed scale")
     assert_allclose(res.bse, [0.30714756, 0.85045308], rtol=1e-3)
 
-    res = WLS(ydata, xdata, weights=weights / 9.).fit(cov_type='fixed scale')
-    assert_allclose(res.bse, [3*0.30714756, 3*0.85045308], rtol=1e-3)
-
-    res = WLS(ydata, xdata, weights=weights).fit(cov_type='fixed scale',
-                                                 cov_kwds={'scale':9})
-    assert_allclose(res.bse, [3*0.30714756, 3*0.85045308], rtol=1e-3)
+    res = WLS(ydata, xdata, weights=weights / 9.0).fit(cov_type="fixed scale")
+    assert_allclose(res.bse, [3 * 0.30714756, 3 * 0.85045308], rtol=1e-3)
+
+    res = WLS(ydata, xdata, weights=weights).fit(
+        cov_type="fixed scale", cov_kwds={"scale": 9}
+    )
+    assert_allclose(res.bse, [3 * 0.30714756, 3 * 0.85045308], rtol=1e-3)
+
+
+@pytest.mark.parametrize(
+    "cov_info",
+    [
+        ("nonrobust", {}),
+        ("HC0", {}),
+        ("HC1", {}),
+        ("HC2", {}),
+        ("HC3", {}),
+        ("HAC", {"maxlags": 7}),
+        ("cluster", {"groups": (np.arange(500) % 27)}),
+    ],
+)
+def test_qr_equiv(cov_info):
+    cov_type, cov_kwds = cov_info
+    rs = np.random.RandomState(123498)
+    x = rs.standard_normal((500, 3))
+    b = np.ones(3)
+    y = x @ b + rs.standard_normal(500)
+    mod = OLS(y, x)
+    pinv_fit = mod.fit(cov_type=cov_type, cov_kwds=cov_kwds)
+    qr_fit = mod.fit(cov_type=cov_type, cov_kwds=cov_kwds, method="qr")
+    assert_allclose(pinv_fit.bse, qr_fit.bse)