Computer Engineering/Data Engineering

Pandas DataFrame apply 성능 이슈 개선하기

jordan.bae 2023. 7. 8. 15:50

Apply는 dataframe의 각 행이나 열에 User-defined function을 실행할 때 사용하는 흔한 옵션입니다. axis=1로 실행하게 되면 각 row에 대해서 연산을 수행하게 됩니다. 하지만, 데이터 사이즈가 큰 경우 메모리 접근 방식과 및 벡터화 되지 않은 연산으로 성능 문제가 발생하기 쉽습니다. 이 글에서는 성능 이슈가 발생하는 원인과 해결책을 간단하게 정리해봤습니다.

 

apply 함수의 성능 이슈 원인

1. 메모리 접근 패턴

 pandas DataFrame은 열(column)-기반의 데이터 저장 구조를 사용합니다. 따라서 열 단위로 데이터에 접근하는 것이 메모리에서 연속적이므로 빠릅니다. 그러나 apply를 사용하여 행(row) 단위로 함수를 적용할 때, 각 행의 데이터는 여러 열에서 가져와져야 하므로 메모리 접근이 연속적이지 않습니다. 이로 인해 캐시 미스와 같은 비효율적인 메모리 접근 패턴이 발생할 수 있습니다.

2. 벡터화 되지 않은 연산

pandasnumpy는 벡터화된 연산을 사용하여 데이터를 빠르게 처리합니다. 그러나 apply 메서드를 사용하면 일반적으로 벡터화되지 않은 연산이 수행됩니다. 이는 성능에 부정적인 영향을 미칠 수 있습니다.

3. 데이터 타입 체크

apply 함수 내에서 데이터 타입 변환이나 체크가 발생할 경우, 이는 추가적인 오버헤드를 발생시킬 수 있습니다.

4. Python 함수 호출 오버헤드

apply 메서드는 각 행 또는 열에 대해 Python 함수를 반복적으로 호출합니다. 이러한 반복적인 함수 호출은 Python의 인터프리터 오버헤드를 초래할 수 있습니다.

 

성능 이슈를 해결하는 방법

코드와 함께 살펴보면 아래와 같습니다.

  1. 최대한 pandas 자체의 벡터 연산 활용하기 (Vectorized Function)
  2. swifter 사용하기
import pandas as pd
import numpy as np
import swifter

def user_func(x) -> int:
    total = sum(x)
    return total

df = pd.DataFrame(np.random.randint(0, 100, size=(3000000, 3)), columns=('a','b','c'))
df2 = df.copy()
df3= df.copy()

%%timeit -n 3 -r 1
df['total'] = df.apply(func=user_func, axis=1)
6.6 s ± 0 ns per loop (mean ± std. dev. of 1 run, 3 loops each)

%%timeit -n 3 -r 1
df2['total'] = df2.sum(axis=1)
173 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 3 loops each)

%%timeit -n 3 -r 1
df3['total'] = df3.swifter.apply(func=user_func, axis=1)
Dask Apply:   0%|          | 0/20 [00:00<?, ?it/s]
Dask Apply:   0%|          | 0/20 [00:00<?, ?it/s]
Dask Apply:   0%|          | 0/20 [00:00<?, ?it/s]
4.86 s ± 0 ns per loop (mean ± std. dev. of 1 run, 3 loops each)

 

Reference

반응형