RetailBrain 要件定義
1. 目的
消滅期限のある在庫(惣菜・施術枠)について「今いくらで売るべきか」を、限界利益と機会費用から算出し、 現場で実際に行われている一律ルールとの利益差を比較する単一ページのシミュレータ。
目的は事業運用ではなくポートフォリオである。 採用担当者が1つのURLを開き、90秒で「小売の利益構造を理解した上で作られたシステムだ」と判断できる状態を作る。
| 順位 | 満たすこと |
|---|---|
| 1 | ロジックの筋が通っていること(会計・意思決定理論として反論に耐える) |
| 2 | 動いて触れること(ブラウザで即座に結果が変わる) |
| 3 | 前提と限界を自分で明示していること |
2. 中核概念 — 消滅期限付き在庫
物販とサービスは一見別物だが、「売れ残りの価値がゼロ」という一点で同じ型に統合できる。 したがって限界利益がプラスである限り、売らないより売るほうが合理的である。 この一点で業種を横断できることが、本プロダクトの主張である。
| STOCK(在庫型) | CAPACITY(キャパシティ型) | |
|---|---|---|
| 例 | 惣菜・弁当・刺身 | 施術60分枠・座席・レッスン枠 |
| 消滅の仕方 | 賞味期限・閉店で廃棄 | 時刻が過ぎれば枠が消滅 |
variableCost の発生 | 発注時に initialQty 分が既発生=サンクコスト。売れても売れなくても支出済み | 売れたときだけ発生(施術者の人件費など) |
disposalCost | 売れ残ると追加発生 | 常に 0(枠は消えるだけ) |
| 限界利益率 | 低〜中 | 非常に高い(=値引き余地が構造的に大きい) |
3. 会計ロジック仕様
会計計算は予測ではなく恒等式である。したがって合成データ上でも一切の疑義が生じない。 本システムで最初に、かつ最も厚く作る層はここになる。
3-1. 用語規約
| 用語 | 定義 | 備考 |
|---|---|---|
| 限界利益 | 価格 − 変動費 | 本システムで使う唯一の利益概念 |
| 限界利益率 | 限界利益 ÷ 価格 | コード・UI・文書では contributionMarginRatio のみ |
| 粗利率 | — | 使用禁止。売上原価の範囲が業種で変わり比較不能 |
売上原価ベースの粗利率と変動費ベースの限界利益率は、小売では近い値になるがサービス業では 人件費が売上原価に入るかどうかで結果が反転しうる。用語を混ぜた時点で実装は必ず間違える。
3-2. 動的下限価格
floorPrice = incurredIfSold + opportunityCost - disposalCostAvoided
incurredIfSold = (kind === "CAPACITY" ? variableCost : 0) + incrementalSellingCost
disposalCostAvoided = (kind === "STOCK" ? disposalCost : 0)
opportunityCost = この1個を今売らずに残した場合の期待限界利益
floorPrice = incrementalSellingCost − disposalCost + opportunityCost としていた。
これは variableCost = 仕入原価 = サンクコスト を暗黙に仮定した STOCK 専用の式である。
本システムの型定義は variableCost に施術者人件費を含めるため、CAPACITY では加算しないと下限が過小になり、
限界利益が赤になる価格を推奨してしまう。上の式は STOCK では従来式と完全に一致する。
この式から2つの帰結が出る。両方を画面で説明する。
- STOCK で廃棄費用が正なら、下限価格は理論上マイナスになりうる。 処理費を払って捨てるより 0円で配るほうが得な領域が実在する。これが本システムで最も驚かれる結論であり、隠さず目立たせる
- 第2項の機会費用が「時点間カニバリ」の正体。 値引きが早すぎると、定価で買ったはずの客に安く売ることになる。これが下限価格が時間の関数である理由であり、 本問題が逐次意思決定=動的計画問題になる理由でもある
fixedCostAlloc(固定費配賦)は表示専用とし、値引き判断のロジックに入れない。
入れた瞬間に全部原価思考へ戻る。
3-3. 値引き耐性 — 中心となる問い
requiredVolumeLift = 値引額 ÷ (元の単位限界利益 − 値引額)
例: 限界利益率40%の商品を10%引き
10 ÷ (40 − 10) = 0.33 → 33%の販売増がなければ利益は減る
分母が 0 以下なら回収不能なので null を返す(0除算や Infinity を返さない)。
現場担当者はこの数字を知らずに値引きしている。「その値引き、33%売れないと損です」と即座に出せることが、
本プロダクトの最も分かりやすい価値である。
variableCost で算出する。これは「この商売として値引きが割に合うか」を問う指標であり、
「今夜この1個をどうするか」を問う下限価格とは判断の時点が違う。画面上で同じ枠に並べて混同させてはならない。
3-4. 実装する関数
| 関数 | 定義 | 備考 |
|---|---|---|
unitContribution | price − variableCost | 全判断の基礎。負の値もクリップしない |
contributionMarginRatio | unitContribution ÷ price | price ≦ 0 なら null |
requiredVolumeLift | d ÷ (baseContribution − d) | 分母 ≦ 0 なら null |
dynamicFloor | 3-2 の式 | kind で分岐 |
breakEvenUnits | fixedCost ÷ unitContribution | 単位限界利益 ≦ 0 なら null |
couponCap | 増分限界利益 > クーポン費用 となる上限額 | Day 10 以降。型のみ先行 |
aggregate | スロット結果の集計 | 下記の規定に従う |
netContribution = revenue - variableCost - incrementalSellingCost - disposalCost
variableCost : STOCK → unit.variableCost × initialQty(ポリシー間で定数)
CAPACITY → sold 比例
4. 需要モデル
営業日は 10:00〜22:00 を30分刻みにした24スロット。商材は slot 0 から自分の expiryAt の直前スロットまで販売でき、expiryAt 到達時点の残数が廃棄になる。
q(p, t) = arrivalCurve[t] × baseQty × (p / listPrice) ^ eps(t)
eps(t) = elasticity × (1 + elasticityTimeAmp × (1 − remainingRatio))
remainingRatio = 残スロット数 ÷ その商材の全販売可能スロット数
実現需要は平均 q のポアソン乱数(シード固定)。sold = min(demand, remaining)。
4-1. パラメータ台帳と出典
| パラメータ | 初期値 | 可変域 | 根拠 |
|---|---|---|---|
elasticity(STOCK) |
−2.60 | −0.5 〜 −4.0 | Bijmolt, van Heerde & Pieters (2005), Journal of Marketing Research 42(2), 141–156。 81研究・1,851推定のメタ分析、平均価格弾力性 −2.62。ブランド/SKUレベルの推定であり、単品の値引き判断と階層が一致する |
elasticity(CAPACITY) |
−2.00仮値・出典なし | −0.5 〜 −4.0 | サービス業単体の信頼できる文献値は発見できず。外食カテゴリ値とブランドレベル値の中間として設定。画面に常時明示する |
elasticityTimeAmp(λ) |
0.50仮値 | 0.0 〜 2.0 | 残時間が短いほど価格反応が鋭くなるという業務観察に基づく仮定。文献的裏付けなし。λ=0 で時間非依存モデルに退化するため、感度分析で影響度を明示できる |
4-2. 既知の限界
- −2.62 はブランドレベル、かつ主に食料品・日用消費財の数値。サービス業や専門サービスにそのまま適用できない。 従うべき法則ではなく、更新すべき事前分布として扱う
- Bijmolt らは、価格の内生性を考慮すると弾力性の絶対値が大きくなる方向に強い影響があると報告している。素朴な推定は過小評価側に振れる
- 本システムは弾力性を推定していない。外部から与える。よって推定精度は主張の対象外である
baseQtyは「定価のままなら初期在庫の約75%が売れる」水準に自動調整している。これは値引き判断に意味が出るように置いた設計値であり、実測値ではない
5. 3ポリシーと評価
| ID | 内容 | 役割 |
|---|---|---|
NO_ACTION | 定価のまま。値引きしない | 下限ベースライン |
FLAT_RULE | 消滅120分前に一律30%引き | 本命の比較対象。現場で実際に行われているルール |
ENGINE | 各スロットで動的下限と機会費用を計算し、期待残存限界利益を最大化する値引率を選択 | 提案手法 |
ENGINE は後ろ向き帰納法(動的計画法)で解く。状態は (slot, remaining)、遷移はポアソン需要を残数で打ち切ったもの。
opportunityCost(t, r) = V(t+1, r) − V(t+1, r−1)(1個余分に持つことの限界価値=シャドープライス)。
3ポリシーは必ず同一シードの同一乱数系列で回す。別々に引くと、ポリシーの比較が乱数の比較になる。
5-1. 評価指標
| 指標 | 用途 |
|---|---|
| 純限界利益(円) | 主指標。対 FLAT_RULE の利益差のみを主張する |
| 廃棄数・販売数・平均実現価格・定価販売比率 | 副指標 |
| MAE 等の予測精度 | 使用しない |
本システムは意思決定エンジンであり、予測精度が上がっても利益が下がることは普通に起きる。 発注・値引きは欠品コストと過剰コストが非対称な newsvendor 構造であり、最適解は期待値ではなく分位点になる。 点予測の誤差最小化は目的関数として構造的に誤りである。
5-2. 主張してよい範囲
| 内容 | |
|---|---|
| 主張してよい | ポリシー間の相対比較、およびその比較が成立する条件範囲 |
| 主張してはいけない | 絶対的な効果量(「廃棄ロス◯%削減」等)、需要予測の精度 |
6. 感度分析 — 本システムの目玉
「自分が決めた弾力性で、自分のエンジンが勝った」では説得力がない。 弾力性・来店強度・廃棄費用をスライダーで可変にし、勝つ領域と負ける領域の境界を提示する。 検証不能という弱点を、感度分析という機能に転換する。
| モード | 内容 | 示せること |
|---|---|---|
ORACLE | ENGINE が真の弾力性を知っている前提 | 手法の上限性能 |
MISSPECIFIED | ENGINE は −2.60 と信じたまま、真の弾力性だけがズレる | 前提が外れたときの耐性。実務ではこちらが本番 |
出力例:
弾力性 −1.2 以上の領域では ENGINE は FLAT_RULE に +8〜+15% の利益差で勝つ
弾力性 −0.8 を下回ると負ける
(客が価格に反応しないため、早期値引きは定価売上を削るだけになる)
乱数のばらつきで境界が暴れるため、各点は複数シード(既定20)の平均を取る。
7. 画面要件
単一ページ。上から下へ読むだけで結論に到達する構成とする。
| # | 領域 | 内容 |
|---|---|---|
| 1 | 結論バナー | 「ENGINE は FLAT_RULE に対し純限界利益 +◯%」を最初に表示 |
| 2 | 商材選択 | 12件から1件選択。STOCK / CAPACITY をタブ切替 |
| 3 | 原価構造 | 定価・変動費・追加販売コスト・廃棄費用・初期在庫・消滅時刻。編集可 |
| 4 | いま出す判断 | 推奨価格・値引率・動的下限・必要販売増加率・日本語の根拠文 |
| 5 | 比較グラフ | 3ポリシーの累積純限界利益と在庫推移 |
| 6 | 感度分析 | スライダーと勝敗境界のリアルタイム表示。ORACLE / MISSPECIFIED トグル |
| 7 | 前提の明示 | 需要モデル式・弾力性値・出典・「仮値」バッジ。常時表示。折りたたみ禁止 |
領域7は意匠ではなく信頼設計である。隠すと疑われ、出すと誠実に見える。
8. データ構造
type ItemKind = "STOCK" | "CAPACITY";
type PolicyId = "NO_ACTION" | "FLAT_RULE" | "ENGINE";
interface InventoryUnit {
unitId: string;
name: string;
kind: ItemKind;
listPrice: number; // 定価(円)
variableCost: number; // 単位変動費。仕入原価・材料費・施術人件費を含む
incrementalSellingCost: number; // 追加販売コスト(値引シール・レジ手間)
disposalCost: number; // 単位廃棄費用。CAPACITY は常に 0
fixedCostAlloc: number; // 固定費配賦。表示専用。判断に使わない
initialQty: number;
expiryAt: string; // ISO8601。消滅時刻
}
このモデルが通れば、スーパーの惣菜も、整体の空き枠も、美容室のカラー枠も、飲食店の平日ランチ席も
同じエンジンで処理できる。型を分けないこと自体が主張である。
全型定義は types/index.ts(Day 1 に凍結)を正とする。
9. 技術スタック・スコープ境界
| 項目 | 内容 |
|---|---|
| フロントエンド | Next.js 16 (App Router) / React 19 / TypeScript / Tailwind CSS v4 |
| グラフ | recharts |
| テスト | Vitest(境界値テストを実装より先に置く) |
| デプロイ | Vercel |
| 計算 | すべてクライアントサイド。DBなし。外部API接続なし |
スコープ外(着手しかけたら止める): 外部API接続すべて(気象庁・Open-Meteo・e-Stat・RESAS・国土地理院)/公共データ基盤/ 機械学習による需要予測/顧客・スタッフアプリ/ピッキング・EC機能/LINE連携・クーポン配信/ 認証・マルチテナント・DB永続化/3年分500SKU規模の合成データ生成器。
カニバリ引当と予実差異分析は、型にフィールドだけ用意し実装は後日とする(切ってよい)。
10. 開発計画と成功条件
| Day | 内容 | 並列度 |
|---|---|---|
| 1 | スコープ確定・型定義の凍結・リポジトリ作成・レーン指示書の発行 | 単独(分割不可) |
| 2–4 | A: 会計ロジックコア+単体テスト / B: 需要シミュレータ+3ポリシー / C: UIシェル | 3CLI並列 |
| 5–6 | 統合。A+B を接続し、C に実値を流す | 単独 |
| 7 | 中間判定:ENGINE は FLAT_RULE に勝つか | 単独 |
| 8–9 | 感度分析スライダー / 根拠文の事前生成 | 2並列 |
| 10–11 | カニバリ引当・予実差異(余力次第。切ってよい) | 2並列 |
| 12 | ブラウザ実行テスト・Vercelデプロイ | 単独 |
| 13 | README.html / 振り返り.html 仕上げ | 単独 |
| 14 | 予備日(必ず空ける) | — |
Day 7 の判定基準
| 区分 | 条件 |
|---|---|
| 必達 | 基準パラメータ下で ENGINE の純限界利益が FLAT_RULE を上回る |
| 望ましい | 上回り幅が +5% 以上 |
| 必達 | ENGINE が負けるパラメータ領域を特定し、境界値を提示できる |