AI Demand Forecasting: How Retailers Are Cutting Overstock by 30% with ML

Building machine learning demand forecasting systems that outperform traditional statistical methods

返回教程列表
入门10 分钟

AI Demand Forecasting: How Retailers Are Cutting Overstock by 30% with ML

Building machine learning demand forecasting systems that outperform traditional statistical methods

Learn how major retailers and manufacturers are using machine learning to forecast demand with greater accuracy than traditional methods — reducing stockouts, cutting excess inventory, and optimizing replenishment cycles.

demand-forecastingsupply-chaininventorymachine-learningretail

AI Demand Forecasting: Cutting Inventory Costs with Machine Learning

Inventory is one of the largest working capital costs in retail and manufacturing. Getting demand wrong means either too much (carrying costs, markdowns) or too little (stockouts, lost sales). AI is dramatically improving forecast accuracy.

The Cost of Bad Forecasting

A typical retailer with $500M revenue:

  • Excess inventory carrying cost: 15-30% of inventory value annually
  • Stockout revenue loss: 4-8% of potential sales
  • Combined impact: $30-50M annually from poor forecasting
  • Improving forecast accuracy by even 10% can unlock millions in working capital.

    Why Traditional Methods Fall Short

    Simple moving average: Assumes future = average of past. Misses trends and seasonality. Exponential smoothing: Better for trends, but can't incorporate external signals. ARIMA: Statistically sophisticated but requires manual tuning per SKU, doesn't scale to 100K+ products.

    ML approaches learn patterns across all products simultaneously and can incorporate:

  • Seasonal patterns (multiple seasonality)
  • Promotions and price changes
  • External signals (weather, economic indicators, competitor actions)
  • Product lifecycle patterns
  • Building an ML Demand Forecasting System

    python
    import pandas as pd
    import numpy as np
    from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor
    from sklearn.preprocessing import LabelEncoder
    from sklearn.metrics import mean_absolute_percentage_error
    import warnings
    warnings.filterwarnings('ignore')

    class DemandForecastingModel: """ Multi-step demand forecasting using gradient boosted trees. Handles multiple products, seasonal patterns, and external signals. """ def __init__(self, forecast_horizon_days: int = 28): self.horizon = forecast_horizon_days self.model = GradientBoostingRegressor( n_estimators=300, max_depth=5, learning_rate=0.05, subsample=0.8, random_state=42 ) self.encoders = {} def create_lag_features(self, df: pd.DataFrame, target_col: str = 'sales_units') -> pd.DataFrame: """ Create lag and rolling window features. The core of time series ML: using past values to predict future. """ features = df.copy() # Lag features (past sales values) for lag in [1, 2, 3, 7, 14, 21, 28]: features[f'lag_{lag}d'] = features.groupby('sku_id')[target_col].shift(lag) # Rolling statistics (smoothed historical demand) for window in [7, 14, 28, 90]: roll = features.groupby('sku_id')[target_col].shift(1).rolling(window) features[f'rolling_mean_{window}d'] = roll.mean().reset_index(0, drop=True) features[f'rolling_std_{window}d'] = roll.std().reset_index(0, drop=True) features[f'rolling_max_{window}d'] = roll.max().reset_index(0, drop=True) # Year-over-year (same week last year) features['lag_364d'] = features.groupby('sku_id')[target_col].shift(364) features['lag_371d'] = features.groupby('sku_id')[target_col].shift(371) features['yoy_average'] = (features['lag_364d'] + features['lag_371d']) / 2 # Trend feature: is demand growing or shrinking? features['demand_trend'] = ( features['rolling_mean_14d'] - features['rolling_mean_28d'] ) / (features['rolling_mean_28d'] + 1) return features def create_calendar_features(self, df: pd.DataFrame) -> pd.DataFrame: """ Extract calendar-based features. Captures weekly, monthly, and annual seasonality. """ features = df.copy() date_col = pd.to_datetime(features['date']) # Basic calendar features features['day_of_week'] = date_col.dt.dayofweek # 0=Monday features['day_of_month'] = date_col.dt.day features['week_of_year'] = date_col.dt.isocalendar().week.astype(int) features['month'] = date_col.dt.month features['quarter'] = date_col.dt.quarter features['year'] = date_col.dt.year # Cyclical encoding (Monday=0 and Sunday=6 are adjacent) features['dow_sin'] = np.sin(2 * np.pi * features['day_of_week'] / 7) features['dow_cos'] = np.cos(2 * np.pi * features['day_of_week'] / 7) features['month_sin'] = np.sin(2 * np.pi * features['month'] / 12) features['month_cos'] = np.cos(2 * np.pi * features['month'] / 12) # Key shopping periods features['is_weekend'] = (features['day_of_week'] >= 5).astype(int) features['is_month_start'] = (features['day_of_month'] <= 5).astype(int) features['is_month_end'] = (features['day_of_month'] >= 26).astype(int) # Holiday indicators (add your own) features['days_to_holiday'] = self._days_to_next_holiday(date_col) features['days_after_holiday'] = self._days_after_last_holiday(date_col) return features def create_product_features(self, df: pd.DataFrame) -> pd.DataFrame: """Features specific to each product.""" features = df.copy() # Encode categorical variables for col in ['sku_id', 'category', 'subcategory', 'brand']: if col in features.columns: if col not in self.encoders: self.encoders[col] = LabelEncoder() features[f'{col}_encoded'] = self.encoders[col].fit_transform( features[col].astype(str) ) else: features[f'{col}_encoded'] = self.encoders[col].transform( features[col].astype(str) ) # Price features if 'price' in features.columns: features['price_log'] = np.log1p(features['price']) features['price_vs_category_avg'] = ( features['price'] / features.groupby('category')['price'].transform('mean') ) # Promotion features if 'is_on_promotion' in features.columns: features['promotion_lift'] = ( features.groupby('sku_id')['is_on_promotion'].shift(1) * features.groupby('sku_id')['sales_units'].transform('std') ).fillna(0) return features def train(self, historical_data: pd.DataFrame) -> dict: """Train the forecasting model.""" # Feature engineering pipeline df = self.create_lag_features(historical_data) df = self.create_calendar_features(df) df = self.create_product_features(df) # Drop rows with NaN from lag features df = df.dropna() # Define features to use feature_cols = [c for c in df.columns if c not in ['date', 'sku_id', 'sales_units', 'category', 'subcategory', 'brand']] self.feature_columns = feature_cols X = df[feature_cols].fillna(0) y = df['sales_units'] # Train/test split (time-based) split_date = df['date'].quantile(0.8) train_mask = df['date'] < split_date X_train, X_test = X[train_mask], X[~train_mask] y_train, y_test = y[train_mask], y[~train_mask] self.model.fit(X_train, y_train) # Evaluate y_pred = self.model.predict(X_test) y_pred = np.maximum(y_pred, 0) # No negative forecasts mape = mean_absolute_percentage_error(y_test + 1, y_pred + 1) print(f"Model MAPE: {mape:.1%}") print(f"Training on {len(X_train):,} observations, testing on {len(X_test):,}") return {'mape': mape, 'n_train': len(X_train), 'n_test': len(X_test)} def forecast(self, recent_data: pd.DataFrame, forecast_date: str, sku_ids: list[str] = None) -> pd.DataFrame: """ Generate {horizon}-day demand forecast. Returns DataFrame with sku_id, date, forecasted_demand. """ if sku_ids: data = recent_data[recent_data['sku_id'].isin(sku_ids)] else: data = recent_data # Build feature matrix for forecast date forecast_df = pd.DataFrame() for sku_id in data['sku_id'].unique(): sku_data = data[data['sku_id'] == sku_id].copy() # Create future dates future_dates = pd.date_range( start=forecast_date, periods=self.horizon, freq='D' ) sku_future = pd.DataFrame({ 'date': future_dates, 'sku_id': sku_id, 'sales_units': np.nan # Unknown future values }) # Combine with history for feature creation combined = pd.concat([sku_data.tail(400), sku_future], ignore_index=True) combined = self.create_lag_features(combined) combined = self.create_calendar_features(combined) combined = self.create_product_features(combined) # Get only future rows future_rows = combined[combined['date'].isin(future_dates)] if len(future_rows) > 0: X_future = future_rows[self.feature_columns].fillna(0) predictions = self.model.predict(X_future) sku_forecasts = pd.DataFrame({ 'sku_id': sku_id, 'date': future_dates[:len(predictions)], 'forecasted_demand': np.maximum(predictions, 0).round(0).astype(int) }) forecast_df = pd.concat([forecast_df, sku_forecasts], ignore_index=True) return forecast_df def _days_to_next_holiday(self, dates: pd.Series) -> pd.Series: """Calculate days to next major holiday.""" # Simplified - would have full holiday calendar in production return pd.Series([0] * len(dates)) def _days_after_last_holiday(self, dates: pd.Series) -> pd.Series: return pd.Series([0] * len(dates))

    Calculate reorder recommendations

    def generate_reorder_plan(forecasts: pd.DataFrame, inventory_levels: pd.DataFrame, supplier_lead_times: dict) -> pd.DataFrame: """ Generate purchase order recommendations based on forecasts. """ reorder_plan = [] for sku_id in forecasts['sku_id'].unique(): sku_forecast = forecasts[forecasts['sku_id'] == sku_id] current_stock = inventory_levels[ inventory_levels['sku_id'] == sku_id ]['current_stock'].values[0] if len(inventory_levels[inventory_levels['sku_id'] == sku_id]) > 0 else 0 lead_time = supplier_lead_times.get(sku_id, 14) # Default 14 days safety_stock_days = 7 # Hold 7 days safety stock # Demand during lead time + safety stock demand_window = sku_forecast['forecasted_demand'].sum() reorder_point = (demand_window / len(sku_forecast)) * (lead_time + safety_stock_days) if current_stock < reorder_point: # How much to order? # Order enough for lead time + 30 days of demand order_quantity = max(0, demand_window - current_stock + safety_stock_days) reorder_plan.append({ 'sku_id': sku_id, 'current_stock': current_stock, 'reorder_point': round(reorder_point, 0), 'recommended_order_qty': round(order_quantity, 0), 'days_of_stock_remaining': current_stock / max(demand_window / 28, 1), 'urgency': 'CRITICAL' if current_stock < reorder_point * 0.5 else 'NORMAL' }) return pd.DataFrame(reorder_plan).sort_values('urgency', ascending=False)

    Results Companies Are Achieving

    Walmart's AI demand forecasting:

  • Reduced out-of-stocks by 16%
  • Cut excess inventory by 30%
  • Saved $1.5B in inventory carrying costs (2022)
  • Target's supply chain AI:

  • Forecast accuracy improved from 82% to 91%
  • $800M reduction in safety stock across network
  • Smaller retailer (10,000 SKUs, $200M revenue):

  • MAPE improved from 28% to 14%
  • Inventory reduction: $12M released working capital
  • Stockout rate: 6% → 2.5%
  • The ROI calculation is compelling: improving MAPE by 10 percentage points typically unlocks 15-25% inventory reduction.