- Published on
Polars 1.x vs Pandas — Pandas 시대의 끝인가? Rust·Arrow·lazy 시대의 dataframe 심층 분석 2026
- Authors

- Name
- Youngju Kim
- @fjvbn20031
프롤로그 — "Pandas의 시대가 끝났다"는 말의 정확한 의미
2025~2026년에 dataframe 커뮤니티에서 가장 많이 들린 문장은 이거다.
"Polars로 갈아탔더니 같은 코드가 10배 빨라졌어요."
그리고 그 옆에 항상 따라붙는 또 다른 문장.
"그런데 sklearn에 넣으려면 결국
to_pandas()를 부르더라고요."
이 두 문장이 2026년 dataframe 세계를 정확히 요약한다. Polars는 새 워크로드를 거의 다 가져가고 있다. 그러나 Pandas는 사라지지 않는다. 15년 치 ecosystem inertia — sklearn 입력, statsmodels, 시각화 라이브러리, 수십만 개의 stackoverflow 답변, 학교 강의자료 — 가 Pandas를 안전 자산으로 만들었다.
이 글은 그 사이의 균형점을 정확히 본다. Polars 1.x의 stability 약속, Rust + Arrow 아키텍처, lazy evaluation과 query optimizer가 실제로 무엇을 하는지, Pandas 2.2 + PyArrow backend가 어디까지 따라잡았고 어디서 막혔는지, DuckDB가 같은 Arrow-native query engine으로서 어디서 Polars를 이기고 어디서 지는지, Dask·Ibis가 가진 자리, 마이그레이션의 진짜 함정. 그리고 마지막에 — 그래도 Pandas를 떠나면 안 되는 경우.
1장 · Arrow가 깔린 바닥 — dataframe revolution의 진짜 주인공
Polars vs Pandas 비교에서 가장 자주 빠뜨리는 것: 두 라이브러리 모두 Apache Arrow를 깔고 있다. 진짜 혁명은 Polars가 아니다. Arrow다.
왜 Arrow가 중요한가
Arrow는 columnar in-memory 포맷이다. NumPy도 columnar지만, 차이가 크다.
- 언어 중립 메모리 레이아웃 — Python, Rust, C++, Java, Go가 같은 메모리를 zero-copy로 공유한다.
- column-major — 같은 column의 값이 메모리에서 연속(contiguous)이다. SIMD에 최적.
- type system 내장 — string, list, struct, dictionary(category), date/time/decimal까지 표준 정의.
- null bitmap이 별도 —
NaN을 null로 오용하는 NumPy/Pandas 1.x의 고질병이 사라진다. - chunked array — 큰 column을 chunk로 나누고 lazy/streaming에 쓴다.
이 한 줄이 핵심이다.
Arrow 위에서는 Polars·Pandas·DuckDB·Spark가 같은 메모리를 본다. 변환 비용이 0에 수렴한다.
df.to_arrow() → DuckDB가 그대로 읽는다. DuckDB가 쓴 Arrow → Polars가 그대로 받는다. zero-copy. 이게 2020년대 dataframe 세계가 합쳐지는 이유다.
Arrow vs NumPy 백엔드의 실용 차이
| 항목 | NumPy backend (Pandas 1.x) | Arrow backend (Polars / Pandas 2.2+) |
|---|---|---|
| 문자열 | object dtype, Python 객체 list | string dtype, 연속 메모리, 5–10배 빠름 |
| null | NaN 강제, int column이 float로 변환됨 | 정식 null bitmap, dtype 보존 |
| 카테고리 | category dtype, 별도 구현 | dictionary type, 표준 |
| 시계열 | datetime64[ns] 고정 | timestamp[us, tz] 등 풍부 |
| zero-copy 공유 | 어렵다 | DuckDB·Polars·Spark와 즉시 |
| SIMD 활용 | 제한적 | columnar 구조에 최적 |
이 표가 곧 Polars가 Pandas를 압도하는 거의 모든 이유다. Polars는 이걸 처음부터 했고, Pandas는 2.x에서 옵션으로 따라잡고 있다.
2장 · Polars 1.x — Rust 코어와 안정성 약속
Polars는 Ritchie Vink가 2020년에 시작한 Rust dataframe 라이브러리다. 2024년 8월 1.0이 나왔고, 그 이후로 minor를 빠르게 올리며 public API stability를 약속했다. 2026년 현재 1.x 후반(릴리즈 노트는 Polars GitHub Releases)이며, 깨지는 변경은 옵트인 새 API로 들어오고 기존 API는 deprecation 사이클을 거친다.
Rust 코어 + Python binding
- 코어는 Rust —
polarscrate. memory safety, SIMD, no GIL 병렬. - Python은 binding —
py-polars. Rust 코어를 PyO3로 감싼다. - R, NodeJS binding도 같은 코어 위.
이게 의미하는 바: Python의 GIL이 Polars의 발목을 잡지 않는다. group-by가 진짜로 모든 코어를 쓴다. NumPy도 BLAS로 병렬이지만 dataframe 연산(group-by, join) 전체는 GIL에 묶여 있다.
Polars의 데이터 모델
import polars as pl
df = pl.DataFrame({
"user_id": [1, 2, 3, 4],
"country": ["KR", "US", "KR", "JP"],
"amount": [120.0, 95.0, 200.0, 75.0],
})
print(df.schema)
# Schema({'user_id': Int64, 'country': String, 'amount': Float64})
schema는 column 이름 -> Arrow dtype의 OrderedDict. 모든 dtype이 명시적이고 추론 마법이 적다.
Eager vs Lazy
Polars는 두 모드를 가진다.
- eager —
pl.DataFrame. Pandas처럼 즉시 실행. - lazy —
pl.LazyFrame. 표현식을 plan으로 쌓아두고.collect()에서 한 번에 최적화·실행.
# eager
df = pl.read_parquet("sales.parquet")
big_kr = df.filter(pl.col("country") == "KR").select(["user_id", "amount"])
# lazy — query plan을 쌓고 .collect()에서 실행
plan = (
pl.scan_parquet("sales.parquet") # I/O를 미룬다
.filter(pl.col("country") == "KR")
.select(["user_id", "amount"])
)
big_kr = plan.collect()
차이가 보이는가? scan_parquet은 파일을 아직 읽지 않는다. 다음 장에서 그 의미가 폭발한다.
3장 · Lazy evaluation과 query optimizer — Polars가 마법처럼 빠른 이유
Polars 성능의 절반은 lazy plan을 최적화하는 query optimizer에서 나온다. 무엇을 하는가?
3.1 Predicate pushdown — 필터를 I/O 단계로 밀어넣는다
plan = (
pl.scan_parquet("sales.parquet") # 1억 row
.filter(pl.col("country") == "KR") # KR만 100만 row
.select(["user_id", "amount"])
)
순진한 실행: 1억 row 다 읽고 -> 메모리에서 필터링. 100배 비싸다.
Polars optimizer가 하는 일: filter를 scan으로 밀어넣는다. Parquet의 row group 통계(min/max)를 보고 country != "KR"인 row group은 건너뛴다. 즉, 디스크 I/O가 100배 줄어든다.
3.2 Projection pushdown — 필요한 column만 읽는다
select(["user_id", "amount"])가 끝에 있어도, Parquet은 columnar이므로 그 두 column만 디스크에서 읽는다. Pandas는 pd.read_parquet() 시점에 column 전체를 읽고 나중에 select하면 늦다. (Pandas도 columns= 인자를 받지만, plan 차원의 자동 추론은 아니다.)
3.3 Common subexpression elimination
plan = (
df.lazy()
.with_columns([
(pl.col("price") * pl.col("qty")).alias("revenue"),
(pl.col("price") * pl.col("qty") * 0.1).alias("commission"),
])
)
price * qty가 두 번 등장한다. optimizer는 한 번만 계산하고 재사용한다.
3.4 Slice pushdown, type coercion, dead column 제거
.head(100)을 plan 끝에 두면 그 100개를 만족시킬 만큼만 위에서 끌어올린다. join 후 안 쓰는 column은 join 전에 제거. 명시적이지 않은 cast를 줄인다.
3.5 Streaming engine
collect(streaming=True)(2026년에는 새 streaming engine이 stable에 가까워졌다 — Polars 블로그의 streaming 발표 참조)는 메모리에 다 안 들어가는 데이터셋도 chunk 단위로 처리한다. out-of-core를 한 라이브러리 안에서 한다.
plan 시각화
plan.explain() # 사람이 읽는 plan
plan.show_graph() # graphviz, optimized vs naive 비교 가능
이게 Polars의 두뇌다. Pandas는 이런 게 없다. 명령을 받으면 그대로 실행한다.
4장 · Polars expression API — "Series 1개 = 식 1개"의 사고
Polars의 가장 큰 mental model 변화는 expression이다.
import polars as pl
df.select([
pl.col("amount").sum().alias("total"),
pl.col("amount").mean().over("country").alias("country_avg"),
(pl.col("amount") - pl.col("amount").mean()).alias("diff_from_mean"),
])
각 expression은 **column 변환의 식(DAG)**이다. Pandas의 column 조작이 imperative라면, Polars expression은 declarative — Spark DataFrame이나 SQL에 가깝다.
Pandas와의 사고 차이:
| 사고 | Pandas | Polars |
|---|---|---|
| column 새로 만들기 | df["x"] = df["a"] + df["b"] (대입) | df.with_columns((pl.col("a") + pl.col("b")).alias("x")) (식) |
| group-by 집계 | df.groupby("k")["x"].sum() (체이닝) | df.group_by("k").agg(pl.col("x").sum()) (expression list) |
| window | df.groupby("k")["x"].transform("mean") | pl.col("x").mean().over("k") |
| 다중 column 동시 변환 | for loop or apply | expression list로 한 번에 |
처음엔 어색하지만 며칠이면 익숙해진다. 그리고 expression이 plan optimizer의 노드라는 사실이 중요하다. 식을 declarative로 표현하기 때문에 Polars가 최적화할 수 있는 것이다.
5장 · Pandas 2.2 — PyArrow backend로 어디까지 따라잡았는가
Pandas는 2.0(2023)에서 PyArrow backend를 도입하고, 2.2(2024)에서 안정화했다. 2026년 현재 Pandas 2.2.x는 사실상 표준이며 3.0이 길게 베타 중이다.
Arrow backend를 켜는 법
import pandas as pd
# 방법 A — 명시적으로 Arrow dtype 지정
df = pd.read_parquet("sales.parquet", dtype_backend="pyarrow")
# 방법 B — Series dtype을 Arrow로
s = pd.Series([1, 2, None], dtype="int64[pyarrow]")
이게 켜지면 무엇이 좋아지나:
- 문자열 column이 진짜 빠르고(연속 메모리), 메모리도 절반 이상 줄어든다.
- int column에 null이 있어도 float로 변환되지 않는다 (
Int64[pyarrow]nullable). - date/time이 Arrow timestamp 그대로.
- DuckDB·Polars·Spark와 zero-copy 가능.
그런데 Pandas 2.2가 못 한 것
- lazy evaluation이 여전히 없다. 모든 연산이 eager. predicate pushdown은
read_parquet(filters=)로만 부분 지원. - query optimizer가 없다. Pandas는 사용자가 쓴 순서대로 실행한다.
- GIL 안에서 single-thread가 기본.
numba/numexpr/pyarrow.compute를 케이스별로 호출해야 병렬 혜택. - expression API가 없다. group-by 직후의 agg에서 multi-column 식을 깔끔히 표현하기 힘들다.
- API 표면이 거대 — pandas 자체가 거대한 dictionary다. 마법 같은 일이 많이 일어난다.
요약: 데이터 구조는 Arrow에 와 있지만, 실행 엔진은 여전히 1990년대식 imperative다. 그래서 Polars가 같은 Arrow 위에서도 더 빠른 것이다.
6장 · Polars vs Pandas — 본격 비교 매트릭스
같은 일을 두 라이브러리로 나란히 써본다.
6.1 Parquet 읽고, 필터링, group-by
Pandas:
import pandas as pd
df = pd.read_parquet("sales.parquet", dtype_backend="pyarrow")
kr = df[df["country"] == "KR"]
result = (
kr.groupby("user_id", as_index=False)["amount"]
.agg(["sum", "mean", "count"])
)
Polars (lazy):
import polars as pl
result = (
pl.scan_parquet("sales.parquet")
.filter(pl.col("country") == "KR")
.group_by("user_id")
.agg([
pl.col("amount").sum().alias("total"),
pl.col("amount").mean().alias("avg"),
pl.col("amount").count().alias("n"),
])
.collect()
)
코드 양이 비슷해 보여도 plan 차원에서 일어나는 일이 다르다. Polars는 filter를 scan으로 밀고, amount·user_id·country 세 column만 디스크에서 읽는다.
6.2 join
Pandas:
merged = pd.merge(orders, users, on="user_id", how="left")
Polars:
merged = orders.lazy().join(users.lazy(), on="user_id", how="left").collect()
Polars의 join은 기본적으로 hash join, parallel. 큰 데이터에서 차이가 크다.
6.3 window function
Pandas:
df["avg_country"] = df.groupby("country")["amount"].transform("mean")
df["rank_in_country"] = df.groupby("country")["amount"].rank(method="dense")
Polars:
df = df.with_columns([
pl.col("amount").mean().over("country").alias("avg_country"),
pl.col("amount").rank(method="dense").over("country").alias("rank_in_country"),
])
여러 window를 한 plan에 묶으면 Polars는 같은 partition을 한 번만 계산한다.
6.4 비교 매트릭스
| 항목 | Pandas 2.2 + Arrow | Polars 1.x |
|---|---|---|
| 메모리 백엔드 | NumPy 기본 / Arrow 옵션 | Arrow 전용 |
| 문자열 성능 | Arrow 켜면 좋음 | 처음부터 빠름 |
| null handling | Arrow dtype이면 정상 | 표준 null bitmap |
| 멀티코어 | numba/numexpr 호출 시 부분적 | 기본 병렬, GIL 영향 없음 |
| lazy plan | 없음 | LazyFrame |
| query optimizer | 없음 | predicate/projection/CSE 등 |
| streaming | 없음 | streaming engine (out-of-core) |
| expression API | 없음 (체이닝 중심) | 1급 시민 |
| 시계열 | 강력하고 성숙 | 좋아지고 있으나 일부 기능 부족 |
| 시각화 통합 | seaborn/matplotlib 직결 | to_pandas() 필요한 경우 多 |
| sklearn 호환 | 입력으로 그대로 | to_pandas() 또는 to_numpy() 필요 |
| 학습 자료 | 압도적으로 많음 | 빠르게 늘고 있으나 적음 |
성능만 보면 Polars의 압승. 그러나 ecosystem 통합·자료·legacy를 더하면 Pandas가 여전히 안전.
7장 · TPC-H 벤치마크 — 실제 숫자
Polars 팀은 TPC-H 22개 query를 직접 다 돌려 결과를 공개한다(원본은 Polars 벤치마크 페이지 — 측정은 SF=10 / 32 core 기준 등 환경별로 다름). 2026년 시점에서 자주 인용되는 대략적 형태를 표로 정리한다(정확한 절대값은 환경/버전에 따라 변하므로, 결정의 근거로 쓸 때는 원문 표를 확인하라).
| Query | Pandas 2.x | Polars 1.x (lazy) | DuckDB | Dask |
|---|---|---|---|---|
| Q1 (단순 group-by) | 기준선 | 약 1/10 시간대 | 약 1/15 | 약 1/3 |
| Q3 (join + filter) | 기준선 | 약 1/8 | 약 1/10 | 약 1/2 |
| Q9 (multi-join, 복잡) | 기준선 | 약 1/7 | 약 1/8 | 비슷~약간 빠름 |
| Q21 (correlated subquery) | 자주 OOM | 잘 돔 | 매우 빠름 | OOM 가능 |
| 메모리 다 안 들어가는 dataset | 불가 | streaming engine으로 가능 | 가능 | 분산으로 가능 |
핵심 관찰:
- Polars와 DuckDB는 single-node에서 같은 리그다 — query optimizer + Arrow + multi-core. Pandas는 거기서 한참 뒤다.
- 분산이 필요한 규모면 Dask/Spark/Ray로 간다. Polars는 단일 노드(혹은 cloud의 큰 VM) 최적화에 집중한다.
- TPC-H 같은 SQL 스타일 분석에서는 DuckDB가 가장 자주 1등. dataframe API 그대로가 필요하면 Polars.
벤치마크는 "어떤 워크로드"인지가 모든 것을 결정한다. 위 표는 의사결정의 출발점이지 결론이 아니다.
8장 · Polars vs DuckDB — 같은 엔진의 두 얼굴
이 비교가 2026년에 가장 흥미롭다. 둘 다 Arrow-native, 둘 다 vectorized columnar engine, 둘 다 single-node에서 큰 데이터 학살. 그런데 인터페이스가 다르다.
- DuckDB — embeddable SQL engine.
duckdb.sql("SELECT ... FROM parquet_scan('sales.parquet') WHERE ..."). 무거운 분석 SQL, OLAP에 강하다. window function, complex join, subquery에서 최강. - Polars — dataframe API. expression 기반의 declarative dataframe.
# 같은 일, 두 인터페이스
# DuckDB
import duckdb
result = duckdb.sql("""
SELECT user_id, SUM(amount) AS total
FROM 'sales.parquet'
WHERE country = 'KR'
GROUP BY user_id
""").to_df()
# Polars
import polars as pl
result = (
pl.scan_parquet("sales.parquet")
.filter(pl.col("country") == "KR")
.group_by("user_id")
.agg(pl.col("amount").sum().alias("total"))
.collect()
)
언제 DuckDB가 이기는가
- 복잡한 multi-table join, CTE 폭주, correlated subquery — SQL이 더 자연스럽다.
- 분석가가 SQL을 이미 쓰고 있다 — 인지 비용이 0.
- BI 도구·Jupyter Magic·dbt 통합 — 거의 모든 BI 도구가 DuckDB를 받는다.
언제 Polars가 이기는가
- column 단위 변환 파이프라인이 길다 — expression이 SQL CTE보다 깔끔하다.
- 기존 코드가 Pandas다 — 마이그레이션 표면이 작다.
- ML feature engineering처럼 program 안에서 dataframe을 손으로 잡고 싶다.
- 사용자 정의 Python 함수가 끼어 있다 (
map_elements,pipe).
둘 다 쓰는 패턴
# Polars로 받고, 무거운 SQL은 DuckDB에 넘기고, 결과를 다시 Polars로
import duckdb, polars as pl
lf = pl.scan_parquet("sales.parquet").filter(pl.col("amount") > 100)
out = duckdb.sql("SELECT country, percentile_cont(0.95) WITHIN GROUP (ORDER BY amount) AS p95 FROM lf GROUP BY country").pl()
Arrow 위에서 둘은 zero-copy로 데이터를 주고받는다. 하나만 골라야 한다는 강박을 버려라.
9장 · Dask와 Ibis — 보조 출연자들의 자리
Dask — out-of-core·분산이 필요할 때
Dask는 Pandas API를 가진 분산 dataframe이다. partition 단위로 Pandas DataFrame을 띄우고, lazy graph로 묶어 cluster에서 실행한다.
import dask.dataframe as dd
df = dd.read_parquet("s3://bucket/sales-*.parquet")
result = df[df.country == "KR"].groupby("user_id").amount.sum().compute()
Dask는 "Pandas의 분산 버전"이라는 정체성을 유지한다. 2026년 현재 Dask 2024.x 이상은 query optimizer를 점진적으로 도입(Coiled가 주도)했고, Pandas 2.x의 PyArrow dtype과 잘 어울린다. 그러나 단일 노드 성능에서는 Polars/DuckDB에 진다 — 그게 목표가 아니니까. 수십 TB, S3에 흩어진 데이터가 진짜 사용 사례다.
Ibis — backend-agnostic dataframe spec
Ibis는 dataframe 표현식을 쓰고, 그걸 DuckDB·BigQuery·Snowflake·Polars·Pandas·Spark 등 20여 개 backend에 컴파일하는 라이브러리다.
import ibis
t = ibis.read_parquet("sales.parquet") # 기본 backend는 DuckDB
expr = t.filter(t.country == "KR").group_by("user_id").agg(t.amount.sum().name("total"))
expr.execute() # Pandas로 반환
같은 query를 DuckDB에서 돌리든, BigQuery로 보내든, Snowflake로 보내든 — 코드를 안 바꾼다. data warehouse에 무거운 일을 던지고, 로컬에서 같은 코드로 테스트한다.
2026년의 Ibis 위치:
- "dataframe interface 표준" 후보. PyData ecosystem이 이쪽으로 결집하는 분위기.
- analytics team이 cloud warehouse를 쓰면 강력한 선택.
- 다만 backend별 quirks가 새는 경우가 있다 — 식 자체가 모든 backend에서 똑같이 도는 건 아니다.
10장 · 마이그레이션 함정 — Pandas → Polars의 진짜 비용
성능만 보면 Polars로 가는 게 자명해 보인다. 그런데 실제 마이그레이션에서 사람들이 부딪히는 함정들이 있다.
10.1 index가 없다
Pandas의 모든 것이 index 중심이라면 Polars는 index가 없다. .set_index(), .reset_index(), .loc[], MultiIndex 같은 게 다 사라진다. join은 명시적 key column, 정렬은 명시적 sort. 처음엔 답답하지만, 결과적으로 버그가 줄어든다 — 암묵적 index alignment로 인한 silent data corruption이 사라진다.
10.2 inplace가 없다
# Pandas
df["x"] = df["a"] + df["b"] # 동작
df.rename(columns={"a": "A"}, inplace=True)
# Polars
df = df.with_columns((pl.col("a") + pl.col("b")).alias("x"))
df = df.rename({"a": "A"})
모든 연산이 새 DataFrame을 돌려준다. immutable 사고로 강제 전환된다.
10.3 apply의 죽음(같은 의미)
# Pandas — apply가 너무 흔해서 boilerplate처럼 보임
df["y"] = df.apply(lambda r: complicated_func(r["a"], r["b"]), axis=1)
Polars에서 map_elements는 명시적으로 느린 길이다. 가능하면 expression을 합쳐라. 정 안 되면 map_batches로 batch 단위 vectorized 함수를 써라.
10.4 group-by의 결과 형태
Pandas group-by의 결과가 Series냐 DataFrame이냐 헷갈리는 모든 경우가 Polars에서는 같은 형태(DataFrame)로 통일된다. 그러나 column 이름 규칙이 달라서 KeyError가 나는 경우가 있다 — Polars는 명시적으로 .alias()를 쓰게 한다.
10.5 datetime이 다르다
Pandas의 datetime64[ns]는 nanosecond 고정. Polars(Arrow)는 Datetime("us", "UTC") 같은 형식. tz 비교가 엄격해진다. mixed-tz column을 그냥 두면 Polars는 화를 낸다. 이게 처음엔 짜증, 나중엔 축복이다.
10.6 csv 추론의 차이
read_csv에서 dtype 추론이 다르다. Pandas는 column 일부를 보고 추측, Polars는 더 보수적으로 본다. dtype을 명시하는 습관을 들여야 한다.
10.7 method 이름이 미묘하게 다름
reset_index → 그냥 없다. concat(axis=1) → pl.concat([..], how="horizontal"). pd.melt → pl.DataFrame.unpivot. merge → join. rename → 시그니처가 다르다. 코드 검색·치환만으로는 안 되고 사람이 읽어야 한다.
10.8 점진적 마이그레이션 전략
# 데이터를 받을 때부터 Polars로
df = pl.read_parquet("a.parquet")
# 무거운 변환은 Polars
df = df.filter(...).group_by(...).agg(...)
# 마지막에 sklearn/seaborn 단계에서만 pandas로
pdf = df.to_pandas()
이 패턴이 가장 현실적이다. 모든 코드를 한 번에 갈아엎지 마라. 새 파이프라인부터 Polars로, 기존은 그대로 둔다.
11장 · 그래도 Pandas를 떠나면 안 되는 경우
이 글의 가장 정직한 장이다. 2026년에도 Pandas를 쓰는 게 옳은 경우가 분명히 있다.
11.1 sklearn 입력이 끝점인 경우
sklearn은 pandas DataFrame을 1급 시민으로 본다. ColumnTransformer, OneHotEncoder(sparse_output=False), pipeline의 모든 단계가 column 이름을 보존하며 흐른다. Polars로 가도 결국 .to_pandas() 직전에 도는 거면, 굳이 갈 이유가 적다.
11.2 statsmodels·causal-inference 라이브러리
대부분의 통계 라이브러리(statsmodels, lifelines, dowhy, econml)가 Pandas를 입력 가정으로 한다. 식 표기(y ~ x1 + x2)가 column 이름에 묶여 있다.
11.3 시각화 통합
seaborn, plotnine, 많은 plotly 예제가 Pandas를 받는다. Polars도 __dataframe__ protocol을 지원하면서 직접 받는 라이브러리가 늘고 있지만, 가장 매끄러운 길은 여전히 Pandas다.
11.4 작은 데이터 + 빠른 prototyping
100만 row 미만이면 성능 차이는 수십~수백 ms — 의미 없다. 자료가 많고 손에 익은 라이브러리가 이긴다.
11.5 팀이 Pandas만 알고 있고, 마이그레이션 비용 > 이득
성능 병목이 dataframe이 아니라 다른 곳이면(예: 네트워크 I/O, 모델 추론), Polars로 가도 전체 latency가 별로 안 줄어든다. 이런 곳에서 마이그레이션은 비용일 뿐이다.
11.6 학교·교육
15년 치 강의자료, stackoverflow, 책이 Pandas 기준이다. 학습 자료의 양이 곧 학습 비용을 결정한다.
Polars는 새 파이프라인의 기본값이 되어가지만, Pandas는 글루(glue) 언어로 살아남는다. 둘 다 알아두는 게 합리적이다.
12장 · 2026년 dataframe stack의 결론
같은 표를 다른 각도로 한 번 더 정리한다 — "이 워크로드면 무엇을 쓰는가" 관점.
| 워크로드 | 1순위 | 2순위 |
|---|---|---|
| 메모리에 들어가는 분석 dataframe, Python 안에서 | Polars | DuckDB |
| 무거운 SQL 분석, BI/Jupyter에서 | DuckDB | Polars |
| TB 단위 분산, S3에 흩어진 데이터 | Dask / Spark | Ray Data |
| Snowflake/BigQuery로 push down | Ibis | (각 warehouse SDK) |
| ML feature engineering | Polars | Pandas (+Arrow) |
| sklearn 모델 학습 입력 | Pandas | Polars → to_pandas() |
| 시계열 분석 (statsmodels) | Pandas | (간단한 건 Polars) |
| 작은 데이터·빠른 prototyping | Pandas | Polars |
| 표준화된 backend-agnostic 식 | Ibis | (직접 wrapper) |
에필로그 — 균형 잡힌 한 줄
이 글의 한 문장 요약:
Polars는 새 워크로드를 거의 다 가져가고 있다. Pandas는 sklearn·시각화·교육이라는 ecosystem 늪에서 살아남는다. 2026년의 정답은 둘 다 알고, 워크로드에 맞춰 고르는 것이다.
데이터 엔지니어로서의 결정 트리:
- 새 파이프라인이고, 단일 노드 분석이라면 → Polars 기본값.
- SQL이 더 자연스럽고 분석가가 같이 본다면 → DuckDB.
- 데이터가 한 노드에 안 들어간다면 → Dask/Spark/Ray.
- cloud warehouse가 진짜 저장소라면 → Ibis + warehouse.
- ML 끝점이 sklearn이라면 → 변환은 Polars, 마지막에
to_pandas(). - 시각화·통계 마지막 단계 → Pandas로 받기.
12개 항목 체크리스트
- dataframe 백엔드가 Arrow인가(Pandas면
dtype_backend="pyarrow")? - I/O가 Parquet인가? (CSV에 묶여 있으면 어떤 라이브러리든 손해)
- lazy plan을 쓰는가? (Polars의 가장 큰 이점)
- filter/select를 plan 위쪽에 두는가? (predicate/projection pushdown)
- group-by 안의 식을 expression list로 모았는가?
- 같은 식이 두 번 나오는지 확인했는가? (CSE의 대상)
- join key의 dtype이 양쪽에서 일치하는가?
- timestamp의 tz가 명시되어 있는가?
map_elements를 남발하지 않는가?- streaming engine이 필요한 데이터 규모인지 측정했는가?
- 결과를 시각화/ML에 넘기기 전
to_pandas()한 번에 모았는가? - 벤치마크를 본인 환경/데이터로 다시 돌렸는가?
안티패턴 10가지
- Polars로 받자마자
to_pandas()로 변환해서 다시 Pandas 코드 — 이득이 0. - lazy plan을 안 만들고 모든 단계를
collect()— 최적화 기회 다 버림. map_elements를 vectorized expression으로 바꿀 수 있는데 그대로 둠.- Parquet 대신 CSV로 모든 것을 저장 — Arrow의 이점 절반 손실.
- Pandas
inplace=True마인드를 Polars에 그대로 — 안 통한다. - tz를 무시한 datetime 비교 — Polars가 에러를 던지면 그제야 깨달음.
- 분산이 필요 없는 데이터에 Dask/Spark을 미리 도입 — 복잡도 폭증, 성능 손해.
- SQL이 더 자연스러운 분석을 dataframe API로 억지로 짜기.
- 벤치마크 표를 자기 데이터/환경으로 재현 안 하고 그대로 인용 — 결정의 기반이 약함.
- "Pandas는 곧 사라진다"는 가정으로 갑작스러운 전면 마이그레이션 — 비용만 큼.
다음 글 예고
다음 글 후보: DuckDB 심층 — embedded analytics engine의 모든 것, Apache Arrow 데이터 표준 — Flight·DataFusion·ADBC까지, PySpark 4.x와 Polars/Ray Data — 분산 dataframe의 2026년 지도, Pandas → Polars 실전 마이그레이션 케이스 스터디.
"엔진은 같다 — Arrow다. dataframe API가 어떻게 그 위에 앉느냐만 다르다. 2026년의 데이터 엔지니어는 한 라이브러리의 신자가 아니라, 그 위의 stack을 읽는 사람이다."
— Polars 1.x vs Pandas, 끝.
참고 / References
- Polars 공식 사이트
- Polars GitHub 리포지토리
- Polars GitHub Releases (1.x 노트)
- Polars User Guide
- Polars Python API 레퍼런스
- Polars 벤치마크 페이지
- Polars 블로그 (streaming 등 발표)
- Pandas 공식 문서
- Pandas 2.2 What's New
- Pandas PyArrow backend 가이드
- Apache Arrow 공식
- Apache Arrow Columnar Format 명세
- DuckDB 공식
- DuckDB Python API
- DuckDB Polars 통합
- TPC-H 표준 명세
- Dask 공식
- Dask DataFrame Query Optimizer 안내
- Ibis 공식
- Ibis 백엔드 목록
- PyO3 (Rust to Python)
- Apache Spark 4.x What's New
- Modin 공식 (Pandas 호환 분산 옵션)
- Ray Data
- scikit-learn 입력 타입 안내
- DataFrame Interchange Protocol (PEP)
- Awkward Array (참고로 Arrow와의 호환)
- DataFusion (Arrow + Rust query engine)