30만 개 정도의 데이터를 Dataframe을 이용해 append 연산을 하는 작업을 수행하다 비정상적으로 느리게 동작하는 것을 발견했다. 아무리 느린 파이썬이라 해도 이 정도로 삽입 연산이 느린 것은 잘 와닿지 않아 실험을 해보았다.
아래와 같이 7가지 테스트를 진행하였다.
- pandas append
- pandas concat
- pandas loc
- dict in list
- dict list comprehension
- list append
- list comprehension
1, 2, 3 번은 각각 Dataframe 첫 행 생성 후 append, concat, loc를 이용하여 데이터를 삽입한다.
4번은 dictionary를 list에 append하며 생성하고 Dataframe으로 변환시켰고, 5번은 dictionary와 list를 comprehension을 이용하여 생성한 뒤 Dataframe으로 변환시켰다.
6번은 list를 appned한 뒤 Dataframe으로 변환시켰고 7번은 6번의 과정을 list comprehension을 이용해 해결하고 Dataframe으로 변환시켰다.
아래는 python 소스 코드이다. 코드는 단순하다. 테스트는 루프를 5,000번, 50,000번 총 2회 수행하였다.
import numpy as np
import pandas as pd
import time
from functools import wraps
numOfRows = 5000 # number of rows
def check_time(function):
@wraps(function)
def measure(*args, **kwargs):
stime = time.time()
result = function(*args, **kwargs)
print(f"Run Time: {function.__name__} took {time.time() - stime} s")
return result
return measure
@check_time
def pandas_append():
df = pd.DataFrame([[1, 2, 3, 4, 5]], columns=['A', 'B', 'C', 'D', 'E'])
for i in range(numOfRows - 1):
df = df.append(dict((a, np.random.randint(100)) for a in ['A', 'B', 'C', 'D', 'E']),
ignore_index=True)
@check_time
def pandas_concat():
df = pd.DataFrame([[1, 2, 3, 4, 5]], columns=['A', 'B', 'C', 'D', 'E'])
for i in range(numOfRows - 1):
temp = pd.DataFrame(dict((a, [np.random.randint(100)]) for a in ['A', 'B', 'C', 'D', 'E']))
df = pd.concat([df, temp], axis=0)
@check_time
def pandas_loc():
df = pd.DataFrame([[1, 2, 3, 4, 5]], columns=['A', 'B', 'C', 'D', 'E'])
for i in range(5, numOfRows):
df.loc[i] = np.random.randint(100, size=(1, 5))[0]
@check_time
def dict_in_list():
data = [{"A": 1, "B": 2, "C": 3, "D": 4, "E": 5}]
for i in range(numOfRows-1):
data.append(dict((a, np.random.randint(100)) for a in ['A', 'B', 'C', 'D', 'E']))
df = pd.DataFrame.from_dict(data)
@check_time
def dict_list_comprehension():
data = [{(a, np.random.randint(100)) for a in ['A', 'B', 'C', 'D', 'E']} for i in range(numOfRows)]
df = pd.DataFrame.from_dict(data)
@check_time
def list_append():
data = [[1, 2, 3, 4, 5]]
for i in range(numOfRows-1):
data.append([np.random.randint(100) for a in range(5)])
df = pd.DataFrame.from_dict(data)
@check_time
def list_comprehension():
data = [[np.random.randint(100) for a in range(5)] for i in range(numOfRows)]
df = pd.DataFrame.from_dict(data)
pandas_append()
pandas_concat()
pandas_loc()
dict_in_list()
dict_list_comprehension()
list_append()
list_comprehension()
아래는 5,000번 루프를 수행했을 때이다.
pandas append | 4.008279323577881 s |
pandas concat | 2.508300542831421 s |
pandas loc | 4.326159954071045 s |
dict in list | 0.067790985107421 s |
dict list comprehension | 0.063829183578491 s |
list append | 0.060836315155029 s |
list comprehension | 0.059865474700927 s |
아래는 50,000번 루프를 수행했을 때이다.
pandas append | 47.501329898834 s |
pandas concat | 34.139692783355 s |
pandas loc | 64.686416625976 s |
dict in list | 0.6612308025360 s |
dict list comprehension | 0.6831901073455 s |
list append | 0.6123473644256 s |
list comprehension | 0.5934088230133 s |
확실히 list를 이용하여 data를 만든 후 Dataframe으로 만드는 것이 더 빠르다. 4, 5, 6, 7번은 큰 속도 차이가 나지는 않지만 list comprehension을 이용하는 것이 가장 빠르게 나온다. 파이썬의 list는 자료구조에서 말하는 리스트이므로 데이터를 가장 마지막에 삽입하게 되면 append는 \(O(1)\)의 시간복잡도를 가진다.
아마 데이터 프레임은 삽입시 컬럼명을 검사하는 등의 여러 가지 삽입 조건들이 있어 시간이 더 오래 걸리는 것으로 생각된다. 자세한 Dataframe의 구조들은 라이브러리 안에 들어가서 찾아보면 된다. 실제로 들어가 보면 여러 가지 옵션들에 대한 적용과 조건 검사 코드가 들어있다.
Dataframe은 여러가지 편리한 기능들이 많지만 행이나 열을 지속적으로 추가해야 하는 작업이 있거나, 해당 연산이 잦은 경우 list나 dictiondary를 이용하여 데이터를 만든 후 나중에 Dataframe 자료형으로 치환하는 것이 속도면에서 더 이점이 있을 것으로 생각된다.
소스 코드
https://github.com/sehoon787/blog/blob/main/Languages/Python/list_dataframe_test.py