Add forecasting service

This commit is contained in:
Urtzi Alfaro
2025-07-21 19:48:56 +02:00
parent 2d85dd3e9e
commit 0e7ca10a29
24 changed files with 2937 additions and 179 deletions

View File

@@ -0,0 +1,166 @@
# ================================================================
# services/forecasting/app/services/prediction_service.py
# ================================================================
"""
Prediction service for loading models and generating predictions
Handles the actual ML prediction logic
"""
import structlog
from typing import Dict, List, Any, Optional
import asyncio
import pickle
import json
from datetime import datetime, date
import numpy as np
import pandas as pd
import httpx
from pathlib import Path
from app.core.config import settings
from shared.monitoring.metrics import MetricsCollector
logger = structlog.get_logger()
metrics = MetricsCollector("forecasting-service")
class PredictionService:
"""
Service for loading ML models and generating predictions
Interfaces with trained Prophet models from the training service
"""
def __init__(self):
self.model_cache = {}
self.cache_ttl = 3600 # 1 hour cache
async def predict(self, model_id: str, features: Dict[str, Any],
confidence_level: float = 0.8) -> Dict[str, float]:
"""Generate prediction using trained model"""
start_time = datetime.now()
try:
logger.info("Generating prediction",
model_id=model_id,
features_count=len(features))
# Load model
model = await self._load_model(model_id)
if not model:
raise ValueError(f"Model {model_id} not found or failed to load")
# Prepare features for Prophet
df = self._prepare_prophet_features(features)
# Generate prediction
forecast = model.predict(df)
# Extract prediction results
if len(forecast) > 0:
row = forecast.iloc[0]
result = {
"demand": float(row['yhat']),
"lower_bound": float(row[f'yhat_lower']),
"upper_bound": float(row[f'yhat_upper']),
"trend": float(row.get('trend', 0)),
"seasonal": float(row.get('seasonal', 0)),
"holiday": float(row.get('holidays', 0))
}
else:
raise ValueError("No prediction generated from model")
# Update metrics
processing_time = (datetime.now() - start_time).total_seconds()
metrics.histogram_observe("forecast_processing_time_seconds", processing_time)
logger.info("Prediction generated successfully",
model_id=model_id,
predicted_demand=result["demand"],
processing_time_ms=int(processing_time * 1000))
return result
except Exception as e:
logger.error("Error generating prediction",
model_id=model_id,
error=str(e))
raise
async def _load_model(self, model_id: str):
"""Load model from cache or training service"""
# Check cache first
if model_id in self.model_cache:
cached_model, cached_time = self.model_cache[model_id]
if (datetime.now() - cached_time).seconds < self.cache_ttl:
logger.debug("Using cached model", model_id=model_id)
return cached_model
try:
# Download model from training service
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(
f"{settings.TRAINING_SERVICE_URL}/api/v1/models/{model_id}/download",
headers={"X-Service-Auth": settings.SERVICE_AUTH_TOKEN}
)
if response.status_code == 200:
# Load model from bytes
model_data = response.content
model = pickle.loads(model_data)
# Cache the model
self.model_cache[model_id] = (model, datetime.now())
logger.info("Model loaded successfully", model_id=model_id)
return model
else:
logger.error("Failed to download model",
model_id=model_id,
status_code=response.status_code)
return None
except Exception as e:
logger.error("Error loading model", model_id=model_id, error=str(e))
return None
def _prepare_prophet_features(self, features: Dict[str, Any]) -> pd.DataFrame:
"""Convert features to Prophet-compatible DataFrame"""
try:
# Create base DataFrame with required 'ds' column
df = pd.DataFrame({
'ds': [pd.to_datetime(features['date'])]
})
# Add numeric features
numeric_features = [
'temperature', 'precipitation', 'humidity', 'wind_speed',
'traffic_volume', 'pedestrian_count'
]
for feature in numeric_features:
if feature in features and features[feature] is not None:
df[feature] = float(features[feature])
else:
df[feature] = 0.0
# Add categorical features
df['day_of_week'] = int(features.get('day_of_week', 0))
df['is_weekend'] = int(features.get('is_weekend', False))
df['is_holiday'] = int(features.get('is_holiday', False))
# Business type encoding
business_type = features.get('business_type', 'individual')
df['is_central_workshop'] = int(business_type == 'central_workshop')
logger.debug("Prepared Prophet features",
features_count=len(df.columns),
date=features['date'])
return df
except Exception as e:
logger.error("Error preparing Prophet features", error=str(e))
raise