Add POI feature and imporve the overall backend implementation

This commit is contained in:
Urtzi Alfaro
2025-11-12 15:34:10 +01:00
parent e8096cd979
commit 5783c7ed05
173 changed files with 16862 additions and 9078 deletions

View File

@@ -200,8 +200,19 @@ async def trigger_rules_generation(
sales_df['date'] = pd.to_datetime(sales_df['date'])
sales_df['quantity'] = sales_df['quantity'].astype(float)
sales_df['day_of_week'] = sales_df['date'].dt.dayofweek
sales_df['is_holiday'] = False # TODO: Add holiday detection
sales_df['weather'] = 'unknown' # TODO: Add weather data
# NOTE: Holiday detection for historical data requires:
# 1. Tenant location context (calendar_id)
# 2. Bulk holiday check API (currently single-date only)
# 3. Historical calendar data
# For real-time forecasts, holiday detection IS implemented via data_client.py
sales_df['is_holiday'] = False
# NOTE: Weather data for historical analysis requires:
# 1. Historical weather API integration
# 2. Tenant location coordinates
# For real-time forecasts, weather data IS fetched via external service
sales_df['weather'] = 'unknown'
# Run rules learning
results = await orchestrator.learn_and_post_rules(

View File

@@ -434,10 +434,22 @@ async def compare_scenarios(
Compare multiple scenario simulations
**PROFESSIONAL/ENTERPRISE ONLY**
**STATUS**: Not yet implemented - requires scenario persistence layer
**Future implementation would**:
1. Retrieve saved scenarios by ID from database
2. Use ScenarioPlanner.compare_scenarios() to analyze them
3. Return comparison matrix with best/worst case analysis
**Prerequisites**:
- Scenario storage/retrieval database layer
- Scenario CRUD endpoints
- UI for scenario management
"""
# TODO: Implement scenario comparison
# This would retrieve saved scenarios and compare them
# NOTE: HTTP 501 Not Implemented is the correct response for unimplemented optional features
# The ML logic exists in scenario_planner.py but requires a persistence layer
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Scenario comparison not yet implemented"
detail="Scenario comparison requires scenario persistence layer (future feature)"
)

View File

@@ -52,6 +52,10 @@ class BakeryForecaster:
self.predictor = BakeryPredictor(database_manager)
self.use_enhanced_features = use_enhanced_features
# Initialize POI feature service
from app.services.poi_feature_service import POIFeatureService
self.poi_feature_service = POIFeatureService()
if use_enhanced_features:
# Import enhanced data processor from training service
import sys
@@ -167,11 +171,24 @@ class BakeryForecaster:
'pressure': [features.get('pressure', 1013.0)]
})
# Fetch POI features if tenant_id is available
poi_features = None
if 'tenant_id' in features:
poi_features = await self.poi_feature_service.get_poi_features(
features['tenant_id']
)
if poi_features:
logger.info(
f"Retrieved {len(poi_features)} POI features for prediction",
tenant_id=features['tenant_id']
)
# Use data processor to create ALL enhanced features
df = await self.data_processor.prepare_prediction_features(
future_dates=future_dates,
weather_forecast=weather_df,
traffic_forecast=None, # Will add when traffic forecasting is implemented
poi_features=poi_features, # POI features for location-based forecasting
historical_data=historical_data # For lagged features
)

View File

@@ -880,6 +880,9 @@ class EnhancedForecastingService:
request: ForecastRequest
) -> Dict[str, Any]:
"""Prepare features with comprehensive fallbacks"""
# Check for school holidays using external service
is_holiday = await self._check_holiday(tenant_id, request.forecast_date)
features = {
"date": request.forecast_date.isoformat(),
"day_of_week": request.forecast_date.weekday(),
@@ -889,7 +892,7 @@ class EnhancedForecastingService:
"quarter": (request.forecast_date.month - 1) // 3 + 1,
"week_of_year": request.forecast_date.isocalendar().week,
"season": self._get_season(request.forecast_date.month),
"is_holiday": self._is_spanish_holiday(request.forecast_date),
"is_holiday": is_holiday,
# CRITICAL FIX: Add tenant_id and inventory_product_id for historical feature enrichment
"tenant_id": tenant_id,
"inventory_product_id": request.inventory_product_id,
@@ -977,6 +980,9 @@ class EnhancedForecastingService:
weather_map: Dict[str, Any]
) -> Dict[str, Any]:
"""Prepare features with comprehensive fallbacks using a pre-fetched weather map"""
# Check for holidays using external service
is_holiday = await self._check_holiday(tenant_id, request.forecast_date)
features = {
"date": request.forecast_date.isoformat(),
"day_of_week": request.forecast_date.weekday(),
@@ -986,7 +992,7 @@ class EnhancedForecastingService:
"quarter": (request.forecast_date.month - 1) // 3 + 1,
"week_of_year": request.forecast_date.isocalendar().week,
"season": self._get_season(request.forecast_date.month),
"is_holiday": self._is_spanish_holiday(request.forecast_date),
"is_holiday": is_holiday,
# CRITICAL FIX: Add tenant_id and inventory_product_id for historical feature enrichment
"tenant_id": tenant_id,
"inventory_product_id": request.inventory_product_id,
@@ -1041,11 +1047,44 @@ class EnhancedForecastingService:
else:
return 4 # Autumn
def _is_spanish_holiday(self, date_obj: date) -> bool:
"""Check if a date is a major Spanish holiday"""
async def _check_holiday(self, tenant_id: str, date_obj: date) -> bool:
"""
Check if a date is a holiday using external service calendar
Falls back to Spanish national holidays if:
- Tenant has no calendar configured
- External service is unavailable
"""
try:
# Get tenant's calendar ID
calendar_id = await self.data_client.get_tenant_calendar(tenant_id)
if calendar_id:
# Check school holiday via external service
is_school_holiday = await self.data_client.check_school_holiday(
calendar_id=calendar_id,
check_date=date_obj.isoformat(),
tenant_id=tenant_id
)
return is_school_holiday
else:
# Fallback to Spanish national holidays
return self._is_spanish_national_holiday(date_obj)
except Exception as e:
logger.warning(
"Holiday check failed, falling back to national holidays",
error=str(e),
tenant_id=tenant_id,
date=date_obj.isoformat()
)
return self._is_spanish_national_holiday(date_obj)
def _is_spanish_national_holiday(self, date_obj: date) -> bool:
"""Check if a date is a major Spanish national holiday (fallback)"""
month_day = (date_obj.month, date_obj.day)
spanish_holidays = [
(1, 1), (1, 6), (5, 1), (8, 15), (10, 12),
(1, 1), (1, 6), (5, 1), (8, 15), (10, 12),
(11, 1), (12, 6), (12, 8), (12, 25)
]
return month_day in spanish_holidays

View File

@@ -0,0 +1,106 @@
"""
POI Feature Service for Forecasting
Fetches POI features for use in demand forecasting predictions.
Ensures feature consistency between training and prediction.
"""
import httpx
from typing import Dict, Any, Optional
import structlog
logger = structlog.get_logger()
class POIFeatureService:
"""
POI feature service for forecasting.
Fetches POI context from External service to ensure
prediction uses the same features as training.
"""
def __init__(self, external_service_url: str = "http://external-service:8000"):
"""
Initialize POI feature service.
Args:
external_service_url: Base URL for external service
"""
self.external_service_url = external_service_url.rstrip("/")
self.poi_context_endpoint = f"{self.external_service_url}/poi-context"
async def get_poi_features(
self,
tenant_id: str
) -> Dict[str, Any]:
"""
Get POI features for tenant.
Args:
tenant_id: Tenant UUID
Returns:
Dictionary with POI features or empty dict if not available
"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
f"{self.poi_context_endpoint}/{tenant_id}"
)
if response.status_code == 404:
logger.warning(
"No POI context found for tenant",
tenant_id=tenant_id
)
return {}
response.raise_for_status()
data = response.json()
poi_context = data.get("poi_context", {})
ml_features = poi_context.get("ml_features", {})
logger.info(
"POI features retrieved for forecasting",
tenant_id=tenant_id,
feature_count=len(ml_features)
)
return ml_features
except httpx.HTTPError as e:
logger.error(
"Failed to fetch POI features for forecasting",
tenant_id=tenant_id,
error=str(e)
)
return {}
except Exception as e:
logger.error(
"Unexpected error fetching POI features",
tenant_id=tenant_id,
error=str(e),
exc_info=True
)
return {}
async def check_poi_service_health(self) -> bool:
"""
Check if POI service is accessible.
Returns:
True if service is healthy, False otherwise
"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(
f"{self.poi_context_endpoint}/health"
)
return response.status_code == 200
except Exception as e:
logger.error(
"POI service health check failed",
error=str(e)
)
return False