Files
sillyhome-next-dev/tests/ml/test_evaluation.py

56 lines
1.9 KiB
Python

from __future__ import annotations
import pytest
from app.ml.evaluation import Evaluator
from app.ml.feature_store import FeatureStore, FeatureVector
from app.ml.training import TrainingPipeline
def _vector(sensor_id: str, temperature: float, label: str | None = None) -> FeatureVector:
return FeatureVector(sensor_id=sensor_id, values={"temperature": temperature}, label=label)
def evaluator_factory() -> Evaluator:
store = FeatureStore()
store.add_batch([_vector("sensor.kitchen", 19.0), _vector("sensor.bedroom", 18.5)])
pipeline = TrainingPipeline(store)
pipeline.run("artifact_v1")
return Evaluator(pipeline)
def test_evaluate_returns_report_with_metrics() -> None:
evaluator = evaluator_factory()
report = evaluator.evaluate(
"artifact_v1",
[
"artifact_v1:sensor.kitchen:{'temperature': 21.0}",
"artifact_v1:sensor.bedroom:{'temperature': 18.5}",
],
)
assert report.artifact_id == "artifact_v1"
assert report.sample_size == 2
assert {metric.name for metric in report.metrics} == {"coverage", "unknown_rate"}
assert next(metric.value for metric in report.metrics if metric.name == "coverage") == 1.0
def test_evaluate_without_training_raises_value_error() -> None:
evaluator = Evaluator(TrainingPipeline(FeatureStore()))
with pytest.raises(ValueError):
evaluator.evaluate("artifact_v1", [])
def test_coverage_is_bounded_and_requires_exact_sensor_match() -> None:
evaluator = evaluator_factory()
report = evaluator.evaluate(
"artifact_v1",
[
"artifact_v1:sensor.kitchen:{'note': 'sensor.bedroom'}",
"artifact_v1:sensor.kitchen_extra:{}",
"malformed",
],
)
metrics = {metric.name: metric.value for metric in report.metrics}
assert metrics == {"coverage": pytest.approx(1 / 3), "unknown_rate": pytest.approx(2 / 3)}