# JRDB-API データガイド

JRDB-API で予測モデルを作るために必要なことを、この1ファイルにまとめています。
上から順に読めば、データの構造・時制・落とし穴を把握したうえで実装に入れます。

- 対象: `https://dev.api.bigtime.world/v1/*`
- 最終更新: 2026-09-05
- 併せて参照: [エンドポイント一覧](https://jrdb-api.keiba.bigtime.world/docs) / [コード表](https://jrdb-api.keiba.bigtime.world/docs/codes) / [フィールド説明](https://jrdb-api.keiba.bigtime.world/docs/fields)

---

## 目次

1. [5分で最初のリクエスト](#1-5分で最初のリクエスト)
2. [リクエストとレスポンスの形](#2-リクエストとレスポンスの形)
3. [エンドポイント一覧](#3-エンドポイント一覧)
4. [キー体系](#4-キー体系)
5. [時制マップ — 一番重要](#5-時制マップ--一番重要)
6. [コード表とフィールドの対応](#6-コード表とフィールドの対応)
7. [学習データの組み立て方](#7-学習データの組み立て方)
8. [大量取得の作法](#8-大量取得の作法)
9. [既知の注意点・落とし穴](#9-既知の注意点落とし穴)
10. [そのまま動くサンプル](#10-そのまま動くサンプル)
11. [回収率の正しい計算](#11-回収率の正しい計算)

---

## 1. 5分で最初のリクエスト

必要なものは2つです。

| | 取得場所 | 用途 |
|---|---|---|
| `api_key` | アプリ共通のキー | クエリパラメータ `?api_key=...` |
| `x-user-key` | [ダッシュボード](https://jrdb-api.keiba.bigtime.world/dashboard) | 会員個別の認証キー。HTTPヘッダー |

```bash
curl -X POST "https://dev.api.bigtime.world/v1/tra?api_key=API_KEY" \
  -H "Content-Type: application/json" \
  -H "x-user-key: YOUR_AUTH_KEY" \
  -d '{"racedate": "20251129"}'
```

すべてのエンドポイントが **POST** です。GET ではありません。

---

## 2. リクエストとレスポンスの形

### 成功時（HTTP 200）

```json
{
  "request": { "parameters": { "racedate": "20251129" } },
  "result": {
    "status": 200,
    "endpoint_uri": "/v1/tra",
    "membershipLimit": "paid members",
    "result_count": 36,
    "data": [ { "racekey": "05250101", "...": "..." } ]
  }
}
```

`result.status` は **数値の `200`** です。文字列の `"success"` ではありません。

### エラー時（HTTP 400 / 401 / 403 / 404 / 500）

```json
{ "error": true, "status": 400, "message": "Date range exceeds 365 days. Please specify a shorter range." }
```

エラー時は **`result` 自体が返りません**。`data["result"]["message"]` を読むと KeyError になります。

### 判定の定型

```python
data = response.json()
if data.get("error") or data["result"]["status"] != 200:
    raise Exception(data.get("message", "Unknown error"))
rows = data["result"]["data"]
```

### ステータスの意味

| status | 意味 |
|---|---|
| 200 | 成功 |
| 400 | パラメータ不正（形式・必須漏れ・期間365日超過） |
| 401 | `x-user-key` が無い／不正 |
| 403 | プラン権限なし（JRDB-API プラン未契約） |
| 404 | 該当データなし／DBエラー |

### プランによる制限

- **今週データ（`/t*`）とマスタ（`/m*`）は有料会員限定**です。未契約だと 403 が返ります。
- **過去データ（`/d*`）は未課金でも叩けます**が、期間が **2年前の1月1日〜1年前の12月31日**に自動で絞り込まれます（`membershipLimit: "free members"`）。指定した `start_racedate` は無視されます。
- 有料会員は `membershipLimit: "paid members"` となり、期間指定がそのまま効きます。

---

## 3. エンドポイント一覧

命名規則はテーブル名からアンダースコアを除いたものです（`t_ky` → `/tky`）。

### 今週データ（t_*）— 当週のレース用

| エンドポイント | 内容 | パラメータ |
|---|---|---|
| `/tka` | 開催データ。馬場状態・馬場差・クッション値（30列） | **なし**（当週分をすべて返す） |
| `/tra` | 番組データ。距離・トラック種別・クラス・賞金（34列） | `racedate`（必須） |
| `/tky` | 競走馬データ。JRDB指数・印・適性・血統（143列） | `racedate` / `racekey` |
| `/tze` | 前走データ。前走〜5走前の成績（256列） | `racedate` / `racekey` |
| `/tzk` | 前走拡張データ。前走の特記・馬具・脚元コード（276列） | `racedate` / `racekey` |
| `/tcy` | 調教分析。追切指数・仕上指数・調教量評価（22列） | `racedate` / `racekey` |
| `/tch` | 調教本追切。調教コース・追い状態・併せ馬（23列） | `racedate` / `racekey` |
| `/tpd` | 直前情報。パドック指数・馬体重・当日オッズ・馬具/脚元コード（56列） | `racedate` / `racekey` |
| `/tse` | 成績データ（当週分。55列） | `racedate` / `racekey` |
| `/thj` | 払戻データ。的中組番・払戻金・人気（116列） | `racedate` / `racekey` |

### 過去データ（d_*）— 学習用

| エンドポイント | 内容 | パラメータ |
|---|---|---|
| `/dka` | 開催データ。馬場状態・馬場差・芝丈・クッション値（30列） | `start_racedate` + `end_racedate` / `racedate` / `limit` |
| `/dse` | 成績データ。着順・タイム・確定オッズ（55列） | `start_racedate` + `end_racedate` / `racekey` / `limit` |
| `/dra` | 過去番組データ（32列。`/tra` の `cancel_horse`・`correction_flag` は当週専用のため含みません） | `start_racedate` + `end_racedate` / `racedate` / `limit` |
| `/dky` | 過去競走馬データ（143列） | `start_racedate` + `end_racedate` / `racedate` / `racekey` / `limit` |
| `/dzk` | 過去前走拡張データ（276列） | 同上 |
| `/dpd` | 過去直前情報（56列） | 同上 |
| `/dhj` | 過去払戻データ（116列）。**回収率検証はこれを正とする** | 同上 |

> `/dse` は `racedate`（単日）を受け付けません。単日が欲しい場合は `start_racedate` と `end_racedate` に同じ日を指定してください。

### マスタ（m_*）

| エンドポイント | 内容 | パラメータ |
|---|---|---|
| `/muk` | 競走馬マスタ。血統・毛色・馬主・生産者 | `horse_id`（**必須**） |
| `/mjk` | 騎手マスタ。所属・免許年・年次成績 | `jockey_cd`（省略で全件） |
| `/mtn` | 調教師マスタ | `trainer_cd`（省略で全件） |

> `/muk` は後方互換のため `kettonum`（血統登録番号）も受け付けますが**非推奨**です。将来削除します。新しく書くコードは `horse_id` を使ってください。

---

## 4. キー体系

### racekey — レースキー（8桁）

```
09  26  34  11
場  年  回日  R
```

- 場（2桁）: 01 札幌 / 02 函館 / 03 福島 / 04 新潟 / 05 東京 / 06 中山 / 07 中京 / 08 京都 / 09 阪神 / 10 小倉
- 年（2桁）: 西暦下2桁
- 回日（2桁）: 第N回M日目
- R（2桁）: レース番号 01〜12

例 `09263411` = 阪神・2026年・第3回4日目・11R

### horse_id — 馬ID（**12桁の英数字**）

例 `2328aaa95a05`

**1頭の競走馬を一意に識別する、当APIの正式な馬キーです。** 馬に関するほぼ全エンドポイントが
`horse_number` の直後にこの列を返します。馬をまたいで結合するときは必ずこれを使ってください。

意味を持たない不透明なIDです。生年や血統を読み取ることはできません。
桁を分解したり、大小を比較したりしないでください。

> 以前返していた `pedigree_number`（血統登録番号）は**全エンドポイントから削除されました**。
> 移行は `pedigree_number` を `horse_id` に置き換えるだけで、結合の粒度は変わりません。

### 勝ち馬を指す2つの列（混同しやすい）

`/tse`・`/dse` は勝ち馬まわりで2列返します。**指している馬が違います。**

| 列 | 中身 |
|---|---|
| `winning_horse` | **相手馬名**。1着馬の行には**2着馬**、それ以外の行には1着馬の名前が入る（JRDBの仕様） |
| `winner_horse_id` | **そのレースの1着馬の `horse_id`**。1着馬自身の行では `horse_id` と一致する |

「このレースを勝った馬」が欲しいときは `winner_horse_id` を使ってください。
`winning_horse` を勝ち馬名として扱うと、1着馬の行だけ2着馬になります。

1着同着（過去データで181,768レース中144レース）のときは、`winner_horse_id` は
`winning_horse` と同じ馬を返します。両方が勝ち馬なので、どちらか1頭に決まります。

`/tze`・`/dze` の `race_back_N_winner_horse_id` も同じで、N走前のレースの1着馬を指します。

### resultkey — 競走成績キー（16桁）

血統登録番号（8桁）+ `racedate`（8桁）。1頭の1出走を一意に識別します。

例 `1910181420240210` = 2024-02-10 の、ある馬の1出走

**この文字列は不透明な識別子として扱ってください。** 先頭8桁を切り出して馬キーとして使わないこと。
馬をまたぐ結合は `horse_id` で行います。`resultkey` は「その出走の行」を指すためだけに使います。

> JRDB の血統登録番号は8桁で、JRA公式の10桁（例 `2019100001`）とは別体系です。
> `resultkey` の先頭8桁もこの8桁体系です。

### horse_number / umaban — 馬番（2桁ゼロ埋め文字列）

`"01"` 〜 `"18"`。**数値ではなく文字列**です。`1` と `"01"` を結合すると外れます。

### 結合のキー

| 結合したいもの | キー |
|---|---|
| レース条件 ↔ 出走馬 | `racekey` |
| 出走馬 ↔ 成績・直前情報 | `racekey` + `horse_number` |
| 馬の履歴を辿る | `horse_id` + `racedate` |
| 馬 ↔ 血統マスタ | `horse_id` ↔ `/muk` の `horse_id` |

---

## 5. 時制マップ — 一番重要

**予測モデルで最初に事故るのがここです。** どのフィールドが「いつ確定するか」を把握してください。

| 時点 | エンドポイント | 主な内容 |
|---|---|---|
| **前日** | `/tky`・`/dky` | JRDB指数（IDM・調教指数・厩舎指数）、印、脚質、適性、基準オッズ、基準人気、ブリンカー、騎手期待勝率、血統 |
| **前日** | `/tra`・`/dra` | 距離・トラック種別・回り・クラス・賞金 |
| **前日** | `/tze`・`/tzk`（`/dze`・`/dzk`） | 前走〜5走前の成績・特記・馬具・脚元・コメント |
| **前日** | `/tcy`・`/tch` | 調教分析・本追切 |
| **当日直前** | `/tpd`・`/dpd` | パドック指数、気配・馬体コード、馬体重、当日オッズ、馬具変更、脚元判定、装着馬具コード |
| **当日直前** | `/tka` | 馬場状態・馬場差・クッション値（当日更新） |
| **レース後** | `/tse`・`/dse` | 着順、走破タイム、確定オッズ、確定人気、コーナー順位、実走IDM |
| **レース後** | `/thj`・`/dhj` | 単勝〜三連単の的中組番・払戻金・人気、返還馬番 |

### look-ahead の考え方

look-ahead は「テーブルの性質」ではなく **予測時点との関係** で決まります。

- 前日にベットするモデル → `/tpd` はまだ存在しないので使ってはいけない
- 直前（発走前）にベットするモデル → `/tpd` は実在するので使ってよい

エンドポイントが時点で分かれているので、**モデルの決定時点を先に決めて、それ以前のエンドポイントだけを結合する**のが安全です。

### レース後データの正しい使い方

`/dse` の `order_of_finish`・`idm`・`confirmed_win_odds` は結果です。

- 正解ラベル（`order_of_finish` から作る）として使う → OK
- **過去走**の集約（`shift(1)` を挟んだもの）として特徴量にする → OK
- 当該レースの値をそのまま特徴量にする → **リーク**

### `/tpd` を使うときの追加の注意

過去の `/dpd` は最終確定版ですが、本番運用で発走X分前に取る `/tpd` は更新途中です。特に `win_odds`・`place_odds` は取得時刻に強く依存します（`odds_obtained_time` に HHMM が入ります）。**学習も本番と同じ鮮度の版で作る**のが原則です。

また、パドック情報は直前更新漏れや取消で欠けます。学習データでは埋まっているのに本番で未着、という状態だとモデルが「欠測＝◯◯」という誤った規則を覚えます。欠測の扱い方針を決めて固定してください。

---

## 6. コード表とフィールドの対応

コード表は [/docs/codes](https://jrdb-api.keiba.bigtime.world/docs/codes) に全7種を掲載しています。

| コード表 | 使われるフィールド |
|---|---|
| 脚質コード | `/tky` `foottype_cd`、`/tse` `race_kyakushitsu` `running_style_cd` |
| 距離適性コード | `/tky` `distance_aptitude_cd1` `distance_aptitude_cd2` |
| 上昇度 | `/tky` `rising_level_cd` |
| 調教矢印コード | `/tky` `training_arrow` `stable_arrow` |
| 厩舎評価コード | `/tky` `cid_stable` |
| 蹄コード | `/tky` `hoof_cd` |
| 重適性コード | `/tky` `mudder_cd` |
| クラスコード | `/tra` `class_cd` |
| 場コード | `/tra` `racecourse_cd`、`racekey` の先頭2桁 |
| 馬場状態 | `/tra` `/tka` `turf_condition_cd` `dirt_condition_cd` |
| グレード | `/tra` `grade_cd` |
| 異常区分 | `/tse` `trouble_classification` |
| 馬体コード | `/tpd` `this_horse_body_state` |
| 気配コード | `/tpd` `this_horse_mind_state` |
| 印コード | `/tky` `integration_mark` `idm_mark` `paddock_mark` ほか |
| 毛色コード | `/muk` `coatcolor_cd` |
| 馬記号コード | `/tky` `horse_attribute_cd`、`/muk` `horse_attribute_cd` |
| 天候コード | `/tra` `/tka` `/tpd` `weather_cd` |
| **特記コード** | `/tky` `horse_special_mention_cd1〜3`・`horse_bodytype_integration_cd1〜3`、`/tzk` `race_back_N_special_note_cd1〜6` |
| **馬具コード** | `/tpd` `this_gear1_cd〜7`、`/tzk` `race_back_N_tack_cd1〜8`・`bit_cd`・`horse_shoe_cd`・`splint_cd`・`bone_spavin_cd` |
| **脚元コード** | `/tpd` `this_leg_*_cd`、`/tzk` `race_back_N_hoof_*_cd` |
| **調教コースコード** | `/tch` `exercise_course_cd` |
| **追い状態コード** | `/tch` `work_situation` |
| 系統コード | `/muk` `sire_lineage_cd` `mare_lineage_cd` |

### 馬具コードの読み方

馬具コードには「馬具種別」が付いています。1つのコード表にハミも蹄鉄も馬の状態も混在しているため、種別で絞り込んでください。

| 種別 | 内容 |
|---|---|
| 1 | ハミ |
| 2 | その他馬具（ブリンカー、シャドーロール、チークピース等） |
| 3 | 蹄鉄 |
| 4 | 蹄状態 |
| 5 | ソエ状態 |
| 6 | 骨瘤 |
| 7 | 馬状態 |
| 8 | バンテージ |

`/tzk` は種別ごとに列が分かれています（`bit_cd`=種別1、`horse_shoe_cd`=種別3、`splint_cd`=種別5、`bone_spavin_cd`=種別6）。

### 紛らわしいペア

- `/tpd` の `underfoot_info` は **脚元コードではありません**。0:平行線 / 1:良化 / 2:疑問 / 3:悪化 の判定値です。実際の脚元コードは `this_leg_*_cd`。
- `/tpd` の `change_equipment_flag` は **馬具コードではありません**。0:変更なし / 1:変更（通常）/ 2:変更（特注）のフラグです。実際の馬具コードは `this_gear1_cd〜7`。
- `/tky` の `bodytype` は24桁の体型データ（1桁ずつが体型・背中・胴・尻・トモ…の各部位）で、`/tpd` の `this_horse_body_state`（パドックの馬体コード）とは別物です。

---

## 7. 学習データの組み立て方

1出走 = 1行の表を作ります。

```python
# 前日時点の情報だけで作る（前日モデル用）
ky = fetch("dky", start, end)   # 指数・印・適性・基準オッズ
ra = fetch("dra", start, end)   # 距離・トラック種別・回り・クラス
zk = fetch("dzk", start, end)   # 前走の特記・馬具・脚元
se = fetch("dse", start, end)   # 着順（正解ラベル）

df = (
    ky
    .merge(ra[['racekey', 'distance', 'track_type', 'turn_cd',
               'course_cd', 'racecourse_cd', 'turf_condition_cd']],
           on='racekey', how='left')
    .merge(zk, on=['racekey', 'horse_number'], how='left')
    .merge(se[['racekey', 'horse_number', 'order_of_finish',
               'horse_weight', 'weight_cycling']],
           on=['racekey', 'horse_number'], how='inner')
)

# 直前モデルにするなら、ここに /dpd を足す
# pd_ = fetch("dpd", start, end)
# df = df.merge(pd_[['racekey', 'horse_number', 'paddock_index',
#                    'this_horse_mind_state', 'this_horse_body_state',
#                    'change_equipment_flag', 'win_odds']],
#               on=['racekey', 'horse_number'], how='left')
```

### 型の注意

APIは基本的に**すべて文字列で返します**。数値として使う列は明示的に変換してください。

```python
num_cols = ['distance', 'bracket_number', 'horse_number', 'order_of_finish',
            'adjust_idm', 'criteria_win_odds', 'criteria_win_popularity']
for c in num_cols:
    df[c] = pd.to_numeric(df[c], errors='coerce')
```

ただし `horse_number` を数値化すると `/tpd` などとの結合キーとして使えなくなります。**結合を全部終えてから**変換してください。

### 主要な特徴量の置き場所

| 欲しいもの | フィールド | エンドポイント |
|---|---|---|
| 能力指数 | `adjust_idm` | `/tky` `/dky` |
| 調教評価 | `training_index` / `oikirisisu` / `siagarisisu` | `/tky` / `/tcy` |
| 厩舎評価 | `stable_index` / `cid_stable` | `/tky` |
| 市場の事前評価 | `criteria_win_odds` / `criteria_win_popularity` | `/tky` |
| 脚質 | `foottype_cd` | `/tky` |
| 展開予想 | `est_pace` / `est_goal_order` / `est_tenkai_mark` | `/tky` |
| 騎手の期待値 | `jockey_expect_winrate` / `_placerate` / `_showrate` | `/tky` |
| ローテーション | `rotation`（**中n週。日数ではありません**） | `/tky` |
| 外厩 | `outer_stable` / `outer_stable_rank` | `/tky` |
| 前走の敗因 | `race_back_1_special_note_cd1〜6` | `/tzk` |
| 馬具の変化 | `race_back_N_tack_cd*` の差分、`blinker_cd` | `/tzk` / `/tky` |
| 当日の状態 | `paddock_index` / `this_horse_mind_state` / `weight_cycling` | `/tpd` |

---

## 8. 大量取得の作法

### まず一括ファイルを確認してください

2010年以降の過去データは、**年単位の Parquet ファイル**としてダッシュボードから
ダウンロードできます。学習データを作るだけなら、API を繰り返し叩く必要はありません。

https://jrdb-api.keiba.bigtime.world/dashboard の「一括ダウンロード」

- エンドポイントごと・年ごとに1ファイル（`dse_2010.parquet` など）
- 値は API のレスポンスそのまま（すべて文字列）なので、**API と同じコードで扱えます**
- ダウンロード URL は押した時点で発行され、10分で失効します

```python
import pandas as pd
df = pd.read_parquet("bulk/dse/")   # ディレクトリ指定で全年まとめて読める
```

以降は、API から自分で取得する場合の説明です。

### API から取得する場合の制約

過去データの各エンドポイントには次の制約があります。

| 制約 | 値 |
|---|---|
| 期間指定の上限 | **365日**（超過すると 400） |
| 1リクエストの返却件数 | 既定 **10,000** / 上限 **50,000**（`limit` で指定） |
| `end_racedate` 省略時 | DB上の最新日付まで |

**ただし、期間指定が365日まで通ることと、実際に返しきれることは別です。**
1リクエストで返せる量は「行数 × 列数」で効いてきます。年間の出走は約47,000、
レース数は約3,400なので、レース単位のものは1年でも通りますが、
出走単位で列数の多いものは**ゲートウェイが返しきれず 500 になります**
（`/dky` を1年分まとめて要求すると失敗することを実測済み）。

エンドポイントごとの目安です。通らない場合は期間を半分にして試してください。

| エンドポイント | 粒度 | 列数 | 1リクエストの目安 |
|---|---|---|---|
| `/dra` | レース単位 | 30 | 1年 |
| `/dhj` | レース単位 | 116 | 1年 |
| `/dse` | 出走単位 | 53 | 3〜4か月 |
| `/dpd` | 出走単位 | 55 | 3〜4か月 |
| `/dky` | 出走単位 | 141 | 2か月 |
| `/dzk` | 出走単位 | 275 | 2週間 |

```python
from datetime import date, timedelta

def fetch_period(endpoint, start: date, end: date, span_days: int):
    """期間を span_days ずつに区切って取得し、つなげて返す。"""
    frames, cur = [], start
    while cur <= end:
        last = min(cur + timedelta(days=span_days - 1), end)
        df = fetch(endpoint, cur.strftime('%Y%m%d'), last.strftime('%Y%m%d'), limit=50000)
        frames.append(df)
        cur = last + timedelta(days=1)
    return pd.concat(frames, ignore_index=True)

# /dky なら2か月ずつ
df = fetch_period('dky', date(2025, 1, 1), date(2025, 12, 31), span_days=60)
```

- 年をまたぐ範囲を1回で投げると 365日制限に当たります。年単位に切ってください。
- `result_count` が `limit` と同じ値なら**打ち切られている可能性**があります。期間を半分にしてください。
- 取得したものはローカルに Parquet などで保存し、試行のたびに再取得しないでください。

---

## 9. 既知の注意点・落とし穴

### `confirmed_place_odds` は「下限」であって払戻ではない

`/tse`・`/dse`・`/tze` が返す `confirmed_place_odds` は、JRDB の確定複勝オッズ**下限値**です。複勝は着順によって払戻が変わるため、**この値をそのまま払戻として回収率を計算すると過小評価になります**。

実際の払戻は `/thj`・`/dhj`（払戻データ）の `place_payoff_1〜5` を使ってください。下の [11. 回収率の正しい計算](#11-回収率の正しい計算) に手順があります。

なお「確定複勝人気」に相当する列は JRDB のデータ自体に存在しません。複勝の人気順が必要なら、`confirmed_place_odds` をレース内で昇順に並べて自分で作ってください。

### `/tzk`・`/dzk` は同じ出走が2行返ることがある

前走が**同着1着**だったとき、`race_back_N_winner_idm`（前走の勝ち馬IDM）の
結合先が2頭ぶんになり、同じ出走が複数行で返ります。2025年のデータでは
48,137行のうち253行（約0.5%）がこれに該当しました。

**差が出るのは `winner_idm` だけ**で、特記・馬具・脚元コードを含む他の275列は
一致します。集計前に `resultkey` で重複を落としてください。

```python
zk = zk.drop_duplicates(subset=['resultkey'], keep='first')
```

落とさずに結合すると、その馬のレコードが二重に数えられます。
ダッシュボードの一括ファイルは、この処理を済ませた状態で配布しています。

### `/tzk` の `hoof_*` は蹄ではなく脚元

`race_back_N_hoof_left_front_cd1` などの列名は hoof（蹄）ですが、中身は**脚元コード**です。蹄コードは `/tky` の `hoof_cd` です。

### 開催日が丸ごと欠けている日がある

`/dse`（成績）に存在するのに、他のエンドポイントには無い開催日があります。
2009〜2025年で確認できたものは次のとおりです。

| エンドポイント | 欠けている開催日 |
|---|---|
| `/dra` `/dhj` | なし |
| `/dky` `/dzk` | 2020-10-17、2020-10-18 |
| `/dpd` | 上記2日に加えて 2024年8日、2025年4日 |

`/dky`（出馬表）に無い日は `/dzk`・`/dpd` も連鎖して欠けます
（どちらも内部で `d_ky` と結合しているため）。

**成績を基準に内部結合すると、これらの日のレースが黙って消えます。**
件数が想定と合わないときは、まず開催日の集合を突き合わせてください。

```python
missing = sorted(set(se['racedate']) - set(ky['racedate']))
```

### `rotation` は週であって日数ではない

`/tky` の `rotation` は「中n週」です。実日数はおおよそ `(n+1) × 7` になります。日数として扱うと全部ずれます。

### 更新日時を返さないエンドポイントがある

`updated_at`（データ更新日時）は `/tra`・`/tky`・`/tze`・`/tzk` では返りません。
取得時刻を記録したい場合は、自分で付けてください。

### 文字列の前後空白

固定長データ由来のため、`racekey` などに末尾空白が入ることがあります。結合前に `.str.strip()` を通してください。

### 馬番のゼロ埋め

`horse_number` は `"01"` 形式です。`int` に変換したものと結合すると全行外れます。

---

## 10. そのまま動くサンプル

```python
"""JRDB-API の最小クライアントと、学習用データセットの作成例。"""

import time
import pandas as pd
import requests

API_KEY = "API_KEY"           # アプリ共通のAPIキー
AUTH_KEY = "YOUR_AUTH_KEY"    # ダッシュボードで取得した認証キー
BASE_URL = "https://dev.api.bigtime.world/v1"


def call(endpoint, body, retries=3):
    """1エンドポイントを叩いて DataFrame を返す。"""
    url = f"{BASE_URL}/{endpoint}?api_key={API_KEY}"
    headers = {"Content-Type": "application/json", "x-user-key": AUTH_KEY}

    for attempt in range(retries):
        res = requests.post(url, json=body, headers=headers, timeout=120)
        try:
            data = res.json()
        except ValueError:
            raise Exception(f"{endpoint}: JSONを返しませんでした (HTTP {res.status_code})")

        # 成功時は result.status == 200。エラー時はトップレベルに error/message。
        if data.get("error") or data["result"]["status"] != 200:
            message = data.get("message", "Unknown error")
            # 一時的な失敗だけリトライする
            if data.get("status") in (500, 504) and attempt < retries - 1:
                time.sleep(2 ** attempt)
                continue
            raise Exception(f"{endpoint}: {message}")

        result = data["result"]
        if result["result_count"] == body.get("limit"):
            print(f"  ⚠ {endpoint}: limit に達しました。期間を分割してください。")
        return pd.DataFrame(result["data"])


def fetch_year(year, limit=50000):
    """1年分の学習用データを組み立てる（前日時点の情報＋正解ラベル）。"""
    period = {"start_racedate": f"{year}0101",
              "end_racedate": f"{year}1231",
              "limit": limit}

    ky = call("dky", period)   # 指数・印・適性・基準オッズ（前日確定）
    ra = call("dra", period)   # 距離・トラック種別・回り・クラス
    se = call("dse", period)   # 着順（正解ラベル）

    # 固定長由来の空白を落としてから結合する
    for df in (ky, ra, se):
        for col in ("racekey", "horse_number"):
            if col in df.columns:
                df[col] = df[col].astype(str).str.strip()

    df = (
        ky
        .merge(
            ra[["racekey", "distance", "track_type", "turn_cd",
                "course_cd", "racecourse_cd", "turf_condition_cd"]],
            on="racekey", how="left",
        )
        .merge(
            se[["racekey", "horse_number", "order_of_finish",
                "horse_weight", "weight_cycling"]],
            on=["racekey", "horse_number"], how="inner",
        )
    )

    # 結合が終わってから数値化する
    for col in ["distance", "bracket_number", "order_of_finish", "adjust_idm",
                "criteria_win_odds", "criteria_win_popularity", "horse_weight"]:
        if col in df.columns:
            df[col] = pd.to_numeric(df[col], errors="coerce")

    return df


if __name__ == "__main__":
    for year in range(2010, 2026):
        print(f"{year} を取得中...")
        df = fetch_year(year)
        df.to_parquet(f"jrdb_{year}.parquet", index=False)
        print(f"  {len(df):,} 行")
```

### 前走の特記コードを特徴量にする

```python
from collections import Counter

zk = call("dzk", {"start_racedate": "20250101",
                  "end_racedate": "20251231", "limit": 50000})

# 前走で「出遅れ」「不利」に類する特記が付いた馬を拾う
NOTE_COLS = [f"race_back_1_special_note_cd{i}" for i in range(1, 7)]

# コード表（/docs/codes#tokki）から必要なコードを選ぶ
UNLUCKY = {"059", "955", "956", "957", "960", "961"}  # スタート悪い・蓋される・前が壁 等

zk["prev_unlucky"] = zk[NOTE_COLS].apply(
    lambda r: int(bool(set(r.dropna().astype(str)) & UNLUCKY)), axis=1
)

# 出現頻度の高い特記を確認する
counts = Counter(zk[NOTE_COLS].values.ravel())
print(counts.most_common(20))
```

### 前走からの馬具の変化を検出する

```python
TACK = [f"race_back_{{n}}_tack_cd{i}" for i in range(1, 9)]

def tack_set(row, n):
    cols = [c.format(n=n) for c in TACK]
    return {v for v in (row.get(c) for c in cols) if v and str(v).strip()}

# 前走(1)と前々走(2)を比べて、前走時点で何が足された／外れたかを見る
zk["tack_added"]   = zk.apply(lambda r: len(tack_set(r, 1) - tack_set(r, 2)), axis=1)
zk["tack_removed"] = zk.apply(lambda r: len(tack_set(r, 2) - tack_set(r, 1)), axis=1)
```

---

## 11. 回収率の正しい計算

モデルの良し悪しを判断する最後の関門です。ここを間違えると、良く見えるモデルが実際には負けます。

### 払戻データの形

`/thj`・`/dhj` は **1レース1行**で、券種ごとに「的中組番・払戻金・人気」が枠数分並びます。

| 券種 | 接頭辞 | 枠数 |
|---|---|---|
| 単勝 | `win_horse_number_N` / `win_payoff_N` / `win_popularity_N` | 1〜3 |
| 複勝 | `place_horse_number_N` / `place_payoff_N` / `place_popularity_N` | 1〜5 |
| 枠連 | `bracket_quinella_number_N` / `_payoff_N` / `_popularity_N` | 1〜3 |
| 馬連 | `quinella_number_N` / `_payoff_N` / `_popularity_N` | 1〜3 |
| ワイド | `quinella_place_number_N` / `_payoff_N` / `_popularity_N` | 1〜7 |
| 馬単 | `exacta_number_N` / `_payoff_N` / `_popularity_N` | 1〜6 |
| 三連複 | `trio_number_N` / `_payoff_N` / `_popularity_N` | 1〜3 |
| 三連単 | `trifecta_number_N` / `_payoff_N` / `_popularity_N` | 1〜6 |
| 返還 | `refund_number_1〜5` | 1〜5 |

- `_payoff_N` は **100円あたりの払戻金（円）** です。
- 枠が複数あるのは**同着**のためです。同着が無ければ2件目以降は空になります。
- 単勝・複勝は馬番（`_horse_number_`）、それ以外は組番（`_number_`）です。
- 結合キーは `racekey` **のみ**。馬番は含まれないので、自分の買い目と突き合わせる必要があります。

### なぜ `confirmed_place_odds` を使ってはいけないか

複勝は同じ馬でも着順と他の的中馬によって払戻が変わります。`/dse` の `confirmed_place_odds` はその**下限値**なので、これで集計すると回収率が実際より低く出ます。「複勝で勝てない」という結論が、実は計算方法のせいだった、ということが起こります。

### 単勝の回収率

```python
import pandas as pd

se  = fetch("dse", "20250101", "20251231", limit=50000)
hj  = fetch("dhj", "20250101", "20251231", limit=50000)

for df in (se, hj):
    for c in ("racekey", "horse_number"):
        if c in df.columns:
            df[c] = df[c].astype(str).str.strip()

# 払戻を「レース×馬番 → 払戻金」の縦持ちに直す
win = []
for n in range(1, 4):
    part = hj[["racekey", f"win_horse_number_{n}", f"win_payoff_{n}"]].copy()
    part.columns = ["racekey", "horse_number", "win_payoff"]
    win.append(part)
win = pd.concat(win)
win = win[win["horse_number"].astype(str).str.strip().ne("")]
win["horse_number"] = win["horse_number"].astype(str).str.strip().str.zfill(2)
win["win_payoff"] = pd.to_numeric(win["win_payoff"], errors="coerce")

# 自分の買い目（例: モデルの予測1位）
bets = se[se["predicted_rank"] == 1][["racekey", "horse_number"]]

merged = bets.merge(win, on=["racekey", "horse_number"], how="left")
merged["win_payoff"] = merged["win_payoff"].fillna(0)   # 不的中は0

stake  = len(merged) * 100
payout = merged["win_payoff"].sum()
print(f"購入 {len(merged):,}点 / 投資 {stake:,}円 / 払戻 {payout:,.0f}円")
print(f"回収率 {payout / stake * 100:.1f}%  的中率 {(merged['win_payoff'] > 0).mean() * 100:.1f}%")
```

### 複勝の回収率

複勝は枠が5つあり、`place_horse_number_1〜5` のどれに自分の馬が入るか分かりません。上と同じ縦持ち変換を `place_` 接頭辞・`range(1, 6)` で行えば、同じ処理で計算できます。

```python
place = []
for n in range(1, 6):
    part = hj[["racekey", f"place_horse_number_{n}", f"place_payoff_{n}"]].copy()
    part.columns = ["racekey", "horse_number", "place_payoff"]
    place.append(part)
place = pd.concat(place)
place = place[place["horse_number"].astype(str).str.strip().ne("")]
place["horse_number"] = place["horse_number"].astype(str).str.strip().str.zfill(2)
place["place_payoff"] = pd.to_numeric(place["place_payoff"], errors="coerce")
```

### 集計時の注意

- **返還**（`refund_number_1〜5`）に自分の買い目の馬番が入っているレースは、投資も払戻も除外してください。含めると回収率が歪みます。
- 馬番はゼロ埋め2桁に揃えてから結合してください。片方が `"1"`、もう片方が `"01"` だと全件不的中になります。
- 不的中は `NaN` ではなく **0** として投資額の分母に残してください。`dropna()` すると的中したレースだけで割ることになり、回収率が跳ね上がります。
- 分母は「購入点数 × 100円」です。レース数ではありません。

---

## 参考リンク

- [JRDB-API ドキュメント](https://jrdb-api.keiba.bigtime.world/docs)
- [コード表（全7種）](https://jrdb-api.keiba.bigtime.world/docs/codes)
- [レスポンスフィールド説明](https://jrdb-api.keiba.bigtime.world/docs/fields)
- [API Playground](https://jrdb-api.keiba.bigtime.world/playground)
- [JRDB データ仕様（本家）](https://jrdb.com/data_introduction/)
