データの前処理の高速化を行いました。
いまのボトルネックは主に「groupby.apply(lambda … rolling …)」と「同じ列の何度もソート/astype/fillna」の往復です。副作用なく速くできる“差し替えポイント”を順番に挙げます。
1) groupby.apply(lambda s: s.shift(1).rolling(...))
をやめる
apply+lambda
は各グループで Python ループが回るので遅いです。
シフト → もう一度 groupby で束ねて rolling の二段構えにすると C 実装に近くなり速くなります。
def add_rolling_features_fast(df, key, col, windows=(3,5,7)):
# 事前にグループオブジェクトを用意
g = df.groupby(key, sort=False)[col]
prev = g.shift(1) # 1走前
# prev を再び key で束ねて rolling(lambda不要)
gg = prev.groupby(df[key])
for w in windows:
ma = gg.rolling(w, min_periods=1).mean().reset_index(level=0, drop=True)
sd = gg.rolling(w, min_periods=2).std().reset_index(level=0, drop=True)
df[f"{col}_ma{w}_馬"] = ma.astype("float32")
df[f"{col}_sd{w}_馬"] = sd.fillna(0).astype("float32")
diff = (df[col] - df[f"{col}_ma{w}_馬"]).astype("float32")
df[f"{col}_diff_vs_馬ma{w}"] = diff
df[f"{col}_z_vs_馬ma{w}"] = (diff / df[f"{col}_sd{w}_馬"].replace(0, np.nan)) \
.astype("float32").fillna(0.0)
return df
# 置き換え
data = data.sort_values(["馬ID", "日付"], kind="mergesort").reset_index(drop=True)
for col in ["距離", "斤量"]:
data = add_rolling_features_fast(data, "馬ID", col, windows=(3,5,7))
groupby(...).rolling(...)
に渡すシリーズは 一度 shift したもの と覚えると安定します。
2) クラス基準の rolling も同様に最適化
ここも apply+lambda
を排除できます。日付順だけ担保すれば 再ソート回数を減らしつつ rolling が可能です。
data["クラスキー"] = data["距離カテゴリ"].astype("category").astype(str) + "_" + data["芝ダ"].astype("category").astype(str)
# 日付だけで全体を安定ソート(mergesort)→ その順序を使ってグループ rolling
data = data.sort_values(["日付"], kind="mergesort").reset_index(drop=True)
gk = data.groupby("クラスキー", sort=False)
for col in ["距離", "斤量"]:
prev = gk[col].shift(1)
gg = prev.groupby(data["クラスキー"])
for w in (3,5,7):
ma = gg.rolling(w, min_periods=1).mean().reset_index(level=0, drop=True).astype("float32")
data[f"{col}_mean_クラス過去_ma{w}"] = ma
data[f"{col}_diff_vs_クラス_ma{w}"] = (data[col] - ma).astype("float32")
# もうクラスキーは不要
data.drop(columns=["クラスキー"], inplace=True)
- 既存コードのように クラスキー→日付 で再ソートし直す必要はありません。
日付
で一度だけ安定ソートすれば、同一クラス内の並びも日付昇順になり、groupby の順序が保たれます。
3) 体重系 rolling も同じ最適化
g = data.groupby("馬ID", sort=False)
bw = g["馬体重"]
prev_bw = bw.shift(1)
data["馬体重_diff_prev"] = (data["馬体重"] - prev_bw).astype("float32")
data["馬体重_pct_prev"] = (data["馬体重_diff_prev"] / prev_bw.replace(0, np.nan)).astype("float32")
prev = bw.shift(1)
gg = prev.groupby(data["馬ID"])
ma5 = gg.rolling(5, min_periods=1).mean().reset_index(level=0, drop=True)
sd5 = gg.rolling(5, min_periods=2).std().reset_index(level=0, drop=True)
data["馬体重_ma5_馬"] = ma5.astype("float32")
data["馬体重_sd5_馬"] = sd5.fillna(0).astype("float32")
data["馬体重_z_vs_馬ma5"] = (
(data["馬体重"] - data["馬体重_ma5_馬"]) / data["馬体重_sd5_馬"].replace(0, np.nan)
).astype("float32").fillna(0.0)
# diff の移動平均も lambda 排除
diff = data["馬体重_diff_prev"]
ggd = diff.groupby(data["馬ID"])
for k in (2,3,5):
data[f"馬体重_diff_prev_ma{k}"] = ggd.rolling(k, min_periods=1).mean() \
.reset_index(level=0, drop=True).astype("float32").fillna(0.0)
5) 文字列→カテゴリ化でメモリ & 速度改善
頻出の離散列は category
に。value_counts()
や結合、groupby が軽くなります。
ただし、一度 pd.Series
に変換してから astype("category")
してください。
もしくは最初から pd.cut
を使えば DataFrame の列として直接 category
型にできます。
年齢層修正
# 年齢層(np.select の場合)
data["年齢層"] = pd.Series(
np.select(
[data["年齢"] <= 3, data["年齢"] <= 5],
["若駒", "壮年"],
default="ベテラン"
),
index=data.index
).astype("category")
代替: pd.cut を使う場合(おすすめ)
data["年齢層"] = pd.cut(
data["年齢"],
bins=[-np.inf, 3, 5, np.inf],
labels=["若駒", "壮年", "ベテラン"]
).astype("category")
こちらの方がシンプルで pandas ネイティブなので速いです。
距離カテゴリ修正
np.select(...).astype("category")
を
👉 pd.Series(..., index=data.index).astype("category")
にすれば解決します。
修正版
# 距離カテゴリ
data["距離カテゴリ"] = pd.Series(
np.select(
[data["距離"] <= 1400, data["距離"] <= 1800, data["距離"] <= 2200],
["スプリント", "マイル", "中距離"],
default="長距離"
),
index=data.index
).astype("category")
さらにシンプルにしたいなら
pd.cut
を使うと、最初からカテゴリで出てくるので .astype("category")
も不要です:
data["距離カテゴリ"] = pd.cut(
data["距離"],
bins=[-np.inf, 1400, 1800, 2200, np.inf],
labels=["スプリント", "マイル", "中距離", "長距離"]
)
Categorical修正
fill_cols
の中に Categorical 型の列があり、そこへ未登録カテゴリ(”不明”)で fillna("不明")
したためです。Categorical は 事前にカテゴリへ追加しないと新しい値を入れられません。
安全な修正コード(列ごとに分岐)
from pandas.api.types import is_categorical_dtype
fill_cols = ['血統得意距離', '血統得意脚質', '血統得意馬場', '血統得意芝ダ']
for col in fill_cols:
if col not in data.columns:
continue
s = data[col]
if is_categorical_dtype(s):
# 既存がカテゴリなら "不明" をカテゴリに追加してから埋める
data[col] = s.cat.add_categories(["不明"]).fillna("不明")
# 余分なカテゴリを掃除したい場合は次を有効化
# data[col] = data[col].cat.remove_unused_categories()
else:
# 非カテゴリなら普通に埋めて、必要ならカテゴリ化
data[col] = s.fillna("不明").astype("category")
ポイント
- Categorical 列に新しい値を入れるときは
cat.add_categories([...])
が必須。 - すでにカテゴリ化している列は 順序(ordered) も維持されます(
add_categories
は順序を壊しません)。 - もし最初から「欠損は必ず”不明”にする」運用なら、先に fill → 後で一括で
astype("category")
にすると楽です。
バグ修正
「脚質」列に関して、カテゴリに「0」が含まれていないためエラーが発生しています。原因は 脚質
が Categorical 型のまま fillna("0")
していて、カテゴリに "0"
が未登録だからです。
→ 先に "0"
をカテゴリへ追加してから埋めればOK。ついでに将来の同種エラーも避ける安全&高速パターンで書きます。
import pandas as pd
# 直前1走の脚質
s = data.groupby("馬ID", sort=False)["脚質"].shift(1)
if isinstance(s.dtype, pd.CategoricalDtype):
# "0" が未登録なら追加してから埋める
if "0" not in s.cat.categories:
s = s.cat.add_categories(["0"])
data["脚質過去"] = s.fillna("0")
else:
# 非カテゴリなら普通に埋めてからカテゴリ化(必要なら)
data["脚質過去"] = s.fillna("0").astype("category")
コメント