0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kaggle】March Machine Learning Mania 2025の解法を読み解く【その1】

Last updated at Posted at 2025-04-18

はじめに

KaggleのMarch Machine Learning Mania 2025解法を読み解きたいと思います。

補足: March madnessとは?

データ読込

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn
import warnings

# warnings
warnings.filterwarnings("ignore")

# pandas
pd.set_option("display.max_column", 999)

データを読み込みます。

  • RegularSeasonDetailedResults.csv: レギュラーシーズンのスタッツ
  • MNCAATourneyDetailedResults.csv: トーナメントのスタッツ
  • MNCAATourney.csv: シード情報
data_dir = "../input/march-machine-learning-mania-2025
M_regular_results = pd.read_csv(f"{data_dir}/MRegularSeasonDetailedResults.csv")
M_tourney_results = pd.read_csv(f"{data_dir}/MNCAATourneyDetailedResults.csv")
M_seeds = pd.read_csv(f"{data_dir}/MNCAATourneySeeds.csv")

W_regular_results = pd.read_csv(f"{data_dir}/WRegularSeasonDetailedResults.csv")
W_tourney_results = pd.read_csv(f"{data_dir}/WNCAATourneyDetailedResults.csv")
W_seeds = pd.read_csv(f"{data_dir}/WNCAATourneySeeds.csv")

男子データと女子データを縦に結合します。

regular_results = pd.concat([M_regular_results, W_regular_results])
tourney_results = pd.concat([M_tourney_results, W_tourney_results])
seeds = pd.concat([M_seeds, W_seeds])

2003年以降のデータを抽出します。

season = 2003  # もし、別の年数で抽出したければ、変更して change if you want different cutoff year for your models
regular_results = regular_results.loc[regular_results["Season"] >= season]
tourney_results = tourney_results.loc[tourney_results["Season"] >= season]
seeds = seeds.loc[seeds["Season"] >= season]

各データフレームのシェイプは以下のようになっているはずです。

print(regular_results.shape)
print(tourney_results.shape)
print(seeds.shape)
出力
(200590, 34)
(2276, 34)
(2896, 3)

前処理

ここで行う処理の概要は以下の通りです。

  1. 必要な列を抽出する
  2. 延長戦を行った試合に対して、各スタッツを調整する。延長戦(NumOT)が2だったら、試合時間は50分になるので、スコア等のスタッツを40分に換算する。具体的には、0.8倍(40/50)する。
  3. 勝利チームと敗北チームを入れ替えて(swap)、縦に結合する
  4. 点差列を追加する(勝利スコア - 敗北スコア)
  5. 勝利列を追加する
  6. 男女列を追加する

個人的に、上記の中で3番の意味が飲み込みづらかったのですが、後でgroupbyを使って集計する際に、この処理が必要になります。例えば、A, B, Cの3チームがレギュラーシーズンで総当たり戦を行い、以下のような結果になった場合を考えます。

Season T1_TeamID T1_Score T2_TeamID T2_Score
2024 A 100 B 90
2024 B 90 C 80
2024 A 80 C 70

groupbyを使って各チームの得点と失点を集計する場合、上記のままではうまく計算できません。そこで、3番の処理の出番です。勝利チームと敗北チームを入れ替えたデータを作成し、縦に結合します。まずは、入れ替えです。

Season T2_TeamID T2_Score T1_TeamID T1_Score
2024 A 100 B 90
2024 B 90 C 80
2024 A 80 C 70

二つのデータを縦結合すると、以下のようなデータになります。

Season T1_TeamID T1_Score T2_TeamID T2_Score
2024 A 100 B 90
2024 B 90 C 80
2024 A 80 C 70
2024 B 90 A 100
2024 C 80 B 90
2024 C 70 A 80

こうなるとgroupbyを使って、各チームの平均得失点を計算できます。

df.groupby(['Season', 'T1_TeamID'])['T1_Score', 'T2_Score'].ag([np.mean]).reset_index()
Season T1_TeamID T1_Score_mean T2_Score_mean
2024 A 90.0 80.0
2024 B 90.0 90.0
2024 C 75.0 85.0

前置きが長くなりました。実装します。

def prepare_data(df):
    # 1.必要な列を抽出
    df = df[["Season", "DayNum", "LTeamID", "LScore", "WTeamID", "WScore", "NumOT",
            "LFGM", "LFGA", "LFGM3", "LFGA3", "LFTM", "LFTA", "LOR", "LDR", "LAst", "LTO", "LStl", "LBlk", "LPF",
            "WFGM", "WFGA", "WFGM3", "WFGA3", "WFTM", "WFTA", "WOR", "WDR", "WAst", "WTO", "WStl", "WBlk", "WPF"]]
    

    #  延長戦を行った試合に対して、各スタッツを調整する。延長1回なら試合時間が45分になるので、40分に換算する。
    adjot = (40 + 5 * df["NumOT"]) / 40 # NumOTが1のとき、40+5=45分
    adjcols = ["LScore", "WScore", 
               "LFGM", "LFGA", "LFGM3", "LFGA3", "LFTM", "LFTA", "LOR", "LDR", "LAst", "LTO", "LStl", "LBlk", "LPF",
               "WFGM", "WFGA", "WFGM3", "WFGA3", "WFTM", "WFTA", "WOR", "WDR", "WAst", "WTO", "WStl", "WBlk", "WPF"]
    for col in adjcols:
        df[col] = df[col] / adjot # OTの回数に応じてスタッツを調整する。OTが1回なら、試合時間が40分→45分になるので、各スタッツを45/40で割る。各スタッツを40分(=通常の試合時間)あたりのスタッツに換算するため

    # 3.列名を入れ替えて、縦結合する
    dfswap = df.copy()
    df.columns = [x.replace("W", "T1_").replace("L", "T2_") for x in list(df.columns)] # 列名を変更W→T1_、L→T2
    dfswap.columns = [x.replace("L", "T1_").replace("W", "T2_") for x in list(dfswap.columns)] # swap版は、L→T1_、W→T2に変更
    output = pd.concat([df, dfswap]).reset_index(drop=True) # 結合

    # 4~6. 列追加
    output["PointDiff"] = output["T1_Score"] - output["T2_Score"] # 得点差列を追加
    output["win"] = (output["PointDiff"] > 0) * 1 # 勝敗を1/0で表す
    # output["T1_TeamID"] = output["T1_TeamID"].astype(str) # チームIDを文字列に変換
    # output["T2_TeamID"] = output["T2_TeamID"].astype(str) # チームIDを文字列に変換
    output["men_women"] = (output["T1_TeamID"].apply(lambda t: str(t).startswith("1"))) * 1  # 0: women, 1: men
    return output

regular_data = prepare_data(regular_results)
tourney_data = prepare_data(tourney_results)

特徴量エンジニアリング(難易度:EASY)

シード情報を付与します。シードとは、NCAAトーナメントに出場したチームへ割り振られる番号です。レギュラーシーズンの結果がよかったチームほど、小さい番号が割り振られます。つまり、シード番号が小さいチームは強いはずなので、勝敗予測に対して有効な特徴量になってくれそうです。

Season Seed TeamID
1154 2003 W01 1328
1155 2003 W02 1448
1156 2003 W03 1393

シードデータは、W16aのように3桁もくしは4桁の文字列で表されています。一文字目は地域を表し、W, X, Y, Z の4種類です。2~3文字目がシード番号です。4文字目はファースト4(下位8チームのプレーオフ)につけられる記号で、aかbが付きます。要するに2、3文字目が大事です。

seeds["seed"] = seeds["Seed"].apply(lambda x: int(x[1:3])) # 2~3文字目を抽出
Season Seed TeamID Seed
1154 2003 W01 1328 1
1155 2003 W02 1448 2
1156 2003 W03 1393 3

列名を変更します。

seeds_T1 = seeds[["Season", "TeamID", "seed"]].copy()
seeds_T2 = seeds[["Season", "TeamID", "seed"]].copy()
seeds_T1.columns = ["Season", "T1_TeamID", "T1_seed"]
seeds_T2.columns = ["Season", "T2_TeamID", "T2_seed"]

トーナメントデータにマージし、シード番号の差列を追加します。

tourney_data = tourney_data[["Season", "T1_TeamID", "T2_TeamID", "PointDiff", "win", "men_women"]]
tourney_data = pd.merge(tourney_data, seeds_T1, on=["Season", "T1_TeamID"], how="left")
tourney_data = pd.merge(tourney_data, seeds_T2, on=["Season", "T2_TeamID"], how="left")
tourney_data["Seed_diff"] = tourney_data["T2_seed"] - tourney_data["T1_seed"] # シード差を追加
Season T1_TeamID T2_TeamID PointDiff win men_women T1_seed T2_seed Seed_diff
0 2003 1421 1411 7.111 1 1 16 16 0
1 2003 1112 1436 29.111 1 1 1 16 15
1 2003 1113 1272 13.000 1 1 10 7 -3
: : : : : : : : : :
4551 2024 3234 3376 -12.000 0 0 1 1 0

※得点差が少数になっているのは、延長戦を考慮した調整(スコアを40分換算している)が入っているから。

補足: トーナメントについて

  • 68チームが選出される。
  • 下位8チームが試合を行い、勝利チーム(4チーム)が本大会出場できる。ファースト4というらしい。
  • 64チームがノックアウト形式でトーナメントを戦う。
  • なので試合数は、4+32+16+8+4+2+1=67になる。
  • 1回戦はシード番号が小さいチームと、大きいチームがあたる。
  • 地域は4つある。W, X, Y, Z。
  • 末尾にa, bがついているのは、ファースト4。

シードと得点差を確認

シードと得点差の関係を可視化します。まずは、pivot_tableを使って、男子と女子に分けて、シードごとの得点差の平均と標準偏差を計算します。

tmpmean = tourney_data.pivot_table(columns="men_women", index="T1_seed", values="PointDiff", aggfunc="mean").ffill()
tmpstd = tourney_data.pivot_table(columns="men_women", index="T1_seed", values="PointDiff", aggfunc="std").ffill()

男子について、グラフにします。

plt.plot(tmpmean.index, tmpmean[0], 'b-')
plt.fill_between(tmpmean.index, tmpmean[0] - tmpstd[0], tmpmean[0] + tmpstd[0], color="b", alpha=0.1)
plt.xlabel("T1_seed")
plt.ylabel("PointDiff_MEAN")
plt.title('Men')
plt.show()

men_T1_seed_PointDiff.png

続いて女子。

plt.plot(tmpmean.index, tmpmean[1], 'r-')
plt.fill_between(tmpmean.index, tmpmean[1] - tmpstd[1], tmpmean[1] + tmpstd[1], color="r", alpha=0.1)
plt.xlabel("T1_seed")
plt.ylabel("PointDiff_MEAN")
plt.title('Women')
plt.show()

women_T1seed_PointDiff.png

グラフはいずれも右肩下がりです。つまり、T1_Seedが大きいほど、得点差は小さくなる(負は敗北)と言えそうです。男女で比べると、男子の方がグラフの傾きは急なので、その傾向が強く見られそうです。女子の方が番狂わせは起きやすそうです。

シードの差と得点の差の関係を確認

今度はシードの差と、得点の差を可視化します(さっきは、横軸はT1のシードの値でした)。

tmpmean = tourney_data.pivot_table(columns="men_women", index="Seed_diff", values="PointDiff", aggfunc="mean").ffill()
tmpstd = tourney_data.pivot_table(columns="men_women", index="Seed_diff", values="PointDiff", aggfunc="std").ffill()

plotのコードは同じです。

plt.plot(tmpmean.index, tmpmean[0], 'b--')
plt.fill_between(tmpmean.index, tmpmean[0] - tmpstd[0], tmpmean[0] + tmpstd[0], color="b", alpha=0.1)
plt.xlabel("Seed_diff")
plt.ylabel("PointDiff_MEAN")
plt.title('Men')
plt.show()
plt.plot(tmpmean.index, tmpmean[1], 'r--')
plt.fill_between(tmpmean.index, tmpmean[1] - tmpstd[1], tmpmean[1] + tmpstd[1], color="r", alpha=0.1)
plt.xlabel("Seed_diff")
plt.ylabel("PointDiff_MEAN")
plt.title('Women')
plt.show()

men_Seeddiff_Pointdiff.png
women_seeddiff_pointdiff.png

長くなってきたので、一旦、ここで終わり。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?