요약
Pandas는 Python 데이터 분석의 표준이었지만 수 GB를 넘어가면 한계가 명확하다. 모든 연산이 단일 스레드로 동작하고, 중간 결과를 매번 메모리에 구체화하며, Python 객체 오버헤드 때문에 실제 연산보다 메모리 관리에 시간이 더 걸린다.
Polars는 이 문제를 Rust로 구현한 DataFrame 엔진으로 해결한다. Apache Arrow 컬럼형 메모리 포맷을 기반으로, 쿼리를 먼저 분석해 불필요한 연산을 제거하고(Lazy 실행), 메모리에 다 올리지 못하는 데이터도 청크 단위로 처리한다(스트리밍 모드). 2024년 7월 v1.0 GA 이후 데이터 플랫폼 엔지니어링 현장에서 Pandas 대체재로 빠르게 자리 잡고 있다.
핵심 요약:
- Lazy API: 연산 그래프를 명시적으로 구성하고
.collect()로 한 번에 실행. 옵티마이저가 predicate pushdown·projection pushdown·CSE(공통 부분식 제거)를 자동 적용. - 스트리밍 모드:
.collect(streaming=True). 전체 데이터를 메모리에 올리지 않고 배치 청크 단위로 처리. RAM < 데이터 크기 상황 대응. - Arrow 네이티브: Pandas 변환 없이 DuckDB, PyArrow, PyIceberg, Spark와 제로 복사 통합.
- Pandas 대비 5~30× 속도 향상 (연산 유형·데이터 크기에 따라 다름).
배경: Pandas의 구조적 병목
왜 Pandas가 느린가
Pandas의 성능 제약은 세 가지 구조에서 온다.
- 단일 스레드 실행: Python GIL과 Pandas 내부 구조 때문에 대부분의 연산이 단일 CPU 코어를 쓴다. 48코어 서버에서도
groupby·merge는 코어 하나가 일한다.
- 중간 결과 구체화:
df.query(...).groupby(...).agg(...)같은 체인은 각 단계마다 새 DataFrame을 메모리에 만든다. 필요 없는 열도 모두 포함된다.
- 행 기반 메모리 레이아웃: Pandas의 내부 NumPy 배열은 열 단위로 저장되지 않는 경우가 많고,
objectdtype이 Python 객체 포인터 배열이라 CPU 캐시 미스가 많다.
컬럼형 포맷의 이점
Apache Arrow는 동일한 열의 값을 연속 메모리에 저장한다. SELECT price WHERE region = 'KR' 같은 연산은 price 열과 region 열 두 개만 읽으면 된다. SIMD 벡터화 명령어로 필터를 한 번에 처리할 수 있고, CPU 프리페처가 효율적으로 동작한다.
Polars 아키텍처
Lazy API와 Eager API
Polars는 두 실행 모드를 제공한다.
Eager API: 즉시 실행. 디버깅·탐색에 유용하다.
import polars as pl
df = pl.read_parquet("sales.parquet")
result = df.filter(pl.col("region") == "KR").groupby("product").agg(
pl.col("revenue").sum()
)Lazy API: 연산 그래프를 쌓고 .collect()에서 한 번에 실행한다.
result = (
pl.scan_parquet("sales.parquet") # LazyFrame 반환
.filter(pl.col("region") == "KR")
.groupby("product")
.agg(pl.col("revenue").sum())
.sort("revenue", descending=True)
.limit(10)
.collect() # 여기서 실행
)Lazy API의 중요한 점은 .collect() 전까지 어떤 데이터도 읽히지 않는다는 것이다. 옵티마이저가 다음을 수행한다.
- Predicate Pushdown:
filter(region == "KR")조건을 Parquet 파일 읽기 시점으로 밀어 내린다. Parquet row group 필터링으로 I/O 자체를 줄인다. - Projection Pushdown:
revenue,region,product만 읽는다. 불필요한 열은 파일에서부터 제외한다. - 공통 부분식 제거(CSE): 동일한 중간 결과를 두 번 계산하지 않는다.
Expression API
Polars의 표현식(expression) 시스템은 열 연산을 조합 가능한 함수로 추상화한다. Pandas의 apply(lambda ...)와 달리 SIMD 벡터화로 실행된다.
# 여러 집계를 한 번에
result = df.groupby("category").agg([
pl.col("price").mean().alias("avg_price"),
pl.col("price").std().alias("std_price"),
pl.col("qty").sum().alias("total_qty"),
(pl.col("price") * pl.col("qty")).sum().alias("total_revenue"),
])
# 윈도우 함수 (그룹 내 순위)
df_with_rank = df.with_columns(
pl.col("revenue")
.rank("dense", descending=True)
.over("region") # 그룹 내 적용
.alias("rank_in_region")
)
# 문자열 연산
df_clean = df.with_columns(
pl.col("name").str.strip_chars().str.to_lowercase(),
pl.col("date").str.to_date("%Y-%m-%d"),
)over() 함수는 SQL의 PARTITION BY와 동일한 의미다. Pandas에서 transform으로 복잡하게 처리해야 했던 것을 한 줄로 표현한다.
스트리밍 모드
메모리보다 큰 데이터를 처리할 때 .collect(streaming=True)를 사용한다.
# 500GB 로그 파일을 8GB RAM 서버에서 처리
result = (
pl.scan_parquet("logs/year=2026/**/*.parquet") # glob 패턴
.filter(pl.col("level") == "ERROR")
.groupby("service")
.agg(pl.col("latency_ms").mean())
.collect(streaming=True) # 배치 청크 단위 처리
)스트리밍 모드에서 Polars는 데이터를 고정 크기 배치(기본 524,288 행)로 나눠 처리한다. groupby·join 같은 연산도 가능하지만 내부적으로 spill-to-disk 메커니즘을 사용한다.
현재 제약: v1.x 스트리밍은 일부 복잡한 연산(일부 윈도우 함수, 중첩된 join)에서 fallback을 사용하거나 지원하지 않을 수 있다. Polars는 .explain(streaming=True) 명령으로 어떤 연산이 스트리밍으로 처리되는지 미리 확인할 수 있다.
SQL 인터페이스
Polars 1.x는 polars.sql 모듈을 통해 표준 SQL을 직접 실행할 수 있다.
ctx = pl.SQLContext({"sales": df_sales, "products": df_products})
result = ctx.execute("""
SELECT p.name, SUM(s.revenue) AS total_revenue
FROM sales s
JOIN products p ON s.product_id = p.id
WHERE s.date >= '2026-01-01'
GROUP BY p.name
ORDER BY total_revenue DESC
LIMIT 20
""").collect()SQLContext는 Lazy 계획으로 컴파일되므로 SQL 실행도 옵티마이저의 혜택을 받는다.
데이터 플랫폼 통합
DuckDB와 협력
DuckDB와 Polars는 모두 Arrow 포맷을 사용하므로 제로 복사 교환이 가능하다.
import duckdb
# Polars DataFrame → DuckDB 뷰 (복사 없음)
duckdb.register("orders", df_polars.to_arrow())
result_duck = duckdb.sql("SELECT * FROM orders WHERE total > 1000").arrow()
# DuckDB 결과 → Polars (복사 없음)
df_result = pl.from_arrow(result_duck)일반적인 패턴: 파일 읽기와 초기 필터링은 Polars Lazy로, 복잡한 집계 SQL은 DuckDB로, 결과 후처리는 다시 Polars로.
PyIceberg (Apache Iceberg)
from pyiceberg.catalog import load_catalog
catalog = load_catalog("glue", **{"type": "glue"})
table = catalog.load_table("prod.orders")
# Polars Lazy로 Iceberg 테이블 스캔
df = pl.from_arrow(
table.scan()
.filter("region = 'KR'")
.select(["order_id", "revenue", "date"])
.to_arrow()
)Spark와의 관계
Spark와 Polars는 경쟁 관계이기도 하지만, 실무에서는 역할이 다르다.
- Polars: 단일 서버에서 수백 GB 이하 데이터. ETL 전처리, 피처 엔지니어링, 로컬 분석.
- Spark: 분산 클러스터. TB 이상 데이터, shuffle 필요한 대규모 집계.
Spark와 Polars는 Arrow IPC로 데이터를 교환한다. Spark의 toPandas() 대신 toArrow() → pl.from_arrow()를 사용하면 변환 비용을 낮출 수 있다.
Pandas에서 Polars로 마이그레이션
가장 흔한 패턴 변환:
| 작업 | Pandas | Polars |
|---|---|---|
| 파일 읽기 | pd.read_parquet(...) | pl.scan_parquet(...) (lazy) |
| 필터 | df[df.col > 0] | df.filter(pl.col("col") > 0) |
| 열 추가 | df["new"] = df["a"] + df["b"] | df.with_columns((pl.col("a") + pl.col("b")).alias("new")) |
| groupby 집계 | df.groupby("k")["v"].sum() | df.groupby("k").agg(pl.col("v").sum()) |
| apply (행별) | df["col"].apply(func) | df["col"].map_elements(func) (느림) 또는 표현식 조합 (빠름) |
| 날짜 필터 | df[df["date"] >= "2026-01-01"] | df.filter(pl.col("date") >= pl.lit("2026-01-01").str.to_date()) |
map_elements 주의: Polars의 map_elements(구 apply)는 Python 함수를 행별로 호출하므로 Pandas apply만큼 느리다. 가능하면 Polars 표현식 조합으로 대체해야 SIMD 가속의 혜택을 받는다.
운영 체크리스트
- [ ] 데이터 크기 확인: 단일 서버 RAM의 2배 이상이면 스트리밍 모드 또는 DuckDB를 검토
- [ ] Lazy 우선:
pl.read_*대신pl.scan_*+.collect()패턴을 기본으로 - [ ] 스키마 명시:
dtypes파라미터로 열 타입을 미리 지정해 infer 비용 제거 - [ ]
.explain()으로 계획 확인: 옵티마이저가 predicate pushdown을 적용했는지 확인 - [ ]
map_elements지양: 표현식 API로 대체 가능한지 검토 - [ ] Arrow 교환 활용: DuckDB·PyIceberg·Spark와 통합 시
to_arrow()/from_arrow()사용 - [ ] 스트리밍 호환 연산 확인:
.explain(streaming=True)→STREAMING표시된 연산만 보장 - [ ] 메모리 프로파일링:
pl.Config.set_streaming_chunk_size()조정으로 메모리 사용량 제어
References
- Polars 공식 사이트: https://pola.rs/
- Polars GitHub: https://github.com/pola-rs/polars
- Polars 1.0 GA 발표 (2024-07): https://pola.rs/posts/polars-1/
- Polars 공식 문서: https://docs.pola.rs/
- Apache Arrow 컬럼형 포맷 명세: https://arrow.apache.org/docs/format/Columnar.html
- DuckDB + Polars 통합 가이드: https://duckdb.org/docs/guides/python/polars.html
- PyIceberg + Polars 연동: https://py.iceberg.apache.org/getting-started/
- Polars 벤치마크 (TPC-H SF10): https://pola.rs/posts/benchmarks/