Repository: scipy/scipy

Issue:
cluster.vq.whiten return nan for all zeros column in observations
When a column (feature) of the observations is always zero, then its standard deviation will be zero and `vq.whiten` will return a nan, which will make the kmeans functions return meaningless results.

``` python
In [3]: from scipy.cluster import vq

In [4]: data = np.random.rand(10, 3)

In [5]: data[:, 0] = 0.0

In [6]: vq.whiten(data)
scipy/cluster/vq.py:134: RuntimeWarning: invalid value encountered in true_divide
  return obs / std_dev
Out[6]: 
array([[        nan,  3.22503459,  2.94333012],
       [        nan,  1.77495108,  0.66684013],
       [        nan,  1.00021902,  0.54654024],
       [        nan,  0.12284469,  1.5192485 ],
       [        nan,  1.45337014,  2.1255432 ],
       [        nan,  3.22419602,  1.87212932],
       [        nan,  1.54394392,  3.49327298],
       [        nan,  0.96204335,  1.6044576 ],
       [        nan,  1.96667524,  1.74163543],
       [        nan,  0.33583457,  3.58268093]])

In [7]: vq.kmeans(vq.whiten(data), 3)
Out[7]: (array([[        nan,  1.33603787,  2.51857499]]), 1.0000000000000002e+100)
```



Discussion:

Resolving patch:
diff --git a/scipy/cluster/vq.py b/scipy/cluster/vq.py
--- a/scipy/cluster/vq.py
+++ b/scipy/cluster/vq.py
@@ -81,9 +81,8 @@
 import warnings
 
 from numpy.random import randint
-from numpy import shape, zeros, sqrt, argmin, minimum, array, \
-     newaxis, arange, compress, equal, common_type, single, double, take, \
-     std, mean
+from numpy import (shape, zeros, sqrt, argmin, minimum, array, newaxis,
+    arange, compress, equal, common_type, single, double, take, std, mean)
 import numpy as np
 
 
@@ -131,6 +130,12 @@ def whiten(obs):
 
     """
     std_dev = std(obs, axis=0)
+    zero_std_mask = std_dev == 0
+    if zero_std_mask.any():
+        std_dev[zero_std_mask] = 1.0
+        warnings.warn("Some columns have standard deviation zero. "
+                      "The values of these columns will not change.",
+                      RuntimeWarning)
     return obs / std_dev
 
 


Test patch:
diff --git a/scipy/cluster/tests/test_vq.py b/scipy/cluster/tests/test_vq.py
--- a/scipy/cluster/tests/test_vq.py
+++ b/scipy/cluster/tests/test_vq.py
@@ -8,10 +8,11 @@
 import warnings
 
 import numpy as np
-from numpy.testing import assert_array_equal, assert_array_almost_equal, \
-        TestCase, run_module_suite, assert_raises
+from numpy.testing import (assert_array_equal, assert_array_almost_equal,
+    TestCase, run_module_suite, assert_raises, assert_allclose)
 
-from scipy.cluster.vq import kmeans, kmeans2, py_vq, py_vq2, vq, ClusterError
+from scipy.cluster.vq import (kmeans, kmeans2, py_vq, py_vq2, vq, whiten,
+    ClusterError)
 try:
     from scipy.cluster import _vq
     TESTC = True
@@ -19,7 +20,7 @@
     print("== Error while importing _vq, not testing C imp of vq ==")
     TESTC = False
 
-#Optional:
+# Optional:
 # import modules that are located in the same directory as this file.
 DATAFILE1 = os.path.join(os.path.dirname(__file__), "data.txt")
 
@@ -39,6 +40,30 @@
 LABEL1 = np.array([0, 1, 2, 2, 2, 2, 1, 2, 1, 1, 1])
 
 
+class TestWhiten(TestCase):
+    def test_whiten(self):
+        obs = np.array([[0.98744510, 0.82766775],
+                        [0.62093317, 0.19406729],
+                        [0.87545741, 0.00735733],
+                        [0.85124403, 0.26499712],
+                        [0.45067590, 0.45464607]])
+        result = np.array([[5.08738849, 2.97091878],
+                           [3.19909255, 0.69660580],
+                           [4.51041982, 0.02640918],
+                           [4.38567074, 0.95120889],
+                           [2.32191480, 1.63195503]])
+        assert_allclose(whiten(obs), result, rtol=1e-5)
+
+    def test_whiten_zero_std(self):
+        obs = np.array([[0., 1., 0.74109533],
+                        [0., 1., 0.34243798],
+                        [0., 1., 0.96785929]])
+        result = np.array([[0., 1.0, 2.86666544],
+                           [0., 1.0, 1.32460034],
+                           [0., 1.0, 3.74382172]])
+        assert_allclose(whiten(obs), result, rtol=1e-5)
+
+
 class TestVq(TestCase):
     def test_py_vq(self):
         initc = np.concatenate(([[X[0]], [X[1]], [X[2]]]))
@@ -59,14 +84,14 @@ def test_vq(self):
         else:
             print("== not testing C imp of vq ==")
 
-    #def test_py_vq_1d(self):
-    #    """Test special rank 1 vq algo, python implementation."""
-    #    data = X[:, 0]
-    #    initc = data[:3]
-    #    a, b = _py_vq_1d(data, initc)
-    #    ta, tb = py_vq(data[:, np.newaxis], initc[:, np.newaxis])
-    #    assert_array_equal(a, ta)
-    #    assert_array_equal(b, tb)
+    # def test_py_vq_1d(self):
+    #     """Test special rank 1 vq algo, python implementation."""
+    #     data = X[:, 0]
+    #     initc = data[:3]
+    #     a, b = _py_vq_1d(data, initc)
+    #     ta, tb = py_vq(data[:, np.newaxis], initc[:, np.newaxis])
+    #     assert_array_equal(a, ta)
+    #     assert_array_equal(b, tb)
 
     def test_vq_1d(self):
         """Test special rank 1 vq algo, python implementation."""