Skip to content

필사 모드: Polars 1.x vs Pandas — Pandas 시대의 끝인가? Rust·Arrow·lazy 시대의 dataframe 심층 분석 2026

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

프롤로그 — "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](https://github.com/pola-rs/polars/releases))이며, 깨지는 변경은 옵트인 새 API로 들어오고 기존 API는 deprecation 사이클을 거친다.

Rust 코어 + Python binding

- **코어는 Rust** — `polars` crate. 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의 데이터 모델

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 발표](https://pola.rs/posts/) 참조)는 메모리에 다 안 들어가는 데이터셋도 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이다.

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](https://pandas.pydata.org/docs/whatsnew/v2.2.0.html)는 사실상 표준이며 3.0이 길게 베타 중이다.

Arrow backend를 켜는 법

방법 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:

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):

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 벤치마크 페이지](https://pola.rs/posts/benchmarks/) — 측정은 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

result = duckdb.sql("""

SELECT user_id, SUM(amount) AS total

FROM 'sales.parquet'

WHERE country = 'KR'

GROUP BY user_id

""").to_df()

Polars

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로

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에서 실행한다.

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에 컴파일하는 라이브러리다.

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년의 정답은 둘 다 알고, 워크로드에 맞춰 고르는 것이다.**

데이터 엔지니어로서의 결정 트리:

1. 새 파이프라인이고, 단일 노드 분석이라면 → **Polars 기본값**.

2. SQL이 더 자연스럽고 분석가가 같이 본다면 → **DuckDB**.

3. 데이터가 한 노드에 안 들어간다면 → **Dask/Spark/Ray**.

4. cloud warehouse가 진짜 저장소라면 → **Ibis** + warehouse.

5. ML 끝점이 sklearn이라면 → 변환은 Polars, 마지막에 `to_pandas()`.

6. 시각화·통계 마지막 단계 → **Pandas**로 받기.

12개 항목 체크리스트

1. dataframe 백엔드가 Arrow인가(Pandas면 `dtype_backend="pyarrow"`)?

2. I/O가 Parquet인가? (CSV에 묶여 있으면 어떤 라이브러리든 손해)

3. lazy plan을 쓰는가? (Polars의 가장 큰 이점)

4. filter/select를 plan 위쪽에 두는가? (predicate/projection pushdown)

5. group-by 안의 식을 expression list로 모았는가?

6. 같은 식이 두 번 나오는지 확인했는가? (CSE의 대상)

7. join key의 dtype이 양쪽에서 일치하는가?

8. timestamp의 tz가 명시되어 있는가?

9. `map_elements`를 남발하지 않는가?

10. streaming engine이 필요한 데이터 규모인지 측정했는가?

11. 결과를 시각화/ML에 넘기기 전 `to_pandas()` 한 번에 모았는가?

12. 벤치마크를 본인 환경/데이터로 다시 돌렸는가?

안티패턴 10가지

1. Polars로 받자마자 `to_pandas()`로 변환해서 다시 Pandas 코드 — 이득이 0.

2. lazy plan을 안 만들고 모든 단계를 `collect()` — 최적화 기회 다 버림.

3. `map_elements`를 vectorized expression으로 바꿀 수 있는데 그대로 둠.

4. Parquet 대신 CSV로 모든 것을 저장 — Arrow의 이점 절반 손실.

5. Pandas `inplace=True` 마인드를 Polars에 그대로 — 안 통한다.

6. tz를 무시한 datetime 비교 — Polars가 에러를 던지면 그제야 깨달음.

7. 분산이 필요 없는 데이터에 Dask/Spark을 미리 도입 — 복잡도 폭증, 성능 손해.

8. SQL이 더 자연스러운 분석을 dataframe API로 억지로 짜기.

9. 벤치마크 표를 자기 데이터/환경으로 재현 안 하고 그대로 인용 — 결정의 기반이 약함.

10. "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 공식 사이트](https://pola.rs/)

- [Polars GitHub 리포지토리](https://github.com/pola-rs/polars)

- [Polars GitHub Releases (1.x 노트)](https://github.com/pola-rs/polars/releases)

- [Polars User Guide](https://docs.pola.rs/)

- [Polars Python API 레퍼런스](https://docs.pola.rs/api/python/stable/reference/index.html)

- [Polars 벤치마크 페이지](https://pola.rs/posts/benchmarks/)

- [Polars 블로그 (streaming 등 발표)](https://pola.rs/posts/)

- [Pandas 공식 문서](https://pandas.pydata.org/docs/)

- [Pandas 2.2 What's New](https://pandas.pydata.org/docs/whatsnew/v2.2.0.html)

- [Pandas PyArrow backend 가이드](https://pandas.pydata.org/docs/user_guide/pyarrow.html)

- [Apache Arrow 공식](https://arrow.apache.org/)

- [Apache Arrow Columnar Format 명세](https://arrow.apache.org/docs/format/Columnar.html)

- [DuckDB 공식](https://duckdb.org/)

- [DuckDB Python API](https://duckdb.org/docs/api/python/overview)

- [DuckDB Polars 통합](https://duckdb.org/docs/guides/python/polars)

- [TPC-H 표준 명세](https://www.tpc.org/tpch/)

- [Dask 공식](https://www.dask.org/)

- [Dask DataFrame Query Optimizer 안내](https://www.coiled.io/blog/dask-dataframe-2024-q1)

- [Ibis 공식](https://ibis-project.org/)

- [Ibis 백엔드 목록](https://ibis-project.org/backends/)

- [PyO3 (Rust to Python)](https://pyo3.rs/)

- [Apache Spark 4.x What's New](https://spark.apache.org/releases/)

- [Modin 공식 (Pandas 호환 분산 옵션)](https://modin.readthedocs.io/)

- [Ray Data](https://docs.ray.io/en/latest/data/data.html)

- [scikit-learn 입력 타입 안내](https://scikit-learn.org/stable/modules/compose.html)

- [DataFrame Interchange Protocol (PEP)](https://data-apis.org/dataframe-protocol/latest/)

- [Awkward Array (참고로 Arrow와의 호환)](https://awkward-array.org/)

- [DataFusion (Arrow + Rust query engine)](https://datafusion.apache.org/)

현재 단락 (1/295)

2025~2026년에 dataframe 커뮤니티에서 가장 많이 들린 문장은 이거다.

작성 글자: 0원문 글자: 17,032작성 단락: 0/295