Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

groupby rolling の遅さを解消する方法

解決したいこと

pyhon pandas で競馬予想のコードを作成しています。
データの前処理において、競走馬毎に 前 n走前の各種レースデータの平均値を取得したと思っております。
希望通りの処理は可能なのですが、とにかく遅です。
良い修正方法を教えて下さい。

データの形式

イメージとしては下表

ID列に、馬の識別コード
order列に時系列を表す数値
他は各種データ

実際は 100万データ✕30カラムくらいあります

スクリーンショット 2020-12-23 11.50.35.png

目的とする処理

下表に基づくと ID毎に、過去 n[3,5,9]個前(order順)の平均値を選択したカラムで取得し、
IDとorder のあるデータフレームにまとめて新規データフレームを作成したい

最初作成したコード

最初に作成したコード
これで 100万行データの 12カラムを [3,5,9]でローリングすると私のPCで 1時間超え
私のPCのスペックは
スクリーンショット 2020-12-23 12.37.30.png

df_r = df.sort_values('order', ascending = True).reset_index(drop = True)
df_ave = df_r[['ID','order']].copy()

columns = ['DATA1','DATA2','DATA3']

for column in columns:
    for i in [3, 5, 9]:
        df_ave[column + '_ave' + str(i)] = df_r.groupby('ID')[column].rolling(i).mean().reset_index(0, drop=True)0.6

修正したコード

あまりに遅いので、下記のコードに修正
かなり改善したが、まだ10分ほどの処理時間

df_r = df.sort_values('order', ascending = True).reset_index(drop = True)
df_ave = df_r[['ID','order']].copy()

columns = ['DATA1','DATA2','DATA3'] 

list_ave = []
list_ave.append(df_ave)
for i in [3,5,9]:
    df_mean = df_r.groupby('ID')[columns].rolling(i).mean().reset_index(0, drop=True).rename(columns=lambda s: s + '_ave' + str(i))
    list_ave.append(df_mean)
df_ave = pd.concat(list_ave, axis=1)

さいごに

どうも groupby について、理解が出来てないので力業のコードになっております。
もっと処理が早く方法をあれば、ご教授お願いします。

1

6Answer

np.lib.stride_tricks.as_strided()を用いることをベースに考えてみました。

まず「ID→order」の順に並べて、.as_strided().mean()で移動平均を一気に計算した後、各IDの頭の部分をNaNに置換し、最後に「order」のみの順に並び替えています。
また、データフレームの作成回数を減らすため、先にout配列を作成し、そこに移動窓の大きさごとに計算した値を順次格納しています。

import numpy as np
import pandas as pd


# 質問文中の「修正したコード」(比較用)
def myfunc1(df, columns, n_list):
    df_r = df.sort_values('order', ascending=True).reset_index(drop=True)

    list_ave = [df_r[['ID', 'order']]]
    for n in n_list:
        df_mean = df_r.groupby('ID')[columns].rolling(n).mean().reset_index(
            0, drop=True).rename(columns=lambda c: f'{c}_ave{n}')
        list_ave.append(df_mean)
    return pd.concat(list_ave, axis=1)


# 作ったコード
def myfunc2(df, columns, n_list):
    df_s = df.sort_values(['ID', 'order'], ascending=True)
    arr = df_s[columns].to_numpy()
    sidx = df_s['order'].to_numpy().argsort()

    id_array = df_s['ID'].to_numpy()
    idx = np.concatenate(([True], id_array[1:] != id_array[:-1])).nonzero()[0]
    m = len(columns)

    out = np.empty((arr.shape[0], m * len(n_list)), dtype=arr.dtype)
    for i, n in enumerate(n_list):
        sub_out = out[:, m*i:m*i+m]
        sub_out[n-1:] = np.lib.stride_tricks.as_strided(
            arr, writeable=False,
            strides=(arr.strides[0], arr.strides[0], arr.strides[1]),
            shape=(arr.shape[0]-(n-1), n, m)).mean(1)
        sub_out[(idx[:, None] + np.arange(n-1)).ravel()] = np.nan

    new_col = [f'{c}_ave{n}' for n in n_list for c in columns]
    return pd.concat([df_s[['ID', 'order']].iloc[sidx].reset_index(drop=True),
                      pd.DataFrame(out[sidx], columns=new_col)],
                     axis=1)

対象が5列100万行のとき、4.6秒から1.9秒になりました。

np.random.seed(0)
df = pd.DataFrame(
    {'ID': np.array(list('ABC'))[np.random.randint(0, 3, 1000000)],
     'order': np.random.permutation(1000000),
     'DATA1': np.random.rand(1000000),
     'DATA2': np.random.rand(1000000),
     'DATA3': np.random.rand(1000000),
     'DATA4': np.random.rand(1000000),
     'DATA5': np.random.rand(1000000)}
)
df.head(10)

columns = ['DATA1', 'DATA2', 'DATA3', 'DATA4', 'DATA5']
n_list = [3, 5, 9]

%timeit myfunc1(df, columns=columns, n_list=n_list)
# 4.59 s ± 88.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit myfunc2(df, columns=columns, n_list=n_list)
# 1.9 s ± 18.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

df1 = myfunc1(df, columns=columns, n_list=n_list)
df2 = myfunc1(df, columns=columns, n_list=n_list)
np.allclose(df1.iloc[:, 2:].to_numpy(),
            df2.iloc[:, 2:].to_numpy(), equal_nan=True)
# True
3Like

Comments

  1. @H58

    Questioner

    @nkay さん
    回答ありがとうございます。
    .as_strided() 使ったことなかったです。

    後ほど、試してみて報告させていただきます。
    ありがとうございます。

私の環境(i5-5200U 2.20Ghz 8G)で改善されたコード部分を実行してみましたが、30数秒で完了しています。バージョンはpandas 1.1.5, numpy 1.19.3です。pandasは最新ですか?

3.78 s ± 63.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1Like

Comments

  1. @H58

    Questioner

    回答ありがとうございます。
    バージョンはpandas 1.1.5, numpy 1.19.2です

    データは100万行程度のデータフレームでの数値でしょうか?

@H58 さん

追記
とりあえず、コピーして試したのですが下記のエラーがでます。
index の範囲外を切り抜いている??

pandas ばかりで処理していて numpy の知識が乏しくてすみません。

出現回数がnより小さいIDがあるのかもしれません。
(そういうわけではない場合はデータフレームの情報を書いてください)

1Like

@nkay さん

追記
とりあえず、コピーして試したのですが下記のエラーがでます。
index の範囲外を切り抜いている??

pandas ばかりで処理していて numpy の知識が乏しくてすみません。

IndexError Traceback (most recent call last)
<ipython-input-52-debabea78325> in <module>
27 strides=(arr.strides[0], arr.strides[0], arr.strides[1]),
28 shape=(arr.shape[0]-(n-1), n, m)).mean(1)
---> 29 sub_out[(idx[:, None] + np.arange(n-1)).ravel()] = np.nan
30
31 new_col = [f'{c}_ave{n}' for n in [3, 5, 9] for c in columns]

IndexError: index 1024923 is out of bounds for axis 0 with size 1024923
0Like

@nkay さん

出現回数がnより小さいIDがあるのかもしれません。

その通りです。
今、 np.lib.stride_tricks.as_strided について調べてますが、未だ理解できてません💦 難しい;
対応策があればお願いします

0Like

@nkay さん
下記のように修正したら、エラー解消しました

ありがとうございました

10倍以上速くなりました
pandas 卒業できるようにがんばります

#修正
sub_out[(idx[:, None] + np.arange(n-1)).ravel()] = np.nan

#修正後
idx_none = idx[:, None] + np.arange(n-1)
sub_out[np.unique(idx_none[idx_none < arr.shape[0]]).ravel()] = np.nan

#元々のコード
Wall time: 6min 34s

#修正後
Wall time: 3.8 s
0Like

Your answer might help someone💌