RetailBrain README
消滅期限のある在庫(惣菜・施術枠)について「今いくらで売るべきか」を限界利益と機会費用から算出し、 現場の一律ルールとの利益差を比較する単一ページのシミュレータ。計算はすべてブラウザ側で完結する。 外部APIへの接続もデータベースもない。
1. 動作環境
| 項目 | バージョン | 備考 |
|---|---|---|
| Node.js | v24.16.0 | この環境で実測 |
| npm | 11.13.0 | 同上 |
| Next.js | 16.3.4 | App Router |
| React | 19.2.8 | |
| TypeScript | 5.x | strict |
| Tailwind CSS | 4.x | PostCSS 経由 |
| recharts | 3.10.x | 領域5・6のグラフ |
| Vitest | 5.x | 単体・統合テスト |
2. セットアップと起動
npm install
npm run dev # http://localhost:3000 を開く
| コマンド | 内容 |
|---|---|
npm run dev | 開発サーバ。ポート 3000 |
npm run build | 本番ビルド |
npm start | ビルド済みの本番サーバ |
npm test | Vitest を1回実行(tests/**/*.spec.ts) |
npm run test:watch | 同上を watch モードで |
npm run typecheck | tsc --noEmit |
npm run lint | ESLint |
環境変数は使わない。.env は不要で、APIキーの類も持たない。
データは data/units.ts(12商材の原価構造)と data/demand.ts(来店強度カーブと既定パラメータ)に
直接書かれている。
2-1. コードの置き場所
| ディレクトリ | 内容 |
|---|---|
types/ | レーン間で共有する型。ここが契約の正 |
data/ | 12商材の原価構造・来店強度カーブ・既定パラメータ |
lib/accounting/ | 会計の恒等式。動的下限価格・値引き耐性・集計・日本語の根拠文 |
lib/sim/ | 需要シミュレータ。乱数・DP・3ポリシー実行・感度分析・比較統計 |
components/, app/ | 画面 |
tests/ | 単体(accounting / sim)と統合(integration) |
3. 画面の使い方
単一ページを上から下へ読むと結論に到達する構成になっている。7つの領域がある。
3-1. 領域1 — 結論バナー
エンジンが現場の一律ルールに対して純限界利益をどれだけ上回ったかを最初に出す。 差が小さいときは「ほぼ互角」と表示し、優位として語らない。
3-2. 領域2 — 商材選択
物販(STOCK。惣菜・弁当・刺身)とサービス枠(CAPACITY。施術枠・座席)をタブで切り替え、12商材から1つ選ぶ。 両者を同じ型で扱えること自体がこのシステムの主張なので、切り替えても画面の構造は変わらない。
3-3. 領域3 — 原価構造
定価・変動費・追加販売コスト・廃棄費用・初期在庫・消滅時刻。その場で編集できる。 数字を変えると以降の領域がすべて再計算される。
3-4. 領域4 — いま出す判断
選んだ時刻での推奨価格・値引率・動的下限・必要販売増加率と、日本語の根拠文。
動的下限は「今夜この1個をどうするか」を問う指標である。 STOCK で廃棄費用が追加販売コストを上回る商材では下限がマイナスになる。 処理費を払って捨てるより 0 円で配るほうが得な領域が存在する、という意味であり、表示上も握りつぶしていない。 ただしすべての商材で負になるわけではない。既定データでは、手作りコロッケは 8 − 8 = 0 円、 彩り野菜サラダは 8 − 7 = +1 円で、下限は負にならない。
必要販売増加率は「この商売として値引きが割に合うか」を問う別の時点の指標で、
仕入原価を含んだ変動費で計算する。限界利益率40%の商品を10%引きするなら
10 / (40 − 10) = 33% の販売増がなければ利益は減る。
下限と必要販売増加率は時点が違うので、同じ枠に並べて読まないこと。
3-5. 領域5 — 3ポリシーの比較グラフ
| ポリシー | 内容 |
|---|---|
| 定価据え置き | 値引きしない。下限ベースライン |
| 一律ルール | 消滅120分前に一律30%引き。現場で実際に行われているルールで、本命の比較対象 |
| エンジン | 各スロットで動的下限と機会費用を計算し、期待残存限界利益を最大化する値引率を選ぶ |
累積純限界利益と在庫推移を並べる。3ポリシーは同一シードの同一乱数系列で回しているので、 線の差はポリシーの差であって乱数の差ではない。
3-6. 領域6 — 感度分析(ORACLE と MISSPECIFIED)
弾力性・来店強度・廃棄費用をスライダーで動かし、エンジンが一律ルールを上回る領域と下回る領域の境界を出す。 「自分で決めた前提の下で自分のエンジンが勝った」では説得力がないため、前提が外れる範囲まで含めて示す。 モードの切り替えがこの領域の中心にある。
| モード | 意味 |
|---|---|
| ORACLE | エンジンが、その商品が値引きにどれだけ反応するかを正確に知っている場合。上限性能 |
| MISSPECIFIED | エンジンは弾力性 −2.60 だと信じたまま値引き計画を立てるが、実際の客の反応だけがそこからズレる場合 |
既定データの唐揚げ弁当では、ORACLE は観測した弾力性の全域で一律ルールを上回った。 MISSPECIFIED では、真の弾力性がおよそ −0.78 より弱くなると一律ルールを下回る (400シード×独立5ブロックで −0.768 〜 −0.812)。整体では MISSPECIFIED でも観測範囲の全域で上回った。 限界利益率が構造的に高く、値引きの下振れが小さいためである。
画面は境界のスカラー値だけでなく負けている区間も描く。 エンジンと一律ルールの差は軸に対して単調ではなく、一度負けてからまた勝ちに戻る形が実際に現れるため、 1つの数値だけでは「その先はずっと負け」という誤読を生む。
3-7. 領域7 — 前提の明示
需要モデルの式、使っている弾力性の値と出典、仮値であるパラメータのバッジ、主張しないと決めていることを常時表示する。 折りたたまない。隠すと疑われ、出すと確かめられる、という判断による。
4. パラメータの意味
需要モデルは次の形をしている。乱数はポアソン分布で、平均が下の q になる。
q(p, t) = arrivalCurve[t] × baseQty × (p / listPrice) ^ ε(t)
ε(t) = elasticity × (1 + elasticityTimeAmp × (1 − 残スロット比))
| パラメータ | 既定値 | 意味と、動かすと何が起きるか |
|---|---|---|
elasticity |
STOCK −2.60 CAPACITY −2.00仮値 |
価格弾力性。負値で、絶対値が大きいほど値引きに敏感。−2.60 は「10%値引くと販売数がおよそ1.32倍になる」見当。 弱くする(0に近づける)と、値引いても客が増えないためエンジンの値引きが利幅を削るだけになり、一律ルールに近づくか下回る。 強くすると値引きの効きが良くなり、エンジンの優位が広がる |
elasticityTimeAmp (λ) |
0.50仮値 | 消滅までの残り時間が短いほど価格反応が鋭くなる度合い。0.50 なら消滅直前の実効弾力性は素の値の1.5倍 (−2.60 → −3.90)。0 にすると時間に依存しないモデルに退化する |
arrivalCurve |
24スロット | 10:00〜22:00 を30分刻みにした来店強度。平均を1.0とした相対値であって客数そのものではない。 12:00 と18:00 に山がある。全体を倍にすると客足が増え、定価のまま売り切れやすくなる。 売り切れる領域では3ポリシーの結果が一致し、値引き判断そのものが意味を持たなくなる |
baseQty |
自動 | 「定価のままなら初期在庫の約75%が売れる」水準に商材ごとに自動調整される(baseQtyFor)。
商材ごとの売れ残り量を揃えるための設計値であり、実測値ではない。
揃えないと、ポリシーの比較が商材の当たり外れの比較になってしまう |
seed |
20260904 | 乱数シード。3ポリシーは同一シードの同一乱数系列で回る。 スロットごとに一様乱数を1本だけ引き、逆関数法でポアソンに変換しているので、 値引率が違っても同じスロットで同じ乱数が使われる(共通乱数)。 これにより同じ1日について3つのポリシーを比較できる |
5. 既知の限界
ここが最も重要な節である。以下はすべて実測に基づく。
5-1. 合成データである
実店舗のPOSデータではない。ただしこれは実データが手に入らないための代用ではない。 「同じ日に3つのポリシーを同時に試す」という反実仮想は実データでは原理的に取れないため、 ポリシー比較には合成データが必要になる。
したがって主張できるのはポリシー間の相対比較と、その比較が成立する条件範囲だけである。 廃棄ロスが何%減る、といった絶対的な効果量は主張の対象にしない。
5-2. 弾力性は推定していない
このシステムは弾力性を外部から与えられた値として受け取る。データから推定していない。 よって需要予測の精度は主張の対象外である。予測精度を評価指標に置いていないのも同じ理由による。 値引き・発注の意思決定は、欠品コストと過剰コストが非対称な構造をしており、 予測精度が上がっても利益が下がることが起こりうる。
5-3. 文献的裏付けのない仮値が2つある
| パラメータ | 値 | 状態 |
|---|---|---|
| STOCK の弾力性 | −2.60 | Bijmolt et al. (2005) のブランドレベル平均 −2.62 に基づく |
| CAPACITY の弾力性 | −2.00 | 仮値・出典なし |
| λ(時間による反応の鋭さ) | 0.50 | 仮値・文献的裏付けなし |
サービス枠の結果はこの仮値に依存している。λ を 0 にすると時間に依存しないモデルに退化するので、 仮定の効き方はスライダーで確かめられる。
5-4. 平均では上回るが、1日単位では下回る日がある
これが最も誤解されやすい点である。既定の一律ルール(消滅120分前に一律30%引き)に対して、 400シードの対応のある差で測ると12商材すべてで95%信頼区間の下限が正になった。 それでも1日単位で見ると下回る日が一定の割合で存在する。
| 商材 | 型 | 平均差 | 95%信頼区間 | 下回った日の割合 |
|---|---|---|---|---|
| 唐揚げ弁当 | STOCK | 668円 | [610, 726] | 10.3% |
| 握り寿司 10貫 | STOCK | 891円 | [800, 981] | 14.5% |
| 刺身盛り合わせ | STOCK | 674円 | [615, 733] | 11.5% |
| 手作りコロッケ 5個 | STOCK | 543円 | [499, 586] | 8.0% |
| 彩り野菜サラダ | STOCK | 343円 | [312, 375] | 12.0% |
| 焼き鳥 5本盛り | STOCK | 611円 | [531, 691] | 15.3% |
| 整体 60分枠 | CAPACITY | 481円 | [373, 588] | 27.8% |
| 美容室 カラー枠 90分 | CAPACITY | 536円 | [317, 756] | 29.0% |
| パーソナルトレーニング 50分 | CAPACITY | 565円 | [373, 756] | 30.8% |
| ネイル 60分枠 | CAPACITY | 515円 | [355, 675] | 30.5% |
| ヨガレッスン 1枠 | CAPACITY | 924円 | [784, 1064] | 23.5% |
| 平日ランチ席 | CAPACITY | 2,089円 | [1947, 2230] | 9.3% |
物販は 8.0〜15.3%、サービス枠は 9.3〜30.8% の日で一律ルールを下回った。 サービス枠のほうが高いのは、1日の枠数が少なく(4〜20枠)1件の売れ方の影響が大きいためである。 平日ランチ席が 9.3% と低いのは、20枠あり消滅時刻が早く、物販に近い挙動になるからである。
5-5. 感度分析の境界値そのものにばらつきがある
画面に出る境界は推定値であって確定値ではない。唐揚げ弁当の MISSPECIFIED で、 独立した乱数ブロックごとに境界を測り直すと次のように振れる。
| シード数 | 独立した5ブロックで得られた境界 | 振れ幅 |
|---|---|---|
| 20(画面の既定) | −0.600 / −0.981 / −0.848 / −0.772 / −0.838 | 0.38 |
| 100 | −0.766 / −0.801 / −0.772 / −0.756 / −0.746 | 0.055 |
| 400 | −0.812 / −0.770 / −0.774 / −0.785 / −0.768 | 0.044 |
落ち着く先はおよそ −0.78 だが、画面の既定シード数では境界が ±0.2 程度動く。 境界は「この辺りで勝敗が入れ替わる」という目安として読むもので、小数点以下2桁を根拠にした議論には使えない。
5-6. 「在庫が減るほど残す価値が上がる」は全域では成り立たない
エンジンは動的計画法で「この1個を残す価値」を求めている。傾向としては在庫が少ないほどこの価値は高い (唐揚げ弁当のあるスロットで、在庫1個なら609円、在庫40個なら272円)。 しかし隣り合う在庫数で比べると858点中31点(3.6%)で逆転する。 最大の逆転はスロット20・在庫38個で、74.23円から169.41円へ跳ねる。
これは数値誤差ではない。値引率が5%刻みの離散的な選択肢であるため、在庫が1個増えた瞬間に 最適な値引率が刻みの上で飛び、増分価値が不連続に変わることによる構造的な性質である。 画面で「在庫が減るほど残す価値が上がる」と単調に説明すると、在庫が多い領域で実際の数値と食い違う。
5-7. スコープ外
外部APIとの接続、公共データ基盤、機械学習による需要予測、顧客・スタッフ向けアプリ、EC機能、 外部メッセージング連携、認証・マルチテナント・データベース永続化は範囲外である。 固定費配賦は表示のみで、値引き判断のロジックには入れていない。 入れた時点で「全部原価を回収できる価格でしか売らない」という発想に戻り、消滅期限付き在庫を扱えなくなるためである。
6. テストの構成
| 場所 | 内容 |
|---|---|
tests/accounting/ | 会計の恒等式。境界値(限界利益が負・値引額0・残時間0・下限が負になる条件) |
tests/sim/ | 乱数の再現性と共通乱数、需要モデル、動的計画法の終端条件と単調性、 感度分析(並び順に依存しないこと・引き分けを負けと数えないこと)、比較統計 |
tests/integration/ | 動的下限と動的計画法の接続、後知恵で調律した一律ルールとの比較、 境界の定義に関する回帰テスト |
テストの期待値は仕様として扱う。実装に合わせて期待値を書き換えることはしない。 期待値のほうが誤っていると判断した場合は、根拠を実測で示したうえで期待値を直す。