Files
sillyhome-next-dev/app/ml/predictor.py
Otto df2ddacfbf
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled
ML-008: add statistical baseline model
Closes #19
2026-06-13 20:13:06 +02:00

92 lines
3.3 KiB
Python

from __future__ import annotations
import logging
import math
from dataclasses import dataclass
from typing import Sequence
from app.ml.feature_store import FeatureVector
from app.ml.registry.model_registry import ModelRegistry
from app.ml.training import TrainedArtifact, TrainingPipeline
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class PredictionResult:
artifact_id: str
sensor_id: str
predictions: dict[str, float]
confidence: float
model_type: str
class Predictor:
def __init__(
self,
pipeline: TrainingPipeline | None = None,
registry: ModelRegistry | None = None,
) -> None:
if isinstance(pipeline, ModelRegistry) and registry is None:
registry = pipeline
pipeline = None
if pipeline is None and registry is None:
raise ValueError("Predictor erfordert TrainingPipeline oder ModelRegistry.")
self._pipeline = pipeline
self._registry = registry
def predict(self, artifact_id: str, entity: FeatureVector) -> PredictionResult:
artifact = self._get_artifact(artifact_id)
if entity.sensor_id not in artifact.supported_sensors:
raise ValueError(
f"Sensor '{entity.sensor_id}' wird vom Modell '{artifact_id}' nicht unterstützt."
)
sensor_models = artifact.feature_models.get(entity.sensor_id, {})
if not sensor_models:
raise ValueError(f"Modell '{artifact_id}' enthält keine statistischen Parameter.")
feature_names = sorted(set(sensor_models).intersection(entity.values))
if not feature_names:
raise ValueError(
f"Keine Eingabemerkmale werden vom Modell '{artifact_id}' unterstützt."
)
predictions: dict[str, float] = {}
confidences: list[float] = []
for feature_name in feature_names:
model = sensor_models[feature_name]
current_value = float(entity.values[feature_name])
if not math.isfinite(current_value):
raise ValueError("Vorhersagewerte müssen endlich sein.")
predictions[feature_name] = model.forecast(current_value)
confidences.append(model.confidence)
return PredictionResult(
artifact_id=artifact_id,
sensor_id=entity.sensor_id,
predictions=predictions,
confidence=sum(confidences) / len(confidences),
model_type=artifact.model_type,
)
def predict_batch(
self,
artifact_id: str,
entities: Sequence[FeatureVector],
) -> list[PredictionResult]:
return [self.predict(artifact_id, entity) for entity in entities]
@staticmethod
def default_artifact(pipeline: TrainingPipeline) -> TrainedArtifact:
artifacts = list(pipeline._artifacts)
if not artifacts:
raise ValueError("Kein trainiertes Modell gefunden.")
return pipeline.export(artifacts[-1])
def _get_artifact(self, artifact_id: str) -> TrainedArtifact:
if self._registry is not None:
return self._registry.load_artifact(artifact_id)
if self._pipeline is not None:
return self._pipeline.export(artifact_id)
raise RuntimeError("Predictor nicht initialisiert.")