はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Pandasである一覧データから別のキー一覧指定のレコードのみ抽出(の処理時間傾向の雑な計測)

ある一覧データから別の一覧データのキーに存在するものを抽出するという要件があります。

f:id:azotar:20210201200945p:plain

Pandasで言うと、上図のように、2つのDataFrameをmerge(つまり表データのJOINの論法ですね)することで、このようなデータを取得するというのが私の手グセなのですが、結合したDataFrameで後続の処理を行うことも多いので、この方法に特に疑問を持っていませんでした。

今更なのですが、この処理方式って自分が思うよりコスト高な処理なのではとふと頭をよぎったので、今回、同じようなことができる複数のロジック表記例について、雑に時間計測してみました。

前準備

import pandas as pd
import numpy as np
import random

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')

df = df_orig.copy()

listb = random.sample(lista, int(len(lista)/3))
df2 = pd.DataFrame([[i, j] for i, j in zip(listb, listb)], columns=['a', 'b'])
bar = df2.set_index('a').to_dict(orient='index')

比較例一覧

def a(df2):
    # mergeでフィルターです。
    # 生成したDataFrameの二次利用もふまえるとそんなに悪くない(個人的にはスキな)イディオムだと思うのですが....
    df3 = pd.merge(df, df2, how='inner', on='a')
    df3['a']


def b(listb):
    # isinです。
    cond = df['a'].isin(listb)
    df[cond]


def c(listb):
    # これは比較用の例で、Pandasは使われていません。
    for i in listb:
        foo.get(i)


def c2(bar):
    # これは比較用の例で、Pandasは使われていません。
    for i in foo.keys():
        bar.get(i)


def d():
    # mergeではなく、applyを使う案のひとつです。
    # DataFrame全体をターゲットにしているため、これは遅いです。
    df.apply(lambda s: foo.get(s['a']), axis=1)


def d2():
    # d()の要件であれば、最初にSeriesを取り出した方が良いですね。
    df['a'].apply(lambda x: foo.get(x))


def e():
    # mergeではなく、縦に結合して、重複(=キーが重なる。つまり、対象のデータである)を抜き出すことで、該当の要件を実現します。
    # この例では、全ての項目が同じものがあぶり出されます(ので、オーバースペックというか、ここまでの要件をぴったり実現したものではありません)
    df3 = pd.concat([df, df2])
    df3[df3.duplicated()]


def e2():
    # e()の実際の例
    # 個人の感想ですが、「関数型プログラミング」ぽいアプローチかも。 
    df3 = pd.concat([df, df2])
    df3[df3.duplicated('a')]

計測結果

In [6]: %time a(df2) 
   ...:  
   ...: %time b(listb) 
   ...:  
   ...: %time c(listb) 
   ...:  
   ...: %time c2(bar) 
   ...:  
   ...: %time d() 
   ...:  
   ...: %time d2() 
   ...:  
   ...: %time e() 
   ...:  
   ...: %time e2()                               
                                                                                                                          
CPU times: user 693 ms, sys: 84.7 ms, total: 777 ms
Wall time: 777 ms

CPU times: user 422 ms, sys: 4.78 ms, total: 427 ms
Wall time: 428 ms

CPU times: user 174 ms, sys: 289 µs, total: 174 ms
Wall time: 174 ms

CPU times: user 251 ms, sys: 363 µs, total: 252 ms
Wall time: 252 ms

CPU times: user 10.4 s, sys: 16.4 ms, total: 10.4 s
Wall time: 10.5 s

CPU times: user 431 ms, sys: 2.29 ms, total: 433 ms
Wall time: 435 ms

CPU times: user 732 ms, sys: 48.4 ms, total: 780 ms
Wall time: 784 ms

CPU times: user 420 ms, sys: 11.8 ms, total: 432 ms
Wall time: 433 ms

In [7]:                                                                                                                                                                    

所感(mergeでフィルターを代用するのはそれほど悪くないかも)

データバリエーションや複数回計測していない状況なので、あくまで所感ですが、個人的なフェイバリットのmerge方式(関数aの例)は、生成したDataFrameの二次利用に続くならそれほど悪くないと思いました。

抽出自体のみで良い場合は、isin(リスト)方式(関数bの例)がコードが簡潔であることも含めて、優勝かな。