Repository: statsmodels/statsmodels

Issue:
`KDEUnivariate.fit()` crashes on particular data
#### Describe the bug

Hi, and thanks for making statsmodels!

I encountered an exception which I think is a bug. The `nonparametric.kde.KDEUnivariate` class throws an exception when I try to fit it on a specific piece of data. The data contains several equal values, however it is not "singular", there are different values in the array, too. Please see the reproducer below.

#### Code Sample

```python
import numpy as np
import statsmodels
from statsmodels.nonparametric.kde import KDEUnivariate

print('Numpy:', np.__version__)
print('Statsmodels:', statsmodels.__version__)
print()

x = np.array([4.59511985, 4.59511985, 4.59511985, 4.59511985, 4.59511985, 4.59511985, 4.59511985, 4.59511985, 4.59511985, 4.59511985, 5.67332327, 6.19847872, 7.43189192])
kde = KDEUnivariate(x)
kde.fit()
```

#### Expected Output

The `kde.fit()` method call should fit the model without throwing an exception.

#### Actual output

```
Numpy: 1.18.3
Statsmodels: 0.11.1

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/statsmodels/nonparametric/kde.py", line 451, in kdensityfft
    bw = float(bw)
ValueError: could not convert string to float: 'normal_reference'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "repr.py", line 11, in <module>
    kde.fit()
  File "/usr/local/lib/python3.7/site-packages/statsmodels/nonparametric/kde.py", line 140, in fit
    clip=clip, cut=cut)
  File "/usr/local/lib/python3.7/site-packages/statsmodels/nonparametric/kde.py", line 453, in kdensityfft
    bw = bandwidths.select_bandwidth(X, bw, kern) # will cross-val fit this pattern?
  File "/usr/local/lib/python3.7/site-packages/statsmodels/nonparametric/bandwidths.py", line 174, in select_bandwidth
    raise RuntimeError(err)
RuntimeError: Selected KDE bandwidth is 0. Cannot estimate density.
```

#### Output of ``import statsmodels.api as sm; sm.show_versions()``

<details>

INSTALLED VERSIONS
------------------
Python: 3.7.6.final.0
OS: Darwin 16.7.0 Darwin Kernel Version 16.7.0: Sun Jun  2 20:26:31 PDT 2019; root:xnu-3789.73.50~1/RELEASE_X86_64 x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8

statsmodels
===========

Installed: 0.11.1 (/usr/local/lib/python3.7/site-packages/statsmodels)

Required Dependencies
=====================

cython: Not installed
numpy: 1.18.3 (/usr/local/lib/python3.7/site-packages/numpy)
scipy: 1.4.1 (/usr/local/lib/python3.7/site-packages/scipy)
pandas: 1.0.3 (/usr/local/lib/python3.7/site-packages/pandas)
    dateutil: 2.7.5 (/usr/local/lib/python3.7/site-packages/dateutil)
patsy: 0.5.1 (/usr/local/lib/python3.7/site-packages/patsy)

Optional Dependencies
=====================

matplotlib: 3.2.1 (/usr/local/lib/python3.7/site-packages/matplotlib)
    backend: MacOSX 
cvxopt: Not installed
joblib: 0.14.1 (/usr/local/lib/python3.7/site-packages/joblib)

Developer Tools
================

IPython: Not installed
    jinja2: Not installed
sphinx: Not installed
    pygments: Not installed
pytest: 5.3.4 (/usr/local/lib/python3.7/site-packages/pytest)
virtualenv: 16.6.0 (/usr/local/lib/python3.7/site-packages)

</details>



Discussion:
This is the correct behavior. You have asked it to compute the bandwidth for you and it computes a bandwidth of 0, in which case the KDE cannot be estimated. In these cases, the user needs to provide a bandwidth estimate.

Maybe not; simple fix is to ensure that sigma > 0.


Resolving patch:
diff --git a/statsmodels/nonparametric/bandwidths.py b/statsmodels/nonparametric/bandwidths.py
--- a/statsmodels/nonparametric/bandwidths.py
+++ b/statsmodels/nonparametric/bandwidths.py
@@ -1,10 +1,11 @@
 import numpy as np
-from scipy.stats import scoreatpercentile as sap
+from scipy.stats import scoreatpercentile
 
 from statsmodels.compat.pandas import Substitution
 from statsmodels.sandbox.nonparametric import kernels
 
-def _select_sigma(X):
+
+def _select_sigma(x, percentile=25):
     """
     Returns the smaller of std(X, ddof=1) or normalized IQR(X) over axis 0.
 
@@ -12,12 +13,14 @@ def _select_sigma(X):
     ----------
     Silverman (1986) p.47
     """
-#    normalize = norm.ppf(.75) - norm.ppf(.25)
+    # normalize = norm.ppf(.75) - norm.ppf(.25)
     normalize = 1.349
-#    IQR = np.subtract.reduce(percentile(X, [75,25],
-#                             axis=axis), axis=axis)/normalize
-    IQR = (sap(X, 75) - sap(X, 25))/normalize
-    return np.minimum(np.std(X, axis=0, ddof=1), IQR)
+    IQR = (scoreatpercentile(x, 75) - scoreatpercentile(x, 25)) / normalize
+    std_dev = np.std(x, axis=0, ddof=1)
+    if IQR > 0:
+        return np.minimum(std_dev, IQR)
+    else:
+        return std_dev
 
 
 ## Univariate Rule of Thumb Bandwidths ##
@@ -170,7 +173,9 @@ def select_bandwidth(x, bw, kernel):
     bandwidth = bandwidth_funcs[bw](x, kernel)
     if np.any(bandwidth == 0):
         # eventually this can fall back on another selection criterion.
-        err = "Selected KDE bandwidth is 0. Cannot estimate density."
+        err = "Selected KDE bandwidth is 0. Cannot estimate density. " \
+              "Either provide the bandwidth during initialization or use " \
+              "an alternative method."
         raise RuntimeError(err)
     else:
         return bandwidth


Test patch:
diff --git a/statsmodels/nonparametric/tests/test_kde.py b/statsmodels/nonparametric/tests/test_kde.py
--- a/statsmodels/nonparametric/tests/test_kde.py
+++ b/statsmodels/nonparametric/tests/test_kde.py
@@ -1,34 +1,36 @@
 import os
+
 import numpy.testing as npt
 import numpy as np
 import pytest
+from scipy import stats
+
 from statsmodels.distributions.mixture_rvs import mixture_rvs
 from statsmodels.nonparametric.kde import KDEUnivariate as KDE
 import statsmodels.sandbox.nonparametric.kernels as kernels
-from scipy import stats
 
 # get results from Stata
 
 curdir = os.path.dirname(os.path.abspath(__file__))
-rfname = os.path.join(curdir,'results','results_kde.csv')
-#print rfname
+rfname = os.path.join(curdir, 'results', 'results_kde.csv')
+# print rfname
 KDEResults = np.genfromtxt(open(rfname, 'rb'), delimiter=",", names=True)
 
-rfname = os.path.join(curdir,'results','results_kde_univ_weights.csv')
+rfname = os.path.join(curdir, 'results', 'results_kde_univ_weights.csv')
 KDEWResults = np.genfromtxt(open(rfname, 'rb'), delimiter=",", names=True)
 
 # get results from R
 curdir = os.path.dirname(os.path.abspath(__file__))
-rfname = os.path.join(curdir,'results','results_kcde.csv')
-#print rfname
+rfname = os.path.join(curdir, 'results', 'results_kcde.csv')
+# print rfname
 KCDEResults = np.genfromtxt(open(rfname, 'rb'), delimiter=",", names=True)
 
-
 # setup test data
 
 np.random.seed(12345)
-Xi = mixture_rvs([.25,.75], size=200, dist=[stats.norm, stats.norm],
-                kwargs = (dict(loc=-1,scale=.5),dict(loc=1,scale=.5)))
+Xi = mixture_rvs([.25, .75], size=200, dist=[stats.norm, stats.norm],
+                 kwargs=(dict(loc=-1, scale=.5), dict(loc=1, scale=.5)))
+
 
 class TestKDEExceptions(object):
 
@@ -56,8 +58,8 @@ def test_non_gaussian_fft_exception(self):
         with pytest.raises(NotImplementedError):
             self.kde.fit(kernel="epa", gridsize=50, fft=True, bw="silverman")
 
-class CheckKDE(object):
 
+class CheckKDE(object):
     decimal_density = 7
 
     def test_density(self):
@@ -69,9 +71,9 @@ def test_evaluate(self):
         # fails for Epan, Triangular and Biweight, only Gaussian is correct
         # added it as test method to TestKDEGauss below
         # inDomain is not vectorized
-        #kde_vals = self.res1.evaluate(self.res1.support)
+        # kde_vals = self.res1.evaluate(self.res1.support)
         kde_vals = [np.squeeze(self.res1.evaluate(xi)) for xi in self.res1.support]
-        kde_vals = np.squeeze(kde_vals)  #kde_vals is a "column_list"
+        kde_vals = np.squeeze(kde_vals)  # kde_vals is a "column_list"
         mask_valid = np.isfinite(kde_vals)
         # TODO: nans at the boundaries
         kde_vals[~mask_valid] = 0
@@ -88,9 +90,9 @@ def setup_class(cls):
         cls.res_density = KDEResults["gau_d"]
 
     def test_evaluate(self):
-        #kde_vals = self.res1.evaluate(self.res1.support)
+        # kde_vals = self.res1.evaluate(self.res1.support)
         kde_vals = [self.res1.evaluate(xi) for xi in self.res1.support]
-        kde_vals = np.squeeze(kde_vals)  #kde_vals is a "column_list"
+        kde_vals = np.squeeze(kde_vals)  # kde_vals is a "column_list"
         mask_valid = np.isfinite(kde_vals)
         # TODO: nans at the boundaries
         kde_vals[~mask_valid] = 0
@@ -128,6 +130,7 @@ def setup_class(cls):
         cls.res1 = res1
         cls.res_density = KDEResults["epa2_d"]
 
+
 class TestKDETriangular(CheckKDE):
     @classmethod
     def setup_class(cls):
@@ -136,6 +139,7 @@ def setup_class(cls):
         cls.res1 = res1
         cls.res_density = KDEResults["tri_d"]
 
+
 class TestKDEBiweight(CheckKDE):
     @classmethod
     def setup_class(cls):
@@ -146,8 +150,8 @@ def setup_class(cls):
 
 
 # FIXME: enable/xfail/skip or delete
-#NOTE: This is a knownfailure due to a definitional difference of Cosine kernel
-#class TestKDECosine(CheckKDE):
+# NOTE: This is a knownfailure due to a definitional difference of Cosine kernel
+# class TestKDECosine(CheckKDE):
 #    @classmethod
 #    def setup_class(cls):
 #        res1 = KDE(Xi)
@@ -156,23 +160,23 @@ def setup_class(cls):
 #        cls.res_density = KDEResults["cos_d"]
 
 
-#weighted estimates taken from matlab so we can allow len(weights) != gridsize
+# weighted estimates taken from matlab so we can allow len(weights) != gridsize
 class TestKdeWeights(CheckKDE):
 
     @classmethod
     def setup_class(cls):
         res1 = KDE(Xi)
-        weights = np.linspace(1,100,200)
+        weights = np.linspace(1, 100, 200)
         res1.fit(kernel="gau", gridsize=50, weights=weights, fft=False,
-                    bw="silverman")
+                 bw="silverman")
         cls.res1 = res1
-        rfname = os.path.join(curdir,'results','results_kde_weights.csv')
-        cls.res_density = np.genfromtxt(open(rfname, 'rb'), skip_header=1)
+        fname = os.path.join(curdir, 'results', 'results_kde_weights.csv')
+        cls.res_density = np.genfromtxt(open(fname, 'rb'), skip_header=1)
 
     def test_evaluate(self):
-        #kde_vals = self.res1.evaluate(self.res1.support)
+        # kde_vals = self.res1.evaluate(self.res1.support)
         kde_vals = [self.res1.evaluate(xi) for xi in self.res1.support]
-        kde_vals = np.squeeze(kde_vals)  #kde_vals is a "column_list"
+        kde_vals = np.squeeze(kde_vals)  # kde_vals is a "column_list"
         mask_valid = np.isfinite(kde_vals)
         # TODO: nans at the boundaries
         kde_vals[~mask_valid] = 0
@@ -183,13 +187,14 @@ def test_evaluate(self):
 class TestKDEGaussFFT(CheckKDE):
     @classmethod
     def setup_class(cls):
-        cls.decimal_density = 2 # low accuracy because binning is different
+        cls.decimal_density = 2  # low accuracy because binning is different
         res1 = KDE(Xi)
         res1.fit(kernel="gau", fft=True, bw="silverman")
         cls.res1 = res1
-        rfname2 = os.path.join(curdir,'results','results_kde_fft.csv')
+        rfname2 = os.path.join(curdir, 'results', 'results_kde_fft.csv')
         cls.res_density = np.genfromtxt(open(rfname2, 'rb'))
 
+
 class CheckKDEWeights(object):
 
     @classmethod
@@ -214,14 +219,14 @@ def test_evaluate(self):
         if self.kernel_name == 'cos':
             pytest.skip("Cosine kernel fails against Stata")
         kde_vals = [self.res1.evaluate(xi) for xi in self.x]
-        kde_vals = np.squeeze(kde_vals)  #kde_vals is a "column_list"
+        kde_vals = np.squeeze(kde_vals)  # kde_vals is a "column_list"
         npt.assert_almost_equal(kde_vals, self.res_density,
                                 self.decimal_density)
 
     def test_compare(self):
         xx = self.res1.support
         kde_vals = [np.squeeze(self.res1.evaluate(xi)) for xi in xx]
-        kde_vals = np.squeeze(kde_vals)  #kde_vals is a "column_list"
+        kde_vals = np.squeeze(kde_vals)  # kde_vals is a "column_list"
         mask_valid = np.isfinite(kde_vals)
         # TODO: nans at the boundaries
         kde_vals[~mask_valid] = 0
@@ -236,7 +241,7 @@ def test_compare(self):
         npt.assert_allclose(v, v_direct, rtol=1e-10)
 
         ci = kern.density_confint(kde_vals, nobs)
-        crit = 1.9599639845400545 #stats.norm.isf(0.05 / 2)
+        crit = 1.9599639845400545  # stats.norm.isf(0.05 / 2)
         hw = kde_vals - ci[:, 0]
         npt.assert_allclose(hw, crit * np.sqrt(v), rtol=1e-10)
         hw = ci[:, 1] - kde_vals
@@ -265,54 +270,48 @@ def test_kernel_constants(self):
 
 
 class TestKDEWGauss(CheckKDEWeights):
-
     kernel_name = "gau"
     res_kernel_name = "x_gau_wd"
 
 
 class TestKDEWEpa(CheckKDEWeights):
-
     kernel_name = "epa"
     res_kernel_name = "x_epan2_wd"
 
 
 class TestKDEWTri(CheckKDEWeights):
-
     kernel_name = "tri"
     res_kernel_name = "x_" + kernel_name + "_wd"
 
 
 class TestKDEWBiw(CheckKDEWeights):
-
     kernel_name = "biw"
     res_kernel_name = "x_bi_wd"
 
 
 class TestKDEWCos(CheckKDEWeights):
-
     kernel_name = "cos"
     res_kernel_name = "x_cos_wd"
 
 
 class TestKDEWCos2(CheckKDEWeights):
-
     kernel_name = "cos2"
     res_kernel_name = "x_cos_wd"
 
 
-class T_estKDEWRect(CheckKDEWeights):
-    #TODO in docstring but not in kernel_switch
+class _TestKDEWRect(CheckKDEWeights):
+    # TODO in docstring but not in kernel_switch
     kernel_name = "rect"
     res_kernel_name = "x_rec_wd"
 
 
-class T_estKDEWPar(CheckKDEWeights):
+class _TestKDEWPar(CheckKDEWeights):
     # TODO in docstring but not implemented in kernels
     kernel_name = "par"
     res_kernel_name = "x_par_wd"
 
 
-class TestKdeRefit():
+class TestKdeRefit:
     np.random.seed(12345)
     data1 = np.random.randn(100) * 100
     pdf = KDE(data1)
@@ -327,9 +326,18 @@ class TestKdeRefit():
                                     getattr(pdf2, attr)[:10]))
 
 
-class TestNormConstant():
-
+class TestNormConstant:
     def test_norm_constant_calculation(self):
-        custom_gauss = kernels.CustomKernel(lambda x: np.exp(-x**2/2.0))
+        custom_gauss = kernels.CustomKernel(lambda x: np.exp(-x ** 2 / 2.0))
         gauss_true_const = 0.3989422804014327
         npt.assert_almost_equal(gauss_true_const, custom_gauss.norm_const)
+
+
+def test_kde_bw_positive():
+    # GH 6679
+    x = np.array([4.59511985, 4.59511985, 4.59511985, 4.59511985, 4.59511985,
+                  4.59511985, 4.59511985, 4.59511985, 4.59511985, 4.59511985,
+                  5.67332327, 6.19847872, 7.43189192])
+    kde = KDE(x)
+    kde.fit()
+    assert kde.bw > 0