TLDR; いいえ、for
ループは、必ずしも常にではありませんが、包括的ではありません。いくつかのベクトル化された操作は反復よりも遅いと言う方が、おそらくより正確であり、反復はいくつかのベクトル化された操作よりも速いと言います。コードから最大のパフォーマンスを引き出すには、いつ、どのような理由でそれを知ることが重要です。一言で言えば、これらはベクトル化されたパンダ関数の代替を検討する価値がある状況です:
- データが小さい場合(...何をしているかによります)、
object
/ mixed dtypes を扱う場合
str
/ regexアクセサー関数を使用する場合
これらの状況を個別に調べてみましょう。
小さなデータでの反復対ベクトル化
Pandasは、そのAPI設計において、「Convention Over Configuration」アプローチに従います。これは、同じAPIが幅広いデータとユースケースに対応するように適合されていることを意味します。
pandas関数が呼び出されたとき、機能することを保証するために、(特に)次のことを関数によって内部的に処理する必要があります。
- インデックス/軸合わせ
- 混合データ型の処理
- 欠落データの処理
ほとんどすべての関数はこれらをさまざまな範囲で処理する必要があり、これによりオーバーヘッドが発生します。オーバーヘッドは、数値関数(たとえば、Series.add
)の方が少なく、文字列関数(たとえば、Series.str.replace
)の場合はより顕著です。
for
一方、ループは思ったよりも高速です。さらに優れているのは、リスト内包表記(for
ループを介してリストを作成する)が、リスト作成のための最適化された反復メカニズムであるため、さらに高速であることです。
リスト内包表記は次のパターンに従います
[f(x) for x in seq]
seq
pandasシリーズまたはDataFrame列はどこにありますか。または、複数のカラムを操作する場合、
[f(x, y) for x, y in zip(seq1, seq2)]
どこseq1
とseq2
列です。
数値比較
単純なブールインデックス操作を考えてみましょう。リスト内包表記メソッドは、Series.ne
(!=
)およびに対してタイミングがとられていquery
ます。ここに関数があります:
# Boolean indexing with Numeric value comparison.
df[df.A != df.B] # vectorized !=
df.query('A != B') # query (numexpr)
df[[x != y for x, y in zip(df.A, df.B)]] # list comp
簡単にするために、perfplot
この記事ではすべてのtimeitテストを実行するためにパッケージを使用しました。上記の操作のタイミングは次のとおりです。
リスト内包表記はquery
、適度なサイズのNの場合よりも優れており、小さなNの場合のベクトル化不等比較よりも優れています。残念ながら、リスト内包表記は線形にスケーリングするため、より大きなNの場合、パフォーマンスはあまり向上しません。
注
リスト内包表記の利点の多くはインデックスアライメントを気にする必要がないことからもたらされますが、これは、コードがインデックスアライメントに依存している場合、機能しなくなることを意味します。場合によっては、基になるNumPy配列に対するベクトル化された操作は、「両方の長所」をもたらすと見なすことができ、パンダ関数の不要なオーバーヘッドなしでベクトル化が可能になります。これは、上記の操作を次のように書き換えることができることを意味します
df[df.A.values != df.B.values]
これはパンダとリスト内包表記の両方を上回っています
。NumPyベクトル化はこの投稿の範囲外ですが、パフォーマンスが重要な場合は、検討する価値があります。
値のカウント
別の例として、今回は、forループよりも高速な別のpythonコンストラクトを使用しますcollections.Counter
。一般的な要件は、値の数を計算し、結果を辞書として返すことです。これはで行われvalue_counts
、np.unique
とCounter
:
# Value Counts comparison.
ser.value_counts(sort=False).to_dict() # value_counts
dict(zip(*np.unique(ser, return_counts=True))) # np.unique
Counter(ser) # Counter
結果はより顕著になり、Counter
小さいN(〜3500)のより広い範囲で両方のベクトル化された方法よりも優れています。
メモ
雑学(@ user2357112提供)。Counter
で実装されているCアクセラレータ代わりに基礎となるCのデータ型のオブジェクト、それはより速くまだそれはまだのpythonで動作するように持っているようにしながら、for
ループ。Pythonパワー!
もちろん、ここからのポイントは、パフォーマンスはデータとユースケースに依存するということです。これらの例のポイントは、これらのソリューションを正当なオプションとして除外しないように説得することです。それでも必要なパフォーマンスが得られない場合は、常にcythonとnumbaがあります。このテストをミックスに追加しましょう。
from numba import njit, prange
@njit(parallel=True)
def get_mask(x, y):
result = [False] * len(x)
for i in prange(len(x)):
result[i] = x[i] != y[i]
return np.array(result)
df[get_mask(df.A.values, df.B.values)] # numba
Numbaは、ループpythonコードのJITコンパイルを非常に強力なベクトル化されたコードに提供します。numbaを機能させる方法を理解するには、学習曲線が必要です。
混合/ object
dtypeの操作
文字列ベースの比較
最初のセクションのフィルタリングの例に戻り、比較される列が文字列の場合はどうなりますか?上記と同じ3つの関数を考えますが、入力DataFrameは文字列にキャストされます。
# Boolean indexing with string value comparison.
df[df.A != df.B] # vectorized !=
df.query('A != B') # query (numexpr)
df[[x != y for x, y in zip(df.A, df.B)]] # list comp
では、何が変わったのですか?ここで注意すべきことは、文字列操作は本質的にベクトル化するのが難しいということです。Pandasは文字列をオブジェクトとして扱い、オブジェクトに対するすべての操作は低速でループの多い実装にフォールバックします。
さて、このルーピーな実装は、上記のすべてのオーバーヘッドに囲まれているため、これらのソリューションのスケーリングは同じであっても、これらのソリューションの間には一定の大きさの違いがあります。
可変/複雑なオブジェクトの操作に関しては、比較はありません。リスト内包表記は、辞書やリストを含むすべての操作よりも優れています。
キーによるディクショナリ値へのアクセスディクショナリの
列から値を抽出する2つの操作のタイミングmap
と、リスト内包表記を次に示します。設定は、付録の「コードスニペット」の下にあります。
# Dictionary value extraction.
ser.map(operator.itemgetter('value')) # map
pd.Series([x.get('value') for x in ser]) # list comprehension
列のリストから0番目の要素(例外の処理)map
、str.get
アクセサメソッド、およびリスト内包を抽出する3つの操作の定位置リストインデックスのタイミング:
# List positional indexing.
def get_0th(lst):
try:
return lst[0]
# Handle empty lists and NaNs gracefully.
except (IndexError, TypeError):
return np.nan
ser.map(get_0th) # map
ser.str[0] # str accessor
pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]) # list comp
pd.Series([get_0th(x) for x in ser]) # list comp safe
注
インデックスが重要な場合は、次のようにします。
pd.Series([...], index=ser.index)
シリーズを再構築するとき。
リストのフラット化
最後の例は、リストのフラット化です。これはもう1つの一般的な問題であり、純粋なPythonがいかに強力であるかを示しています。
# Nested list flattening.
pd.DataFrame(ser.tolist()).stack().reset_index(drop=True) # stack
pd.Series(list(chain.from_iterable(ser.tolist()))) # itertools.chain
pd.Series([y for x in ser for y in x]) # nested list comp
両方itertools.chain.from_iterable
とネストされたリスト内包表記は、純粋なpython構造であり、stack
ソリューションよりもはるかに優れています。
これらのタイミングは、パンダが混合dtypeで動作するように装備されていないという事実を強く示しており、そのために使用することはおそらく控える必要があります。可能な限り、データは個別の列にスカラー値(ints / floats / strings)として存在する必要があります。
最後に、これらのソリューションの適用性はデータに大きく依存します。したがって、何をすべきかを決定する前に、データに対してこれらの操作をテストするのが最善の方法です。apply
グラフをゆがめるため、これらのソリューションで時間を計っていないことに注意してください(そう、それは遅いです)。
正規表現の操作と.str
アクセサーメソッド
パンダは、次のような正規表現の操作を適用することができstr.contains
、str.extract
およびstr.extractall
、ならびに(例えば、他の「ベクトル化」文字列操作str.split
str.find、,
文字列の列に、等str.translate`)。これらの関数はリスト内包表記よりも遅く、何よりも便利な関数であることが意図されています。
通常、正規表現パターンをプリコンパイルしてデータを反復処理する方がはるかに高速ですre.compile
(Pythonのre.compileを使用する価値はありますか?も参照してください)。に相当するリストcomp str.contains
は次のようになります。
p = re.compile(...)
ser2 = pd.Series([x for x in ser if p.search(x)])
または、
ser2 = ser[[bool(p.search(x)) for x in ser]]
NaNを処理する必要がある場合は、次のようなことができます
ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]]
str.extract
(グループなしの)に相当するリストcomp は次のようになります。
df['col2'] = [p.search(x).group(0) for x in df['col']]
非一致とNaNを処理する必要がある場合は、カスタム関数を使用できます(さらに高速です)。
def matcher(x):
m = p.search(str(x))
if m:
return m.group(0)
return np.nan
df['col2'] = [matcher(x) for x in df['col']]
matcher
機能は非常に拡張可能です。必要に応じて、各キャプチャグループのリストを返すように調整できます。matcherオブジェクトのgroup
or groups
属性を抽出するだけです。
の場合str.extractall
、に変更p.search
しp.findall
ます。
文字列抽出
簡単なフィルタリング操作を考えてみましょう。大文字が前に付いている場合は、4桁を抽出するという考え方です。
# Extracting strings.
p = re.compile(r'(?<=[A-Z])(\d{4})')
def matcher(x):
m = p.search(x)
if m:
return m.group(0)
return np.nan
ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False) # str.extract
pd.Series([matcher(x) for x in ser]) # list comprehension
その他の例
完全な開示-私は、以下にリストするこれらの投稿の作成者(一部または全体)です。
結論
上記の例からわかるように、DataFrameの小さな行、混合データ型、および正規表現を操作する場合に、反復が効果的です。
あなたが得るスピードアップはあなたのデータとあなたの問題に依存するので、あなたのマイレージは異なるかもしれません。最善の方法は、慎重にテストを実行し、支払いの価値があるかどうかを確認することです。
「ベクトル化された」関数は、そのシンプルさと読みやすさの点で優れています。そのため、パフォーマンスが重要でない場合は、これらを優先する必要があります。
別の注意点として、特定の文字列操作はNumPyの使用を好む制約を扱います。以下は、注意深いNumPyベクトル化がPythonよりも優れている2つの例です。
さらに、.values
SeriesまたはDataFramesとは対照的に、基になるアレイを操作するだけで、ほとんどの通常のシナリオで十分な速度の向上が得られる場合があります(上記の数値比較セクションの注を参照)。したがって、たとえばは、を超えるとすぐにパフォーマンスが向上します。使用はすべての状況で適切であるとは限りませんが、知っておくと便利です。df[df.A.values != df.B.values]
df[df.A != df.B]
.values
上記のように、これらのソリューションを実装するという面倒な価値があるかどうかを決めるのはあなた次第です。
付録:コードスニペット
import perfplot
import operator
import pandas as pd
import numpy as np
import re
from collections import Counter
from itertools import chain
# Boolean indexing with Numeric value comparison.
perfplot.show(
setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B']),
kernels=[
lambda df: df[df.A != df.B],
lambda df: df.query('A != B'),
lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
lambda df: df[get_mask(df.A.values, df.B.values)]
],
labels=['vectorized !=', 'query (numexpr)', 'list comp', 'numba'],
n_range=[2**k for k in range(0, 15)],
xlabel='N'
)
# Value Counts comparison.
perfplot.show(
setup=lambda n: pd.Series(np.random.choice(1000, n)),
kernels=[
lambda ser: ser.value_counts(sort=False).to_dict(),
lambda ser: dict(zip(*np.unique(ser, return_counts=True))),
lambda ser: Counter(ser),
],
labels=['value_counts', 'np.unique', 'Counter'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=lambda x, y: dict(x) == dict(y)
)
# Boolean indexing with string value comparison.
perfplot.show(
setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B'], dtype=str),
kernels=[
lambda df: df[df.A != df.B],
lambda df: df.query('A != B'),
lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
],
labels=['vectorized !=', 'query (numexpr)', 'list comp'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Dictionary value extraction.
ser1 = pd.Series([{'key': 'abc', 'value': 123}, {'key': 'xyz', 'value': 456}])
perfplot.show(
setup=lambda n: pd.concat([ser1] * n, ignore_index=True),
kernels=[
lambda ser: ser.map(operator.itemgetter('value')),
lambda ser: pd.Series([x.get('value') for x in ser]),
],
labels=['map', 'list comprehension'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# List positional indexing.
ser2 = pd.Series([['a', 'b', 'c'], [1, 2], []])
perfplot.show(
setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
kernels=[
lambda ser: ser.map(get_0th),
lambda ser: ser.str[0],
lambda ser: pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]),
lambda ser: pd.Series([get_0th(x) for x in ser]),
],
labels=['map', 'str accessor', 'list comprehension', 'list comp safe'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Nested list flattening.
ser3 = pd.Series([['a', 'b', 'c'], ['d', 'e'], ['f', 'g']])
perfplot.show(
setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
kernels=[
lambda ser: pd.DataFrame(ser.tolist()).stack().reset_index(drop=True),
lambda ser: pd.Series(list(chain.from_iterable(ser.tolist()))),
lambda ser: pd.Series([y for x in ser for y in x]),
],
labels=['stack', 'itertools.chain', 'nested list comp'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Extracting strings.
ser4 = pd.Series(['foo xyz', 'test A1234', 'D3345 xtz'])
perfplot.show(
setup=lambda n: pd.concat([ser4] * n, ignore_index=True),
kernels=[
lambda ser: ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False),
lambda ser: pd.Series([matcher(x) for x in ser])
],
labels=['str.extract', 'list comprehension'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
pd.Series
そしてpd.DataFrame
今、反復可能オブジェクトから構築をサポート。つまり、最初にリストを作成する必要はなく(リスト内包表記を使用)、Pythonジェネレーターをコンストラクター関数に渡すだけで済みます。多くの場合、これは遅くなる可能性があります。ただし、発電機出力のサイズを事前に決定することはできません。どのくらいの時間/メモリのオーバーヘッドが発生するかわかりません。