RetailBrain 要件定義

価格設計・販促意思決定エンジン / v1.0 / 2026-09-04 / 単一ページの値引き判断シミュレータ

1. 目的

消滅期限のある在庫(惣菜・施術枠)について「今いくらで売るべきか」を、限界利益と機会費用から算出し、 現場で実際に行われている一律ルールとの利益差を比較する単一ページのシミュレータ。

目的は事業運用ではなくポートフォリオである。 採用担当者が1つのURLを開き、90秒で「小売の利益構造を理解した上で作られたシステムだ」と判断できる状態を作る。

順位満たすこと
1ロジックの筋が通っていること(会計・意思決定理論として反論に耐える)
2動いて触れること(ブラウザで即座に結果が変わる)
3前提と限界を自分で明示していること
この順序に意味がある 見栄えの良い画面は誰でも作れるが、「なぜその価格なのか」に会計で答えられる画面は少ない。 逆に、限界を隠した画面は開いた瞬間に疑われる。3番目を落とすと1番目の説得力も同時に落ちる。

2. 中核概念 — 消滅期限付き在庫

物販とサービスは一見別物だが、「売れ残りの価値がゼロ」という一点で同じ型に統合できる。 したがって限界利益がプラスである限り、売らないより売るほうが合理的である。 この一点で業種を横断できることが、本プロダクトの主張である。

STOCK(在庫型)CAPACITY(キャパシティ型)
例惣菜・弁当・刺身施術60分枠・座席・レッスン枠
消滅の仕方賞味期限・閉店で廃棄時刻が過ぎれば枠が消滅
variableCost の発生発注時に initialQty 分が既発生=サンクコスト。売れても売れなくても支出済み売れたときだけ発生(施術者の人件費など)
disposalCost売れ残ると追加発生常に 0(枠は消えるだけ)
限界利益率低〜中非常に高い(=値引き余地が構造的に大きい)
費用の発生タイミングの違いが、2つの型を分ける唯一の実質 ここを取り違えると、STOCK では「原価割れするから値引きしない」という現場の誤判断をシステムが再現し、 CAPACITY では限界利益が赤になる価格を推奨する。本システムが否定したかったものを本システムが実装することになる。

3. 会計ロジック仕様

会計計算は予測ではなく恒等式である。したがって合成データ上でも一切の疑義が生じない。 本システムで最初に、かつ最も厚く作る層はここになる。

3-1. 用語規約

用語定義備考
限界利益価格 − 変動費本システムで使う唯一の利益概念
限界利益率限界利益 ÷ 価格コード・UI・文書では contributionMarginRatio のみ
粗利率—使用禁止。売上原価の範囲が業種で変わり比較不能

売上原価ベースの粗利率と変動費ベースの限界利益率は、小売では近い値になるがサービス業では 人件費が売上原価に入るかどうかで結果が反転しうる。用語を混ぜた時点で実装は必ず間違える。

3-2. 動的下限価格

floorPrice = incurredIfSold + opportunityCost - disposalCostAvoided

  incurredIfSold      = (kind === "CAPACITY" ? variableCost : 0) + incrementalSellingCost
  disposalCostAvoided = (kind === "STOCK" ? disposalCost : 0)
  opportunityCost     = この1個を今売らずに残した場合の期待限界利益
企画書 v3.1 の式からの是正(2026-09-04) 企画書 13-2 は floorPrice = incrementalSellingCost − disposalCost + opportunityCost としていた。 これは variableCost = 仕入原価 = サンクコスト を暗黙に仮定した STOCK 専用の式である。 本システムの型定義は variableCost に施術者人件費を含めるため、CAPACITY では加算しないと下限が過小になり、 限界利益が赤になる価格を推奨してしまう。上の式は STOCK では従来式と完全に一致する。

この式から2つの帰結が出る。両方を画面で説明する。

  1. STOCK で廃棄費用が正なら、下限価格は理論上マイナスになりうる。 処理費を払って捨てるより 0円で配るほうが得な領域が実在する。これが本システムで最も驚かれる結論であり、隠さず目立たせる
  2. 第2項の機会費用が「時点間カニバリ」の正体。 値引きが早すぎると、定価で買ったはずの客に安く売ることになる。これが下限価格が時間の関数である理由であり、 本問題が逐次意思決定=動的計画問題になる理由でもある

fixedCostAlloc(固定費配賦)は表示専用とし、値引き判断のロジックに入れない。 入れた瞬間に全部原価思考へ戻る。

3-3. 値引き耐性 — 中心となる問い

requiredVolumeLift = 値引額 ÷ (元の単位限界利益 − 値引額)

例: 限界利益率40%の商品を10%引き
    10 ÷ (40 − 10) = 0.33  →  33%の販売増がなければ利益は減る

分母が 0 以下なら回収不能なので null を返す(0除算や Infinity を返さない)。 現場担当者はこの数字を知らずに値引きしている。「その値引き、33%売れないと損です」と即座に出せることが、 本プロダクトの最も分かりやすい価値である。

下限価格と値引き耐性は「時点」が違う 値引き耐性は仕入原価を含んだ variableCost で算出する。これは「この商売として値引きが割に合うか」を問う指標であり、 「今夜この1個をどうするか」を問う下限価格とは判断の時点が違う。画面上で同じ枠に並べて混同させてはならない。

3-4. 実装する関数

関数定義備考
unitContributionprice − variableCost全判断の基礎。負の値もクリップしない
contributionMarginRatiounitContribution ÷ priceprice ≦ 0 なら null
requiredVolumeLiftd ÷ (baseContribution − d)分母 ≦ 0 なら null
dynamicFloor3-2 の式kind で分岐
breakEvenUnitsfixedCost ÷ unitContribution単位限界利益 ≦ 0 なら null
couponCap増分限界利益 > クーポン費用 となる上限額Day 10 以降。型のみ先行
aggregateスロット結果の集計下記の規定に従う
netContribution = revenue - variableCost - incrementalSellingCost - disposalCost

  variableCost : STOCK    → unit.variableCost × initialQty(ポリシー間で定数)
                 CAPACITY → sold 比例
STOCK の変動費を sold 比例にしてはならない 仕入原価はどのポリシーでも同額が既に発生している。sold 比例で計上すると、深い値引きが廃棄より損に見え、 エンジンが不当に負ける。中間判定(Day 7)の結論がここ一箇所で反転する。

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 で時間非依存モデルに退化するため、感度分析で影響度を明示できる
カテゴリレベル弾力性を単品判断に使ってはならない カテゴリレベル(Andreyeva, Long & Brownell 2010, −0.27〜−0.81)とブランド/SKUレベル(−2.6前後)は約3倍異なるが、 これは矛盾ではなく測定階層の違いである。カテゴリは「肉全体の需要」(値上げしても人は肉を食べる)、 ブランドは「この商品の需要」(隣の棚に代替がある)を測っている。 値引き判断は単品=SKUレベルであるため、カテゴリ値を使うとエンジンは「値引いても売れない」と誤判定し、 常に値引きを回避する。設計が根本から壊れる。

4-2. 既知の限界

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. 主張してよい範囲

内容
主張してよいポリシー間の相対比較、およびその比較が成立する条件範囲
主張してはいけない絶対的な効果量(「廃棄ロス◯%削減」等)、需要予測の精度
合成データは代用品ではない 本システムが出す答えは「30%引いた場合」と「引かなかった場合」の比較=反実仮想の比較である。 実POSデータには「実際に起きたこと」しか記録されていないため、反実仮想は原理的に存在しない。 したがって合成データは実データがないから使う代用品ではなく、反実仮想を回すために原理的に必要なものである。

6. 感度分析 — 本システムの目玉

「自分が決めた弾力性で、自分のエンジンが勝った」では説得力がない。 弾力性・来店強度・廃棄費用をスライダーで可変にし、勝つ領域と負ける領域の境界を提示する。 検証不能という弱点を、感度分析という機能に転換する。

モード内容示せること
ORACLEENGINE が真の弾力性を知っている前提手法の上限性能
MISSPECIFIEDENGINE は −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–4A: 会計ロジックコア+単体テスト / B: 需要シミュレータ+3ポリシー / C: UIシェル3CLI並列
5–6統合。A+B を接続し、C に実値を流す単独
7中間判定:ENGINE は FLAT_RULE に勝つか単独
8–9感度分析スライダー / 根拠文の事前生成2並列
10–11カニバリ引当・予実差異(余力次第。切ってよい)2並列
12ブラウザ実行テスト・Vercelデプロイ単独
13README.html / 振り返り.html 仕上げ単独
14予備日(必ず空ける)—

Day 7 の判定基準

区分条件
必達基準パラメータ下で ENGINE の純限界利益が FLAT_RULE を上回る
望ましい上回り幅が +5% 以上
必達ENGINE が負けるパラメータ領域を特定し、境界値を提示できる
負けても成立するように設計している 単純ルールは意外と強く、ENGINE が勝たない可能性は現実にある。Day 12 に判明すると詰むため判定を Day 7 に置いた。 負けた場合は「勝つ条件・負ける条件の提示」へ設計を寄せる。 むしろ「この条件では負けます」と明示できるポートフォリオは希少であり、感度分析がそのまま保険になる。