Repository: graspologic-org/graspologic

Issue:
Make cosine similarity handling in AutoGMM less fragile
Currently, errors are thrown when trying to use cosine similarity on a dataset that contains at least one 0-vector. 

I think we should have AGMM skip cosine similarity in this case, potentially throwing a warning. 


Discussion:
I'll get this done this semester

Resolving patch:
diff --git a/graspy/cluster/autogmm.py b/graspy/cluster/autogmm.py
--- a/graspy/cluster/autogmm.py
+++ b/graspy/cluster/autogmm.py
@@ -17,12 +17,15 @@
 from sklearn.cluster import AgglomerativeClustering
 from sklearn.metrics import adjusted_rand_score
 from sklearn.mixture import GaussianMixture
-from sklearn.mixture.gaussian_mixture import (
+from sklearn.mixture._gaussian_mixture import (
     _compute_precision_cholesky,
     _estimate_gaussian_parameters,
 )
 from sklearn.model_selection import ParameterGrid
+from sklearn.utils._testing import ignore_warnings
+from sklearn.exceptions import ConvergenceWarning
 from joblib import Parallel, delayed
+import warnings
 
 from .base import BaseCluster
 
@@ -70,6 +73,10 @@ class AutoGMMCluster(BaseCluster):
         If a list/array, it must be a list/array of strings containing only
         'euclidean', 'manhattan', 'cosine', and/or 'none'.
 
+        Note that cosine similarity can only work when all of the rows are not the zero vector.
+        If the input matrix has a zero row, cosine similarity will be skipped and a warning will
+        be thrown.
+
     linkage : {'ward','complete','average','single', 'all' (default)}, optional
         String or list/array describing the type of linkages to use in agglomeration.
         If a string, it must be one of:
@@ -379,6 +386,9 @@ def __init__(
         self.max_agglom_size = max_agglom_size
         self.n_jobs = n_jobs
 
+    # ignoring warning here because if convergence is not reached, the regularization
+    # is automatically increased
+    @ignore_warnings(category=ConvergenceWarning)
     def _fit_cluster(self, X, y, params):
         label_init = self.label_init
         if label_init is not None:
@@ -442,8 +452,8 @@ def _fit_cluster(self, X, y, params):
             break
 
         if y is not None:
-            predictions = model.predict(X)
-            ari = adjusted_rand_score(y, predictions)
+            self.predictions = model.predict(X)
+            ari = adjusted_rand_score(y, self.predictions)
         else:
             ari = float("nan")
         results = {
@@ -504,9 +514,13 @@ def fit(self, X, y=None):
             raise ValueError(msg)
         # check if X contains the 0 vector
         if np.any(~X.any(axis=1)) and ("cosine" in self.affinity):
-            msg = "When using cosine affinity, X cannot contain the 0 vector, but "
-            msg += "X[{},] is 0".format(np.where(~X.any(axis=1)))
-            raise ValueError(msg)
+            if isinstance(self.affinity, np.ndarray):
+                self.affinity = np.delete(
+                    self.affinity, np.argwhere(self.affinity == "cosine")
+                )
+            if isinstance(self.affinity, list):
+                self.affinity.remove("cosine")
+            warnings.warn("X contains a zero vector, will not run cosine affinity.")
 
         label_init = self.label_init
         if label_init is not None:


Test patch:
diff --git a/tests/cluster/test_autogmm.py b/tests/cluster/test_autogmm.py
--- a/tests/cluster/test_autogmm.py
+++ b/tests/cluster/test_autogmm.py
@@ -161,6 +161,28 @@ def test_cosine_on_0():
         AutoGMM.fit(X)
 
 
+def test_cosine_with_0():
+    X = np.array(
+        [
+            [0, 1, 0],
+            [1, 0, 1],
+            [0, 0, 0],
+            [1, 1, 0],
+            [0, 0, 1],
+            [0, 1, 1],
+            [1, 1, 1],
+            [1, 0, 0],
+            [0, 1, 1],
+            [1, 1, 0],
+            [0, 1, 0],
+        ]
+    )
+
+    with pytest.warns(UserWarning):
+        AutoGMM = AutoGMMCluster(min_components=2, affinity="all")
+        AutoGMM.fit(X)
+
+
 def test_no_y():
     np.random.seed(1)