Skip to content

Instantly share code, notes, and snippets.

@dakeshi19
Last active November 6, 2021 10:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dakeshi19/106acb28c8c14ee34ba98a76b68c9c4f to your computer and use it in GitHub Desktop.
Save dakeshi19/106acb28c8c14ee34ba98a76b68c9c4f to your computer and use it in GitHub Desktop.
Pandasのapply関連の書きっぷりバリエーションと処理時間の雑な傾向確認
import numpy as np
import random
import pandas as pd
import numpy as np
import sys
import inspect
"""
Pandasでapply、特にaxis=1で処理したくなるような要件の可読性と速度の雑な確認
(IPythonのREPLに貼り付けしてためした)
関数にコメントをつけていますが、手元の環境で走行して得られた走行時間を見た感想です。
(PandasやNumpyのソースコードは確認していませんが、DataFrameは列方向は、リスト系のストアになっていて、高速なんだろうな、
逆にいうと行方向は低速なんだな)という印象です。適切な用語ではないかもしれませんが、DataFrameは表を連想させますが、中身はカラムナデータベースっぽいアプローチなのではと思いました。)
"""
# サンプルデータの仕込み
np.random.seed(seed=64)
random.seed(64)
lista = list(map(str, list(np.random.rand(1000000))))
df_orig = pd.DataFrame([[i, j]
for i, j in zip(lista, lista)], columns=['a', 'b'])
foo = df_orig.set_index('a').to_dict(orient='index')
"""
IPythonで%time計測
df = df_orig.copy()
%time df.to_dict(orient='index')
CPU times: user 2.66 s, sys: 73.5 ms, total: 2.74 s
Wall time: 2.74 s
%time df.to_dict(orient='list')
CPU times: user 16.9 ms, sys: 38 µs, total: 17 ms
Wall time: 17 ms
%time df.to_dict(orient='records')
CPU times: user 2.18 s, sys: 15.6 ms, total: 2.19 s
Wall time: 2.19 s
**** to_dict(orient='list')が100倍高速 !!! ****
"""
# [観点1] DataFrameとSeriesでのapplyの違い
def ex1():
# まず見本市の例
colname = sys._getframe().f_code.co_name
df[colname] = df['a'].apply(lambda x: foo.get(x))
def ex2():
# 1カラムのみターゲットとしているのはex1と同じだが、DataFrameにapplyする場合の所用時間を知りたい
# (お話にならないぐらい遅い。ex1の10倍以上のオーダー。...なので、ex1で良い場合はex1のシンタックスで記載すべき。)
colname = sys._getframe().f_code.co_name
df[colname] = df[['a']].apply(lambda s: foo.get(s['a']), axis=1)
def ex3():
# ex1のおおよそ2倍になるのか気になって確認 → 2倍だった
colname = sys._getframe().f_code.co_name
for i in ['a', 'b']:
df[colname + i] = df[i].apply(lambda x: foo.get(x))
def ex4():
# ex3などと比較して、locの場合はどうか(高速なのでは?)→この例ではあまり変わらず。
colname = sys._getframe().f_code.co_name
for i in ['a', 'b']:
df.loc[:, colname + i] = df[i].apply(lambda x: foo.get(x))
# [観点2] 以降は「右辺」側に興味がシフト。複数カラムから何かを生成する要件を意識した比較。
def ex5():
# (この要件ならもちろんこのような記述はしない方が良いが、) axis=1で複数カラムから新たな値を算出したい場合の擬似の例
# → 個人的にはこのシンタックスは感覚に馴染むが、まあ、それなりにというか圧倒的に遅い。手元だと約20秒。
colname = sys._getframe().f_code.co_name
df[colname] = df.apply(lambda s: s['a'] + s['b'], axis=1)
def ex6():
# 一度別の形式にして、Seriesライクなデータ編集したものを代入するとex5より高速か? → 高速!
colname = sys._getframe().f_code.co_name
l = len(df)
s = df.to_dict(orient='list')
df[colname] = [s['a'][i] + s['b'][i] for i in range(l)]
def ex7():
# ex6の一度別の形式にしたもの。→ 可読性は上がった気がするが、どっちかといえば、df.to_dict(orient='records')が遅いので、割に合わなさそう。
colname = sys._getframe().f_code.co_name
s = df.to_dict(orient='records')
df[colname] = [i['a'] + i['b'] for i in s]
# 引き続き同じ例を念頭におくが、しばし、applyのlambdaへのカラムの渡し方での違いを確認する。
# 先にapplyで適用することになる関数を定義
# Seriesを受け取って、そのうちaとbの値を結合する
def abfunc1(s):
return s['a'] + s['b']
def ex8():
# あまり関係なさそうだけど、事前に定義した関数をapplyに渡すとどうだろうかの例
# 残念ながら、DataFrameに対するapplyの時点で、遅い。
colname = sys._getframe().f_code.co_name
df[colname] = df.apply(abfunc1, axis=1)
# 2つのリストを受け取って、対応する要素を結合する
v_abfunc2 = np.vectorize(lambda a, b: a+b)
def ex9():
# ex6などで高速な部類であることを確かめられたやり方(一度他の形式にして演算後に代入)を、
# ユニバーサル化した関数で可読性を保ちつつ実現
# → 私見では、手間と得られる効果のバランスは良い(それなりに高速)と感じた
colname = sys._getframe().f_code.co_name
s = df.to_dict(orient='list')
df[colname] = v_abfunc2(s['a'], s['b'])
def ex10():
# ex9のシンタックスシュガーのつもりだったが、手元の確認だと、ex9の2倍程度の処理時間を要した。... ので、同じことをするなら、ex9推奨。
colname = sys._getframe().f_code.co_name
s = df.to_dict(orient='list')
df[colname] = v_abfunc2(**s)
def ex11():
# 実は、ex5以降の共通例とした要件で良いなら、これが一番高速
colname = sys._getframe().f_code.co_name
df[colname] = df['a'] + df['b']
def ex12():
# 何か挟む編集だとex11に比べスピードが落ちるのでは? → そうでもなさそう(結合数相応)。
colname = sys._getframe().f_code.co_name
df[colname] = df['a'] + ',' + df['b']
def ex13():
# カンマ結合であれば、例えば strアクセサを使うと良い。宣言的に書けるのは良いが、+で結合する方が、可読性が高いと感じる人も多いかも。
colname = sys._getframe().f_code.co_name
df[colname] = df['a'].str.cat([df['b'], df['b']], sep=',')
# 一気に確認
funcs = [ex1, ex2, ex3, ex4, ex5, ex6, ex7, ex8, ex9, ex10, ex11, ex12, ex13]
for fn in funcs:
print()
print('---------')
print()
print(fn.__name__)
print(inspect.getsource(fn).split('\n')[-2])
df = df_orig.copy()
fn()
# %time fn() # ********** IPythonではこちらで確認 *************
print(df.head(3))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment