AI-Powered Demand Forecasting for eCommerce

IKEA moves billions of products across 450+ stores in 54 markets. When their forecasting is off by even 1%, it means warehouses full of BILLY bookcases nobody wants and empty shelves where KALLAX units should be. Their solution? A three-year collaboration with Blue Yonder that produced Demand Sensing — an AI tool that analyses up to 200 data sources per product, from weather forecasts to local salary payment dates, and improved forecast accuracy by 5% in Portugal while cutting manual overrides from 8% to 2%. That's the gold standard. But here's what matters for the rest of us: the same fundamental techniques IKEA uses are available to any ecommerce platform. The models are open source. The data you need is already in your database. And the difference between gut-feel inventory management and ML-driven forecasting is the difference between clearing dead stock at a loss and having the right products available when customers want them. I build ecommerce platforms on Solidus (the open-source Rails framework that powers shops like Meundies, Bonobos, and plenty of DACH-region retailers) and custom SaaS. Across [Auto-Prammer.at](https://auto-prammer.at), [GrowCentric.ai](https://growcentric.ai), [Stint.co](https://stint.co), and [Regios.at](https://regios.at), demand forecasting drives everything from inventory allocation to campaign budget planning to email send scheduling. This post breaks down three forecasting approaches I use — Prophet, SARIMA, and XGBoost — explains when each one shines, and shows the practical Rails implementation that ties them into a Solidus-based ecommerce stack.

What IKEA Got Right (and What It Means for Smaller Shops)

IKEA's traditional forecasting relied on statistical sales patterns — essentially, what sold last year at this time. That works until it doesn't. It misses the heatwave that drives unexpected demand for desk fans. It misses the TikTok trend that makes a particular shelf unit go viral. It misses the salary payment cycle that means people buy furniture on the 27th of the month, not the 15th.

Their Demand Sensing tool changed this by pulling in external signals: weather forecasts, local economic data, promotional calendars, web traffic patterns, holiday schedules, and even in-store footfall data. The AI analyses how these signals correlate with demand for each specific product at each specific location.

The result: forecast accuracy improved by 5% in Portugal (which sounds small until you multiply it across billions of euros in inventory), manual overrides dropped from 8% to 2%, and the tool can forecast from one day to four months ahead.

Three lessons translate directly to smaller ecommerce operations:

First, the data you already have is more valuable than you think. Your Solidus database contains order history, product views, search queries, cart abandonment patterns, email engagement data, and promotional calendars. That's already enough to build useful forecasts.

Second, external signals matter enormously. Weather, holidays, payday cycles, competitor promotions, and marketing campaigns all influence demand. The more of these you can feed into your model, the better it gets.

Third, forecast at the right granularity. IKEA forecasts per product per store. You might forecast per product category per marketing channel. The granularity depends on what decisions you're making.

The Three Models: When to Use Each One

Let me introduce the contenders with a practical ecommerce lens.

Prophet (developed by Meta) is built for business time series. It automatically decomposes your data into three components: trend (is this product growing or declining over time?), seasonality (weekly, monthly, yearly patterns), and holiday effects (Black Friday, Christmas, Prime Day). It handles missing data gracefully, which matters when you have products that weren't in stock for periods. And it's designed for analysts who know their business but aren't time series specialists.

When to use Prophet: Products with strong seasonal patterns and holiday effects. New products where you have limited history but strong seasonal priors. Situations where you need interpretable components ("sales trend is up 3% monthly, with a 40% Christmas spike").

SARIMA (Seasonal AutoRegressive Integrated Moving Average) is the mathematical workhorse. It models the relationship between a value and its past values (autoregressive), accounts for differencing to handle trends (integrated), incorporates past forecast errors (moving average), and adds seasonal components. It's the benchmark — if your fancy ML model can't beat SARIMA, you don't need the fancy ML model.

When to use SARIMA: Staple products with predictable, repeating patterns. Situations where you need a statistically rigorous baseline. Products with clear weekly/monthly/yearly cycles and not much external disruption.

XGBoost (eXtreme Gradient Boosting) is the Swiss army knife. Unlike Prophet and SARIMA, which are fundamentally time series models that look at one variable's history, XGBoost is a general-purpose ML model that can process dozens of input features simultaneously. Price changes, promotional flags, competitor activity, weather data, marketing spend, day of week, month, holiday proximity — all go in as features, and XGBoost learns the complex, non-linear relationships between them and demand.

When to use XGBoost: Products where external factors significantly influence sales (promotions, pricing, marketing). Multi-product forecasting where cross-product effects matter. When you have rich feature data beyond just historical sales.

The Practical Comparison

Let me make this concrete with an Auto-Prammer.at example. We're forecasting demand for vehicle listing enquiries (how many people will contact a seller about a specific type of car).

Prophet sees: "SUV enquiries spike 35% in September (back-to-school family car shopping), dip 20% in December (holiday spending elsewhere), and have been trending up 8% annually."

SARIMA sees: "SUV enquiries follow a seasonal pattern with period 12, the series is stationary after first differencing, and the optimal parameters are (1,1,1)(1,1,1,12)."

XGBoost sees: "SUV enquiries correlate with: fuel price (negative), new model release dates (positive spike 2 weeks after), competitor pricing (negative when competitors drop prices), weather (positive in autumn when people think about winter driving), and marketing spend on SUV campaigns (positive with 3-day lag)."

All three give you a number. The question is which number is most accurate for your specific situation.

The Rails Implementation

Here's how I wire forecasting into a Solidus-based ecommerce platform. The approach uses Python for the ML models (Prophet, statsmodels for SARIMA, xgboost) called from Ruby via a service layer.

module Forecasting
  class Engine
    MODELS = [:prophet, :sarima, :xgboost].freeze

    def forecast(product_or_category:, horizon_days: 30)
      # Gather historical data
      history = gather_history(product_or_category)
      features = gather_features(product_or_category, horizon_days)

      # Run all three models
      predictions = MODELS.each_with_object({}) do |model, results|
        results[model] = run_model(model, history, features, horizon_days)
      end

      # Evaluate on holdout if enough history
      if history.length > 90
        best_model = evaluate_models(predictions, history)
      else
        best_model = :prophet  # Default for limited history
      end

      ForecastResult.new(
        product_or_category: product_or_category,
        predictions: predictions,
        selected_model: best_model,
        selected_forecast: predictions[best_model],
        confidence_interval: predictions[best_model][:confidence],
        generated_at: Time.current
      )
    end

    private

    def gather_history(entity)
      # Pull from Solidus order data
      Spree::LineItem
        .joins(:order)
        .where(orders: { state: 'complete' })
        .where(variant: entity.respond_to?(:variants) ? entity.variants : entity)
        .group("DATE(spree_orders.completed_at)")
        .select("DATE(spree_orders.completed_at) as ds, SUM(quantity) as y")
        .order(:ds)
        .map { |r| { ds: r.ds, y: r.y } }
    end

    def gather_features(entity, horizon)
      {
        price_history: price_changes(entity),
        promotions: upcoming_promotions(entity, horizon),
        holidays: Holiday.for_region(Current.region, horizon.days.from_now),
        weather: WeatherForecast.for_region(Current.region, horizon),
        marketing_spend: MarketingBudget.planned(entity, horizon),
        competitor_prices: CompetitorTracker.recent(entity)
      }
    end

    def evaluate_models(predictions, history)
      # Use last 20% as holdout
      holdout_size = (history.length * 0.2).to_i
      actual = history.last(holdout_size).map { |h| h[:y] }

      scores = predictions.transform_values do |pred|
        predicted = pred[:values].last(holdout_size)
        {
          mae: mean_absolute_error(actual, predicted),
          rmse: root_mean_squared_error(actual, predicted),
          mape: mean_absolute_percentage_error(actual, predicted)
        }
      end

      # Select model with lowest MAE
      scores.min_by { |_, v| v[:mae] }.first
    end
  end
end

The Python models are called via a service that manages the ML runtime:

module Forecasting
  class PythonBridge
    def run_prophet(history, features, horizon)
      script = build_prophet_script(history, features, horizon)
      execute_python(script)
    end

    def run_sarima(history, horizon)
      script = build_sarima_script(history, horizon)
      execute_python(script)
    end

    def run_xgboost(history, features, horizon)
      script = build_xgboost_script(history, features, horizon)
      execute_python(script)
    end

    private

    def build_prophet_script(history, features, horizon)
      <<~PYTHON
        import json
        from prophet import Prophet
        import pandas as pd

        history = pd.DataFrame(#{history.to_json})
        history['ds'] = pd.to_datetime(history['ds'])

        model = Prophet(
          yearly_seasonality=True,
          weekly_seasonality=True,
          daily_seasonality=False,
          changepoint_prior_scale=0.05
        )

        # Add holiday effects
        holidays = pd.DataFrame(#{features[:holidays].to_json})
        if not holidays.empty:
          model = Prophet(holidays=holidays)

        # Add promotional regressor if available
        if #{features[:promotions].present?}:
          model.add_regressor('promotion')

        model.fit(history)

        future = model.make_future_dataframe(periods=#{horizon})
        forecast = model.predict(future)

        result = forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail(#{horizon})
        print(result.to_json(orient='records', date_format='iso'))
      PYTHON
    end
  end
end

How Each Product Uses Forecasting

Auto-Prammer.at (Solidus automotive marketplace): Forecasts enquiry volume per vehicle category (SUV, saloon, estate, electric) to optimise listing placement and seller notifications. XGBoost performs best here because fuel prices, new model releases, and seasonal driving patterns are strong external signals. The forecast drives which categories get homepage prominence and when to push seller notifications.

GrowCentric.ai (campaign optimisation SaaS): Forecasts campaign performance metrics — click-through rates, conversion rates, cost-per-acquisition — for ecommerce clients. Prophet works well for campaign metrics because they have strong weekly seasonality (weekday vs weekend patterns), monthly patterns (payday effects), and holiday spikes. The forecast drives budget allocation recommendations: "Increase spend 40% in the two weeks before Black Friday, reduce 25% in the first week of January."

Stint.co (marketing dashboard): Forecasts email engagement rates to optimise send timing and volume. SARIMA performs surprisingly well here because email open rates have very stable weekly patterns (Tuesday morning opens are consistently 23% higher than Friday afternoon). The forecast drives send scheduling: "For this segment, send Tuesday at 9:15am for maximum opens."

Regios.at (regional discovery platform): Forecasts search demand for local business categories (restaurants, activities, events) to help businesses plan staffing and promotions. Prophet handles the complex seasonality well — restaurant searches spike on Friday evenings, hiking activity searches peak in spring and autumn, event searches correlate with school holidays. The forecast powers the "trending near you" feature.

The Ensemble Approach

In practice, I don't always pick one model. An ensemble that averages the predictions of all three models often outperforms any single model:

module Forecasting
  class Ensemble
    def combine(predictions, weights: nil)
      weights ||= calculate_inverse_error_weights(predictions)

      combined = predictions.values.first[:values].length.times.map do |i|
        weighted_sum = predictions.sum do |model, pred|
          pred[:values][i] * weights[model]
        end
        weighted_sum
      end

      {
        values: combined,
        model: :ensemble,
        component_weights: weights
      }
    end

    private

    def calculate_inverse_error_weights(predictions)
      # Weight inversely proportional to error
      errors = predictions.transform_values { |p| p[:holdout_mae] || 1.0 }
      inverse_sum = errors.values.sum { |e| 1.0 / e }
      errors.transform_values { |e| (1.0 / e) / inverse_sum }
    end
  end
end

The ensemble automatically gives more weight to whichever model performed best on recent holdout data. For Auto-Prammer.at, this typically means XGBoost gets 50-60% weight, Prophet 25-30%, and SARIMA 10-20%. For Stint.co's email engagement, SARIMA gets 45%, Prophet 35%, and XGBoost 20%.

Connecting Forecasts to Inventory Decisions

A forecast is only useful if it drives action. In Solidus, this means connecting the forecasting engine to inventory management:

module Inventory
  class ForecastDrivenManager
    SAFETY_STOCK_MULTIPLIER = 1.15  # 15% buffer

    def recommend_stock_levels(product:, horizon_days: 30)
      forecast = Forecasting::Engine.new.forecast(
        product_or_category: product,
        horizon_days: horizon_days
      )

      daily_demand = forecast.selected_forecast[:values]
      total_demand = daily_demand.sum
      peak_demand = daily_demand.max

      lead_time = product.supplier_lead_time_days || 14
      lead_time_demand = daily_demand.first(lead_time).sum

      {
        recommended_stock: (total_demand * SAFETY_STOCK_MULTIPLIER).ceil,
        reorder_point: (lead_time_demand * SAFETY_STOCK_MULTIPLIER).ceil,
        peak_daily_demand: peak_demand.ceil,
        forecast_model: forecast.selected_model,
        confidence: forecast.selected_forecast[:confidence]
      }
    end
  end
end

This connects forecasting to purchasing decisions in a way that's auditable and explainable — important for the EU AI Act documentation and the GDPR compliance architecture we've built into these products.

Getting Started

You don't need 200 data sources like IKEA. Start here:

  1. Export your order history from Solidus or your ecommerce platform. You need at minimum: date, product/category, quantity sold.

  2. Start with Prophet. It's the most forgiving of the three models, handles missing data, and gives interpretable results. Fit it to your history and generate a 30-day forecast.

  3. Add SARIMA as a benchmark. If SARIMA beats Prophet on your holdout data, your sales patterns are regular enough that simpler is better.

  4. Add XGBoost when you have external features. Price changes, promotional calendars, and marketing spend are the highest-value additions.

  5. Build the ensemble. Weight by holdout performance. Evaluate monthly.

  6. Connect to decisions. A forecast sitting in a dashboard is decorative. A forecast that triggers reorder alerts, adjusts marketing spend, or shifts homepage merchandising is valuable.

The gap between gut-feel inventory management and ML-driven forecasting isn't about sophisticated technology. It's about taking the data you already have and asking it better questions.

Want to add AI-powered demand forecasting to your Solidus store or ecommerce SaaS? Whether you need Prophet-based seasonal forecasting, XGBoost multi-signal prediction, or a full ensemble approach connected to your inventory management, I build these for ecommerce platforms across the DACH market. Let's talk about making your inventory decisions smarter.