Repository: statsmodels/statsmodels

Issue:
GEE:  set scale=1 for Poisson, Binomial, Multinomial, changes cov_naive
changed in #2158 

we consider this as a bug-fix, it changes scale of cov_naive compared to previous version.

cov_naive was more a `cov_glm` with over or under dispersed scale, but not completely `"nonrobust"`



Discussion:

Resolving patch:
diff --git a/statsmodels/genmod/cov_struct.py b/statsmodels/genmod/cov_struct.py
--- a/statsmodels/genmod/cov_struct.py
+++ b/statsmodels/genmod/cov_struct.py
@@ -395,7 +395,6 @@ def initialize(self, model):
     def update(self, params):
 
         endog = self.model.endog_li
-        offset = self.model.offset_li
 
         nobs = self.model.nobs
         dim = len(params)
@@ -414,7 +413,7 @@ def update(self, params):
             expval, _ = cached_means[i]
 
             stdev = np.sqrt(varfunc(expval))
-            resid = (endog[i] - offset[i] - expval) / stdev
+            resid = (endog[i] - expval) / stdev
 
             ix1, ix2 = np.tril_indices(len(resid), -1)
             dvmat.append(resid[ix1] * resid[ix2])
diff --git a/statsmodels/genmod/generalized_estimating_equations.py b/statsmodels/genmod/generalized_estimating_equations.py
--- a/statsmodels/genmod/generalized_estimating_equations.py
+++ b/statsmodels/genmod/generalized_estimating_equations.py
@@ -46,24 +46,6 @@
 import warnings
 
 
-# Workaround for block_diag, not available until scipy version
-# 0.11. When the statsmodels scipy dependency moves to version 0.11,
-# we can remove this function and use:
-# from scipy.sparse import block_diag
-def block_diag(dblocks, format=None):
-
-    from scipy.sparse import bmat
-
-    n = len(dblocks)
-    blocks = []
-    for i in range(n):
-        b = [None,] * n
-        b[i] = dblocks[i]
-        blocks.append(b)
-
-    return bmat(blocks, format)
-
-
 class ParameterConstraint(object):
     """
     A class for managing linear equality constraints for a parameter
@@ -171,6 +153,8 @@ def unpack_cov(self, bcov):
 
 
 _gee_init_doc = """
+    Marginal regression model fit using Generalized Estimating Equations.
+
     GEE can be used to fit Generalized Linear Models (GLMs) when the
     data have a grouped structure, and the observations are possibly
     correlated within groups but not between groups.
@@ -182,8 +166,8 @@ def unpack_cov(self, bcov):
         dependent variables, or 'Y' values).
     exog : array-like
         2d array of exogeneous values (i.e. covariates, predictors,
-        independent variables, regressors, or 'X' values). A nobs x k
-        array where `nobs` is the number of observations and `k` is
+        independent variables, regressors, or 'X' values). A `nobs x
+        k` array where `nobs` is the number of observations and `k` is
         the number of regressors. An intercept is not included by
         default and should be added by the user. See
         `statsmodels.tools.add_constant`.
@@ -490,14 +474,17 @@ def __init__(self, endog, exog, groups, time=None, family=None,
         self.cov_struct = cov_struct
 
         # Handle the offset and exposure
-        self._offset_exposure = np.zeros(len(self.endog))
+        self._offset_exposure = None
         if offset is not None:
-            self._offset_exposure += self.offset
+            self._offset_exposure = self.offset.copy()
             self.offset = offset
         if exposure is not None:
             if not isinstance(self.family.link, families.links.Log):
                 raise ValueError("exposure can only be used with the log link function")
-            self._offset_exposure += np.log(exposure)
+            if self._offset_exposure is not None:
+                self._offset_exposure += np.log(exposure)
+            else:
+                self._offset_exposure = np.log(exposure)
             self.exposure = exposure
 
         # Handle the constraint
@@ -513,7 +500,10 @@ def __init__(self, endog, exog, groups, time=None, family=None,
                                                   constraint[1],
                                                   self.exog)
 
-            self._offset_exposure += self.constraint.offset_increment()
+            if self._offset_exposure is not None:
+                self._offset_exposure += self.constraint.offset_increment()
+            else:
+                self._offset_exposure = self.constraint.offset_increment().copy()
             self.exog = self.constraint.reduced_exog()
 
         # Convert the data to the internal representation, which is a
@@ -544,7 +534,10 @@ def __init__(self, endog, exog, groups, time=None, family=None,
                  for y in self.endog_li]
             self.time = np.concatenate(self.time_li)
 
-        self.offset_li = self.cluster_list(self._offset_exposure)
+        if self._offset_exposure is not None:
+            self.offset_li = self.cluster_list(self._offset_exposure)
+        else:
+            self.offset_li = None
         if constraint is not None:
             self.constraint.exog_fulltrans_li = \
                 self.cluster_list(self.constraint.exog_fulltrans)
@@ -560,39 +553,6 @@ def __init__(self, endog, exog, groups, time=None, family=None,
         self.df_model = self.exog.shape[1] - 1  # assumes constant
         self.df_resid = self.nobs - self.exog.shape[1]
 
-        # mean_deriv is the derivative of E[endog|exog] with respect
-        # to params
-        try:
-            # This custom mean_deriv is currently only used for the
-            # multinomial logit model
-            self.mean_deriv = self.family.link.mean_deriv
-        except AttributeError:
-            # Otherwise it can be obtained easily from inverse_deriv
-            mean_deriv_lpr = self.family.link.inverse_deriv
-
-            def mean_deriv(exog, lpr):
-                dmat = exog * mean_deriv_lpr(lpr)[:, None]
-                return dmat
-
-            self.mean_deriv = mean_deriv
-
-        # mean_deriv_exog is the derivative of E[endog|exog] with
-        # respect to exog
-        try:
-            # This custom mean_deriv_exog is currently only used for
-            # the multinomial logit model
-            self.mean_deriv_exog = self.family.link.mean_deriv_exog
-        except AttributeError:
-            # Otherwise it can be obtained easily from inverse_deriv
-            mean_deriv_lpr = self.family.link.inverse_deriv
-
-            def mean_deriv_exog(exog, params):
-                lpr = np.dot(exog, params)
-                dmat = np.outer(mean_deriv_lpr(lpr), params)
-                return dmat
-
-            self.mean_deriv_exog = mean_deriv_exog
-
         # Skip the covariance updates if all groups have a single
         # observation (reduces to fitting a GLM).
         maxgroup = max([len(x) for x in self.endog_li])
@@ -699,13 +659,16 @@ def cluster_list(self, array):
 
     def estimate_scale(self):
         """
-        Returns an estimate of the scale parameter `phi` at the
-        current parameter value.
+        Returns an estimate of the scale parameter at the current
+        parameter value.
         """
 
+        if isinstance(self.family, (families.Binomial, families.Poisson,
+                                    _Multinomial)):
+            return 1.
+
         endog = self.endog_li
         exog = self.exog_li
-        offset = self.offset_li
 
         cached_means = self.cached_means
 
@@ -723,7 +686,7 @@ def estimate_scale(self):
             expval, _ = cached_means[i]
 
             sdev = np.sqrt(varfunc(expval))
-            resid = (endog[i] - offset[i] - expval) / sdev
+            resid = (endog[i] - expval) / sdev
 
             scale += np.sum(resid**2)
 
@@ -731,6 +694,61 @@ def estimate_scale(self):
 
         return scale
 
+    def mean_deriv(self, exog, lin_pred):
+        """
+        Derivative of the expected endog with respect to the parameters.
+
+        Parameters
+        ----------
+        exog : array-like
+           The exogeneous data at which the derivative is computed.
+        lin_pred : array-like
+           The values of the linear predictor.
+
+        Returns
+        -------
+        The value of the derivative of the expected endog with respect
+        to the parameter vector.
+
+        Notes
+        -----
+        If there is an offset or exposure, it should be added to
+        `lin_pred` prior to calling this function.
+        """
+
+        idl = self.family.link.inverse_deriv(lin_pred)
+        dmat = exog * idl[:, None]
+        return dmat
+
+
+    def mean_deriv_exog(self, exog, params, offset_exposure=None):
+        """
+        Derivative of the expected endog with respect to exog.
+
+        Parameters
+        ----------
+        exog : array-like
+            Values of the independent variables at which the derivative
+            is calculated.
+        params : array-like
+            Parameter values at which the derivative is calculated.
+        offset_exposure : array-like, optional
+            Combined offset and exposure.
+
+        Returns
+        -------
+        The derivative of the expected endog with respect to exog.
+        """
+
+        lin_pred = np.dot(exog, params)
+        if offset_exposure is not None:
+            lin_pred += offset_exposure
+
+        idl = self.family.link.inverse_deriv(lin_pred)
+        dmat = np.outer(idl, params)
+        return dmat
+
+
     def _update_mean_params(self):
         """
         Returns
@@ -797,7 +815,9 @@ def update_cached_means(self, mean_params):
             if len(endog[i]) == 0:
                 continue
 
-            lpr = offset[i] + np.dot(exog[i], mean_params)
+            lpr = np.dot(exog[i], mean_params)
+            if offset is not None:
+                lpr += offset[i]
             expval = linkinv(lpr)
 
             self.cached_means.append((expval, lpr))
@@ -941,13 +961,15 @@ def predict(self, params, exog=None, offset=None,
             if not isinstance(self.family.link, families.links.Log):
                 # Don't need to worry about exposure
                 if offset is None:
-                    _offset = self._offset_exposure
+                    if self._offset_exposure is not None:
+                        _offset = self._offset_exposure.copy()
                 else:
                     _offset = offset
 
             else:
                 if offset is None and exposure is None:
-                    _offset = self._offset_exposure
+                    if self._offset_exposure is not None:
+                        _offset = self._offset_exposure
                 elif offset is None and exposure is not None:
                     _offset = np.log(exposure)
                     if hasattr(self, "offset"):
@@ -963,7 +985,7 @@ def predict(self, params, exog=None, offset=None,
         # never use model exog or exposure if exog is provided.
         else:
             if offset is not None:
-                _offset += offset
+                _offset = _offset + offset
             if exposure is not None:
                 _offset += np.log(exposure)
 
@@ -1009,6 +1031,7 @@ def fit(self, maxiter=60, ctol=1e-6, start_params=None,
         if start_params is None:
             mean_params = self._starting_params()
         else:
+            start_params = np.asarray(start_params)
             mean_params = start_params.copy()
 
         self.update_cached_means(mean_params)
@@ -1196,23 +1219,22 @@ def _update_assoc(self, params):
     def _derivative_exog(self, params, exog=None, transform='dydx',
             dummy_idx=None, count_idx=None):
         """
-        For computing marginal effects returns dF(XB) / dX where F(.) is
-        the predicted probabilities
+        For computing marginal effects, returns dF(XB) / dX where F(.)
+        is the fitted mean.
 
         transform can be 'dydx', 'dyex', 'eydx', or 'eyex'.
 
         Not all of these make sense in the presence of discrete regressors,
         but checks are done in the results in get_margeff.
         """
-        #note, this form should be appropriate for
-        ## group 1 probit, logit, logistic, cloglog, heckprob, xtprobit
+        # This form should be appropriate for group 1 probit, logit,
+        # logistic, cloglog, heckprob, xtprobit.
+        offset_exposure = None
         if exog is None:
             exog = self.exog
-        margeff = self.mean_deriv_exog(exog, params)
-#        lpr = np.dot(exog, params)
-#        margeff = (self.mean_deriv(exog, lpr) / exog) * params
-#        margeff = np.dot(self.pdf(np.dot(exog, params))[:, None],
-#                                                          params[None,:])
+            offset_exposure = self._offset_exposure
+
+        margeff = self.mean_deriv_exog(exog, params, offset_exposure)
 
         if 'ex' in transform:
             margeff *= exog
@@ -1508,6 +1530,73 @@ def summary(self, yname=None, xname=None, title=None, alpha=.05):
 
         return smry
 
+    def get_margeff(self, at='overall', method='dydx', atexog=None,
+                          dummy=False, count=False):
+        """Get marginal effects of the fitted model.
+
+        Parameters
+        ----------
+        at : str, optional
+            Options are:
+
+            - 'overall', The average of the marginal effects at each
+              observation.
+            - 'mean', The marginal effects at the mean of each regressor.
+            - 'median', The marginal effects at the median of each regressor.
+            - 'zero', The marginal effects at zero for each regressor.
+            - 'all', The marginal effects at each observation. If `at` is 'all'
+              only margeff will be available.
+
+            Note that if `exog` is specified, then marginal effects for all
+            variables not specified by `exog` are calculated using the `at`
+            option.
+        method : str, optional
+            Options are:
+
+            - 'dydx' - dy/dx - No transformation is made and marginal effects
+              are returned.  This is the default.
+            - 'eyex' - estimate elasticities of variables in `exog` --
+              d(lny)/d(lnx)
+            - 'dyex' - estimate semielasticity -- dy/d(lnx)
+            - 'eydx' - estimate semeilasticity -- d(lny)/dx
+
+            Note that tranformations are done after each observation is
+            calculated.  Semi-elasticities for binary variables are computed
+            using the midpoint method. 'dyex' and 'eyex' do not make sense
+            for discrete variables.
+        atexog : array-like, optional
+            Optionally, you can provide the exogenous variables over which to
+            get the marginal effects.  This should be a dictionary with the key
+            as the zero-indexed column number and the value of the dictionary.
+            Default is None for all independent variables less the constant.
+        dummy : bool, optional
+            If False, treats binary variables (if present) as continuous.  This
+            is the default.  Else if True, treats binary variables as
+            changing from 0 to 1.  Note that any variable that is either 0 or 1
+            is treated as binary.  Each binary variable is treated separately
+            for now.
+        count : bool, optional
+            If False, treats count variables (if present) as continuous.  This
+            is the default.  Else if True, the marginal effect is the
+            change in probabilities when each observation is increased by one.
+
+        Returns
+        -------
+        effects : ndarray
+            the marginal effect corresponding to the input options
+
+        Notes
+        -----
+        When using after Poisson, returns the expected number of events
+        per period, assuming that the model is loglinear.
+        """
+
+        if self.model.constraint is not None:
+            warnings.warn("marginal effects ignore constraints")
+
+        return GEEMargins(self, (at, method, atexog, dummy, count))
+
+
     def plot_isotropic_dependence(self, ax=None, xpoints=10,
                                   min_n=50):
         """
@@ -1961,6 +2050,109 @@ def setup_nominal(self, endog, exog, groups, time, offset):
 
         return endog_out, exog_out, groups_out, time_out, offset_out
 
+
+    def mean_deriv(self, exog, lin_pred):
+        """
+        Derivative of the expected endog with respect to the parameters.
+
+        Parameters
+        ----------
+        exog : array-like
+           The exogeneous data at which the derivative is computed,
+           number of rows must be a multiple of `ncut`.
+        lin_pred : array-like
+           The values of the linear predictor, length must be multiple
+           of `ncut`.
+
+        Returns
+        -------
+        The derivative of the expected endog with respect to the
+        parameters.
+        """
+
+        expval = np.exp(lin_pred)
+
+        # Reshape so that each row contains all the indicators
+        # corresponding to one multinomial observation.
+        expval_m = np.reshape(expval, (len(expval) / self.ncut,
+                                       self.ncut))
+
+        # The normalizing constant for the multinomial probabilities.
+        denom = 1 + expval_m.sum(1)
+        denom = np.kron(denom, np.ones(self.ncut, dtype=np.float64))
+
+        # The multinomial probabilities
+        mprob = expval / denom
+
+        # First term of the derivative: denom * expval' / denom^2 =
+        # expval' / denom.
+        dmat = mprob[:, None] * exog
+
+        # Second term of the derivative: -expval * denom' / denom^2
+        ddenom = expval[:, None] * exog
+        dmat -= mprob[:, None] * ddenom / denom[:, None]
+
+        return dmat
+
+
+    def mean_deriv_exog(self, exog, params, offset_exposure=None):
+        """
+        Derivative of the expected endog with respect to exog for the
+        multinomial model, used in analyzing marginal effects.
+
+        Parameters
+        ----------
+        exog : array-like
+           The exogeneous data at which the derivative is computed,
+           number of rows must be a multiple of `ncut`.
+        lpr : array-like
+           The linear predictor values, length must be multiple of
+           `ncut`.
+
+        Returns
+        -------
+        The value of the derivative of the expected endog with respect
+        to exog.
+
+        Notes
+        -----
+        offset_exposure must be set at None for the multinoial family.
+        """
+
+        if offset_exposure is not None:
+            warnings.warn("Offset/exposure ignored for the multinomial family")
+
+        lpr = np.dot(exog, params)
+        expval = np.exp(lpr)
+
+        expval_m = np.reshape(expval, (len(expval) / self.ncut,
+                                       self.ncut))
+
+        denom = 1 + expval_m.sum(1)
+        denom = np.kron(denom, np.ones(self.ncut, dtype=np.float64))
+
+        bmat0 = np.outer(np.ones(exog.shape[0]), params)
+
+        # Masking matrix
+        qmat = []
+        for j in range(self.ncut):
+            ee = np.zeros(self.ncut, dtype=np.float64)
+            ee[j] = 1
+            qmat.append(np.kron(ee, np.ones(len(params) / self.ncut)))
+        qmat = np.array(qmat)
+        qmat = np.kron(np.ones((exog.shape[0]/self.ncut, 1)), qmat)
+        bmat = bmat0 * qmat
+
+        dmat = expval[:, None] * bmat / denom[:, None]
+
+        expval_mb = np.kron(expval_m, np.ones((self.ncut, 1)))
+        expval_mb = np.kron(expval_mb, np.ones((1, self.ncut)))
+
+        dmat -= expval[:, None] * (bmat * expval_mb) / denom[:, None]**2
+
+        return dmat
+
+
     def fit(self, maxiter=60, ctol=1e-6, start_params=None,
             params_niter=1, first_dep_update=0,
             cov_type='robust'):
@@ -2128,93 +2320,6 @@ def inverse(self, lpr):
 
         return prob
 
-    def mean_deriv(self, exog, lpr):
-        """
-        Derivative of the expected endog with respect to param.
-
-        Parameters
-        ----------
-        exog : array-like
-           The exogeneous data at which the derivative is computed,
-           number of rows must be a multiple of `ncut`.
-        lpr : array-like
-           The linear predictor values, length must be multiple of
-           `ncut`.
-
-        Returns
-        -------
-        The value of the derivative of the expected endog with respect
-        to param
-        """
-
-        expval = np.exp(lpr)
-
-        expval_m = np.reshape(expval, (len(expval) / self.ncut,
-                                       self.ncut))
-
-        denom = 1 + expval_m.sum(1)
-        denom = np.kron(denom, np.ones(self.ncut, dtype=np.float64))
-
-        dmat = expval[:, None] * exog / denom[:, None]
-
-        ones = np.ones(self.ncut, dtype=np.float64)
-        cmat = block_diag([np.outer(ones, x) for x in expval_m], "csr")
-        rmat = cmat.dot(exog)
-        dmat -= expval[:, None] * rmat / denom[:, None]**2
-
-        return dmat
-
-    # Minimally tested
-    def mean_deriv_exog(self, exog, params):
-        """
-        Derivative of the expected endog with respect to exog for the
-        multinomial model, used in analyzing marginal effects.
-
-        Parameters
-        ----------
-        exog : array-like
-           The exogeneous data at which the derivative is computed,
-           number of rows must be a multiple of `ncut`.
-        lpr : array-like
-           The linear predictor values, length must be multiple of
-           `ncut`.
-
-        Returns
-        -------
-        The value of the derivative of the expected endog with respect
-        to exog.
-        """
-
-        lpr = np.dot(exog, params)
-        expval = np.exp(lpr)
-
-        expval_m = np.reshape(expval, (len(expval) / self.ncut,
-                                       self.ncut))
-
-        denom = 1 + expval_m.sum(1)
-        denom = np.kron(denom, np.ones(self.ncut, dtype=np.float64))
-
-        bmat0 = np.outer(np.ones(exog.shape[0]), params)
-
-        # Masking matrix
-        qmat = []
-        for j in range(self.ncut):
-            ee = np.zeros(self.ncut, dtype=np.float64)
-            ee[j] = 1
-            qmat.append(np.kron(ee, np.ones(len(params) / self.ncut)))
-        qmat = np.array(qmat)
-        qmat = np.kron(np.ones((exog.shape[0]/self.ncut, 1)), qmat)
-        bmat = bmat0 * qmat
-
-        dmat = expval[:, None] * bmat / denom[:, None]
-
-        expval_mb = np.kron(expval_m, np.ones((self.ncut, 1)))
-        expval_mb = np.kron(expval_mb, np.ones((1, self.ncut)))
-
-        dmat -= expval[:, None] * (bmat * expval_mb) / denom[:, None]**2
-
-        return dmat
-
 
 class _Multinomial(families.Family):
     """
@@ -2242,17 +2347,16 @@ def initialize(self, nlevels):
 
 
 
-from statsmodels.discrete.discrete_margins import \
-    _get_margeff_exog, _get_const_index, _check_margeff_args, \
-    _effects_at, margeff_cov_with_se, _check_at_is_all, \
-    _transform_names, \
-    _check_discrete_args, _get_dummy_index, _get_count_index
+from statsmodels.discrete.discrete_margins import (_get_margeff_exog,
+    _get_const_index, _check_margeff_args, _effects_at,
+    margeff_cov_with_se, _check_at_is_all, _transform_names,
+    _check_discrete_args, _get_dummy_index, _get_count_index)
 
 
 
 class GEEMargins(object):
-    """Estimate the marginal effects of a model fit using generalized
-    estimating equations.
+    """
+    Estimated marginal effects for a regression model fit with GEE.
 
     Parameters
     ----------
@@ -2415,65 +2519,8 @@ def summary(self, alpha=.05):
         return smry
 
     def get_margeff(self, at='overall', method='dydx', atexog=None,
-                          dummy=False, count=False):
-        """Get marginal effects of the fitted model.
-
-        Parameters
-        ----------
-        at : str, optional
-            Options are:
-
-            - 'overall', The average of the marginal effects at each
-              observation.
-            - 'mean', The marginal effects at the mean of each regressor.
-            - 'median', The marginal effects at the median of each regressor.
-            - 'zero', The marginal effects at zero for each regressor.
-            - 'all', The marginal effects at each observation. If `at` is all
-              only margeff will be available.
-
-            Note that if `exog` is specified, then marginal effects for all
-            variables not specified by `exog` are calculated using the `at`
-            option.
-        method : str, optional
-            Options are:
-
-            - 'dydx' - dy/dx - No transformation is made and marginal effects
-              are returned.  This is the default.
-            - 'eyex' - estimate elasticities of variables in `exog` --
-              d(lny)/d(lnx)
-            - 'dyex' - estimate semielasticity -- dy/d(lnx)
-            - 'eydx' - estimate semeilasticity -- d(lny)/dx
+                    dummy=False, count=False):
 
-            Note that tranformations are done after each observation is
-            calculated.  Semi-elasticities for binary variables are computed
-            using the midpoint method. 'dyex' and 'eyex' do not make sense
-            for discrete variables.
-        atexog : array-like, optional
-            Optionally, you can provide the exogenous variables over which to
-            get the marginal effects.  This should be a dictionary with the key
-            as the zero-indexed column number and the value of the dictionary.
-            Default is None for all independent variables less the constant.
-        dummy : bool, optional
-            If False, treats binary variables (if present) as continuous.  This
-            is the default.  Else if True, treats binary variables as
-            changing from 0 to 1.  Note that any variable that is either 0 or 1
-            is treated as binary.  Each binary variable is treated separately
-            for now.
-        count : bool, optional
-            If False, treats count variables (if present) as continuous.  This
-            is the default.  Else if True, the marginal effect is the
-            change in probabilities when each observation is increased by one.
-
-        Returns
-        -------
-        effects : ndarray
-            the marginal effect corresponding to the input options
-
-        Notes
-        -----
-        When using after Poisson, returns the expected number of events
-        per period, assuming that the model is loglinear.
-        """
         self._reset() # always reset the cache when this is called
         #TODO: if at is not all or overall, we can also put atexog values
         # in summary table head
@@ -2504,37 +2551,20 @@ def get_margeff(self, at='overall', method='dydx', atexog=None,
 
         # get base marginal effects, handled by sub-classes
         effects = model._derivative_exog(params, exog, method,
-                                                    dummy_idx, count_idx)
-
-        J = getattr(model, 'J', 1)
-        effects_idx = np.tile(effects_idx, J) # adjust for multi-equation.
-
+                                         dummy_idx, count_idx)
         effects = _effects_at(effects, at)
 
         if at == 'all':
-            if J > 1:
-                K = model.K - np.any(~effects_idx) # subtract constant
-                self.margeff = effects[:, effects_idx].reshape(-1, K, J,
-                                                                order='F')
-            else:
-                self.margeff = effects[:, effects_idx]
+            self.margeff = effects[:, effects_idx]
         else:
             # Set standard error of the marginal effects by Delta method.
             margeff_cov, margeff_se = margeff_cov_with_se(model, params, exog,
                                                 results.cov_params(), at,
                                                 model._derivative_exog,
                                                 dummy_idx, count_idx,
-                                                method, J)
-
-            # reshape for multi-equation
-            if J > 1:
-                K = model.K - np.any(~effects_idx) # subtract constant
-                self.margeff = effects[effects_idx].reshape(K, J, order='F')
-                self.margeff_se = margeff_se[effects_idx].reshape(K, J,
-                                                                  order='F')
-                self.margeff_cov = margeff_cov[effects_idx][:, effects_idx]
-            else:
-                # don't care about at constant
-                self.margeff_cov = margeff_cov[effects_idx][:, effects_idx]
-                self.margeff_se = margeff_se[effects_idx]
-                self.margeff = effects[effects_idx]
+                                                method, 1)
+
+            # don't care about at constant
+            self.margeff_cov = margeff_cov[effects_idx][:, effects_idx]
+            self.margeff_se = margeff_se[effects_idx]
+            self.margeff = effects[effects_idx]


Test patch:
diff --git a/statsmodels/genmod/tests/test_gee.py b/statsmodels/genmod/tests/test_gee.py
--- a/statsmodels/genmod/tests/test_gee.py
+++ b/statsmodels/genmod/tests/test_gee.py
@@ -1,11 +1,12 @@
 """
 Test functions for GEE
 
-External comparisons are to R.  The statmodels GEE implementation
-should generally agree with the R GEE implementation for the
-independence and exchangeable correlation structures.  For other
-correlation structures, the details of the correlation estimation
-differ among implementations and the results will not agree exactly.
+External comparisons are to R and Stata.  The statmodels GEE
+implementation should generally agree with the R GEE implementation
+for the independence and exchangeable correlation structures.  For
+other correlation structures, the details of the correlation
+estimation differ among implementations and the results will not agree
+exactly.
 """
 
 from statsmodels.compat import lrange
@@ -15,8 +16,7 @@
 from numpy.testing import (assert_almost_equal, assert_equal, assert_allclose,
                            assert_array_less, assert_raises, assert_)
 from statsmodels.genmod.generalized_estimating_equations import (GEE,
-     OrdinalGEE, NominalGEE, GEEMargins,
-     NominalGEEResults, OrdinalGEEResults,
+     OrdinalGEE, NominalGEE, NominalGEEResults, OrdinalGEEResults,
      NominalGEEResultsWrapper, OrdinalGEEResultsWrapper)
 from statsmodels.genmod.families import Gaussian, Binomial, Poisson
 from statsmodels.genmod.cov_struct import (Exchangeable, Independence,
@@ -72,30 +72,119 @@ def check_wrapper(results):
 class TestGEE(object):
 
 
-    def test_margins(self):
+    def test_margins_gaussian(self):
+        """
+        Check marginal effects for a Gaussian GEE fit.  Marginal
+        effects and ordinary effects should be equal.
+        """
 
-        n = 300
-        exog = np.random.normal(size=(n, 4))
-        exog[:,0] = 1
-        exog[:,1] = 1*(exog[:,2] < 0)
+        n = 40
+        np.random.seed(34234)
+        exog = np.random.normal(size=(n, 3))
+        exog[:, 0] = 1
 
-        group = np.kron(np.arange(n/4), np.ones(4))
-        time = np.zeros((n, 1))
+        groups = np.kron(np.arange(n/4), np.r_[1, 1, 1, 1])
 
-        beta = np.r_[0, 1, -1, 0.5]
-        lpr = np.dot(exog, beta)
-        prob = 1 / (1 + np.exp(-lpr))
+        params = np.r_[0, 1, -1]
+        lin_pred = np.dot(exog, params)
+        prob = 1 / (1 + np.exp(-lin_pred))
 
-        endog = 1*(np.random.uniform(size=n) < prob)
+        endog = exog[:, 1] + np.random.normal(size=n)
 
-        fa = Binomial()
-        ex = Exchangeable()
+        model = sm.GEE(endog, exog, groups)
+        result = model.fit(start_params=[-4.88085602e-04, 1.18501903, 4.78820100e-02])
 
-        md = GEE(endog, exog, group, time, fa, ex)
-        mdf = md.fit()
+        marg = result.get_margeff()
+
+        assert_allclose(marg.margeff, result.params[1:])
+        assert_allclose(marg.margeff_se, result.bse[1:])
+
+    def test_margins_logistic(self):
+        """
+        Check marginal effects for a binomial GEE fit.  Comparison
+        comes from Stata.
+        """
+
+        np.random.seed(34234)
+        endog = np.r_[0, 0, 0, 0, 1, 1, 1, 1]
+        exog = np.ones((8, 2))
+        exog[:, 1] = np.r_[1, 2, 1, 1, 2, 1, 2, 2]
+
+        groups = np.arange(8)
+
+        model = sm.GEE(endog, exog, groups, family=sm.families.Binomial())
+        result = model.fit(cov_type='naive', start_params=[-3.29583687,  2.19722458])
+
+        marg = result.get_margeff()
+
+        assert_allclose(marg.margeff, np.r_[0.4119796])
+        assert_allclose(marg.margeff_se, np.r_[0.1379962], rtol=1e-6)
+
+
+    def test_margins_multinomial(self):
+        """
+        Check marginal effects for a 2-class multinomial GEE fit,
+        which should be equivalent to logistic regression.  Comparison
+        comes from Stata.
+        """
+
+        np.random.seed(34234)
+        endog = np.r_[0, 0, 0, 0, 1, 1, 1, 1]
+        exog = np.ones((8, 2))
+        exog[:, 1] = np.r_[1, 2, 1, 1, 2, 1, 2, 2]
+
+        groups = np.arange(8)
+
+        model = sm.NominalGEE(endog, exog, groups)
+        result = model.fit(cov_type='naive', start_params=[3.295837, -2.197225])
+
+        marg = result.get_margeff()
 
-        marg = GEEMargins(mdf, ())
-        marg.summary()
+        assert_allclose(marg.margeff, np.r_[-0.41197961], rtol=1e-5)
+        assert_allclose(marg.margeff_se, np.r_[0.1379962], rtol=1e-6)
+
+    def test_margins_poisson(self):
+        """
+        Check marginal effects for a Poisson GEE fit.
+        """
+
+        np.random.seed(34234)
+        endog = np.r_[10, 15, 12, 13, 20, 18, 26, 29]
+        exog = np.ones((8, 2))
+        exog[:, 1] = np.r_[0, 0, 0, 0, 1, 1, 1, 1]
+
+        groups = np.arange(8)
+
+        model = sm.GEE(endog, exog, groups, family=sm.families.Poisson())
+        result = model.fit(cov_type='naive', start_params=[2.52572864, 0.62057649])
+
+        marg = result.get_margeff()
+
+        assert_allclose(marg.margeff, np.r_[11.0928], rtol=1e-6)
+        assert_allclose(marg.margeff_se, np.r_[3.269015], rtol=1e-6)
+
+    def test_multinomial(self):
+        """
+        Check the 2-class multinomial (nominal) GEE fit against
+        logistic regression.
+        """
+
+        np.random.seed(34234)
+        endog = np.r_[0, 0, 0, 0, 1, 1, 1, 1]
+        exog = np.ones((8, 2))
+        exog[:, 1] = np.r_[1, 2, 1, 1, 2, 1, 2, 2]
+
+        groups = np.arange(8)
+
+        model = sm.NominalGEE(endog, exog, groups)
+        results = model.fit(cov_type='naive', start_params=[3.295837, -2.197225])
+
+
+        logit_model = sm.GEE(endog, exog, groups, family=sm.families.Binomial())
+        logit_results = logit_model.fit(cov_type='naive')
+
+        assert_allclose(results.params, -logit_results.params, rtol=1e-5)
+        assert_allclose(results.bse, logit_results.bse, rtol=1e-5)
 
 
     # This is in the release announcement for version 0.6.
@@ -109,7 +198,7 @@ def test_poisson_epil(self):
         ind = Independence()
         mod1 = GEE.from_formula("y ~ age + trt + base", data["subject"],
                                 data, cov_struct=ind, family=fam)
-        rslt1 = mod1.fit()
+        rslt1 = mod1.fit(cov_type='naive')
 
         # Coefficients should agree with GLM
         from statsmodels.genmod.generalized_linear_model import GLM
@@ -117,14 +206,14 @@ def test_poisson_epil(self):
 
         mod2 = GLM.from_formula("y ~ age + trt + base", data,
                                family=families.Poisson())
-        rslt2 = mod2.fit(scale="X2")
+        rslt2 = mod2.fit()
 
         # don't use wrapper, asserts_xxx don't work
         rslt1 = rslt1._results
         rslt2 = rslt2._results
 
-        assert_almost_equal(rslt1.params, rslt2.params, decimal=6)
-        assert_almost_equal(rslt1.scale, rslt2.scale, decimal=6)
+        assert_allclose(rslt1.params, rslt2.params,rtol=1e-6, atol=1e-6)
+        assert_allclose(rslt1.bse, rslt2.bse, rtol=1e-6, atol=1e-6)
 
     def test_missing(self):
         #Test missing data handling for calling from the api.  Missing
@@ -585,10 +674,10 @@ def test_nominal(self):
         rslt1 = mod1.fit()
 
         # Regression test
-        cf1 = np.r_[0.44944752,  0.45569985, -0.92007064, -0.46766728]
-        se1 = np.r_[0.09801821,  0.07718842,  0.13229421,  0.08544553]
-        assert_almost_equal(rslt1.params, cf1, decimal=5)
-        assert_almost_equal(rslt1.standard_errors(), se1, decimal=5)
+        cf1 = np.r_[0.450009, 0.451959, -0.918825, -0.468266]
+        se1 = np.r_[0.08915936, 0.07005046, 0.12198139, 0.08281258]
+        assert_allclose(rslt1.params, cf1, rtol=1e-5, atol=1e-5)
+        assert_allclose(rslt1.standard_errors(), se1, rtol=1e-5, atol=1e-5)
 
         # Test with global odds ratio dependence
         va = GlobalOddsRatio("nominal")
@@ -596,10 +685,10 @@ def test_nominal(self):
         rslt2 = mod2.fit(start_params=rslt1.params)
 
         # Regression test
-        cf2 = np.r_[0.45448248,  0.41945568, -0.92008924, -0.50485758]
-        se2 = np.r_[0.09632274,  0.07433944,  0.13264646,  0.0911768]
-        assert_almost_equal(rslt2.params, cf2, decimal=5)
-        assert_almost_equal(rslt2.standard_errors(), se2, decimal=5)
+        cf2 = np.r_[0.455365, 0.415334, -0.916589, -0.502116]
+        se2 = np.r_[0.08803614, 0.06628179, 0.12259726, 0.08411064]
+        assert_allclose(rslt2.params, cf2, rtol=1e-5, atol=1e-5)
+        assert_allclose(rslt2.standard_errors(), se2, rtol=1e-5, atol=1e-5)
 
         # Make sure we get the correct results type
         assert_equal(type(rslt1), NominalGEEResultsWrapper)
@@ -807,9 +896,10 @@ def test_compare_poisson(self):
     def test_predict(self):
 
         n = 50
+        np.random.seed(4324)
         X1 = np.random.normal(size=n)
         X2 = np.random.normal(size=n)
-        groups = np.kron(np.arange(25), np.r_[1, 1])
+        groups = np.kron(np.arange(n / 2), np.r_[1, 1])
         offset = np.random.uniform(1, 2, size=n)
         Y = np.random.normal(0.1*(X1 + X2) + offset, size=n)
         data = pd.DataFrame({"Y": Y, "X1": X1, "X2": X2, "groups": groups,
@@ -818,7 +908,7 @@ def test_predict(self):
         fml = "Y ~ X1 + X2"
         model = GEE.from_formula(fml, groups, data, family=Gaussian(),
                                  offset="offset")
-        result = model.fit()
+        result = model.fit(start_params=[0, 0.1, 0.1])
         assert_equal(result.converged, True)
 
         pred1 = result.predict()
@@ -930,6 +1020,7 @@ def test_sensitivity(self):
         va = Exchangeable()
         family = Gaussian()
 
+        np.random.seed(34234)
         n = 100
         Y = np.random.normal(size=n)
         X1 = np.random.normal(size=n)
@@ -948,7 +1039,7 @@ def test_sensitivity(self):
 
         # Regression test
         assert_almost_equal([x.params[0] for x in ps],
-                            np.r_[-0.1256575, -0.126747036])
+                            [0.1696214707458818, 0.17836097387799127])
 
 
 class CheckConsistency(object):