Repository: scikit-learn/scikit-learn
Pull request: TST Fix test_float_precision fails on arm
Fixes #17796
(Extracted from #17622)

The failure was that `inertia[float64]=325.9393` and `inertia[float32]=325.9395` were not equal up to the 4th decimal place. But 4th decimal place corresponds to a relative error of ~3e-7 which is barely above machine epsilon for float32. There are many operations involved in computing the inertia so rounding errors accumulate and we can't expect such a high precision.

Problem statement:
[0.23.1] test_float_precision[True-KMeans] failes on arm(rhel) processors
#### Describe the bug
On arm processors (AWS:gravition2, rhel), I get the following failure in version 0.23.1
```
Estimator = <class 'sklearn.cluster._kmeans.KMeans'>, is_sparse = True

    @pytest.mark.parametrize('Estimator', [KMeans, MiniBatchKMeans])
    @pytest.mark.parametrize('is_sparse', [False, True])
    def test_float_precision(Estimator, is_sparse):
    
        estimator = Estimator(n_init=1, random_state=30)
    
        inertia = {}
        X_new = {}
        centers = {}
    
        for dtype in [np.float64, np.float32]:
            if is_sparse:
                X_test = sp.csr_matrix(X_csr, dtype=dtype)
            else:
                X_test = X.astype(dtype)
            estimator.fit(X_test)
            # dtype of cluster centers has to be the dtype of the input
            # data
            assert estimator.cluster_centers_.dtype == dtype
            inertia[dtype] = estimator.inertia_
            X_new[dtype] = estimator.transform(X_test)
            centers[dtype] = estimator.cluster_centers_
            # ensure the extracted row is a 2d array
            assert estimator.predict(X_test[:1]) == estimator.labels_[0]
            if hasattr(estimator, 'partial_fit'):
                estimator.partial_fit(X_test[0:3])
                # dtype of cluster centers has to stay the same after
                # partial_fit
                assert estimator.cluster_centers_.dtype == dtype
    
        # compare arrays with low precision since the difference between
        # 32 and 64 bit sometimes makes a difference up to the 4th decimal
        # place
        assert_array_almost_equal(inertia[np.float32], inertia[np.float64],
>                                 decimal=4)
E       AssertionError: 
E       Arrays are not almost equal to 4 decimals
E       
E       (mismatch 100.0%)
E        x: array(325.9395)
E        y: array(325.9393)

sklearn/cluster/tests/test_k_means.py:863: AssertionError
```

#### Steps/Code to Reproduce
`pytest -v sklearn/cluster/tests/test_k_means.py::test_float_precision[True-KMeans]`

#### Expected Results
PASSED is thrown. 

#### Actual Results
FAILED is thrown.
```
        assert_array_almost_equal(inertia[np.float32], inertia[np.float64],
>                                 decimal=4)
E       AssertionError: 
E       Arrays are not almost equal to 4 decimals
E       
E       (mismatch 100.0%)
E        x: array(325.9395)
E        y: array(325.9393)
```

#### Versions
```
System:
    python: 3.6.8 (default, Dec  5 2019, 16:02:25)  [GCC 8.3.1 20191121 (Red Hat 8.3.1-5)]
executable: /usr/bin/python3
   machine: Linux-4.18.0-193.1.2.el8_2.aarch64-aarch64-with-redhat-8.2-Ootpa

Python dependencies:
          pip: 20.1.1
   setuptools: 39.2.0
      sklearn: 0.23.1
        numpy: 1.14.3
        scipy: 1.0.0
       Cython: 0.29
       pandas: 1.0.5
   matplotlib: 3.2.1
       joblib: 0.14.0
threadpoolctl: 2.1.0

Built with OpenMP: True
Linux-4.18.0-193.1.2.el8_2.aarch64-aarch64-with-redhat-8.2-Ootpa
Traceback (most recent call last):
  File "<string>", line 3, in <module>
NameError: name 'Python' is not defined
```

Hints from issue discussion:
Thanks for the report. The requested precision for this test is too high. #17802 should fix it.

Commit message: fix astype

Patch to review:
diff --git a/sklearn/cluster/tests/test_k_means.py b/sklearn/cluster/tests/test_k_means.py
index 8ae054b3607f6..934e8634128f1 100644
--- a/sklearn/cluster/tests/test_k_means.py
+++ b/sklearn/cluster/tests/test_k_means.py
@@ -828,45 +828,41 @@ def test_max_iter_error():
                          km.fit, X)
 
 
-@pytest.mark.parametrize('Estimator', [KMeans, MiniBatchKMeans])
-@pytest.mark.parametrize('is_sparse', [False, True])
-def test_float_precision(Estimator, is_sparse):
-
-    estimator = Estimator(n_init=1, random_state=30)
+@pytest.mark.parametrize("data", [X, X_csr], ids=["dense", "sparse"])
+@pytest.mark.parametrize("estimator", [KMeans, MiniBatchKMeans])
+def test_float_precision(estimator, data):
+    # Check that the results are the same for single and double precision.
+    km = estimator(n_init=1, random_state=0)
 
     inertia = {}
-    X_new = {}
+    Xt = {}
     centers = {}
+    labels = {}
 
     for dtype in [np.float64, np.float32]:
-        if is_sparse:
-            X_test = sp.csr_matrix(X_csr, dtype=dtype)
-        else:
-            X_test = X.astype(dtype)
-        estimator.fit(X_test)
-        # dtype of cluster centers has to be the dtype of the input
-        # data
-        assert estimator.cluster_centers_.dtype == dtype
-        inertia[dtype] = estimator.inertia_
-        X_new[dtype] = estimator.transform(X_test)
-        centers[dtype] = estimator.cluster_centers_
-        # ensure the extracted row is a 2d array
-        assert estimator.predict(X_test[:1]) == estimator.labels_[0]
-        if hasattr(estimator, 'partial_fit'):
-            estimator.partial_fit(X_test[0:3])
-            # dtype of cluster centers has to stay the same after
-            # partial_fit
-            assert estimator.cluster_centers_.dtype == dtype
-
-    # compare arrays with low precision since the difference between
-    # 32 and 64 bit sometimes makes a difference up to the 4th decimal
-    # place
-    assert_array_almost_equal(inertia[np.float32], inertia[np.float64],
-                              decimal=4)
-    assert_array_almost_equal(X_new[np.float32], X_new[np.float64],
-                              decimal=4)
-    assert_array_almost_equal(centers[np.float32], centers[np.float64],
-                              decimal=4)
+        # FIXME when scipy min version >= 1.0, add copy=False
+        X = data.astype(dtype)
+        km.fit(X)
+
+        inertia[dtype] = km.inertia_
+        Xt[dtype] = km.transform(X)
+        centers[dtype] = km.cluster_centers_
+        labels[dtype] = km.labels_
+
+        # dtype of cluster centers has to be the dtype of the input data
+        assert km.cluster_centers_.dtype == dtype
+
+        # same with partial_fit
+        if estimator is MiniBatchKMeans:
+            km.partial_fit(X[0:3])
+            assert km.cluster_centers_.dtype == dtype
+
+    # compare arrays with low precision since the difference between 32 and
+    # 64 bit comes from an accumulation of rounding errors.
+    assert_allclose(inertia[np.float32], inertia[np.float64], rtol=1e-5)
+    assert_allclose(Xt[np.float32], Xt[np.float64], rtol=1e-5)
+    assert_allclose(centers[np.float32], centers[np.float64], rtol=1e-5)
+    assert_array_equal(labels[np.float32], labels[np.float64])
 
 
 def test_k_means_init_centers():


Code review comments:
diff_hunk: @@ -828,45 +828,41 @@ def test_max_iter_error():
                          km.fit, X)
 
 
-@pytest.mark.parametrize('Estimator', [KMeans, MiniBatchKMeans])
-@pytest.mark.parametrize('is_sparse', [False, True])
-def test_float_precision(Estimator, is_sparse):
-
-    estimator = Estimator(n_init=1, random_state=30)
+@pytest.mark.parametrize("data", [X, X_csr], ids=["dense", "sparse"])
+@pytest.mark.parametrize("estimator", [KMeans, MiniBatchKMeans])
+def test_float_precision(estimator, data):
+    # Check that the results are the same for single and double precision.
+    km = estimator(n_init=1, random_state=0)
 
     inertia = {}
-    X_new = {}
+    Xt = {}
     centers = {}
+    labels = {}
 
     for dtype in [np.float64, np.float32]:
-        if is_sparse:
-            X_test = sp.csr_matrix(X_csr, dtype=dtype)
-        else:
-            X_test = X.astype(dtype)
-        estimator.fit(X_test)
-        # dtype of cluster centers has to be the dtype of the input
-        # data
-        assert estimator.cluster_centers_.dtype == dtype
-        inertia[dtype] = estimator.inertia_
-        X_new[dtype] = estimator.transform(X_test)
-        centers[dtype] = estimator.cluster_centers_
-        # ensure the extracted row is a 2d array
-        assert estimator.predict(X_test[:1]) == estimator.labels_[0]
-        if hasattr(estimator, 'partial_fit'):
-            estimator.partial_fit(X_test[0:3])
-            # dtype of cluster centers has to stay the same after
-            # partial_fit
-            assert estimator.cluster_centers_.dtype == dtype
-
-    # compare arrays with low precision since the difference between
-    # 32 and 64 bit sometimes makes a difference up to the 4th decimal
-    # place
-    assert_array_almost_equal(inertia[np.float32], inertia[np.float64],
-                              decimal=4)
-    assert_array_almost_equal(X_new[np.float32], X_new[np.float64],
-                              decimal=4)
-    assert_array_almost_equal(centers[np.float32], centers[np.float64],
-                              decimal=4)
+        # FIXME when scipy min version >= 1.0, add copy=False
+        X = data.astype(dtype)
path: sklearn/cluster/tests/test_k_means.py
text: @user1:
you mean `astype(dtype, copy=False)`? Is this important for this test?

@author:
An effort has been made to avoid unnecessary copies, see https://github.com/scikit-learn/scikit-learn/issues/11970.

Actually I realized that we already have a helper to distinguish between dense and sparse. Updated

diff_hunk: @@ -828,45 +828,41 @@ def test_max_iter_error():
                          km.fit, X)
 
 
-@pytest.mark.parametrize('Estimator', [KMeans, MiniBatchKMeans])
-@pytest.mark.parametrize('is_sparse', [False, True])
-def test_float_precision(Estimator, is_sparse):
-
-    estimator = Estimator(n_init=1, random_state=30)
+@pytest.mark.parametrize("data", [X, X_csr], ids=["dense", "sparse"])
+@pytest.mark.parametrize("estimator", [KMeans, MiniBatchKMeans])
+def test_float_precision(estimator, data):
path: sklearn/cluster/tests/test_k_means.py
text: @user1:
```suggestion
def test_float_precision(Estimator, data):
```

Since it's a class rather than an instance