ホーリンラブブックス(HLB)の売上予測 Redash クエリ 2102 の予測値が実績と乖離している問題を調査した。
shortest_shipping_date(予測出荷日)ベースの集計と、MS(マンガサーバー)の実際の出荷実績(delivery_date)を突き合わせ、ずれの原因と改善策を分析した。
結論: Redash SQL自体に不具合はないが、shortest_shipping_date は「理論上の最短出荷日」であり、実際の出荷日とは構造的にずれる。+4日のオフセットを加算すると1月データでは最も実績に近い予測になる。
| 項目 | 内容 |
|---|---|
| リポジトリ | torico-tokyo/torico-blog |
| Redash クエリ | 2102: ホーリン期間予測売上 |
| 対象ショップ | ホーリンラブブックス (site_id=18) |
| 分析対象期間 | 2026年1月 |
| データ | 件数 | 金額 |
|---|---|---|
| Redash 2102 相当 (shortest_shipping_date=1月) | 4,489件 | 15,315,245円 |
| MS 1月出荷実績 (delivery_date=1月) | 3,453件 | 13,210,869円 |
| 差額 | +1,036件 | +2,104,376円 (Redash の方が多い) |
| ステータス | 件数 | 金額 | 説明 |
|---|---|---|---|
| SHIPPED | 1,198件 | 2,995,888円 | shortest_shipping_date=1月だが実際は2月出荷 |
| UNSHIPPED | 74件 | 103,921円 | 未出荷 |
| SHIPPED_PENDING_PAYMENT | 18件 | 132,493円 | 代引き配送中 |
| UNPAID/ORDERED | 4件 | 15,230円 | 未入金/注文完了 |
shortest_shipping_date が12月以前だが、実際には1月に出荷された注文。
A - B = 3,247,532 - 1,143,156 = 2,104,376円 → Redash予測 - MS実績 と一致。
- 共通の 3,195件は 全件金額が一致(torico-blog の
o.totalと MS 側のorder_total_priceが完全一致) - Redash 2102 の SQL 自体に二重計上の問題なし(orderitem JOIN なし、BUNDLED 除外、1 reserve = 1 order)
shortest_shipping_date(配送基準日)は「注文内の全商品のうち、最も遅い発売日/入荷予定日」を基準にした配送予定日。
ソース: reserveshop/cache.py L808-836
shortest_date = max(
max(item.sell_start_date, item.expected_restock_date, 今日)
for item in カート内商品 (同時購入特典は除外)
)- 各商品の
sell_start_date(発売日)、expected_restock_date(入荷予定日)、当日の最大値を取る - 全商品の中から最大を取る(= 一番遅い商品に合わせる)
- カットオフ時刻(深夜2時)考慮:当日の2時以降なら翌日にずらす
ソース: reserveshop/models.py L375-401 (_max_shortest_shipping_date)
管理コマンド: reserveshop/management/commands/t10_update_shortest_shipping_date.py
呼び出し元: reserveshop/update_reserve.py L222-309 (update_shortest_shipping_date)
def _max_shortest_shipping_date():
dates = [order_date] # 注文日
for item in order.items:
# 現在の商品マスタの sell_start_date
# OrderItem.extra_data_json の sell_start_date(購入時の発売日)
# OrderItem.extra_data_json の expected_restock_date(入荷予定日)
dates.append(各日付)
return max(dates).date()商品マスタの最新の sell_start_date を参照して更新する。発売日が延期されたら shortest_shipping_date も後ろにずれる。
対象ステータス: UNPAID, PAID, ORDERED のみ(UNSHIPPED 以降は更新対象外)。
ソース: reserveshop/update_reserve.py L425-617 (UpdateReserveForBackorder)
MS側で在庫引き当てが完了したら expected_restock_date をクリアし、post_sell_start_date_save で再計算。
| タイミング | トリガー | ロジック |
|---|---|---|
| 注文時 | チェックアウト | cart.shortest_shipping_date — 全商品の発売日/入荷予定日の最大 |
| 発売日変更時 | バッチ t10_update_shortest_shipping_date |
_max_shortest_shipping_date() — 商品マスタの最新発売日で再計算 |
| 取寄入荷時 | バッチ UpdateReserveForBackorder |
入荷予定日クリア後 post_sell_start_date_save で再計算 |
共通 3,195件について delivery_date - shortest_shipping_date を計算した。
| 指標 | 値 |
|---|---|
| 最小 | -1日 |
| 最大 | 27日 |
| 平均 | 3.25日 |
| 中央値 | 0日 |
| 金額加重平均 | 2.36日 |
-1日: 82件 ( 2.6%) 金額: 333,937円
+0日: 1777件 ( 55.6%) 金額: 6,803,602円 ← 過半数は当日出荷
+1日: 374件 ( 11.7%) 金額: 1,562,033円
+2日: 137件 ( 4.3%) 金額: 579,080円
+3日: 91件 ( 2.8%) 金額: 454,391円
+4日: 70件 ( 2.2%) 金額: 334,369円
+5日: 30件 ( 0.9%) 金額: 195,587円
+6日: 50件 ( 1.6%) 金額: 285,976円
+7日: 34件 ( 1.1%) 金額: 202,861円
+8日: 16件 ( 0.5%) 金額: 171,478円
+9日: 36件 ( 1.1%) 金額: 85,152円
+10日: 6件 ( 0.2%) 金額: 36,981円
+11日: 51件 ( 1.6%) 金額: 77,917円
+12日: 7件 ( 0.2%) 金額: 58,883円
+13日: 90件 ( 2.8%) 金額: 182,057円
+14日: 6件 ( 0.2%) 金額: 28,482円
+15日: 3件 ( 0.1%) 金額: 49,811円
+16日: 54件 ( 1.7%) 金額: 97,332円
+17日: 57件 ( 1.8%) 金額: 91,361円
+18日: 71件 ( 2.2%) 金額: 128,038円
+19日: 97件 ( 3.0%) 金額: 175,336円
+20日: 38件 ( 1.2%) 金額: 55,182円
+21日: 6件 ( 0.2%) 金額: 19,351円
+22日: 2件 ( 0.1%) 金額: 7,994円
+23日: 4件 ( 0.1%) 金額: 24,383円
+25日: 4件 ( 0.1%) 金額: 13,067円
+26日: 1件 ( 0.0%) 金額: 3,194円
+27日: 1件 ( 0.0%) 金額: 9,878円
55.6% は当日出荷(ずれなし)。残り44.4%は1日以上のずれがあり、分布はロングテール。
shortest_shipping_date + N日 を「予測出荷日」として1月に入る件数・金額を計算。
実績(3,453件 / 13,210,869円)にどれだけ近いかを比較。
-5日: 4,203件 / 14,158,902円 (差額: +948,033円)
-4日: 4,412件 / 14,941,784円 (差額: +1,730,915円)
-3日: 4,430件 / 15,027,578円 (差額: +1,816,709円)
-2日: 4,453件 / 15,144,221円 (差額: +1,933,352円)
-1日: 4,463件 / 15,174,768円 (差額: +1,963,899円)
+0日: 4,489件 / 15,315,245円 (差額: +2,104,376円) ← 現状
+1日: 4,387件 / 15,085,863円 (差額: +1,874,994円)
+2日: 4,213件 / 13,930,761円 (差額: +719,892円)
+3日: 4,102件 / 13,392,462円 (差額: +181,593円)
+4日: 4,073件 / 13,293,948円 (差額: +83,079円) ← 最小差額 ★
+5日: 4,035件 / 13,119,482円 (差額: -91,387円)
+6日: 3,913件 / 12,895,090円 (差額: -315,779円)
+7日: 3,884件 / 12,782,015円 (差額: -428,854円)
+8日: 3,852件 / 12,645,951円 (差額: -564,918円)
shortest_shipping_date + 4日 で金額差が最小(+83,079円、誤差0.6%)になる。
今回の1月分析では Redash の方が 多い(+2,104,376円)。
推定される理由:
- 「Redashが少ない」は 2月予測に対する指摘 — 12月・1月の
shortest_shipping_dateの注文が2月に実際出荷された分は、Redash の2月予測には含まれないため、2月は逆に Redash が少なくなる - 比較対象の期間・条件が異なる可能性
reserveshop_reserveをtorico_shop_order,charge_chargeと LEFT JOINorderitemは JOIN していない → 二重計上の問題なし- BUNDLED ステータスは除外していない(ステータスフィルタに BUNDLED が含まれていない = 実質除外)
- 1 reserve = 1 order の関係
調査用に torico_shop_orderitem を JOIN した SQL は、1注文に複数商品がある場合に行が重複する。
正しい合計(reserve 単位): 15,315,245円 vs 全行合計(orderitem 重複含む): 62,785,415円。
集計時は必ず order_id で重複排除が必要。
- 同梱元 (source) の reserve → status が
BUNDLEDになる - 同梱先 (destination) として新しい Reserve/Order/Charge が作成される(
checkout_type = 'reserveshop-bundle') - Redash 2102 では BUNDLED (同梱元) が対象外、同梱先の合算注文が集計に入る → 正しい
shipping_ec_analysisテーブルに出荷日ベースの EC サイト別売上集計がある- 集計元:
orderテーブルとdeliveryテーブル (delivery_status = 5: 配送済み確認) total_due_sum = balance_due + delivery_charge + commission- EC サイト ID: 74 (HLB旧), 111 (HLB新)
Redash 2102 の WHERE 条件を変更:
-- 現状
AND r.shortest_shipping_date >= '{{ delivery_date_start }}'
AND r.shortest_shipping_date <= '{{ delivery_date_end }}'
-- 改善案
AND DATE_ADD(r.shortest_shipping_date, INTERVAL 4 DAY) >= '{{ delivery_date_start }}'
AND DATE_ADD(r.shortest_shipping_date, INTERVAL 4 DAY) <= '{{ delivery_date_end }}'1月データでは誤差0.6%に改善される。ただし、この値が他の月でも安定するかは未検証。
SHIPPED で shortest_shipping_date が期間内だが実際は翌月出荷のケース(1,198件)が最大の要因。 SHIPPED を除外し「まだ出荷されていない注文」のみ集計する案もあるが、予測の趣旨が変わる。
予測ではなく MS の delivery_date ベースで実績集計する場合は、shipping_ec_analysis テーブルを直接参照する方が正確。
SELECT
COUNT(*) AS cnt,
SUM(o.total) AS total_amount
FROM reserveshop_reserve r
LEFT JOIN torico_shop_order o ON o.id = r.order_id
WHERE r.site_id = 18
AND r.shortest_shipping_date >= '2026-01-01'
AND r.shortest_shipping_date <= '2026-01-31'
AND r.status IN ('UNPAID','PAID','ORDERED','UNSHIPPED','SHIPPED_PENDING_PAYMENT','SHIPPED');SELECT
o.order_id,
o.m_ec_id,
d.delivery_date,
IFNULL(o.balance_due, 0) + IFNULL(o.delivery_charge, 0) + IFNULL(o.commission, 0) AS total_due,
o.order_date,
o.order_status,
o.money_status
FROM `order` o
INNER JOIN delivery d ON d.order_id = o.order_id
WHERE o.m_ec_id IN (74, 111)
AND d.delivery_status = 5
AND d.delivery_date >= '2026-01-01'
AND d.delivery_date < '2026-02-01'
ORDER BY d.delivery_date, o.order_id;調査日: 2026-02-25 リポジトリ: torico-tokyo/torico-blog (commit: f6221f7)