SIENTIAPDE-1430: Add unit tests for Silverman radius and RCE drift metrics.

This commit is contained in:
Bruno Domingues
2025-12-18 18:19:35 -03:00
parent 06fd08dc70
commit 0d50a76f4c

View File

@@ -1,8 +1,17 @@
"""Unit tests for sientia metrics module."""
import numpy as np
import pandas as pd
from model_manager.sientia.metrics import mae, mse, r2
from model_manager.sientia.metrics import (
mae,
mse,
r2,
rce_drift,
rce_test,
rce_train,
silverman_radius,
)
def test_mse_perfect_predictions():
@@ -200,3 +209,273 @@ def test_r2_with_mixed_positive_negative():
result = r2(real_data, predictions)
assert result == 1.0
# ============================================================================
# Tests for silverman_radius
# ============================================================================
def test_silverman_radius_basic():
"""Test silverman_radius returns a positive float."""
data = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])
result = silverman_radius(data)
assert isinstance(result, float)
assert result > 0
def test_silverman_radius_uniform_data():
"""Test silverman_radius with uniformly distributed data."""
data = np.linspace(0, 100, 50)
result = silverman_radius(data)
assert result > 0
assert np.isfinite(result)
def test_silverman_radius_normal_distribution():
"""Test silverman_radius with normally distributed data."""
np.random.seed(42)
data = np.random.normal(loc=50, scale=10, size=100)
result = silverman_radius(data)
assert result > 0
assert np.isfinite(result)
def test_silverman_radius_small_dataset():
"""Test silverman_radius with small dataset."""
data = np.array([1.0, 2.0, 3.0])
result = silverman_radius(data)
assert result > 0
# ============================================================================
# Tests for rce_train
# ============================================================================
def test_rce_train_returns_dataframe():
"""Test rce_train returns a DataFrame."""
training_set = pd.DataFrame({'a': [1.0, 2.0, 3.0, 4.0, 5.0], 'b': [2.0, 3.0, 4.0, 5.0, 6.0]})
result = rce_train(training_set, 0.1)
assert isinstance(result, pd.DataFrame)
def test_rce_train_includes_first_vector():
"""Test rce_train always includes the first vector as a prototype."""
training_set = pd.DataFrame({'a': [1.0, 2.0, 3.0], 'b': [1.0, 2.0, 3.0]})
result = rce_train(training_set, 0.1)
assert len(result) >= 1
assert result.iloc[0].tolist() == [1.0, 1.0]
def test_rce_train_with_identical_vectors():
"""Test rce_train with identical vectors returns single prototype."""
training_set = pd.DataFrame({'a': [1.0, 1.0, 1.0], 'b': [2.0, 2.0, 2.0]})
result = rce_train(training_set, 0.1)
# All vectors are identical, so only one prototype should be created
assert len(result) == 1
def test_rce_train_with_distant_vectors():
"""Test rce_train with very distant vectors creates multiple prototypes."""
training_set = pd.DataFrame({'a': [0.0, 100.0, 200.0], 'b': [0.0, 100.0, 200.0]})
result = rce_train(training_set, 0.1)
# Distant vectors should create multiple prototypes
assert len(result) >= 1
# ============================================================================
# Tests for rce_test
# ============================================================================
def test_rce_test_returns_series():
"""Test rce_test returns a pandas Series."""
test_set = pd.DataFrame({'a': [1.5, 2.5], 'b': [1.5, 2.5]})
prototypes = pd.DataFrame({'a': [1.0, 3.0], 'b': [1.0, 3.0]})
result = rce_test(test_set, prototypes)
assert isinstance(result, pd.Series)
assert len(result) == len(test_set)
def test_rce_test_with_exact_match():
"""Test rce_test with test vector matching a prototype."""
test_set = pd.DataFrame({'a': [1.0], 'b': [2.0]})
prototypes = pd.DataFrame({'a': [1.0], 'b': [2.0]})
result = rce_test(test_set, prototypes)
# Distance should be 0 for exact match
assert result.iloc[0] == 0.0
def test_rce_test_multiple_prototypes():
"""Test rce_test finds closest prototype."""
test_set = pd.DataFrame({'a': [1.1], 'b': [1.1]})
prototypes = pd.DataFrame({'a': [1.0, 10.0], 'b': [1.0, 10.0]})
result = rce_test(test_set, prototypes)
# Should find the closest prototype (1.0, 1.0)
assert len(result) == 1
assert np.isfinite(result.iloc[0])
def test_rce_test_signed_distances():
"""Test rce_test returns signed distances."""
test_set = pd.DataFrame({'a': [0.0, 5.0], 'b': [0.0, 5.0]})
prototypes = pd.DataFrame({'a': [2.0], 'b': [2.0]})
result = rce_test(test_set, prototypes)
assert len(result) == 2
# First test vector (0,0) is less than prototype (2,2) - should be negative
# Second test vector (5,5) is greater than prototype (2,2) - should be positive
assert result.iloc[0] < 0
assert result.iloc[1] > 0
# ============================================================================
# Tests for rce_drift
# ============================================================================
def test_rce_drift_returns_series():
"""Test rce_drift returns a pandas Series."""
reference_data = pd.DataFrame(
{
'feature1': [1.0, 2.0, 3.0, 4.0, 5.0],
'feature2': [2.0, 3.0, 4.0, 5.0, 6.0],
'target': [10.0, 20.0, 30.0, 40.0, 50.0],
'prediction': [11.0, 21.0, 31.0, 41.0, 51.0],
}
)
real_data = pd.DataFrame(
{
'feature1': [1.5, 2.5],
'feature2': [2.5, 3.5],
'target': [15.0, 25.0],
'prediction': [16.0, 26.0],
}
)
result = rce_drift(reference_data, real_data, 'target')
assert isinstance(result, pd.Series)
assert len(result) == len(real_data)
def test_rce_drift_with_target_column():
"""Test rce_drift using target column (drops prediction)."""
reference_data = pd.DataFrame(
{
'feature1': [1.0, 2.0, 3.0],
'target': [10.0, 20.0, 30.0],
'prediction': [11.0, 21.0, 31.0],
}
)
real_data = pd.DataFrame(
{
'feature1': [1.5],
'target': [15.0],
'prediction': [16.0],
}
)
result = rce_drift(reference_data, real_data, 'target')
assert isinstance(result, pd.Series)
assert len(result) == 1
def test_rce_drift_with_prediction_column():
"""Test rce_drift using prediction column (drops target)."""
reference_data = pd.DataFrame(
{
'feature1': [1.0, 2.0, 3.0],
'target': [10.0, 20.0, 30.0],
'prediction': [11.0, 21.0, 31.0],
}
)
real_data = pd.DataFrame(
{
'feature1': [1.5],
'target': [15.0],
'prediction': [16.0],
}
)
result = rce_drift(reference_data, real_data, 'prediction')
assert isinstance(result, pd.Series)
assert len(result) == 1
def test_rce_drift_normalized_output():
"""Test rce_drift returns normalized distances."""
reference_data = pd.DataFrame(
{
'feature1': [1.0, 2.0, 3.0, 4.0, 5.0],
'target': [10.0, 20.0, 30.0, 40.0, 50.0],
'prediction': [10.0, 20.0, 30.0, 40.0, 50.0],
}
)
real_data = pd.DataFrame(
{
'feature1': [2.5, 3.5],
'target': [25.0, 35.0],
'prediction': [25.0, 35.0],
}
)
result = rce_drift(reference_data, real_data, 'target')
# Result should be a Series with same length as real_data
assert isinstance(result, pd.Series)
assert len(result) == len(real_data)
def test_rce_drift_handles_common_columns():
"""Test rce_drift correctly handles common columns between datasets."""
reference_data = pd.DataFrame(
{
'feature1': [1.0, 2.0, 3.0],
'feature2': [2.0, 3.0, 4.0],
'extra_ref': [100.0, 200.0, 300.0],
'target': [10.0, 20.0, 30.0],
'prediction': [11.0, 21.0, 31.0],
}
)
real_data = pd.DataFrame(
{
'feature1': [1.5],
'feature2': [2.5],
'extra_real': [150.0],
'target': [15.0],
'prediction': [16.0],
}
)
result = rce_drift(reference_data, real_data, 'target')
# Should work with only common columns
assert isinstance(result, pd.Series)
assert len(result) == 1