| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import numpy as np |
| from numpy.testing import (assert_allclose, assert_equal, assert_array_equal, assert_, |
| assert_warns) |
| import pytest |
| from pytest import raises as assert_raises |
|
|
| import scipy.cluster.hierarchy |
| from scipy.cluster.hierarchy import ( |
| ClusterWarning, linkage, from_mlab_linkage, to_mlab_linkage, |
| num_obs_linkage, inconsistent, cophenet, fclusterdata, fcluster, |
| is_isomorphic, single, leaders, |
| correspond, is_monotonic, maxdists, maxinconsts, maxRstat, |
| is_valid_linkage, is_valid_im, to_tree, leaves_list, dendrogram, |
| set_link_color_palette, cut_tree, optimal_leaf_ordering, |
| _order_cluster_tree, _hierarchy, _LINKAGE_METHODS) |
| from scipy.spatial.distance import pdist |
| from scipy.cluster._hierarchy import Heap |
| from scipy.conftest import array_api_compatible |
| from scipy._lib._array_api import xp_assert_close, xp_assert_equal |
|
|
| from threading import Lock |
|
|
| from . import hierarchy_test_data |
|
|
|
|
| |
| |
| try: |
| import matplotlib |
| |
| matplotlib.use('Agg') |
| |
| import matplotlib.pyplot as plt |
| have_matplotlib = True |
| except Exception: |
| have_matplotlib = False |
|
|
|
|
| pytestmark = [array_api_compatible, pytest.mark.usefixtures("skip_xp_backends")] |
| skip_xp_backends = pytest.mark.skip_xp_backends |
|
|
|
|
| class TestLinkage: |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_linkage_non_finite_elements_in_distance_matrix(self, xp): |
| |
| |
| y = xp.asarray([xp.nan] + [0.0]*5) |
| assert_raises(ValueError, linkage, y) |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_linkage_empty_distance_matrix(self, xp): |
| |
| y = xp.zeros((0,)) |
| assert_raises(ValueError, linkage, y) |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_linkage_tdist(self, xp): |
| for method in ['single', 'complete', 'average', 'weighted']: |
| self.check_linkage_tdist(method, xp) |
|
|
| def check_linkage_tdist(self, method, xp): |
| |
| Z = linkage(xp.asarray(hierarchy_test_data.ytdist), method) |
| expectedZ = getattr(hierarchy_test_data, 'linkage_ytdist_' + method) |
| xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-10) |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_linkage_X(self, xp): |
| for method in ['centroid', 'median', 'ward']: |
| self.check_linkage_q(method, xp) |
|
|
| def check_linkage_q(self, method, xp): |
| |
| Z = linkage(xp.asarray(hierarchy_test_data.X), method) |
| expectedZ = getattr(hierarchy_test_data, 'linkage_X_' + method) |
| xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-06) |
|
|
| y = scipy.spatial.distance.pdist(hierarchy_test_data.X, |
| metric="euclidean") |
| Z = linkage(xp.asarray(y), method) |
| xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-06) |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_compare_with_trivial(self, xp): |
| rng = np.random.RandomState(0) |
| n = 20 |
| X = rng.rand(n, 2) |
| d = pdist(X) |
|
|
| for method, code in _LINKAGE_METHODS.items(): |
| Z_trivial = _hierarchy.linkage(d, n, code) |
| Z = linkage(xp.asarray(d), method) |
| xp_assert_close(Z, xp.asarray(Z_trivial), rtol=1e-14, atol=1e-15) |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_optimal_leaf_ordering(self, xp): |
| Z = linkage(xp.asarray(hierarchy_test_data.ytdist), optimal_ordering=True) |
| expectedZ = getattr(hierarchy_test_data, 'linkage_ytdist_single_olo') |
| xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-10) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestLinkageTies: |
|
|
| _expectations = { |
| 'single': np.array([[0, 1, 1.41421356, 2], |
| [2, 3, 1.41421356, 3]]), |
| 'complete': np.array([[0, 1, 1.41421356, 2], |
| [2, 3, 2.82842712, 3]]), |
| 'average': np.array([[0, 1, 1.41421356, 2], |
| [2, 3, 2.12132034, 3]]), |
| 'weighted': np.array([[0, 1, 1.41421356, 2], |
| [2, 3, 2.12132034, 3]]), |
| 'centroid': np.array([[0, 1, 1.41421356, 2], |
| [2, 3, 2.12132034, 3]]), |
| 'median': np.array([[0, 1, 1.41421356, 2], |
| [2, 3, 2.12132034, 3]]), |
| 'ward': np.array([[0, 1, 1.41421356, 2], |
| [2, 3, 2.44948974, 3]]), |
| } |
|
|
| def test_linkage_ties(self, xp): |
| for method in ['single', 'complete', 'average', 'weighted', |
| 'centroid', 'median', 'ward']: |
| self.check_linkage_ties(method, xp) |
|
|
| def check_linkage_ties(self, method, xp): |
| X = xp.asarray([[-1, -1], [0, 0], [1, 1]]) |
| Z = linkage(X, method=method) |
| expectedZ = self._expectations[method] |
| xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-06) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestInconsistent: |
|
|
| def test_inconsistent_tdist(self, xp): |
| for depth in hierarchy_test_data.inconsistent_ytdist: |
| self.check_inconsistent_tdist(depth, xp) |
|
|
| def check_inconsistent_tdist(self, depth, xp): |
| Z = xp.asarray(hierarchy_test_data.linkage_ytdist_single) |
| xp_assert_close(inconsistent(Z, depth), |
| xp.asarray(hierarchy_test_data.inconsistent_ytdist[depth])) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestCopheneticDistance: |
|
|
| def test_linkage_cophenet_tdist_Z(self, xp): |
| |
| expectedM = xp.asarray([268, 295, 255, 255, 295, 295, 268, 268, 295, 295, |
| 295, 138, 219, 295, 295]) |
| Z = xp.asarray(hierarchy_test_data.linkage_ytdist_single) |
| M = cophenet(Z) |
| xp_assert_close(M, xp.asarray(expectedM, dtype=xp.float64), atol=1e-10) |
|
|
| def test_linkage_cophenet_tdist_Z_Y(self, xp): |
| |
| Z = xp.asarray(hierarchy_test_data.linkage_ytdist_single) |
| (c, M) = cophenet(Z, xp.asarray(hierarchy_test_data.ytdist)) |
| expectedM = xp.asarray([268, 295, 255, 255, 295, 295, 268, 268, 295, 295, |
| 295, 138, 219, 295, 295], dtype=xp.float64) |
| expectedc = xp.asarray(0.639931296433393415057366837573, dtype=xp.float64)[()] |
| xp_assert_close(c, expectedc, atol=1e-10) |
| xp_assert_close(M, expectedM, atol=1e-10) |
|
|
| def test_gh_22183(self, xp): |
| |
| |
| |
| |
| arr=[[0.0, 1.0, 1.0, 2.0], |
| [2.0, 12.0, 1.0, 3.0], |
| [3.0, 4.0, 1.0, 2.0], |
| [5.0, 14.0, 1.0, 3.0], |
| [6.0, 7.0, 1.0, 2.0], |
| [8.0, 16.0, 1.0, 3.0], |
| [9.0, 10.0, 1.0, 2.0], |
| [11.0, 18.0, 1.0, 3.0], |
| [13.0, 15.0, 2.0, 6.0], |
| [17.0, 20.0, 2.0, 32.0], |
| [19.0, 21.0, 2.0, 12.0]] |
| with pytest.raises(ValueError, match="excessive observations"): |
| cophenet(xp.asarray(arr)) |
|
|
|
|
| class TestMLabLinkageConversion: |
|
|
| def test_mlab_linkage_conversion_empty(self, xp): |
| |
| X = xp.asarray([], dtype=xp.float64) |
| xp_assert_equal(from_mlab_linkage(X), X) |
| xp_assert_equal(to_mlab_linkage(X), X) |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_mlab_linkage_conversion_single_row(self, xp): |
| |
| Z = xp.asarray([[0., 1., 3., 2.]]) |
| Zm = xp.asarray([[1, 2, 3]]) |
| xp_assert_close(from_mlab_linkage(Zm), xp.asarray(Z, dtype=xp.float64), |
| rtol=1e-15) |
| xp_assert_close(to_mlab_linkage(Z), xp.asarray(Zm, dtype=xp.float64), |
| rtol=1e-15) |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_mlab_linkage_conversion_multiple_rows(self, xp): |
| |
| Zm = xp.asarray([[3, 6, 138], [4, 5, 219], |
| [1, 8, 255], [2, 9, 268], [7, 10, 295]]) |
| Z = xp.asarray([[2., 5., 138., 2.], |
| [3., 4., 219., 2.], |
| [0., 7., 255., 3.], |
| [1., 8., 268., 4.], |
| [6., 9., 295., 6.]], |
| dtype=xp.float64) |
| xp_assert_close(from_mlab_linkage(Zm), Z, rtol=1e-15) |
| xp_assert_close(to_mlab_linkage(Z), xp.asarray(Zm, dtype=xp.float64), |
| rtol=1e-15) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestFcluster: |
|
|
| def test_fclusterdata(self, xp): |
| for t in hierarchy_test_data.fcluster_inconsistent: |
| self.check_fclusterdata(t, 'inconsistent', xp) |
| for t in hierarchy_test_data.fcluster_distance: |
| self.check_fclusterdata(t, 'distance', xp) |
| for t in hierarchy_test_data.fcluster_maxclust: |
| self.check_fclusterdata(t, 'maxclust', xp) |
|
|
| def check_fclusterdata(self, t, criterion, xp): |
| |
| expectedT = xp.asarray(getattr(hierarchy_test_data, 'fcluster_' + criterion)[t]) |
| X = xp.asarray(hierarchy_test_data.Q_X) |
| T = fclusterdata(X, criterion=criterion, t=t) |
| assert_(is_isomorphic(T, expectedT)) |
|
|
| def test_fcluster(self, xp): |
| for t in hierarchy_test_data.fcluster_inconsistent: |
| self.check_fcluster(t, 'inconsistent', xp) |
| for t in hierarchy_test_data.fcluster_distance: |
| self.check_fcluster(t, 'distance', xp) |
| for t in hierarchy_test_data.fcluster_maxclust: |
| self.check_fcluster(t, 'maxclust', xp) |
|
|
| def check_fcluster(self, t, criterion, xp): |
| |
| expectedT = xp.asarray(getattr(hierarchy_test_data, 'fcluster_' + criterion)[t]) |
| Z = single(xp.asarray(hierarchy_test_data.Q_X)) |
| T = fcluster(Z, criterion=criterion, t=t) |
| assert_(is_isomorphic(T, expectedT)) |
|
|
| def test_fcluster_monocrit(self, xp): |
| for t in hierarchy_test_data.fcluster_distance: |
| self.check_fcluster_monocrit(t, xp) |
| for t in hierarchy_test_data.fcluster_maxclust: |
| self.check_fcluster_maxclust_monocrit(t, xp) |
|
|
| def check_fcluster_monocrit(self, t, xp): |
| expectedT = xp.asarray(hierarchy_test_data.fcluster_distance[t]) |
| Z = single(xp.asarray(hierarchy_test_data.Q_X)) |
| T = fcluster(Z, t, criterion='monocrit', monocrit=maxdists(Z)) |
| assert_(is_isomorphic(T, expectedT)) |
|
|
| def check_fcluster_maxclust_monocrit(self, t, xp): |
| expectedT = xp.asarray(hierarchy_test_data.fcluster_maxclust[t]) |
| Z = single(xp.asarray(hierarchy_test_data.Q_X)) |
| T = fcluster(Z, t, criterion='maxclust_monocrit', monocrit=maxdists(Z)) |
| assert_(is_isomorphic(T, expectedT)) |
|
|
| def test_fcluster_maxclust_gh_12651(self, xp): |
| y = xp.asarray([[1], [4], [5]]) |
| Z = single(y) |
| assert_array_equal(fcluster(Z, t=1, criterion="maxclust"), |
| xp.asarray([1, 1, 1])) |
| assert_array_equal(fcluster(Z, t=2, criterion="maxclust"), |
| xp.asarray([2, 1, 1])) |
| assert_array_equal(fcluster(Z, t=3, criterion="maxclust"), |
| xp.asarray([1, 2, 3])) |
| assert_array_equal(fcluster(Z, t=5, criterion="maxclust"), |
| xp.asarray([1, 2, 3])) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestLeaders: |
|
|
| def test_leaders_single(self, xp): |
| |
| X = hierarchy_test_data.Q_X |
| Y = pdist(X) |
| Y = xp.asarray(Y) |
| Z = linkage(Y) |
| T = fcluster(Z, criterion='maxclust', t=3) |
| Lright = (xp.asarray([53, 55, 56]), xp.asarray([2, 3, 1])) |
| T = xp.asarray(T, dtype=xp.int32) |
| L = leaders(Z, T) |
| assert_allclose(np.concatenate(L), np.concatenate(Lright), rtol=1e-15) |
|
|
|
|
| @skip_xp_backends(np_only=True, |
| reason='`is_isomorphic` only supports NumPy backend') |
| class TestIsIsomorphic: |
|
|
| @skip_xp_backends(np_only=True, |
| reason='array-likes only supported for NumPy backend') |
| def test_array_like(self, xp): |
| assert is_isomorphic([1, 1, 1], [2, 2, 2]) |
| assert is_isomorphic([], []) |
|
|
| def test_is_isomorphic_1(self, xp): |
| |
| a = xp.asarray([1, 1, 1]) |
| b = xp.asarray([2, 2, 2]) |
| assert is_isomorphic(a, b) |
| assert is_isomorphic(b, a) |
|
|
| def test_is_isomorphic_2(self, xp): |
| |
| a = xp.asarray([1, 7, 1]) |
| b = xp.asarray([2, 3, 2]) |
| assert is_isomorphic(a, b) |
| assert is_isomorphic(b, a) |
|
|
| def test_is_isomorphic_3(self, xp): |
| |
| a = xp.asarray([]) |
| b = xp.asarray([]) |
| assert is_isomorphic(a, b) |
|
|
| def test_is_isomorphic_4A(self, xp): |
| |
| |
| a = xp.asarray([1, 2, 3]) |
| b = xp.asarray([1, 3, 2]) |
| assert is_isomorphic(a, b) |
| assert is_isomorphic(b, a) |
|
|
| def test_is_isomorphic_4B(self, xp): |
| |
| |
| a = xp.asarray([1, 2, 3, 3]) |
| b = xp.asarray([1, 3, 2, 3]) |
| assert is_isomorphic(a, b) is False |
| assert is_isomorphic(b, a) is False |
|
|
| def test_is_isomorphic_4C(self, xp): |
| |
| |
| a = xp.asarray([7, 2, 3]) |
| b = xp.asarray([6, 3, 2]) |
| assert is_isomorphic(a, b) |
| assert is_isomorphic(b, a) |
|
|
| def test_is_isomorphic_5(self, xp): |
| |
| |
| for nc in [2, 3, 5]: |
| self.help_is_isomorphic_randperm(1000, nc, xp=xp) |
|
|
| def test_is_isomorphic_6(self, xp): |
| |
| |
| |
| for nc in [2, 3, 5]: |
| self.help_is_isomorphic_randperm(1000, nc, True, 5, xp=xp) |
|
|
| def test_is_isomorphic_7(self, xp): |
| |
| a = xp.asarray([1, 2, 3]) |
| b = xp.asarray([1, 1, 1]) |
| assert not is_isomorphic(a, b) |
|
|
| def help_is_isomorphic_randperm(self, nobs, nclusters, noniso=False, nerrors=0, |
| *, xp): |
| for k in range(3): |
| a = (np.random.rand(nobs) * nclusters).astype(int) |
| b = np.zeros(a.size, dtype=int) |
| P = np.random.permutation(nclusters) |
| for i in range(0, a.shape[0]): |
| b[i] = P[a[i]] |
| if noniso: |
| Q = np.random.permutation(nobs) |
| b[Q[0:nerrors]] += 1 |
| b[Q[0:nerrors]] %= nclusters |
| a = xp.asarray(a) |
| b = xp.asarray(b) |
| assert is_isomorphic(a, b) == (not noniso) |
| assert is_isomorphic(b, a) == (not noniso) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestIsValidLinkage: |
|
|
| def test_is_valid_linkage_various_size(self, xp): |
| for nrow, ncol, valid in [(2, 5, False), (2, 3, False), |
| (1, 4, True), (2, 4, True)]: |
| self.check_is_valid_linkage_various_size(nrow, ncol, valid, xp) |
|
|
| def check_is_valid_linkage_various_size(self, nrow, ncol, valid, xp): |
| |
| Z = xp.asarray([[0, 1, 3.0, 2, 5], |
| [3, 2, 4.0, 3, 3]], dtype=xp.float64) |
| Z = Z[:nrow, :ncol] |
| assert_(is_valid_linkage(Z) == valid) |
| if not valid: |
| assert_raises(ValueError, is_valid_linkage, Z, throw=True) |
|
|
| def test_is_valid_linkage_int_type(self, xp): |
| |
| Z = xp.asarray([[0, 1, 3.0, 2], |
| [3, 2, 4.0, 3]], dtype=xp.int64) |
| assert_(is_valid_linkage(Z) is False) |
| assert_raises(TypeError, is_valid_linkage, Z, throw=True) |
|
|
| def test_is_valid_linkage_empty(self, xp): |
| |
| Z = xp.zeros((0, 4), dtype=xp.float64) |
| assert_(is_valid_linkage(Z) is False) |
| assert_raises(ValueError, is_valid_linkage, Z, throw=True) |
|
|
| def test_is_valid_linkage_4_and_up(self, xp): |
| |
| |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| assert_(is_valid_linkage(Z) is True) |
|
|
| @skip_xp_backends('jax.numpy', |
| reason='jax arrays do not support item assignment') |
| def test_is_valid_linkage_4_and_up_neg_index_left(self, xp): |
| |
| |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| Z[i//2,0] = -2 |
| assert_(is_valid_linkage(Z) is False) |
| assert_raises(ValueError, is_valid_linkage, Z, throw=True) |
|
|
| @skip_xp_backends('jax.numpy', |
| reason='jax arrays do not support item assignment') |
| def test_is_valid_linkage_4_and_up_neg_index_right(self, xp): |
| |
| |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| Z[i//2,1] = -2 |
| assert_(is_valid_linkage(Z) is False) |
| assert_raises(ValueError, is_valid_linkage, Z, throw=True) |
|
|
| @skip_xp_backends('jax.numpy', |
| reason='jax arrays do not support item assignment') |
| def test_is_valid_linkage_4_and_up_neg_dist(self, xp): |
| |
| |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| Z[i//2,2] = -0.5 |
| assert_(is_valid_linkage(Z) is False) |
| assert_raises(ValueError, is_valid_linkage, Z, throw=True) |
|
|
| @skip_xp_backends('jax.numpy', |
| reason='jax arrays do not support item assignment') |
| def test_is_valid_linkage_4_and_up_neg_counts(self, xp): |
| |
| |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| Z[i//2,3] = -2 |
| assert_(is_valid_linkage(Z) is False) |
| assert_raises(ValueError, is_valid_linkage, Z, throw=True) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestIsValidInconsistent: |
|
|
| def test_is_valid_im_int_type(self, xp): |
| |
| R = xp.asarray([[0, 1, 3.0, 2], |
| [3, 2, 4.0, 3]], dtype=xp.int64) |
| assert_(is_valid_im(R) is False) |
| assert_raises(TypeError, is_valid_im, R, throw=True) |
|
|
| def test_is_valid_im_various_size(self, xp): |
| for nrow, ncol, valid in [(2, 5, False), (2, 3, False), |
| (1, 4, True), (2, 4, True)]: |
| self.check_is_valid_im_various_size(nrow, ncol, valid, xp) |
|
|
| def check_is_valid_im_various_size(self, nrow, ncol, valid, xp): |
| |
| R = xp.asarray([[0, 1, 3.0, 2, 5], |
| [3, 2, 4.0, 3, 3]], dtype=xp.float64) |
| R = R[:nrow, :ncol] |
| assert_(is_valid_im(R) == valid) |
| if not valid: |
| assert_raises(ValueError, is_valid_im, R, throw=True) |
|
|
| def test_is_valid_im_empty(self, xp): |
| |
| R = xp.zeros((0, 4), dtype=xp.float64) |
| assert_(is_valid_im(R) is False) |
| assert_raises(ValueError, is_valid_im, R, throw=True) |
|
|
| def test_is_valid_im_4_and_up(self, xp): |
| |
| |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| R = inconsistent(Z) |
| assert_(is_valid_im(R) is True) |
|
|
| @skip_xp_backends('jax.numpy', reason='jax arrays do not support item assignment') |
| def test_is_valid_im_4_and_up_neg_index_left(self, xp): |
| |
| |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| R = inconsistent(Z) |
| R[i//2,0] = -2.0 |
| assert_(is_valid_im(R) is False) |
| assert_raises(ValueError, is_valid_im, R, throw=True) |
|
|
| @skip_xp_backends('jax.numpy', reason='jax arrays do not support item assignment') |
| def test_is_valid_im_4_and_up_neg_index_right(self, xp): |
| |
| |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| R = inconsistent(Z) |
| R[i//2,1] = -2.0 |
| assert_(is_valid_im(R) is False) |
| assert_raises(ValueError, is_valid_im, R, throw=True) |
|
|
| @skip_xp_backends('jax.numpy', reason='jax arrays do not support item assignment') |
| def test_is_valid_im_4_and_up_neg_dist(self, xp): |
| |
| |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| R = inconsistent(Z) |
| R[i//2,2] = -0.5 |
| assert_(is_valid_im(R) is False) |
| assert_raises(ValueError, is_valid_im, R, throw=True) |
|
|
|
|
| class TestNumObsLinkage: |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_num_obs_linkage_empty(self, xp): |
| |
| Z = xp.zeros((0, 4), dtype=xp.float64) |
| assert_raises(ValueError, num_obs_linkage, Z) |
|
|
| def test_num_obs_linkage_1x4(self, xp): |
| |
| Z = xp.asarray([[0, 1, 3.0, 2]], dtype=xp.float64) |
| assert_equal(num_obs_linkage(Z), 2) |
|
|
| def test_num_obs_linkage_2x4(self, xp): |
| |
| Z = xp.asarray([[0, 1, 3.0, 2], |
| [3, 2, 4.0, 3]], dtype=xp.float64) |
| assert_equal(num_obs_linkage(Z), 3) |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_num_obs_linkage_4_and_up(self, xp): |
| |
| |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| assert_equal(num_obs_linkage(Z), i) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestLeavesList: |
|
|
| def test_leaves_list_1x4(self, xp): |
| |
| Z = xp.asarray([[0, 1, 3.0, 2]], dtype=xp.float64) |
| to_tree(Z) |
| assert_allclose(leaves_list(Z), [0, 1], rtol=1e-15) |
|
|
| def test_leaves_list_2x4(self, xp): |
| |
| Z = xp.asarray([[0, 1, 3.0, 2], |
| [3, 2, 4.0, 3]], dtype=xp.float64) |
| to_tree(Z) |
| assert_allclose(leaves_list(Z), [0, 1, 2], rtol=1e-15) |
|
|
| def test_leaves_list_Q(self, xp): |
| for method in ['single', 'complete', 'average', 'weighted', 'centroid', |
| 'median', 'ward']: |
| self.check_leaves_list_Q(method, xp) |
|
|
| def check_leaves_list_Q(self, method, xp): |
| |
| X = xp.asarray(hierarchy_test_data.Q_X) |
| Z = linkage(X, method) |
| node = to_tree(Z) |
| assert_allclose(node.pre_order(), leaves_list(Z), rtol=1e-15) |
|
|
| def test_Q_subtree_pre_order(self, xp): |
| |
| X = xp.asarray(hierarchy_test_data.Q_X) |
| Z = linkage(X, 'single') |
| node = to_tree(Z) |
| assert_allclose(node.pre_order(), (node.get_left().pre_order() |
| + node.get_right().pre_order()), |
| rtol=1e-15) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestCorrespond: |
|
|
| def test_correspond_empty(self, xp): |
| |
| y = xp.zeros((0,), dtype=xp.float64) |
| Z = xp.zeros((0,4), dtype=xp.float64) |
| assert_raises(ValueError, correspond, Z, y) |
|
|
| def test_correspond_2_and_up(self, xp): |
| |
| |
| for i in range(2, 4): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| assert_(correspond(Z, y)) |
| for i in range(4, 15, 3): |
| y = np.random.rand(i*(i-1)//2) |
| y = xp.asarray(y) |
| Z = linkage(y) |
| assert_(correspond(Z, y)) |
|
|
| def test_correspond_4_and_up(self, xp): |
| |
| |
| for (i, j) in (list(zip(list(range(2, 4)), list(range(3, 5)))) + |
| list(zip(list(range(3, 5)), list(range(2, 4))))): |
| y = np.random.rand(i*(i-1)//2) |
| y2 = np.random.rand(j*(j-1)//2) |
| y = xp.asarray(y) |
| y2 = xp.asarray(y2) |
| Z = linkage(y) |
| Z2 = linkage(y2) |
| assert not correspond(Z, y2) |
| assert not correspond(Z2, y) |
|
|
| def test_correspond_4_and_up_2(self, xp): |
| |
| |
| for (i, j) in (list(zip(list(range(2, 7)), list(range(16, 21)))) + |
| list(zip(list(range(2, 7)), list(range(16, 21))))): |
| y = np.random.rand(i*(i-1)//2) |
| y2 = np.random.rand(j*(j-1)//2) |
| y = xp.asarray(y) |
| y2 = xp.asarray(y2) |
| Z = linkage(y) |
| Z2 = linkage(y2) |
| assert not correspond(Z, y2) |
| assert not correspond(Z2, y) |
|
|
| def test_num_obs_linkage_multi_matrix(self, xp): |
| |
| for n in range(2, 10): |
| X = np.random.rand(n, 4) |
| Y = pdist(X) |
| Y = xp.asarray(Y) |
| Z = linkage(Y) |
| assert_equal(num_obs_linkage(Z), n) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestIsMonotonic: |
|
|
| def test_is_monotonic_empty(self, xp): |
| |
| Z = xp.zeros((0, 4), dtype=xp.float64) |
| assert_raises(ValueError, is_monotonic, Z) |
|
|
| def test_is_monotonic_1x4(self, xp): |
| |
| Z = xp.asarray([[0, 1, 0.3, 2]], dtype=xp.float64) |
| assert is_monotonic(Z) |
|
|
| def test_is_monotonic_2x4_T(self, xp): |
| |
| Z = xp.asarray([[0, 1, 0.3, 2], |
| [2, 3, 0.4, 3]], dtype=xp.float64) |
| assert is_monotonic(Z) |
|
|
| def test_is_monotonic_2x4_F(self, xp): |
| |
| Z = xp.asarray([[0, 1, 0.4, 2], |
| [2, 3, 0.3, 3]], dtype=xp.float64) |
| assert not is_monotonic(Z) |
|
|
| def test_is_monotonic_3x4_T(self, xp): |
| |
| Z = xp.asarray([[0, 1, 0.3, 2], |
| [2, 3, 0.4, 2], |
| [4, 5, 0.6, 4]], dtype=xp.float64) |
| assert is_monotonic(Z) |
|
|
| def test_is_monotonic_3x4_F1(self, xp): |
| |
| Z = xp.asarray([[0, 1, 0.3, 2], |
| [2, 3, 0.2, 2], |
| [4, 5, 0.6, 4]], dtype=xp.float64) |
| assert not is_monotonic(Z) |
|
|
| def test_is_monotonic_3x4_F2(self, xp): |
| |
| Z = xp.asarray([[0, 1, 0.8, 2], |
| [2, 3, 0.4, 2], |
| [4, 5, 0.6, 4]], dtype=xp.float64) |
| assert not is_monotonic(Z) |
|
|
| def test_is_monotonic_3x4_F3(self, xp): |
| |
| Z = xp.asarray([[0, 1, 0.3, 2], |
| [2, 3, 0.4, 2], |
| [4, 5, 0.2, 4]], dtype=xp.float64) |
| assert not is_monotonic(Z) |
|
|
| def test_is_monotonic_tdist_linkage1(self, xp): |
| |
| |
| Z = linkage(xp.asarray(hierarchy_test_data.ytdist), 'single') |
| assert is_monotonic(Z) |
|
|
| @skip_xp_backends('jax.numpy', reason='jax arrays do not support item assignment') |
| def test_is_monotonic_tdist_linkage2(self, xp): |
| |
| |
| Z = linkage(xp.asarray(hierarchy_test_data.ytdist), 'single') |
| Z[2,2] = 0.0 |
| assert not is_monotonic(Z) |
|
|
| def test_is_monotonic_Q_linkage(self, xp): |
| |
| |
| X = xp.asarray(hierarchy_test_data.Q_X) |
| Z = linkage(X, 'single') |
| assert is_monotonic(Z) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestMaxDists: |
|
|
| def test_maxdists_empty_linkage(self, xp): |
| |
| Z = xp.zeros((0, 4), dtype=xp.float64) |
| assert_raises(ValueError, maxdists, Z) |
|
|
| @skip_xp_backends('jax.numpy', reason='jax arrays do not support item assignment') |
| def test_maxdists_one_cluster_linkage(self, xp): |
| |
| Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64) |
| MD = maxdists(Z) |
| expectedMD = calculate_maximum_distances(Z, xp) |
| xp_assert_close(MD, expectedMD, atol=1e-15) |
|
|
| @skip_xp_backends('jax.numpy', reason='jax arrays do not support item assignment') |
| def test_maxdists_Q_linkage(self, xp): |
| for method in ['single', 'complete', 'ward', 'centroid', 'median']: |
| self.check_maxdists_Q_linkage(method, xp) |
|
|
| def check_maxdists_Q_linkage(self, method, xp): |
| |
| X = xp.asarray(hierarchy_test_data.Q_X) |
| Z = linkage(X, method) |
| MD = maxdists(Z) |
| expectedMD = calculate_maximum_distances(Z, xp) |
| xp_assert_close(MD, expectedMD, atol=1e-15) |
|
|
|
|
| class TestMaxInconsts: |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_maxinconsts_empty_linkage(self, xp): |
| |
| Z = xp.zeros((0, 4), dtype=xp.float64) |
| R = xp.zeros((0, 4), dtype=xp.float64) |
| assert_raises(ValueError, maxinconsts, Z, R) |
|
|
| def test_maxinconsts_difrow_linkage(self, xp): |
| |
| |
| Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64) |
| R = np.random.rand(2, 4) |
| R = xp.asarray(R) |
| assert_raises(ValueError, maxinconsts, Z, R) |
|
|
| @skip_xp_backends('jax.numpy', reason='jax arrays do not support item assignment', |
| cpu_only=True) |
| def test_maxinconsts_one_cluster_linkage(self, xp): |
| |
| Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64) |
| R = xp.asarray([[0, 0, 0, 0.3]], dtype=xp.float64) |
| MD = maxinconsts(Z, R) |
| expectedMD = calculate_maximum_inconsistencies(Z, R, xp=xp) |
| xp_assert_close(MD, expectedMD, atol=1e-15) |
|
|
| @skip_xp_backends('jax.numpy', reason='jax arrays do not support item assignment', |
| cpu_only=True) |
| def test_maxinconsts_Q_linkage(self, xp): |
| for method in ['single', 'complete', 'ward', 'centroid', 'median']: |
| self.check_maxinconsts_Q_linkage(method, xp) |
|
|
| def check_maxinconsts_Q_linkage(self, method, xp): |
| |
| X = xp.asarray(hierarchy_test_data.Q_X) |
| Z = linkage(X, method) |
| R = inconsistent(Z) |
| MD = maxinconsts(Z, R) |
| expectedMD = calculate_maximum_inconsistencies(Z, R, xp=xp) |
| xp_assert_close(MD, expectedMD, atol=1e-15) |
|
|
|
|
| class TestMaxRStat: |
|
|
| def test_maxRstat_invalid_index(self, xp): |
| for i in [3.3, -1, 4]: |
| self.check_maxRstat_invalid_index(i, xp) |
|
|
| def check_maxRstat_invalid_index(self, i, xp): |
| |
| Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64) |
| R = xp.asarray([[0, 0, 0, 0.3]], dtype=xp.float64) |
| if isinstance(i, int): |
| assert_raises(ValueError, maxRstat, Z, R, i) |
| else: |
| assert_raises(TypeError, maxRstat, Z, R, i) |
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_maxRstat_empty_linkage(self, xp): |
| for i in range(4): |
| self.check_maxRstat_empty_linkage(i, xp) |
|
|
| def check_maxRstat_empty_linkage(self, i, xp): |
| |
| Z = xp.zeros((0, 4), dtype=xp.float64) |
| R = xp.zeros((0, 4), dtype=xp.float64) |
| assert_raises(ValueError, maxRstat, Z, R, i) |
|
|
| def test_maxRstat_difrow_linkage(self, xp): |
| for i in range(4): |
| self.check_maxRstat_difrow_linkage(i, xp) |
|
|
| def check_maxRstat_difrow_linkage(self, i, xp): |
| |
| |
| Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64) |
| R = np.random.rand(2, 4) |
| R = xp.asarray(R) |
| assert_raises(ValueError, maxRstat, Z, R, i) |
|
|
| @skip_xp_backends('jax.numpy', reason='jax arrays do not support item assignment', |
| cpu_only=True) |
| def test_maxRstat_one_cluster_linkage(self, xp): |
| for i in range(4): |
| self.check_maxRstat_one_cluster_linkage(i, xp) |
|
|
| def check_maxRstat_one_cluster_linkage(self, i, xp): |
| |
| Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64) |
| R = xp.asarray([[0, 0, 0, 0.3]], dtype=xp.float64) |
| MD = maxRstat(Z, R, 1) |
| expectedMD = calculate_maximum_inconsistencies(Z, R, 1, xp) |
| xp_assert_close(MD, expectedMD, atol=1e-15) |
|
|
| @skip_xp_backends('jax.numpy', reason='jax arrays do not support item assignment', |
| cpu_only=True) |
| def test_maxRstat_Q_linkage(self, xp): |
| for method in ['single', 'complete', 'ward', 'centroid', 'median']: |
| for i in range(4): |
| self.check_maxRstat_Q_linkage(method, i, xp) |
|
|
| def check_maxRstat_Q_linkage(self, method, i, xp): |
| |
| X = xp.asarray(hierarchy_test_data.Q_X) |
| Z = linkage(X, method) |
| R = inconsistent(Z) |
| MD = maxRstat(Z, R, 1) |
| expectedMD = calculate_maximum_inconsistencies(Z, R, 1, xp) |
| xp_assert_close(MD, expectedMD, atol=1e-15) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| class TestDendrogram: |
|
|
| def test_dendrogram_single_linkage_tdist(self, xp): |
| |
| Z = linkage(xp.asarray(hierarchy_test_data.ytdist), 'single') |
| R = dendrogram(Z, no_plot=True) |
| leaves = R["leaves"] |
| assert_equal(leaves, [2, 5, 1, 0, 3, 4]) |
|
|
| def test_valid_orientation(self, xp): |
| Z = linkage(xp.asarray(hierarchy_test_data.ytdist), 'single') |
| assert_raises(ValueError, dendrogram, Z, orientation="foo") |
|
|
| def test_labels_as_array_or_list(self, xp): |
| |
| Z = linkage(xp.asarray(hierarchy_test_data.ytdist), 'single') |
| labels = [1, 3, 2, 6, 4, 5] |
| result1 = dendrogram(Z, labels=xp.asarray(labels), no_plot=True) |
| result2 = dendrogram(Z, labels=labels, no_plot=True) |
| assert result1 == result2 |
|
|
| @pytest.mark.skipif(not have_matplotlib, reason="no matplotlib") |
| def test_valid_label_size(self, xp): |
| link = xp.asarray([ |
| [0, 1, 1.0, 4], |
| [2, 3, 1.0, 5], |
| [4, 5, 2.0, 6], |
| ]) |
| plt.figure() |
| with pytest.raises(ValueError) as exc_info: |
| dendrogram(link, labels=list(range(100))) |
| assert "Dimensions of Z and labels must be consistent."\ |
| in str(exc_info.value) |
|
|
| with pytest.raises( |
| ValueError, |
| match="Dimensions of Z and labels must be consistent."): |
| dendrogram(link, labels=[]) |
|
|
| plt.close() |
|
|
| @skip_xp_backends('torch', |
| reason='MPL 3.9.2 & torch DeprecationWarning from __array_wrap__' |
| ' and NumPy 2.0' |
| ) |
| @pytest.mark.skipif(not have_matplotlib, reason="no matplotlib") |
| def test_dendrogram_plot(self, xp): |
| for orientation in ['top', 'bottom', 'left', 'right']: |
| self.check_dendrogram_plot(orientation, xp) |
|
|
| def check_dendrogram_plot(self, orientation, xp): |
| |
| Z = linkage(xp.asarray(hierarchy_test_data.ytdist), 'single') |
| expected = {'color_list': ['C1', 'C0', 'C0', 'C0', 'C0'], |
| 'dcoord': [[0.0, 138.0, 138.0, 0.0], |
| [0.0, 219.0, 219.0, 0.0], |
| [0.0, 255.0, 255.0, 219.0], |
| [0.0, 268.0, 268.0, 255.0], |
| [138.0, 295.0, 295.0, 268.0]], |
| 'icoord': [[5.0, 5.0, 15.0, 15.0], |
| [45.0, 45.0, 55.0, 55.0], |
| [35.0, 35.0, 50.0, 50.0], |
| [25.0, 25.0, 42.5, 42.5], |
| [10.0, 10.0, 33.75, 33.75]], |
| 'ivl': ['2', '5', '1', '0', '3', '4'], |
| 'leaves': [2, 5, 1, 0, 3, 4], |
| 'leaves_color_list': ['C1', 'C1', 'C0', 'C0', 'C0', 'C0'], |
| } |
|
|
| fig = plt.figure() |
| ax = fig.add_subplot(221) |
|
|
| |
| R1 = dendrogram(Z, ax=ax, orientation=orientation) |
| R1['dcoord'] = np.asarray(R1['dcoord']) |
| assert_equal(R1, expected) |
|
|
| |
| |
| dendrogram(Z, ax=ax, orientation=orientation, |
| leaf_font_size=20, leaf_rotation=90) |
| testlabel = ( |
| ax.get_xticklabels()[0] |
| if orientation in ['top', 'bottom'] |
| else ax.get_yticklabels()[0] |
| ) |
| assert_equal(testlabel.get_rotation(), 90) |
| assert_equal(testlabel.get_size(), 20) |
| dendrogram(Z, ax=ax, orientation=orientation, |
| leaf_rotation=90) |
| testlabel = ( |
| ax.get_xticklabels()[0] |
| if orientation in ['top', 'bottom'] |
| else ax.get_yticklabels()[0] |
| ) |
| assert_equal(testlabel.get_rotation(), 90) |
| dendrogram(Z, ax=ax, orientation=orientation, |
| leaf_font_size=20) |
| testlabel = ( |
| ax.get_xticklabels()[0] |
| if orientation in ['top', 'bottom'] |
| else ax.get_yticklabels()[0] |
| ) |
| assert_equal(testlabel.get_size(), 20) |
| plt.close() |
|
|
| |
| R2 = dendrogram(Z, orientation=orientation) |
| plt.close() |
| R2['dcoord'] = np.asarray(R2['dcoord']) |
| assert_equal(R2, expected) |
|
|
| @skip_xp_backends('torch', |
| reason='MPL 3.9.2 & torch DeprecationWarning from __array_wrap__' |
| ' and NumPy 2.0' |
| ) |
| @pytest.mark.skipif(not have_matplotlib, reason="no matplotlib") |
| def test_dendrogram_truncate_mode(self, xp): |
| Z = linkage(xp.asarray(hierarchy_test_data.ytdist), 'single') |
|
|
| R = dendrogram(Z, 2, 'lastp', show_contracted=True) |
| plt.close() |
| R['dcoord'] = np.asarray(R['dcoord']) |
| assert_equal(R, {'color_list': ['C0'], |
| 'dcoord': [[0.0, 295.0, 295.0, 0.0]], |
| 'icoord': [[5.0, 5.0, 15.0, 15.0]], |
| 'ivl': ['(2)', '(4)'], |
| 'leaves': [6, 9], |
| 'leaves_color_list': ['C0', 'C0'], |
| }) |
|
|
| R = dendrogram(Z, 2, 'mtica', show_contracted=True) |
| plt.close() |
| R['dcoord'] = np.asarray(R['dcoord']) |
| assert_equal(R, {'color_list': ['C1', 'C0', 'C0', 'C0'], |
| 'dcoord': [[0.0, 138.0, 138.0, 0.0], |
| [0.0, 255.0, 255.0, 0.0], |
| [0.0, 268.0, 268.0, 255.0], |
| [138.0, 295.0, 295.0, 268.0]], |
| 'icoord': [[5.0, 5.0, 15.0, 15.0], |
| [35.0, 35.0, 45.0, 45.0], |
| [25.0, 25.0, 40.0, 40.0], |
| [10.0, 10.0, 32.5, 32.5]], |
| 'ivl': ['2', '5', '1', '0', '(2)'], |
| 'leaves': [2, 5, 1, 0, 7], |
| 'leaves_color_list': ['C1', 'C1', 'C0', 'C0', 'C0'], |
| }) |
|
|
| @pytest.fixture |
| def dendrogram_lock(self): |
| return Lock() |
|
|
| def test_dendrogram_colors(self, xp, dendrogram_lock): |
| |
| Z = linkage(xp.asarray(hierarchy_test_data.ytdist), 'single') |
|
|
| with dendrogram_lock: |
| |
| set_link_color_palette(['c', 'm', 'y', 'k']) |
| R = dendrogram(Z, no_plot=True, |
| above_threshold_color='g', color_threshold=250) |
| set_link_color_palette(['g', 'r', 'c', 'm', 'y', 'k']) |
|
|
| color_list = R['color_list'] |
| assert_equal(color_list, ['c', 'm', 'g', 'g', 'g']) |
|
|
| |
| set_link_color_palette(None) |
|
|
| def test_dendrogram_leaf_colors_zero_dist(self, xp): |
| |
| |
| x = xp.asarray([[1, 0, 0], |
| [0, 0, 1], |
| [0, 2, 0], |
| [0, 0, 1], |
| [0, 1, 0], |
| [0, 1, 0]]) |
| z = linkage(x, "single") |
| d = dendrogram(z, no_plot=True) |
| exp_colors = ['C0', 'C1', 'C1', 'C0', 'C2', 'C2'] |
| colors = d["leaves_color_list"] |
| assert_equal(colors, exp_colors) |
|
|
| def test_dendrogram_leaf_colors(self, xp): |
| |
| |
| x = xp.asarray([[1, 0, 0], |
| [0, 0, 1.1], |
| [0, 2, 0], |
| [0, 0, 1], |
| [0, 1, 0], |
| [0, 1, 0]]) |
| z = linkage(x, "single") |
| d = dendrogram(z, no_plot=True) |
| exp_colors = ['C0', 'C1', 'C1', 'C0', 'C2', 'C2'] |
| colors = d["leaves_color_list"] |
| assert_equal(colors, exp_colors) |
|
|
|
|
| def calculate_maximum_distances(Z, xp): |
| |
| n = Z.shape[0] + 1 |
| B = xp.zeros((n-1,), dtype=Z.dtype) |
| q = xp.zeros((3,)) |
| for i in range(0, n - 1): |
| q[:] = 0.0 |
| left = Z[i, 0] |
| right = Z[i, 1] |
| if left >= n: |
| q[0] = B[xp.asarray(left, dtype=xp.int64) - n] |
| if right >= n: |
| q[1] = B[xp.asarray(right, dtype=xp.int64) - n] |
| q[2] = Z[i, 2] |
| B[i] = xp.max(q) |
| return B |
|
|
|
|
| def calculate_maximum_inconsistencies(Z, R, k=3, xp=np): |
| |
| n = Z.shape[0] + 1 |
| dtype = xp.result_type(Z, R) |
| B = xp.zeros((n-1,), dtype=dtype) |
| q = xp.zeros((3,)) |
| for i in range(0, n - 1): |
| q[:] = 0.0 |
| left = Z[i, 0] |
| right = Z[i, 1] |
| if left >= n: |
| q[0] = B[xp.asarray(left, dtype=xp.int64) - n] |
| if right >= n: |
| q[1] = B[xp.asarray(right, dtype=xp.int64) - n] |
| q[2] = R[i, k] |
| B[i] = xp.max(q) |
| return B |
|
|
|
|
| @pytest.mark.thread_unsafe |
| @skip_xp_backends(cpu_only=True) |
| def test_unsupported_uncondensed_distance_matrix_linkage_warning(xp): |
| assert_warns(ClusterWarning, linkage, xp.asarray([[0, 1], [1, 0]])) |
|
|
|
|
| def test_euclidean_linkage_value_error(xp): |
| for method in scipy.cluster.hierarchy._EUCLIDEAN_METHODS: |
| assert_raises(ValueError, linkage, xp.asarray([[1, 1], [1, 1]]), |
| method=method, metric='cityblock') |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_2x2_linkage(xp): |
| Z1 = linkage(xp.asarray([1]), method='single', metric='euclidean') |
| Z2 = linkage(xp.asarray([[0, 1], [0, 0]]), method='single', metric='euclidean') |
| xp_assert_close(Z1, Z2, rtol=1e-15) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_node_compare(xp): |
| np.random.seed(23) |
| nobs = 50 |
| X = np.random.randn(nobs, 4) |
| X = xp.asarray(X) |
| Z = scipy.cluster.hierarchy.ward(X) |
| tree = to_tree(Z) |
| assert_(tree > tree.get_left()) |
| assert_(tree.get_right() > tree.get_left()) |
| assert_(tree.get_right() == tree.get_right()) |
| assert_(tree.get_right() != tree.get_left()) |
|
|
|
|
| @skip_xp_backends(np_only=True, reason='`cut_tree` uses non-standard indexing') |
| def test_cut_tree(xp): |
| np.random.seed(23) |
| nobs = 50 |
| X = np.random.randn(nobs, 4) |
| X = xp.asarray(X) |
| Z = scipy.cluster.hierarchy.ward(X) |
| cutree = cut_tree(Z) |
|
|
| |
| xp_assert_close(cutree[:, 0], xp.arange(nobs), rtol=1e-15, check_dtype=False) |
| xp_assert_close(cutree[:, -1], xp.zeros(nobs), rtol=1e-15, check_dtype=False) |
| assert_equal(np.asarray(cutree).max(0), np.arange(nobs - 1, -1, -1)) |
|
|
| xp_assert_close(cutree[:, [-5]], cut_tree(Z, n_clusters=5), rtol=1e-15) |
| xp_assert_close(cutree[:, [-5, -10]], cut_tree(Z, n_clusters=[5, 10]), rtol=1e-15) |
| xp_assert_close(cutree[:, [-10, -5]], cut_tree(Z, n_clusters=[10, 5]), rtol=1e-15) |
|
|
| nodes = _order_cluster_tree(Z) |
| heights = xp.asarray([node.dist for node in nodes]) |
|
|
| xp_assert_close(cutree[:, np.searchsorted(heights, [5])], |
| cut_tree(Z, height=5), rtol=1e-15) |
| xp_assert_close(cutree[:, np.searchsorted(heights, [5, 10])], |
| cut_tree(Z, height=[5, 10]), rtol=1e-15) |
| xp_assert_close(cutree[:, np.searchsorted(heights, [10, 5])], |
| cut_tree(Z, height=[10, 5]), rtol=1e-15) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_optimal_leaf_ordering(xp): |
| |
| Z = optimal_leaf_ordering(linkage(xp.asarray(hierarchy_test_data.ytdist)), |
| xp.asarray(hierarchy_test_data.ytdist)) |
| expectedZ = hierarchy_test_data.linkage_ytdist_single_olo |
| xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-10) |
|
|
| |
| Z = optimal_leaf_ordering(linkage(xp.asarray(hierarchy_test_data.X), 'ward'), |
| xp.asarray(hierarchy_test_data.X)) |
| expectedZ = hierarchy_test_data.linkage_X_ward_olo |
| xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-06) |
|
|
|
|
| @skip_xp_backends(np_only=True, reason='`Heap` only supports NumPy backend') |
| def test_Heap(xp): |
| values = xp.asarray([2, -1, 0, -1.5, 3]) |
| heap = Heap(values) |
|
|
| pair = heap.get_min() |
| assert_equal(pair['key'], 3) |
| assert_equal(pair['value'], -1.5) |
|
|
| heap.remove_min() |
| pair = heap.get_min() |
| assert_equal(pair['key'], 1) |
| assert_equal(pair['value'], -1) |
|
|
| heap.change_value(1, 2.5) |
| pair = heap.get_min() |
| assert_equal(pair['key'], 2) |
| assert_equal(pair['value'], 0) |
|
|
| heap.remove_min() |
| heap.remove_min() |
|
|
| heap.change_value(1, 10) |
| pair = heap.get_min() |
| assert_equal(pair['key'], 4) |
| assert_equal(pair['value'], 3) |
|
|
| heap.remove_min() |
| pair = heap.get_min() |
| assert_equal(pair['key'], 1) |
| assert_equal(pair['value'], 10) |
|
|
|
|
| @skip_xp_backends(cpu_only=True) |
| def test_centroid_neg_distance(xp): |
| |
| values = xp.asarray([0, 0, -1]) |
| with pytest.raises(ValueError): |
| |
| linkage(values, method='centroid') |
|
|