@@ -24,14 +24,15 @@ def test_evaluate_returns_report_with_metrics() -> None:
|
||||
report = evaluator.evaluate(
|
||||
"artifact_v1",
|
||||
[
|
||||
"artifact_v1:sensor.kitchen:{'temperature': 21.0}",
|
||||
"artifact_v1:sensor.bedroom:{'temperature': 18.5}",
|
||||
_vector("sensor.kitchen", 21.0),
|
||||
_vector("sensor.bedroom", 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 {metric.name for metric in report.metrics} == {"mae", "rmse", "coverage"}
|
||||
assert next(metric.value for metric in report.metrics if metric.name == "coverage") == 1.0
|
||||
assert next(metric.value for metric in report.metrics if metric.name == "mae") == 0.0
|
||||
|
||||
|
||||
def test_evaluate_without_training_raises_value_error() -> None:
|
||||
@@ -40,16 +41,16 @@ def test_evaluate_without_training_raises_value_error() -> None:
|
||||
evaluator.evaluate("artifact_v1", [])
|
||||
|
||||
|
||||
def test_coverage_is_bounded_and_requires_exact_sensor_match() -> None:
|
||||
def test_coverage_counts_only_supported_sensor_features() -> None:
|
||||
evaluator = evaluator_factory()
|
||||
report = evaluator.evaluate(
|
||||
"artifact_v1",
|
||||
[
|
||||
"artifact_v1:sensor.kitchen:{'note': 'sensor.bedroom'}",
|
||||
"artifact_v1:sensor.kitchen_extra:{}",
|
||||
"malformed",
|
||||
_vector("sensor.kitchen", 21.0),
|
||||
FeatureVector(sensor_id="sensor.kitchen", values={"humidity": 50.0}),
|
||||
_vector("sensor.kitchen_extra", 20.0),
|
||||
],
|
||||
)
|
||||
|
||||
metrics = {metric.name: metric.value for metric in report.metrics}
|
||||
assert metrics == {"coverage": pytest.approx(1 / 3), "unknown_rate": pytest.approx(2 / 3)}
|
||||
assert metrics["coverage"] == pytest.approx(1 / 3)
|
||||
|
||||
@@ -19,6 +19,24 @@ def test_registry_loads_persisted_artifacts_after_restart(tmp_path: Path) -> Non
|
||||
assert restarted.load_artifact("model-v1") == artifact
|
||||
|
||||
|
||||
def test_registry_persists_statistical_parameters(tmp_path: Path) -> None:
|
||||
from app.ml.feature_store import FeatureStore, FeatureVector
|
||||
from app.ml.training import TrainingPipeline
|
||||
|
||||
store = FeatureStore()
|
||||
store.add_batch(
|
||||
[
|
||||
FeatureVector("sensor.kitchen", {"temperature": 19.0}),
|
||||
FeatureVector("sensor.kitchen", {"temperature": 20.0}),
|
||||
]
|
||||
)
|
||||
artifact = TrainingPipeline(store).run("model-v1")
|
||||
|
||||
ModelRegistry(tmp_path).register(artifact)
|
||||
|
||||
assert ModelRegistry(tmp_path).load_artifact("model-v1") == artifact
|
||||
|
||||
|
||||
def test_registry_replaces_persisted_artifact_after_restart(tmp_path: Path) -> None:
|
||||
registry = ModelRegistry(tmp_path)
|
||||
registry.register(TrainedArtifact("model-v1", ("sensor.kitchen",)))
|
||||
|
||||
@@ -13,16 +13,26 @@ def _vector(sensor_id: str, temperature: float, label: str | None = None) -> Fea
|
||||
|
||||
def predictor() -> Predictor:
|
||||
store = FeatureStore()
|
||||
store.add_batch([_vector("sensor.kitchen", 19.0), _vector("sensor.bedroom", 18.5)])
|
||||
store.add_batch(
|
||||
[
|
||||
_vector("sensor.kitchen", 19.0),
|
||||
_vector("sensor.kitchen", 20.0),
|
||||
_vector("sensor.bedroom", 18.5),
|
||||
]
|
||||
)
|
||||
pipeline = TrainingPipeline(store)
|
||||
pipeline.run("artifact_v1")
|
||||
return Predictor(pipeline)
|
||||
|
||||
|
||||
def test_predict_returns_expected_format() -> None:
|
||||
def test_predict_returns_statistical_forecast() -> None:
|
||||
p = predictor()
|
||||
result = p.predict("artifact_v1", _vector("sensor.kitchen", 21.0))
|
||||
assert result == "artifact_v1:sensor.kitchen:{'temperature': 21.0}"
|
||||
assert result.artifact_id == "artifact_v1"
|
||||
assert result.sensor_id == "sensor.kitchen"
|
||||
assert result.predictions == {"temperature": 22.0}
|
||||
assert 0.0 < result.confidence <= 1.0
|
||||
assert result.model_type == "statistical_baseline"
|
||||
|
||||
|
||||
def test_predict_rejects_unknown_sensor() -> None:
|
||||
@@ -45,4 +55,4 @@ def test_default_artifact_returns_last_registered() -> None:
|
||||
pipeline = TrainingPipeline(store)
|
||||
pipeline.run("first")
|
||||
pipeline.run("second")
|
||||
assert Predictor.default_artifact(pipeline).artifact_id == "second"
|
||||
assert Predictor.default_artifact(pipeline).artifact_id == "second"
|
||||
|
||||
@@ -27,6 +27,11 @@ def test_run_returns_trained_artifact() -> None:
|
||||
artifact = pipeline.run("artifact_v1")
|
||||
assert artifact.artifact_id == "artifact_v1"
|
||||
assert artifact.supported_sensors == ("sensor.bedroom", "sensor.kitchen")
|
||||
kitchen = artifact.feature_models["sensor.kitchen"]["temperature"]
|
||||
assert kitchen.sample_count == 2
|
||||
assert kitchen.mean == 19.5
|
||||
assert kitchen.slope == 1.0
|
||||
assert kitchen.forecast() == 21.0
|
||||
|
||||
|
||||
def test_run_without_data_raises_value_error() -> None:
|
||||
|
||||
@@ -16,18 +16,18 @@ def test_end_to_end_training_then_evaluation() -> None:
|
||||
artifact = pipeline.run("artifact_v1")
|
||||
|
||||
evaluator = Evaluator(pipeline)
|
||||
predictions = [
|
||||
"artifact_v1:sensor.kitchen:{'temperature': 21.0}",
|
||||
"artifact_v1:sensor.bedroom:{'temperature': 18.5}",
|
||||
samples = [
|
||||
_vector("sensor.kitchen", 21.0),
|
||||
_vector("sensor.bedroom", 18.5),
|
||||
]
|
||||
report = evaluator.evaluate(artifact.artifact_id, predictions)
|
||||
report = evaluator.evaluate(artifact.artifact_id, samples)
|
||||
assert isinstance(report, EvalReport)
|
||||
assert report.sample_size == len(predictions)
|
||||
assert report.sample_size == len(samples)
|
||||
assert any(metric.name == "coverage" for metric in report.metrics)
|
||||
|
||||
|
||||
def test_metric_helpers_are_serializable() -> None:
|
||||
metric = Metric(name="coverage", value=0.85, threshold=0.8)
|
||||
assert metric.name == "coverage"
|
||||
metric = Metric(name="mae", value=0.85, threshold=1.0)
|
||||
assert metric.name == "mae"
|
||||
assert metric.value == 0.85
|
||||
assert metric.threshold == 0.8
|
||||
assert metric.threshold == 1.0
|
||||
|
||||
Reference in New Issue
Block a user