Code import - branch release/SIENTIAPDE-1645
This commit is contained in:
481
tests/sientia/test_metrics.py
Normal file
481
tests/sientia/test_metrics.py
Normal file
@@ -0,0 +1,481 @@
|
||||
"""Unit tests for sientia metrics module."""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from model_manager.sientia.metrics import (
|
||||
mae,
|
||||
mse,
|
||||
r2,
|
||||
rce_drift,
|
||||
rce_test,
|
||||
rce_train,
|
||||
silverman_radius,
|
||||
)
|
||||
|
||||
|
||||
def test_mse_perfect_predictions():
|
||||
"""Test MSE with perfect predictions returns 0.0."""
|
||||
real_data = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
predictions = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
|
||||
result = mse(real_data, predictions)
|
||||
|
||||
assert result == 0.0
|
||||
|
||||
|
||||
def test_mse_with_errors():
|
||||
"""Test MSE calculation with prediction errors."""
|
||||
real_data = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
predictions = pd.Series([1.5, 2.5, 3.5, 4.5, 5.5])
|
||||
|
||||
result = mse(real_data, predictions)
|
||||
|
||||
# MSE = mean((0.5^2, 0.5^2, 0.5^2, 0.5^2, 0.5^2)) = 0.25
|
||||
assert result == 0.25
|
||||
|
||||
|
||||
def test_mse_with_integer_input():
|
||||
"""Test MSE handles integer input and converts to float64."""
|
||||
real_data = pd.Series([1, 2, 3, 4, 5])
|
||||
predictions = pd.Series([2, 3, 4, 5, 6])
|
||||
|
||||
result = mse(real_data, predictions)
|
||||
|
||||
# MSE = mean((1^2, 1^2, 1^2, 1^2, 1^2)) = 1.0
|
||||
assert result == 1.0
|
||||
|
||||
|
||||
def test_mse_with_large_errors():
|
||||
"""Test MSE with large prediction errors."""
|
||||
real_data = pd.Series([10.0, 20.0, 30.0])
|
||||
predictions = pd.Series([5.0, 15.0, 25.0])
|
||||
|
||||
result = mse(real_data, predictions)
|
||||
|
||||
# MSE = mean((25, 25, 25)) = 25.0
|
||||
assert result == 25.0
|
||||
|
||||
|
||||
def test_mse_rounds_to_two_decimals():
|
||||
"""Test MSE rounds result to 2 decimal places."""
|
||||
real_data = pd.Series([1.111, 2.222, 3.333])
|
||||
predictions = pd.Series([1.222, 2.333, 3.444])
|
||||
|
||||
result = mse(real_data, predictions)
|
||||
|
||||
# Result should be rounded to 2 decimals
|
||||
assert isinstance(result, float)
|
||||
assert len(str(result).split('.')[-1]) <= 2
|
||||
|
||||
|
||||
def test_mae_perfect_predictions():
|
||||
"""Test MAE with perfect predictions returns 0.0."""
|
||||
real_data = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
predictions = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
|
||||
result = mae(real_data, predictions)
|
||||
|
||||
assert result == 0.0
|
||||
|
||||
|
||||
def test_mae_with_errors():
|
||||
"""Test MAE calculation with prediction errors."""
|
||||
real_data = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
predictions = pd.Series([1.5, 2.5, 3.5, 4.5, 5.5])
|
||||
|
||||
result = mae(real_data, predictions)
|
||||
|
||||
# MAE = mean(|0.5|, |0.5|, |0.5|, |0.5|, |0.5|) = 0.5
|
||||
assert result == 0.5
|
||||
|
||||
|
||||
def test_mae_with_integer_input():
|
||||
"""Test MAE handles integer input and converts to float64."""
|
||||
real_data = pd.Series([1, 2, 3, 4, 5])
|
||||
predictions = pd.Series([2, 3, 4, 5, 6])
|
||||
|
||||
result = mae(real_data, predictions)
|
||||
|
||||
# MAE = mean(|1|, |1|, |1|, |1|, |1|) = 1.0
|
||||
assert result == 1.0
|
||||
|
||||
|
||||
def test_mae_with_negative_errors():
|
||||
"""Test MAE with negative prediction errors (absolute value)."""
|
||||
real_data = pd.Series([10.0, 20.0, 30.0])
|
||||
predictions = pd.Series([15.0, 25.0, 35.0])
|
||||
|
||||
result = mae(real_data, predictions)
|
||||
|
||||
# MAE = mean(|5|, |5|, |5|) = 5.0
|
||||
assert result == 5.0
|
||||
|
||||
|
||||
def test_mae_rounds_to_two_decimals():
|
||||
"""Test MAE rounds result to 2 decimal places."""
|
||||
real_data = pd.Series([1.111, 2.222, 3.333])
|
||||
predictions = pd.Series([1.222, 2.333, 3.444])
|
||||
|
||||
result = mae(real_data, predictions)
|
||||
|
||||
# Result should be rounded to 2 decimals
|
||||
assert isinstance(result, float)
|
||||
assert len(str(result).split('.')[-1]) <= 2
|
||||
|
||||
|
||||
def test_r2_perfect_predictions():
|
||||
"""Test R2 with perfect predictions returns 1.0."""
|
||||
real_data = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
predictions = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
|
||||
result = r2(real_data, predictions)
|
||||
|
||||
assert result == 1.0
|
||||
|
||||
|
||||
def test_r2_with_good_predictions():
|
||||
"""Test R2 calculation with good predictions."""
|
||||
real_data = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
predictions = pd.Series([1.1, 2.1, 2.9, 4.1, 4.9])
|
||||
|
||||
result = r2(real_data, predictions)
|
||||
|
||||
# R2 should be close to 1.0 for good predictions
|
||||
assert result > 0.9
|
||||
assert result <= 1.0
|
||||
|
||||
|
||||
def test_r2_with_integer_input():
|
||||
"""Test R2 handles integer input and converts to float64."""
|
||||
real_data = pd.Series([1, 2, 3, 4, 5])
|
||||
predictions = pd.Series([1, 2, 3, 4, 5])
|
||||
|
||||
result = r2(real_data, predictions)
|
||||
|
||||
assert result == 1.0
|
||||
|
||||
|
||||
def test_r2_with_poor_predictions():
|
||||
"""Test R2 with poor predictions returns low score."""
|
||||
real_data = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])
|
||||
predictions = pd.Series([5.0, 4.0, 3.0, 2.0, 1.0])
|
||||
|
||||
result = r2(real_data, predictions)
|
||||
|
||||
# R2 should be negative for predictions worse than mean
|
||||
assert result < 0
|
||||
|
||||
|
||||
def test_r2_rounds_to_two_decimals():
|
||||
"""Test R2 rounds result to 2 decimal places."""
|
||||
real_data = pd.Series([1.111, 2.222, 3.333, 4.444, 5.555])
|
||||
predictions = pd.Series([1.222, 2.333, 3.444, 4.555, 5.666])
|
||||
|
||||
result = r2(real_data, predictions)
|
||||
|
||||
# Result should be rounded to 2 decimals
|
||||
assert isinstance(result, float)
|
||||
assert len(str(result).split('.')[-1]) <= 2
|
||||
|
||||
|
||||
def test_mse_with_mixed_positive_negative():
|
||||
"""Test MSE with mixed positive and negative values."""
|
||||
real_data = pd.Series([-5.0, -2.0, 0.0, 3.0, 7.0])
|
||||
predictions = pd.Series([-4.0, -1.0, 1.0, 4.0, 8.0])
|
||||
|
||||
result = mse(real_data, predictions)
|
||||
|
||||
# MSE = mean((1^2, 1^2, 1^2, 1^2, 1^2)) = 1.0
|
||||
assert result == 1.0
|
||||
|
||||
|
||||
def test_mae_with_mixed_positive_negative():
|
||||
"""Test MAE with mixed positive and negative values."""
|
||||
real_data = pd.Series([-5.0, -2.0, 0.0, 3.0, 7.0])
|
||||
predictions = pd.Series([-4.0, -1.0, 1.0, 4.0, 8.0])
|
||||
|
||||
result = mae(real_data, predictions)
|
||||
|
||||
# MAE = mean(|1|, |1|, |1|, |1|, |1|) = 1.0
|
||||
assert result == 1.0
|
||||
|
||||
|
||||
def test_r2_with_mixed_positive_negative():
|
||||
"""Test R2 with mixed positive and negative values."""
|
||||
real_data = pd.Series([-5.0, -2.0, 0.0, 3.0, 7.0])
|
||||
predictions = pd.Series([-5.0, -2.0, 0.0, 3.0, 7.0])
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user