Skip to content

Commit aa78e9c

Browse files
authored
Merge pull request statsmodels#8889 from josef-pkt/glm_margeff
ENH: add get_margeff to GLM
2 parents da1a1d2 + f622be3 commit aa78e9c

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

statsmodels/discrete/tests/test_sandwich_cov.py

+17
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import os
1010
import numpy as np
1111
import pandas as pd
12+
import pytest
13+
1214
import statsmodels.discrete.discrete_model as smd
1315
from statsmodels.genmod.generalized_linear_model import GLM
1416
from statsmodels.genmod import families
@@ -499,6 +501,21 @@ def test_score_test(self):
499501
res_lm2 = res2.score_test(exog_extra, cov_type='nonrobust')
500502
assert_allclose(np.hstack(res_lm1), np.hstack(res_lm2), rtol=5e-7)
501503

504+
def test_margeff(self):
505+
if (isinstance(self.res2.model, OLS) or
506+
hasattr(self.res1.model, "offset")):
507+
pytest.skip("not available yet")
508+
509+
marg1 = self.res1.get_margeff()
510+
marg2 = self.res2.get_margeff()
511+
assert_allclose(marg1.margeff, marg2.margeff, rtol=1e-10)
512+
assert_allclose(marg1.margeff_se, marg2.margeff_se, rtol=1e-10)
513+
514+
marg1 = self.res1.get_margeff(count=True, dummy=True)
515+
marg2 = self.res2.get_margeff(count=True, dummy=True)
516+
assert_allclose(marg1.margeff, marg2.margeff, rtol=1e-10)
517+
assert_allclose(marg1.margeff_se, marg2.margeff_se, rtol=1e-10)
518+
502519

503520
class TestGLMPoisson(CheckDiscreteGLM):
504521

statsmodels/genmod/generalized_linear_model.py

+171
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
# need import in module instead of lazily to copy `__doc__`
5555
from . import families
5656

57+
5758
__all__ = ['GLM', 'PredictionResultsMean']
5859

5960

@@ -624,6 +625,90 @@ def information(self, params, scale=None):
624625
scale = float_like(scale, "scale", optional=True)
625626
return self.hessian(params, scale=scale, observed=False)
626627

628+
def _derivative_exog(self, params, exog=None, transform="dydx",
629+
dummy_idx=None, count_idx=None,
630+
offset=None, exposure=None):
631+
"""
632+
Derivative of mean, expected endog with respect to the parameters
633+
"""
634+
if exog is None:
635+
exog = self.exog
636+
if (offset is not None) or (exposure is not None):
637+
raise NotImplementedError("offset and exposure not supported")
638+
639+
lin_pred = self.predict(params, exog, which="linear",
640+
offset=offset, exposure=exposure)
641+
642+
k_extra = getattr(self, 'k_extra', 0)
643+
params_exog = params if k_extra == 0 else params[:-k_extra]
644+
645+
margeff = (self.family.link.inverse_deriv(lin_pred)[:, None] *
646+
params_exog)
647+
if 'ex' in transform:
648+
margeff *= exog
649+
if 'ey' in transform:
650+
mean = self.family.link.inverse(lin_pred)
651+
margeff /= mean[:,None]
652+
653+
return self._derivative_exog_helper(margeff, params, exog,
654+
dummy_idx, count_idx, transform)
655+
656+
def _derivative_exog_helper(self, margeff, params, exog, dummy_idx,
657+
count_idx, transform):
658+
"""
659+
Helper for _derivative_exog to wrap results appropriately
660+
"""
661+
from statsmodels.discrete.discrete_margins import (
662+
_get_count_effects,
663+
_get_dummy_effects,
664+
)
665+
666+
if count_idx is not None:
667+
margeff = _get_count_effects(margeff, exog, count_idx, transform,
668+
self, params)
669+
if dummy_idx is not None:
670+
margeff = _get_dummy_effects(margeff, exog, dummy_idx, transform,
671+
self, params)
672+
673+
return margeff
674+
675+
def _derivative_predict(self, params, exog=None, transform='dydx',
676+
offset=None, exposure=None):
677+
"""
678+
Derivative of the expected endog with respect to the parameters.
679+
680+
Parameters
681+
----------
682+
params : ndarray
683+
parameter at which score is evaluated
684+
exog : ndarray or None
685+
Explanatory variables at which derivative are computed.
686+
If None, then the estimation exog is used.
687+
offset, exposure : None
688+
Not yet implemented.
689+
690+
Returns
691+
-------
692+
The value of the derivative of the expected endog with respect
693+
to the parameter vector.
694+
"""
695+
# core part is same as derivative_mean_params
696+
# additionally handles exog and transform
697+
if exog is None:
698+
exog = self.exog
699+
if (offset is not None) or (exposure is not None) or (
700+
getattr(self, 'offset', None) is not None):
701+
raise NotImplementedError("offset and exposure not supported")
702+
703+
lin_pred = self.predict(params, exog=exog, which="linear")
704+
idl = self.family.link.inverse_deriv(lin_pred)
705+
dmat = exog * idl[:, None]
706+
if 'ey' in transform:
707+
mean = self.family.link.inverse(lin_pred)
708+
dmat /= mean[:, None]
709+
710+
return dmat
711+
627712
def _deriv_mean_dparams(self, params):
628713
"""
629714
Derivative of the expected endog with respect to the parameters.
@@ -2210,6 +2295,92 @@ def get_distribution(self, exog=None, exposure=None,
22102295
mu, scale, var_weights=var_weights, **kwds)
22112296
return distr
22122297

2298+
def get_margeff(self, at='overall', method='dydx', atexog=None,
2299+
dummy=False, count=False):
2300+
"""Get marginal effects of the fitted model.
2301+
2302+
Warning: offset, exposure and weights (var_weights and freq_weights)
2303+
are not supported by margeff.
2304+
2305+
Parameters
2306+
----------
2307+
at : str, optional
2308+
Options are:
2309+
2310+
- 'overall', The average of the marginal effects at each
2311+
observation.
2312+
- 'mean', The marginal effects at the mean of each regressor.
2313+
- 'median', The marginal effects at the median of each regressor.
2314+
- 'zero', The marginal effects at zero for each regressor.
2315+
- 'all', The marginal effects at each observation. If `at` is all
2316+
only margeff will be available from the returned object.
2317+
2318+
Note that if `exog` is specified, then marginal effects for all
2319+
variables not specified by `exog` are calculated using the `at`
2320+
option.
2321+
method : str, optional
2322+
Options are:
2323+
2324+
- 'dydx' - dy/dx - No transformation is made and marginal effects
2325+
are returned. This is the default.
2326+
- 'eyex' - estimate elasticities of variables in `exog` --
2327+
d(lny)/d(lnx)
2328+
- 'dyex' - estimate semi-elasticity -- dy/d(lnx)
2329+
- 'eydx' - estimate semi-elasticity -- d(lny)/dx
2330+
2331+
Note that tranformations are done after each observation is
2332+
calculated. Semi-elasticities for binary variables are computed
2333+
using the midpoint method. 'dyex' and 'eyex' do not make sense
2334+
for discrete variables. For interpretations of these methods
2335+
see notes below.
2336+
atexog : array_like, optional
2337+
Optionally, you can provide the exogenous variables over which to
2338+
get the marginal effects. This should be a dictionary with the key
2339+
as the zero-indexed column number and the value of the dictionary.
2340+
Default is None for all independent variables less the constant.
2341+
dummy : bool, optional
2342+
If False, treats binary variables (if present) as continuous. This
2343+
is the default. Else if True, treats binary variables as
2344+
changing from 0 to 1. Note that any variable that is either 0 or 1
2345+
is treated as binary. Each binary variable is treated separately
2346+
for now.
2347+
count : bool, optional
2348+
If False, treats count variables (if present) as continuous. This
2349+
is the default. Else if True, the marginal effect is the
2350+
change in probabilities when each observation is increased by one.
2351+
2352+
Returns
2353+
-------
2354+
DiscreteMargins : marginal effects instance
2355+
Returns an object that holds the marginal effects, standard
2356+
errors, confidence intervals, etc. See
2357+
`statsmodels.discrete.discrete_margins.DiscreteMargins` for more
2358+
information.
2359+
2360+
Notes
2361+
-----
2362+
Interpretations of methods:
2363+
2364+
- 'dydx' - change in `endog` for a change in `exog`.
2365+
- 'eyex' - proportional change in `endog` for a proportional change
2366+
in `exog`.
2367+
- 'dyex' - change in `endog` for a proportional change in `exog`.
2368+
- 'eydx' - proportional change in `endog` for a change in `exog`.
2369+
2370+
When using after Poisson, returns the expected number of events per
2371+
period, assuming that the model is loglinear.
2372+
2373+
Status : unsupported features offset, exposure and weights. Default
2374+
handling of freq_weights for average effect "overall" might change.
2375+
2376+
"""
2377+
if getattr(self.model, "offset", None) is not None:
2378+
raise NotImplementedError("Margins with offset are not available.")
2379+
if (np.any(self.model.var_weights != 1) or
2380+
np.any(self.model.freq_weights != 1)):
2381+
warnings.warn("weights are not taken into account by margeff")
2382+
from statsmodels.discrete.discrete_margins import DiscreteMargins
2383+
return DiscreteMargins(self, (at, method, atexog, dummy, count))
22132384

22142385
@Appender(base.LikelihoodModelResults.remove_data.__doc__)
22152386
def remove_data(self):

statsmodels/stats/_delta_method.py

+3
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ def var(self):
151151
"""
152152
g = self.grad()
153153
var = (np.dot(g, self.cov_params) * g).sum(-1)
154+
155+
if var.ndim == 2:
156+
var = var.T
154157
return var
155158

156159
def se_vectorized(self):

0 commit comments

Comments
 (0)