맨땅에 헤딩하는 사람

[Python] pandas DataFrame 최적화 (삽입, 생성, 반복, 문자열) 본문

파이썬/이론

[Python] pandas DataFrame 최적화 (삽입, 생성, 반복, 문자열)

purplechip 2020. 8. 25. 01:27

pandas DataFrame은 데이터 분석에 있어 매우 강력한 도구이다. 그러나 그만큼 올바른 사용법이 필요하고 잘못된 사용법을 적용할 경우 매우 답답한 프로그램을 경험하게 된다. 그만큼 DataFrame 최적화는 프로그램의 효율성을 위해 어느정도 필요하다. 이번 포스팅에서는 pandas DataFrame을 최적화하는 방법을 알아본다.

 

반복 최적화

DataFrame에서 row 값을 사용한 어떤 수행을 반복한다고 하자. 이 때 고려해야 할 순서는 다음과 같다.

 

  1. 벡터화 (vectorization)
  2. pandas apply 사용 (Cython에서 실행)
  3. itertuples() 사용
  4. index 사용(속도 df.at() > df.loc()
  5. iterrows() 사용
  6. len 단순 반복  

이에 대한 자세한 설명은 "판다스 코드 속도 최적화를 위한 초보자 안내서"에서 자세히 소개한다. 그렇다면 '대체 왜 pandas 최적화를 사용하는 것이 속도 절감이 되는 것일까?' 하는 의문을 갖지 않을 수 없다. 그 이유는 3가지 정도로 압축할 수 있는데 파이썬 자료구조의 런타임 오버헤드 제거, C 배열 캐싱, SIMD이다. 

 

파이썬은 동적으로 프로그램이 실행되는 인터프리터 언어이기 때문에 파이썬 내 list 등 배열의 각 요소에 대해 참조 시 런타임 오버헤드가 있다. 가령 list의 경우 슬라이싱 등을 판단해야 하기 때문에 인덱스에 들어가는 값을 decode 해야한다. 하지만 pandas는 C언어 array 기반이기 때문에 처음 참조 시만 오버헤드가 생기며 이 차이는 큰 차이를 만들어낸다. 반복의 모든 부분에서 오버헤드가 생기는 것과 반복이 시작될 때만 오버헤드가 발생하는 것을 고려하면 2n과 n의 차이만큼 속도 변화가 있음을 알 수 있다. 또한 C array라는 차이가 또 다른 부분에서 적용이 된다. 캐시 활용 측면에서도 C array를 반복하는 것이 캐시 친화적이다. (물론 DataFrame을 각 요소에 대해 참조 시간이 부여될 수 있도록 코딩할 수 있다. 이런 코딩 방식은 DataFrame을 사용할 때 반드시 지양해야 한다.)

 

추가적으로 SIMD 명령어를 통해 벡터화가 가능하다. SIMD란 Single Instruction Multiple Data의 약자로 하나의 CPU 명령어로 다수의 데이터를 한번에 처리하는 방식을 의미한다. 아래 그림이 SIMD를 도식화한 것이다.

[그림 1. SIMD 동작구조 (출처 : Wikipedia)]

쉽게 C array 배열이 있다고 보면 그 배열 자체를 한꺼번에 계산한다고 생각하면 된다. 이렇게 되면 DataFrame 내 row 갯수만큼 명령어가 반복실행되는 것이 크게 한 번의 명령어 실행으로 해결되는 것이다. 당연히 속도면에서 큰 체감이 생길 수 밖에 없다.

 

문자열 최적화

C array가 문자열 함수에도 적용이 될까 싶은 의문이 있는데 pandas에서는 그 고민을 말끔히 날려준다. Series 자료구조는 str() 이라는 문자열 벡터화 함수를 제공한다. 이에 대한 내용은 pandas document에서 자세히 확인 가능하다. regular expression 사용도 가능하며 다재다능한 여러 기능을 가지고 있다. 위 링크 글에서는 실수 연산을 예로 들고 있으므로 한번 위에 제시된 여러 반복 처리 방식을 문자열 column에 적용하여 속도 측정을 해보자. 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from pandas import DataFrame 
import pandas
import re
import string
import random
 
alphabet = string.ascii_uppercase
 
def str_cat(s1, s2):
    s1 = s1.replace('-''')
    return s1.join(s2)
 
list1 = [random.choice(alphabet) + '-' for _ in range(100000)]
list2 = [random.choice(alphabet) for _ in range(100000)]
 
df = DataFrame(zip(list1, list2), columns=['s1''s2'])
 
## len을 이용한 반복
%%timeit
s3_list = []
for idx in range(len(df)):
    s3 = str_cat(df.iloc[idx]['s1'], df.iloc[idx]['s2'])
    s3_list.append(s3)
df['s3'= s3_list
 
## iterrow을 이용한 반복
%%timeit
s3_list = []
for idx, row in df.iterrows():
    s3 = str_cat(row['s1'], row['s2'])
    s3_list.append(s3)
df['s3'= s3_list
 
## index loc를 이용한 반복
%%timeit
s3_list = []
for idx in df.index:
    s3 = str_cat(df.loc[idx, 's1'], df.loc[idx, 's2'])
    s3_list.append(s3)
df['s3'= s3_list
 
## index at를 이용한 반복
%%timeit
s3_list = []
for idx in df.index:
    s3 = str_cat(df.at[idx, 's1'], df.at[idx, 's2'])
    s3_list.append(s3)
df['s3'= s3_list
 
## itertuple을 이용한 반복
%%timeit
s3_list = []
for row in df.itertuples():
    s3 = str_cat(getattr(row, 's1'), getattr(row, 's2'))
    s3_list.append(s3)
df['s3'= s3_list
 
## apply 함수 사용
%%timeit
df['s3'= df.apply(lambda row: str_cat(row['s1'], row['s2']), axis=1)
 
## str 함수 사용 (벡터화)
%%timeit
df['s3'= df['s1'].str.replace('-''').str.cat(df['s2'])
cs

위 코드는 's1'에서 '-'를 제거한 후 's2'cat한 결과를 's3'에 할당하는 코드들이다. df 를 출력하면 다음과 같다.

>>> df
       s1 s2  s3
0      D-  M  DM
1      Q-  J  QJ
2      P-  V  PV
3      G-  K  GK
4      V-  U  VU
...    .. ..  ..
99995  B-  C  BC
99996  C-  L  CL
99997  I-  K  IK
99998  A-  T  AT
99999  B-  D  BD
 
[100000 rows x 3 columns]
cs

코드 설명을 하면 %%timeit 은 ipython(jupyter, spyder 등)에서 셀의 실행 시간을 측정해주는 기능이다. 또한 df.loc 함수를 df.loc[idx][col], df.loc[idx, col] 두 가지로 표현할 수 있는데 후자가 속도가 월등히 빠른 대비 전자는 len 사용과 비슷한 수준임을 확인했다. 전자는 두 번 참조라면 후자는 한 번에 위치를 정해주기 때문으로 추측한다. 그리고 df.loc이 슬라이싱 기능까지 있는거에 비해 df.at은 scalar 참조만 가능하므로 속도가 훨씬 빠르다. 요소만 참조하는 코드라면 df.at을 사용하는 것이 효율적이다. 결과를 정리하면 아래와 같다.

방식 소요시간 직전 대비 증가율
len 단순 반복

iterrows() 사용

apply 함수 

index loc 사용

index at 사용

itertuples() 사용

벡터화 (str 함수)
13.2 s ± 85.8 ms per loop

7.19 s ± 43.8 ms per loop

1.98 s ± 45.5 ms per loop

1.22 s ± 38.2 ms per loop 

754 ms ± 34 ms per loop

103 ms ± 1.25 ms per loop

48.8 ms ± 672 µs per loop
0

1.84x

3.63x

1.62x

1.61x

7.32x

2.11x

결과에서 apply 함수를 사용한 경우가 index, itertuples()를 사용한 경우보다 느렸는데 이는 문자열을 다루다보니 apply 가 Cython에서 동작하지 않은 것으로 추측된다. 최고 속도와 최저 속도는 약 270배가 차이난다.

 

삽입, 생성 최적화

DataFrame이 C array 기반이라는 사실을 알면 삽입, 생성에서 어떤 것이 더 빠른 속도를 가지는 것일지 예상이 갈 것이다. 결론부터 말하면 삽입보단 생성이 빠르다. 반복문을 통해 계속 배열 사이즈를 늘려가며 삽입하는 것보다 애초부터 큰 메모리 할당을 받아 생성하는 것이 C array 기준에서 빠를 수 밖에 없다. 이것 역시 코드를 직접 실행하면서 결과를 확인해보자. 비교 대상은 총 4개로 (1) append, (2) loc, (3) 미리 할당된 loc, (4) DataFrame 생성이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pandas import DataFrame 
import string
import random
 
alphabet = string.ascii_uppercase
 
## append
%%timeit
df = DataFrame(columns=['s1''s2'])
for i in range(1000):
    df.append([random.choice(alphabet), random.choice(alphabet)])
 
## loc 
%%timeit
df = DataFrame(columns=['s1''s2'])
for i in range(1000):
    df.loc[i] = [random.choice(alphabet), random.choice(alphabet)]
 
## 정의된 loc
%%timeit
index = [i for i in range(1000)]
df = DataFrame(columns=['s1''s2'], index=index)
for i in index:
    df.loc[i] = [random.choice(alphabet), random.choice(alphabet)
 
## DataFrame 생성
%%timeit
index = [i for i in range(1000)]
list1 = [[random.choice(alphabet), random.choice(alphabet)] for _ in range(1000)]
df = DataFrame(list1, columns=['s1''s2'], index=index)
cs

원래 십만 번을 하려고 했지만 엄청 느릴 것 같아서 천 번만 반복시키기로 했다. 결과는 아래와 같다.

방식 소요시간 직전 대비 증가율
loc()

append()

정의된 loc()

DataFrame 생성
1.63 s ± 23.7 ms per loop

1.26 s ± 26.2 ms per loop

83.9 ms ± 1.31 ms per loop

1.69 ms ± 20.9 µs per loop
0

1.29x

15x

49.6x

엄청난 차이를 확인할 수 있다. 제일 빠른 방식과 느린 방식은 약 960배의 차이를 보인다. 작은 데이터는 상관없지만 큰 데이터를 DataFrame으로 만들 때 반드시 생성하는 것을 추천한다.

 

마치며

pandas는 정말 강력한 도구이지만, 잘못 사용할 경우 굼벵이같은 속도를 경험할 것이다. 코드 가독성 면으로나, 효율적인 면으로 보나 벡터화는 필수이다.

 

참고

DataFrame 반복 최적화

aldente0630.github.io/data-science/2018/08/05/a-beginners-guide-to-optimizing-pandas-code-for-speed.html

DataFrame 반복 최적화 (2)

www.it-swarm.dev/ko/python/pandas-iterrows%EC%97%90-%EC%84%B1%EB%8A%A5-%EB%AC%B8%EC%A0%9C%EA%B0%80-%EC%9E%88%EC%8A%B5%EB%8B%88%EA%B9%8C/1048592965/

DataFrame append, loc, iloc, 생성 속도 비교

stackoverflow.com/questions/10715965/add-one-row-to-pandas-dataframe/10716007#comment101375152_47979665

Pandas 최적화에 대한 이해

https://towardsdatascience.com/understanding-the-need-for-optimization-when-using-pandas-8ce23b83330c

Pandas str document

pandas.pydata.org/pandas-docs/stable/user_guide/text.html

Comments