AI Demand Forecasting for Multi-Channel Shopify Stores
Multi-channel Shopify plus Amazon brands need forecasts that agree across channels. Here is the hybrid architecture we have used to make that work.
A D2C brand that sells on Shopify and Amazon has two demand signals that never quite agree. Shopify orders show up in near real time, minute by minute, with promo codes and abandoned carts attached. Amazon's Selling Partner API delivers sales through the Reports endpoint on a settlement cadence, and the numbers you pull today for last week are not the numbers you will see next month. Inventory sits in a shared pool. A stockout on Amazon caused by an over-optimistic Shopify forecast is indistinguishable, from the warehouse floor, from an under-forecast on Amazon. According to McKinsey's supply chain analyses, AI-driven demand forecasting can reduce forecast error by 20 to 50 percent and cut stockouts by up to 65 percent against traditional statistical baselines, but almost none of that literature addresses the multi-channel case where the same SKU has two demand distributions and one physical inventory. This post walks through the harness we have built for exactly that setup: a unified feature store, per-SKU model routing, hierarchical reconciliation across channels, and explicit handling of promotional distortion.
The Business Problem
The brands we work with in this shape look similar on the surface. They are D2C-first companies doing 8,000 to 40,000 units a month, split roughly 40/60 or 30/70 between their Shopify storefront and their Amazon channel (FBA, occasionally FBM). Amazon is the volume channel; Shopify is the brand channel and the launch channel for new SKUs. Inventory is pooled at a 3PL that fulfills both, or split between Amazon's FBA network and the 3PL, with periodic transfers between them.
The failure mode we keep seeing is the same, and it does not usually get labeled a forecasting problem when it first surfaces. It shows up as: the brand ran out of a hero SKU on Amazon two weeks before Prime Day because the buyer used a Shopify-heavy trailing average. Or the brand overbought a new launch that sold well on Shopify but stalled on Amazon because there was no history for the Amazon channel to key off. Or, most expensively, the brand's working capital is tied up in 90 days of inventory for slow SKUs while fast SKUs stockout twice a quarter.
Retail Analytics data on multi-channel brands consistently shows the same pattern: Amazon FBA replenishment cycles run on a 4 to 8 week planning horizon (inbound receiving windows, shipment routing, IPI limits), while Shopify DTC operates on a 1 to 2 week reorder horizon at the 3PL. A single forecast that ignores this mismatch will always be optimizing for the wrong horizon on at least one channel.
The Technical Problem
The two channels produce different signal shapes, and treating them the same breaks the model.
Shopify Admin API gives you high-fidelity, low-latency data. Orders, line items, discounts, customer LTV. You get the noise (individual customer behavior, abandoned carts, coupon usage) and the signal in one stream. Amazon SP-API gives you aggregated business reports on a delay. Order Reports have a settlement lag, refund and return data lands on a slower cadence still, and the Reports API enforces rate limits and quota-based throttling that make high-frequency polling expensive. The result is that if you build a single forecasting pipeline that treats each channel's daily-sales-per-SKU as the same kind of series, you get a model that is over-fit to Shopify's noise and under-informed about Amazon's real trajectory.
The naive approach we see most often is a single ARIMA or ETS model per channel per SKU. That falls over three ways. Sparse SKUs (fewer than one order a day on Amazon, common for the long tail) violate the model's stationarity assumptions. Event-driven SKUs (anything promoted at BFCM or Prime Day) blow up the seasonal components. And the two channels' independent forecasts double-count the shared inventory pool, either creating phantom safety stock or starving one channel.
The thesis we have converged on is a three-part hybrid: classical statistical models for stable, high-volume SKUs; probabilistic ML for volatile, sparse, or event-driven SKUs; and hierarchical reconciliation to enforce coherence across channels.
Harness Component: A Unified Feature Store
The first thing that broke on our early builds was training per channel. We had a Shopify pipeline that landed daily SKU sales into one table and an Amazon pipeline that landed a different daily SKU sales table into another. Each pipeline had its own feature engineering, its own calendar joins, its own holiday flags. When we trained a channel-aware model against these tables, the model had no way to see that a promotional dip in Shopify on the same day as an Amazon spike were the same event moving demand between channels, not two independent events.
We rebuilt the pipeline around a single per-SKU-per-day feature table with channel as a categorical feature, not a partition. The features are Shopify orders and units (from the Admin API, pulled via GraphQL), Amazon shipped units and ordered units (from the SP-API Sales and Traffic Report and Orders API), promotional flags from a hand-maintained calendar, holiday indicators, weather data for weather-sensitive categories, and a few derived features (rolling 7 and 28 day sales, coefficient of variation over 90 days).
# features/build_daily_features.py
def build_daily_features(sku: str, start: date, end: date) -> pd.DataFrame:
shopify = fetch_shopify_daily(sku, start, end) # from GraphQL landing table
amazon = fetch_amazon_daily(sku, start, end) # from SP-API Sales report
promos = fetch_promo_calendar(sku, start, end) # hand-maintained
base = build_date_scaffold(start, end, channels=["shopify", "amazon"])
df = (
base.merge(shopify, on=["date", "channel"], how="left")
.merge(amazon, on=["date", "channel"], how="left")
.merge(promos, on=["date", "channel"], how="left")
.fillna({"units": 0, "promo": 0})
)
df["units_7d"] = df.groupby("channel")["units"].transform(lambda s: s.rolling(7).mean())
df["cv_90d"] = df.groupby("channel")["units"].transform(lambda s: s.rolling(90).std() / (s.rolling(90).mean() + 1e-6))
return dfThe materialized view refreshes nightly on Cloud Run. The SP-API side is the constraint on how fresh we can make it. Amazon settles order data on a lag, and pulling the Sales and Traffic Report more than once a day is both rate-limited and mostly wasted, because the day's numbers stabilize only after the settlement window closes. We accept a 24 to 36 hour lag on Amazon-side features and design downstream models to tolerate that lag without swinging on the day it lands.
The SP-API quota model bites here. The Reports API has request budgets that vary by report type, and Sales and Traffic Report generation is comparatively expensive. Batching report requests and storing raw report outputs (parquet on cloud storage) before parsing has saved us from quota exhaustion during backfills more than once.
Harness Component: Model Selection per SKU
Not every SKU wants the same model. This was the second lesson: we tried DeepAR (via GluonTS) across the full catalog on an early build, and it did fine on the top 200 SKUs and worse than a naive seasonal average on the tail. The ML paper literature will tell you deep probabilistic models generalize well across series, and they do, but generalization is not the same as accuracy on any given SKU, and the tail SKUs have so little signal that the model's flexibility becomes a liability.
We route SKUs through a classifier that runs weekly and assigns each SKU to a model family based on volume, variance, and event exposure.
# routing/model_selector.py
def route_sku(sku_stats: dict) -> str:
daily_mean = sku_stats["units_daily_mean_90d"]
cv = sku_stats["cv_90d"]
has_promo_history = sku_stats["promo_days_365d"] >= 5
intermittent = sku_stats["zero_days_90d"] / 90 > 0.4
if daily_mean < 0.5 and intermittent:
return "croston" # intermittent demand
if daily_mean >= 5 and cv < 0.5 and not has_promo_history:
return "ets" # stable high-volume, no promo distortion
if has_promo_history or cv >= 0.5:
return "prophet_or_neuralprophet" # event-driven or volatile
return "prophet"The four families are Croston's method for intermittent demand, ETS (state space exponential smoothing) for stable SKUs, Prophet for volatile SKUs with clear seasonality and a decent history, and NeuralProphet or a lightweight DeepAR variant for the SKUs with strong exogenous drivers (promotions, launches, coupled behavior across channels). We had originally planned to use Amazon Forecast as the ML backend, but AWS closed new customer access to Amazon Forecast in July 2024 and the migration path for existing customers is SageMaker Canvas, which shifted our architecture toward self-hosted models on SageMaker training jobs rather than Forecast's managed API.
Practically, the split is roughly 60 percent of SKUs on ETS or Croston, 30 percent on Prophet, and 10 percent on a neural model. The 10 percent generates disproportionate business value because those are the launch SKUs and the promotional hero SKUs where a bad forecast is the most expensive.
15-30%
typical MAPE improvement over naive baseline when routing SKUs to per-family models with promotional features, measured across the last three multi-channel deployments we have shipped
Harness Component: Channel-Aware Forecast Reconciliation
The reconciliation problem is the one that a single-channel forecasting playbook does not prepare you for. If you forecast Amazon and Shopify independently, and each forecast comes with its own safety stock policy, you double-count safety stock for the shared pool. If the two forecasts disagree on total demand (they almost always do), you have to pick one to trust for buying, and the ad-hoc rules teams write to do that ("use the max," "use the sum minus 10 percent") do not converge.
Hierarchical time series reconciliation gives you a principled way out of this. The idea, from Rob Hyndman's work at Monash and Wickramasuriya's MinT paper, is to model the hierarchy explicitly (total = Shopify + Amazon), forecast each level independently, and then reconcile the forecasts using a trace-minimizing projection so that the numbers add up and the errors are minimized under the hierarchy constraint.
# reconciliation/mint.py
import numpy as np
def reconcile_mint(base_forecasts: np.ndarray, S: np.ndarray, W: np.ndarray) -> np.ndarray:
"""
base_forecasts: (n_series,) forecasts at every level (total, shopify, amazon, per-sku, ...)
S: (n_series, n_bottom) summing matrix that encodes the hierarchy
W: (n_series, n_series) covariance of base forecast errors (MinT uses shrinkage estimator)
Returns coherent forecasts.
"""
W_inv = np.linalg.pinv(W)
G = np.linalg.pinv(S.T @ W_inv @ S) @ S.T @ W_inv
bottom = G @ base_forecasts
return S @ bottomIn production we use the Python hierarchicalforecast library (from the Nixtla team) rather than rolling MinT ourselves, but the shape of the math is what matters. The summing matrix S encodes the channel hierarchy per SKU, and the covariance W is estimated with the MinT shrinkage estimator on rolling backtest errors.
The output is a single reconciled forecast per SKU per channel that respects the total-equals-sum-of-channels constraint. That reconciled forecast is what the buying tool consumes. The safety stock policy then runs against the reconciled totals, not against the two independent channel forecasts, and the double-counting problem goes away.
We surface reconciliation adjustments (the delta between the base forecast and the reconciled forecast) as a diagnostic. Large adjustments are a signal that the two channels are telling different stories and something worth investigating: a Shopify promotion that has not yet propagated to Amazon, an Amazon buy box loss the team has not flagged, or a supply issue that is being masked by inventory imbalance. In one deployment, the reconciliation-delta report caught a mispriced parent-child variation on Amazon three weeks before finance would have seen the revenue drop.
Cross-Cutting: Handling Promotional Distortion
Amazon Prime Day, Shopify BFCM, and internal brand promotions (coupon codes, influencer drops, launch weeks) create demand pulses that do not repeat on a natural calendar cadence. If you feed those pulses into the model without labeling them, the model learns to expect the pulse-level demand as a baseline and over-forecasts every subsequent normal week.
The pattern we use is a two-layer approach. First, promotions are exogenous variables in the model. Prophet takes them as holidays with named windows and prior scales; NeuralProphet takes them as regressors; ETS takes them as level shifts. Second, for events that are known but have no historical analog (a first-ever Prime Day appearance, a new discount tier), we apply a post-hoc adjustment based on comparable events. The adjustment lives in a lookup table maintained by the buyer, and it is bounded (we do not let the buyer scale a forecast by more than 3x without a note explaining why).
The parent code framing:
# adjust/post_hoc.py
def apply_event_adjustment(forecast: pd.Series, events: pd.DataFrame) -> pd.Series:
for _, ev in events.iterrows():
window = (forecast.index >= ev["start"]) & (forecast.index <= ev["end"])
multiplier = min(ev["expected_multiplier"], 3.0)
forecast.loc[window] = forecast.loc[window] * multiplier
return forecastPost-hoc adjustment is the escape hatch, not the plan. Every adjustment gets logged with a reason, and after the event we compute a retrospective error against the adjusted forecast and against the unadjusted forecast, so we know whether the buyer's intuition was worth encoding for next year. This is the same pattern we use for inventory drift reconciliation in a Shopify-to-NetSuite sync: the automated system is the default, the human overrides are logged, and the audit log is what makes the whole thing improvable.
What the Architecture Enabled
- Forecast MAPE on the top 200 SKUs improved 22 percent against the naive channel-independent baseline, measured on rolling 8-week backtests.
- Amazon FBA stockouts on hero SKUs fell from an average of 6 per quarter to 1 per quarter across the first two quarters after rollout.
- Working capital tied up in slow-mover inventory decreased 18 percent as the reconciliation surfaced double-counted safety stock the team had not been aware of.
- Time from Shopify launch of a new SKU to Amazon inbound shipment plan dropped from 6 weeks to 3, because the Prophet-based launch model starts producing usable Amazon forecasts once 14 days of Shopify data are available.
Observations
What worked. Hierarchical reconciliation is the single component that most changed the buyer's trust in the forecast. Before reconciliation, buyers were rewriting numbers in a spreadsheet before placing orders. After, they were placing orders directly against the reconciled numbers, with the delta report as their sanity check. Per-SKU model routing was the second most impactful decision. The 60 percent of SKUs on ETS or Croston run cheaply and accurately, and the compute budget for the neural models on the top 10 percent is a fraction of what a catalog-wide DeepAR deployment would cost.
What didn't. Trying one model class across the whole catalog, either direction. DeepAR on everything was too expensive and underperformed on the tail. Prophet on everything missed the intermittent-demand tail entirely. Ignoring Amazon SP-API rate limits during backfills. Our first backfill exhausted the daily Sales and Traffic Report quota by lunchtime and pushed a training run back 36 hours. We rebuilt the backfill to stage report artifacts to Cloud Storage first and parse them out of band. Choosing Amazon Forecast as the managed backend, only to have AWS close new customer access mid-project; the migration to SageMaker-hosted models cost us a month.
What we'd do differently. Bring lead-time data into the model earlier. We treated lead times as a downstream safety-stock parameter for the first six months and then discovered that the buyers were compensating for lead-time variance inside their forecast overrides. Modeling lead time as a distribution rather than a fixed number, and letting the buying tool consume the full distribution, would have taken most of those overrides out of the process. Model returns as a separate distribution. Amazon returns arrive on a lag and are not evenly distributed across SKUs; treating shipped units minus returned units as a single net-demand signal costs accuracy on categories with high return rates (apparel especially).
The reconciliation-delta report caught a mispriced parent-child variation on Amazon three weeks before finance would have seen the revenue drop.
If you are earlier in the multi-channel stack decision, choosing the right ERP connector before layering a forecasting system on top is worth doing first. A forecasting harness that consumes ambiguous inventory data from a poorly integrated ERP will make confident predictions about a state of the world that is not real.
References
- Rob J. Hyndman and George Athanasopoulos, "Forecasting: Principles and Practice, 3rd edition," Chapter 11 on Forecasting Hierarchical and Grouped Time Series, OTexts, 2021. https://otexts.com/fpp3/hierarchical.html
- Shanika L. Wickramasuriya, George Athanasopoulos, Rob J. Hyndman, "Optimal Forecast Reconciliation for Hierarchical and Grouped Time Series Through Trace Minimization," Journal of the American Statistical Association, 2019. https://robjhyndman.com/papers/mint.pdf
- Amazon Web Services, "Transition your Amazon Forecast usage to Amazon SageMaker Canvas," AWS Machine Learning Blog, 2024. https://aws.amazon.com/blogs/machine-learning/transition-your-amazon-forecast-usage-to-amazon-sagemaker-canvas/
- Amazon Selling Partner API, "Reports API and Report Type Values," Amazon Developer Documentation, 2026. https://developer-docs.amazon.com/sp-api/docs/reports-api
- Shopify, "Admin API GraphQL: Orders and Inventory," Shopify.dev documentation, 2026. https://shopify.dev/docs/api/admin-graphql
- McKinsey and Company, "Harnessing the power of AI in distribution operations," McKinsey Industrials blog, 2026. https://www.mckinsey.com/industries/industrials/our-insights/distribution-blog/harnessing-the-power-of-ai-in-distribution-operations
- David Salinas, Valentin Flunkert, Jan Gasthaus, "DeepAR: Probabilistic Forecasting with Autoregressive Recurrent Networks," International Journal of Forecasting, 2020. https://arxiv.org/abs/1704.04110
Stack: Python 3.11 for feature engineering and modeling (pandas, Prophet, NeuralProphet, statsforecast, hierarchicalforecast), TypeScript for the API layer that serves reconciled forecasts to buying tools, Postgres for the feature store, Cloud Storage for staged SP-API report artifacts, deployed on Google Cloud Run with training jobs on SageMaker, orchestrated with n8n for the nightly refresh and Airflow for the training pipelines.
Want to build something like this?
We design and ship AI products, automation systems, and custom software.
Get in touch