導入
競馬では「昇級初戦は厳しい」と言われることがあります。前走で好走してクラスが上がった馬は、相手が強くなるため苦戦しやすいという考え方です。
しかし、本当に昇級初戦は成績が下がるのでしょうか。地方競馬の過去データを使い、昇級初戦の馬と同級継続馬・降級馬を比較して検証します。
検証に使ったデータ
- 使用期間:2017/01/01〜2025/12/19
- 前走情報付き対象頭数:1,127,825
- 対象レース数:115,739
- 除外条件:取消・失格・除外・中止・単勝オッズ欠損・前走情報なし
- 使用カラム:レース名(クラス抽出)・着順・人気・単勝・距離・場所
クラス表記の内訳:A1:8399件 A2:16366件 A3:1045件 A4:1397件 B1:38004件 B2:55599件 B3:29265件 B4:10696件 C1:172654件 C2:261644件 C3:142247件 C4:23934件
クラスが抽出できないレース(重賞・特別戦・年齢限定戦等)は「判定不能」として別集計しています。
複勝払戻データはDBに含まれていないため、複勝回収率は算出できません。
昇級初戦の定義
地方競馬のクラス(A1〜A4, B1〜B4, C1〜C4)をレース名から正規表現で抽出し、以下の順に数値ランク化しました。
- C4(1) < C3(2) < C2(3) < C1(4) < B4(5) < B3(6) < B2(7) < B1(8) < A4(9) < A3(10) < A2(11) < A1(12)
- 今走ランク > 前走ランク → 昇級初戦
- 今走ランク = 前走ランク → 同級継続
- 今走ランク < 前走ランク → 降級初戦
- クラスが抽出できないレース → 判定不能(参考集計のみ)
全体結果
昇級初戦・同級継続・降級初戦の成績を比較しました。
| グループ | 対象頭数 | 対象レース数 | 勝率 | 連対率 | 3着内率 | 平均人気 | 平均単勝オッズ | 単勝回収率 |
|---|---|---|---|---|---|---|---|---|
| 昇級初戦 | 65173 | 27134 | 14.28% | 26.27% | 37.01% | 4.81 | 35.42 | 0.71 |
| 同級継続 | 555759 | 70809 | 8.53% | 17.74% | 27.31% | 6.07 | 56.47 | 0.70 |
| 降級初戦 | 54467 | 23646 | 10.10% | 19.65% | 28.95% | 5.82 | 51.69 | 0.72 |
| 判定不能 | 452426 | 64995 | 10.34% | 20.61% | 30.86% | 5.53 | 47.46 | 0.70 |
| 昇級初戦_前走1着 | 26004 | 17020 | 21.80% | 37.24% | 49.20% | 3.43 | 14.18 | 0.72 |
| 昇級初戦_前走2着以下 | 39169 | 18647 | 9.30% | 18.98% | 28.92% | 5.73 | 49.52 | 0.70 |
人気グループ別に見た結果(昇級初戦)
昇級初戦馬でも、今走の人気によって成績が大きく変わります。
| 人気グループ | 対象頭数 | 勝率 | 連対率 | 3着内率 |
|---|---|---|---|---|
| ①1番人気 | 11089 | 46.56% | 65.70% | 76.51% |
| ②2〜3番人気 | 16367 | 16.89% | 37.22% | 53.09% |
| ③4〜6番人気 | 18541 | 5.94% | 15.61% | 28.40% |
| ④7番人気以下 | 19176 | 1.47% | 4.42% | 8.77% |
全グループ × 人気グループ比較
昇級・同級・降級それぞれで、人気グループ別の成績がどう違うかを確認します。
| クラス変化 | 人気グループ | 対象頭数 | 勝率 | 連対率 | 3着内率 |
|---|---|---|---|---|---|
| 同級継続 | ①1番人気 | 44503 | 41.93% | 62.49% | 74.47% |
| 同級継続 | ②2〜3番人気 | 99993 | 15.81% | 35.64% | 52.40% |
| 同級継続 | ③4〜6番人気 | 162530 | 5.77% | 15.32% | 27.72% |
| 同級継続 | ④7番人気以下 | 248733 | 1.42% | 4.12% | 8.52% |
| 昇級初戦 | ①1番人気 | 11089 | 46.56% | 65.70% | 76.51% |
| 昇級初戦 | ②2〜3番人気 | 16367 | 16.89% | 37.22% | 53.09% |
| 昇級初戦 | ③4〜6番人気 | 18541 | 5.94% | 15.61% | 28.40% |
| 昇級初戦 | ④7番人気以下 | 19176 | 1.47% | 4.42% | 8.77% |
| 降級初戦 | ①1番人気 | 5495 | 44.40% | 64.71% | 75.32% |
| 降級初戦 | ②2〜3番人気 | 10625 | 16.66% | 35.40% | 51.35% |
| 降級初戦 | ③4〜6番人気 | 15545 | 6.14% | 15.61% | 27.01% |
| 降級初戦 | ④7番人気以下 | 22802 | 1.48% | 4.21% | 8.66% |
前走着順別に見た結果(昇級初戦)
前走でどの順位だった馬が昇級初戦で通用しているかを見ます。
| 前走着順 | 対象頭数 | 勝率 | 連対率 | 3着内率 |
|---|---|---|---|---|
| 1 | 26004 | 21.80% | 37.24% | 49.20% |
| 2 | 8717 | 15.88% | 30.54% | 43.50% |
| 3 | 5842 | 10.78% | 22.99% | 35.18% |
| 4 | 4663 | 9.09% | 19.19% | 30.13% |
| 5 | 4031 | 7.10% | 16.03% | 26.67% |
| 6 | 2939 | 6.81% | 13.51% | 22.56% |
| 7 | 2907 | 6.81% | 14.24% | 22.36% |
| 8 | 2661 | 5.75% | 11.76% | 18.60% |
| 9 | 2343 | 5.59% | 11.91% | 18.35% |
| 10 | 1916 | 5.48% | 10.07% | 15.61% |
| 11 | 1424 | 3.72% | 8.71% | 13.41% |
| 12 | 981 | 4.08% | 9.68% | 15.39% |
「1着差」カラムは各馬と1着馬のタイム差(秒)であり、勝ち馬は0になります。 前走での勝ち幅(2着との差)はDBから直接取得できないため、前走圧勝・辛勝の分類は今回省略しています。
場所別に見た結果(昇級初戦)
| 場所 | 対象頭数 | 勝率 | 連対率 | 3着内率 |
|---|---|---|---|---|
| 園田 | 11372 | 15.47% | 28.56% | 39.35% |
| 金沢 | 6654 | 16.82% | 29.77% | 41.49% |
| 大井 | 6364 | 12.43% | 23.21% | 32.89% |
| 門別 | 6216 | 14.17% | 26.06% | 36.95% |
| 水沢 | 4629 | 13.67% | 25.17% | 35.84% |
| 川崎 | 4421 | 11.51% | 21.24% | 30.90% |
| 盛岡 | 4412 | 13.83% | 26.45% | 37.49% |
| 船橋 | 3930 | 13.56% | 25.24% | 35.67% |
| 浦和 | 3701 | 11.46% | 22.40% | 32.56% |
| 高知 | 3519 | 19.69% | 33.59% | 44.64% |
| 名古屋 | 3489 | 10.00% | 19.55% | 29.61% |
| 佐賀 | 2763 | 20.05% | 35.25% | 46.94% |
| 笠松 | 1443 | 11.78% | 23.49% | 35.97% |
| 姫路 | 1389 | 14.47% | 27.07% | 39.88% |
| 帯広 | 871 | 9.53% | 17.22% | 26.98% |
対象数が少ない競馬場の数値は変動が大きいため、参考程度にご覧ください。
距離帯別に見た結果(昇級初戦)
| 距離帯 | 対象頭数 | 勝率 | 連対率 | 3着内率 |
|---|---|---|---|---|
| マイル前後(1300〜1700m) | 48135 | 14.88% | 27.29% | 38.19% |
| 中距離(1800m〜) | 3825 | 9.86% | 19.11% | 29.20% |
| 短距離(〜1200m) | 13213 | 13.39% | 24.64% | 34.96% |
データから分かったこと
- 昇級初戦の勝率は14.28%(65,173頭)、同級継続は8.53%(555,759頭)でした。昇級初戦馬の勝率は数値上高く見えますが、昇級馬は前走好走の実力馬が多く人気も低め(平均人気 4.81番人気 vs 同級継続 6.07番人気)なため、単純な比較には注意が必要です。
- 前走1着で昇級した馬の勝率は21.80%・3着内率は49.20%(26,004頭)と高い水準を維持していました。
- 前走2着以下で昇級した馬の勝率は9.30%・3着内率は28.92%(39,169頭)と大きく下がる傾向が確認できます。
- 単勝回収率(昇級初戦 0.71円 / 同級継続 0.70円)はいずれも100円以下で、昇級初戦だから回収率が特別高いわけではないことが分かります。
- 競馬場・距離帯によっても傾向が異なるため、一律に評価することは難しいです。
注意点・限界
- クラス表記が競馬場・時期によって異なる可能性があり、クラスを抽出できないレースは判定不能に分類されます。
- 年齢限定戦・重賞など、単純なクラス比較が難しいレースは除外しています。
- 昇級理由や出走編成のルールを完全には反映できていない可能性があります。
- 前走圧勝・辛勝の判定に必要な「前走2着馬との差」はDBから直接取得できないため、前走着順のみで代替しています。
- 単勝回収率はサンプル数や高配当の影響を受けます。
- 成績差には馬の能力・相手関係・馬場・展開なども影響します。
まとめ
地方競馬データ(2017/01/01〜2025/12/19)を使い、昇級初戦・同級継続・降級初戦の成績を比較しました。
- 昇級初戦の勝率(14.28%)は同級継続(8.53%)より高い数値ですが、昇級馬は人気馬が多く、単純な比較には注意が必要です。
- 最も差が大きかったのは前走着順で、前走1着→昇級は勝率21.80%・3着内率49.20%と好成績なのに対し、前走2着以下→昇級は勝率9.30%・3着内率28.92%と苦戦する傾向が見られます。
- 「昇級初戦はすべて消し」とは言えませんが、前走2着以下での昇級は今走人気に関わらず苦戦する傾向がデータから確認できます。
分析スクリプト
"""
【地方競馬データ検証】昇級初戦は本当に成績が落ちるのか?
"""
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着差', '距離',
'失', '取', '除', '中', '取消レース', '失格レース', '除外レース', '中止レース',
]
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)
mask_valid = (
df[special_flags].sum(axis=1) == 0
) & df['着順'].between(1, 99) & df['単勝'].notna() & (df['単勝'] > 0)
df_valid = df[mask_valid].copy()
# ================================================================
# 3. クラス抽出・ランク化
# ================================================================
# 実DBに A3, A4, B4 が存在するため完全順序で定義
CLASS_ORDER = {
'C4': 1, 'C3': 2, 'C2': 3, 'C1': 4,
'B4': 5, 'B3': 6, 'B2': 7, 'B1': 8,
'A4': 9, 'A3': 10, 'A2': 11, 'A1': 12,
}
def extract_class(race_name: str):
"""レース名から最初に出現する [ABC][1-4] パターンを返す。"""
if not isinstance(race_name, str):
return None
m = re.search(r'([ABC][1-4])', race_name)
return m.group(1) if m else None
df_valid['クラス'] = df_valid['レース名'].apply(extract_class)
# ================================================================
# 4. 前走情報を付与(馬名でグループ化してshift)
# ================================================================
df_valid = df_valid.sort_values(['馬名', '日付', 'レースID', 'レース番号'])
for col in ['クラス', '着順', '人気', '1着差', '距離', '場所']:
df_valid[f'prev_{col}'] = df_valid.groupby('馬名')[col].shift(1)
# 前走情報が揃っている行のみ対象
df_p = df_valid[df_valid['prev_日付' if 'prev_日付' in df_valid.columns else 'prev_着順'].notna()].copy() \
if 'prev_日付' not in df_valid.columns else df_valid[df_valid['prev_着順'].notna()].copy()
# prev_日付を個別に計算
df_valid['prev_日付'] = df_valid.groupby('馬名')['日付'].shift(1)
df_p = df_valid[df_valid['prev_日付'].notna()].copy()
# ================================================================
# 5. 昇降級判定
# ================================================================
def judge_class_change(curr_cls, prev_cls):
if (not isinstance(curr_cls, str) or not isinstance(prev_cls, str)
or curr_cls not in CLASS_ORDER or prev_cls not in CLASS_ORDER):
return '判定不能'
diff = CLASS_ORDER[curr_cls] - CLASS_ORDER[prev_cls]
if diff > 0: return '昇級初戦'
elif diff == 0: return '同級継続'
else: return '降級初戦'
df_p['クラス変化'] = df_p.apply(
lambda r: judge_class_change(r['クラス'], r['prev_クラス']), axis=1
)
# ================================================================
# 6. 成績フラグ・グループ変数
# ================================================================
df_p['勝ち'] = (df_p['着順'] == 1).astype(int)
df_p['連対'] = (df_p['着順'] <= 2).astype(int)
df_p['3着内'] = (df_p['着順'] <= 3).astype(int)
df_p['単勝回収'] = np.where(df_p['着順'] == 1, df_p['単勝'], 0.0)
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_p['人気グループ'] = df_p['人気'].apply(pop_group)
df_p['距離帯'] = df_p['距離'].apply(dist_group)
# ================================================================
# 7. 集計
# ================================================================
def agg_df(df_g):
n = len(df_g)
if n == 0:
return {'対象頭数': 0, '対象レース数': 0, '勝率': np.nan,
'連対率': np.nan, '3着内率': np.nan,
'平均人気': np.nan, '平均単勝オッズ': np.nan, '単勝回収率': np.nan}
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,
}
# ---- 全体サマリー ----
main_groups = ['昇級初戦', '同級継続', '降級初戦', '判定不能']
summary_rows = [{'グループ': g, **agg_df(df_p[df_p['クラス変化'] == g])} for g in main_groups]
# 昇級初戦の細分類
df_sho = df_p[df_p['クラス変化'] == '昇級初戦'].copy()
summary_rows += [
{'グループ': '昇級初戦_前走1着', **agg_df(df_sho[df_sho['prev_着順'] == 1])},
{'グループ': '昇級初戦_前走2着以下', **agg_df(df_sho[df_sho['prev_着順'] > 1])},
]
summary_df = pd.DataFrame(summary_rows)
summary_df.to_csv(OUTPUT_DIR / 'summary.csv', index=False)
# ---- カテゴリ別テーブル ----
def agg_by(df_g, by_col):
return (
df_g.groupby(by_col)
.agg(
対象頭数=('勝ち','size'),
勝率=('勝ち','mean'),
連対率=('連対','mean'),
t3着内率=('3着内','mean'),
)
.rename(columns={'t3着内率':'3着内率'})
.reset_index()
)
category_tables = {}
# 昇級初戦 × 人気グループ
pop_tbl = agg_by(df_sho, '人気グループ').sort_values('人気グループ')
category_tables['昇級初戦_人気グループ別'] = pop_tbl
# 昇級初戦 × 場所
loc_tbl = agg_by(df_sho, '場所').sort_values('対象頭数', ascending=False)
category_tables['昇級初戦_場所別'] = loc_tbl
# 昇級初戦 × 距離帯
dist_tbl = agg_by(df_sho, '距離帯').sort_values('距離帯')
category_tables['昇級初戦_距離帯別'] = dist_tbl
# 昇級初戦 × 前走着順(1〜12着)
prevrank_tbl = (
df_sho[df_sho['prev_着順'].between(1, 12)]
.assign(前走着順=lambda x: x['prev_着順'].astype(int))
.groupby('前走着順')
.agg(
対象頭数=('勝ち','size'),
勝率=('勝ち','mean'),
連対率=('連対','mean'),
t3着内率=('3着内','mean'),
)
.rename(columns={'t3着内率':'3着内率'})
.reset_index()
.sort_values('前走着順')
)
category_tables['昇級初戦_前走着順別'] = prevrank_tbl
# 全グループ × 人気グループ(比較用)
cross_tbl = (
df_p[df_p['クラス変化'].isin(['昇級初戦','同級継続','降級初戦'])]
.groupby(['クラス変化','人気グループ'])
.agg(
対象頭数=('勝ち','size'),
勝率=('勝ち','mean'),
連対率=('連対','mean'),
t3着内率=('3着内','mean'),
)
.rename(columns={'t3着内率':'3着内率'})
.reset_index()
.sort_values(['クラス変化','人気グループ'])
)
category_tables['全体_クラス変化x人気グループ'] = cross_tbl
# CSV 保存
for name, tbl in category_tables.items():
parts = name.split('_', 1)
def_dir = DATA_DIR / parts[0]
def_dir.mkdir(parents=True, exist_ok=True)
tbl.to_csv(def_dir / f'{parts[1]}.csv', index=False)
# ================================================================

