ITや趣味など気軽に投稿しています。

【Pandas】データフレームの1行1列ごとに複雑な処理を実行する

Pandasのデータフレームで、1行ごとや1列ごとに処理を実行したい場面ではapply()メソッドを使用します。

この記事では、apply()の基本的な使い方から、for文との処理速度の比較まで解説します。

Pandasバージョン

本記事は、Pandas 2.2.3の情報を基に執筆しています。

import pandas as pd
print(pd.__version__)

>>
2.2.3

pd.DataFrame.apply()

apply()はデータフレームの行や列に対して関数を適用するためのメソッドです。

DataFrame.apply(func, axis=0, raw=False, result_type=None, args=(), by_row='compat', engine='python', engine_kwargs=None, **kwargs)

基本的な使い方

以下のデータフレームを例に説明します。

df = pd.DataFrame(
    data={"A":[1,2,3,4,5],
          "B":[10,20,30,40,50],
          "C":[100,200,300,400,500]})

>>
    A    B    C
0   1   36  121
1   4   49  144
2   9   64  169
3  16   81  196
4  25  100  225

ユニバーサル関数の適用

NumPyのユニバーサル関数を指定すると、各要素に対して関数が適用されます。以下はnp.sqrtで平方根を求める例です。

import numpy as np

df.apply(np.sqrt)

>>
A     B     C
0  1.0   6.0  11.0
1  2.0   7.0  12.0
2  3.0   8.0  13.0
3  4.0   9.0  14.0
4  5.0  10.0  15.0

補足: この処理はnp.sqrt(df)と同じ結果になります。ユニバーサル関数の場合は直接適用する方が簡潔です。

集約関数の適用

sum(和)やmean(平均)などの集約関数を使用する場合は、axisで処理方向を指定します。

# 各列ごとの処理(axis=0)
df.apply(np.sum, axis=0)

>>
A     55
B    330
C    855
# 各行ごとの処理(axis=1)
df.apply(np.sum, axis=1)

>>
0    158
1    197
2    242
3    293
4    350

lambda式・ユーザー定義関数の適用

lambda式や独自に定義した関数を適用することもできます。

df.apply(lambda x: x*2, axis=1)

>>
A    B    C
0   2   72  242
1   8   98  288
2  18  128  338
3  32  162  392
4  50  200  450
def multiply(x):
    return x*2

df.apply(multiply, axis=1)

>>
A    B    C
0   2   72  242
1   8   98  288
2  18  128  338
3  32  162  392
4  50  200  450

引数一覧

引数必須説明
func適用する関数
axis0または'index'で列方向の処理、1または'columns'で行方向の処理
rawFalse: 各行をSeriesとして関数に渡す。True: 各行をndarrayとして関数に渡す
result_typeaxis=1の場合のみ有効。関数の戻り値の形式を指定する。デフォルトはNone
args関数に渡す追加の位置引数をタプル形式で指定する
by_rowfuncがリストまたは辞書形式の場合に有効。関数の適用方法を制御する
engine'python'または'numba'のいずれかを指定する
engine_kwargsengine='numba'の場合に使用する追加引数を辞書形式で指定する

for文とのパフォーマンス比較

apply()とfor文は似たような処理を記述できますが、大量のデータに対する処理速度に大きな差があります。

検証データ

以下のようなウォルマートの株価データフレームを使用します。

import pandas as pd

df = pd.read_csv("../data/walmart_stock.csv").set_index("Date").head(5)

>>
Date     Close/Last  Volume    Open    High      Low                                                
09/22/2020   138.31    11034020  137.07  138.9900  136.57
09/21/2020   137.07    15758500  136.50  137.2400  135.29
09/18/2020   135.29    18236350  136.44  137.5300  134.75
09/17/2020   136.69    12415030  135.43  137.5700  135.02
09/16/2020   136.26     9292649   138.41  138.6825  136.05

処理内容は、Open列が136.5以上の行について、新規列Diffを作成しHighLowの差分を格納するものです。

# 処理後の結果。Diff列が追加される。
>>
Date         Close/Last  Diff    High      Low     Open    Volume                                                        
09/22/2020   138.31      2.4200  138.9900  136.57  137.07  11034020.0
09/21/2020   137.07      1.9500  137.2400  135.29  136.50  15758500.0
09/18/2020   135.29      NaN     137.5300  134.75  136.44  18236350.0
09/17/2020   136.69      NaN     137.5700  135.02  135.43  12415030.0
09/16/2020   136.26      2.6325  138.6825  136.05  138.41   9292649.0

for文での実装

for i, row in df.iterrows():
    if row["Open"] >= 136.5:
        df.loc[i, "Diff"] = row["High"] - row["Low"]

apply()での実装

def SetDiff(row):
    if row["Open"] >= 136.5:
        row["Diff"] = row["High"] - row["Low"]
    return row

# 1行ごとにSetDiff()を適用
df = df.apply(SetDiff, axis=1)

パフォーマンス比較結果

データフレームを複製し、10万件・100万件のデータで処理時間を比較しました。

for文apply()
10万件37.5秒10秒
100万件3,152.3秒324.6秒

100万件の場合、処理時間に約10倍の差が生じます。大量データを扱う場合はapply()の使用を強く推奨します。

補足: さらなる高速化が必要な場合は、ベクトル演算(NumPyの配列操作)やPandasのeval()/query()の活用を検討してください。

まとめ

今回は、apply()の使い方とfor文とのパフォーマンス比較を紹介しました。

  • apply()はデータフレームの行・列に対して関数を適用するメソッド
  • axisパラメータで行方向(axis=1)と列方向(axis=0)を切り替えられる
  • lambda式やユーザー定義関数を柔軟に適用できる
  • for文と比較して、大量データ処理時に約10倍の速度差がある

大量のデータを処理する際は、for文ではなくapply()を活用することで、処理時間を大幅に短縮できます。