"""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