From 0d50a76f4c9c90d9cd6d85310209939cde1e6424 Mon Sep 17 00:00:00 2001 From: Bruno Domingues Date: Thu, 18 Dec 2025 18:19:35 -0300 Subject: [PATCH] SIENTIAPDE-1430: Add unit tests for Silverman radius and RCE drift metrics. --- tests/sientia/test_metrics.py | 281 +++++++++++++++++++++++++++++++++- 1 file changed, 280 insertions(+), 1 deletion(-) diff --git a/tests/sientia/test_metrics.py b/tests/sientia/test_metrics.py index ad7ccbb..015a8e3 100644 --- a/tests/sientia/test_metrics.py +++ b/tests/sientia/test_metrics.py @@ -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