티스토리 뷰
사내에서 데이터 처리를 위해 pandas로 데이터 계산 및 csv 저장 등을 처리하고 있다.
pandas가 이미 많이 발전된 라이브러리이지만, 기존 레거시 솔루션을 신규 프로젝트로 컨버팅하는 과정에서 병렬 처리, 비동기 요청에 대해 미지원 등의 이슈가 있어, 핵심적인 계산은 기존과 동일한 상태였다.
(물론 컨버팅 과정에서 불필요한 로직 삭제, 불필요한 데이터 제거 등으로 최적화를 꽤 많이 시켰지만 아쉬움이 있는 상태였다.)
최근 kafka를 공부하다가 우연히 polars라는 pandas 대체 라이브러리를 알게 되어 간단히 테스트 결과를 정리하고자 한다.
(polars 외에도 dask, modin 등의 pandas 대체 라이브러리 프로젝트가 많은 것도 알게 되었다. 하지만 기존 솔루션의 상황에 가장 적합한 것이 polars라 판단되었다.)
polars에 대한 자세한 내용은 우아한형제들 기술 블로그에 좋은 설명 및 실제 적용 사례를 소개한 것이 있어 아래 내용을 참고하는 것이 좋다.
(나는 단순 테스트만.....)
https://techblog.woowahan.com/18632/
Polars로 데이터 처리를 더 빠르고 가볍게 with 실무 적용기 | 우아한형제들 기술블로그
배달시간예측서비스팀은 배달의민족 앱 내의 각종 서비스(배민배달, 비마트, 배민스토어 등)에서 볼 수 있는 배달 예상 시간과 주문 후 고객에게 전달되기까지의 시간을 데이터와 AI를 활용하여
techblog.woowahan.com
테스트는 아래 환경에서 진행된다.
python 3.11
pandas 2.2.3
polars 1.21.0
numpy 2.2.2
cpu Intel i5 2.7GHz 듀얼 코어
ram ddr3 8gb
테스트 내용은 다음과 같다.
1. 100 x 1,000,000 table 2개 생성 (랜덤 값 부여) (칼럼 100개, row 100만개)
2. 테이블의 특정 칼럼을 기준으로 최대값 row 추출
3. 2개의 100 x 1,000,000 table을 inner join
1. 100 x 1,000,000 table 2개 생성 (랜덤 값 부여)
# pandas vs polars
import polars as pl
import pandas as pd
import numpy as np
import time
import statistics
import gc
# 더미 데이터 생성
np.random.seed(42)
data_a = {f'col_{i}': np.random.random(1000000) for i in range(100)}
data_b = {f'col_{i}': np.random.random(1000000) for i in range(50, 150)}
def creation_check(func, iter=5) :
times = []
for _ in range(iter) :
gc.collect() # garbage collector 실행하여 메모리 초기화
start = time.time()
func()
end = time.time()
times.append(end - start)
return statistics.mean(times)
# pandas
pandas_chk = creation_check(lambda: (
pd.DataFrame(data_a), pd.DataFrame(data_b)
))
print(f'pandas - creation : {pandas_chk:.3f} seconds')
time.sleep(1)
# polars
polars_chk = creation_check(lambda: (
pl.DataFrame(data_a), pl.DataFrame(data_b)
))
print(f'polars - creation : {polars_chk:.3f} seconds')
100 x 1,000,000 개의 데이터를 미리 생성 후, pandas와 polars를 이용해 각각 dataframe을 만드는데 걸리는 시간을 측정하였다. (5회 반복 후 평균)
단순 생성하는데만 무려 671배나 차이가 발생한다.
꽤 큰 프레임이지만, 100만개 정도는 상용에서 그렇게 크지 않은 값이라 볼 수 있는데, 단순 dataframe 생성에만 pandas가 1초 이상 걸린다는 것이 놀랍다. 로직 상 프레임 생성이 10회 이상 사용된다면 10초 이상 딜레이가 생성하는데에서만 소요가 된다.
2. 테이블의 특정 칼럼을 기준으로 최대값 row 추출
# pandas vs polars
import polars as pl
import pandas as pd
import numpy as np
import time
import statistics
import gc
# 더미 데이터 생성
np.random.seed(42)
data = {f'col_{i}': np.random.random(1000000) for i in range(100)}
def max_row_check(df, func, iter=5) :
columns = ['col_10', 'col_30', 'col_50', 'col_70', 'col_90']
times = []
for col in columns :
gc.collect() # garbage collector 실행하여 메모리 초기화
start = time.time()
func(df, col)
end = time.time()
times.append(end - start)
return statistics.mean(times)
def pandas_max_row(df, col) :
max_row = df.loc[df[col].idxmax()]
return max_row
def polars_max_row(df, col) :
max_row = df[df[col].arg_max()]
return max_row
# pandas
pandas_df = pd.DataFrame(data)
pandas_chk = max_row_check(pandas_df, lambda df, col : pandas_max_row(df, col))
print(f'pandas - max row : {pandas_chk:.5f} seconds')
time.sleep(1)
# polars
polars_df = pl.DataFrame(data)
polars_chk = max_row_check(polars_df, lambda df, col : polars_max_row(df, col))
print(f'polars - max row : {polars_chk:.5f} seconds')
100만개 데이터 중 최대값을 계산하는 것은 pandas와 polars 모두 빠르게 이루어지지만, polars가 약 3배 정도 더 빠름을 알 수 있다. 단순히 몇 퍼센트 우세가 아닌, 단순 연산에서도 몇 배씩 차이가 나는 것은 분명 polars로 변경해야 할 이유가 더욱 타당해보인다.
3. 2개의 100 x 1,000,000 table을 inner join
# pandas vs polars
import polars as pl
import pandas as pd
import numpy as np
import time
import statistics
import gc
# 더미 데이터 생성
np.random.seed(42)
data_a = {f'col_{i}': np.random.random(1000000) for i in range(100)}
data_b = {f'col_{i}': np.random.random(1000000) for i in range(50, 150)}
def join_check(df_a, df_b, func) :
columns = ['col_50', 'col_60', 'col_70', 'col_80', 'col_90']
times = []
for col in columns :
gc.collect() # garbage collector 실행하여 메모리 초기화
start = time.time()
func(df_a, df_b, col)
end = time.time()
times.append(end - start)
return statistics.mean(times)
def pandas_join(df_a, df_b, col) :
return pd.merge(df_a, df_b, on=col, how='inner')
def polars_join(df_a, df_b, col) :
return df_a.join(df_b, on=col, how='inner')
# pandas
pandas_df_a = pd.DataFrame(data_a)
pandas_df_b = pd.DataFrame(data_b)
pandas_chk = join_check(pandas_df_a, pandas_df_b, lambda df_a, df_b, col : pandas_join(df_a, df_b, col))
print(f'pandas - join tables : {pandas_chk:.5f} seconds')
time.sleep(1)
# polars
polars_df_a = pl.DataFrame(data_a)
polars_df_b = pl.DataFrame(data_b)
polars_chk = join_check(polars_df_a, polars_df_b, lambda df_a, df_b, col : polars_join(df_a, df_b, col))
print(f'polars - join tables : {polars_chk:.5f} seconds')
polars에는 merge는 따로 없고 (merge_sorted는 존재.) join이 pandas의 merge와 동일한 역할을 수행한다.
특정 칼럼을 기준으로 inner join을 테스트한 결과, polars가 약 7배 가량 빠름을 알 수 있다. 단순 연산 외에 join과 같이 heavy한 작업에서도 몇 배씩 차이가 남을 알 수 있다.
결론 : pandas는 python으로 작성된 라이브러리이다 보니 python이 가진 한계(느린 연산, 병렬 처리 부족 등)를 안고 갈 수 밖에 없다.
polars는 rust로 작성되어 공식 홈페이지에서도 python, rust, js 가이드를 제공하고 있으며, 실제 테스트에서도 dataframe 생성은 몇 백 배, dataframe 내부 연산도 몇 배씩 차이가 남을 알 수 있다.
장점 : pandas로 된 걸 polars로 변경 시, 엄청난 속도 향상을 얻을 수 있다. 개발 중인 솔루션에는 무조건 바꿔야 할 사항으로 판단된다. (대용량 데이터 처리 시, 기존 배치 작업 안에 못 끝나는 경우도 다수 발생하기 때문)
단점 : 테스트 과정에서 아직 polars에 익숙하지 않아 docs를 계속 찾아봐야 했으며, 개념적으로 pandas와 상충하는 것들, rust로 내부가 동작하다 보니 에러 메시지에도 rust 관련 키워드 (dyn 등) 가 등장해 에러 해결에 pandas보다 상대적으로 어려움을 겪었다.
(사내에서 변경 작업을 한다면 시간이 좀 소요될 듯 하다)
'메모' 카테고리의 다른 글
cent os 7 eos에 따른 repo 이슈 해결 (0) | 2025.02.23 |
---|---|
Modular Mojo / MAX (0) | 2024.09.29 |
linux golang 설치 (1) | 2024.01.17 |
git 간단 정리 (0) | 2024.01.11 |
PS Rust 입출력 (0) | 2023.01.09 |