Skip to content
Published on

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

Authors

프롤로그 — "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 객체 liststring dtype, 연속 메모리, 5–10배 빠름
nullNaN 강제, 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

  • 코어는 Rustpolars crate. memory safety, SIMD, no GIL 병렬.
  • Python은 bindingpy-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})

schemacolumn 이름 -> Arrow dtype의 OrderedDict. 모든 dtype이 명시적이고 추론 마법이 적다.

Eager vs Lazy

Polars는 두 모드를 가진다.

  • eagerpl.DataFrame. Pandas처럼 즉시 실행.
  • lazypl.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와의 사고 차이:

사고PandasPolars
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)
windowdf.groupby("k")["x"].transform("mean")pl.col("x").mean().over("k")
다중 column 동시 변환for loop or applyexpression 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 + ArrowPolars 1.x
메모리 백엔드NumPy 기본 / Arrow 옵션Arrow 전용
문자열 성능Arrow 켜면 좋음처음부터 빠름
null handlingArrow 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년 시점에서 자주 인용되는 대략적 형태를 표로 정리한다(정확한 절대값은 환경/버전에 따라 변하므로, 결정의 근거로 쓸 때는 원문 표를 확인하라).

QueryPandas 2.xPolars 1.x (lazy)DuckDBDask
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.meltpl.DataFrame.unpivot. mergejoin. 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 안에서PolarsDuckDB
무거운 SQL 분석, BI/Jupyter에서DuckDBPolars
TB 단위 분산, S3에 흩어진 데이터Dask / SparkRay Data
Snowflake/BigQuery로 push downIbis(각 warehouse SDK)
ML feature engineeringPolarsPandas (+Arrow)
sklearn 모델 학습 입력PandasPolars → to_pandas()
시계열 분석 (statsmodels)Pandas(간단한 건 Polars)
작은 데이터·빠른 prototypingPandasPolars
표준화된 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