python

Python pandasで性能を出すためにやったこと

pandas

最近、pandasを使って性能をあげる必要があり、色々わかったことを備忘ついでに記載していきます。

データ量が増えてくると下手な書き方をすると処理時間にうーんと差がでます。

差が出てくると時間はかかるし、お金もかかるのでいいことありません。

こいつちょっとわかってるな思われるようなpandas職人になれるよう書いていきます。

「前処理大全データ分析のためのSQL/R/Python実践テクニック』を読む

この本を読めば大抵のことは書いてあります。

特に初心者が犯しやすいミスやアンチパターンと比較して、おすすめの実装方法が書かれているのでなぜこの実装方法が良いのかが理論的にわかるようになっています。

この本の通り書けば、「Python、pandasわかってるね。」と思われると思います。

初心者から中級者へレベルをあげるために1冊辞書的に持っておくべき1冊です。

pandasで読み込むデータのスリム化

結局、時間がかかる処理ってデータの読み込みです。

pandasの場合、下記のようにファイルを読み込みます。

import pandas as pd
pd.read_csv(ファイルパス)

read_csvの処理でメモリと時間をかなり使います。

なので、必要ないデータは最初から削っておく必要があります。

  • 必要のない列は削除
  • 途中で連結するものは事前に連結しておく

pandasの処理順の変更

処理の時間がかかるのは、大量のデータにアクセスするためです。

アクセスするデータが減れば、その分処理が早くなります。

なので、処理順を変えてやることで不要なデータへのアクセスを減らします。

例えば、削除などのデータ量を削る処理を先に実施するなどです。

pandasのapplyは使わない

これは、色んな場所で言われてますが、applyを使うとパフォーマンスがかなり悪くなります。

単純な文字列結合をapplyを使った場合と使わなかった場合で、10万件のデータで検証した結果、

13倍の性能差がでました。

データが増えていくとさらに顕著になって700万件を超えてくると50倍差でした。

applyを利用した場合(遅)

# 計測開始
start = time.time()

df['con_char'] = df[['dt', 'user']].apply(lambda x: '{}_{}'.format(x[0], x), axis=1)

# 計測終了
elapsed_time = time.time() - start
print('elapsed_time: {}{}'.format(elapsed_time, '[sec]'))
elapsed_time: 326.3245301246643[sec]

applyを使わず連結した場合(速)

# 計測開始
start = time.time()

df['con_char'] = df['dt'].astype('str') + '_|_' +df['user'].astype('str')

# 計測終了
elapsed_time = time.time() - start
print('elapsed_time: {}{}'.format(elapsed_time, '[sec]'))
elapsed_time: 25.225428104400635[sec]

pandasのinplaceを使わない

pandasで列名や行名を変更するときにrenameメソッドが用意されています。

renameメソッドには、inplaceという引数が用意されており、元の変数に対して処理を行ってくれます。

同じく10万件で試した時に1秒の差が出ました。

inplaceを使わず再代入(速)

# 計測開始
start = time.time()
df_new = df.rename(columns={'con_char': 'rename'})

# 計測終了
elapsed_time = time.time() - start
print('elapsed_time: {}{}'.format(elapsed_time, '[sec]'))
elapsed_time: 1.450697422027588[sec]

inplaceを使った場合(遅)

# 計測開始
start = time.time()

df.rename(columns={'con_char': 'rename'}, inplace=True)

# 計測終了
elapsed_time = time.time() - start
print('elapsed_time: {}{}'.format(elapsed_time, '[sec]'))
elapsed_time: 2.413647174835205[sec]

他にもdropやqueryメソッドでも試してみましたが、inplaceを使わない場合の方が速かったです。

 

pandasのgroupbyにアクセスする回数を減らす

pandasは、データフレームにアクセスする処理が基本的に重くなります。

つまり、アクセスする回数を減らせば必然的に性能はあがります。

ループのたびに毎回groupbyする(遅)

for i in 1000:
 df['name'] = df.groupby('key')['name'].apply(
     lambda x: x.shift(periods=i)
 )

ループの前で変数に格納し、groupbyする回数を1回にする(速)

df.groupby('key')['name']
for i in 1000:
 df['name'] = df_cpi.apply(lambda x: x.shift(periods=i))

今回の場合、処理が重くなるshiftメソッドを使ってます。

shiftは、行または列を指定した数分ずらすことが可能です。

上記、ソースコードでは0〜1000まで列をずらしていきます。

daskは使えるなら使う

daskは、並列処理ができ、かつpandasと互換性があるため、

高速処理できるということから採用されるケースが目立っています。

しかし、私は今回使用を諦めました。

なぜかという、完全にpandasと互換性があるわけではなく

途中でどうしてもpandasの処理を置き換えることができなくなったためです。

完全に互換性があれば、使用したと思います。

 

ABOUT ME
アバター
ロッピー
コンサルタントから2018年にエンジニアに転向。年収400万円のサラリーマンエンジニアから、半年で月収100万円を稼ぐエンジニアになった。 Python、Golangなど単価の高い言語を得意とする。