【地方競馬データで検証】重馬場では本当に前残りが増えるのか?

導入

競馬では「重馬場は前残りになりやすい」と言われることがあります。馬場が悪くなると後方から差す馬が伸びにくく、前に行った馬が残りやすいという考え方です。

しかし、本当に重馬場では前残りが増えるのでしょうか。地方競馬の過去データを使い、馬場状態別に脚質成績を比較して検証します。

検証に使ったデータ

  • 使用期間: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着内率平均人気平均単勝オッズ単勝回収率
①逃げ549175071248.69%69.72%80.54%3.0211.412.74
②先行1516795068612.86%32.83%51.76%4.1821.970.96
③中団194862506842.09%6.16%13.51%6.1555.990.27
④後方109377395160.42%1.29%2.90%8.51114.310.09
稍重①逃げ234172148349.40%70.55%81.05%3.0311.332.76
稍重②先行643032147512.55%32.61%51.74%4.2122.310.97
稍重③中団82904214722.03%5.96%13.17%6.1956.790.26
稍重④後方49212173090.37%1.15%2.72%8.54115.460.09
①逃げ191941781351.80%72.92%83.10%2.9610.442.94
②先行532461780712.31%32.89%52.72%4.2122.050.96
③中団69105178091.72%5.35%12.18%6.2256.460.22
④後方41152144510.33%1.08%2.49%8.54111.360.06
不良①逃げ155271456655.26%76.30%85.82%2.9210.203.16
不良②先行436151456211.66%32.96%53.73%4.1820.980.94
不良③中団56372145611.48%4.67%11.14%6.1853.140.22
不良④後方31793116680.26%0.97%2.17%8.43103.620.06
重・不良①逃げ347213237953.35%74.43%84.32%2.9410.333.04
重・不良②先行968613236912.02%32.92%53.18%4.2021.570.95
重・不良③中団125477323701.62%5.04%11.71%6.2054.970.22
重・不良④後方72945261190.30%1.03%2.35%8.49107.990.06

単勝回収率は1レース100円換算の参考値です。

良馬場と重・不良馬場の比較

良馬場と重・不良馬場(合算)で、脚質別成績がどう変わるかをまとめました。

馬場脚質_絶対対象頭数勝率連対率3着内率平均単勝オッズ単勝回収率
①逃げ5491748.69%69.72%80.54%11.412.74
②先行15167912.86%32.83%51.76%21.970.96
③中団1948622.09%6.16%13.51%55.990.27
④後方1093770.42%1.29%2.90%114.310.09
重・不良①逃げ3472153.35%74.43%84.32%10.333.04
重・不良②先行9686112.02%32.92%53.18%21.570.95
重・不良③中団1254771.62%5.04%11.71%54.970.22
重・不良④後方729450.30%1.03%2.35%107.990.06

3着内構成比(1〜3着に入った馬の脚質内訳)

3着内に入った馬の脚質構成比を確認します。逃げ・先行の比率が良馬場より高ければ「前残り傾向」と言えます。

脚質_絶対3着内構成比(%)合計頭数馬場グループ
①逃げ29.1510835
②先行51.6510835
③中団17.3510835
④後方2.1510835
①逃げ30.1330004重・不良
②先行53.0330004重・不良
③中団15.1330004重・不良
④後方1.8330004重・不良

逃げ馬の3着内率:良 80.54% → 重・不良 84.32%

先行馬の3着内率:良 51.76% → 重・不良 53.18%

後方馬の3着内率:良 2.90% → 重・不良 2.35%

相対位置分類でも確認(補足)

頭数比による相対位置分類(前方25%以内・中団・後方)での集計も参考として示します。

馬場脚質_相対対象頭数勝率連対率3着内率
①前方11430732.92%56.75%70.69%
②中団1790826.40%17.46%32.64%
③後方2174460.78%2.46%5.97%
重・不良①前方7358734.56%59.71%74.04%
重・不良②中団1156095.38%15.74%31.16%
重・不良③後方1408080.54%1.90%4.76%

人気別に見た結果

前残り傾向が「人気馬が前に行っているだけ」ではないかを確認します。

人気グループ脚質_絶対馬場グループ対象頭数勝率連対率3着内率
①1番人気①逃げ1935172.13%87.90%93.54%
①1番人気②先行2281833.64%60.50%76.17%
①1番人気③中団739712.60%25.92%40.23%
①1番人気④後方11465.85%12.04%19.63%
②2〜3番人気①逃げ1836145.36%71.53%83.68%
②2〜3番人気②先行4861314.14%40.87%62.90%
②2〜3番人気③中団286524.70%13.48%26.75%
②2〜3番人気④後方57981.88%5.52%10.49%
③4〜6番人気①逃げ1168929.36%52.93%69.01%
③4〜6番人気②先行516117.41%23.89%44.18%
③4〜6番人気③中団694311.76%5.94%14.46%
③4〜6番人気④後方188640.72%2.41%5.54%
④7番人気以下①逃げ551618.53%35.48%48.88%
④7番人気以下②先行286373.95%13.24%27.06%
④7番人気以下③中団893820.63%2.35%6.32%
④7番人気以下④後方835690.17%0.60%1.54%
①1番人気①逃げ重・不良1281574.31%90.01%94.69%
①1番人気②先行重・不良1417930.54%58.21%75.12%
①1番人気③中団重・不良45869.55%21.11%34.10%
①1番人気④後方重・不良7994.13%9.64%14.27%
②2〜3番人気①逃げ重・不良1147449.59%75.40%86.62%
②2〜3番人気②先行重・不良3136313.43%41.01%63.72%
②2〜3番人気③中団重・不良180573.64%11.16%23.38%
②2〜3番人気④後方重・不良38641.32%4.40%8.44%
③4〜6番人気①逃げ重・不良717235.69%59.84%74.75%
③4〜6番人気②先行重・不良330237.08%24.27%46.03%
③4〜6番人気③中団重・不良441671.44%4.84%12.41%
③4〜6番人気④後方重・不良125260.51%2.00%4.44%
④7番人気以下①逃げ重・不良326022.98%41.87%56.50%
④7番人気以下②先行重・不良182964.15%15.09%30.99%
④7番人気以下③中団重・不良586670.51%2.05%5.85%
④7番人気以下④後方重・不良557560.13%0.46%1.29%

重・不良馬場での前残り傾向が、低人気馬でも見られるかを確認してください。

競馬場別に見た結果

競馬場によって、重・不良馬場での前残り傾向の強さに差があるかを確認します。

(対象頭数が合計30頭未満の組み合わせは除外しています)

場所脚質良_3着内率重不良_3着内率良_頭数重不良_頭数3着内率差(重不良-良)
佐賀①逃げ90.37%91.93%50893568+1.56pp
佐賀②先行54.70%56.19%1516010662+1.49pp
佐賀③中団11.04%9.58%1983113909-1.45pp
佐賀④後方0.98%0.76%100967002-0.22pp
名古屋①逃げ87.83%90.59%61053007+2.76pp
名古屋②先行52.78%53.56%172078428+0.78pp
名古屋③中団12.00%10.84%2270111089-1.16pp
名古屋④後方1.24%1.07%129656159-0.17pp
園田①逃げ81.13%86.44%83003289+5.31pp
園田②先行51.25%52.60%234019514+1.34pp
園田③中団14.22%12.24%3007812179-1.98pp
園田④後方2.24%1.74%164626510-0.51pp
大井①逃げ59.32%61.08%44983009+1.77pp
大井②先行43.06%45.70%125688451+2.64pp
大井③中団19.76%18.28%1654011083-1.48pp
大井④後方6.41%6.02%1964112850-0.39pp
姫路①逃げ78.86%83.82%1206241+4.96pp
姫路②先行50.90%53.90%3234629+3.00pp
姫路③中団14.65%11.58%4219812-3.07pp
姫路④後方2.09%0.86%2388466-1.24pp
川崎①逃げ72.76%79.48%33591715+6.72pp
川崎②先行48.43%50.65%91194535+2.22pp
川崎③中団15.84%12.91%116265896-2.93pp
川崎④後方2.61%2.13%92294403-0.48pp
水沢①逃げ85.08%85.82%24732328+0.75pp
水沢②先行54.74%54.58%66516085-0.17pp
水沢③中団10.85%10.26%84167932-0.59pp
水沢④後方1.28%1.07%32913564-0.21pp
浦和①逃げ89.82%92.54%24941260+2.72pp
浦和②先行55.65%56.14%73713705+0.49pp
浦和③中団10.31%9.41%96484888-0.90pp
浦和④後方0.85%0.51%72613712-0.34pp
盛岡①逃げ74.88%75.18%35671527+0.30pp
盛岡②先行51.67%51.78%96844077+0.11pp
盛岡③中団15.32%14.99%122875157-0.33pp
盛岡④後方3.44%3.46%50342139+0.02pp
笠松①逃げ86.59%87.17%58231302+0.59pp
笠松②先行56.06%57.22%156823532+1.16pp
笠松③中団10.55%9.69%199744501-0.87pp
笠松④後方0.88%0.29%44431050-0.59pp
船橋①逃げ61.80%65.81%26051404+4.01pp
船橋②先行48.63%50.09%77874212+1.46pp
船橋③中団20.06%17.91%98365411-2.15pp
船橋④後方6.22%5.05%69144179-1.17pp
金沢①逃げ91.26%92.90%37643266+1.64pp
金沢②先行54.48%54.74%102728973+0.26pp
金沢③中団9.03%8.15%1242111110-0.89pp
金沢④後方1.00%0.87%34953680-0.13pp
門別①逃げ73.41%73.63%42092666+0.22pp
門別②先行47.60%46.96%92835700-0.64pp
門別③中団15.37%15.20%117197321-0.17pp
門別④後方3.16%2.95%51533526-0.21pp
高知①逃げ89.68%92.16%14256139+2.48pp
高知②先行54.65%55.94%426018358+1.29pp
高知③中団10.98%9.60%556624189-1.38pp
高知④後方1.53%0.95%300513705-0.58pp

サンプル数が少ない競馬場・脚質の数値は変動が大きいため、参考程度にご覧ください。

距離帯別に見た結果

短距離・マイル前後・中距離で、重・不良馬場の前残り傾向に差があるかを見ます。

距離帯脚質_絶対馬場グループ対象頭数勝率連対率3着内率
マイル前後(1300〜1700m)①逃げ4184150.01%71.45%82.24%
マイル前後(1300〜1700m)②先行11665412.79%33.06%52.36%
マイル前後(1300〜1700m)③中団1505391.96%5.83%13.05%
マイル前後(1300〜1700m)④後方800040.31%1.00%2.39%
中距離(1800m〜)①逃げ236647.08%69.27%80.30%
中距離(1800m〜)②先行660913.16%32.80%51.51%
中距離(1800m〜)③中団83692.28%6.33%13.71%
中距離(1800m〜)④後方56530.50%1.20%2.72%
短距離(〜1200m)①逃げ1071043.90%63.05%73.91%
短距離(〜1200m)②先行2841613.08%31.89%49.35%
短距離(〜1200m)③中団359542.58%7.49%15.38%
短距離(〜1200m)④後方237200.76%2.31%4.66%
マイル前後(1300〜1700m)①逃げ重・不良2723755.50%77.05%86.82%
マイル前後(1300〜1700m)②先行重・不良7704911.86%33.25%54.00%
マイル前後(1300〜1700m)③中団重・不良1002651.40%4.51%11.02%
マイル前後(1300〜1700m)④後方重・不良553060.19%0.69%1.73%
中距離(1800m〜)①逃げ重・不良138754.22%75.20%83.20%
中距離(1800m〜)②先行重・不良382211.33%31.95%52.62%
中距離(1800m〜)③中団重・不良49321.82%5.41%12.15%
中距離(1800m〜)④後方重・不良34910.23%0.92%2.29%
短距離(〜1200m)①逃げ重・不良609743.53%62.54%73.38%
短距離(〜1200m)②先行重・不良1599012.95%31.57%49.32%
短距離(〜1200m)③中団重・不良202802.62%7.56%15.06%
短距離(〜1200m)④後方重・不良141480.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()
タイトルとURLをコピーしました