やりたいこと
あるグループごとの平均と標準偏差を求めたい。
df1.columns # ['クラス','番号','名前','性別','科目','点数']
↑こんなかんじのdfを使う。
行数は約5,000行。
ちなみにpythonのgroupbyは初めて使いました!
SQLはあるのだけども。
第1案
よくわからないまま調べて、動いたものを採用。
st_tm1 = time.time()
df_g = pd.DataFrame()
for group, df_group in df1.groupby(['クラス', '科目']).__iter__():
# 計算
lll = df_group['点数'].tolist()
df_group['Xbar'] =np.mean(lll)
df_group['s'] =statistics.stdev(lll)
# 結合
df_g = pd.concat([df_g, df_group], axis=0)
df1 = df_g.copy()
logger.info("Elapsed time: {:,.3f} sec".format(time.time() - st_tm1))
# 結果
# Elapsed time: 31.139 sec
おっそい!
第2案
for使うから遅くなるのだと思い、forを使わない方法を探す。
st_tm1 = time.time()
gcol = ['クラス', '科目']
# 計算
a=df1.groupby(gcol, as_index=False)
a1 = a.mean(numeric_only=True)[['クラス', '科目','点数']].rename(columns={'点数': 'Xbar'})
a2 = a.std(numeric_only=True)[['クラス', '科目','点数']].rename(columns={'点数': 's'})
# 結合
df1 = pd.merge(pd.merge(df1, a1,on = gcol), a2,on = gcol)
logger.info("Elapsed time: {:,.3f} sec".format(time.time() - st_tm1))
# 結果
# Elapsed time: 0.133 sec
それなり。
もうすこしスマートに書けないかと思い始める。
番外編
そういやlambdaってあったよね、を実践。
st_tm1 = time.time()
gcol = ['クラス', '科目']
# 計算
a1 = lambda df1, gcol,c_c = {'点数': 'Xbar'}: df1.groupby(gcol, as_index=False).mean(numeric_only=True)[gcol+list(c_c.keys())].rename(columns=c_c)
a2 = lambda df1, gcol,c_c = {'点数': 's'}: df1.groupby(gcol, as_index=False).std(numeric_only=True)[gcol+list(c_c.keys())].rename(columns=c_c)
# 結合
df1 = lambda df, a1: pd.merge(df1, a1,on = gcol)
df1 = lambda df, a2: pd.merge(df1, a2,on = gcol)
logger.info("Elapsed time: {:,.4f} sec".format(time.time() - st_tm1))
# 結果
# Elapsed time: 0.0077 sec
# ただし df1 の中身は下記のようになっている
# <function <lambda> at 0x0000020233501EE0>
変数に式が代入されてるのでそもそも計算していませんでしたオチ。
何やってんだろう……で終わる。
第3案
transformというものを知ったので使ってみる。
st_tm1 = time.time()
gcol = ['クラス', '科目','点数','Xbar','s']
# 計算と結合
df1[gcol[3]] = df1[gcol[:3]].groupby(gcol[:2]).transform("mean")[gcol[2]]
df1[gcol[4]] = df1[gcol[:3]].groupby(gcol[:2]).transform("std")[gcol[2]]
logger.info("Elapsed time: {:,.4f} sec".format(time.time() - st_tm1))
# 結果
# Elapsed time: 0.0523 sec
短くなったし分かりやすくなった!
ところで下記のように書くとFutureWarningが出ました。
df1[gcol[3]] = df1.groupby(gcol[:2]).transform("mean")[gcol[2]]
df1[gcol[4]] = df1.groupby(gcol[:2]).transform("std")[gcol[2]]
'''
FutureWarning: The default value of numeric_only in DataFrameGroupBy.mean is deprecated. In a future version, numeric_only will default to False. Either specify numeric_only or select only
columns which should be valid for the function.
'''
第2案の場合はmean(numeric_only=True)のように書いて対処できたのだけれど、transformの引数にnumeric_onlyはないようです。
https://github.com/pandas-dev/pandas/issues/50538
でもnumeric_onlyが云々ってことは文字列が計算時に含まれることが悪いんだろうと思って、使う列をグループに使う列と計算したい数値列だけにしたところ、警告が出なくなりました。
どっちかというとSerieasにしたから出なくなった気がします。
結論
一番早くなった第3案で行こうと思いました。
30秒も0.05秒になったし、とりあえずこれくらいがベストと思って終焉。
参考