Repository: statsmodels/statsmodels

Issue:
no data cleaning for statsmodels.genmod.families.varfuncs.NegativeBinomial()
p = self._clean(mu) 
return mu + self.alpha_mu_*2

nothing is done with the object p

If the data cleaning is necessary I would have thought the code should be

mu = self._clean(mu) 
return mu + self.alpha_mu_*2

Allow linear link model with Binomial families
Currently, the Binomial family for generalized linear models does not accept the identity link. 
When modifying the code to allow for the identity link, the link seems to work as expected and the results returned appear reasonable. 
Is there a reason for not allowing the identity link? It would be useful for things like estimating risk difference.



Discussion:
Thanks for the report, clearly a typo.

a warning: 
genmod.NegativeBinomial does not have any unittests so far.
discrete.NegativeBinomial has unittests

GLM Negative Binomial has unit tests

https://github.com/statsmodels/statsmodels/blob/master/statsmodels/genmod/tests/test_glm.py#L430

my mistake: I don't know why I didn't see the tests, when I checked



Resolving patch:
diff --git a/statsmodels/genmod/families/family.py b/statsmodels/genmod/families/family.py
--- a/statsmodels/genmod/families/family.py
+++ b/statsmodels/genmod/families/family.py
@@ -10,6 +10,7 @@
 from scipy.stats import ss
 import links as L
 import varfuncs as V
+FLOAT_EPS = np.finfo(float).eps
 
 class Family(object):
 
@@ -28,7 +29,6 @@ class Family(object):
 #TODO: change these class attributes, use valid somewhere...
     valid = [-np.inf, np.inf]
 
-    tol = 1.0e-05
     links = []
 
     def _setlink(self, link):
@@ -563,7 +563,7 @@ def _clean(self, x):
         possible that other families might need a check for validity of the
         domain.
         """
-        return np.clip(x, 1.0e-10, np.inf)
+        return np.clip(x, FLOAT_EPS, np.inf)
 
     def deviance(self, Y, mu, scale=1.):
         """
@@ -696,7 +696,7 @@ class Binomial(Family):
     endog for Binomial can be specified in one of three ways.
     """
 
-    links = [L.logit, L.probit, L.cauchy, L.log, L.cloglog]
+    links = [L.logit, L.probit, L.cauchy, L.log, L.cloglog, L.identity]
     variance = V.binary # this is not used below in an effort to include n
 
     def __init__(self, link=L.logit):  #, n=1.):
@@ -1102,7 +1102,7 @@ def _clean(self, x):
         possible that other families might need a check for validity of the
         domain.
         """
-        return np.clip(x, 1.0e-10, np.inf)
+        return np.clip(x, FLOAT_EPS, np.inf)
 
     def deviance(self, Y, mu, scale=1.):
         """
diff --git a/statsmodels/genmod/families/links.py b/statsmodels/genmod/families/links.py
--- a/statsmodels/genmod/families/links.py
+++ b/statsmodels/genmod/families/links.py
@@ -4,6 +4,7 @@
 
 import numpy as np
 import scipy.stats
+FLOAT_EPS = np.finfo(float).eps
 
 #TODO: are the instance actually "aliases"
 # I used this terminology in varfuncs as well -ss
@@ -70,17 +71,15 @@ class Logit(Link):
     Notes
     -----
     call and derivative use a private method _clean to make trim p by
-    1e-10 so that p is in (0,1)
+    machine epsilon so that p is in (0,1)
 
     Alias of Logit:
     logit = Logit()
     """
 
-    tol = 1.0e-10
-
     def _clean(self, p):
         """
-        Clip logistic values to range (tol, 1-tol)
+        Clip logistic values to range (eps, 1-eps)
 
         Parameters
         -----------
@@ -92,7 +91,7 @@ def _clean(self, p):
         pclip : array
             Clipped probabilities
         """
-        return np.clip(p, Logit.tol, 1. - Logit.tol)
+        return np.clip(p, FLOAT_EPS, 1. - FLOAT_EPS)
 
     def __call__(self, p):
         """
@@ -310,13 +309,11 @@ class Log(Link):
     Notes
     -----
     call and derivative call a private method _clean to trim the data by
-    1e-10 so that p is in (0,1). log is an alias of Log.
+    machine epsilon so that p is in (0,1). log is an alias of Log.
     """
 
-    tol = 1.0e-10
-
     def _clean(self, x):
-        return np.clip(x, Logit.tol, np.inf)
+        return np.clip(x, FLOAT_EPS, np.inf)
 
     def __call__(self, p, **extra):
         """
@@ -599,13 +596,11 @@ class NegativeBinomial(object):
         values are usually assumed to be in (.01,2).
     '''
 
-    tol = 1.0e-10
-
     def __init__(self, alpha=1.):
         self.alpha = alpha
 
     def _clean(self, x):
-        return np.clip(x, NegativeBinomial.tol, np.inf)
+        return np.clip(x, FLOAT_EPS, np.inf)
 
     def __call__(self, x):
         '''
diff --git a/statsmodels/genmod/families/varfuncs.py b/statsmodels/genmod/families/varfuncs.py
--- a/statsmodels/genmod/families/varfuncs.py
+++ b/statsmodels/genmod/families/varfuncs.py
@@ -5,6 +5,7 @@
 __docformat__ = 'restructuredtext'
 
 import numpy as np
+FLOAT_EPS = np.finfo(float).eps
 
 class VarianceFunction(object):
     """
@@ -147,16 +148,15 @@ class Binomial(object):
     Alias for Binomial:
     binary = Binomial()
 
-    A private method _clean trims the data by 1e-10 so that p is in (0,1)
+    A private method _clean trims the data by machine epsilon so that p is
+    in (0,1)
     """
 
-    tol = 1.0e-10
-
     def __init__(self, n=1):
         self.n = n
 
     def _clean(self, p):
-        return np.clip(p, Binomial.tol, 1 - Binomial.tol)
+        return np.clip(p, FLOAT_EPS, 1 - FLOAT_EPS)
 
     def __call__(self, mu):
         """
@@ -208,16 +208,15 @@ class NegativeBinomial(object):
     Alias for NegativeBinomial:
     nbinom = NegativeBinomial()
 
-    A private method _clean trims the data by 1e-10 so that p is in (0,inf)
+    A private method _clean trims the data by machine epsilon so that p is
+    in (0,inf)
     '''
 
-    tol = 1.0e-10
-
     def __init__(self, alpha=1.):
         self.alpha = alpha
 
     def _clean(self, p):
-        return np.clip(p, NegativeBinomial.tol, np.inf)
+        return np.clip(p, FLOAT_EPS, np.inf)
 
     def __call__(self, mu):
         """
@@ -234,7 +233,7 @@ def __call__(self, mu):
             variance = mu + alpha*mu**2
         """
         p = self._clean(mu)
-        return mu + self.alpha*mu**2
+        return p + self.alpha*p**2
 
 nbinom = NegativeBinomial()
 nbinom.__doc__ = """


Test patch: