【地方競馬データ検証】前走大敗馬は次走で巻き返せるのか?

分析・検証

導入

競馬では「前走大敗馬は消し」という考え方がよく聞かれます。地方競馬データを使い、前走の敗退が次走の成績にどう影響するかを実際の数字で検証します。

検証に使ったデータ

  • 使用期間:2017/01/05〜2025/12/19
  • 対象レース数:115,739
  • 対象有効出走数:1,127,825
  • 除外条件:取消・失格・除外・中止などの特殊成績
  • 馬の識別は「馬名」で行い、直前レースから次走を追跡しました

複勝払戻データはDBに含まれていないため、複勝回収率は算出できません。

今回の検証条件

前走大敗の定義は複数用意しました。着順だけでなく、着差や人気との比較も加えることで、敗因の重さや再起力を幅広く比較します。

  • 前走着順が8着以下
  • 前走着順が10着以下
  • 前走1着差が2.0秒以上
  • 前走人気より着順が5つ以上悪い
  • 前走人気が5番人気以内かつ前走着順が8着以下

全体結果

各定義の次走成績は以下です。

定義対象頭数対象レース数勝率連対率3着内率平均人気平均単勝オッズ単勝回収率複勝回収率
前走着順8着以下3360101052364.09%9.01%15.00%7.7795.130.64データなし
前走着順10着以下145273715723.52%7.64%12.56%8.36112.020.63データなし
前走1着差2.0秒以上8787791040237.67%16.46%25.87%6.2261.040.69データなし
前走人気より着順が5つ以上悪い70522512499.58%18.79%28.02%5.4232.100.77データなし
前走5番人気以内かつ前走着順8着以下72410538448.15%16.63%25.60%5.6734.810.74データなし

人気別に見た結果(前走着順8着以下)

前走大敗馬でも、今走で上位人気に支持された場合は成績が大きく改善します。

人気対象頭数勝率連対率3着内率
1671941.57%59.44%70.22%
21085220.61%41.54%55.79%
31477912.71%28.27%43.48%
4191438.67%20.23%33.93%
5245035.57%13.87%24.89%
6309493.86%9.64%17.80%
7379812.39%6.30%12.46%
8445911.60%4.33%9.00%
9465190.95%3.03%6.09%
10414290.74%2.10%4.54%
11266980.51%1.58%3.37%
12201490.37%1.09%2.50%
1351210.49%1.27%2.62%
1441950.24%0.67%1.86%
1513450.07%0.89%2.08%
1610370.10%0.68%1.25%

今走1番人気は勝率40%超・3着内率70%超と高水準です。一方で6番人気以下では急降下し、「前走大敗+低人気」の組み合わせは成績が厳しくなります。

場所別に見た結果(前走着順8着以下)

競馬場によって前走大敗馬の次走成績に差が見られます。

場所対象頭数勝率連対率3着内率
大井428613.08%6.95%11.83%
園田401193.99%8.87%15.01%
名古屋312213.95%8.54%14.07%
帯広284445.65%11.86%19.22%
佐賀279803.45%7.84%13.35%
高知276574.60%10.03%16.32%
川崎209093.76%8.66%14.39%
浦和201074.02%8.63%14.47%
門別179603.13%7.26%12.59%
船橋173253.53%8.21%14.02%
金沢149985.16%10.59%17.40%
水沢144264.84%10.40%17.16%
盛岡138434.67%10.42%16.81%
笠松130195.03%11.51%18.85%
姫路51413.99%8.60%14.37%

帯広・笠松・金沢などは比較的勝率・3着内率が高め、大井・門別はやや低めの傾向です。ただし各場で出走条件や頭数が異なるため、単純比較には注意が必要です。

距離変更別に見た結果(前走着順8着以下)

距離変更対象頭数勝率連対率3着内率
同距離1871763.73%8.39%14.23%
距離短縮815435.19%11.04%17.73%
距離延長672913.78%8.28%13.86%

距離短縮に変わった場合、前走大敗馬の次走成績はやや改善する傾向が見られます。

データから分かったこと

  • 「前走着順8着以下」「10着以下」だけで見ると、次走勝率は3〜4%台と低調。
  • 「前走1着差2.0秒以上」や「前走人気より着順が5つ以上悪い」は、単純着順基準より次走成績が良い。
  • 今走で人気が回復した馬は、前走大敗でも巻き返す余地がある。
  • 距離短縮は相対的に次走成績が改善する傾向。

注意点・限界

  • 今回の分析はデータ上の成績とオッズのみを使用しています。
  • 馬体調・展開・騎手判断・落馬・不利など、敗因の詳細はデータからは判別できません。
  • 馬名で馬を追跡しているため、同名馬の混在リスクは完全には排除できません。
  • 単勝回収率はサンプル数や高配当に影響されます。

まとめ

今回の地方競馬データでは、前走大敗馬を一律に「消し」とするのは難しいという傾向が出ました。

  • 着順だけで判断すると確かに次走成績は悪め
  • 今走で人気が高い場合は巻き返す余地がある
  • 距離短縮は比較的良い傾向
  • 「人気より着順が悪い」条件は単純着順基準より次走成績が良い

最終判断には「条件・人気・距離変化・開催場の特性」を併せて見ることが重要です。

分析スクリプト

import sqlite3
import pandas as pd
import numpy as np
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)

cols = [
    '日付', '場所', 'レースID', 'レース番号', '馬名', '馬番', '着順', '人気', '単勝', '1着差',
    '距離', '方向', '馬場状態', '年', '月', '日',
    '失', '取', '除', '中', '取消レース', '失格レース', '除外レース', '中止レース'
]
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)

# 日付を datetime に変換
if df['日付'].dtype == object:
    df['日付'] = pd.to_datetime(df['日付'], format='%Y%m%d', errors='coerce')
else:
    df['日付'] = pd.to_datetime(df['日付'].astype(str), format='%Y%m%d', errors='coerce')

# 特殊成績を除外
special_flags = ['失', '取', '除', '中', '取消レース', '失格レース', '除外レース', '中止レース']
for flag in special_flags:
    if flag in df.columns:
        df[flag] = df[flag].fillna(0).astype(int)

mask_valid_finish = df['着順'].between(1, 99)
mask_no_special = df[special_flags].sum(axis=1) == 0
mask_valid = mask_valid_finish & mask_no_special

df = df[mask_valid].copy()

# 直前レースを同馬名で取得
sort_cols = ['馬名', '日付', 'レースID', 'レース番号']
df = df.sort_values(sort_cols)

df['prev_日付'] = df.groupby('馬名')['日付'].shift(1)
df['prev_着順'] = df.groupby('馬名')['着順'].shift(1)
df['prev_人気'] = df.groupby('馬名')['人気'].shift(1)
df['prev_単勝'] = df.groupby('馬名')['単勝'].shift(1)
df['prev_1着差'] = df.groupby('馬名')['1着差'].shift(1)
df['prev_距離'] = df.groupby('馬名')['距離'].shift(1)
df['prev_場所'] = df.groupby('馬名')['場所'].shift(1)

df = df[df['prev_日付'].notna()].copy()

# 距離変更カテゴリ
conditions = [
    df['距離'] < df['prev_距離'],
    df['距離'] == df['prev_距離'],
    df['距離'] > df['prev_距離'],
]
choices = ['距離短縮', '同距離', '距離延長']
df['距離変更'] = np.select(conditions, choices, default='不明')

df['勝ち'] = (df['着順'] == 1).astype(int)
df['連対'] = (df['着順'] <= 2).astype(int)
df['3着内'] = (df['着順'] <= 3).astype(int)
df['単勝回収'] = np.where(df['着順'] == 1, df['単勝'], 0.0)

# 前走大敗馬定義
criteria = {
    '前走着順8着以下': lambda x: x['prev_着順'] >= 8,
    '前走着順10着以下': lambda x: x['prev_着順'] >= 10,
    '前走1着差2.0秒以上': lambda x: x['prev_1着差'] >= 2.0,
    '前走人気より着順が5つ以上悪い': lambda x: (x['prev_着順'] - x['prev_人気']) >= 5,
    '前走5番人気以内かつ前走着順8着以下': lambda x: (x['prev_人気'] <= 5) & (x['prev_着順'] >= 8),
}

summary_rows = []
category_tables = {}

for label, cond in criteria.items():
    df_label = df[cond(df)].copy()
    count = len(df_label)
    summary_rows.append({
        '定義': label,
        '対象頭数': count,
        '対象レース数': df_label[['日付', 'レースID']].drop_duplicates().shape[0],
        '勝率': df_label['勝ち'].mean() if count else np.nan,
        '連対率': df_label['連対'].mean() if count else np.nan,
        '3着内率': df_label['3着内'].mean() if count else np.nan,
        '平均人気': df_label['人気'].mean() if count else np.nan,
        '平均単勝オッズ': df_label['単勝'].mean() if count else np.nan,
        '単勝回収率': df_label['単勝回収'].sum() / count if count else np.nan,
        '複勝回収率': np.nan,
    })
    # 今走人気別
    pop_table = (
        df_label
        .groupby('人気')
        .agg(
            対象頭数=('勝ち','size'),
            勝率=('勝ち','mean'),
            連対率=('連対','mean'),
            t3着内率=('3着内','mean'),
        )
        .rename(columns={'t3着内率': '3着内率'})
        .reset_index()
        .sort_values('人気')
    )
    category_tables[f'{label}_人気別'] = pop_table
    # 場所別
    loc_table = (
        df_label
        .groupby('場所')
        .agg(
            対象頭数=('勝ち','size'),
            勝率=('勝ち','mean'),
            連対率=('連対','mean'),
            t3着内率=('3着内','mean'),
        )
        .rename(columns={'t3着内率': '3着内率'})
        .reset_index()
        .sort_values('対象頭数', ascending=False)
    )
    category_tables[f'{label}_場所別'] = loc_table
    # 距離変更別
    dist_table = (
        df_label
        .groupby('距離変更')
        .agg(
            対象頭数=('勝ち','size'),
            勝率=('勝ち','mean'),
            連対率=('連対','mean'),
            t3着内率=('3着内','mean'),
        )
        .rename(columns={'t3着内率': '3着内率'})
        .reset_index()
        .sort_values('対象頭数', ascending=False)
    )
    category_tables[f'{label}_距離変更別'] = dist_table

summary_df = pd.DataFrame(summary_rows)
summary_df.to_csv(OUTPUT_DIR / 'summary.csv', index=False)
for name, table in category_tables.items():
    # name 例: "前走着順8着以下_人気別" -> data/前走着順8着以下/人気別.csv
    parts = name.rsplit('_', 1)
    def_dir = DATA_DIR / parts[0]
    def_dir.mkdir(parents=True, exist_ok=True)
    table.to_csv(def_dir / f'{parts[1]}.csv', index=False)
タイトルとURLをコピーしました