app / tests /unit /test_model_evaluator.py
Raksha11's picture
Upload folder using huggingface_hub
9a97708 verified
Raw
History Blame Contribute Delete
8.01 kB
"""
Unit tests for ModelEvaluator (app/ml/evaluator.py).
Tests verify that accuracy, precision, recall, F1-score, and confusion-matrix
counts are computed correctly for known prediction scenarios.
"""
import numpy as np
import pytest
from sklearn.dummy import DummyClassifier
from app.ml.evaluator import EvaluationMetrics, ModelEvaluator
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
class _FixedPredictor:
"""Minimal model stub that always returns a pre-defined prediction array."""
def __init__(self, predictions):
self._predictions = np.asarray(predictions)
def predict(self, X): # noqa: N802
return self._predictions
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestEvaluationMetricsDataclass:
def test_fields_accessible(self):
m = EvaluationMetrics(
accuracy=0.9,
precision=0.85,
recall=0.80,
f1_score=0.82,
tp=45,
tn=45,
fp=5,
fn=5,
)
assert m.accuracy == 0.9
assert m.precision == 0.85
assert m.recall == 0.80
assert m.f1_score == 0.82
assert m.tp == 45
assert m.tn == 45
assert m.fp == 5
assert m.fn == 5
class TestModelEvaluatorBinaryClassification:
"""Tests using a simple binary (0/1) classification scenario."""
def setup_method(self):
self.evaluator = ModelEvaluator()
def _make_data(self, y_true, y_pred):
"""Return (model_stub, X_dummy, y_true_array)."""
model = _FixedPredictor(y_pred)
X = np.zeros((len(y_true), 1)) # features are irrelevant for stub
return model, X, np.asarray(y_true)
# --- perfect predictions ---
def test_perfect_predictions_accuracy_is_one(self):
y_true = [1, 1, 0, 0, 1, 0]
model, X, y = self._make_data(y_true, y_true)
result = self.evaluator.evaluate(model, X, y)
assert result.accuracy == pytest.approx(1.0)
def test_perfect_predictions_confusion_matrix(self):
y_true = [1, 1, 0, 0]
model, X, y = self._make_data(y_true, y_true)
result = self.evaluator.evaluate(model, X, y)
assert result.tp == 2
assert result.tn == 2
assert result.fp == 0
assert result.fn == 0
def test_perfect_predictions_f1_is_one(self):
y_true = [1, 1, 0, 0]
model, X, y = self._make_data(y_true, y_true)
result = self.evaluator.evaluate(model, X, y)
assert result.f1_score == pytest.approx(1.0)
# --- all wrong predictions ---
def test_all_wrong_accuracy_is_zero(self):
y_true = [1, 1, 0, 0]
y_pred = [0, 0, 1, 1]
model, X, y = self._make_data(y_true, y_pred)
result = self.evaluator.evaluate(model, X, y)
assert result.accuracy == pytest.approx(0.0)
def test_all_wrong_confusion_matrix(self):
y_true = [1, 1, 0, 0]
y_pred = [0, 0, 1, 1]
model, X, y = self._make_data(y_true, y_pred)
result = self.evaluator.evaluate(model, X, y)
assert result.tp == 0
assert result.tn == 0
assert result.fp == 2
assert result.fn == 2
# --- mixed predictions ---
def test_mixed_accuracy(self):
# 3 correct out of 4
y_true = [1, 1, 0, 0]
y_pred = [1, 0, 0, 0] # one FN, rest correct
model, X, y = self._make_data(y_true, y_pred)
result = self.evaluator.evaluate(model, X, y)
assert result.accuracy == pytest.approx(0.75)
def test_mixed_confusion_matrix_counts(self):
# TP=1, TN=2, FP=0, FN=1
y_true = [1, 1, 0, 0]
y_pred = [1, 0, 0, 0]
model, X, y = self._make_data(y_true, y_pred)
result = self.evaluator.evaluate(model, X, y)
assert result.tp == 1
assert result.tn == 2
assert result.fp == 0
assert result.fn == 1
def test_confusion_matrix_counts_sum_to_total_samples(self):
y_true = [1, 0, 1, 0, 1, 1, 0, 0]
y_pred = [1, 0, 0, 1, 1, 0, 0, 1]
model, X, y = self._make_data(y_true, y_pred)
result = self.evaluator.evaluate(model, X, y)
total = result.tp + result.tn + result.fp + result.fn
assert total == len(y_true)
# --- return type ---
def test_returns_evaluation_metrics_instance(self):
y_true = [1, 0, 1, 0]
model, X, y = self._make_data(y_true, y_true)
result = self.evaluator.evaluate(model, X, y)
assert isinstance(result, EvaluationMetrics)
def test_metrics_are_floats(self):
y_true = [1, 0, 1, 0]
model, X, y = self._make_data(y_true, y_true)
result = self.evaluator.evaluate(model, X, y)
assert isinstance(result.accuracy, float)
assert isinstance(result.precision, float)
assert isinstance(result.recall, float)
assert isinstance(result.f1_score, float)
def test_confusion_matrix_counts_are_ints(self):
y_true = [1, 0, 1, 0]
model, X, y = self._make_data(y_true, y_true)
result = self.evaluator.evaluate(model, X, y)
assert isinstance(result.tp, int)
assert isinstance(result.tn, int)
assert isinstance(result.fp, int)
assert isinstance(result.fn, int)
# --- metrics are in [0, 1] ---
def test_accuracy_in_unit_interval(self):
y_true = [1, 0, 1, 0, 1]
y_pred = [1, 0, 0, 1, 1]
model, X, y = self._make_data(y_true, y_pred)
result = self.evaluator.evaluate(model, X, y)
assert 0.0 <= result.accuracy <= 1.0
def test_precision_in_unit_interval(self):
y_true = [1, 0, 1, 0, 1]
y_pred = [1, 0, 0, 1, 1]
model, X, y = self._make_data(y_true, y_pred)
result = self.evaluator.evaluate(model, X, y)
assert 0.0 <= result.precision <= 1.0
def test_recall_in_unit_interval(self):
y_true = [1, 0, 1, 0, 1]
y_pred = [1, 0, 0, 1, 1]
model, X, y = self._make_data(y_true, y_pred)
result = self.evaluator.evaluate(model, X, y)
assert 0.0 <= result.recall <= 1.0
def test_f1_in_unit_interval(self):
y_true = [1, 0, 1, 0, 1]
y_pred = [1, 0, 0, 1, 1]
model, X, y = self._make_data(y_true, y_pred)
result = self.evaluator.evaluate(model, X, y)
assert 0.0 <= result.f1_score <= 1.0
# --- works with a real sklearn model ---
def test_works_with_sklearn_dummy_classifier(self):
rng = np.random.default_rng(42)
X_train = rng.random((100, 5))
y_train = rng.integers(0, 2, size=100)
X_test = rng.random((20, 5))
y_test = rng.integers(0, 2, size=20)
clf = DummyClassifier(strategy="most_frequent")
clf.fit(X_train, y_train)
result = self.evaluator.evaluate(clf, X_test, y_test)
assert isinstance(result, EvaluationMetrics)
assert 0.0 <= result.accuracy <= 1.0
total = result.tp + result.tn + result.fp + result.fn
assert total == len(y_test)
# --- original ILPD label encoding (1 = disease, 2 = no disease) ---
def test_works_with_labels_1_and_2(self):
"""Evaluator must handle the original ILPD dataset label encoding."""
y_true = [1, 1, 2, 2, 1, 2]
y_pred = [1, 2, 2, 1, 1, 2]
model, X, y = self._make_data(y_true, y_pred)
result = self.evaluator.evaluate(model, X, y)
assert isinstance(result, EvaluationMetrics)
total = result.tp + result.tn + result.fp + result.fn
assert total == len(y_true)