본 글은 Cluade 4 Sonnet으로 작성 후 수정했습니다.
데이터 처리 작업에서 Pandas는 오랫동안 Python 개발자들의 필수 도구였다. 하지만 데이터 규모가 커지고 성능에 대한 요구가 높아지면서 새로운 대안이 필요해졌다. 바로 Polars다.
Polars의 핵심 장점
테스트에 사용한 하드웨어: Ryzen5, 32G RAM
1. 성능 최적화
Polars는 Rust로 구현되어 C/C++ 수준의 성능을 제공한다. 가장 큰 차이점은 멀티스레드 병렬 처리와 벡터화 연산이다.
import pandas as pd
import polars as pl
import time
# 대용량 데이터셋 생성 (1천만 행)
df_pandas = pd.DataFrame({
'A': range(10_000_000),
'B': range(10_000_000, 20_000_000),
'C': ['group_' + str(i % 1000) for i in range(10_000_000)]
})
df_polars = pl.DataFrame({
'A': range(10_000_000),
'B': range(10_000_000, 20_000_000),
'C': ['group_' + str(i % 1000) for i in range(10_000_000)]
})
# Pandas 성능 측정
start = time.time()
result_pandas = df_pandas.groupby('C').agg({'A': 'mean', 'B': 'sum'})
pandas_time = time.time() - start
# Polars 성능 측정
start = time.time()
result_polars = df_polars.group_by('C').agg([
pl.col('A').mean(),
pl.col('B').sum()
])
polars_time = time.time() - start
print(f"Pandas: {pandas_time:.2f}초")
print(f"Polars: {polars_time:.2f}초")
print(f"성능 향상: {pandas_time/polars_time:.1f}배")
- Pandas: 0.38초
- Polars: 0.10초
- 성능 향상: 3.7배
2. 메모리 효율성
Polars는 Apache Arrow의 컬럼 지향 저장 방식을 사용한다. 이는 메모리 사용량을 크게 줄이고 Zero-Copy 연산을 가능하게 한다.
# 메모리 사용량 비교
import psutil
import os
def get_memory_usage():
"""현재 프로세스의 메모리 사용량 반환 (MB)"""
process = psutil.Process(os.getpid())
return process.memory_info().rss / 1024 / 1024
# Pandas 메모리 사용량
memory_before = get_memory_usage()
df_pandas_large = pd.DataFrame({
'col1': range(5_000_000),
'col2': [f'string_{i}' for i in range(5_000_000)]
})
pandas_memory = get_memory_usage() - memory_before
# Polars 메모리 사용량
memory_before = get_memory_usage()
df_polars_large = pl.DataFrame({
'col1': range(5_000_000),
'col2': [f'string_{i}' for i in range(5_000_000)]
})
polars_memory = get_memory_usage() - memory_before
print(f"Pandas 메모리 사용량: {pandas_memory:.1f}MB")
print(f"Polars 메모리 사용량: {polars_memory:.1f}MB")
print(f"메모리 절약: {(1 - polars_memory/pandas_memory)*100:.1f}%")
- Pandas 메모리 사용량: 384.2MB
- Polars 메모리 사용량: 180.2MB
- 메모리 절약: 53.1%
3. 지연 실행(Lazy Execution)
Polars의 가장 강력한 기능 중 하나는 지연 실행이다. 연산을 즉시 수행하지 않고 쿼리 플랜을 구성한 후 최적화하여 실행한다.
# Lazy 연산 예제
df = pl.scan_csv("large_dataset.csv") # 파일을 즉시 로드하지 않음
# 연산 체인을 구성 (아직 실행되지 않음)
result = (
df
.filter(pl.col("age") > 25) # 나이 25 이상 필터링
.group_by("department") # 부서별 그룹화
.agg([
pl.col("salary").mean().alias("avg_salary"), # 평균 급여
pl.col("employee_id").count().alias("count") # 직원 수
])
.sort("avg_salary", descending=True) # 평균 급여 기준 정렬
)
# 이때까지 실제 연산은 수행되지 않았음
print("쿼리 플랜:")
print(result.explain())
# collect() 호출 시 최적화된 연산 실행
final_result = result.collect()
print(final_result)
4. 직관적인 API
Pandas 사용자라면 Polars API를 쉽게 익힐 수 있다. 체이닝 방식으로 가독성 높은 코드 작성이 가능하다.
Polars 기본 사용법
설치 및 임포트
# 설치
# pip install polars
import polars as pl
import numpy as np
DataFrame 생성
# 딕셔너리로 생성
df = pl.DataFrame({
"name": ["Alice", "Bob", "Charlie", "Diana"],
"age": [25, 30, 35, 28],
"salary": [50000, 60000, 75000, 55000],
"department": ["Engineering", "Marketing", "Engineering", "Sales"]
})
# 파일에서 읽기
df_csv = pl.read_csv("data.csv")
df_parquet = pl.read_parquet("data.parquet")
# Pandas DataFrame에서 변환
pandas_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
polars_df = pl.from_pandas(pandas_df)
기본 데이터 탐색
# 기본 정보 확인
print(df.shape) # (행수, 열수)
print(df.columns) # 컬럼명 리스트
print(df.dtypes) # 데이터 타입
print(df.head()) # 상위 5행
print(df.describe()) # 기본 통계
# 특정 컬럼 선택
names = df.select("name") # 단일 컬럼
subset = df.select(["name", "age"]) # 여러 컬럼
numeric_cols = df.select(pl.col(pl.Int64)) # 타입별 선택
필터링
# 조건부 필터링
young_employees = df.filter(pl.col("age") < 30)
# 복합 조건
high_earners = df.filter(
(pl.col("salary") > 55000) &
(pl.col("department") == "Engineering")
)
# 문자열 패턴 매칭
eng_dept = df.filter(pl.col("department").str.contains("Eng"))
# isin 조건
selected_names = df.filter(pl.col("name").is_in(["Alice", "Bob"]))
데이터 변환
# 새 컬럼 생성
df_with_bonus = df.with_columns([
(pl.col("salary") * 0.1).alias("bonus"), # 보너스 계산
(pl.col("age") // 10).alias("age_group"), # 연령대 그룹
pl.col("name").str.to_uppercase().alias("name_upper") # 대문자 변환
])
# 기존 컬럼 수정
df_modified = df.with_columns([
pl.col("salary").cast(pl.Float64), # 타입 변환
pl.col("age").fill_null(0) # null 값 채우기
])
그룹화 및 집계
# 부서별 집계
dept_stats = df.group_by("department").agg([
pl.col("salary").mean().alias("avg_salary"), # 평균 급여
pl.col("salary").max().alias("max_salary"), # 최대 급여
pl.col("age").min().alias("min_age"), # 최소 나이
pl.count().alias("employee_count") # 직원 수
])
# 복수 컬럼으로 그룹화
age_dept_stats = df.group_by(["department", "age_group"]).agg([
pl.col("salary").median().alias("median_salary"),
pl.col("name").count().alias("count")
])
정렬
# 단일 컬럼 정렬
sorted_by_salary = df.sort("salary", descending=True)
# 복수 컬럼 정렬
sorted_multiple = df.sort(["department", "salary"], descending=[False, True])
# null 값 처리 옵션
sorted_with_nulls = df.sort("salary", nulls_last=True)
조인
# 부서 정보 DataFrame
dept_info = pl.DataFrame({
"department": ["Engineering", "Marketing", "Sales"],
"budget": [1000000, 500000, 750000],
"location": ["Seoul", "Busan", "Daegu"]
})
# Inner Join
merged = df.join(dept_info, on="department", how="inner")
# Left Join (기본값)
left_merged = df.join(dept_info, on="department")
# 다른 컬럼명으로 조인
# df.join(other_df, left_on="col1", right_on="col2")
윈도우 함수
# 부서별 급여 순위
df_with_rank = df.with_columns([
pl.col("salary").rank("dense").over("department").alias("salary_rank")
])
# 이동 평균
df_with_ma = df.with_columns([
pl.col("salary").rolling_mean(window_size=2).alias("salary_ma")
])
# 누적 합계
df_with_cumsum = df.with_columns([
pl.col("salary").cumsum().alias("salary_cumsum")
])
파일 저장
# CSV 저장
df.write_csv("output.csv")
# Parquet 저장 (권장 - 압축률과 성능이 뛰어남)
df.write_parquet("output.parquet")
# JSON 저장
df.write_json("output.json")
# Pandas DataFrame으로 변환
pandas_result = df.to_pandas()
성능 최적화 팁
1. Lazy 연산 활용
# Eager 연산 (즉시 실행)
result_eager = (
pl.read_csv("large_file.csv")
.filter(pl.col("status") == "active")
.group_by("category")
.agg(pl.col("value").sum())
)
# Lazy 연산 (지연 실행) - 권장
result_lazy = (
pl.scan_csv("large_file.csv") # scan_ 사용
.filter(pl.col("status") == "active")
.group_by("category")
.agg(pl.col("value").sum())
.collect() # 마지막에 collect()
)
2. 적절한 데이터 타입 사용
# 메모리 최적화를 위한 타입 지정
optimized_df = pl.DataFrame({
"id": pl.Series([1, 2, 3], dtype=pl.UInt32), # 작은 정수형
"category": pl.Series(["A", "B", "C"], dtype=pl.Categorical), # 카테고리형
"value": pl.Series([1.1, 2.2, 3.3], dtype=pl.Float32) # 단정도 실수
})
3. 조건부 연산 최적화
# when().then().otherwise() 사용
df_with_grade = df.with_columns([
pl.when(pl.col("salary") > 70000)
.then(pl.lit("Senior"))
.when(pl.col("salary") > 55000)
.then(pl.lit("Mid"))
.otherwise(pl.lit("Junior"))
.alias("grade")
])
마무리
Polars는 현대적인 데이터 처리 환경에서 Pandas의 한계를 뛰어넘는 강력한 도구다. 특히 대용량 데이터 처리, 성능이 중요한 ETL 파이프라인, 그리고 메모리 효율성이 필요한 환경에서 그 진가를 발휘한다.
Pandas에 익숙한 개발자라면 비교적 쉽게 Polars로 전환할 수 있으며, 지연 실행과 병렬 처리의 이점을 누릴 수 있다. 다음 프로젝트에서는 Polars를 고려해보는 것이 어떨까?