Files
2026-07-13 12:49:22 +08:00

241 lines
12 KiB
Python

import numpy as np
import pytest
import pandas as pd
import scipy.sparse as sp
from cleanlab.datalab.internal.issue_manager.underperforming_group import (
UnderperformingGroupIssueManager,
)
from sklearn.datasets import make_blobs, load_iris
SEED = 42
class TestUnderperformingGroupIssueManager:
def generate_pred_probs(self, N, K, labels, noisy=False):
pred_probs = np.full((N, K), 0.1)
pred_probs[np.arange(N), labels] = 0.9
pred_probs = pred_probs / np.sum(pred_probs, axis=-1, keepdims=True)
if noisy: # Swap columns of a class to generate incorrect predictions
pred_probs_slice = pred_probs[labels == 0]
pred_probs_slice[:, [0, 1]] = pred_probs_slice[:, [1, 0]]
pred_probs[labels == 0] = pred_probs_slice
return pred_probs
@pytest.fixture
def make_data(self, noisy=False):
def data(noisy=noisy):
N = 400
K = 4
features, labels = make_blobs(n_samples=N, centers=K, n_features=2, random_state=SEED)
pred_probs = self.generate_pred_probs(N, K, labels, noisy)
data = {"features": features, "pred_probs": pred_probs, "labels": labels}
return data
return data
@pytest.fixture
def iris_data(self):
iris_dataset = load_iris()
features, labels = iris_dataset.data, iris_dataset.target
K = len(iris_dataset.target_names)
N = features.shape[0]
pred_probs = self.generate_pred_probs(N, K, labels, noisy=True)
data = {"features": features, "pred_probs": pred_probs, "labels": labels}
return data
@pytest.fixture
def issue_manager(self, lab, make_data, monkeypatch):
data = make_data()
monkeypatch.setattr(lab._labels, "labels", data["labels"])
clustering_kwargs = {"eps": 2}
return UnderperformingGroupIssueManager(
datalab=lab, threshold=0.2, clustering_kwargs=clustering_kwargs
)
def test_find_issues_no_underperforming_group(self, issue_manager, make_data):
data = make_data()
features, labels, pred_probs = data["features"], data["labels"], data["pred_probs"]
N = len(labels)
issue_manager.find_issues(features=features, pred_probs=pred_probs)
issues, summary = issue_manager.issues, issue_manager.summary
assert np.sum(issues["is_underperforming_group_issue"]) == 0
expected_issue_mask = np.full(N, False, bool)
assert np.all(
issues["is_underperforming_group_issue"] == expected_issue_mask
), "Issue mask should be correct"
expected_scores = np.ones(N)
np.testing.assert_allclose(
issues["underperforming_group_score"],
expected_scores,
err_msg="Scores should be correct",
)
assert summary["issue_type"][0] == "underperforming_group"
assert summary["score"][0] == pytest.approx(1.0, abs=0.01)
# Check with cluster_ids param
issue_manager.find_issues(features=features, pred_probs=pred_probs, cluster_ids=labels)
issues_with_clabels, summary_with_clabels = issue_manager.issues, issue_manager.summary
pd.testing.assert_frame_equal(issues_with_clabels, issues)
pd.testing.assert_frame_equal(summary_with_clabels, summary)
def test_find_issues(self, issue_manager, make_data):
RELATIVE_TOLERANCE = 1e-3
data = make_data(noisy=True)
features, labels, pred_probs = data["features"], data["labels"], data["pred_probs"]
N = len(labels)
issue_manager.find_issues(features=features, pred_probs=pred_probs)
issues, summary = issue_manager.issues, issue_manager.summary
assert np.sum(issues["is_underperforming_group_issue"]) == 100
expected_issue_mask = np.zeros(N, bool)
expected_issue_mask[labels == 0] = True
assert np.all(
issues["is_underperforming_group_issue"] == expected_issue_mask
), "Issue mask should be correct"
expected_loss_ratio = 0.1429
expected_scores = np.ones(N)
expected_scores[labels == 0] = expected_loss_ratio
np.testing.assert_allclose(
issues["underperforming_group_score"],
expected_scores,
err_msg="Scores should be correct",
rtol=1e-3,
)
assert summary["issue_type"][0] == "underperforming_group"
assert summary["score"][0] == pytest.approx(expected_loss_ratio, rel=RELATIVE_TOLERANCE)
# Check with cluster_ids param
issue_manager.find_issues(features=features, pred_probs=pred_probs, cluster_ids=labels)
issues_with_clabels, summary_with_clabels = issue_manager.issues, issue_manager.summary
pd.testing.assert_frame_equal(issues_with_clabels, issues, rtol=RELATIVE_TOLERANCE)
pd.testing.assert_frame_equal(summary_with_clabels, summary, rtol=RELATIVE_TOLERANCE)
# With shifted cluster_ids
issue_manager.find_issues(features=features, pred_probs=pred_probs, cluster_ids=labels + 10)
issues_with_clabels, summary_with_clabels = issue_manager.issues, issue_manager.summary
pd.testing.assert_frame_equal(issues_with_clabels, issues, rtol=RELATIVE_TOLERANCE)
pd.testing.assert_frame_equal(summary_with_clabels, summary, rtol=RELATIVE_TOLERANCE)
def test_collect_info(self, issue_manager, make_data):
"""Test some values in the info dict.
Mainly focused on the clustering info.
"""
UNDERPERFORMING_CLUSTER_ID = 0
data = make_data(noisy=True)
features, pred_probs, labels = data["features"], data["pred_probs"], data["labels"]
issue_manager.find_issues(features=features, pred_probs=pred_probs)
info = issue_manager.info
assert "weighted_knn_graph" in info["statistics"]
assert "clustering" in info
# Check clustering info
clustering_info = info["clustering"]
assert clustering_info["algorithm"] == "DBSCAN"
assert clustering_info["params"]["metric"] == "precomputed"
assert clustering_info["stats"]["n_clusters"] == 4
# Test collect_info() with cluster_ids
issue_manager.find_issues(features=features, pred_probs=pred_probs, cluster_ids=labels)
info = issue_manager.info
assert "nearest_neighbor" not in info
assert "distance_to_nearest_neighbor" not in info
assert info["statistics"] == {}
# Check clustering info
clustering_info = info["clustering"]
assert clustering_info["algorithm"] is None
assert clustering_info["params"] == {}
assert clustering_info["stats"]["underperforming_cluster_id"] == UNDERPERFORMING_CLUSTER_ID
cluster_labels = clustering_info["stats"]["cluster_ids"]
issues = issue_manager.issues
issue_indices = issues.index[issues["is_underperforming_group_issue"]].values
assert np.all(
cluster_labels[issue_indices] == UNDERPERFORMING_CLUSTER_ID
), "All samples with issue should belong to underperforming cluster"
np.testing.assert_equal(clustering_info["stats"]["cluster_ids"], labels)
def test_no_meaningful_clusters(self, issue_manager, make_data, monkeypatch):
np.random.seed(SEED)
data = make_data()
k = 10
N = 20
# Generate sparse data that cannot be clustered by DBSCAN
features = np.random.uniform(-100, 100, (N, 2))
# Dummy pred_probs and labels for running issue manager
pred_probs = data["pred_probs"][:N]
monkeypatch.setattr(issue_manager.datalab._labels, "labels", data["labels"][:N])
monkeypatch.setattr(issue_manager, "k", k) # Ensure that k is smaller than N
exception_pattern = "No meaningful clusters"
with pytest.raises(ValueError, match=exception_pattern):
issue_manager.find_issues(features=features, pred_probs=pred_probs)
# Cluster labels passed containing all outliers
cluster_ids = np.full(N, -1) # -1 is the outlier label for DBSCAN
with pytest.raises(ValueError, match=exception_pattern):
issue_manager.find_issues(
features=features, pred_probs=pred_probs, cluster_ids=cluster_ids
)
# Empty cluster ids
with pytest.raises(ValueError, match=exception_pattern):
issue_manager.find_issues(
features=features, pred_probs=pred_probs, cluster_ids=np.array([], dtype=int)
)
def test_min_cluster_samples(self, lab, issue_manager, make_data):
data = make_data()
features, pred_probs, labels = data["features"], data["pred_probs"], data["labels"]
labels[:3] = max(labels) + 1 # New cluster with very few samples
n_clusters = len(set(labels))
# Check if small cluster is filtered
issue_manager.find_issues(features=features, pred_probs=pred_probs, cluster_ids=labels)
clustering_info = issue_manager.info["clustering"]
assert clustering_info["stats"]["n_clusters"] == n_clusters - 1
# New issue manager to consider small cluster as well
issue_manager = UnderperformingGroupIssueManager(
datalab=lab, threshold=0.2, min_cluster_samples=3
)
issue_manager.find_issues(features=features, pred_probs=pred_probs, cluster_ids=labels)
clustering_info = issue_manager.info["clustering"]
assert clustering_info["stats"]["n_clusters"] == n_clusters
def test_find_issues_feature_subset(self, issue_manager, iris_data, monkeypatch):
features, pred_probs, labels = (
iris_data["features"],
iris_data["pred_probs"],
iris_data["labels"],
)
# TODO: Better asserts required. Ideally -> 3 clusters, 50 samples with issue.
monkeypatch.setattr(issue_manager.datalab._labels, "labels", labels)
monkeypatch.setattr(issue_manager, "clustering_kwargs", {"eps": 0.5})
# Find underperforming group based on one feature
single_feature = features[:, 0].reshape(-1, 1)
issue_manager.find_issues(features=single_feature, pred_probs=pred_probs)
assert np.sum(issue_manager.issues["is_underperforming_group_issue"]) == 16
# Find underperforming group based on two features
monkeypatch.setattr(issue_manager, "info", {})
issue_manager.find_issues(features=features[:, [1, 3]], pred_probs=pred_probs)
assert np.sum(issue_manager.issues["is_underperforming_group_issue"]) == 49
# Find underperforming group based on all features
monkeypatch.setattr(issue_manager, "info", {})
issue_manager.find_issues(features=features, pred_probs=pred_probs)
assert np.sum(issue_manager.issues["is_underperforming_group_issue"]) == 48
def test_knn_graph_change(self, issue_manager):
dist_matrix = np.random.randint(1, 5, size=(10, 10))
np.fill_diagonal(dist_matrix, 0) # Make diagonal 0 to mimic distance matrix
knn_graph = sp.csr_matrix(dist_matrix)
nnz_before_clustering = knn_graph.nnz
issue_manager.perform_clustering(knn_graph)
nnz_after_clustering = knn_graph.nnz
assert nnz_before_clustering == nnz_after_clustering
def test_report(self, issue_manager, make_data):
data = make_data()
features, pred_probs = data["features"], data["pred_probs"]
issue_manager.find_issues(features=features, pred_probs=pred_probs)
report = issue_manager.report(
issues=issue_manager.issues,
summary=issue_manager.summary,
info=issue_manager.info,
)
assert isinstance(report, str)
assert (
"--------------- underperforming_group issues ---------------\n\nNumber of examples with this issue"
) in report