티스토리 뷰
안녕하세요. Teus입니다,
오랜만에 인사드립니다. 이번 포스팅에서 살펴볼 내용은
Pandas이후에 Python에서 Data처리용으로 곽광받고 있는 패키지인 Polars에 대해서 다룹니다.
1. Polars
Polars를 Pandas와 비슷하게 2차원 형태의 Sheet형 데이터 처리에 특화된 라이브러리 입니다.
특징으로는
- Pola-rs. Rust기반으로 작성되어, 빠른 속도로 동작을 보장해 줍니다.
- SIMD를 지원하여 Vectorized 동작을 통해서 빠른 데이터 처리 보장.
- Multicore를 전부 활용할 수 있는 Backend
- Lazy Execution을 통한 최적화된 IO
- 실험적인 모드로 GPU Support(beta)
등의 특징을 볼 수 있습니다.
2. 분산처리 라이브러리와 비교.
pola-rs이전에도 Pandas의 한계를 극복하기 위한 다수의 라이브러리가 있었습니다.
분산처리(클러스터 환경) : PySpark ,Dask
분산처리2 : Modin, Ray
딥러닝을 위한 GPU처리 : Tensorflow. Pytorch
하지만 위와같은 라이브러리들의 단점은, 초기 실행을 위해서는 overload가 동반됩니다.
PySpark를 예로 들면
- 분산처리를 관리하기위한 매니저 생성
- 매니저를 통해서 데이터를 분산 위치
- 분산된 데이터를 한곳으로 모음
의 과정 때문에 PySpark를 사용한 분산처리 속도가 일반적인 Pandas를 사용하는것 대비 느린 경우가 다수 존재하게 됩니다.
PySpark뿐만 아니라 다른 패키지들 역시 굉장히 무거운 패키지에 속합니다.
3. 그러면 Pola-rs는?
Numpy의 경우 Python에서 사용할 수 있는 C언어로 작성된 Low Level 패키지 입니다.
그래서 Python패키지임에도 불구하고 C언어에 근접하는 속도로 동작합니다.
numpy의 언어구성
마찬가지로 Pola-rs의 경우 Pandas와 유사한 환경을 Rust를 사용해서 구현 해 놓은 패키지 입니다.
그리고 Rust를 사용해서 GIL에서 해방된 처리환경을 보장합니다.
pola-rs의 언어구성
4. 간단한 찍먹
Polars의 경우 Pandas를 개선하기 위해서 나온 만큼
Pandas와는 비슷(?)한 방식의 사용 방법을 제공하려 했으나....
실제 활용방법은 PySpark의 spark.sql.DataFrame과 비슷한 사용자 경험을 제공합니다.
github에서 제공되는 기본적은 예시를 보겠습니다.
1. I/O
import polars as pl
>>> df = pl.DataFrame(
{
"A": [1, 2, 3, 4, 5],
"fruits": ["banana", "banana", "apple", "apple", "banana"],
"B": [5, 4, 3, 2, 1],
"cars": ["beetle", "audi", "beetle", "beetle", "beetle"],
}
)
기본적으로 DataFrame을 만들 때는 Pandas와 동일하게 Dict를 제공하거나
단순하게 pl.read_csv()를 사용해서 일반 Pandas를 사용하던 패턴을 사용할 수 있습니다.
pl.read_csv("data.csv", separator="|")
pl.read_excel(
source="test.xlsx",
sheet_name="data",
)
이 외의 특징으로, scan 접미사가 붙은 read방법을 통해서 lazy execution이 가능하게 해줍니다.
(
pl.scan_csv("my_long_file.csv") # 여기서 실제로 데이터를 읽지는 않음
.select(
["a", "c"]
) # 전체 데이터 중 오직 두개의 column만 읽어옴
.filter(
pl.col("a") > 10
) # 여기까지 주어진 데이터 조건이 만들어지고,
#해당하는 조건에 만족하는 데이터만 최종적으로 읽어들여서 I/O동작을 최소화
.head(100) # constrain number of returned results to 100
)
뿐만아니라 기존에 만들어진 DataFrame을 Lazy상태로 변경하고, 변경 후 해당하는 DataFrame을 Lazy Execution을 통해서 효율적은 처리를 가능하게 해줍니다.
df = pl.DataFrame(
{
"a": [None, 2, 3, 4],
"b": [0.5, None, 2.5, 13],
"c": [True, True, False, None],
}
)
#기존에 만들어진 DataFrame을 lazy frame으로 만들어줌
df.lazy()
2. Data Operation
Data처리의 경우 기존 Pandas에 사용했던것과 유사하게
apply, filter, sort 등을 지원 합니다.
이때 Polars를 불변성을 유지해야하는 라이브러리 이기 때문에
기존의 DataFrame의 값을 변경하는 것이 아니라,
변경된 값을 가지고있는 새로운 DataFrame을 생성하는 방식으로 사용자 경험을 가져갑니다.
import polars as pl
df = pl.DataFrame(
{
"A": [1, 2, 3, 4, 5],
"fruits": ["banana", "banana", "apple", "apple", "banana"],
"B": [5, 4, 3, 2, 1],
"cars": ["beetle", "audi", "beetle", "beetle", "beetle"],
}
)
# embarrassingly parallel execution & very expressive query language
# furits col 기준으로 정렬하고, 정렬된 table에서 fruits, cars col을 가지고온다.
df.sort("fruits").select(
"fruits",
"cars",
#새로운 col을 추가하는데
#1. A열의 값 중 B열의 값이 2보다 큰 경우에 대해서는 각 cars col별로 합을 구하고
#이 col의 이름은 sum_A_by_cars로 지정
pl.col("A").filter(pl.col("B") > 2).sum().over("cars").alias("sum_A_by_cars"),
#2. fruits마다 grouping 이후 A의 합을 구하고, 해당 col을 sum_A_by_fruits라고 명명
pl.col("A").sum().over("fruits").alias("sum_A_by_fruits"),
)
이런 method chain을 사용하는 방법 이외에
LazyFrame에 직접 SQL을 사용해서 조회하는 동작 역시 가능합니다.
df = pl.scan_csv("docs/assets/data/iris.csv")
# run SQL queries on frame-level
df.sql("""
SELECT species,
AVG(sepal_length) AS avg_sepal_length
FROM self
GROUP BY species
""").collect()
5. Pandas vs Pola-rs퍼포먼스 비교
역시 요즘 AI가 빠질 수 없죠?
AI의 도움을 통해서 테스트 코드를 생성합니다.
import polars as pl
import pandas as pd
import numpy as np
import time
# 데이터 생성
size = 50_000_000
data = {
'id': np.arange(size),
'value': np.random.randn(size),
'category': np.random.choice(['A', 'B', 'C'], size)
}
# Pandas DataFrame 생성
df_pandas = pd.DataFrame(data)
# Polars DataFrame 생성
df_polars = pl.DataFrame(data)
# 테스트 함수 정의
def benchmark_pandas():
start_time = time.time()
result = df_pandas[df_pandas['value'] > 0]\
.groupby('category')\
.agg({'value': 'sum'})
end_time = time.time()
print(f"Pandas: {end_time - start_time:.6f} seconds")
return result
def benchmark_polars():
start_time = time.time()
result = df_polars\
.filter(pl.col('value') > 0)\
.group_by('category')\
.agg([pl.sum('value').alias('value')])
end_time = time.time()
print(f"Polars: {end_time - start_time:.6f} seconds")
return result
# 벤치마크 실행
pandas_result = benchmark_pandas()
polars_result = benchmark_polars()
# 결과 비교
print("Pandas Result:");print(pandas_result)
print("Polars Result:");print(polars_result)
위 코드 같은 경우 아래와 같은 Dataframe을 만들고
컬럼명의미
id | 0~49999999까지의 숫자 |
value | 정규분포로 만들어진 난수 |
category | A,B,C중 하나의 값을 갖는 범주형 변수 |
여기서
- value가 0보다 큰 경우에 대해서만 데이터를 추출
- category별로 데이터를 grouping
- grouping 한 뒤 데이터에 대해서 합을 취함
간단하지만 Pandas를 사용할 때 사용하는 Boolean indexing, groupby, aggfunc를 사용한 예시를 줬네요.
이렇게 구 코드의 퍼포먼스를 비교할 경우
DataFrame이 만들어지는 시간을 포함하지 않으면, 데이터 처리 자체에서 큰 속도차이가 있는것을 확인할 수 있습니다.
하지만 python dict를 활용해서 DataFrame을 만들 경우, pola-rs의 속도가 느린것을 확인할 수가 있습니다.
# 테스트 함수 정의
def benchmark_pandas():
start_time = time.time()
# Pandas DataFrame 생성
df_pandas = pd.DataFrame(data)
result = df_pandas[df_pandas['value'] > 0].groupby('category').agg({'value': 'sum'})
end_time = time.time()
print(f"Pandas: {end_time - start_time:.6f} seconds")
return result
def benchmark_polars():
start_time = time.time()
# Polars DataFrame 생성
df_polars = pl.DataFrame(data)
result = df_polars.filter(pl.col('value') > 0).group_by('category').agg([pl.sum('value').alias('value')])
end_time = time.time()
print(f"Polars: {end_time - start_time:.6f} seconds")
return result
하지만 이는 python dict -> pola-rs dataframe변환에 생기는 병목으로 보입니다.
실제로 큰 사이즈의 csv파일을 만들어 읽기시간을 비교해보면
Polars가 압도적으로 빠른 속도로 동작하는것을 확인할 수 있습니다.
(위 벤치마크에서 사용된 데이터의 Row 3000만건을 저장한 csv파일)
import polars as pl
import pandas as pd
import numpy as np
import time
# 테스트 함수 정의
def benchmark_pandas():
start_time = time.time()
pd.read_csv("testdt.csv")
end_time = time.time()
print(f"Pandas: {end_time - start_time:.6f} seconds")
def benchmark_polars():
start_time = time.time()
pl.read_csv("testdt.csv")
end_time = time.time()
print(f"Polars: {end_time - start_time:.6f} seconds")
# 벤치마크 실행
pandas_result = benchmark_pandas()
polars_result = benchmark_polars()
이제 여기에 lazy를 사용한 경우와, lazy를 사용하지 않고 실행한 경우를 단독으로 비교하면
Polars를 사용했을 때의 최고 효율을 가져갈 수가 있습니다.
(Lazy의 특성상 모든 데이터를 읽지 않고, 마지막 처리 flow만큼만 읽기 때문에 I/O에 걸리는 시간을 최소화 할 수 있습니다)
import polars as pl
import time
# 테스트 함수 정의
def benchmark_polars_lazy():
start_time = time.time()
df_polars_lazy = pl.scan_csv("testdt.csv")
result = (
df_polars_lazy\
.filter(pl.col('value') > 0)\
.group_by('category')\
.agg([pl.sum('value').alias('value')])
).collect()
end_time = time.time()
print(f"Polars Lazy: {end_time - start_time:.6f} seconds")
return result
def benchmark_polars():
start_time = time.time()
df_polars = pl.read_csv("testdt.csv")
result = df_polars\
.filter(pl.col('value') > 0)\
.group_by('category')\
.agg([pl.sum('value').alias('value')])
end_time = time.time()
print(f"Polars: {end_time - start_time:.6f} seconds")
return result
# 벤치마크 실행
pandas_result = benchmark_polars_lazy()
polars_result = benchmark_polars()
# 결과 비교
print("Pandas Result:");print(pandas_result)
print("Polars Result:");print(polars_result)
6. 이모저모
Polars의 경우 느린 Python의 환경을 극복하고, Pandas와 유사한 편한 인터페이스를 제공하는 장점이 있습니다.
뿐만 아니라 분산처리를 위한 클러스터 필요 없이 client computer level에서 유의미한 성능개선을 할 수 있다는 점이 큽니다.
pandas만큼 초심자에게 편한 인터페이스는 아니지만
데이터 분석가 이면서 데이터 전처리에 너무 많은 시간이 걸려서
데이터 처리에 어려움을 겪고있던 분들이라면, 충분히 좋은 선택지로 고려해 볼만 합니다.
편리한 것과 빠른것이 공존할 수 없다는 저의 관념을 때려주는 아주 유용한 패키지 같습니다.
'Python 잡지식' 카테고리의 다른 글
[Python]CPython with JIT(3.13버전 실험기능) (0) | 2024.11.23 |
---|---|
[Python]Free-threaded CPython(3.13버전 릴리즈) (0) | 2024.11.13 |
[Python]PEP703. CPython의 GIL을 선택사항으로 관련해서 (0) | 2024.11.13 |
[Python]MultiProcessing 탐구 (0) | 2024.08.21 |
[Python]Python의 asyncio 탐구 (0) | 2024.07.10 |
- Total
- Today
- Yesterday
- 동적계획법
- javascript
- 자료구조
- SIMD
- 모바일청첩장
- Search알고리즘
- GDC
- 알고리즘
- 셀프모청
- stack
- 병렬처리
- 분할정복
- 프로그래머스
- react
- Greedy알고리즘
- Sort알고리즘
- 완전탐색 알고리즘
- 코딩테스트
- AVX
- prime number
- heap
- 사칙연산
- git
- 청첩장
- 컴퓨터그래픽스
- C++
- Python
- 이분탐색
- hash
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |