# セッション引継ぎ — 期待値予想 `ev_new`（2026-05-26 版）

**別 Window / 別 Cursor に引き継ぐときは、このファイルを最初に読むこと。**

**→ 実務手順・データ残存確認は [`session-handoff-ev-new-window.md`](session-handoff-ev-new-window.md) を先に読むとよい。**

---

## 0. スコープ（重要）

| 対象 | 説明 |
|------|------|
| **この資料の正** | **2026-05-26** に作った **`ev_new`（期待値 New）** の予想仕様 |
| **対象外（読まなくてよい）** | **6月以降**の話 — `prob_odds` 化、`base_forecast` / `ev_forecast` 再設計、133 で走らせた 6/15 再予想 など |

「一番良かった」のは **◎を期待値(New)の降順で付け替える `ev_new` 一覧** の成績（◎1着 **12%台**・単勝回収 **73%** 前後）。

---

## 1. 当時のユーザー要望（2026-05-26 11:13）

> 勝率(New) に対して単勝オッズを掛けて、**期待値が一番良い馬**を見つけたい。期待値指数を出してほしい。

これを満たすのが **`profile_key = ev_new`** の予想結果一覧。

---

## 2. 予想ロジック（`ev_new`）

### 2.1 処理の流れ

```
① predictions から各馬のモデルスコアを読む
   （prediction_type = stage1_screening_accuracy, win_probability 列）
      ↓ softmax（温度 T=10）
② 勝率(New) win_prob_new_pct … レース内合計 100%
      ↓ × 単勝オッズ
③ 期待値(New) ev_new = (勝率% ÷ 100) × オッズ
      ↓ 降順（同値なら勝率(New) も降順）
④ ◎本命・〇対抗・▲単穴・△連下
```

### 2.2 堅実順（`p6_latest`）との関係

| 項目 | `p6_latest`（従来・予想結果） | `ev_new`（期待値 New） |
|------|------------------------------|------------------------|
| 材料 | Stage1〜3 パイプライン | **同じ predictions** を再利用 |
| 勝率 | note 内の推定勝率等 | **softmax で 100% 正規化**した「勝率(New)」 |
| ◎の決め方 | **堅実順位 1 位**（`rank_accuracy`） | **期待値(New) 1 位** |
| 画面 | `all_race_compare_profiles.html` | `all_race_compare_profiles_new.html` |

**印の付け方だけが違う。** 集計 KPI（単勝・馬連回収）は同じ `all_race_ai_profile_compare` テーブル・同じ PHP 集計 API。

### 2.3 単勝オッズの優先順位

1. `odds_lines`（`odds_type = 'tansho'`）の `max_odds`（なければ `min_odds`）
2. 無ければ `predictions.expected_odds` / `note.win_odds`

### 2.4 数式（実装の正）

```python
# ai/win_prob_ev_new.py

# 勝率(New): モデルスコア s_i に softmax（T=10）
z_i = (s_i - max(s)) / 10
w_i = exp(z_i)
win_prob_new_pct_i = 100 * w_i / sum(w)   # 端数は最後の1頭で調整

# 期待値(New)
ev_new_i = (win_prob_new_pct_i / 100) * tansho_odds_i
```

### 2.5 取込バッチの母集団

- **結果確定済み**（`race_results.finish_position = 1` あり）
- **`predictions`（stage1_screening_accuracy）あり**
- 後期版スクリプトは **`p6_latest` と同じ母集団**（`all_race_ai_profile_compare` の p6 行）も選択可能
- predictions が無いレースは **`run_stage1`（p6_latest 相当）をその場実行**してから ev_new を計算（`--write-predictions`）

---

## 3. ファイル一覧（2026-06 時点で git から消えている）

**別 Window ではまず存在確認。** 無ければ transcript から復元する。

| 役割 | パス |
|------|------|
| 計算ロジック | `ai/win_prob_ev_new.py` |
| 取込バッチ | `scripts/import_all_race_ev_new_compare.py` |
| 一覧 HTML | `public/all_race_compare_profiles_new.html` |
| 集計 API 説明文 | `public/api/all_race_compare_profiles.php` の `PROFILE_META['ev_new']` |
| レース詳細（参考） | `public/api/race_detail.php` の `win_prob_new_pct` / 期待値(New) 列 |
| レース詳細 UI | `public/JRADB/accessS.html` の「勝率(New)」「期待値(New)」列 |

### 復元元

- Agent transcript: `541544e7-0691-4145-af5c-d09a4920c978`（2026-05-26 付近の `Write` ツール）
- **`win_prob_ev_new.py` 全文** → 本資料 **付録 A**
- import / HTML は transcript の `Write` から復元（行数多いのでファイルごとコピー）

---

## 4. DB・画面

| 項目 | 値 |
|------|-----|
| テーブル | `all_race_ai_profile_compare` |
| profile_key | **`ev_new`** |
| 1行 | レースごとに ◎〜△・期待値・的中フラグ等を JSON スナップショット |
| 一覧 URL（例） | `public/all_race_compare_profiles_new.html` |
| 集計 JSON | `public/api/all_race_compare_profiles.php?profile_key=ev_new&summary=1` |

### 133 DB の件数（2026-06-15 確認）

```
ev_new     : 5,166 件
p6_latest  : 5,166 件
ev_forecast: 5,166 件（6月以降の別設計。本資料の対象外）
```

**163（新本番）** はレース・オッズが 22k 規模。**163 で ev_new 取込を全件やり直す**のが本番復元の正。

---

## 5. 環境（固定）

| 項目 | 値 |
|------|-----|
| **新本番（やる場所）** | `163.44.117.9` … `/var/www/html/tools.kachiumaai.com` |
| 旧コードサーバー | `133.130.90.66` … `/var/www/html/ai-horse-race.maspis.com` |
| Windows JV | `C:\Users\oshim\jrvltsql` |
| MySQL | `keiba_ai`（**133 と 163 は別 DB**） |
| DNS 未設定時 | `http://163.44.117.9/...` で確認 |
| モデル（取込時） | `ai/models/stage1_lgbm_enhanced.txt` + `.meta.json` |

### オッズ（2020〜）

163 には単勝・馬連オッズ取込済み（約 22,239 レース）。手順: `docs/ops_odds_2020_tansho_umaren.md`

---

## 6. 別 Window でやること（1 STEP ずつ）

### STEP A — 163 に SSH 接続

```text
Cursor Remote SSH → root@163.44.117.9
ワークスペース → /var/www/html/tools.kachiumaai.com
```

**133 ではなく 163 を開く。**

### STEP B — `ev_new` 用ファイルを置く

133 から scp（パスワード入力あり）:

```bat
scp root@133.130.90.66:/var/www/html/ai-horse-race.maspis.com/ai/win_prob_ev_new.py root@163.44.117.9:/var/www/html/tools.kachiumaai.com/ai/
scp root@133.130.90.66:/var/www/html/ai-horse-race.maspis.com/scripts/import_all_race_ev_new_compare.py root@163.44.117.9:/var/www/html/tools.kachiumaai.com/scripts/
scp root@133.130.90.66:/var/www/html/ai-horse-race.maspis.com/public/all_race_compare_profiles_new.html root@163.44.117.9:/var/www/html/tools.kachiumaai.com/public/
```

**133 にもファイルが無い場合:** 付録 A の Python を保存し、transcript から import / HTML を復元。

確認:

```bash
ls -la ai/win_prob_ev_new.py scripts/import_all_race_ev_new_compare.py public/all_race_compare_profiles_new.html
```

### STEP C — 163 で ev_new 全件取込

```bash
cd /var/www/html/tools.kachiumaai.com
mkdir -p logs

nohup python3 scripts/import_all_race_ev_new_compare.py \
  --from-date 2020-01-01 \
  --to-date 2026-12-31 \
  >> logs/import_ev_new_$(date +%Y%m%d_%H%M%S).log 2>&1 &

echo PID=$!
tail -f logs/import_ev_new_*.log
```

**成功の目安:** ログ末尾 `done ok=22000前後 err=0`（オッズ・predictions が揃ったレース数による）

predictions が足りない場合はスクリプトに `--write-predictions` および `--model-path` / `--model-meta-path` を付ける（スクリプト後期版のオプション）。

### STEP D — 成績確認（2026 年フィルタ）

MySQL で `profile_key = ev_new` を集計:

```bash
mysql keiba_ai -e "
SELECT COUNT(*) n,
  SUM(match_honmei_winner=1) hits,
  ROUND(100.0*SUM(match_honmei_winner=1)/COUNT(*),2) honmei_pct
FROM all_race_ai_profile_compare
WHERE profile_key='ev_new' AND status='ok'
  AND YEAR(race_date)=2026 AND winner_umaban IS NOT NULL;
"
```

**当時の目安（2026 年）:**

| 指標 | ev_new（良かった方） |
|------|---------------------|
| ◎1着 的中率 | **約 12.3%** |
| ◎単勝 回収率 | **約 73%** |
| 馬連 ◎-〇 的中 | **48 / 1193** 程度 |

`p6_latest`（6月 `prob_odds` 再取込後）と比べると ◎1着 **1.3%** まで落ちる。**混同しないこと。**

### STEP E — 画面確認

ブラウザ（強制再読み込み）:

```text
http://163.44.117.9/public/all_race_compare_profiles_new.html
```

タブ「予想結果（New）」相当。`PROFILE_KEY` が **`ev_new`** であること。

---

## 7. やってはいけないこと

- **`prob_odds` で ◎を決める**（6/2 頃の p6 変更。本資料の「良かった」状態ではない）
- **133 の DB を 163 にリストア**（163 の方がレース・オッズが多い）
- **`ev_forecast` / `base_forecast` と混同**（6月中旬以降の別設計。`docs/session-handoff-ev-forecast-20260615.md` は **読まなくてよい**）
- 133 で再予想して終わる（本番は 163）

---

## 8. 関連資料（参考）

| ファイル | 内容 |
|----------|------|
| **本ファイル** | ev_new 引継ぎの正 |
| `docs/ops_odds_2020_tansho_umaren.md` | 2020〜 単勝・馬連オッズ取込 |
| `docs/session-handoff-ev-forecast-20260615.md` | **6月以降**の base/ev 設計（本タスクとは別） |
| `docs/spec/06_forecast_base_and_ev.md` | 同上（6月設計） |

---

## 9. スクリーンショットの数字が出るまでの全処理（5/26 頃）

添付スクショ（「予想結果 (New)」・2026年・**1194件**）は、次の **3層** で出ている。

```
[バッチ] import_all_race_ev_new_compare.py
    → DB all_race_ai_profile_compare (profile_key=ev_new)
        → [API] all_race_compare_profiles.php?summary=1&profile_key=ev_new
            → [画面] all_race_compare_profiles_new.html
```

### 9.1 レイヤー1 — 取込バッチ（印を決める）

**コマンド（当時・全件）:**

```bash
cd /var/www/html/ai-horse-race.maspis.com
python3 scripts/import_all_race_ev_new_compare.py
# 後期版: predictions 不足時は stage1(p6_latest) をその場実行
```

**1レースあたりの処理（`_process_race`）:**

| # | 処理 | 詳細 |
|---|------|------|
| 1 | 対象選定 | 結果確定済み ＋ `predictions`（`stage1_screening_accuracy`）あり。既定は **`p6_latest` と同じ母集団** |
| 2 | 材料読込 | `predictions.win_probability` を **モデルスコア**、`odds_lines`（tansho）を単勝オッズとして各馬を読む |
| 3 | 勝率(New) | `win_prob_ev_new.py` … softmax（**温度10**）でレース内 **合計100%** |
| 4 | 期待値(New) | `ev_new = (勝率% ÷ 100) × 単勝オッズ` |
| 5 | ◎〜△ | `marks_by_ev_new()` … **期待値(New)の降順**（同値なら勝率(New)も降順） |
| 6 | DB保存 | `all_race_ai_profile_compare` に `profile_key='ev_new'` で UPSERT |

**保存される主な列:**

- `ai_honmei_umaban` 〜 `ai_renka_umaban` … ◎〜△の馬番
- `ai_honmei_score` 等 … 各印の **期待値(New)**（スコア列に EV を入れる）
- `match_honmei_winner` … ◎が1着か（0/1）
- `winner_umaban` … 実際の1着馬番
- `ai_snapshot_json` … `marks` 配列（`ev_new`, `win_prob_new_pct`, `odds` 付き）

**スナップショット例（2026/1/4 中山）:**

```json
"marks": [
  {"mark":"◎本命","horse_number":11,"ev_new":5.3037,"win_prob_new_pct":74.7,"odds":7.1},
  {"mark":"〇対抗","horse_number":16,"ev_new":1.2441,...},
  ...
]
```

※ **堅実順の p6_latest とは ◎が違う**（同レースで p6 は別馬が ◎ になりうる）。

### 9.2 レイヤー2 — 集計 API（KPI を数える）

**画面が叩く URL:**

```text
api/all_race_compare_profiles.php?summary=1&profile_key=ev_new&from_date=2026-01-01&to_date=2026-12-31
```

**単勝（上段2枚のカード）**

| KPI | 定義 |
|-----|------|
| ◎1着的中率 12.3% | `match_honmei_winner=1` の割合（2026年・1194レース） |
| ◎単勝回収率 73.0% | 各レース **◎に100円** → `payouts`（`bet_type=tansho`）の払戻合計 ÷ 投資合計 |

**馬連（下段・100円/点）**

◎〜△の4頭（`ai_honmei_umaban` 〜 `ai_renka_umaban`）と、実際の1・2着馬番・`payouts`（`bet_type=umaren`）を突合。

| 買い目 | 投資/レース | スクショの数字（2026年） |
|--------|-------------|-------------------------|
| ◎-〇 | 100円 | 回収 **69.2%**、的中 **48/1193** |
| ◎-▲ | 100円 | 回収 **557.2%**、的中 **32/1193** |
| ◎-△ | 100円 | 回収 **329.8%**、的中 **31/1193** |
| **3点合計** | **300円**（◎-〇・◎-▲・◎-△） | 回収 **318.8%**、レース的中 **111/1193** |

3点合計は「3点のいずれかが当たったレース」で払戻を加算（1レースあたり投資300円想定）。

### 9.3 レイヤー3 — 画面 HTML

| 項目 | 値 |
|------|-----|
| ファイル | `public/all_race_compare_profiles_new.html` |
| タイトル | **予想結果 (New)** |
| JS定数 | `const PROFILE_KEY = 'ev_new';` |
| 年フィルタ | `from_date` / `to_date` を API に渡す（2026年 → 1194件） |
| 表の列 | ◎(期待値順)・〇・▲・△・**◎期待値**・1着・判定 |
| リンク | 「従来の予想結果（堅実順）」→ `all_race_compare_profiles.html`（`p6_latest`） |

### 9.4 当時の実装タイムライン（5/26 前後）

| 日付 | やったこと |
|------|-----------|
| **5/26 午前** | ユーザー要望「勝率×オッズで期待値」 |
| **5/26 昼** | `win_prob_ev_new.py` 作成、`race_detail.php` / `accessS.html` に勝率(New)・期待値(New)列 |
| **5/26 午後** | `import_all_race_ev_new_compare.py` + `all_race_compare_profiles_new.html` 作成、**全件取込開始**（約5,180レース） |
| **5/26〜末** | 馬連3点KPI・年フィルタ・回収率メイン表示を `new.html` + PHP API に追加 |

### 9.5 いま DB で再現確認（133）

```sql
-- スクショの ◎1着率と一致
SELECT COUNT(*) n, SUM(match_honmei_winner=1) hits,
  ROUND(100*SUM(match_honmei_winner=1)/COUNT(*),1) pct
FROM all_race_ai_profile_compare
WHERE profile_key='ev_new' AND status='ok'
  AND YEAR(race_date)=2026 AND winner_umaban IS NOT NULL;
-- → n=1194, pct=12.3
```

### 9.6 復元に必要なファイル（git から消えているものあり）

| 必須 | パス |
|------|------|
| 計算 | `ai/win_prob_ev_new.py` |
| 取込 | `scripts/import_all_race_ev_new_compare.py` |
| 画面 | `public/all_race_compare_profiles_new.html` |
| API | `public/api/all_race_compare_profiles.php`（`ev_new` 用 `fetch_umaren_box_for_profile` 分岐含む） |

`all_race_compare_profiles.php` は git コミット `15d76c4` 以前の作業ツリーに `ev_new` 馬連分岐が入っていた（transcript 上）。**現ワークスペースには PHP 本体が無い**ので、復元時は git 履歴または transcript から戻す。

---

## 付録 A — `ai/win_prob_ev_new.py` 全文（2026-05-26 版）

別 Window でファイルが無いときは、この内容で `ai/win_prob_ev_new.py` を新規作成する。

```python
"""レース内の勝率(New)・期待値(New)（単勝オッズ×勝率）の共通計算。"""
from __future__ import annotations

import math
from typing import Any


def win_prob_new_pcts(model_scores: list[float | None], *, temperature: float = 10.0) -> list[float]:
    """
    モデルスコア（predictions.win_probability に保存されている値）から、
    レース内合計100%の勝率(New)％を返す（softmax・温度付き）。
    """
    if not model_scores:
        return []
    max_s: float | None = None
    for s in model_scores:
        if s is not None:
            max_s = s if max_s is None else max(max_s, s)
    weights: list[float] = []
    sum_w = 0.0
    for s in model_scores:
        if s is None or max_s is None:
            w = 1.0
        else:
            z = (s - max_s) / temperature
            if z < -50.0:
                z = -50.0
            w = math.exp(z)
        weights.append(w)
        sum_w += w
    if sum_w <= 0.0:
        sum_w = float(len(weights))
        weights = [1.0] * len(weights)
    pcts = [round((w / sum_w) * 100.0, 1) for w in weights]
    delta = round(100.0 - sum(pcts), 1)
    if pcts and abs(delta) <= 5.0:
        pcts[-1] = round(pcts[-1] + delta, 1)
    return pcts


def expected_value_new(prob_pct: float | None, odds: float | None) -> float | None:
    """期待値(New) = (勝率%/100) × 単勝オッズ（倍率）。"""
    if prob_pct is None or odds is None:
        return None
    o = float(odds)
    if o <= 0:
        return None
    return round((float(prob_pct) / 100.0) * o, 4)


def attach_win_prob_and_ev(
    horses: list[dict[str, Any]],
    *,
    score_key: str = "model_score",
    odds_key: str = "odds",
    temperature: float = 10.0,
) -> list[dict[str, Any]]:
    """
    各要素に win_prob_new_pct / ev_new を付与して返す（破壊的更新）。
    horses: model_score, odds を持つ dict のリスト
    """
    scores = [h.get(score_key) for h in horses]
    norm_scores: list[float | None] = []
    for s in scores:
        if s is None or s == "":
            norm_scores.append(None)
        else:
            norm_scores.append(float(s))
    pcts = win_prob_new_pcts(norm_scores, temperature=temperature)
    for i, h in enumerate(horses):
        pct = pcts[i] if i < len(pcts) else None
        h["win_prob_new_pct"] = pct
        od = h.get(odds_key)
        odds_f = float(od) if od is not None and od != "" else None
        h["ev_new"] = expected_value_new(pct, odds_f)
    return horses


def marks_by_ev_new(horses: list[dict[str, Any]], *, top_n: int = 4) -> list[dict[str, Any]]:
    """期待値(New)の降順で ◎〜△ を付与したマーク行（最大 top_n 頭）。"""
    labels = ["◎本命", "〇対抗", "▲単穴", "△連下"]
    ranked = sorted(
        horses,
        key=lambda x: (
            float(x.get("ev_new") or 0.0),
            float(x.get("win_prob_new_pct") or 0.0),
        ),
        reverse=True,
    )
    out: list[dict[str, Any]] = []
    for i, row in enumerate(ranked[:top_n]):
        out.append(
            {
                "mark": labels[i] if i < len(labels) else f"候補{i + 1}",
                "horse_number": row.get("horse_number"),
                "horse_name": row.get("horse_name") or "",
                "win_prob_new_pct": row.get("win_prob_new_pct"),
                "ev_new": row.get("ev_new"),
                "odds": row.get("odds"),
                "model_score": row.get("model_score"),
            }
        )
    return out
```

---

## 10. 予想方法の完全ロジカルマップ（5/26 版・データ依存込み）

**目的:** 133 と 163 で結果がズレるとき、「どの段階のどのデータが違うか」を切り分けるための正。

### 10.1 全体像（4段階パイプライン）

```mermaid
flowchart TB
  subgraph upstream ["上流: p6_latest 相当の Stage1（材料づくり）"]
    DB[(DB: races / entries / results / odds)]
    LGBM[LightGBM stage1_lgbm_enhanced]
    S15[第1.5段階 OFF]
    S2[第2段階: 適性・展開・騎手・コースナレッジ]
    S3[第3段階: オッズ妙味 ※ev_newの◎決定には直接使わない]
    PRED[(predictions stage1_screening_accuracy)]
    DB --> LGBM --> S15 --> S2 --> S3 --> PRED
  end

  subgraph evnew ["ev_new 層（5/26 の本丸）"]
    SCORE[predictions.win_probability をモデルスコアとして読む]
    ODDS[odds_lines 単勝オッズ]
    SOFT[softmax T=10 → 勝率 New 100%]
    EV[期待値 New = 勝率 × オッズ]
    MARK[◎〜△ = 期待値 New 降順]
    SNAP[(all_race_ai_profile_compare ev_new)]
    PRED --> SCORE
    ODDS --> EV
    SCORE --> SOFT --> EV --> MARK --> SNAP
  end

  subgraph display ["表示層"]
    API[all_race_compare_profiles.php]
    UI[all_race_compare_profiles_new.html]
    SNAP --> API --> UI
  end
```

**要点:** `ev_new` は **Stage1 を丸ごと再実装していない**。`predictions` のスコアと `odds_lines` の単勝オッズを組み合わせて **印だけ付け替える** 層である。

### 10.2 上流 — Stage1（`p6_latest` 相当）が使うデータ

5/26 当時の `p6_latest` 設定（`ai/prediction_profiles.py` の git 履歴より）:

| 設定 | 値 |
|------|-----|
| lookback_runs | 10（直近10走） |
| pass_rate | 0.50（約半数通過） |
| enable_stage1_5 | **False** |
| enable_stage2 | **True** |
| enable_stage3 | **True**（min_prob=0.03, odds_cap=80） |
| rank_mode | accuracy（堅実順） |
| forecast_pipeline | **legacy**（6月の `ev_forecast` とは別） |
| モデル | `ai/models/stage1_lgbm_enhanced.txt` + `.meta.json` |

#### 10.2.1 DB テーブルと用途

| テーブル | 使う主なカラム | 用途 |
|----------|----------------|------|
| `races` | `race_date`, `distance_m`, `surface`, `grade`, `circuit`, `track_id` | レース文脈・過去走の切り口 |
| `tracks` | `code`, `name` | 競馬場コード（コースナレッジ・同場成績） |
| `race_entries` | `horse_id`, `horse_number`, `bracket_number`, `carry_weight`, `odds_win`, `jockey_id`, `is_scratched` | 出走馬・斤量・騎手・補完オッズ |
| `horses` | `name` | 馬名表示 |
| `race_results` | `finish_position`, `race_time_seconds`, `last_3f_time`, `passing_order` | **過去走10走**・騎手通算・脚質推定 |
| `odds_lines` | `odds_type='tansho'`, `combination`, `min_odds`, `max_odds` | Stage3 妙味計算・**ev_new の単勝オッズ** |

#### 10.2.2 特徴量（`ai/feature_engineering.compute_stage1_features`）

各馬の **レース日より前** の JRA 過去走（最大10走）から算出:

- 直近勝率・複勝率・平均着順・重賞5着内率
- **同芝/ダ複勝率**（`same_surface_top3_rate`）
- **距離帯±200m複勝率**（`distance_band_top3_rate`）
- **同競馬場複勝率**（`same_track_top3_rate`）
- 上がり3F平均・斤量・フォーム勢い

→ LightGBM が `model_win_prob`（0〜1）を出力 → **×100 で score（0〜100）** に変換。

#### 10.2.3 第2段階（`ai/stage2_factors` + `ai/course_knowledge`）

| 要素 | 内容 |
|------|------|
| 脚質 | 過去走の通過順から front / stalker / closer を推定 |
| 展開 | レース内脚質構成からペースシナリオ |
| 適性 | 同芝ダ・距離帯の過去複勝率で加点減点 |
| 騎手 | レース日より前の JRA 通算 3着内率・勝率（サンプル数で信頼度調整、±5.5 クリップ） |
| コースナレッジ | 阪神・中山の内/外回りと脚質の相性（`docs/knowledge/` 準拠、微小補正） |

第2段階後の `score` が **レース内相対化**（`_apply_within_race_score_spread`）され、これが `score_accuracy` の元になる。

#### 10.2.4 predictions に保存される「モデルスコア」（ev_new が読む値）

**5/26 当時の正（git `581d35c` 以前の `write_predictions`）:**

```
predictions.prediction_type = 'stage1_screening_accuracy'
predictions.win_probability   = score_accuracy（0〜100 の統合スコア）
```

※ 確率（0〜1）ではない。`note.score` / `note.score_accuracy` と同じスケール。

第3段階は `expected_roi` 等を note に付けるが、**ev_new の softmax 入力は score_accuracy 側**（堅実スコア）。

### 10.3 ev_new 層 — 印を決める計算（`import_all_race_ev_new_compare.py`）

#### 10.3.1 母集団（どのレースを処理するか）

| モード | 条件 |
|--------|------|
| **既定** | `all_race_ai_profile_compare` で `profile_key='p6_latest'` かつ `status='ok'` のレース |
| `--all-finished-races` | 1着確定の全 JRA レース |

#### 10.3.2 1レースの処理順（ロジック）

```
1. predictions から stage1_screening_accuracy を読む
   └ 無い → run_stage1(p6_latest 相当) をその場実行（--write-predictions で DB 保存可）

2. 各馬について:
   model_score ← predictions.win_probability（= score_accuracy 0〜100）
   odds        ← odds_lines(tansho).max_odds（無ければ min_odds）
                 → 無ければ predictions.expected_odds / note.win_odds

3. attach_win_prob_and_ev()  … ai/win_prob_ev_new.py
   z_i = (s_i - max(s)) / 10
   win_prob_new_pct_i = 100 × exp(z_i) / Σexp(z)
   ev_new_i = (win_prob_new_pct_i / 100) × odds_i

4. marks_by_ev_new() … ev_new 降順（同値なら win_prob_new_pct 降順）で ◎〇▲△

5. all_race_ai_profile_compare に profile_key='ev_new' で UPSERT
```

#### 10.3.3 オッズが無いとどうなるか

全馬で `ev_new` が算出できないレースは `status='error'`（`err: no odds`）。**単勝オッズは必須。**

#### 10.3.4 p6_latest（堅実順）との違い

| | p6_latest | ev_new |
|--|-----------|--------|
| 同じ材料 | predictions（stage1_screening_accuracy） | 同左 |
| ◎の決め方 | `rank_accuracy` 1位（堅実スコア順） | **期待値(New) 1位** |
| 同レースで◎が違う | あり得る | — |

### 10.4 表示層 — KPI の数え方

| KPI | データソース |
|-----|-------------|
| ◎1着率 | `match_honmei_winner` の平均 |
| ◎単勝回収 | 各レース ◎に100円 → `payouts`（tansho） |
| 馬連 ◎-〇 等 | `ai_honmei_umaban`〜`ai_renka_umaban` と 1・2着・`payouts`（umaren） |
| 3点合計 | ◎-〇・◎-▲・◎-△ の300円投資に対する払戻合計 |

### 10.5 133 と 163 が合わない典型原因（チェックリスト）

**優先度順に確認する。**

| # | 原因 | 症状 | 確認方法 |
|---|------|------|----------|
| 1 | **predictions の中身が違う** | ◎が別馬・KPI全体がズレ | 同一 `race_id` で `predictions.win_probability` を 133/163 比較 |
| 2 | **predictions を現行コードで再生成した** | スコアのスケールや値が変わる | 現行 `write_predictions` は `stage3_prob×100` を保存（5/26 は `score_accuracy`）。**再生成すると ev_new が壊れる** |
| 3 | **`p6_latest` が `ev_forecast` にエイリアス** | `--write-predictions` 時に legacy 以外のパイプライン | 現行 `get_profiles('p6_latest')` → `ev_forecast`。import の `run_stage1` は `forecast_pipeline` を渡していない（legacy 既定）が、**別バッチで上書きされた predictions は ev_forecast 由来** |
| 4 | **単勝オッズ不足・値の差** | `err: no odds` または ev_new だけ違う | `odds_lines` 件数・`max_odds` をレース単位で比較 |
| 5 | **過去走（race_results）不足** | Stage1 スコアが全体的に薄い/デフォルト寄り | 各馬の `race_date` 以前の `race_results` 件数（10走欲しい） |
| 6 | **出走馬（race_entries）不足** | 頭数・馬番・斤量・騎手が欠ける | `race_entries` と `is_scratched=0` |
| 7 | **競馬場・コース情報不足** | 第2段階・同場成績が効かない | `tracks` 結合、`races.surface` / `distance_m` |
| 8 | **モデルファイル不一致** | LGBM 出力が変わる | `stage1_lgbm_enhanced.txt` / `.meta.json` のハッシュ比較 |
| 9 | **母集団の違い** | 件数が違う（1194 vs 別数） | 既定は p6 母集団。163 で `--all-finished-races` だけ使うと対象レースが増減 |
| 10 | **JV取込の時期差** | 同レースでもオッズ・馬番確定前後で差 | `race_entries.horse_number IS NULL` の有無 |

### 10.6 163 を 133 に揃えるための推奨手順

**データ移行より「同じ入力で再計算」が正。**

1. **133 の `predictions`（stage1_screening_accuracy）を 163 にコピー**するか、163 で **5/26 当時と同じ legacy + score_accuracy 保存**のコードで Stage1 を再実行する。
2. **`odds_lines`（tansho）** が 163 で揃っていることをレース単位で確認（`docs/ops_odds_2020_tansho_umaren.md`）。
3. **過去走・出走馬・tracks** の取込が 163 で 133 と同等以上であることを確認。
4. `import_all_race_ev_new_compare.py` を **predictions を読むだけ**（`--write-predictions` なし、既存 predictions あり）で実行。
5. 1レースずつ `scripts/diagnose_ev_new_one_race.py`（あれば）で 133/163 の `model_score`・`odds`・`ev_new`・◎を突合。

**やってはいけない:** 現行の `write_predictions` で predictions を上書きしてから ev_new 取込（スコア定義が 5/26 と異なる）。

---

## 付録 B — 別 Window への渡し方（コピペ用）

```text
@docs/session-handoff-ev-new-20260526.md を読んで、163 本番で ev_new（2026-05-26 版）を復元して。
6月以降の base_forecast / ev_forecast / prob_odds は対象外。
1 STEP ずつ進めて。
```

---

最終更新: 2026-06-02（§10 予想方法ロジカルマップ・163不一致チェックリスト追記）
