Pandas·Polars·DuckDB

src/content/documents/data-analysis/pandas-polars-duckdb-when-to-use.json

26편의 예제는 20행이었다. 실무 데이터는 수십만~수억 행이고, 파일도 CSV 하나가 아니라 여러 개로 흩어져 있는 경우가 흔하다. 이 규모에서는 Pandas 하나로 밀어붙이기보다 PolarsDuckDB 를 상황에 맞게 섞어 쓰는 편이 낫다.

한 줄 정의

Pandas 는 즉시 실행되는 싱글스레드 표 처리 라이브러리, Polars 는 Rust로 작성된 멀티스레드 DataFrame 엔진(즉시 실행 Eager와 실행 계획을 먼저 세우는 Lazy API를 모두 지원), DuckDB 는 CSV·Parquet 파일에 서버 설치 없이 SQL을 직접 실행하는 임베디드 분석 엔진이다. 셋 다 표 형태 데이터 를 다루지만 실행 방식이 다르다.

Pandas가 힘들어지는 지점

Pandas는 한 스레드에서 순서대로 연산을 처리한다. 데이터가 수백만 행을 넘어가면 연산 하나하나의 시간이 눈에 띄게 늘고, 중간 결과를 복사하며 메모리를 원본 파일 크기의 여러 배까지 쓰기도 한다. Polars와 DuckDB는 이 두 가지 — 속도와 메모리 — 를 각자 다른 방식으로 해결한다.

Polars: Eager API vs Lazy API

구분

실행 시점

적합한 상황

Eager

코드 한 줄마다 즉시 실행 (Pandas와 비슷한 감각)

작은 데이터를 탐색적으로 확인할 때

Lazy

scan_ 함수로 시작해 실행 계획을 세운 뒤 collect() 에서 한 번에 실행

큰 파일을 걸러내고 집계하는 파이프라인

import polars as pl

# Eager: 한 줄씩 즉시 실행
df = pl.read_csv("sales.csv")
result = df.filter(pl.col("amount") > 0)

# Lazy: 실행 계획을 세운 뒤 collect()에서 한 번에 실행
result = (
    pl.scan_csv("sales.csv")
    .filter(pl.col("amount") > 0)
    .group_by("region")
    .agg([pl.col("amount").sum().alias("total")])
    .sort("total", descending=True)
    .collect()  # 여기서 실제 실행
)

collect() 를 부르기 전까지는 아무 연산도 일어나지 않는다. 그 사이 Polars는 전체 계획을 보고 필요한 열만 읽고(projection pushdown), 필터를 파일을 읽는 단계로 최대한 앞당기는(predicate pushdown) 최적화를 적용한다. .explain() 로 실제 계획을 확인할 수 있다.

plan.explain()
AGGREGATE[maintain_order: false]
  [col("amount").sum().alias("total")] BY [col("region")]
  FROM
  Csv SCAN [sales.csv]
  PROJECT 2/4 COLUMNS
  SELECTION: col("amount") > 0.0

PROJECT 2/4 COLUMNS 는 파일에 열이 4개 있어도 이 계획에 필요한 2개(region, amount)만 실제로 읽었다는 뜻이다. Pandas라면 read_csv 시점에 열 4개를 전부 메모리에 올린 뒤 걸러낸다.

Polars 핵심 문법 — Pandas와 대응 관계

하는 일

Pandas

Polars

열 참조

df['amount']

pl.col('amount')

행 필터

df[df['amount'] > 0]

df.filter(pl.col('amount') > 0)

열 선택

df[['region', 'amount']]

df.select(['region', 'amount'])

열 추가·수정

df.assign(adj=df['amount']*1.1)

df.with_columns((pl.col('amount')*1.1).alias('adj'))

그룹 집계

df.groupby('region').agg(...)

df.group_by('region').agg([...])

Pandas와 서로 변환도 가능하다.

df_pd = df_pl.to_pandas()
df_pl = pl.from_pandas(df_pd)

DuckDB — 파일에 바로 SQL

DuckDB는 CSV·Parquet 파일을 먼저 표로 읽어들이지 않고, 파일 경로를 SQL의 FROM 절에 직접 쓴다.

import duckdb

result = duckdb.sql("""
    SELECT region,
           SUM(amount) AS total,
           COUNT(*)    AS cnt
    FROM 'sales.csv'
    WHERE amount > 0
    GROUP BY region
    ORDER BY total DESC
""").df()  # 결과를 Pandas DataFrame으로

*.csv, *.parquet 같은 와일드카드로 여러 파일을 한 번에 조회할 수 있고, JOIN·GROUP BY·윈도우 함수까지 표준 SQL을 그대로 지원한다.

duckdb.sql("""
    SELECT s.*, c.tier
    FROM 'sales.parquet' s
    JOIN 'customers.csv' c ON s.customer_id = c.id
""").show()

결과를 어느 도구로 이어받을지는 변환 메서드로 정한다 — .df() 는 Pandas, .pl() 는 Polars DataFrame을 돌려준다.

실측: 같은 집계, 세 도구

50만 행짜리 CSV(약 14.7MB)에 같은 조건(amount > 0)으로 필터링한 뒤 지역별 합계를 구하는 집계를 네 가지 방식으로 실행해 시간을 쟀다.

방식

걸린 시간

Pandas (read_csv + groupby)

0.078초

Polars Eager (read_csv + filter + group_by)

0.032초

Polars Lazy (scan_csv + collect)

0.019초

DuckDB (SQL, 파일에 직접)

0.053초

이 표는 로컬 환경에서 한 번 측정한 값이다. 실제 격차는 데이터 크기, 열 개수, 파일 형식(CSV vs Parquet), 필터가 얼마나 많은 행을 걸러내는지에 따라 달라진다. 다만 방향성 — Lazy API가 Eager보다, Polars가 Pandas보다 대체로 빠르다 — 은 데이터가 커질수록 더 뚜렷해진다.

Parquet — 컬럼형 저장의 이득

CSV는 행 단위 텍스트라 열 하나만 읽어도 파일 전체를 훑어야 한다. Parquet은 열 단위(컬럼형)로 저장해 필요한 열만 골라 읽을 수 있고, 압축 효율도 더 좋다. 같은 50만 행 데이터를 두 형식으로 저장해 비교했다.

항목

CSV

Parquet

파일 크기

14.7MB

5.7MB (2.6배 작음)

전체 로딩

0.058초

0.051초

열 2개만 선택해 로딩

0.049초

0.030초 (1.6배 빠름)

열을 적게 선택할수록 Parquet의 이득이 커진다. CSV는 어떤 열을 읽든 파일 전체를 파싱해야 하지만, Parquet은 선택한 열의 데이터만 디스크에서 골라 읽기 때문이다.

Arrow — 도구 사이를 오가는 비용을 줄이는 포맷

Apache Arrow 는 컬럼형 인메모리 데이터 포맷이다. Polars, DuckDB, PyArrow가 모두 이 포맷을 내부적으로 쓰기 때문에, 세 도구 사이를 오갈 때 데이터를 다시 직렬화하지 않고 그대로 주고받을 수 있다. duckdb.sql(...).pl() 처럼 결과를 Polars로 바로 받는 것도 이 덕분이다. Pandas는 내부적으로 NumPy 배열을 쓰므로, Pandas와 주고받을 때는 이 제로카피 이점이 적용되지 않는다.

도구 선택 가이드

상황

선택

수백만 행 이하를 Jupyter에서 탐색하고 바로 시각화한다

Pandas

대용량 파일을 빠르게 필터링·집계해야 한다

Polars (Lazy API)

여러 CSV·Parquet 파일을 SQL 조인·집계로 다뤄야 한다

DuckDB

최종 결과를 시각화·리포트·모델 입력으로 넘긴다

필요한 시점에

.to_pandas() / .df() 로 Pandas 변환

세 도구를 배타적으로 고르지 않아도 된다. 원본 CSV를 Parquet으로 변환해 저장하고, DuckDB나 Polars Lazy로 걸러 집계한 뒤, 최종 검토와 시각화 단계에서만 Pandas로 바꾸는 흐름이 실무에서 흔하다.

장점과 한계

구분

내용

장점

Polars·DuckDB는 대용량에서 Pandas보다 빠르고 메모리를 덜 쓰며, Arrow 포맷 덕분에 서로 변환 비용도 낮다.

한계

생태계(시각화·통계·모델 라이브러리)는 여전히 Pandas 중심이라, 다른 도구로 처리한 결과도 결국 Pandas로 변환해 넘기는 경우가 많다. 팀 전체가 Pandas API에 익숙하다면 문법을 새로 배우는 비용도 고려해야 한다.

자주 발생하는 문제

Lazy 체인 끝에 collect()를 빼먹는다. scan_csv 로 시작한 체인은 collect() 를 호출하기 전까지 LazyFrame 상태로만 존재한다. 결과를 출력하거나 다음 단계로 넘기려면 반드시 collect() 로 실제 실행을 트리거해야 한다.

세 도구의 벤치마크 조건을 다르게 잰다. 캐시된 파일과 캐시되지 않은 파일, 다른 반복 횟수로 시간을 재면 비교 자체가 무의미해진다. 같은 데이터, 같은 반복 횟수로 측정해야 한다.

Polars Eager로 큰 데이터를 처리하고 Lazy를 안 쓴다. read_csv 는 Pandas처럼 파일 전체를 즉시 메모리에 올린다. 대용량 파일에서는 scan_csv 로 시작해야 프로젝션·필터 최적화의 이점을 받는다.

관련 문서

이 문서에서 비교한 세 도구의 기초 문법은 Pandas로 데이터 탐색·정제·집계하기 에서 다룬 EDA·groupby·merge를 전제로 한다. 파일 형식별 읽기·쓰기 API는 CSV·JSON·Parquet 파일 읽고 쓰기 를 먼저 본다.

참고 자료

댓글 0

댓글을 불러오는 중…