Add forecasting service
This commit is contained in:
166
services/forecasting/app/services/prediction_service.py
Normal file
166
services/forecasting/app/services/prediction_service.py
Normal 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
|
||||
Reference in New Issue
Block a user