268 lines
8.7 KiB
Python
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)
|