RetailBrain README

セットアップ / 画面の使い方 / パラメータ / 既知の限界 / 2026-09-04

消滅期限のある在庫(惣菜・施術枠)について「今いくらで売るべきか」を限界利益と機会費用から算出し、 現場の一律ルールとの利益差を比較する単一ページのシミュレータ。計算はすべてブラウザ側で完結する。 外部APIへの接続もデータベースもない。

このドキュメントの数値はすべて現物から取っている 効果量・負けシード率・境界値は、このリポジトリのコードを実際に走らせて測った値である。 測っていないことは「未確認」と書いた。推測で埋めた数値はない。

1. 動作環境

項目バージョン備考
Node.jsv24.16.0この環境で実測
npm11.13.0同上
Next.js16.3.4App Router
React19.2.8
TypeScript5.xstrict
Tailwind CSS4.xPostCSS 経由
recharts3.10.x領域5・6のグラフ
Vitest5.x単体・統合テスト

2. セットアップと起動

npm install
npm run dev        # http://localhost:3000 を開く
コマンド内容
npm run dev開発サーバ。ポート 3000
npm run build本番ビルド
npm startビルド済みの本番サーバ
npm testVitest を1回実行(tests/**/*.spec.ts)
npm run test:watch同上を watch モードで
npm run typechecktsc --noEmit
npm run lintESLint

環境変数は使わない。.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 — 原価構造

定価・変動費・追加販売コスト・廃棄費用・初期在庫・消滅時刻。その場で編集できる。 数字を変えると以降の領域がすべて再計算される。

STOCK と CAPACITY で変動費の意味が違う STOCK の変動費(仕入原価)は発注時点で初期在庫ぶんが既に発生している。売れても売れなくても支出済みなので、 「今夜この1個をいくらまで下げてよいか」の計算には入れない。 CAPACITY の変動費(施術者の人件費など)は売れたときだけ発生するので、こちらは入れる。 この違いが2つの業態を分ける唯一の構造的差である。

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 だと信じたまま値引き計画を立てるが、実際の客の反応だけがそこからズレる場合
MISSPECIFIED を小売の言葉で言うと 「去年までの感覚で値引き計画を組んだが、今年は客層が変わっていた」という状況である。 エンジンは自分の思い込みに基づいて「この幅まで下げれば十分売れる」と判断するので、 実際の反応が思ったより鈍いと、値引いた分だけ利幅を削って販売数が伸びない。 一方で一律ルールは思い込みを持たない。前提が外れても同じ振る舞いをするだけなので、 前提が大きく外れる領域では一律ルールのほうが強くなりうる。 検証できないという弱点を、どこまで外れたら負けるかを示す機能に変えているのがこの領域である。

既定データの唐揚げ弁当では、ORACLE は観測した弾力性の全域で一律ルールを上回った。 MISSPECIFIED では、真の弾力性がおよそ −0.78 より弱くなると一律ルールを下回る (400シード×独立5ブロックで −0.768 〜 −0.812)。整体では MISSPECIFIED でも観測範囲の全域で上回った。 限界利益率が構造的に高く、値引きの下振れが小さいためである。

画面は境界のスカラー値だけでなく負けている区間も描く。 エンジンと一律ルールの差は軸に対して単調ではなく、一度負けてからまた勝ちに戻る形が実際に現れるため、 1つの数値だけでは「その先はずっと負け」という誤読を生む。

弾力性軸以外ではモードの差が出ないことがある モードは弾力性についてのみ定義される。来店強度軸・廃棄費用軸を選び、かつ真の弾力性がエンジンの思い込み(−2.60)と 同じ値のままなら、ORACLE と MISSPECIFIED は一致する。これは不具合ではない。

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つのポリシーを比較できる
カテゴリレベルの弾力性を入れてはいけない STOCK の −2.60 は Bijmolt, van Heerde & Pieters (2005), JMR 42(2), 141-156 の ブランド/SKUレベルのメタ分析(81研究1,851推定、平均 −2.62)に基づく。 食品カテゴリ全体の弾力性(−0.27〜−0.81 の水準)は「肉全体の需要」を測っており階層が約3倍違う。 値引き判断は単品=SKUレベルの意思決定なので、カテゴリ値を入れるとエンジンが「値引いても売れない」と誤判定する。

5. 既知の限界

ここが最も重要な節である。以下はすべて実測に基づく。

5-1. 合成データである

実店舗のPOSデータではない。ただしこれは実データが手に入らないための代用ではない。 「同じ日に3つのポリシーを同時に試す」という反実仮想は実データでは原理的に取れないため、 ポリシー比較には合成データが必要になる。

したがって主張できるのはポリシー間の相対比較と、その比較が成立する条件範囲だけである。 廃棄ロスが何%減る、といった絶対的な効果量は主張の対象にしない。

5-2. 弾力性は推定していない

このシステムは弾力性を外部から与えられた値として受け取る。データから推定していない。 よって需要予測の精度は主張の対象外である。予測精度を評価指標に置いていないのも同じ理由による。 値引き・発注の意思決定は、欠品コストと過剰コストが非対称な構造をしており、 予測精度が上がっても利益が下がることが起こりうる。

5-3. 文献的裏付けのない仮値が2つある

パラメータ値状態
STOCK の弾力性−2.60Bijmolt et al. (2005) のブランドレベル平均 −2.62 に基づく
CAPACITY の弾力性−2.00仮値・出典なし
λ(時間による反応の鋭さ)0.50仮値・文献的裏付けなし

サービス枠の結果はこの仮値に依存している。λ を 0 にすると時間に依存しないモデルに退化するので、 仮定の効き方はスライダーで確かめられる。

5-4. 平均では上回るが、1日単位では下回る日がある

これが最も誤解されやすい点である。既定の一律ルール(消滅120分前に一律30%引き)に対して、 400シードの対応のある差で測ると12商材すべてで95%信頼区間の下限が正になった。 それでも1日単位で見ると下回る日が一定の割合で存在する。

商材型平均差95%信頼区間下回った日の割合
唐揚げ弁当STOCK668円[610, 726]10.3%
握り寿司 10貫STOCK891円[800, 981]14.5%
刺身盛り合わせSTOCK674円[615, 733]11.5%
手作りコロッケ 5個STOCK543円[499, 586]8.0%
彩り野菜サラダSTOCK343円[312, 375]12.0%
焼き鳥 5本盛りSTOCK611円[531, 691]15.3%
整体 60分枠CAPACITY481円[373, 588]27.8%
美容室 カラー枠 90分CAPACITY536円[317, 756]29.0%
パーソナルトレーニング 50分CAPACITY565円[373, 756]30.8%
ネイル 60分枠CAPACITY515円[355, 675]30.5%
ヨガレッスン 1枠CAPACITY924円[784, 1064]23.5%
平日ランチ席CAPACITY2,089円[1947, 2230]9.3%

物販は 8.0〜15.3%、サービス枠は 9.3〜30.8% の日で一律ルールを下回った。 サービス枠のほうが高いのは、1日の枠数が少なく(4〜20枠)1件の売れ方の影響が大きいためである。 平日ランチ席が 9.3% と低いのは、20枠あり消滅時刻が早く、物販に近い挙動になるからである。

シード数が少ないと判定できない商材がある 画面の既定は30シードである。この本数だとパーソナルトレーニング50分は 95%信頼区間が [−400, 816] と0をまたぎ、上回るとも下回るとも言えない。 画面はこの場合「判定不能」と出す。数を増やせば判定できるようになるが、 既定の本数で断定しないことのほうを優先している。

5-5. 感度分析の境界値そのものにばらつきがある

画面に出る境界は推定値であって確定値ではない。唐揚げ弁当の MISSPECIFIED で、 独立した乱数ブロックごとに境界を測り直すと次のように振れる。

シード数独立した5ブロックで得られた境界振れ幅
20(画面の既定)−0.600 / −0.981 / −0.848 / −0.772 / −0.8380.38
100−0.766 / −0.801 / −0.772 / −0.756 / −0.7460.055
400−0.812 / −0.770 / −0.774 / −0.785 / −0.7680.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/動的下限と動的計画法の接続、後知恵で調律した一律ルールとの比較、 境界の定義に関する回帰テスト

テストの期待値は仕様として扱う。実装に合わせて期待値を書き換えることはしない。 期待値のほうが誤っていると判断した場合は、根拠を実測で示したうえで期待値を直す。