# 12. Next Best Action エンジン 設計書
## 言問散歩 名店DB 202社 — 各店の「次の一手」を一意に決め・スコア化・測定可能にする

> 作成：CMO（マーケティング大臣）/ 2026-06-06  
> 正本DB：`meiten-business/data/meiten-master.json`（v1.0.0・202社）  
> 先行文書：`03_segmentation-report.md` / `04_gold-top10-dossier.md` / `11_crm-design.md`  
> 実装依頼先：CTO → Codex（擬似コード粒度まで本書で確定）  
> 数値規律（GR19）：係数の初期値はすべて「根拠付き仮置き・要キャリブレーション」と明記。捏造禁止。

---

## 0. エグゼクティブ・サマリ（設計の本質を3行で）

現状の「nextAction」フィールドは定性テキストで、**誰が・いつ・なぜ・どの優先度で**が不明確。
このエンジンは「期待価値（EV）スコア」で202店を一意にランク付けし、スコアとラベルから
**次の一手（NBA：Next Best Action）を機械的に決定し、担当・期限・KPIまで生成する**仕組みである。

**データ実態（2026-06-06時点）：**
- 202店の内訳：L1継続54社 / L2途絶8社 / L3埋蔵金8社 / L4グループ3社 / L5域外大手8社 / L6未接触121社
- Vol.15受注46社・商談中7社・未着手3社・未設定146社
- PotentialTier：A=27社 / B=142社 / C=33社
- spendKnownTotal合計：¥6,976,352（上限¥1,200,000は日鉄興和不動産）

---

## 1. アクション分類（MECE・有限集合）

### 1.1 アクションタイプ一覧（10分類）

| ID | アクション名 | 対象ラベル | 目的 | 標準工数（h） | 直接コスト | 期待リフト（1回） |
|---|---|---|---|---|---|---|
| **A01** | 年契更新・継続確認 | L1a/L1b/L1c/L1d（継続中） | 離脱防止・継続確実化 | 1h（電話/訪問） | ¥0 | 既存額の100%維持 |
| **A02** | アップセル提案 | L1b→L1a / L1c→L1b / L1d→L1a | 単価引き上げ | 2h（商談準備+訪問） | ¥0 | +¥25k〜+¥340k |
| **A03** | 取材オファー | L3 / L6（worldviewFit高） | 無償接点→ファネル入口 | 3h（企画+訪問） | ¥0 | 将来CVへの投資（LAG KPI） |
| **A04** | 名店リスト勧誘 | L3取材済 / L6潜在 | エントリー商品で関係構築 | 1.5h | ¥0 | ¥3,000/号 + 翌号アップセル原資 |
| **A05** | ブランド枠初回提案 | L3取材済 / L6潜在（potentialTier A/B） | 初収益化 | 2h | ¥0 | ¥35,000〜¥60,000 |
| **A06** | 大型枠・記事広告提案 | L2（大型復活） / L6（best100・A tier） | 高単価獲得 | 3h | ¥0 | ¥100,000〜¥600,000 |
| **A07** | 途絶復活打診 | L2（lapsed=true） | 関係再構築・再受注 | 2h | ¥0 | ¥3,000〜¥400,000（前回額の60〜80%） |
| **A08** | 手形/ホテル連携提案 | tegataFit高 or hotelEastFit高（既存客優先） | クロスセル・関係深化 | 1.5h | ¥0 | 定性（関係強化）+ 将来単価UP |
| **A09** | 担当アサイン | salesReps空 or 接点なし | 担当真空の解消 | 0.5h（内部作業） | ¥0 | アクション実行の前提 |
| **A10** | 休眠・見送り | L5域外非接触 / potentialTier C・worldviewFit低 | リソース集中のための除外 | 0h | ¥0 | 機会コスト削減（他への集中） |

**注：** 工数はフィールド営業1件あたりの準備+実施の合計（移動時間は含まない）。直接コスト¥0は営業人件費を含まないことを意味する（人件費は固定費として別計上）。期待リフトは「このアクションが成功した場合の1号あたり増収額または最終契約額」の推計値（要キャリブレーション）。

### 1.2 ファネル構造（取材→広告化の実証パス）

```
【入口】      【関係構築】   【初収益化】    【単価拡大】   【大型化】
A03取材     → A04名店リスト → A05ブランド枠 → A02アップセル → A06大型枠
オファー      ¥3,000/号      ¥35k単発        ¥60k年契        ¥100k〜¥600k
（無償投資）  （ロスリーダー）（ROI転換点）   （安定収益）    （収益の柱）
```

実証データ（03_segmentation-report.md §1.3）：
- Vol.8取材 → Vol.12年契化（チーズ谷・清浄無垢・キャノンボールダイナー・ROSSONERO）= 4社が同一コホートで転換
- 取材→有料化の平均ラグ：4号（約1年）
- 転換後の継続率：追跡可能4社で100%（Vol.15まで継続中）

---

## 2. 定量スコアリング・モデル

### 2.1 期待価値（EV）スコアの基本式

```
EV = ( P_convert × V_expected × S_strategic ) − C_action

where:
  P_convert   = 獲得/転換確率（0〜1）
  V_expected  = 想定受注額（¥）
  S_strategic = 戦略係数（0.5〜2.0）
  C_action    = 営業コスト（工数×人件費換算・¥）
```

**この式の根拠：** 確率的期待値アプローチはリード・スコアリング・モデル（Kotler, Marketing Management 15th ed.）の標準形を援用。S_strategicによる補正は、純粋な金銭価値に収まらない「誌面ブランド価値・象徴性・ファネル乗数効果」を反映するための補正項（戦略係数の設計は §2.4）。

**対立する考え方：** ICEフレームワーク（Impact × Confidence × Ease）では「実行容易性」を独立軸に置く。本モデルでは「実行容易性」をP_convertに内包し、EVの引き算としてC_actionで表現する設計を採用したが、ICEで別軸にしたほうが営業現場の直感に合う可能性がある（→ §2.6で補助スコアとして併置）。

### 2.2 P_convert（獲得確率）の算出式

```python
def calc_p_convert(store):
    """
    獲得確率 P_convert ∈ [0.02, 0.90]
    
    基底確率（label × 接点履歴から）:
    """
    base_p = {
        "L1a": 0.90,  # 継続大型：既に受注済みが多い→維持確率
        "L1b": 0.85,  # 言問ブランド年契：年契継続は高確率
        "L1c": 0.80,  # 名店リスト：小額だが慣性継続
        "L1d": 0.85,  # 継続記事下：士業系は安定継続
        "L2":  0.25,  # 途絶：過去接点あり→ゼロより高いが1年以上空白
        "L3":  0.30,  # 取材済：関係あり→初有料化の最短コース
        "L4":  0.95,  # グループ内：自社判断で決まる
        "L5":  0.15,  # 域外大手：法人営業・決裁長い
        "L6":  0.10,  # 未接触：コールドアプローチ
    }[store["label"]]
    
    # 補正1：Vol.15受注済みなら翌号継続確率を上げる
    if store.get("vol15", {}).get("status") == "受注":
        base_p = min(base_p * 1.15, 0.95)  # 要キャリブレーション
    
    # 補正2：lapsed（途絶）ならL1でもペナルティ
    if store.get("lapsed") == True:
        base_p = base_p * 0.40  # 要キャリブレーション
    
    # 補正3：issuesPlacedが多いほど（長期接点）確率UP
    n_issues = len(store.get("issuesPlaced") or [])
    if n_issues >= 4:
        base_p = min(base_p * 1.10, 0.95)  # 要キャリブレーション
    elif n_issues >= 8:
        base_p = min(base_p * 1.20, 0.95)  # 要キャリブレーション
    
    # 補正4：potentialTierによる補正
    tier_mult = {"A": 1.10, "B": 1.00, "C": 0.85, None: 1.00}
    base_p = base_p * tier_mult.get(store.get("potentialTier"), 1.00)
    
    return max(0.02, min(0.90, base_p))
```

**要キャリブレーション：** 上記の基底確率（L1=0.85-0.90、L2=0.25、L3=0.30、L6=0.10）は、03_segmentation-report.mdの実績パターンと業界標準的な営業転換率（BtoB地域誌営業の新規開拓CVR：5〜15%、既存客維持率：80〜90%）を参考にした仮置き値。実際のVol.15営業結果が出た時点で「Vol.14継続率・L3転換率・L6転換率」を実測し、全パラメータを更新すること。

### 2.3 V_expected（想定受注額）の算出式

```python
def calc_v_expected(store):
    """
    想定受注額（1号あたり）= レートカードの「該当ラベルの標準枠」
    
    算定優先順位：
    1. vol15.amount が確定していればその値を使う（実績ベース）
    2. lastSpend > 0 なら lastSpend を使う（直近実績）
    3. label×potentialTierからのデフォルト値を使う
    """
    # 1. vol15確定額
    v15 = store.get("vol15") or {}
    if v15.get("amount") and v15.get("status") in ["受注", "商談中"]:
        return v15["amount"]
    
    # 2. 直近実績
    last = store.get("lastSpend") or 0
    if last > 0:
        return last
    
    # 3. label×potentialTierデフォルト
    label = store.get("label", "L6")
    tier  = store.get("potentialTier", "B")
    
    # レートカード準拠のデフォルト値（推計・要キャリブレーション）
    defaults = {
        ("L1a", "A"): 400000, ("L1a", "B"): 200000,
        ("L1b", "A"):  60000, ("L1b", "B"):  60000,
        ("L1c", "A"):   3000, ("L1c", "B"):   3000, ("L1c", "C"):  3000,
        ("L1d", "A"):  50000, ("L1d", "B"):  35000, ("L1d", "C"): 30000,
        ("L2",  "A"): 200000, ("L2",  "B"): 100000, ("L2",  "C"): 50000,
        ("L3",  "A"):  60000, ("L3",  "B"):  35000, ("L3",  "C"):  3000,
        ("L5",  "A"): 200000, ("L5",  "B"): 100000,
        ("L6",  "A"):  60000, ("L6",  "B"):  35000, ("L6",  "C"):  3000,
    }
    return defaults.get((label, tier), defaults.get((label, "B"), 3000))
```

**注：** L4グループ内はV_expected算出の対象外（グループ内振替として別管理を推奨）。

### 2.4 S_strategic（戦略係数）の算出式

戦略係数は「金銭価値の外に存在する戦略的重要度」を数値化する。0.5〜2.0の範囲で、1.0が中立。

```python
def calc_s_strategic(store):
    """
    戦略係数 S_strategic ∈ [0.5, 2.0]
    
    構成要素（各成分の初期重み・要キャリブレーション）:
      W_world  = worldviewFit（誌面世界観適合）
      W_tegata = tegataFit（手形アプリ加盟可能性）
      W_hotel  = hotelEastFit（ホテル送客親和）
      W_symbol = 象徴性（老舗・誌名連動・朝顔市連動）
      W_funnel = ファネル乗数（この店の広告化が他店を連れてくるか）
    """
    
    # === worldviewFit ===
    wf_text = str(store.get("worldviewFit", "") or "")
    if "最高" in wf_text or ("高" in wf_text and "低" not in wf_text):
        w_world = 1.0
    elif "中" in wf_text:
        w_world = 0.5
    else:
        w_world = 0.0
    
    # === tegataFit ===
    tf = store.get("tegataFit") or {}
    tegata_map = {"高": 1.0, "中": 0.5, "低": 0.0}
    w_tegata = tegata_map.get(tf.get("level") if isinstance(tf, dict) else str(tf), 0.0)
    
    # === hotelEastFit ===
    hf_text = str(store.get("hotelEastFit", "") or "")
    if "高" in hf_text:
        w_hotel = 1.0
    elif "中" in hf_text:
        w_hotel = 0.5
    else:
        w_hotel = 0.0
    
    # === 象徴性スコア ===
    # bizDevNote + nextAction テキストで「象徴」「老舗」「誌名」「朝顔」等のシグナル検出
    biz_text = str(store.get("bizDevNote", "") or "") + str(store.get("nextAction", "") or "")
    symbol_keywords = ["象徴", "老舗", "誌名", "言問団子", "朝顔", "300年", "明治", "大正", "元禄", "約100年"]
    w_symbol = min(1.0, sum(0.3 for kw in symbol_keywords if kw in biz_text))
    
    # === ファネル乗数（L3埋蔵金・高評価L6は他店の広告化を誘発する呼び水）===
    w_funnel = 0.0
    label = store.get("label", "")
    rating = store.get("rating") or {}
    rating_score = float(rating.get("score", 0) or 0) if isinstance(rating, dict) else 0.0
    
    if label == "L3":
        w_funnel = 0.8  # 取材済で関係あり・ファネルの実証済み（要キャリブレーション）
    elif label in ["L6", "L2"] and rating_score >= 3.5:
        w_funnel = 0.4  # 高評価店は誌面ブランドを引き上げる呼び水になる
    elif label in ["L1a", "L1b"] and (store.get("issuesPlaced") or []) and len(store.get("issuesPlaced", [])) >= 4:
        w_funnel = 0.2  # 長期継続客は「言問ブランドの信頼を体現する存在」
    
    # === 重み付き合計（初期重み・要キャリブレーション）===
    # world:0.30 / tegata:0.20 / hotel:0.15 / symbol:0.20 / funnel:0.15
    raw_score = (0.30 * w_world + 0.20 * w_tegata + 0.15 * w_hotel 
                 + 0.20 * w_symbol + 0.15 * w_funnel)
    
    # raw_score ∈ [0.0, 1.0] を [0.5, 2.0] に線形変換
    s_strategic = 0.5 + raw_score * 1.5
    return round(s_strategic, 3)
```

**初期重みの根拠と要キャリブレーション箇所：**
- worldviewFit 0.30 ← 誌面広告は「世界観に合わない広告主は誌面価値を毀損する」という価値観を最重視
- tegataFit 0.20 ← 手形アプリ加盟は既存広告主からクロスセルできる戦略資産
- hotelEastFit 0.15 ← ホテル送客は言問East OTA依存脱却の補完的価値
- symbol 0.20 ← 老舗・誌名連動店の広告化は他店への「社会的証明（Social Proof）」として機能（Cialdini, Influence理論より）
- funnel 0.15 ← 取材→広告化の実証パスにおけるL3の優位性（03_segmentation-report §1.3）

### 2.5 C_action（営業コスト換算）

```python
def calc_c_action(action_type: str, hourly_rate: int = 3000) -> int:
    """
    営業コスト = 工数（h） × 人件費単価
    hourly_rate: ¥3,000/h（パート・契約社員想定。正社員なら¥5,000で置き換え要検討）
    
    初期値・要キャリブレーション
    """
    action_hours = {
        "A01": 1.0, "A02": 2.0, "A03": 3.0, "A04": 1.5,
        "A05": 2.0, "A06": 3.0, "A07": 2.0, "A08": 1.5,
        "A09": 0.5, "A10": 0.0,
    }
    hours = action_hours.get(action_type, 1.0)
    return int(hours * hourly_rate)
```

### 2.6 EV計算と優先度スコアの正規化

```python
def calc_ev(store, action_type: str) -> float:
    """
    EV（円）= P_convert × V_expected × S_strategic - C_action
    """
    p = calc_p_convert(store)
    v = calc_v_expected(store)
    s = calc_s_strategic(store)
    c = calc_c_action(action_type)
    return (p * v * s) - c

def calc_priority_score(store, action_type: str, all_ev_list: list) -> int:
    """
    優先度スコア = EV を 0-100 に正規化
    正規化方式: min-max normalization
    ただし、A10（見送り）はスコア0に固定
    """
    if action_type == "A10":
        return 0
    ev = calc_ev(store, action_type)
    ev_min = min(all_ev_list)
    ev_max = max(all_ev_list)
    if ev_max == ev_min:
        return 50
    normalized = (ev - ev_min) / (ev_max - ev_min)
    return max(0, min(100, int(normalized * 100)))
```

**補助スコア（ICE）：** 営業現場向けの補助指標として ICE スコアも算出する。

```python
def calc_ice_score(store, action_type: str) -> dict:
    """
    ICEスコア（1〜10・要キャリブレーション）
    I（Impact）  = EV を 1-10 スケールに変換（V_expected が大きいほど高い）
    C（Confidence）= P_convert × 10（信頼度）
    E（Ease）    = 10 - (action_hours × 2)（実行容易性。最小1）
    """
    v = calc_v_expected(store)
    p = calc_p_convert(store)
    action_hours = {"A01":1,"A02":2,"A03":3,"A04":1.5,"A05":2,"A06":3,"A07":2,"A08":1.5,"A09":0.5,"A10":0}
    hours = action_hours.get(action_type, 1)
    
    impact     = max(1, min(10, int(v / 40000) + 1))  # ¥400kが最高10、¥3kが1
    confidence = max(1, min(10, int(p * 10)))
    ease       = max(1, min(10, 10 - int(hours * 2)))
    return {
        "I": impact, "C": confidence, "E": ease,
        "ICE": (impact + confidence + ease) / 3
    }
```

---

## 3. 次アクション（NBA）の決定ルール

### 3.1 デシジョンツリー（if-then・スコア閾値）

```python
def determine_nba(store) -> dict:
    """
    各店の Next Best Action を一意に決定する。
    出力: {action_type, action_name, rep_recommended, deadline_days, reason}
    
    優先順位：
    P0 = 今号受注に関わる緊急アクション
    P1 = 高EV・高確度の即効アクション
    P2 = 中期ファネル投資
    P3 = 低優先・見送り
    """
    label = store.get("label", "L6")
    lapsed = store.get("lapsed", False)
    v15 = store.get("vol15") or {}
    v15_status = v15.get("status", "")
    p_convert = calc_p_convert(store)
    v_expected = calc_v_expected(store)
    ev = calc_ev(store, "A01")  # まず維持EVで評価
    tier = store.get("potentialTier", "B")
    
    # === P0: 今号（Vol.15）緊急案件 ===
    if v15_status == "商談中":
        return {
            "action_type": "A06" if v_expected >= 100000 else "A05",
            "priority": "P0",
            "deadline_days": 7,
            "reason": f"Vol.15商談中・今号クローズ必須。想定¥{v_expected:,}。即フォロー。"
        }
    
    # === L1（継続）の分岐 ===
    if label in ["L1a", "L1b", "L1c", "L1d"]:
        if v15_status == "受注":
            # 継続確認済み → アップセル or 手形連携を検討
            tf = store.get("tegataFit") or {}
            tf_level = tf.get("level", "低") if isinstance(tf, dict) else str(tf)
            if tf_level == "高" and store.get("hotelEastFit", "低") in ["高", "中"]:
                return {
                    "action_type": "A08",
                    "priority": "P2",
                    "deadline_days": 60,
                    "reason": "Vol.15受注済み。手形/ホテル連携クロスセルで関係深化。次号まで。"
                }
            elif label in ["L1c", "L1d"] and tier in ["A", "B"]:
                return {
                    "action_type": "A02",
                    "priority": "P1",
                    "deadline_days": 30,
                    "reason": f"名店リスト/記事下の継続中。potentialTier={tier}→上位枠({_next_tier(label)})へのアップセル余地。"
                }
            else:
                return {
                    "action_type": "A01",
                    "priority": "P2",
                    "deadline_days": 90,
                    "reason": "継続受注済み。次号継続確認（90日以内）を実施。"
                }
        else:
            # 継続中だがvol15未着手 → 更新確認が最優先
            return {
                "action_type": "A01",
                "priority": "P0" if label in ["L1a", "L1b"] else "P1",
                "deadline_days": 14 if label in ["L1a", "L1b"] else 30,
                "reason": f"継続広告主({label})だがVol.15未受注。即更新確認必須。"
            }
    
    # === L2（途絶）の分岐 ===
    if label == "L2":
        if v_expected >= 200000:
            # 大型途絶（伊藤忠・パナホーム相当）→ 社長級で復活打診
            return {
                "action_type": "A07",
                "priority": "P0",
                "deadline_days": 14,
                "reason": f"大型途絶（最終¥{store.get('lastSpend',0):,}）。まず離脱理由ヒアリング→復活提案。最優先。"
            }
        else:
            # 中小途絶 → 復活打診
            return {
                "action_type": "A07",
                "priority": "P1",
                "deadline_days": 30,
                "reason": f"途絶（lapsed=True）。最終号:{store.get('lastIssue')}。関係が残るうちに復活打診。"
            }
    
    # === L3（埋蔵金）の分岐 ===
    if label == "L3":
        worldview = str(store.get("worldviewFit", "") or "")
        if "最高" in worldview or "高" in worldview:
            # 世界観適合が高いL3 → 記事連動でブランド枠初収益化
            return {
                "action_type": "A05",
                "priority": "P1",
                "deadline_days": 21,
                "reason": f"L3埋蔵金（取材済・未広告）。世界観適合高。取材連動でブランド枠¥35k〜¥100k初収益化。"
            }
        else:
            # 世界観適合中以下のL3 → 名店リストで入口
            return {
                "action_type": "A04",
                "priority": "P2",
                "deadline_days": 45,
                "reason": "L3埋蔵金。名店リスト¥3kで関係再構築→翌号アップセル。"
            }
    
    # === L4（グループ内）===
    if label == "L4":
        return {
            "action_type": "A01",
            "priority": "P3",
            "deadline_days": 90,
            "reason": "グループ内。社内調整で継続確認。外部収益カウントの是非はCEO判断。"
        }
    
    # === L5（域外大手）===
    if label == "L5":
        if tier == "A" and v15_status != "受注":
            return {
                "action_type": "A06",
                "priority": "P1",
                "deadline_days": 30,
                "reason": f"域外大手potentialTier=A。大型枠提案の余地。法人広報へアプローチ。"
            }
        else:
            return {
                "action_type": "A10",
                "priority": "P3",
                "deadline_days": 999,
                "reason": "域外大手（L5）・世界観適合低・B/C tier。今期は見送り。リソースをL3/L2に集中。"
            }
    
    # === L6（未接触）の分岐 ===
    if label == "L6":
        best100 = store.get("best100Rank")
        worldview = str(store.get("worldviewFit", "") or "")
        worldview_high = ("最高" in worldview or ("高" in worldview and "低" not in worldview))
        tf = store.get("tegataFit") or {}
        tf_level = tf.get("level", "低") if isinstance(tf, dict) else "低"
        
        if tier == "A" or (best100 and int(best100) <= 20):
            # ベスト100上位・A tierの高潜在 → 取材でファネル入口
            return {
                "action_type": "A03",
                "priority": "P1",
                "deadline_days": 30,
                "reason": f"L6高潜在（tier={tier}/best100={best100}）。取材オファーでファネル入口を作る。"
            }
        elif worldview_high and tf_level in ["高", "中"]:
            # 世界観適合高×手形適性ありのB tier → 名店リストで関係開始
            return {
                "action_type": "A04",
                "priority": "P2",
                "deadline_days": 45,
                "reason": "L6（世界観適合高×手形適性あり）。名店リスト¥3kで関係開始。"
            }
        elif tier == "C" or (not worldview_high and tf_level == "低"):
            # 低潜在 → 見送り
            return {
                "action_type": "A10",
                "priority": "P3",
                "deadline_days": 999,
                "reason": "L6低潜在（C tier・世界観低・手形適性低）。今期見送り。"
            }
        else:
            # B tier・普通 → 担当アサイン後に打診
            return {
                "action_type": "A09" if not store.get("salesReps") else "A04",
                "priority": "P2",
                "deadline_days": 60,
                "reason": "L6 B tier。担当アサイン→名店リスト打診。"
            }
    
    # フォールバック
    return {"action_type": "A09", "priority": "P3", "deadline_days": 90, "reason": "ラベル不明・担当アサインから開始。"}


def _next_tier(current_label: str) -> str:
    """アップセル先の枠を示すテキスト"""
    map = {"L1c": "ブランド枠¥60k", "L1d": "1P¥100k", "L1b": "記事広告¥600k", "L1a": "年契化/面積拡大"}
    return map.get(current_label, "上位枠")
```

### 3.2 担当割当ルール

```python
def assign_rep(store, nba: dict) -> str:
    """
    担当割当の優先順位：
    1. vol15.rep が設定済み → その担当者を継続
    2. salesReps に既存担当者がいる → 直近号の担当者
    3. ラベル×カテゴリから推奨担当者を割当
    4. いずれも不明 → A09（担当アサイン）を発動
    
    推奨担当者マッピング（初期値・実態に合わせて要更新）:
    """
    # 1. vol15担当者
    v15 = store.get("vol15") or {}
    if v15.get("rep"):
        return v15["rep"]
    
    # 2. 既存担当者
    reps = store.get("salesReps") or []
    if reps:
        # 最新号を担当した人を優先
        sorted_reps = sorted(
            reps, 
            key=lambda r: max(r.get("issues") or [0]), 
            reverse=True
        )
        return sorted_reps[0]["rep"]
    
    # 3. カテゴリ・エリア別のデフォルト担当マッピング（要実態確認・仮置き）
    category = store.get("category", "")
    ward = (store.get("area") or {}).get("ward", "")
    label = store.get("label", "L6")
    
    # 大型案件（L1a・L2大型・L5大型）→ 社長（大田）または常務（石井）
    if label in ["L1a"] or (label == "L2" and calc_v_expected(store) >= 200000):
        return "大田（社長）"
    
    # 飲食・根岸エリア → 浅川
    if "飲食" in category and ("根岸" in str(ward) or "台東" in str(ward)):
        return "浅川"
    
    # 不動産・金融 → 石井 or 金井
    if "不動産" in category or "金融" in category:
        return "石井"
    
    # デフォルト → 担当未定（A09発動）
    return "未割当（A09要対応）"
```

### 3.3 期限の決め方

期限の基準（deadline_days）の設計思想：
- P0（7〜14日）：今号クローズに関わる緊急案件。逃すと号の機会損失が確定する
- P1（21〜30日）：高EV案件。Vol.16締切から逆算して最低1ヶ月前に着手する
- P2（45〜60日）：中期ファネル投資。次号（Vol.17）に向けた仕込み
- P3（90〜999日）：低優先・見送り。リソースが空いた時のみ着手

期限の絶対日付への変換：`deadline_date = 実行日 + deadline_days`（スクリプトが自動計算）

---

## 4. 出力フォーマット（各店のNBA出力仕様）

```json
{
  "storeId": "MTN-050",
  "name": "入谷鬼子母神門前 のだや",
  "label": "L3",
  "nba": {
    "actionType": "A05",
    "actionName": "ブランド枠初回提案",
    "priority": "P1",
    "priorityScore": 87,
    "ev": 28750,
    "ev_breakdown": {
      "p_convert": 0.31,
      "v_expected": 60000,
      "s_strategic": 1.70,
      "c_action": 6000
    },
    "ice": {"I": 2, "C": 3, "E": 7, "ICE": 4.0},
    "repRecommended": "阿部（編集長）",
    "deadlineDays": 21,
    "deadlineDate": "2026-06-27",
    "reason": "L3埋蔵金（取材済・未広告）。世界観適合最高（朝顔市号の本丸）。取材連動でブランド枠¥35k〜¥100k初収益化。",
    "confidence": "推計（P_convert初期値）",
    "generatedAt": "2026-06-06T00:00:00Z"
  }
}
```

---

## 5. 全202店への一括計算スクリプト仕様

### 5.1 入力フィールド → 出力フィールドの対応表

| 入力フィールド | 使用箇所 | 出力フィールド |
|---|---|---|
| `label` | P_convert基底確率・決定ルールの分岐点 | `nba.actionType` / `nba.priority` |
| `vol15.status` / `vol15.amount` | P0判定・V_expected優先値 | `nba.ev` / `nba.deadlineDays` |
| `lapsed` | P_convert補正2 | `nba.ev` |
| `issuesPlaced` | P_convert補正3（接点履歴の長さ） | `nba.ev_breakdown.p_convert` |
| `potentialTier` | P_convert補正4・V_expected・決定ルール | `nba.priorityScore` |
| `lastSpend` | V_expected優先値2 | `nba.ev_breakdown.v_expected` |
| `worldviewFit` | S_strategic（W_world）・決定ルールL3/L6分岐 | `nba.ev_breakdown.s_strategic` |
| `tegataFit.level` | S_strategic（W_tegata）・決定ルールL6分岐 | `nba.ev_breakdown.s_strategic` |
| `hotelEastFit` | S_strategic（W_hotel）・A08判定 | `nba.ev_breakdown.s_strategic` |
| `bizDevNote` / `nextAction` | S_strategic（W_symbol）・象徴性シグナル | `nba.ev_breakdown.s_strategic` |
| `best100Rank` | L6決定ルール（高潜在判定） | `nba.actionType` |
| `salesReps` | 担当割当・A09発動判定 | `nba.repRecommended` |
| `category` | 担当デフォルトマッピング | `nba.repRecommended` |
| `area.ward` | 担当デフォルトマッピング | `nba.repRecommended` |
| `rating.score` | S_strategic（W_funnel）・L6高評価判定 | `nba.ev_breakdown.s_strategic` |

### 5.2 メイン処理の擬似コード

```python
#!/usr/bin/env python3
"""
nba_engine.py — 名店DB Next Best Action エンジン
実行: python nba_engine.py [--output nba_output.json] [--as-of YYYY-MM-DD]
依存: meiten-master.json（入力）, nba_output.json（出力）
"""
import json
from datetime import datetime, timedelta

def run_nba_engine(master_path: str, output_path: str, as_of: datetime = None):
    """
    1. meiten-master.json を読み込む
    2. 各店に対して:
       a. NBA を決定（determine_nba）
       b. EV を計算（calc_ev）
       c. 担当を割当（assign_rep）
       d. 期限日を計算（as_of + deadline_days）
    3. 全店のEVリストを使って 0-100 優先度スコアを正規化
    4. nba_output.json に書き出す（meiten-master.jsonには書き込まない）
    5. 実行ログを nba_engine.jsonl に追記（GR13準拠）
    """
    if as_of is None:
        as_of = datetime.now()
    
    with open(master_path, encoding="utf-8") as f:
        master = json.load(f)
    stores = master["stores"]
    
    # --- Pass 1: NBA・EVを仮計算 ---
    results = []
    ev_list = []
    for store in stores:
        nba = determine_nba(store)
        ev = calc_ev(store, nba["action_type"])
        ice = calc_ice_score(store, nba["action_type"])
        rep = assign_rep(store, nba)
        deadline_date = (as_of + timedelta(days=nba["deadline_days"])).strftime("%Y-%m-%d")
        ev_list.append(ev)
        results.append({
            "store": store,
            "nba_raw": nba,
            "ev": ev,
            "ice": ice,
            "rep": rep,
            "deadline_date": deadline_date,
        })
    
    # --- Pass 2: 優先度スコア正規化（全EVを使って0-100化）---
    for r in results:
        r["priority_score"] = calc_priority_score(r["store"], r["nba_raw"]["action_type"], ev_list)
    
    # --- 出力JSONを組立 ---
    ACTION_NAMES = {
        "A01": "年契更新・継続確認", "A02": "アップセル提案",
        "A03": "取材オファー", "A04": "名店リスト勧誘",
        "A05": "ブランド枠初回提案", "A06": "大型枠・記事広告提案",
        "A07": "途絶復活打診", "A08": "手形/ホテル連携提案",
        "A09": "担当アサイン", "A10": "休眠・見送り",
    }
    output_stores = []
    for r in results:
        store = r["store"]
        nba = r["nba_raw"]
        output_stores.append({
            "storeId": store["storeId"],
            "name": store["name"],
            "label": store["label"],
            "potentialTier": store.get("potentialTier"),
            "nba": {
                "actionType": nba["action_type"],
                "actionName": ACTION_NAMES.get(nba["action_type"], ""),
                "priority": nba["priority"],
                "priorityScore": r["priority_score"],
                "ev": int(r["ev"]),
                "ev_breakdown": {
                    "p_convert": round(calc_p_convert(store), 3),
                    "v_expected": calc_v_expected(store),
                    "s_strategic": round(calc_s_strategic(store), 3),
                    "c_action": calc_c_action(nba["action_type"]),
                },
                "ice": r["ice"],
                "repRecommended": r["rep"],
                "deadlineDays": nba["deadline_days"],
                "deadlineDate": r["deadline_date"],
                "reason": nba["reason"],
                "confidence": "推計（初期モデルv1.0・要キャリブレーション）",
                "generatedAt": as_of.isoformat(),
            }
        })
    
    # --- ソート：priority（P0>P1>P2>P3）→ priorityScore 降順 ---
    priority_order = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
    output_stores.sort(
        key=lambda x: (
            priority_order.get(x["nba"]["priority"], 9),
            -x["nba"]["priorityScore"]
        )
    )
    
    # --- 出力 ---
    output = {
        "_meta": {
            "title": "名店DB Next Best Action レポート",
            "generated_at": as_of.isoformat(),
            "model_version": "1.0.0",
            "total_stores": len(output_stores),
            "note": "係数は初期値・要キャリブレーション。P_convert/V_expectedは実績でアップデートすること。"
        },
        "stores": output_stores,
    }
    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(output, f, ensure_ascii=False, indent=2)
    
    # --- ログ（GR13）---
    log_record = {
        "ts": as_of.isoformat(),
        "status": "success",
        "total": len(output_stores),
        "p0_count": sum(1 for s in output_stores if s["nba"]["priority"] == "P0"),
        "p1_count": sum(1 for s in output_stores if s["nba"]["priority"] == "P1"),
        "p2_count": sum(1 for s in output_stores if s["nba"]["priority"] == "P2"),
        "p3_count": sum(1 for s in output_stores if s["nba"]["priority"] == "P3"),
        "output_path": output_path,
    }
    with open("logs/nba_engine.jsonl", "a", encoding="utf-8") as f:
        f.write(json.dumps(log_record, ensure_ascii=False) + "\n")
    
    print(f"[NBA Engine] {len(output_stores)} stores processed. P0={log_record['p0_count']} P1={log_record['p1_count']} P2={log_record['p2_count']} P3={log_record['p3_count']}")
    return output
```

### 5.3 期待される出力サマリ（初期モデルの推計・要実行確認）

初期モデルを202店に適用した場合の推計分布（要キャリブレーション）：

| Priority | 推計店舗数 | 主な内訳 |
|---|---|---|
| P0 | 約7〜10社 | 商談中7社（vol15）+ 継続大型で未受注の緊急案件 |
| P1 | 約40〜50社 | L2途絶8社 + L3埋蔵金8社 + L1継続未受注 + L6潜在A/B |
| P2 | 約80〜100社 | L1継続受注済のクロスセル + L6 B tier + L4グループ内 |
| P3 | 約60〜80社 | L5域外低ポテンシャル + L6 C tier + A10見送り |

---

## 6. 測定・判定（PDCAの設計）

### 6.1 先行指標（LAG→LEAD KPIへの変換）

このエンジンの「正しさ」を検証するために計測すべき指標：

| KPI名 | 定義 | 計測方法 | 更新頻度 |
|---|---|---|---|
| **取材→広告化転換率** | 取材（A03実施）後2号以内に有料化した比率 | visit_logs + vol15.status | 号別 |
| **途絶復活率** | A07実施後の再受注率（L2のうち受注に至った割合） | vol15.status変化 | 号別 |
| **名店リスト→アップグレード率** | L1c→L1b/L1d/L1aに昇格した比率（号数ラグ込み） | label変化 | 半期 |
| **担当アサイン解消率** | A09が発動した店のうち実際に担当が決まった割合 | salesReps有無 | 月次 |
| **優先度スコア vs 実受注額の相関** | priorityScore上位50社の実受注額/下位50社の実受注額 | vol実績 | 号別 |
| **P_convert予測精度** | ラベル別の予測P_convert vs 実際の転換率の差 | 実績受注集計 | 半期 |

### 6.2 ベースライン（現状）

| KPI | 現状ベースライン | 出典 |
|---|---|---|
| 取材→広告化転換率 | 4/4 = **100%**（Vol.8コホート・4社が揃って転換） | 03_segmentation-report §1.3 |
| 途絶復活率 | 不明（L2復活の実績データ未収録） | 要計測 |
| 名店リスト→アップグレード率 | チーズ谷・清浄無垢・ROSSONERO等 実績あり（比率未計算） | 03_segmentation §1.2 |
| パイプライン額（Vol.15時点） | 受注46社（額不明全体）+ 商談中7社 | meiten-master.json |
| L3埋蔵金の収益化率 | 0%（8社中0社が有料化） | 03_segmentation §2 |

### 6.3 目標値・判定期限

| KPI | 目標値 | 判定期限 | 判定方法 |
|---|---|---|---|
| L3埋蔵金 → 有料化率 | 3/8社（37.5%）以上 | Vol.16締切（約6ヶ月後） | vol16.status確認 |
| L2途絶復活率 | 2/8社（25%）以上 | Vol.16締切 | vol16.status確認 |
| 優先度スコア vs 実受注 相関 | Pearson r ≥ 0.40 | Vol.16完了後 | EV計算vs実績の相関係数 |
| P0案件（商談中7社）クローズ率 | 7/7 = 100% | Vol.15締切（2週間以内） | vol15.status「受注」への変化 |
| L6（best100上位20社）からの新規受注 | 3社以上 | Vol.17締切（1年後） | label変化（L6→L1/受注） |

### 6.4 A/B比較設計（モデルの優位性検証）

エンジン適用の前後比較（before/after comparison）：

**Before（コントロール群）：** エンジン導入前の定性的nextActionテキストを基に営業した結果（Vol.14〜Vol.15実績）
**After（介入群）：** 本エンジンのpriorityScore上位50社に対して優先的に営業活動を行った結果（Vol.16〜Vol.17）

比較指標：
- Before期間：Vol.14→Vol.15の受注件数・合計額
- After期間：Vol.15→Vol.16の受注件数・合計額

**注意：** 単純前後比較は「季節性・営業チームの動き」が混入するため、精度は限定的。
可能であれば「エンジン未使用担当者の担当店（コントロール群）vs 使用担当者（介入群）」のRCT的設計が理想だが、営業チームが少ないため現実的でない。前後比較で代替する。

### 6.5 撤退・見直し条件

**エンジンの見直し条件（いずれか1つ以上で即見直し）：**

| 条件 | 閾値 | 対応 |
|---|---|---|
| P_convert予測精度 | 各ラベルの実転換率とモデル値の乖離が±15%p以上 | 該当ラベルの基底確率を実績値で更新 |
| 優先度スコアvs実受注相関 | r < 0.20（相関なし判定） | スコア構成式・重みの全面見直し |
| 担当割当マッピングの不一致 | 推奨担当vs実際の担当の一致率 < 50% | assign_repの担当マッピングを実態に更新 |
| フィールド変化 | meiten-master.jsonにフィールド追加/削除があった場合 | calc_s_strategic・determine_nbaのロジック更新 |
| モデル自体の廃棄条件 | Vol.16・Vol.17の2号連続で、P0/P1案件の受注率がランダム期待値（P1/P2/P3均等仮定）を下回る | エンジン設計の根本見直し |

---

## 7. 実装着手チェックリスト（CTO/Codex向け）

### 7.1 実装スコープ（本書で確定）

- [ ] `scripts/nba_engine.py`：本設計書の擬似コードをフル実装
  - calc_p_convert / calc_v_expected / calc_s_strategic / calc_c_action / calc_ev
  - calc_priority_score / calc_ice_score
  - determine_nba / assign_rep
  - run_nba_engine（メインフロー）
- [ ] 出力ファイル：`data/nba_output.json`（meiten-master.jsonには書かない）
- [ ] ログファイル：`logs/nba_engine.jsonl`（GR13準拠）
- [ ] `--dry-run`オプション：ファイル書き込みなし・コンソール出力のみ
- [ ] `--as-of YYYY-MM-DD`オプション：指定日基点でdeadline_dateを計算
- [ ] `--store-id MTN-XXX`オプション：単店デバッグ用

### 7.2 テスト仕様

```
テスト1: MTN-001（日鉄興和不動産・L1a・受注済）
  期待NBA: A01またはA08。priority ≤ P2。ev > 0。
  
テスト2: MTN-040（伊藤忠都市開発・L2・V=400000）
  期待NBA: A07。priority = P0。ev ≥ 80000。

テスト3: MTN-050（のだや・L3・世界観最高）
  期待NBA: A05。priority = P1。s_strategic ≥ 1.5。

テスト4: L6・potentialTier=C・worldviewFit=低
  期待NBA: A10。priority = P3。priorityScore = 0。

テスト5: vol15.status="商談中"の全7社
  期待NBA: A05またはA06。priority = P0。deadline_days ≤ 7。

テスト6: 全202店でのバリデーション
  期待: priorityScore の分布が 0〜100 に広がっている（極端な偏りがない）
  期待: P0が5〜15社・P3が50〜100社の範囲
```

### 7.3 実装上の注意

- `worldviewFit`は自由記述テキストのため文字列マッチングで処理（§2.4参照）。例外的な表記（「低（域外不動産・下町/ことば文脈と薄い）」等）に注意
- `tegataFit`は`{"level": "高", "reason": "..."}`形式と文字列形式が混在している可能性あり（事前確認要）
- `vol15`がnullの店が多数存在（146社）。`None`ガード必須
- `issuesPlaced`が空配列または`null`の店あり。`len(store.get("issuesPlaced") or [])` で安全に扱う
- `rating.score`は文字列（"3.17"）で格納されているため`float()`変換が必要。変換失敗は0.0でフォールバック

---

## 8. CEOが決めるべきパラメータ（3〜6点に絞って提示）

本エンジンは初期値で動くが、以下の6点は**CEOの戦略判断によって変わる定数**であり、スクリプトの外で決定する必要がある。

### P1（最優先）: S_strategicの重み配分

```python
# 現在の初期値（要CEO確認）
W_WORLD  = 0.30  # 世界観適合度の重み
W_TEGATA = 0.20  # 手形アプリ適性の重み
W_HOTEL  = 0.15  # ホテル送客親和の重み
W_SYMBOL = 0.20  # 象徴性（老舗/誌名連動）の重み
W_FUNNEL = 0.15  # ファネル乗数（L3/高評価店）の重み
```

**CEOが決める問い：**「言問散歩の次の5年で何を最大化するか？
- 収益最大化 → W_WORLD下げ・W_FUNNEL上げで大型枠に集中する設定
- 誌面ブランド価値最大化 → W_SYMBOL・W_WORLD上げで象徴店と老舗を優先
- 手形アプリの加盟店拡大 → W_TEGATA上げで来街型飲食をP1に引き上げる

### P2（重要）: 許容CAC（最大営業コスト許容額）

```python
HOURLY_RATE = 3000  # 現在の人件費単価（¥/h）
# これが上がると低単価案件（L1c名店リスト¥3k・L6ブランド枠¥35k）が
# EV<0になりA10（見送り）に流れる閾値が変わる。
```

**CEOが決める問い：**「¥3,000の名店リスト枠に2時間の商談工数をかけることを許容するか？」
- 許容するなら：hourly_rate ≤ ¥1,500（ファネル投資として割り切る）
- 許容しないなら：hourly_rate ≥ ¥3,000（名店リストをA10にしてブランド枠以上に集中）

### P3（中程度）: L6の打診量（潜在121社のうち何社を今号ターゲットにするか）

```python
# 現在：best100上位20社をP1（取材オファー）、他は世界観次第でP2/P3に振り分け
L6_ACTIVE_THRESHOLD = 20  # best100rankの上位N社をP1に入れる閾値
# これを10に絞るか50に広げるかで営業チームの負荷が大きく変わる
```

**CEOが決める問い：**「Vol.16に向けて新規L6から何社に本格アプローチするか？」
営業チームの実態工数（9名×週稼働時間）と比較して決定。

### P4（中程度）: L4グループ内の扱い

```python
# 現在：A01（継続確認・P3）に設定。EV算出の対象外。
# L4（言問East・花重・言問茶屋）を「外部収益」としてカウントするか否かで
# 事業計画の損益が¥2.4M変わる（03_segmentation §1）
L4_COUNT_AS_REVENUE = False  # True/Falseをどちらに設定するか
```

**CEOが決める問い：**「グループ内売上¥2.4Mは広告事業の収益に含めるか？」
（含める場合：A01維持が最優先・A02アップセルも意義あり / 含めない場合：L4は営業リソース対象外）

### P5（中程度）: S_strategicの象徴性スコアのキーワード

```python
SYMBOL_KEYWORDS = ["象徴", "老舗", "誌名", "言問団子", "朝顔", "300年", "明治", "大正", "元禄", "約100年"]
# → 上記に登場するかどうかで象徴性スコアが変わる
# 10周年号・朝顔市号という特定文脈キーワードも追加すべきか？
```

**CEOが決める問い：**「今号（朝顔市・10周年号）ならではの象徴性評価を追加するか？」
（追加するなら：「朝顔」「10周年」「Vol.15」「入谷」等を加え、今号限定の戦略係数を持たせる）

### P6（中程度）: モデルのキャリブレーションサイクル

現在の初期値は「仮置き・要キャリブレーション」だが、何号後に更新するかを決める必要がある。

**CEOが決める問い：**「Vol.16確定後（約3ヶ月後）に実績でモデルを更新するか、Vol.17完了後（約6ヶ月後）に年次更新するか？」

推奨：Vol.16完了後に最初のキャリブレーション実施（3ヶ月後）。特に「L3転換率・L2復活率・L6新規CVR」の3指標を実測値で更新する。

---

## 付録A：ファイル構成（実装後の想定）

```
meiten-business/
├── data/
│   ├── meiten-master.json        ← 社内正本（入力。直接書き込まない）
│   └── nba_output.json          ← NBA計算結果（本エンジンの出力・毎号更新）
├── scripts/
│   └── nba_engine.py            ← 本設計書の実装（Codexが作成）
├── logs/
│   └── nba_engine.jsonl         ← 実行ログ（GR13準拠）
├── 12_next-action-engine.md     ← 本設計書（正本）
└── tests/
    └── test_nba_engine.py       ← テスト（§7.2準拠）
```

## 付録B：本設計の学術的・実務的根拠

- **EV（期待価値）アプローチ：** Kotler & Keller, Marketing Management 15th ed. Part IV「Communicating Value」Chapter 19のリードスコアリング理論を援用。
- **RFM分析との比較：** 本モデルはRFM（Recency/Frequency/Monetary）の「R=lastIssue/lapsed」「F=issuesPlacedの長さ」「M=spendKnownTotal」を P_convert・V_expected に内包している。RFMと独立させなかったのは「新規（L6）と既存（L1）を同一軸で扱えない」ためで、ラベル別基底確率がその代替。
- **ICEスコアの位置づけ：** ICE（Impact × Confidence × Ease）はIntercom社が提唱したプロダクト優先度フレームワークを営業アクション評価に転用。EVとの相違は「Ease（実行容易性）」を独立軸にするか・EVの引き算（-C_action）にするかの設計選択。本モデルは両者を算出し、営業現場にはICEを、経営層にはEVを提示する二層設計を採る。
- **社会的証明（Social Proof）：** Cialdini, Influence (2021) Chapter 3。笹乃雪・のだや等の象徴老舗が広告出稿することで他の老舗（L3/L6）の「うちも出してみようか」という意思決定を誘発する効果を、W_symbolとW_funnelで定量化した。この非線形効果は純粋なEVに現れないため戦略係数として補正する設計。

---

> 本設計書の変更は CMO（マーケティング大臣）が起案し CEO が承認すること。  
> 初回実装：Codex（夜勤製造ライン）が `scripts/nba_engine.py` を実装。  
> 初回キャリブレーション期限：Vol.16完了後（推計 2026年9月〜10月）。  
> 撤退条件：§6.5 参照。
