Repository: statsmodels/statsmodels

Issue:
BUG: GLM fit does not allow int scale instead of float
scale argument in GLM.fit is too picky

`scale = 1.` (float) works
`res_glm_fs = GLM(eff, np.ones(len(eff)), var_weights=1/(var_eff+res3.tau2)).fit(scale=1.)`

`scale = 1` (int) raises an intentional (?) exception 
"ValueError: Scale 1 with type <class 'int'> not understood"
`res_glm_fs = GLM(eff, np.ones(len(eff)), var_weights=1/(var_eff+res3.tau2)).fit(scale=1)`
https://github.com/statsmodels/statsmodels/issues/6585#issuecomment-609559090

I don't see a reason why int shouldn't be allowed. (I haven't looked at the relevant code.)

docstring says `float`, so it's a annoying feature and not really a bug, taking float too literal


Discussion:
int exog breaks Poisson, works with float exog (irls raises perfect prediction)

```

count1, n1, count2, n2 = 60, 51477.5, 30, 54308.7
y = [count1, count2]
x = np.asarray([[1, 1], [1, 0]])
exposure = np.asarray([n1, n2])
mod = GLM(y, x, exposure=exposure, family=families.Poisson())
res = mod.fit(method='bfgs', max_start_irls=0)
print(res.summary())
```

```
statsmodels\statsmodels_py37\statsmodels\statsmodels\genmod\generalized_linear_model.py in hessian(self, params, scale, observed)
    579 
    580         factor = self.hessian_factor(params, scale=scale, observed=observed)
--> 581         np.multiply(self.exog.T, factor, out=tmp.T)
    582         return -tmp.T.dot(self.exog)
    583 

TypeError: Cannot cast ufunc multiply output from dtype('float64') to dtype('int32') with casting rule 'same_kind'
```

Resolving patch:
diff --git a/statsmodels/genmod/generalized_linear_model.py b/statsmodels/genmod/generalized_linear_model.py
--- a/statsmodels/genmod/generalized_linear_model.py
+++ b/statsmodels/genmod/generalized_linear_model.py
@@ -23,6 +23,7 @@
 
 from statsmodels.tools.decorators import (cache_readonly,
                                           cached_data, cached_value)
+from statsmodels.tools.validation import float_like
 from statsmodels.compat.pandas import Appender
 
 import statsmodels.base.model as base
@@ -399,6 +400,7 @@ def loglike_mu(self, mu, scale=1.):
         """
         Evaluate the log-likelihood for a generalized linear model.
         """
+        scale = float_like(scale, "scale")
         return self.family.loglike(self.endog, mu, self.var_weights,
                                    self.freq_weights, scale)
 
@@ -406,6 +408,7 @@ def loglike(self, params, scale=None):
         """
         Evaluate the log-likelihood for a generalized linear model.
         """
+        scale = float_like(scale, "scale", optional=True)
         lin_pred = np.dot(self.exog, params) + self._offset_exposure
         expval = self.family.link.inverse(lin_pred)
         if scale is None:
@@ -432,7 +435,7 @@ def score_obs(self, params, scale=None):
             The first derivative of the loglikelihood function evaluated at
             params for each observation.
         """
-
+        scale = float_like(scale, "scale", optional=True)
         score_factor = self.score_factor(params, scale=scale)
         return score_factor[:, None] * self.exog
 
@@ -454,6 +457,7 @@ def score(self, params, scale=None):
             The first derivative of the loglikelihood function calculated as
             the sum of `score_obs`
         """
+        scale = float_like(scale, "scale", optional=True)
         score_factor = self.score_factor(params, scale=scale)
         return np.dot(score_factor, self.exog)
 
@@ -477,6 +481,7 @@ def score_factor(self, params, scale=None):
             A 1d weight vector used in the calculation of the score_obs.
             The score_obs are obtained by `score_factor[:, None] * exog`
         """
+        scale = float_like(scale, "scale", optional=True)
         mu = self.predict(params)
         if scale is None:
             scale = self.estimate_scale(mu)
@@ -573,8 +578,8 @@ def hessian(self, params, scale=None, observed=None):
                 observed = False
             else:
                 observed = True
-
-        tmp = getattr(self, '_tmp_like_exog', np.empty_like(self.exog))
+        scale = float_like(scale, "scale", optional=True)
+        tmp = getattr(self, '_tmp_like_exog', np.empty_like(self.exog, dtype=float))
 
         factor = self.hessian_factor(params, scale=scale, observed=observed)
         np.multiply(self.exog.T, factor, out=tmp.T)
@@ -584,6 +589,7 @@ def information(self, params, scale=None):
         """
         Fisher information matrix.
         """
+        scale = float_like(scale, "scale", optional=True)
         return self.hessian(params, scale=scale, observed=False)
 
     def _deriv_mean_dparams(self, params):
@@ -624,6 +630,7 @@ def _deriv_score_obs_dendog(self, params, scale=None):
             can is given by `score_factor0[:, None] * exog` where
             `score_factor0` is the score_factor without the residual.
         """
+        scale = float_like(scale, "scale", optional=True)
         mu = self.predict(params)
         if scale is None:
             scale = self.estimate_scale(mu)
@@ -874,7 +881,7 @@ def predict(self, params, exog=None, exposure=None, offset=None,
         else:
             return self.family.fitted(linpred)
 
-    def get_distribution(self, params, scale=1, exog=None, exposure=None,
+    def get_distribution(self, params, scale=1., exog=None, exposure=None,
                          offset=None):
         """
         Return a random number generator for the predictive distribution.
@@ -902,7 +909,7 @@ def get_distribution(self, params, scale=1, exog=None, exposure=None,
         to fit the model.  If any other value is used for ``n``, misleading
         results will be produced.
         """
-
+        scale = float_like(scale, "scale", optional=True)
         fit = self.predict(params, exog, exposure, offset, linear=False)
 
         import scipy.stats.distributions as dist
@@ -1016,6 +1023,20 @@ def fit(self, start_params=None, maxiter=100, method='IRLS', tol=1e-8,
         instance of the IRLS iteration is attached to the results instance
         as `results_wls` attribute.
         """
+        if isinstance(scale, str):
+            scale = scale.lower()
+            if scale not in ("x2", "dev"):
+                raise ValueError(
+                    "scale must be either X2 or dev when a string."
+                )
+        elif scale is not None:
+            # GH-6627
+            try:
+                scale = float(scale)
+            except Exception as exc:
+                raise type(exc)(
+                    "scale must be a float if given and no a string."
+                )
         self.scaletype = scale
 
         if method.lower() == "irls":
@@ -1026,7 +1047,7 @@ def fit(self, start_params=None, maxiter=100, method='IRLS', tol=1e-8,
                                   cov_kwds=cov_kwds, use_t=use_t, **kwargs)
         else:
             self._optim_hessian = kwargs.get('optim_hessian')
-            self._tmp_like_exog = np.empty_like(self.exog)
+            self._tmp_like_exog = np.empty_like(self.exog, dtype=float)
             fit_ = self._fit_gradient(start_params=start_params,
                                       method=method,
                                       maxiter=maxiter,


Test patch:
diff --git a/statsmodels/genmod/tests/test_glm.py b/statsmodels/genmod/tests/test_glm.py
--- a/statsmodels/genmod/tests/test_glm.py
+++ b/statsmodels/genmod/tests/test_glm.py
@@ -18,7 +18,7 @@
 from statsmodels.discrete import discrete_model as discrete
 from statsmodels.tools.sm_exceptions import DomainWarning
 from statsmodels.tools.numdiff import approx_fprime, approx_hess
-from statsmodels.datasets import cpunish
+from statsmodels.datasets import cpunish, longley
 
 # Test Precisions
 DECIMAL_4 = 4
@@ -2318,3 +2318,24 @@ def test_non_invertible_hessian_fails_summary():
         mod = sm.GLM(data.endog, data.exog, family=sm.families.Gamma())
         res = mod.fit(maxiter=1, method='bfgs', max_start_irls=0)
         res.summary()
+
+
+def test_int_scale():
+    # GH-6627, make sure it works with int scale
+    data = longley.load(as_pandas=True)
+    mod = GLM(data.endog, data.exog, family=sm.families.Gaussian())
+    res = mod.fit(scale=1)
+    assert isinstance(res.params, pd.Series)
+    assert res.scale.dtype == np.float64
+
+
+@pytest.mark.parametrize("dtype", [np.int8, np.int16, np.int32, np.int64])
+def test_int_exog(dtype):
+    # GH-6627, make use of floats internally
+    count1, n1, count2, n2 = 60, 51477.5, 30, 54308.7
+    y = [count1, count2]
+    x = np.asarray([[1, 1], [1, 0]]).astype(dtype)
+    exposure = np.asarray([n1, n2])
+    mod = GLM(y, x, exposure=exposure, family=sm.families.Poisson())
+    res = mod.fit(method='bfgs', max_start_irls=0)
+    assert isinstance(res.params, np.ndarray)