はじめに:欠品と過剰在庫のジレンマ
ECの在庫管理は、いつも2つの失敗のあいだで揺れています。仕入れを絞れば欠品して機会損失が出る。多めに仕入れれば売れ残りが資金を縛り、最後は値引きやデッドストックになる。どちらに振れても痛い、という板挟みです。
この「ちょうどいい量」を勘で当て続けるのは、商品点数が増えるほど難しくなります。人気商品とニッチ商品で売れ方はまったく違いますし、季節やセールでも需要は動きます。担当者の頭のなかにある暗黙知を、いつまでも頼りにするわけにもいきません。
そこで本記事では、BigQuery ML の需要予測を使って、商品別に「これから何個売れそうか」を見積もり、そこから安全在庫と発注点を算出するまでの流れを整理します。Python もインフラも用意せず、すでに BigQuery に入っている受注データと SQL だけで進められるのが利点です。
なお、本記事に登場する数値はすべて構文や考え方を示すための例です。実際の予測精度や最適な在庫水準は、商材の特性やデータの量・質によって大きく変わります。まずは自分のデータで動かして確かめてください。
商品別の需要を予測する
需要予測の中心になるのが、BigQuery ML の時系列モデル ARIMA_PLUS です。トレンドや季節性、祝日効果を自動で考慮してくれるので、SQL を数本書くだけで予測が出せます。
ARIMA_PLUS そのものの基礎、つまり学習データの作り方やパラメータの意味、定期再学習の自動化については BigQuery ML × 時系列モデルARIMA_PLUSでEC売上の週次予測を自動化する で詳しく解説しています。本記事はその応用編にあたり、予測の対象を「売上金額」ではなく「商品別の販売数量」に置き換え、そこから安全在庫・発注点という発注判断の数字まで踏み込む点が違いです。
学習データを商品×日付で用意する
売上金額ではなく、在庫管理に直結する販売数量を予測対象にします。受注明細テーブルから、商品ごと・日付ごとの販売数を集計します。
-- 商品別・日次の販売数量を集計する
SELECT
DATE(ordered_at) AS sales_date,
product_id,
SUM(quantity) AS sold_qty
FROM `myproject.raw.order_items`
WHERE status = 'paid' -- キャンセル・未決済は除外
GROUP BY sales_date, product_id
ORDER BY product_id, sales_date;
ポイントは、商品IDを列として残しておくことです。これを time_series_id_col に渡すと、1本の CREATE MODEL で商品ごとの予測モデルをまとめて学習できます。
商品ごとにまとめてモデルを学習する
CREATE OR REPLACE MODEL `myproject.mart.demand_forecast_model`
OPTIONS(
model_type = 'ARIMA_PLUS',
time_series_timestamp_col = 'sales_date', -- 時刻の列
time_series_data_col = 'sold_qty', -- 予測したい値(販売数量)
time_series_id_col = 'product_id', -- 商品ごとに別系列として学習
holiday_region = 'JP', -- 日本の祝日を考慮
auto_arima = TRUE -- パラメータを自動探索
) AS
SELECT
DATE(ordered_at) AS sales_date,
product_id,
SUM(quantity) AS sold_qty
FROM `myproject.raw.order_items`
WHERE status = 'paid'
GROUP BY sales_date, product_id;
time_series_id_col を指定すると、商品ごとに個別のトレンドや季節性を学習してくれます。人気商品とニッチ商品をまとめて1つのモデルに押し込まず、それぞれの売れ方に合わせた予測が出せるわけです。
予測を取り出す
学習したモデルから将来の販売数量を取り出すには ML.FORECAST を使います。horizon で何日先まで予測するかを指定します。発注リードタイムを踏まえ、ここでは2週間先までを例にします。
SELECT
product_id,
forecast_timestamp, -- 予測対象日
forecast_value, -- 予測販売数量(中央値)
prediction_interval_lower_bound, -- 予測区間の下限
prediction_interval_upper_bound -- 予測区間の上限
FROM
ML.FORECAST(
MODEL `myproject.mart.demand_forecast_model`,
STRUCT(
14 AS horizon, -- 14日先まで予測
0.8 AS confidence_level -- 予測区間の信頼水準(80%)
)
)
ORDER BY product_id, forecast_timestamp;
forecast_value が予測の中心値、*_bound の2列が「だいたいこの範囲に収まりそう」という幅です。在庫の世界では、この幅(ばらつき)こそが安全在庫の根拠になります。1つの数字だけでなく、上下の幅もセットで見るのが大切です。
予測から安全在庫・発注点を出す
予測販売数量がそろったら、ここからが在庫最適化の本番です。需要予測は「在庫をいくつ持つべきか」を直接は教えてくれません。予測値に、欠品をどこまで許すかという経営判断を組み合わせて、はじめて発注の数字になります。
3つの数字の関係を整理する
在庫の発注を考えるとき、押さえておきたい数字は3つです。
- リードタイム需要:発注してから商品が届くまでの期間に売れる見込み数。予測販売数量をリードタイム日数ぶん足し合わせたものです。
- 安全在庫:需要のばらつきや予測のズレに備える「保険」のぶんの在庫。
- 発注点(リオーダーポイント):在庫がこの数まで減ったら発注する、という基準。「リードタイム需要 + 安全在庫」で求めます。
考え方はシンプルです。商品が届くまでに売れるぶん(リードタイム需要)を切らさず、さらに予想外の売れ行きにも耐えられる余裕(安全在庫)を持っておき、その合計を下回ったら発注する、というだけです。
安全在庫を予測区間から見積もる
安全在庫は、本来は需要のばらつき(標準偏差)から計算しますが、ARIMA_PLUS の予測区間を使うと近似的に見積もれます。予測区間の上限と中央値の差は、需要が上振れしたときの「のりしろ」に相当するからです。リードタイム期間ぶんのこののりしろを足し合わせれば、簡易的な安全在庫として使えます。
-- リードタイム7日を想定し、発注点を算出する例
WITH forecast AS (
SELECT
product_id,
DATE(forecast_timestamp) AS forecast_date,
forecast_value AS qty_mid,
prediction_interval_upper_bound AS qty_upper
FROM
ML.FORECAST(
MODEL `myproject.mart.demand_forecast_model`,
STRUCT(14 AS horizon, 0.8 AS confidence_level)
)
),
lead_window AS (
-- 直近からリードタイム(7日)ぶんの予測だけを対象にする
SELECT *
FROM forecast
WHERE forecast_date < DATE_ADD(CURRENT_DATE(), INTERVAL 7 DAY)
)
SELECT
product_id,
ROUND(SUM(qty_mid)) AS lead_time_demand, -- リードタイム需要
ROUND(SUM(qty_upper - qty_mid)) AS safety_stock, -- 安全在庫(簡易)
ROUND(SUM(qty_upper)) AS reorder_point -- 発注点
FROM lead_window
GROUP BY product_id
ORDER BY reorder_point DESC;
この例では「リードタイム需要 + 安全在庫」が、予測区間の上限を足し合わせた値(reorder_point)に一致します。つまり「上振れしても届くまで持ちこたえられる量」を発注点に置く、という保守的な設計です。欠品をどこまで嫌うかに応じて、confidence_level を上げ下げして調整します。
現在在庫と突き合わせて発注リストにする
発注点が出たら、最後は手元の在庫数と突き合わせます。在庫が発注点を下回っている商品が、いま発注すべき商品です。
SELECT
r.product_id,
s.stock_qty, -- 現在在庫
r.reorder_point, -- 発注点
GREATEST(r.reorder_point - s.stock_qty, 0) AS suggested_order_qty -- 発注提案数
FROM `myproject.mart.reorder_points` AS r
JOIN `myproject.raw.stock` AS s
USING (product_id)
WHERE s.stock_qty < r.reorder_point -- 在庫が発注点を割った商品だけ
ORDER BY suggested_order_qty DESC;
これで「どの商品を、おおよそ何個発注すべきか」の一覧ができます。あとはこの結果をスケジュールクエリで定期的に更新すれば、毎朝の発注判断の下敷きとして使えます。
なお、売れずに在庫だけが積み上がっている死に筋の側を見つける視点も欠かせません。発注を絞るべき商品の特定については ECの在庫回転率をGA4×BigQueryで商品別に可視化して死に筋を特定する で扱っています。需要予測で「増やす」側と、回転率分析で「減らす」側、両方そろってはじめて在庫は最適化に近づきます。
⚠️ 予測の限界と構文の確認について
便利な需要予測ですが、過信は禁物です。いくつか現場で踏みやすい落とし穴を挙げておきます。
ARIMA_PLUS は過去のパターンの延長線を引くモデルなので、過去に例のない出来事は予測できません。新商品の発売、初開催の大型セール、SNSでの突発的なバズなどは、予測区間を平気で飛び越えます。こうしたイベントが分かっている期間は、予測値を機械的に信じず、人の判断で補正してください。
販売実績が極端に少ない商品も苦手です。週に数個しか売れないニッチ商品は、そもそも統計的なパターンを読み取りづらく、予測が不安定になりがちです。こうした商品は予測に頼りきらず、定期発注や最低在庫の固定運用に切り替えるほうが現実的なこともあります。
学習データに混ざった異常値にも注意します。システム障害で1日だけ受注ゼロ、といった日が残っていると、トレンドの推定が歪みます。明らかに異常な日は、学習用クエリの WHERE で除外しておくのが安全です。
最後に、本記事の ML.FORECAST や OPTIONS の引数は執筆時点のものです。BigQuery ML は機能追加や仕様変更が比較的多いため、実装の前にかならず最新の公式ドキュメントで構文と引数を確認してください。
まとめ
BigQuery ML の需要予測を使えば、欠品と過剰在庫のジレンマに、勘ではなくデータの根拠を持ち込めます。本記事の流れを整理すると次のとおりです。
- 商品別の需要予測 — 販売数量を商品×日付で集計し、
time_series_id_colでまとめて学習する - 発注点の算出 — 予測値からリードタイム需要を、予測区間から安全在庫を見積もる
- 発注リスト化 — 発注点と現在在庫を突き合わせ、発注すべき商品を抽出する
大事なのは、予測値を「絶対の正解」ではなく「判断の補助線」として使うことです。予測区間の幅を安全在庫の根拠にしつつ、機械が知らないイベントは人が補う。この組み合わせで、これまで勘に頼っていた仕入れ量に、確かな根拠が加わります。
まずは売れ筋の数商品だけでも、2週間先の需要を予測して発注点を出すところから始めてみてください。
商品別の需要予測から発注点算出までを自社データで組み込みたい、あるいは在庫データと受注データをまたいだBigQuery基盤そのものを整えたいという場合は、BigQueryデータ基盤構築サービスで対応しています。まずは現状を話してみたいという方は、お問い合わせからご連絡ください。