import numpy as np import pytest from scipy.sparse import csr_matrix from sklearn.neighbors import NearestNeighbors from cleanlab.datalab.internal.issue_manager.knn_graph_helpers import ( _process_knn_graph_from_inputs as _test_fn_1, # Rename for testing purposes num_neighbors_in_knn_graph as _get_num_neighbors, set_knn_graph as _test_fn_2, # Rename for testing purposes ) from cleanlab.internal.neighbor.knn_graph import create_knn_graph_and_index as _make_knn class TestProcessKNNGraphFromInputs: def test_knn_graph_provided_in_kwargs(self): """ Test case 1: Verify that the function uses the knn_graph provided in kwargs. """ kwargs = {"knn_graph": csr_matrix(np.random.random((10, 10)))} statistics = {"weighted_knn_graph": None} result = _test_fn_1(kwargs, statistics, k_for_recomputation=5) assert isinstance(result, csr_matrix) assert result.shape == (10, 10) def test_knn_graph_stored_in_statistics(self): """ Test case 2: Verify that the function uses the knn_graph stored in statistics when not provided in kwargs. """ kwargs = {"knn_graph": None} statistics = {"weighted_knn_graph": csr_matrix(np.random.random((10, 10)))} result = _test_fn_1(kwargs, statistics, k_for_recomputation=5) assert isinstance(result, csr_matrix) assert result.shape == (10, 10) def test_knn_graph_precedence_of_kwargs_over_statistics(self): """ Test case 3: Verify that the knn_graph provided in kwargs takes precedence over the one stored in statistics. The knn_graph provided in kwargs is ALWAYS used if available. """ kwargs = {"knn_graph": csr_matrix(np.random.random((10, 10))), "k": 5} statistics = {"weighted_knn_graph": csr_matrix(np.random.random((5, 5)))} result = _test_fn_1(kwargs, statistics, k_for_recomputation=5) assert isinstance(result, csr_matrix) assert result.shape == (10, 10) # Even if the statistics knn_graph is larger, the user-provided knn_graph is preferred statistics = {"weighted_knn_graph": csr_matrix(np.random.random((15, 15)))} result = _test_fn_1(kwargs, statistics, k_for_recomputation=5) assert result.shape == (10, 10) # Even if k is larger than the user-provided knn_graph, the user-provided knn_graph is preferred kwargs = {"knn_graph": csr_matrix(np.random.random((10, 10)))} statistics = {"weighted_knn_graph": csr_matrix(np.random.random((11, 11)))} result = _test_fn_1(kwargs, statistics, k_for_recomputation=15) assert result.shape == (10, 10) def test_no_knn_graph_provided(self): """ Test case 4: Verify that the function returns None when no knn_graph is provided in either kwargs or statistics. """ kwargs = {"knn_graph": None} statistics = {"weighted_knn_graph": None} result = _test_fn_1(kwargs, statistics, k_for_recomputation=0) assert result is None def test_insufficient_knn_graph(self): """ Test case 5: Verify the behavior of the function when the knn_graph in statistics is insufficient for the given k value. """ k = 20 kwargs = {"knn_graph": csr_matrix(np.random.random((10, 10)))} statistics = {"weighted_knn_graph": None} result = _test_fn_1(kwargs, statistics, k) assert result.shape == (10, 10) kwargs = {"knn_graph": None} statistics = {"weighted_knn_graph": csr_matrix(np.random.random((10, 10)))} result = _test_fn_1(kwargs, statistics, k) assert result is None statistics = {"weighted_knn_graph": csr_matrix(np.random.random((21, 21)))} result = _test_fn_1(kwargs, statistics, k) assert result.shape == (21, 21) # With sufficiently small k, the kwargs graph is preferred as it's explicitly provided by the user k = 5 kwargs = {"knn_graph": csr_matrix(np.random.random((10, 10)))} result = _test_fn_1(kwargs, statistics, k) assert result.shape == (10, 10) kwargs = {"knn_graph": None} result = _test_fn_1(kwargs, statistics, k) assert result.shape == (21, 21) class TestSetKNNGraph: @pytest.fixture def small_knn_graph(self): knn_graph, _ = _make_knn(np.random.random((10, 5)), n_neighbors=5, metric="euclidean") return knn_graph @pytest.fixture def mid_knn_graph(self): knn_graph, _ = _make_knn(np.random.random((10, 5)), n_neighbors=7, metric="euclidean") return knn_graph @pytest.fixture def large_knn_graph(self): knn_graph, _ = _make_knn(np.random.random((10, 5)), n_neighbors=9, metric="euclidean") return knn_graph @pytest.fixture def manhattan_knn_graph(self): knn_graph, _ = _make_knn(np.random.random((10, 5)), n_neighbors=5, metric="manhattan") return knn_graph def test_knn_graph_provided_in_kwargs(self, small_knn_graph): """ Test case 1: Verify that the function uses the knn_graph provided in kwargs. """ features = np.random.random((10, 5)) find_issues_kwargs = {"knn_graph": small_knn_graph} statistics = {"weighted_knn_graph": None} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=5, statistics=statistics ) assert isinstance(result_graph, csr_matrix) assert _get_num_neighbors(result_graph) == 5 assert result_metric == "euclidean" def test_knn_graph_stored_in_statistics(self, small_knn_graph): """ Test case 2: Verify that the function uses the knn_graph stored in statistics when not provided in kwargs. """ features = np.random.random((10, 5)) find_issues_kwargs = {"knn_graph": None} statistics = {"weighted_knn_graph": small_knn_graph, "knn_metric": "euclidean"} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=5, statistics=statistics ) assert isinstance(result_graph, csr_matrix) assert _get_num_neighbors(result_graph) == 5 assert result_metric == "euclidean" # Even if k is smaller than what is in statitics, the metric will cause a recompute statistics = {"weighted_knn_graph": small_knn_graph, "knn_metric": "euclidean_outdated"} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=4, statistics=statistics ) assert _get_num_neighbors(result_graph) == 4 assert result_metric == "euclidean" # If the metric hasn't changed, but the value of k is larger than the stored knn_graph, the knn_graph is recomputed statistics = {"weighted_knn_graph": small_knn_graph, "knn_metric": "euclidean"} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=6, statistics=statistics ) assert _get_num_neighbors(result_graph) == 6 assert result_metric == "euclidean" def test_knn_graph_precedence_of_kwargs_over_statistics( self, small_knn_graph, mid_knn_graph, large_knn_graph ): """ Test case 3: Verify that the knn_graph provided in kwargs takes precedence over the one stored in statistics. The knn_graph provided in kwargs is ALWAYS used if available. """ features = np.random.random((10, 5)) find_issues_kwargs = {"knn_graph": mid_knn_graph} statistics = {"weighted_knn_graph": small_knn_graph, "knn_metric": "euclidean"} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=5, statistics=statistics ) assert isinstance(result_graph, csr_matrix) assert _get_num_neighbors(result_graph) == 7 assert result_metric == "euclidean" # Even if the statistics knn_graph is larger, the user-provided knn_graph is preferred statistics = {"weighted_knn_graph": large_knn_graph, "knn_metric": "euclidean"} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=5, statistics=statistics ) assert _get_num_neighbors(result_graph) == 7 assert result_metric == "euclidean" # Even if k is larger than the user-provided knn_graph, the user-provided knn_graph is preferred result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=8, statistics=statistics ) assert _get_num_neighbors(result_graph) == 7 assert result_metric == "euclidean" def test_no_knn_graph_provided(self): """ Test case 4: Verify that the function creates a new knn_graph when no knn_graph is provided in either kwargs or statistics. Features are required. """ features = np.random.random((10, 5)) find_issues_kwargs = {"knn_graph": None} statistics = {"weighted_knn_graph": None} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="cosine", k=3, statistics=statistics ) assert _get_num_neighbors(result_graph) == 3 assert result_metric == "cosine" with pytest.raises( AssertionError, match="Features must be provided to compute the knn graph." ): _test_fn_2(None, find_issues_kwargs, metric="cosine", k=0, statistics=statistics) def test_metric_change_requires_new_knn_graph(self, manhattan_knn_graph): """ Test case 5: Verify that the function creates a new knn_graph if the metric has changed. """ features = np.random.random((10, 5)) find_issues_kwargs = {"knn_graph": None} statistics = {"weighted_knn_graph": manhattan_knn_graph, "knn_metric": "manhattan"} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=2, statistics=statistics ) assert _get_num_neighbors(result_graph) == 2 assert result_metric == "euclidean" def test_knn_graph_with_insufficient_graph(self, small_knn_graph, large_knn_graph): """ Test case 6: Verify the behavior of the function when the knn_graph in statistics is insufficient for the given k value. """ features = np.random.random((10, 5)) k = 8 find_issues_kwargs = {"knn_graph": small_knn_graph} statistics = {"weighted_knn_graph": None} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=k, statistics=statistics ) assert _get_num_neighbors(result_graph) == 5 assert result_metric == "euclidean" # The small graph doesn't have enough neighbors, so it should be recomputed find_issues_kwargs = {"knn_graph": None} statistics = {"weighted_knn_graph": small_knn_graph} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=k, statistics=statistics ) assert _get_num_neighbors(result_graph) == 8 assert result_metric is "euclidean" # The large graph has more than enough neighbors, so it should be used statistics = {"weighted_knn_graph": large_knn_graph} result_graph, result_metric, _ = _test_fn_2( features, find_issues_kwargs, metric="euclidean", k=k, statistics=statistics ) assert _get_num_neighbors(result_graph) == 9 assert result_metric is "euclidean" def test_knn_returned(self, small_knn_graph): features = np.random.random((10, 5)) k = 3 result_graph, result_metric, result_knn = _test_fn_2( features, {"knn_graph": None}, metric="cosine", k=k, statistics={} ) assert isinstance(result_knn, NearestNeighbors) assert result_knn.n_neighbors == k assert result_knn.metric == "cosine" result_graph, result_metric, result_knn = _test_fn_2( features, {"knn_graph": small_knn_graph}, metric="euclidean", k=k, statistics={} ) assert result_knn == None assert result_metric == "euclidean" np.testing.assert_array_equal(result_graph.toarray(), small_knn_graph.toarray()) result_graph, result_metric, result_knn = _test_fn_2( features, {"knn_graph": None}, metric="euclidean", k=k, statistics={"weighted_knn_graph": small_knn_graph, "knn_metric": "euclidean"}, ) assert result_knn == None assert result_metric == "euclidean" np.testing.assert_array_equal(result_graph.toarray(), small_knn_graph.toarray())