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

268 lines
8.7 KiB
Python

import pytest
import random
import numpy as np
import pandas as pd
from sklearn.svm import SVR
from sklearn.metrics import r2_score
from sklearn.linear_model import LinearRegression
from cleanlab.regression.rank import (
get_label_quality_scores,
_get_residual_score_for_each_label,
_get_outre_score_for_each_label,
)
from cleanlab.regression.learn import CleanLearning
# set seed for reproducability
SEED = 1
np.random.seed(SEED)
random.seed(SEED)
def make_data(num_examples=200, num_features=3, noise=0.2, error_frac=0.1, error_noise=5):
X = np.random.random(size=(num_examples, num_features))
coefficients = np.random.uniform(-1, 1, size=num_features)
label_noise = np.random.normal(scale=noise, size=num_examples)
true_y = np.dot(X, coefficients)
y = np.dot(X, coefficients) + label_noise
# add extra noisy examples
num_errors = int(num_examples * error_frac)
extra_noise = np.random.normal(scale=error_noise, size=num_errors)
random_idx = np.random.choice(num_examples, num_errors)
y[random_idx] += extra_noise
error_idx = np.argsort(abs(y - true_y))[-num_errors:] # get the noisiest examples idx
# create test set
X_test = np.random.random(size=(num_examples, num_features))
label_noise = np.random.normal(scale=noise, size=num_examples)
y_test = np.dot(X_test, coefficients) + label_noise
return {
"X": X,
"y": y,
"true_y": true_y,
"X_test": X_test,
"y_test": y_test,
"error_idx": error_idx,
}
# To be used for most tests
data = make_data()
X, labels, predictions = data["X"], data["y"], data["true_y"]
error_idx = data["error_idx"]
X_test, y_test = data["X_test"], data["y_test"]
y = labels # for ease
# Used for characterization tests
small_labels = np.array([1, 2, 3, 4])
small_predictions = np.array([2, 2, 5, 4.1])
expected_score_outre = np.array([0.2162406, 0.62585509, 0.20275104, 0.62585509])
expected_score_residual = np.array([0.36787944, 1.0, 0.13533528, 0.90483742])
expected_scores = {"outre": expected_score_outre, "residual": expected_score_residual}
# Inputs that are not array like
aConstant = 1
aString = "predictions_non_array"
aDict = {"labels": [1, 2], "predictions": [2, 3]}
aSet = {1, 2, 3, 4}
aBool = True
@pytest.fixture
def non_array_input():
return [aConstant, aString, aDict, aSet, aBool]
# test with deafault parameters
def test_output_shape_type():
scores = get_label_quality_scores(labels=labels, predictions=predictions)
assert labels.shape == scores.shape
assert isinstance(scores, np.ndarray)
def test_labels_are_arraylike(non_array_input):
for new_input in non_array_input:
with pytest.raises(ValueError) as error:
get_label_quality_scores(labels=new_input, predictions=predictions)
assert error.type == ValueError
def test_predictionns_are_arraylike(non_array_input):
for new_input in non_array_input:
with pytest.raises(ValueError) as error:
get_label_quality_scores(labels=labels, predictions=new_input)
assert error.type == ValueError
# test for input shapes
def test_input_shape_labels():
with pytest.raises(AssertionError) as error:
get_label_quality_scores(labels=labels[:-1], predictions=predictions)
assert (
str(error.value)
== f"Number of examples in labels {labels[:-1].shape} and predictions {predictions.shape} are not same."
)
def test_input_shape_predictions():
with pytest.raises(AssertionError) as error:
get_label_quality_scores(labels=labels, predictions=predictions[:-1])
assert (
str(error.value)
== f"Number of examples in labels {labels.shape} and predictions {predictions[:-1].shape} are not same."
)
# test individual scoring functions
@pytest.mark.parametrize(
"scoring_funcs",
[_get_residual_score_for_each_label, _get_outre_score_for_each_label],
)
def test_individual_scoring_functions(scoring_funcs):
scores = scoring_funcs(labels=labels, predictions=predictions)
assert labels.shape == scores.shape
assert isinstance(scores, np.ndarray)
# test for method argument
@pytest.mark.parametrize(
"method",
[
"residual",
"outre",
],
)
def test_method_pass_get_label_quality_scores(method):
scores = get_label_quality_scores(labels=labels, predictions=predictions, method=method)
assert labels.shape == scores.shape
assert isinstance(scores, np.ndarray)
@pytest.mark.parametrize(
"method",
[
"residual",
"outre",
],
)
def test_expected_scores(method):
# characterization test
scores = get_label_quality_scores(
labels=small_labels, predictions=small_predictions, method=method
)
assert np.allclose(scores, expected_scores[method], atol=1e-08)
def test_cleanlearning():
# test fit and predict
cl = CleanLearning()
cl.fit(X, y)
preds = cl.predict(X)
cl_r2_score = cl.score(X, y)
manual_r2_score = r2_score(y, preds)
assert len(preds) == len(y)
assert isinstance(cl_r2_score, float)
assert cl_r2_score == manual_r2_score
# check if label issues were identified
label_issues = cl.get_label_issues()
identified_label_issues = label_issues[label_issues["is_label_issue"] == True].index
frac_errors_identified = np.mean([e in identified_label_issues for e in error_idx])
assert frac_errors_identified >= 0.9 # assert most errors were detected
# compare perf to base LinearRegression model
cl_score = cl.score(X_test, y_test)
lr = LinearRegression()
lr.fit(X, y)
lr_score = lr.score(X_test, y_test)
assert cl_score > lr_score
# test passing in label issues in various forms
# also test different regression model
cl = CleanLearning(model=SVR())
label_issues = cl.find_label_issues(X, y)
assert isinstance(label_issues, pd.DataFrame)
cl.fit(X, y, label_issues=label_issues)
cl.fit(X, pd.Series(y), label_issues=label_issues["is_label_issue"])
cl.fit(X, list(y), label_issues=label_issues["is_label_issue"].values)
def test_optional_inputs():
# test with sample_weight input
cl = CleanLearning(verbose=1)
cl.fit(X, y, sample_weight=np.random.random(size=len(y)))
cl.fit(X, y, label_issues=cl.get_label_issues(), sample_weight=np.random.random(size=len(y)))
# test with uncertainty input
cl = CleanLearning()
cl.find_label_issues(X, y, uncertainty=5) # constant uncertainty
cl.find_label_issues(X, y, uncertainty=np.random.random(size=len(y))) # per-example uncertainty
# test with not calculating uncertainty
cl = CleanLearning(n_boot=0, include_aleatoric_uncertainty=False)
cl.find_label_issues(X, y)
# test with odd grid search sizes
cl = CleanLearning()
cl.find_label_issues(X, y, coarse_search_range=[0.2])
cl.find_label_issues(X, y, fine_search_size=0)
cl.fit(
X, y, find_label_issues_kwargs={"coarse_search_range": [0.2, 0.1], "fine_search_size": 2}
)
def test_low_example_count():
data_tiny = make_data(num_examples=3)
X_tiny, y_tiny = data_tiny["X"], data_tiny["y"]
try:
cl = CleanLearning()
cl.find_label_issues(X_tiny, y_tiny)
except ValueError as e:
assert "There are too few examples" in str(e)
cl = CleanLearning(cv_n_folds=3)
cl.find_label_issues(X_tiny, y_tiny)
assert isinstance(cl.get_label_issues(), pd.DataFrame)
@pytest.mark.filterwarnings("ignore::UserWarning")
def test_save_space():
# test label issues df does not save
cl = CleanLearning()
cl.find_label_issues(X, y, save_space=True)
assert cl.get_label_issues() is None
# test label issues df deletes properly
cl = CleanLearning()
cl.find_label_issues(X, y)
assert isinstance(cl.get_label_issues(), pd.DataFrame)
cl.save_space()
assert cl.get_label_issues() is None
@pytest.mark.parametrize("N", [10, 100, 1000])
@pytest.mark.parametrize("method", ["residual", "outre"])
def test_all_identical_examples(N, method):
# All examples have predictions identical to the given labels/targets
labels = np.zeros(N)
predictions = np.copy(labels)
# Except the last ~quarter of examples have labels that are further away from the predictions
cutoff_index = N // 4
predictions[-cutoff_index:] += 1
scores = get_label_quality_scores(labels=labels, predictions=predictions, method=method)
np.testing.assert_allclose(scores[:-cutoff_index], 1, atol=1e-04)
if method == "outre":
# Assert that the scores for the last (bad) ~quarter of examples are close to 0
np.testing.assert_allclose(scores[-cutoff_index:], 0, atol=1e-04)
else:
# Residual method should give "imperfect" scores for the last ~quarter of examples, but not necessarily near 0
assert np.all(scores[-cutoff_index:] < 1)