""" Traffic Forecasting System Predicts bakery foot traffic using weather and temporal features """ import pandas as pd import numpy as np from typing import Dict, List, Any, Optional from prophet import Prophet import structlog from datetime import datetime, timedelta logger = structlog.get_logger() class TrafficForecaster: """ Forecast bakery foot traffic using Prophet with weather and temporal features. Traffic patterns are influenced by: - Weather: Temperature, precipitation, conditions - Time: Day of week, holidays, season - Special events: Local events, promotions """ def __init__(self): self.model = None self.is_trained = False def train( self, historical_traffic: pd.DataFrame, weather_data: pd.DataFrame = None ) -> Dict[str, Any]: """ Train traffic forecasting model. Args: historical_traffic: DataFrame with columns ['date', 'traffic_count'] weather_data: Optional weather data with columns ['date', 'temperature', 'precipitation', 'condition'] Returns: Training metrics """ try: logger.info("Training traffic forecasting model", data_points=len(historical_traffic)) # Prepare Prophet format df = historical_traffic.copy() df = df.rename(columns={'date': 'ds', 'traffic_count': 'y'}) df['ds'] = pd.to_datetime(df['ds']) df = df.sort_values('ds') # Merge with weather data if available if weather_data is not None: weather_data = weather_data.copy() weather_data['date'] = pd.to_datetime(weather_data['date']) df = df.merge(weather_data, left_on='ds', right_on='date', how='left') # Create Prophet model with custom settings for traffic self.model = Prophet( seasonality_mode='multiplicative', yearly_seasonality=True, weekly_seasonality=True, daily_seasonality=False, changepoint_prior_scale=0.05, # Moderate flexibility seasonality_prior_scale=10.0, holidays_prior_scale=10.0 ) # Add weather regressors if available if 'temperature' in df.columns: self.model.add_regressor('temperature') if 'precipitation' in df.columns: self.model.add_regressor('precipitation') if 'is_rainy' in df.columns: self.model.add_regressor('is_rainy') # Add custom holidays for Spain from app.ml.prophet_manager import BakeryProphetManager spanish_holidays = self._get_spanish_holidays( df['ds'].min().year, df['ds'].max().year + 1 ) self.model.add_country_holidays(country_name='ES') # Fit model self.model.fit(df) self.is_trained = True # Calculate training metrics predictions = self.model.predict(df) metrics = self._calculate_metrics(df['y'].values, predictions['yhat'].values) logger.info("Traffic forecasting model trained successfully", mape=metrics['mape'], rmse=metrics['rmse']) return metrics except Exception as e: logger.error(f"Failed to train traffic forecasting model: {e}") raise def predict( self, future_dates: pd.DatetimeIndex, weather_forecast: pd.DataFrame = None ) -> pd.DataFrame: """ Predict traffic for future dates. Args: future_dates: Dates to predict traffic for weather_forecast: Optional weather forecast data Returns: DataFrame with columns ['date', 'predicted_traffic', 'yhat_lower', 'yhat_upper'] """ if not self.is_trained: raise ValueError("Model not trained. Call train() first.") try: # Create future dataframe future = pd.DataFrame({'ds': future_dates}) # Add weather features if available if weather_forecast is not None: weather_forecast = weather_forecast.copy() weather_forecast['date'] = pd.to_datetime(weather_forecast['date']) future = future.merge(weather_forecast, left_on='ds', right_on='date', how='left') # Fill missing weather with defaults if 'temperature' in future.columns: future['temperature'].fillna(15.0, inplace=True) if 'precipitation' in future.columns: future['precipitation'].fillna(0.0, inplace=True) if 'is_rainy' in future.columns: future['is_rainy'].fillna(0, inplace=True) # Predict forecast = self.model.predict(future) # Format results results = pd.DataFrame({ 'date': forecast['ds'], 'predicted_traffic': forecast['yhat'].clip(lower=0), # Traffic can't be negative 'yhat_lower': forecast['yhat_lower'].clip(lower=0), 'yhat_upper': forecast['yhat_upper'].clip(lower=0) }) logger.info("Traffic predictions generated", dates=len(results), avg_traffic=results['predicted_traffic'].mean()) return results except Exception as e: logger.error(f"Failed to predict traffic: {e}") raise def _calculate_metrics(self, actual: np.ndarray, predicted: np.ndarray) -> Dict[str, float]: """Calculate forecast accuracy metrics""" mae = np.mean(np.abs(actual - predicted)) mse = np.mean((actual - predicted) ** 2) rmse = np.sqrt(mse) # MAPE (handle zeros) mask = actual != 0 mape = np.mean(np.abs((actual[mask] - predicted[mask]) / actual[mask])) * 100 if mask.any() else 0 return { 'mae': float(mae), 'mse': float(mse), 'rmse': float(rmse), 'mape': float(mape) } def _get_spanish_holidays(self, start_year: int, end_year: int) -> pd.DataFrame: """Get Spanish holidays for the date range""" try: import holidays es_holidays = holidays.Spain(years=range(start_year, end_year + 1)) holiday_dates = [] holiday_names = [] for date, name in es_holidays.items(): holiday_dates.append(date) holiday_names.append(name) return pd.DataFrame({ 'ds': pd.to_datetime(holiday_dates), 'holiday': holiday_names }) except Exception as e: logger.warning(f"Could not load Spanish holidays: {e}") return pd.DataFrame(columns=['ds', 'holiday']) class TrafficFeatureGenerator: """ Generate traffic-related features for demand forecasting. Uses predicted traffic as a feature in product demand models. """ def __init__(self, traffic_forecaster: TrafficForecaster = None): self.traffic_forecaster = traffic_forecaster or TrafficForecaster() def generate_traffic_features( self, dates: pd.DatetimeIndex, weather_forecast: pd.DataFrame = None ) -> pd.DataFrame: """ Generate traffic features for given dates. Args: dates: Dates to generate features for weather_forecast: Optional weather forecast Returns: DataFrame with traffic features """ if not self.traffic_forecaster.is_trained: logger.warning("Traffic forecaster not trained, using default traffic values") return pd.DataFrame({ 'date': dates, 'predicted_traffic': 100.0, # Default baseline 'traffic_normalized': 1.0 }) # Predict traffic traffic_predictions = self.traffic_forecaster.predict(dates, weather_forecast) # Normalize traffic (0-2 range, 1 = average) mean_traffic = traffic_predictions['predicted_traffic'].mean() traffic_predictions['traffic_normalized'] = ( traffic_predictions['predicted_traffic'] / mean_traffic ).clip(0, 2) # Add traffic categories traffic_predictions['traffic_category'] = pd.cut( traffic_predictions['predicted_traffic'], bins=[0, 50, 100, 150, np.inf], labels=['low', 'medium', 'high', 'very_high'] ) return traffic_predictions def add_traffic_features_to_forecast_data( self, forecast_data: pd.DataFrame, traffic_predictions: pd.DataFrame ) -> pd.DataFrame: """ Add traffic features to forecast input data. Args: forecast_data: Existing forecast data with 'date' column traffic_predictions: Traffic predictions from generate_traffic_features() Returns: Enhanced forecast data with traffic features """ forecast_data = forecast_data.copy() forecast_data['date'] = pd.to_datetime(forecast_data['date']) traffic_predictions['date'] = pd.to_datetime(traffic_predictions['date']) # Merge traffic features enhanced_data = forecast_data.merge( traffic_predictions[['date', 'predicted_traffic', 'traffic_normalized']], on='date', how='left' ) # Fill missing with defaults enhanced_data['predicted_traffic'].fillna(100.0, inplace=True) enhanced_data['traffic_normalized'].fillna(1.0, inplace=True) return enhanced_data