Add POI feature and imporve the overall backend implementation
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
106
services/forecasting/app/services/poi_feature_service.py
Normal file
106
services/forecasting/app/services/poi_feature_service.py
Normal 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
|
||||
Reference in New Issue
Block a user