Repository: statsmodels/statsmodels

Issue:
WIP: Oaxaca Variance/Other Models
Hi everyone.

I should have added more ability for the end user to choose what type of non-discriminatory model they would like. It used to be a pooled only model, but now users can pass into two_fold() optionally what type of non-discriminatory model they wish to. I have checked these results with STATA and they are the same. I haven't written tests for them, but I will.

The real issue is that I cannot seem to get the variance/standard error calculations to work. I found what the R and Stata command cite as their method for calculating the variance, but I can't get the same numbers. This is what they cite: http://repec.org/dsug2005/oaxaca_se_handout.pdf. I would appreciate some help to figure out where I am going wrong. Var of the three_fold is much easier than the two_fold, so once the three_fold is figured out, I can move onto the two_fold. The output can be seen in the results by calling the .var on the results class. This calculation is the variance instead of the standard error.

I could also bootstrap the standard errors like the R command does, but it takes a while.


Discussion:

Resolving patch:
diff --git a/statsmodels/stats/oaxaca.py b/statsmodels/stats/oaxaca.py
--- a/statsmodels/stats/oaxaca.py
+++ b/statsmodels/stats/oaxaca.py
@@ -1,6 +1,6 @@
-# TODO Variance can be calculated for the three_fold
-# TODO Group Size Effects can be accounted for
-# TODO Non-Linear Oaxaca-Blinder can be used
+# TODO Non-Linear Regressions can be used
+# TODO Further decomposition of the two_fold parameters i.e.
+# the delta method for further two_fold detail
 """
 Author: Austin Adams
 
@@ -8,7 +8,7 @@
 a OaxacaResults Class:
 
 OaxacaBlinder:
-Two-Fold/Pooled (two_fold)
+Two-Fold (two_fold)
 Three-Fold (three_fold)
 
 OaxacaResults:
@@ -44,10 +44,12 @@
 A. S. Blinder "Wage Discrimination: Reduced Form and Structural
 Estimates," The Journal of Human Resources, 1973.
 """
+from textwrap import dedent
+
+import numpy as np
+
 from statsmodels.regression.linear_model import OLS
 from statsmodels.tools.tools import add_constant
-import numpy as np
-from textwrap import dedent
 
 
 class OaxacaBlinder(object):
@@ -108,21 +110,37 @@ class OaxacaBlinder(object):
 
     >>> model.three_fold().summary()
     Oaxaca-Blinder Three-fold Effects
-    Characteristic Effect: 321.74824
+    Endowments Effect: 321.74824
     Coefficient Effect: 75.45371
     Interaction Effect: -238.45151
     Gap: 158.75044
     """
 
-    def __init__(self, endog, exog, bifurcate, hasconst=True,
-                 swap=True, cov_type='nonrobust', cov_kwds=None):
-        if str(type(exog)).find('pandas') != -1:
+    def __init__(
+        self,
+        endog,
+        exog,
+        bifurcate,
+        hasconst=True,
+        swap=True,
+        cov_type="nonrobust",
+        cov_kwds=None,
+    ):
+        if str(type(exog)).find("pandas") != -1:
             bifurcate = exog.columns.get_loc(bifurcate)
             endog, exog = np.array(endog), np.array(exog)
 
+        self.two_fold_type = None
+        self.bifurcate = bifurcate
+        self.cov_type = cov_type
+        self.cov_kwds = cov_kwds
+        self.neumark = np.delete(exog, bifurcate, axis=1)
+        self.exog = exog
+        self.hasconst = hasconst
         bi_col = exog[:, bifurcate]
         endog = np.column_stack((bi_col, endog))
         bi = np.unique(bi_col)
+        self.bi_col = bi_col
 
         # split the data along the bifurcate axis, the issue is you need to
         # delete it after you fit the model for the total model.
@@ -134,72 +152,312 @@ def __init__(self, endog, exog, bifurcate, hasconst=True,
         exog_s = np.delete(exog_s, bifurcate, axis=1)
         endog_f = endog_f[:, 1]
         endog_s = endog_s[:, 1]
-        endog = endog[:, 1]
+        self.endog = endog[:, 1]
 
+        self.len_f, self.len_s = len(endog_f), len(endog_s)
         self.gap = endog_f.mean() - endog_s.mean()
 
         if swap and self.gap < 0:
             endog_f, endog_s = endog_s, endog_f
             exog_f, exog_s = exog_s, exog_f
             self.gap = endog_f.mean() - endog_s.mean()
+            bi[0], bi[1] = bi[1], bi[0]
+
+        self.bi = bi
 
         if hasconst is False:
             exog_f = add_constant(exog_f, prepend=False)
             exog_s = add_constant(exog_s, prepend=False)
-            exog = add_constant(exog, prepend=False)
-
-        self._t_model = OLS(endog, exog).fit(
-                                            cov_type=cov_type,
-                                            cov_kwds=cov_kwds)
-        self._f_model = OLS(endog_f, exog_f).fit(
-                                                cov_type=cov_type,
-                                                cov_kwds=cov_kwds)
-        self._s_model = OLS(endog_s, exog_s).fit(
-                                                cov_type=cov_type,
-                                                cov_kwds=cov_kwds)
+            self.exog = add_constant(self.exog, prepend=False)
+            self.neumark = add_constant(self.neumark, prepend=False)
 
         self.exog_f_mean = np.mean(exog_f, axis=0)
         self.exog_s_mean = np.mean(exog_s, axis=0)
-        self.t_params = np.delete(self._t_model.params, bifurcate)
 
-    def three_fold(self):
+        self._f_model = OLS(endog_f, exog_f).fit(
+            cov_type=cov_type, cov_kwds=cov_kwds
+        )
+        self._s_model = OLS(endog_s, exog_s).fit(
+            cov_type=cov_type, cov_kwds=cov_kwds
+        )
+
+    def variance(self, decomp_type, n=5000, conf=0.99):
+        """
+        A helper function to calculate the variance/std. Used to keep
+        the decomposition functions cleaner
+        """
+        if self.submitted_n is not None:
+            n = self.submitted_n
+        if self.submitted_conf is not None:
+            conf = self.submitted_conf
+        if self.submitted_weight is not None:
+            submitted_weight = [
+                self.submitted_weight,
+                1 - self.submitted_weight,
+            ]
+        bi = self.bi
+        bifurcate = self.bifurcate
+        endow_eff_list = []
+        coef_eff_list = []
+        int_eff_list = []
+        exp_eff_list = []
+        unexp_eff_list = []
+        for _ in range(0, n):
+            endog = np.column_stack((self.bi_col, self.endog))
+            exog = self.exog
+            amount = len(endog)
+
+            samples = np.random.randint(0, high=amount, size=amount)
+            endog = endog[samples]
+            exog = exog[samples]
+            neumark = np.delete(exog, bifurcate, axis=1)
+
+            exog_f = exog[np.where(exog[:, bifurcate] == bi[0])]
+            exog_s = exog[np.where(exog[:, bifurcate] == bi[1])]
+            endog_f = endog[np.where(endog[:, 0] == bi[0])]
+            endog_s = endog[np.where(endog[:, 0] == bi[1])]
+            exog_f = np.delete(exog_f, bifurcate, axis=1)
+            exog_s = np.delete(exog_s, bifurcate, axis=1)
+            endog_f = endog_f[:, 1]
+            endog_s = endog_s[:, 1]
+            endog = endog[:, 1]
+
+            two_fold_type = self.two_fold_type
+
+            if self.hasconst is False:
+                exog_f = add_constant(exog_f, prepend=False)
+                exog_s = add_constant(exog_s, prepend=False)
+                exog = add_constant(exog, prepend=False)
+                neumark = add_constant(neumark, prepend=False)
+
+            _f_model = OLS(endog_f, exog_f).fit(
+                cov_type=self.cov_type, cov_kwds=self.cov_kwds
+            )
+            _s_model = OLS(endog_s, exog_s).fit(
+                cov_type=self.cov_type, cov_kwds=self.cov_kwds
+            )
+            exog_f_mean = np.mean(exog_f, axis=0)
+            exog_s_mean = np.mean(exog_s, axis=0)
+
+            if decomp_type == 3:
+                endow_eff = (exog_f_mean - exog_s_mean) @ _s_model.params
+                coef_eff = exog_s_mean @ (_f_model.params - _s_model.params)
+                int_eff = (exog_f_mean - exog_s_mean) @ (
+                    _f_model.params - _s_model.params
+                )
+
+                endow_eff_list.append(endow_eff)
+                coef_eff_list.append(coef_eff)
+                int_eff_list.append(int_eff)
+
+            elif decomp_type == 2:
+                len_f = len(exog_f)
+                len_s = len(exog_s)
+
+                if two_fold_type == "cotton":
+                    t_params = (len_f / (len_f + len_s) * _f_model.params) + (
+                        len_s / (len_f + len_s) * _s_model.params
+                    )
+
+                elif two_fold_type == "reimers":
+                    t_params = 0.5 * (_f_model.params + _s_model.params)
+
+                elif two_fold_type == "self_submitted":
+                    t_params = (
+                        submitted_weight[0] * _f_model.params
+                        + submitted_weight[1] * _s_model.params
+                    )
+
+                elif two_fold_type == "nuemark":
+                    _t_model = OLS(endog, neumark).fit(
+                        cov_type=self.cov_type, cov_kwds=self.cov_kwds
+                    )
+                    t_params = _t_model.params
+
+                else:
+                    _t_model = OLS(endog, exog).fit(
+                        cov_type=self.cov_type, cov_kwds=self.cov_kwds
+                    )
+                    t_params = np.delete(_t_model.params, bifurcate)
+
+                unexplained = (exog_f_mean @ (_f_model.params - t_params)) + (
+                    exog_s_mean @ (t_params - _s_model.params)
+                )
+
+                explained = (exog_f_mean - exog_s_mean) @ t_params
+
+                unexp_eff_list.append(unexplained)
+                exp_eff_list.append(explained)
+
+        high, low = int(n * conf), int(n * (1 - conf))
+        if decomp_type == 3:
+            return [
+                np.std(np.sort(endow_eff_list)[low:high]),
+                np.std(np.sort(coef_eff_list)[low:high]),
+                np.std(np.sort(int_eff_list)[low:high]),
+            ]
+        elif decomp_type == 2:
+            return [
+                np.std(np.sort(unexp_eff_list)[low:high]),
+                np.std(np.sort(exp_eff_list)[low:high]),
+            ]
+
+    def three_fold(self, std=False, n=None, conf=None):
         """
         Calculates the three-fold Oaxaca Blinder Decompositions
 
+        Parameters
+        ----------
+        std: boolean, optional
+            If true, bootstrapped standard errors will be calculated.
+        n: int, optional
+            A amount of iterations to calculate the bootstrapped
+            standard errors. This defaults to 5000.
+        conf: float, optional
+            This is the confidence required for the standard error
+            calculation. Defaults to .99, but could be anything less
+            than or equal to one. One is heavy discouraged, due to the
+            extreme outliers inflating the variance.
+
         Returns
         -------
         OaxacaResults
             A results container for the three-fold decomposition.
         """
-
-        self.char_eff = (
-                        (self.exog_f_mean - self.exog_s_mean)
-                        @ self._s_model.params)
-        self.coef_eff = self.exog_s_mean @ (self._f_model.params
-                                            - self._s_model.params)
-        self.int_eff = ((self.exog_f_mean - self.exog_s_mean)
-                        @ (self._f_model.params - self._s_model.params))
+        self.submitted_n = n
+        self.submitted_conf = conf
+        self.submitted_weight = None
+        std_val = None
+        self.endow_eff = (
+            self.exog_f_mean - self.exog_s_mean
+        ) @ self._s_model.params
+        self.coef_eff = self.exog_s_mean @ (
+            self._f_model.params - self._s_model.params
+        )
+        self.int_eff = (self.exog_f_mean - self.exog_s_mean) @ (
+            self._f_model.params - self._s_model.params
+        )
+
+        if std is True:
+            std_val = self.variance(3)
 
         return OaxacaResults(
-                            (self.char_eff, self.coef_eff,
-                                self.int_eff, self.gap), 3)
-
-    def two_fold(self):
+            (self.endow_eff, self.coef_eff, self.int_eff, self.gap),
+            3,
+            std_val=std_val,
+        )
+
+    def two_fold(
+        self,
+        std=False,
+        two_fold_type="pooled",
+        submitted_weight=None,
+        n=None,
+        conf=None,
+    ):
         """
         Calculates the two-fold or pooled Oaxaca Blinder Decompositions
 
+        Methods
+        -------
+        std: boolean, optional
+            If true, bootstrapped standard errors will be calculated.
+
+        two_fold_type: string, optional
+            This method allows for the specific calculation of the
+            non-discriminatory model. There are four different types
+            available at this time. pooled, cotton, reimers, self_submitted.
+            Pooled is assumed and if a non-viable parameter is given,
+            pooled will be ran.
+
+            pooled - This type assumes that the pooled model's parameters
+            (a normal regression) is the non-discriminatory model.
+            This includes the indicator variable. This is generally
+            the best idea. If you have economic justification for
+            using others, then use others.
+
+            nuemark - This is similar to the pooled type, but the regression
+            is not done including the indicator variable.
+
+            cotton - This type uses the adjusted in Cotton (1988), which
+            accounts for the undervaluation of one group causing the
+            overevalution of another. It uses the sample size weights for
+            a linear combination of the two model parameters
+
+            reimers - This type uses a linear combination of the two
+            models with both parameters being 50% of the
+            non-discriminatory model.
+
+            self_submitted - This allows the user to submit their
+            own weights. Please be sure to put the weight of the larger mean
+            group only. This should be submitted in the
+            submitted_weights variable.
+
+        submitted_weight: int/float, required only for self_submitted,
+            This is the submitted weight for the larger mean. If the
+            weight for the larger mean is p, then the weight for the
+            other mean is 1-p. Only submit the first value.
+
+        n: int, optional
+            A amount of iterations to calculate the bootstrapped
+            standard errors. This defaults to 5000.
+        conf: float, optional
+            This is the confidence required for the standard error
+            calculation. Defaults to .99, but could be anything less
+            than or equal to one. One is heavy discouraged, due to the
+            extreme outliers inflating the variance.
+
         Returns
         -------
         OaxacaResults
             A results container for the two-fold decomposition.
         """
-        self.unexplained = ((self.exog_f_mean
-                            @ (self._f_model.params - self.t_params))
-                            + (self.exog_s_mean
-                            @ (self.t_params - self._s_model.params)))
+        self.submitted_n = n
+        self.submitted_conf = conf
+        std_val = None
+        self.two_fold_type = two_fold_type
+        self.submitted_weight = submitted_weight
+
+        if two_fold_type == "cotton":
+            self.t_params = (
+                self.len_f / (self.len_f + self.len_s) * self._f_model.params
+            ) + (self.len_s / (self.len_f + self.len_s) * self._s_model.params)
+
+        elif two_fold_type == "reimers":
+            self.t_params = 0.5 * (self._f_model.params + self._s_model.params)
+
+        elif two_fold_type == "self_submitted":
+            if submitted_weight is None:
+                raise ValueError("Please submit weights")
+            submitted_weight = [submitted_weight, 1 - submitted_weight]
+            self.t_params = (
+                submitted_weight[0] * self._f_model.params
+                + submitted_weight[1] * self._s_model.params
+            )
+
+        elif two_fold_type == "nuemark":
+            self._t_model = OLS(self.endog, self.neumark).fit(
+                cov_type=self.cov_type, cov_kwds=self.cov_kwds
+            )
+            self.t_params = self._t_model.params
+
+        else:
+            self._t_model = OLS(self.endog, self.exog).fit(
+                cov_type=self.cov_type, cov_kwds=self.cov_kwds
+            )
+            self.t_params = np.delete(self._t_model.params, self.bifurcate)
+
+        self.unexplained = (
+            self.exog_f_mean @ (self._f_model.params - self.t_params)
+        ) + (self.exog_s_mean @ (self.t_params - self._s_model.params))
         self.explained = (self.exog_f_mean - self.exog_s_mean) @ self.t_params
 
-        return OaxacaResults((self.unexplained, self.explained, self.gap), 2)
+        if std is True:
+            std_val = self.variance(2)
+
+        return OaxacaResults(
+            (self.unexplained, self.explained, self.gap), 2, std_val=std_val
+        )
 
 
 class OaxacaResults:
@@ -208,11 +466,14 @@ class OaxacaResults:
 
     Use .summary() to get a table of the fitted values or
     use .params to receive a list of the values
+    use .std to receive a list of the standard errors
 
     If a two-fold model was fitted, this will return
     unexplained effect, explained effect, and the
-    mean gap. The list will be of the following order
-    and type.
+    mean gap. The list will always be of the following order
+    and type. If standard error was asked for, then standard error
+    calculations will also be included for each variable after each
+    calculated effect.
 
     unexplained : float
         This is the effect that cannot be explained by the data at hand.
@@ -225,9 +486,11 @@ class OaxacaResults:
     If a three-fold model was fitted, this will
     return characteristic effect, coefficient effect
     interaction effect, and the mean gap. The list will
-    be of the following order and type.
+    be of the following order and type. If standard error was asked
+    for, then standard error calculations will also be included for
+    each variable after each calculated effect.
 
-    characteristic effect : float
+    endowment effect : float
         This is the effect due to the group differences in
         predictors
     coefficient effect : float
@@ -243,9 +506,13 @@ class OaxacaResults:
     ----------
     params
         A list of all values for the fitted models.
+    std
+        A list of standard error calculations.
     """
-    def __init__(self, results, model_type):
+
+    def __init__(self, results, model_type, std_val=None):
         self.params = results
+        self.std = std_val
         self.model_type = model_type
 
     def summary(self):
@@ -253,22 +520,57 @@ def summary(self):
         Print a summary table with the Oaxaca-Blinder effects
         """
         if self.model_type == 2:
-            print(dedent("""\
-            Oaxaca-Blinder Two-fold Effects
-
-            Unexplained Effect: {:.5f}
-            Explained Effect: {:.5f}
-            Gap: {:.5f}""".format(
-                                self.params[0], self.params[1],
-                                self.params[2])))
-
+            if self.std is None:
+                print(
+                    dedent(
+                        f"""\
+                Oaxaca-Blinder Two-fold Effects
+                Unexplained Effect: {self.params[0]:.5f}
+                Explained Effect: {self.params[1]:.5f}
+                Gap: {self.params[2]:.5f}"""
+                    )
+                )
+            else:
+                print(
+                    dedent(
+                        """\
+                Oaxaca-Blinder Two-fold Effects
+                Unexplained Effect: {:.5f}
+                Unexplained Standard Error: {:.5f}
+                Explained Effect: {:.5f}
+                Explained Standard Error: {:.5f}
+                Gap: {:.5f}""".format(
+                            self.params[0],
+                            self.std[0],
+                            self.params[1],
+                            self.std[1],
+                            self.params[2],
+                        )
+                    )
+                )
         if self.model_type == 3:
-            print(dedent("""\
-            Oaxaca-Blinder Three-fold Effects
-
-            Characteristic Effect: {:.5f}
-            Coefficient Effect: {:.5f}
-            Interaction Effect: {:.5f}
-            Gap: {:.5f}""".format(
-                            self.params[0], self.params[1],
-                            self.params[2], self.params[3])))
+            if self.std is None:
+                print(
+                    dedent(
+                        f"""\
+                Oaxaca-Blinder Three-fold Effects
+                Endowment Effect: {self.params[0]:.5f}
+                Coefficient Effect: {self.params[1]:.5f}
+                Interaction Effect: {self.params[2]:.5f}
+                Gap: {self.params[3]:.5f}"""
+                    )
+                )
+            else:
+                print(
+                    dedent(
+                        f"""\
+                Oaxaca-Blinder Three-fold Effects
+                Endowment Effect: {self.params[0]:.5f}
+                Endowment Standard Error: {self.std[0]:.5f}
+                Coefficient Effect: {self.params[1]:.5f}
+                Coefficient Standard Error: {self.std[1]:.5f}
+                Interaction Effect: {self.params[2]:.5f}
+                Interaction Standard Error: {self.std[2]:.5f}
+                Gap: {self.params[3]:.5f}"""
+                    )
+                )


Test patch:
diff --git a/statsmodels/stats/tests/test_oaxaca.py b/statsmodels/stats/tests/test_oaxaca.py
--- a/statsmodels/stats/tests/test_oaxaca.py
+++ b/statsmodels/stats/tests/test_oaxaca.py
@@ -3,6 +3,9 @@
 # so I cannot test for having no intercept. This also would make
 # no sense for Oaxaca. All of these stata_results
 # are from using the oaxaca command in STATA.
+# Variance from STATA is bootstrapped. Sometimes STATA
+# does not converge correctly, so mulitple iterations
+# must be done.
 
 import numpy as np
 
@@ -14,7 +17,8 @@
 endog = pandas_df.endog.values
 exog = add_constant(pandas_df.exog.values, prepend=False)
 pd_endog, pd_exog = pandas_df.endog, add_constant(
-    pandas_df.exog, prepend=False)
+    pandas_df.exog, prepend=False
+)
 
 
 class TestOaxaca(object):
@@ -23,12 +27,15 @@ def setup_class(cls):
         cls.model = OaxacaBlinder(endog, exog, 3)
 
     def test_results(self):
+        np.random.seed(0)
         stata_results = np.array([158.7504, 321.7482, 75.45371, -238.4515])
         stata_results_pooled = np.array([158.7504, 130.8095, 27.94091])
-        char, coef, inter, gap = self.model.three_fold().params
+        stata_results_std = np.array([653.10389, 64.584796, 655.0323717])
+        endow, coef, inter, gap = self.model.three_fold().params
         unexp, exp, gap = self.model.two_fold().params
+        endow_var, coef_var, inter_var = self.model.three_fold(True).std
         np.testing.assert_almost_equal(gap, stata_results[0], 3)
-        np.testing.assert_almost_equal(char, stata_results[1], 3)
+        np.testing.assert_almost_equal(endow, stata_results[1], 3)
         np.testing.assert_almost_equal(coef, stata_results[2], 3)
         np.testing.assert_almost_equal(inter, stata_results[3], 3)
 
@@ -36,6 +43,10 @@ def test_results(self):
         np.testing.assert_almost_equal(exp, stata_results_pooled[1], 3)
         np.testing.assert_almost_equal(unexp, stata_results_pooled[2], 3)
 
+        np.testing.assert_almost_equal(endow_var, stata_results_std[0], 3)
+        np.testing.assert_almost_equal(coef_var, stata_results_std[1], 3)
+        np.testing.assert_almost_equal(inter_var, stata_results_std[2], 3)
+
 
 class TestOaxacaNoSwap(object):
     @classmethod
@@ -45,10 +56,10 @@ def setup_class(cls):
     def test_results(self):
         stata_results = np.array([-158.7504, -83.29674, 162.9978, -238.4515])
         stata_results_pooled = np.array([-158.7504, -130.8095, -27.94091])
-        char, coef, inter, gap = self.model.three_fold().params
+        endow, coef, inter, gap = self.model.three_fold().params
         unexp, exp, gap = self.model.two_fold().params
         np.testing.assert_almost_equal(gap, stata_results[0], 3)
-        np.testing.assert_almost_equal(char, stata_results[1], 3)
+        np.testing.assert_almost_equal(endow, stata_results[1], 3)
         np.testing.assert_almost_equal(coef, stata_results[2], 3)
         np.testing.assert_almost_equal(inter, stata_results[3], 3)
 
@@ -60,15 +71,15 @@ def test_results(self):
 class TestOaxacaPandas(object):
     @classmethod
     def setup_class(cls):
-        cls.model = OaxacaBlinder(pd_endog, pd_exog, 'OWNRENT')
+        cls.model = OaxacaBlinder(pd_endog, pd_exog, "OWNRENT")
 
     def test_results(self):
         stata_results = np.array([158.7504, 321.7482, 75.45371, -238.4515])
         stata_results_pooled = np.array([158.7504, 130.8095, 27.94091])
-        char, coef, inter, gap = self.model.three_fold().params
+        endow, coef, inter, gap = self.model.three_fold().params
         unexp, exp, gap = self.model.two_fold().params
         np.testing.assert_almost_equal(gap, stata_results[0], 3)
-        np.testing.assert_almost_equal(char, stata_results[1], 3)
+        np.testing.assert_almost_equal(endow, stata_results[1], 3)
         np.testing.assert_almost_equal(coef, stata_results[2], 3)
         np.testing.assert_almost_equal(inter, stata_results[3], 3)
 
@@ -80,15 +91,15 @@ def test_results(self):
 class TestOaxacaPandasNoSwap(object):
     @classmethod
     def setup_class(cls):
-        cls.model = OaxacaBlinder(pd_endog, pd_exog, 'OWNRENT', swap=False)
+        cls.model = OaxacaBlinder(pd_endog, pd_exog, "OWNRENT", swap=False)
 
     def test_results(self):
         stata_results = np.array([-158.7504, -83.29674, 162.9978, -238.4515])
         stata_results_pooled = np.array([-158.7504, -130.8095, -27.94091])
-        char, coef, inter, gap = self.model.three_fold().params
+        endow, coef, inter, gap = self.model.three_fold().params
         unexp, exp, gap = self.model.two_fold().params
         np.testing.assert_almost_equal(gap, stata_results[0], 3)
-        np.testing.assert_almost_equal(char, stata_results[1], 3)
+        np.testing.assert_almost_equal(endow, stata_results[1], 3)
         np.testing.assert_almost_equal(coef, stata_results[2], 3)
         np.testing.assert_almost_equal(inter, stata_results[3], 3)
 
@@ -100,17 +111,17 @@ def test_results(self):
 class TestOaxacaNoConstPassed(object):
     @classmethod
     def setup_class(cls):
-        cls.model = OaxacaBlinder(pandas_df.endog.values,
-                                  pandas_df.exog.values,
-                                  3, hasconst=False)
+        cls.model = OaxacaBlinder(
+            pandas_df.endog.values, pandas_df.exog.values, 3, hasconst=False
+        )
 
     def test_results(self):
         stata_results = np.array([158.7504, 321.7482, 75.45371, -238.4515])
         stata_results_pooled = np.array([158.7504, 130.8095, 27.94091])
-        char, coef, inter, gap = self.model.three_fold().params
+        endow, coef, inter, gap = self.model.three_fold().params
         unexp, exp, gap = self.model.two_fold().params
         np.testing.assert_almost_equal(gap, stata_results[0], 3)
-        np.testing.assert_almost_equal(char, stata_results[1], 3)
+        np.testing.assert_almost_equal(endow, stata_results[1], 3)
         np.testing.assert_almost_equal(coef, stata_results[2], 3)
         np.testing.assert_almost_equal(inter, stata_results[3], 3)
 
@@ -122,17 +133,21 @@ def test_results(self):
 class TestOaxacaNoSwapNoConstPassed(object):
     @classmethod
     def setup_class(cls):
-        cls.model = OaxacaBlinder(pandas_df.endog.values,
-                                  pandas_df.exog.values,
-                                  3, hasconst=False, swap=False)
+        cls.model = OaxacaBlinder(
+            pandas_df.endog.values,
+            pandas_df.exog.values,
+            3,
+            hasconst=False,
+            swap=False,
+        )
 
     def test_results(self):
         stata_results = np.array([-158.7504, -83.29674, 162.9978, -238.4515])
         stata_results_pooled = np.array([-158.7504, -130.8095, -27.94091])
-        char, coef, inter, gap = self.model.three_fold().params
+        endow, coef, inter, gap = self.model.three_fold().params
         unexp, exp, gap = self.model.two_fold().params
         np.testing.assert_almost_equal(gap, stata_results[0], 3)
-        np.testing.assert_almost_equal(char, stata_results[1], 3)
+        np.testing.assert_almost_equal(endow, stata_results[1], 3)
         np.testing.assert_almost_equal(coef, stata_results[2], 3)
         np.testing.assert_almost_equal(inter, stata_results[3], 3)
 
@@ -144,17 +159,17 @@ def test_results(self):
 class TestOaxacaPandasNoConstPassed(object):
     @classmethod
     def setup_class(cls):
-        cls.model = OaxacaBlinder(pandas_df.endog,
-                                  pandas_df.exog,
-                                  'OWNRENT', hasconst=False)
+        cls.model = OaxacaBlinder(
+            pandas_df.endog, pandas_df.exog, "OWNRENT", hasconst=False
+        )
 
     def test_results(self):
         stata_results = np.array([158.7504, 321.7482, 75.45371, -238.4515])
         stata_results_pooled = np.array([158.7504, 130.8095, 27.94091])
-        char, coef, inter, gap = self.model.three_fold().params
+        endow, coef, inter, gap = self.model.three_fold().params
         unexp, exp, gap = self.model.two_fold().params
         np.testing.assert_almost_equal(gap, stata_results[0], 3)
-        np.testing.assert_almost_equal(char, stata_results[1], 3)
+        np.testing.assert_almost_equal(endow, stata_results[1], 3)
         np.testing.assert_almost_equal(coef, stata_results[2], 3)
         np.testing.assert_almost_equal(inter, stata_results[3], 3)
 
@@ -166,19 +181,114 @@ def test_results(self):
 class TestOaxacaPandasNoSwapNoConstPassed(object):
     @classmethod
     def setup_class(cls):
-        cls.model = OaxacaBlinder(pandas_df.endog, pandas_df.exog,
-                                  'OWNRENT', hasconst=False, swap=False)
+        cls.model = OaxacaBlinder(
+            pandas_df.endog,
+            pandas_df.exog,
+            "OWNRENT",
+            hasconst=False,
+            swap=False,
+        )
 
     def test_results(self):
         stata_results = np.array([-158.7504, -83.29674, 162.9978, -238.4515])
         stata_results_pooled = np.array([-158.7504, -130.8095, -27.94091])
-        char, coef, inter, gap = self.model.three_fold().params
+        endow, coef, inter, gap = self.model.three_fold().params
         unexp, exp, gap = self.model.two_fold().params
         np.testing.assert_almost_equal(gap, stata_results[0], 3)
-        np.testing.assert_almost_equal(char, stata_results[1], 3)
+        np.testing.assert_almost_equal(endow, stata_results[1], 3)
         np.testing.assert_almost_equal(coef, stata_results[2], 3)
         np.testing.assert_almost_equal(inter, stata_results[3], 3)
 
         np.testing.assert_almost_equal(gap, stata_results_pooled[0], 3)
         np.testing.assert_almost_equal(exp, stata_results_pooled[1], 3)
         np.testing.assert_almost_equal(unexp, stata_results_pooled[2], 3)
+
+
+class TestOneModel(object):
+    @classmethod
+    def setup_class(cls):
+        np.random.seed(0)
+        cls.one_model = OaxacaBlinder(
+            pandas_df.endog, pandas_df.exog, "OWNRENT", hasconst=False
+        ).two_fold(True, two_fold_type="self_submitted", submitted_weight=1)
+
+    def test_results(self):
+        unexp, exp, gap = self.one_model.params
+        unexp_std, exp_std = self.one_model.std
+        one_params_stata_results = np.array([75.45370, 83.29673, 158.75044])
+        one_std_stata_results = np.array([64.58479, 71.05619])
+
+        np.testing.assert_almost_equal(unexp, one_params_stata_results[0], 3)
+        np.testing.assert_almost_equal(exp, one_params_stata_results[1], 3)
+        np.testing.assert_almost_equal(gap, one_params_stata_results[2], 3)
+
+        np.testing.assert_almost_equal(unexp_std, one_std_stata_results[0], 3)
+        np.testing.assert_almost_equal(exp_std, one_std_stata_results[1], 3)
+
+
+class TestZeroModel(object):
+    @classmethod
+    def setup_class(cls):
+        np.random.seed(0)
+        cls.zero_model = OaxacaBlinder(
+            pandas_df.endog, pandas_df.exog, "OWNRENT", hasconst=False
+        ).two_fold(True, two_fold_type="self_submitted", submitted_weight=0)
+
+    def test_results(self):
+        unexp, exp, gap = self.zero_model.params
+        unexp_std, exp_std = self.zero_model.std
+        zero_params_stata_results = np.array([-162.9978, 321.7482, 158.75044])
+        zero_std_stata_results = np.array([668.1512, 653.10389])
+
+        np.testing.assert_almost_equal(unexp, zero_params_stata_results[0], 3)
+        np.testing.assert_almost_equal(exp, zero_params_stata_results[1], 3)
+        np.testing.assert_almost_equal(gap, zero_params_stata_results[2], 3)
+
+        np.testing.assert_almost_equal(unexp_std, zero_std_stata_results[0], 3)
+        np.testing.assert_almost_equal(exp_std, zero_std_stata_results[1], 3)
+
+
+class TestOmegaModel(object):
+    @classmethod
+    def setup_class(cls):
+        np.random.seed(0)
+        cls.omega_model = OaxacaBlinder(
+            pandas_df.endog, pandas_df.exog, "OWNRENT", hasconst=False
+        ).two_fold(True, two_fold_type="nuemark")
+
+    def test_results(self):
+        unexp, exp, gap = self.omega_model.params
+        unexp_std, exp_std = self.omega_model.std
+        nue_params_stata_results = np.array([19.52467, 139.22577, 158.75044])
+        nue_std_stata_results = np.array([59.82744, 48.25425])
+
+        np.testing.assert_almost_equal(unexp, nue_params_stata_results[0], 3)
+        np.testing.assert_almost_equal(exp, nue_params_stata_results[1], 3)
+        np.testing.assert_almost_equal(gap, nue_params_stata_results[2], 3)
+
+        np.testing.assert_almost_equal(unexp_std, nue_std_stata_results[0], 3)
+        np.testing.assert_almost_equal(exp_std, nue_std_stata_results[1], 3)
+
+
+class TestPooledModel(object):
+    @classmethod
+    def setup_class(cls):
+        np.random.seed(0)
+        cls.pooled_model = OaxacaBlinder(
+            pandas_df.endog, pandas_df.exog, "OWNRENT", hasconst=False
+        ).two_fold(True)
+
+    def test_results(self):
+        unexp, exp, gap = self.pooled_model.params
+        unexp_std, exp_std = self.pooled_model.std
+        pool_params_stata_results = np.array(
+            [27.940908, 130.809536, 158.75044]
+        )
+        pool_std_stata_results = np.array([89.209487, 58.612367])
+
+        np.testing.assert_almost_equal(unexp, pool_params_stata_results[0], 3)
+        np.testing.assert_almost_equal(exp, pool_params_stata_results[1], 3)
+        np.testing.assert_almost_equal(gap, pool_params_stata_results[2], 3)
+
+        np.testing.assert_almost_equal(unexp_std, pool_std_stata_results[0], 3)
+        np.testing.assert_almost_equal(exp_std, pool_std_stata_results[1], 3)