1
1

【python】groupbyが遅くってェ…2

Last updated at Posted at 2023-11-07

やりたいこと

前回:https://qiita.com/wagahaiCAT/items/353de5aed90ff562cb14

あるグループの特定範囲の平均と標準偏差を求めて、同グループにまとめて代入したい。
条件Xの計算結果をグループ全体に入れるというか、最初だけ計算したいときのパターンというか。

df1.columns  # ['クラス','番号','名前','性別','科目','点数','Xbar','s']

↑こんなかんじのdfを使う。
行数は約5,000行。

第1案

for使ってる版。

import pandas as pd
from decimal import Decimal, getcontext
def return_cll(df,c_side,r_num=2):
    
    avg = df.mean(numeric_only=True).round(r_num)
    sgm = df.std(numeric_only=True).round(r_num)
    three = Decimal(sgm)*3

    if    c_side=='u':    c_std = (Decimal(avg)+three)
    elif  c_side=='l':    c_std = (Decimal(avg)-three)
    return f.format(c_std)

st_tm1 = time.time()
df_g = pd.DataFrame()
for group, df_group in df1.groupby(['学年','クラス','科目']).__iter__():
    
    # 番号10番台だけを計算
    df_first = df_group[df_group['番号']<=10]
    
    if len(df_first)==0:
        df_group['Xbar-CL'] = float('nan')
        df_group['Xbar-UCL'] = float('nan')
        df_group['Xbar-LCL'] = float('nan')
        df_group['s-CL'] = float('nan')
        df_group['s-UCL'] = float('nan')
        df_group['s-LCL'] = float('nan')
    else:   
        # 計算
        lll = df_first['点数'].tolist()
        df_group['Xbar-CL'] = np.mean(lll) 
        df_group['Xbar-UCL'] = cll.return_cll(df_first["点数"],'u',2)
        df_group['Xbar-LCL'] = cll.return_cll(df_first["点数"],'l',2)
        lll = df_first['s'].tolist()
        df_group['s-CL'] = np.mean(lll) 
        df_group['s-UCL'] = cll.return_cll(df_first["s"],'u',2)
        df_group['s-LCL'] = cll.return_cll(df_first["s"],'l',2)
    
    # 結合
    df_g = pd.concat([df_g, df_group], axis=0)

logger.info("Elapsed time: {:,.4f} sec".format(time.time() - st_tm1)) 
# 結果
# Elapsed time: 28.0595 sec

おそいですね。

第2案

transformとlambdaを使うのはどうだろうという挑戦。

st_tm1 = time.time()

df_first = df1[df1['番号']<=10]
gcol_1 = ['学年','クラス', '科目','点数']
gcol_2 = ['学年','クラス', '科目','s']
gcol = ['学年','クラス', '科目','Xbar-UCL','Xbar-LCL','s-UCL','s-LCL']

# Xbar UCL/LCL
df_first[gcol[3]] = df_first[gcol_1].groupby(gcol_1[:3]).transform(lambda x: x.mean()+(x.std()*3))[gcol_1[3]]
df_first[gcol[4]] = df_first[gcol_1].groupby(gcol_1[:3]).transform(lambda x: x.mean()-(x.std()*3))[gcol_1[3]]
# s UCL/LCL
df_first[gcol[5]] = df_first[gcol_2].groupby(gcol_2[:3]).transform(lambda x: x.mean()+(x.std()*3))[gcol_2[3]]
df_first[gcol[6]] = df_first[gcol_2].groupby(gcol_2[:3]).transform(lambda x: x.mean()-(x.std()*3))[gcol_2[3]]
 
df_f = df_first[~df_first.duplicated(subset=gcol[:3])][gcol] 
df1 = pd.merge(df1, df_f,on = gcol[:3])

logger.info("Elapsed time: {:,.4f} sec".format(time.time() - st_tm1)) 

# 結果
# Elapsed time: 13.7416 sec

そこまで速くならないなあと悩む。

第3案

apply版。

st_tm1 = time.time()

df_first = df1[df1['番号']<=10]
gcol0 = ['学年','クラス', '科目','点数','s']
gcol = ['学年','クラス', '科目','Xbar-UCL','Xbar-LCL','s-UCL','s-LCL']

a= df_first[gcol0].groupby(gcol0[:3]).apply(lambda x: x.mean(numeric_only=True)+(x.std(numeric_only=True)*3)).rename(columns={gcol0[3]:gcol[3],gcol0[4]:gcol[5]}) 
b= df_first[gcol0].groupby(gcol0[:3]).apply(lambda x: x.mean(numeric_only=True)-(x.std(numeric_only=True)*3)).rename(columns={gcol0[3]:gcol[4],gcol0[4]:gcol[6]})
            
df1 = pd.merge(pd.merge(df1, a,on = gcol[:3]), b,on = gcol[:3])
logger.info("Elapsed time: {:,.4f} sec".format(time.time() - st_tm1)) 

# 結果
# Elapsed time: 6.4771 sec

applyにしても一行3秒かかるのは変わってない模様。

第4案

pandarallel(並列実行)を使う版。

# pip install pandarallel

from pandarallel import pandarallel
pandarallel.initialize()

st_tm1 = time.time()

df_first = df1[df1['番号']<=10]
gcol0 = ['学年','クラス', '科目','点数','s']
gcol = ['学年','クラス', '科目','Xbar-UCL','Xbar-LCL','s-UCL','s-LCL']

a= df_first[gcol0].groupby(gcol0[:3]).parallel_apply(lambda x: x.mean(numeric_only=True)+(x.std(numeric_only=True)*3)).rename(columns={gcol0[3]:gcol[3],gcol0[4]:gcol[5]})
b= df_first[gcol0].groupby(gcol0[:3]).apply(lambda x: x.mean(numeric_only=True)-(x.std(numeric_only=True)*3)).rename(columns={gcol0[3]:gcol[4],gcol0[4]:gcol[6]})

df1 = pd.merge(pd.merge(df1, a,on = gcol[:3]), b,on = gcol[:3])
logger.info("Elapsed time: {:,.4f} sec".format(time.time() - st_tm1)) 

# 結果
#  Elapsed time: 10.7422 sec

行数が多くないと逆に遅くなるとのことなので、おそらくそれかなと。

第5案

swifter版。

# pip install -U swifter[groupby]
import swifter
st_tm1 = time.time()

df_first = df1[df1['番号']<=10]
gcol0 = ['学年','クラス', '科目','点数','s']
gcol = ['学年','クラス', '科目','Xbar-UCL','Xbar-LCL','s-UCL','s-LCL']

a= df_first[gcol0].swifter.groupby(gcol0[:3]).apply(lambda x: x.mean(numeric_only=True)+(x.std(numeric_only=True)*3)).rename(columns={gcol0[3]:gcol[3],gcol0[4]:gcol[5]})
b= df_first[gcol0].swifter.groupby(gcol0[:3]).apply(lambda x: x.mean(numeric_only=True)-(x.std(numeric_only=True)*3)).rename(columns={gcol0[3]:gcol[4],gcol0[4]:gcol[6]})

df1 = pd.merge(pd.merge(df1, a,on = gcol[:3]), b,on = gcol[:3])
logger.info("Elapsed time: {:,.4f} sec".format(time.time() - st_tm1)) 

# 結果
# Elapsed time: 13.9716 sec

一番速くなるかなと思ったのですがそんなことはなかった!

結論

第3案がベターザンなので、とりあえず第3案を採用しようと思いました。
3秒の壁を越えたい……。

1
1
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
1
1