導入
競馬では「重馬場は前残りになりやすい」と言われることがあります。馬場が悪くなると後方から差す馬が伸びにくく、前に行った馬が残りやすいという考え方です。
しかし、本当に重馬場では前残りが増えるのでしょうか。地方競馬の過去データを使い、馬場状態別に脚質成績を比較して検証します。
検証に使ったデータ
- 使用期間:2017/01/01〜2025/12/19
- 前走情報付き対象頭数:1,060,675
- 対象レース数:104,575
- 除外件数(特殊成績・馬場状態不明・通過順不明を合計):273,210
- 帯広競馬(馬場状態が数値表記)は今回の集計から除外しています
馬場状態別件数: 良 510,835件 / 稍重 219,836件 / 重 182,697件 / 不良 147,307件
今回の前残りの定義
この記事では、4角通過順をもとに以下のように脚質を分類しました。
- 逃げ:4角1番手
- 先行:4角2〜4番手
- 中団:4角5〜8番手
- 後方:4角9番手以上
4角位置は、DBの通過4カラムを優先して使用し、存在しない場合は通過カラムの末尾値を抽出しました。
また補足として、頭数比の相対位置(前方25%以内・中団〜後方)による分類も行っています。
前残りの判定:良馬場と比較して、重・不良馬場で逃げ・先行の3着内率が上昇し、後方馬の3着内率が下落する場合を「前残り傾向あり」と判断します。
馬場状態別の脚質成績(全馬場)
良・稍重・重・不良・重不良合算の馬場状態別に、脚質ごとの成績を集計しました。
| 馬場 | 脚質_絶対 | 対象頭数 | 対象レース数 | 勝率 | 連対率 | 3着内率 | 平均人気 | 平均単勝オッズ | 単勝回収率 |
|---|---|---|---|---|---|---|---|---|---|
| 良 | ①逃げ | 54917 | 50712 | 48.69% | 69.72% | 80.54% | 3.02 | 11.41 | 2.74 |
| 良 | ②先行 | 151679 | 50686 | 12.86% | 32.83% | 51.76% | 4.18 | 21.97 | 0.96 |
| 良 | ③中団 | 194862 | 50684 | 2.09% | 6.16% | 13.51% | 6.15 | 55.99 | 0.27 |
| 良 | ④後方 | 109377 | 39516 | 0.42% | 1.29% | 2.90% | 8.51 | 114.31 | 0.09 |
| 稍重 | ①逃げ | 23417 | 21483 | 49.40% | 70.55% | 81.05% | 3.03 | 11.33 | 2.76 |
| 稍重 | ②先行 | 64303 | 21475 | 12.55% | 32.61% | 51.74% | 4.21 | 22.31 | 0.97 |
| 稍重 | ③中団 | 82904 | 21472 | 2.03% | 5.96% | 13.17% | 6.19 | 56.79 | 0.26 |
| 稍重 | ④後方 | 49212 | 17309 | 0.37% | 1.15% | 2.72% | 8.54 | 115.46 | 0.09 |
| 重 | ①逃げ | 19194 | 17813 | 51.80% | 72.92% | 83.10% | 2.96 | 10.44 | 2.94 |
| 重 | ②先行 | 53246 | 17807 | 12.31% | 32.89% | 52.72% | 4.21 | 22.05 | 0.96 |
| 重 | ③中団 | 69105 | 17809 | 1.72% | 5.35% | 12.18% | 6.22 | 56.46 | 0.22 |
| 重 | ④後方 | 41152 | 14451 | 0.33% | 1.08% | 2.49% | 8.54 | 111.36 | 0.06 |
| 不良 | ①逃げ | 15527 | 14566 | 55.26% | 76.30% | 85.82% | 2.92 | 10.20 | 3.16 |
| 不良 | ②先行 | 43615 | 14562 | 11.66% | 32.96% | 53.73% | 4.18 | 20.98 | 0.94 |
| 不良 | ③中団 | 56372 | 14561 | 1.48% | 4.67% | 11.14% | 6.18 | 53.14 | 0.22 |
| 不良 | ④後方 | 31793 | 11668 | 0.26% | 0.97% | 2.17% | 8.43 | 103.62 | 0.06 |
| 重・不良 | ①逃げ | 34721 | 32379 | 53.35% | 74.43% | 84.32% | 2.94 | 10.33 | 3.04 |
| 重・不良 | ②先行 | 96861 | 32369 | 12.02% | 32.92% | 53.18% | 4.20 | 21.57 | 0.95 |
| 重・不良 | ③中団 | 125477 | 32370 | 1.62% | 5.04% | 11.71% | 6.20 | 54.97 | 0.22 |
| 重・不良 | ④後方 | 72945 | 26119 | 0.30% | 1.03% | 2.35% | 8.49 | 107.99 | 0.06 |
単勝回収率は1レース100円換算の参考値です。
良馬場と重・不良馬場の比較
良馬場と重・不良馬場(合算)で、脚質別成績がどう変わるかをまとめました。
| 馬場 | 脚質_絶対 | 対象頭数 | 勝率 | 連対率 | 3着内率 | 平均単勝オッズ | 単勝回収率 |
|---|---|---|---|---|---|---|---|
| 良 | ①逃げ | 54917 | 48.69% | 69.72% | 80.54% | 11.41 | 2.74 |
| 良 | ②先行 | 151679 | 12.86% | 32.83% | 51.76% | 21.97 | 0.96 |
| 良 | ③中団 | 194862 | 2.09% | 6.16% | 13.51% | 55.99 | 0.27 |
| 良 | ④後方 | 109377 | 0.42% | 1.29% | 2.90% | 114.31 | 0.09 |
| 重・不良 | ①逃げ | 34721 | 53.35% | 74.43% | 84.32% | 10.33 | 3.04 |
| 重・不良 | ②先行 | 96861 | 12.02% | 32.92% | 53.18% | 21.57 | 0.95 |
| 重・不良 | ③中団 | 125477 | 1.62% | 5.04% | 11.71% | 54.97 | 0.22 |
| 重・不良 | ④後方 | 72945 | 0.30% | 1.03% | 2.35% | 107.99 | 0.06 |
3着内構成比(1〜3着に入った馬の脚質内訳)
3着内に入った馬の脚質構成比を確認します。逃げ・先行の比率が良馬場より高ければ「前残り傾向」と言えます。
| 脚質_絶対 | 3着内構成比(%) | 合計頭数 | 馬場グループ |
|---|---|---|---|
| ①逃げ | 29.1 | 510835 | 良 |
| ②先行 | 51.6 | 510835 | 良 |
| ③中団 | 17.3 | 510835 | 良 |
| ④後方 | 2.1 | 510835 | 良 |
| ①逃げ | 30.1 | 330004 | 重・不良 |
| ②先行 | 53.0 | 330004 | 重・不良 |
| ③中団 | 15.1 | 330004 | 重・不良 |
| ④後方 | 1.8 | 330004 | 重・不良 |
逃げ馬の3着内率:良 80.54% → 重・不良 84.32%
先行馬の3着内率:良 51.76% → 重・不良 53.18%
後方馬の3着内率:良 2.90% → 重・不良 2.35%
相対位置分類でも確認(補足)
頭数比による相対位置分類(前方25%以内・中団・後方)での集計も参考として示します。
| 馬場 | 脚質_相対 | 対象頭数 | 勝率 | 連対率 | 3着内率 |
|---|---|---|---|---|---|
| 良 | ①前方 | 114307 | 32.92% | 56.75% | 70.69% |
| 良 | ②中団 | 179082 | 6.40% | 17.46% | 32.64% |
| 良 | ③後方 | 217446 | 0.78% | 2.46% | 5.97% |
| 重・不良 | ①前方 | 73587 | 34.56% | 59.71% | 74.04% |
| 重・不良 | ②中団 | 115609 | 5.38% | 15.74% | 31.16% |
| 重・不良 | ③後方 | 140808 | 0.54% | 1.90% | 4.76% |
人気別に見た結果
前残り傾向が「人気馬が前に行っているだけ」ではないかを確認します。
| 人気グループ | 脚質_絶対 | 馬場グループ | 対象頭数 | 勝率 | 連対率 | 3着内率 |
|---|---|---|---|---|---|---|
| ①1番人気 | ①逃げ | 良 | 19351 | 72.13% | 87.90% | 93.54% |
| ①1番人気 | ②先行 | 良 | 22818 | 33.64% | 60.50% | 76.17% |
| ①1番人気 | ③中団 | 良 | 7397 | 12.60% | 25.92% | 40.23% |
| ①1番人気 | ④後方 | 良 | 1146 | 5.85% | 12.04% | 19.63% |
| ②2〜3番人気 | ①逃げ | 良 | 18361 | 45.36% | 71.53% | 83.68% |
| ②2〜3番人気 | ②先行 | 良 | 48613 | 14.14% | 40.87% | 62.90% |
| ②2〜3番人気 | ③中団 | 良 | 28652 | 4.70% | 13.48% | 26.75% |
| ②2〜3番人気 | ④後方 | 良 | 5798 | 1.88% | 5.52% | 10.49% |
| ③4〜6番人気 | ①逃げ | 良 | 11689 | 29.36% | 52.93% | 69.01% |
| ③4〜6番人気 | ②先行 | 良 | 51611 | 7.41% | 23.89% | 44.18% |
| ③4〜6番人気 | ③中団 | 良 | 69431 | 1.76% | 5.94% | 14.46% |
| ③4〜6番人気 | ④後方 | 良 | 18864 | 0.72% | 2.41% | 5.54% |
| ④7番人気以下 | ①逃げ | 良 | 5516 | 18.53% | 35.48% | 48.88% |
| ④7番人気以下 | ②先行 | 良 | 28637 | 3.95% | 13.24% | 27.06% |
| ④7番人気以下 | ③中団 | 良 | 89382 | 0.63% | 2.35% | 6.32% |
| ④7番人気以下 | ④後方 | 良 | 83569 | 0.17% | 0.60% | 1.54% |
| ①1番人気 | ①逃げ | 重・不良 | 12815 | 74.31% | 90.01% | 94.69% |
| ①1番人気 | ②先行 | 重・不良 | 14179 | 30.54% | 58.21% | 75.12% |
| ①1番人気 | ③中団 | 重・不良 | 4586 | 9.55% | 21.11% | 34.10% |
| ①1番人気 | ④後方 | 重・不良 | 799 | 4.13% | 9.64% | 14.27% |
| ②2〜3番人気 | ①逃げ | 重・不良 | 11474 | 49.59% | 75.40% | 86.62% |
| ②2〜3番人気 | ②先行 | 重・不良 | 31363 | 13.43% | 41.01% | 63.72% |
| ②2〜3番人気 | ③中団 | 重・不良 | 18057 | 3.64% | 11.16% | 23.38% |
| ②2〜3番人気 | ④後方 | 重・不良 | 3864 | 1.32% | 4.40% | 8.44% |
| ③4〜6番人気 | ①逃げ | 重・不良 | 7172 | 35.69% | 59.84% | 74.75% |
| ③4〜6番人気 | ②先行 | 重・不良 | 33023 | 7.08% | 24.27% | 46.03% |
| ③4〜6番人気 | ③中団 | 重・不良 | 44167 | 1.44% | 4.84% | 12.41% |
| ③4〜6番人気 | ④後方 | 重・不良 | 12526 | 0.51% | 2.00% | 4.44% |
| ④7番人気以下 | ①逃げ | 重・不良 | 3260 | 22.98% | 41.87% | 56.50% |
| ④7番人気以下 | ②先行 | 重・不良 | 18296 | 4.15% | 15.09% | 30.99% |
| ④7番人気以下 | ③中団 | 重・不良 | 58667 | 0.51% | 2.05% | 5.85% |
| ④7番人気以下 | ④後方 | 重・不良 | 55756 | 0.13% | 0.46% | 1.29% |
重・不良馬場での前残り傾向が、低人気馬でも見られるかを確認してください。
競馬場別に見た結果
競馬場によって、重・不良馬場での前残り傾向の強さに差があるかを確認します。
(対象頭数が合計30頭未満の組み合わせは除外しています)
| 場所 | 脚質 | 良_3着内率 | 重不良_3着内率 | 良_頭数 | 重不良_頭数 | 3着内率差(重不良-良) |
|---|---|---|---|---|---|---|
| 佐賀 | ①逃げ | 90.37% | 91.93% | 5089 | 3568 | +1.56pp |
| 佐賀 | ②先行 | 54.70% | 56.19% | 15160 | 10662 | +1.49pp |
| 佐賀 | ③中団 | 11.04% | 9.58% | 19831 | 13909 | -1.45pp |
| 佐賀 | ④後方 | 0.98% | 0.76% | 10096 | 7002 | -0.22pp |
| 名古屋 | ①逃げ | 87.83% | 90.59% | 6105 | 3007 | +2.76pp |
| 名古屋 | ②先行 | 52.78% | 53.56% | 17207 | 8428 | +0.78pp |
| 名古屋 | ③中団 | 12.00% | 10.84% | 22701 | 11089 | -1.16pp |
| 名古屋 | ④後方 | 1.24% | 1.07% | 12965 | 6159 | -0.17pp |
| 園田 | ①逃げ | 81.13% | 86.44% | 8300 | 3289 | +5.31pp |
| 園田 | ②先行 | 51.25% | 52.60% | 23401 | 9514 | +1.34pp |
| 園田 | ③中団 | 14.22% | 12.24% | 30078 | 12179 | -1.98pp |
| 園田 | ④後方 | 2.24% | 1.74% | 16462 | 6510 | -0.51pp |
| 大井 | ①逃げ | 59.32% | 61.08% | 4498 | 3009 | +1.77pp |
| 大井 | ②先行 | 43.06% | 45.70% | 12568 | 8451 | +2.64pp |
| 大井 | ③中団 | 19.76% | 18.28% | 16540 | 11083 | -1.48pp |
| 大井 | ④後方 | 6.41% | 6.02% | 19641 | 12850 | -0.39pp |
| 姫路 | ①逃げ | 78.86% | 83.82% | 1206 | 241 | +4.96pp |
| 姫路 | ②先行 | 50.90% | 53.90% | 3234 | 629 | +3.00pp |
| 姫路 | ③中団 | 14.65% | 11.58% | 4219 | 812 | -3.07pp |
| 姫路 | ④後方 | 2.09% | 0.86% | 2388 | 466 | -1.24pp |
| 川崎 | ①逃げ | 72.76% | 79.48% | 3359 | 1715 | +6.72pp |
| 川崎 | ②先行 | 48.43% | 50.65% | 9119 | 4535 | +2.22pp |
| 川崎 | ③中団 | 15.84% | 12.91% | 11626 | 5896 | -2.93pp |
| 川崎 | ④後方 | 2.61% | 2.13% | 9229 | 4403 | -0.48pp |
| 水沢 | ①逃げ | 85.08% | 85.82% | 2473 | 2328 | +0.75pp |
| 水沢 | ②先行 | 54.74% | 54.58% | 6651 | 6085 | -0.17pp |
| 水沢 | ③中団 | 10.85% | 10.26% | 8416 | 7932 | -0.59pp |
| 水沢 | ④後方 | 1.28% | 1.07% | 3291 | 3564 | -0.21pp |
| 浦和 | ①逃げ | 89.82% | 92.54% | 2494 | 1260 | +2.72pp |
| 浦和 | ②先行 | 55.65% | 56.14% | 7371 | 3705 | +0.49pp |
| 浦和 | ③中団 | 10.31% | 9.41% | 9648 | 4888 | -0.90pp |
| 浦和 | ④後方 | 0.85% | 0.51% | 7261 | 3712 | -0.34pp |
| 盛岡 | ①逃げ | 74.88% | 75.18% | 3567 | 1527 | +0.30pp |
| 盛岡 | ②先行 | 51.67% | 51.78% | 9684 | 4077 | +0.11pp |
| 盛岡 | ③中団 | 15.32% | 14.99% | 12287 | 5157 | -0.33pp |
| 盛岡 | ④後方 | 3.44% | 3.46% | 5034 | 2139 | +0.02pp |
| 笠松 | ①逃げ | 86.59% | 87.17% | 5823 | 1302 | +0.59pp |
| 笠松 | ②先行 | 56.06% | 57.22% | 15682 | 3532 | +1.16pp |
| 笠松 | ③中団 | 10.55% | 9.69% | 19974 | 4501 | -0.87pp |
| 笠松 | ④後方 | 0.88% | 0.29% | 4443 | 1050 | -0.59pp |
| 船橋 | ①逃げ | 61.80% | 65.81% | 2605 | 1404 | +4.01pp |
| 船橋 | ②先行 | 48.63% | 50.09% | 7787 | 4212 | +1.46pp |
| 船橋 | ③中団 | 20.06% | 17.91% | 9836 | 5411 | -2.15pp |
| 船橋 | ④後方 | 6.22% | 5.05% | 6914 | 4179 | -1.17pp |
| 金沢 | ①逃げ | 91.26% | 92.90% | 3764 | 3266 | +1.64pp |
| 金沢 | ②先行 | 54.48% | 54.74% | 10272 | 8973 | +0.26pp |
| 金沢 | ③中団 | 9.03% | 8.15% | 12421 | 11110 | -0.89pp |
| 金沢 | ④後方 | 1.00% | 0.87% | 3495 | 3680 | -0.13pp |
| 門別 | ①逃げ | 73.41% | 73.63% | 4209 | 2666 | +0.22pp |
| 門別 | ②先行 | 47.60% | 46.96% | 9283 | 5700 | -0.64pp |
| 門別 | ③中団 | 15.37% | 15.20% | 11719 | 7321 | -0.17pp |
| 門別 | ④後方 | 3.16% | 2.95% | 5153 | 3526 | -0.21pp |
| 高知 | ①逃げ | 89.68% | 92.16% | 1425 | 6139 | +2.48pp |
| 高知 | ②先行 | 54.65% | 55.94% | 4260 | 18358 | +1.29pp |
| 高知 | ③中団 | 10.98% | 9.60% | 5566 | 24189 | -1.38pp |
| 高知 | ④後方 | 1.53% | 0.95% | 3005 | 13705 | -0.58pp |
サンプル数が少ない競馬場・脚質の数値は変動が大きいため、参考程度にご覧ください。
距離帯別に見た結果
短距離・マイル前後・中距離で、重・不良馬場の前残り傾向に差があるかを見ます。
| 距離帯 | 脚質_絶対 | 馬場グループ | 対象頭数 | 勝率 | 連対率 | 3着内率 |
|---|---|---|---|---|---|---|
| マイル前後(1300〜1700m) | ①逃げ | 良 | 41841 | 50.01% | 71.45% | 82.24% |
| マイル前後(1300〜1700m) | ②先行 | 良 | 116654 | 12.79% | 33.06% | 52.36% |
| マイル前後(1300〜1700m) | ③中団 | 良 | 150539 | 1.96% | 5.83% | 13.05% |
| マイル前後(1300〜1700m) | ④後方 | 良 | 80004 | 0.31% | 1.00% | 2.39% |
| 中距離(1800m〜) | ①逃げ | 良 | 2366 | 47.08% | 69.27% | 80.30% |
| 中距離(1800m〜) | ②先行 | 良 | 6609 | 13.16% | 32.80% | 51.51% |
| 中距離(1800m〜) | ③中団 | 良 | 8369 | 2.28% | 6.33% | 13.71% |
| 中距離(1800m〜) | ④後方 | 良 | 5653 | 0.50% | 1.20% | 2.72% |
| 短距離(〜1200m) | ①逃げ | 良 | 10710 | 43.90% | 63.05% | 73.91% |
| 短距離(〜1200m) | ②先行 | 良 | 28416 | 13.08% | 31.89% | 49.35% |
| 短距離(〜1200m) | ③中団 | 良 | 35954 | 2.58% | 7.49% | 15.38% |
| 短距離(〜1200m) | ④後方 | 良 | 23720 | 0.76% | 2.31% | 4.66% |
| マイル前後(1300〜1700m) | ①逃げ | 重・不良 | 27237 | 55.50% | 77.05% | 86.82% |
| マイル前後(1300〜1700m) | ②先行 | 重・不良 | 77049 | 11.86% | 33.25% | 54.00% |
| マイル前後(1300〜1700m) | ③中団 | 重・不良 | 100265 | 1.40% | 4.51% | 11.02% |
| マイル前後(1300〜1700m) | ④後方 | 重・不良 | 55306 | 0.19% | 0.69% | 1.73% |
| 中距離(1800m〜) | ①逃げ | 重・不良 | 1387 | 54.22% | 75.20% | 83.20% |
| 中距離(1800m〜) | ②先行 | 重・不良 | 3822 | 11.33% | 31.95% | 52.62% |
| 中距離(1800m〜) | ③中団 | 重・不良 | 4932 | 1.82% | 5.41% | 12.15% |
| 中距離(1800m〜) | ④後方 | 重・不良 | 3491 | 0.23% | 0.92% | 2.29% |
| 短距離(〜1200m) | ①逃げ | 重・不良 | 6097 | 43.53% | 62.54% | 73.38% |
| 短距離(〜1200m) | ②先行 | 重・不良 | 15990 | 12.95% | 31.57% | 49.32% |
| 短距離(〜1200m) | ③中団 | 重・不良 | 20280 | 2.62% | 7.56% | 15.06% |
| 短距離(〜1200m) | ④後方 | 重・不良 | 14148 | 0.76% | 2.38% | 4.77% |
データから分かったこと
- 逃げ馬の3着内率は良馬場 80.54% → 重・不良馬場 84.32% と変化しました。
- 先行馬の3着内率は良馬場 51.76% → 重・不良馬場 53.18% と変化しました。
- 後方馬の3着内率は良馬場 2.90% → 重・不良馬場 2.35% と変化しました。
- 3着内構成比でも、良馬場と重・不良馬場で逃げ・先行の比率がどう変化するかを確認できます。
- 競馬場や距離帯によって傾向の強さに差があり、一律に「重馬場=前残り」とは言えないことが分かります。
- 人気別に見ても同様の傾向が維持されるかは、表から確認してください。
注意点・限界
- 通過順はレース後に確定する情報であり、レース前に完全には分かりません。今回の分析は「結果からの脚質分類」であり、予測にそのまま使う場合は注意が必要です。
- 馬場状態の表記や基準は競馬場・時期によって差がある可能性があります。
- 前残りには馬場だけでなく、ペース・展開・頭数・騎手の判断なども影響します。
- 帯広競馬(馬場状態が数値表記)は今回の集計から除外しています。
- 単勝回収率はサンプル数や高配当の影響を受けます。
- 競馬場別集計でサンプル数が少ない場合、数値の信頼性は低くなります。
まとめ
地方競馬データ(2017/01/01〜2025/12/19)を使い、馬場状態別に脚質成績を比較しました。
- 今回のデータでは、逃げ馬の3着内率が良馬場 80.54% から重・不良馬場 84.32% へと変化しました。
- ただし、この傾向の強さは競馬場・距離帯・人気帯によって異なり、すべての条件で同じ傾向が出るわけではありません。
- 「重馬場は必ず前残り」と断定するのではなく、競馬場・距離・人気なども考慮して総合的に判断することが重要です。
分析スクリプト
"""
【地方競馬データ検証】重馬場では本当に前残りが増えるのか?
"""
import sqlite3
import pandas as pd
import numpy as np
import re
import html as html_lib
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
DB_PATH = SCRIPT_DIR.parents[2] / 'common.db'
OUTPUT_DIR = SCRIPT_DIR
DATA_DIR = SCRIPT_DIR / 'data'
DATA_DIR.mkdir(parents=True, exist_ok=True)
# ================================================================
# 1. データ読み込み
# ================================================================
cols = [
'日付', '場所', 'レースID', 'レース番号', '距離', '馬場状態',
'馬名', '馬番', '着順', '人気', '単勝', '枠番',
'通過', '通過1', '通過2', '通過3', '通過4', '同走馬の数',
'失', '取', '除', '中', '取消レース', '失格レース', '除外レース', '中止レース',
]
cols_quoted = [f'"{c}"' for c in cols]
query = 'SELECT ' + ','.join(cols_quoted) + ' FROM result_table'
with sqlite3.connect(DB_PATH) as conn:
df = pd.read_sql_query(query, conn)
df['日付'] = pd.to_datetime(df['日付'].astype(str), format='%Y%m%d', errors='coerce')
# ================================================================
# 2. 除外条件
# ================================================================
special_flags = ['失', '取', '除', '中', '取消レース', '失格レース', '除外レース', '中止レース']
for flag in special_flags:
df[flag] = df[flag].fillna(0).astype(int)
n_total = len(df)
n_special = df[special_flags].sum(axis=1).gt(0).sum()
mask_valid = (
df[special_flags].sum(axis=1) == 0
) & df['着順'].between(1, 99) & df['単勝'].notna() & (df['単勝'] > 0)
df = df[mask_valid].copy()
# ================================================================
# 3. 馬場状態 正規化・フィルタ
# ================================================================
def normalize_track(s):
if pd.isna(s): return None
s = str(s).strip()
if s == '稍': return '稍重'
if s == '不': return '不良'
if s in ('良', '稍重', '重', '不良'):
return s
# 帯広などの数値表記は除外
return None
df['馬場'] = df['馬場状態'].apply(normalize_track)
n_no_track = df['馬場'].isna().sum()
df = df[df['馬場'].notna()].copy()
# ================================================================
# 4. 通過順から4角位置を抽出
# ================================================================
def extract_4corner(row):
"""通過4 があればそれを使い、なければ通過文字列の末尾値を使う。"""
v4 = row['通過4']
if pd.notna(v4):
try:
return int(float(v4))
except:
pass
t = row['通過']
if pd.isna(t):
return None
parts = str(t).strip().split('-')
try:
return int(parts[-1])
except:
return None
df['4角位置'] = df.apply(extract_4corner, axis=1)
n_no_passage = df['4角位置'].isna().sum()
df_main = df[df['4角位置'].notna() & df['同走馬の数'].notna()].copy()
df_main['4角位置'] = df_main['4角位置'].astype(int)
df_main['頭数'] = df_main['同走馬の数'].astype(int)
# ================================================================
# 5. 脚質分類(絶対順位 + 相対位置)
# ================================================================
def classify_kyakushitsu_abs(pos: int) -> str:
"""4角絶対位置による脚質分類"""
if pos == 1: return '①逃げ'
if pos <= 4: return '②先行'
if pos <= 8: return '③中団'
return '④後方'
def classify_kyakushitsu_rel(pos: int, n: int) -> str:
"""頭数に対する相対位置による脚質分類"""
ratio = pos / n
if ratio <= 0.25: return '①前方'
if ratio <= 0.60: return '②中団'
return '③後方'
df_main['脚質_絶対'] = df_main['4角位置'].apply(classify_kyakushitsu_abs)
df_main['脚質_相対'] = df_main.apply(
lambda r: classify_kyakushitsu_rel(r['4角位置'], r['頭数']), axis=1
)
# ================================================================
# 6. 成績フラグ・グループ変数
# ================================================================
df_main['勝ち'] = (df_main['着順'] == 1).astype(int)
df_main['連対'] = (df_main['着順'] <= 2).astype(int)
df_main['3着内'] = (df_main['着順'] <= 3).astype(int)
df_main['単勝回収'] = np.where(df_main['着順'] == 1, df_main['単勝'], 0.0)
# 馬場グループ
TRACK_ORDER = {'良': 0, '稍重': 1, '重': 2, '不良': 3}
df_main['馬場順'] = df_main['馬場'].map(TRACK_ORDER)
def pop_group(p):
try:
p = int(p)
except: return 'その他'
if p == 1: return '①1番人気'
if p <= 3: return '②2〜3番人気'
if p <= 6: return '③4〜6番人気'
return '④7番人気以下'
def dist_group(d):
try:
d = int(d)
except: return '不明'
if d <= 1200: return '短距離(〜1200m)'
if d <= 1700: return 'マイル前後(1300〜1700m)'
return '中距離(1800m〜)'
df_main['人気グループ'] = df_main['人気'].apply(pop_group)
df_main['距離帯'] = df_main['距離'].apply(dist_group)
# 道悪・重不良グループを追加した行も作る
df_heavy = df_main[df_main['馬場'].isin(['重', '不良'])].copy()
df_heavy['馬場'] = '重・不良'
df_muddy = df_main[df_main['馬場'].isin(['稍重', '重', '不良'])].copy()
df_muddy['馬場'] = '道悪'
df_aug = pd.concat([df_main, df_heavy, df_muddy], ignore_index=True)
# ================================================================
# 7. 集計関数
# ================================================================
def agg_group(df_g):
n = len(df_g)
if n == 0:
return {k: np.nan for k in ['対象頭数','対象レース数','勝率','連対率','3着内率',
'平均人気','平均単勝オッズ','単勝回収率']}
return {
'対象頭数': n,
'対象レース数': df_g[['日付','レースID']].drop_duplicates().shape[0],
'勝率': df_g['勝ち'].mean(),
'連対率': df_g['連対'].mean(),
'3着内率': df_g['3着内'].mean(),
'平均人気': df_g['人気'].mean(),
'平均単勝オッズ': df_g['単勝'].mean(),
'単勝回収率': df_g['単勝回収'].sum() / n,
}
def agg_pivot(df_g, by_track, by_style):
rows = []
for track in df_g[by_track].unique():
for style in sorted(df_g[by_style].unique()):
sub = df_g[(df_g[by_track] == track) & (df_g[by_style] == style)]
row = {by_track: track, by_style: style}
row.update(agg_group(sub))
rows.append(row)
return pd.DataFrame(rows)
# ---- A. 馬場状態 × 脚質(絶対)----
track_style_abs = agg_pivot(
df_aug[df_aug['馬場'].isin(['良', '稍重', '重', '不良', '道悪', '重・不良'])],
'馬場', '脚質_絶対'
).sort_values(['馬場', '脚質_絶対'])
track_order_display = ['良', '稍重', '重', '不良', '道悪', '重・不良']
track_style_abs['馬場_ord'] = track_style_abs['馬場'].apply(
lambda x: track_order_display.index(x) if x in track_order_display else 99
)
track_style_abs = track_style_abs.sort_values(['馬場_ord', '脚質_絶対']).drop(columns=['馬場_ord'])
# ---- B. 馬場状態 × 脚質(相対)----
track_style_rel = agg_pivot(
df_aug[df_aug['馬場'].isin(['良', '稍重', '重', '不良', '道悪', '重・不良'])],
'馬場', '脚質_相対'
).sort_values(['馬場', '脚質_相対'])
track_style_rel['馬場_ord'] = track_style_rel['馬場'].apply(
lambda x: track_order_display.index(x) if x in track_order_display else 99
)
track_style_rel = track_style_rel.sort_values(['馬場_ord', '脚質_相対']).drop(columns=['馬場_ord'])
# ---- C. 競馬場別 (良 vs 重・不良, 絶対脚質) ----
df_ryou = df_main[df_main['馬場'] == '良'].copy()
df_heavy2 = df_main[df_main['馬場'].isin(['重', '不良'])].copy()
def agg_by_loc(df_g, style_col):
rows = []
for loc in sorted(df_g['場所'].unique()):
for sty in sorted(df_g[style_col].unique()):
sub = df_g[(df_g['場所'] == loc) & (df_g[style_col] == sty)]
row = {'場所': loc, style_col: sty}
row.update(agg_group(sub))
rows.append(row)
return pd.DataFrame(rows)
loc_ryou = agg_by_loc(df_ryou, '脚質_絶対')
loc_heavy = agg_by_loc(df_heavy2, '脚質_絶対')
loc_ryou['馬場グループ'] = '良'
loc_heavy['馬場グループ'] = '重・不良'
loc_combined = pd.concat([loc_ryou, loc_heavy], ignore_index=True)
# ---- D. 距離帯別 (良 vs 重・不良) ----
dist_ryou = (
df_ryou.groupby(['距離帯', '脚質_絶対'])
.agg(対象頭数=('勝ち','size'), 勝率=('勝ち','mean'),
連対率=('連対','mean'), t3着内率=('3着内','mean'))
.rename(columns={'t3着内率':'3着内率'}).reset_index()
)
dist_ryou['馬場グループ'] = '良'
dist_heavy = (
df_heavy2.groupby(['距離帯', '脚質_絶対'])
.agg(対象頭数=('勝ち','size'), 勝率=('勝ち','mean'),
連対率=('連対','mean'), t3着内率=('3着内','mean'))
.rename(columns={'t3着内率':'3着内率'}).reset_index()
)
dist_heavy['馬場グループ'] = '重・不良'
dist_combined = pd.concat([dist_ryou, dist_heavy], ignore_index=True)
# ---- E. 人気グループ別 (良 vs 重・不良) ----
pop_ryou = (
df_ryou.groupby(['人気グループ', '脚質_絶対'])
.agg(対象頭数=('勝ち','size'), 勝率=('勝ち','mean'),
連対率=('連対','mean'), t3着内率=('3着内','mean'))
.rename(columns={'t3着内率':'3着内率'}).reset_index()
)
pop_ryou['馬場グループ'] = '良'
pop_heavy = (
df_heavy2.groupby(['人気グループ', '脚質_絶対'])
.agg(対象頭数=('勝ち','size'), 勝率=('勝ち','mean'),
連対率=('連対','mean'), t3着内率=('3着内','mean'))
.rename(columns={'t3着内率':'3着内率'}).reset_index()
)
pop_heavy['馬場グループ'] = '重・不良'
pop_combined = pd.concat([pop_ryou, pop_heavy], ignore_index=True)
# ---- F. 3着内構成比(脚質別の入着割合)----
def calc_composition(df_g, style_col):
in3 = df_g[df_g['3着内'] == 1]
comp = in3.groupby(style_col).size().rename('3着内頭数')
total3 = len(in3)
comp = (comp / total3 * 100).round(1).reset_index()
comp.columns = [style_col, '3着内構成比(%)']
comp['合計頭数'] = len(df_g)
return comp
comp_ryou = calc_composition(df_ryou, '脚質_絶対'); comp_ryou['馬場グループ'] = '良'
comp_heavy = calc_composition(df_heavy2, '脚質_絶対'); comp_heavy['馬場グループ'] = '重・不良'
comp_all = pd.concat([comp_ryou, comp_heavy], ignore_index=True)
# ================================================================
# 8. CSV 保存
# ================================================================
category_tables = {
'馬場x脚質_絶対': track_style_abs,
'馬場x脚質_相対': track_style_rel,
'競馬場x脚質': loc_combined,
'距離帯x脚質': dist_combined,
'人気グループx脚質': pop_combined,
'3着内構成比': comp_all,
}
for name, tbl in category_tables.items():
tbl.to_csv(DATA_DIR / f'{name}.csv', index=False)
# ================================================================
# 9. 集計値定義(記事本文用)
# ================================================================
def get_val(df_g, track, style, col):
sub = df_g[(df_g['馬場'] == track) & (df_g['脚質_絶対'] == style)]
if sub.empty: return np.nan
return sub[col].values[0]
date_min = df_main['日付'].min().strftime('%Y/%m/%d')
date_max = df_main['日付'].max().strftime('%Y/%m/%d')
total_rows = f'{len(df_main):,}'
total_races = f'{df_main[["日付","レースID"]].drop_duplicates().shape[0]:,}'
n_excluded = f'{n_special + n_no_track + n_no_passage:,}'
def pct(v): return f'{v*100:.2f}%' if pd.notna(v) else '-'
def f2(v): return f'{v:.2f}' if pd.notna(v) else '-'
# 良 vs 重・不良 の逃げ成績比較
nige_ryou_3rd = get_val(track_style_abs[track_style_abs['馬場']=='良'], '良', '①逃げ', '3着内率')
nige_heavy_3rd = get_val(track_style_abs[track_style_abs['馬場']=='重・不良'], '重・不良', '①逃げ', '3着内率')
senkou_ryou_3rd = get_val(track_style_abs[track_style_abs['馬場']=='良'], '良', '②先行', '3着内率')
senkou_heavy_3rd = get_val(track_style_abs[track_style_abs['馬場']=='重・不良'], '重・不良', '②先行', '3着内率')
kohou_ryou_3rd = get_val(track_style_abs[track_style_abs['馬場']=='良'], '良', '④後方', '3着内率')
kohou_heavy_3rd = get_val(track_style_abs[track_style_abs['馬場']=='重・不良'], '重・不良', '④後方', '3着内率')
# サンプル数確認
track_counts = df_main['馬場'].value_counts()
