「弾薬が切れた軍は戦えない」
軍事ロジスティクスにおいて、弾薬・燃料・食料の補給は「作戦の前提条件」だ。
どれだけ優秀な将兵がいても、弾薬が切れた瞬間に戦闘能力はゼロになる。
パランティアがペンタゴン(米国防総省)に提供しているシステムの一つが「予測的メンテナンス・補給」だ。5年・8500万ドルの契約で「機械学習を使って防衛機器のメンテナンス問題を予測し、補給の問題点を解決する」システムを構築している。
戦車や航空機が「故障する前に」部品を交換し、「枯渇する前に」弾薬を補充する。
これはそのまま通販の在庫管理に翻訳できる。
「在庫が切れた通販は売れない」
機会損失は即座に競合へのシェア移転を意味する。
「反応的補充」から「予測的補充」へ
多くの通販企業が実践しているのは「反応的補充」だ。
在庫が安全在庫を下回ったら発注する。売り切れたら緊急発注する。「起きたことへの反応」だ。
しかし軍事ロジスティクスでは「反応的補充」は最悪手だ。
弾薬が切れてから補充を要請しても、戦闘が終わるまでに届かない。
「予測的補充」とは「2週間後に何が何個必要になるか」を今から計算し、発注リードタイムを考慮した上で「今日発注すべき商品と数量」を自動で算出することだ。
-- 予測的補充エンジン:今日発注すべき商品と数量を算出する
WITH sales_velocity AS (
-- 過去30日の商品別日次平均販売数(販売速度)を計算
SELECT
product_id,
product_name,
SUM(quantity_sold) AS total_sold_30d,
COUNT(DISTINCT order_date) AS selling_days,
-- 1日あたりの平均販売数(0除算防止)
ROUND(SUM(quantity_sold) / NULLIF(COUNT(DISTINCT order_date), 0), 2)
AS daily_avg_units,
-- 直近7日の販売速度(トレンド検知)
SUM(CASE WHEN order_date >= DATE_ADD('day', -7, CURRENT_DATE)
THEN quantity_sold ELSE 0 END) / 7.0 AS daily_avg_last_7d
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
WHERE
o.status = 'completed'
AND o.order_date >= DATE_ADD('day', -30, CURRENT_DATE)
GROUP BY product_id, product_name
),
inventory_status AS (
-- 現在の在庫状況
SELECT
product_id,
current_stock,
reorder_lead_days, -- 発注から入荷までのリードタイム(日)
safety_stock_days -- 安全在庫として確保したい日数
FROM inventory_master
),
reorder_calculation AS (
SELECT
sv.product_id,
sv.product_name,
sv.daily_avg_units,
sv.daily_avg_last_7d,
inv.current_stock,
inv.reorder_lead_days,
inv.safety_stock_days,
-- リードタイム中の予測消費量
ROUND(sv.daily_avg_units * inv.reorder_lead_days, 0)
AS consumption_during_lead,
-- 安全在庫量
ROUND(sv.daily_avg_units * inv.safety_stock_days, 0)
AS safety_stock_units,
-- 発注点(この在庫を下回ったら発注すべき水準)
ROUND(sv.daily_avg_units * (inv.reorder_lead_days + inv.safety_stock_days), 0)
AS reorder_point,
-- 現在の在庫で何日分あるか
ROUND(inv.current_stock / NULLIF(sv.daily_avg_units, 0), 1)
AS days_of_stock_remaining,
-- トレンド(直近7日が30日平均より増えているか)
ROUND(sv.daily_avg_last_7d / NULLIF(sv.daily_avg_units, 0), 2)
AS trend_multiplier
FROM sales_velocity sv
JOIN inventory_status inv USING (product_id)
)
SELECT
product_id,
product_name,
current_stock,
days_of_stock_remaining,
reorder_point,
daily_avg_units,
trend_multiplier,
-- 今日発注すべきかどうかの判定
CASE
WHEN current_stock <= reorder_point THEN '🔴 要発注(在庫が発注点を下回っています)'
WHEN current_stock <= reorder_point * 1.2 THEN '🟡 発注準備(まもなく発注点に到達)'
ELSE '🟢 正常'
END AS reorder_status,
-- 推奨発注量(30日分を目標に補充)
CASE
WHEN current_stock <= reorder_point
THEN GREATEST(
ROUND(daily_avg_units * 30 * trend_multiplier - current_stock, 0),
0
)
ELSE 0
END AS recommended_order_qty
FROM reorder_calculation
ORDER BY
CASE WHEN current_stock <= reorder_point THEN 0 ELSE 1 END,
days_of_stock_remaining ASC「トレンドの先読み」—季節性と外部シグナルの統合
軍事ロジスティクスのアナリストは「天候・地形・敵の動向」という外部要因を補給計画に織り込む。
通販の補給計画にも「外部シグナル」が必要だ。
-- 季節性補正を加えた需要予測
-- 前年同期の販売データで季節性インデックスを計算する
WITH yoy_comparison AS (
SELECT
product_id,
-- 今年の過去30日の日次平均
SUM(CASE
WHEN order_date BETWEEN DATE_ADD('day', -30, CURRENT_DATE)
AND CURRENT_DATE
THEN quantity_sold ELSE 0 END
) / 30.0 AS current_daily_avg,
-- 前年同期の日次平均(±15日の範囲)
SUM(CASE
WHEN order_date BETWEEN DATE_ADD('day', -30, DATE_ADD('year', -1, CURRENT_DATE))
AND DATE_ADD('year', -1, CURRENT_DATE)
THEN quantity_sold ELSE 0 END
) / 30.0 AS last_year_daily_avg,
-- 前年の今後30日(来月相当)の日次平均
SUM(CASE
WHEN order_date BETWEEN DATE_ADD('year', -1, CURRENT_DATE)
AND DATE_ADD('day', 30, DATE_ADD('year', -1, CURRENT_DATE))
THEN quantity_sold ELSE 0 END
) / 30.0 AS last_year_next_month_avg
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
WHERE
o.status = 'completed'
AND o.order_date >= DATE_ADD('year', -1, DATE_ADD('day', -30, CURRENT_DATE))
GROUP BY product_id
)
SELECT
product_id,
ROUND(current_daily_avg, 2) AS current_daily_avg,
ROUND(last_year_next_month_avg, 2) AS last_year_next_month_avg,
-- 来月の需要予測(前年の来月実績 × 今年の成長率)
ROUND(
last_year_next_month_avg
* (current_daily_avg / NULLIF(last_year_daily_avg, 0))
, 2) AS forecast_next_month_daily,
-- 30日後の予測在庫(現在の計画発注を含まない素の推定)
-- ※ inventory_master.current_stock と結合して使う
ROUND(
last_year_next_month_avg
* (current_daily_avg / NULLIF(last_year_daily_avg, 0))
* 30
, 0) AS forecast_30day_consumption
FROM yoy_comparison
WHERE last_year_daily_avg > 0
ORDER BY forecast_30day_consumption DESC「機会損失」を可視化する—欠品コストの算出
軍事では「撃てない弾薬を持っていることのコスト(重量・輸送費)」と「弾薬が足りないことのコスト(作戦の失敗)」を常に天秤にかける。
過剰在庫も過少在庫も、どちらもコストだ。
通販でも同じだ。
-- 機会損失の推定:欠品していた期間の潜在的な売上損失を計算する
WITH stockout_periods AS (
-- 在庫ゼロだった期間を特定(在庫履歴テーブルから)
SELECT
product_id,
stockout_start_date,
stockout_end_date,
DATE_DIFF('day', stockout_start_date, stockout_end_date) AS stockout_days
FROM inventory_history
WHERE stock_quantity = 0
AND stockout_start_date >= DATE_ADD('day', -90, CURRENT_DATE)
),
avg_daily_revenue AS (
-- 欠品がなかった期間の1日あたり平均売上
SELECT
oi.product_id,
ROUND(SUM(oi.quantity_sold * oi.unit_price)
/ COUNT(DISTINCT o.order_date), 0) AS avg_daily_revenue
FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
WHERE o.status = 'completed'
AND o.order_date >= DATE_ADD('day', -90, CURRENT_DATE)
GROUP BY oi.product_id
)
SELECT
sp.product_id,
sp.stockout_start_date,
sp.stockout_end_date,
sp.stockout_days,
adr.avg_daily_revenue,
-- 機会損失の推定(欠品日数 × 通常日次売上)
ROUND(sp.stockout_days * adr.avg_daily_revenue, 0)
AS estimated_lost_revenue,
-- 年換算の機会損失
ROUND(
sp.stockout_days * adr.avg_daily_revenue * (365.0 / 90)
, 0) AS annualized_lost_revenue
FROM stockout_periods sp
JOIN avg_daily_revenue adr USING (product_id)
ORDER BY estimated_lost_revenue DESCこのSQLが示す数字が「予測的補充システムへの投資対効果」の根拠になる。
「去年の欠品による機会損失が年間2,000万円だった」という数字があれば、補充システムへの投資予算を正当化できる。
次回予告
パランティアの技術的核心「オントロジー」を、Treasure Dataで実装するとはどういうことか。バラバラなデータに「意味」を与え、行動可能なインテリジェンスに変換する設計哲学を解説する。