高速データサイエンス: DuckDBとPolarsによる高性能アナリティクス

Table of Contents
Pythonでマルチギガバイトのデータ変換にまだPandasを使っているなら、あなたは「Pandas税」を大量に支払っています。それは、シングルスレッドのCPU実行、積極的なインメモリコピー、そしてOutOfMemoryErrorをスローする前に生データセットサイズの5倍から10倍のRAMを必要とするメモリ肥大化です。
現代のデータパイプラインでは、高性能なシングルノード分析のための標準ツールキットとして、Pandasに代わってPolarsとDuckDBが使われています。どちらもカラムナー(列指向)ストレージ、ベクトル化されたSIMD実行、Apache Arrowを活用し、物理RAMよりも大きなデータセットを処理しながら10倍から50倍の高速化を実現しています。
ここでは、それらがどのように機能し、どのように比較され、本番環境でどのように連携して使用するかを説明します。
Pandasの根本的な問題
Dataset on Disk (Parquet/CSV): 2.0 GB
Pandas in RAM: 12.0 GB - 18.0 GB (Peak during merge/groupby)
Polars in RAM: 2.3 GB (Zero-copy Arrow + streaming)
DuckDB in RAM: 0.8 GB (Chunked vectorized execution engine)
Pandasは2008年にNumPyの1D配列をベースに設計されました。これには3つの根本的なボトルネックがあります。
- GILとシングルスレッド: 外部ラッパーを使用しない限り、操作は単一のCPUコアで実行されます。
- 積極的な評価(Eager Evaluation): すべての中間ステップで、メモリ内にデータフレームの新しいコピー全体が作成されます。
- 欠損データのオーバーヘッド: Pandasは歴史的に、
NaNを含む整数列をfloat64にキャストし、メモリ使用量を2倍にしていました。
1. Polars: Rustを搭載したデータフレームエンジン
PolarsはRustでゼロから書かれており、Apache Arrowの列指向メモリフォーマット上に直接構築されています。PythonのGIL制限なしに、利用可能なすべてのCPUコアを使用してデータを処理します。
Eager実行とLazy実行
本番環境では、ほとんどの場合、PolarsのLazy API(LazyFrame)を使用すべきです。Polarsは計算をすぐに実行する代わりに、論理プランを構築し、それを最適化(述語プッシュダウン、射影プッシュダウン、スライスプッシュダウン)し、並行して実行します。
import polars as pl
# Construct lazy query plan — zero disk I/O occurs here
lazy_query = (
pl.scan_parquet("s3://analytics-bucket/events/*.parquet")
.filter(pl.col("timestamp") >= pl.date(2026, 1, 1))
.filter(pl.col("event_type").is_in(["purchase", "subscription"]))
.with_columns([
(pl.col("amount_cents") / 100.0).alias("amount_usd"),
pl.col("user_id").n_unique().over("country").alias("unique_users_per_country")
])
.group_by(["country", "event_type"])
.agg([
pl.col("amount_usd").sum().alias("total_revenue"),
pl.col("amount_usd").mean().alias("avg_order_value"),
pl.len().alias("transaction_count")
])
.sort("total_revenue", descending=True)
)
# Inspect the optimized query plan
print(lazy_query.explain())
# Execute optimized plan in parallel across all CPU cores
result_df = lazy_query.collect(streaming=True)
print(result_df)
Polarsのクエリ最適化が重要な理由
フィルター付きで.scan_parquet()を呼び出すと、次のようになります。
- 述語プッシュダウン(Predicate Pushdown): PolarsはParquetメタデータフッターを検査し、
timestamp >= 2026-01-01に一致しない行グループ全体をディスクからデータを読み取ることなくスキップします。 - 射影プッシュダウン(Projection Pushdown): Polarsはクエリで参照されている4つの列(
timestamp、event_type、amount_cents、country)のみを読み取り、ファイル内の残りの50列は無視します。 - ストリーミングエンジン(
streaming=True): データをストリーミングマイクロバッチで処理するため、マシンの物理RAMを超えるデータセットでも変換が可能です。
2. DuckDB: 「カラムナー分析のためのSQLite」
PolarsがデータフレームAPIを提供する一方で、DuckDBは組み込みのインプロセスSQL OLAPデータベースです。外部サーバーの依存関係なし、ネットワーク遅延なし、ネイティブSQL方言サポートでPythonプロセス内で実行されます。
リモートParquetとS3をSQLで直接クエリする
DuckDBは、ディスク上またはリモートS3にある圧縮されたParquet、CSV、またはJSONファイルに対して、最初にデータベーステーブルにロードすることなく、SQLクエリを直接実行できます。
import duckdb
# Connect to in-process DuckDB instance (or persist to 'analytics.duckdb')
con = duckdb.connect()
# Enable S3 / HTTP filesystem extension
con.execute("INSTALL httpfs; LOAD httpfs;")
con.execute("""
SET s3_region='us-east-1';
SET s3_access_key_id='YOUR_KEY';
SET s3_secret_access_key='YOUR_SECRET';
""")
# Query 100GB of remote Parquet files using Vectorized SQL
query = """
SELECT
country,
event_type,
COUNT(DISTINCT user_id) AS unique_users,
ROUND(SUM(amount_cents) / 100.0, 2) AS total_revenue_usd,
ROUND(AVG(amount_cents) / 100.0, 2) AS aov_usd
FROM read_parquet('s3://analytics-bucket/events/year=2026/*/*.parquet')
WHERE event_type IN ('purchase', 'subscription')
GROUP BY country, event_type
HAVING total_revenue_usd > 10000
ORDER BY total_revenue_usd DESC
LIMIT 20;
"""
# Execute and fetch directly to Arrow, Polars, or Python dictionaries
results = con.execute(query).pl() # Returns native Polars DataFrame
print(results)
3. ゼロコピー相互運用: Polars + DuckDB + Apache Arrow
PolarsとDuckDBはどちらもメモリ表現にApache Arrowを使用しているため、メモリのシリアライズやコピーのオーバーヘッドなしでデータを相互に渡すことができます。
import polars as pl
import duckdb
# 1. Load and clean data with Polars
df = pl.DataFrame({
"user_id": [101, 102, 103, 104],
"scores": [88.5, 92.0, 79.5, 95.0],
"tier": ["gold", "platinum", "gold", "platinum"]
})
# 2. Run complex analytical window SQL in DuckDB directly on the Polars DataFrame
con = duckdb.connect()
# DuckDB can reference the 'df' Python variable directly in the FROM clause!
sql_result = con.execute("""
SELECT
user_id,
scores,
tier,
RANK() OVER (PARTITION BY tier ORDER BY scores DESC) as rank_in_tier,
AVG(scores) OVER (PARTITION BY tier) as tier_avg
FROM df
""").arrow() # Zero-copy Arrow Table
# 3. Convert back to Polars instantaneously
final_df = pl.from_arrow(sql_result)
print(final_df)
4. ベンチマーク: 1000万行 (1.2 GB Parquet)
8コア、16GB RAMの開発者向けラップトップで1000万行のデータセットを集計およびフィルタリングした結果です。
| 操作 | Pandas 2.2 | Polars (Eager) | Polars (Lazy) | DuckDB (SQL) |
|---|---|---|---|---|
| Parquet読み込み + フィルタリング | 4.82秒 | 0.82秒 | 0.29秒 | 0.31秒 |
| グループ化 + 4つの集計 | 3.15秒 | 0.41秒 | 0.28秒 | 0.24秒 |
| ウィンドウ関数 | 2.40秒 | 0.35秒 | 0.26秒 | 0.21秒 |
| ピークRAM使用量 | 約4.6 GB | 約1.4 GB | 約0.7 GB | 約0.4 GB |
5. アーキテクチャ決定マトリックス: いつ何を使うべきか
Dataset Scale & Use Case
│
┌─────────────────────────────────┴─────────────────────────────────┐
▼ ▼
Single Machine (< 500GB) Distributed Cluster (> 1TB)
│ │
┌────┴──────────────────────────┐ ┌─────────┴─────────┐
▼ ▼ ▼ ▼
SQL-Heavy / S3 Parquet DataFrame Transformations Batch Pipeline Real-time OLAP
▼ ▼ ▼ ▼
DuckDB Polars Apache Spark ClickHouse
(Embedded SQL) (Rust Multi-thread) / Ray / StarRocks
- Polarsを選ぶべき時: 特徴量エンジニアリングパイプライン、MLデータ前処理、ETLスクリプト、または型安全性とPythonらしい表現が光る複雑な手続き型データフレームロジックを作成している場合。
- DuckDBを選ぶべき時: 標準SQL、ローカル/S3 Parquetファイルに対するアドホック分析、アプリに組み込まれた分析ダッシュボード、または標準のODBC/JDBC/Pythonコネクタを介したBIツールとの統合が必要な場合。
- 100GB未満のデータにはSparkを避ける: 現代のマルチコアマシンでPolarsまたはDuckDBを実行する方が、JVMとネットワークシリアライゼーションのオーバーヘッドがある分散Sparkクラスターを立ち上げるよりも、多くの場合高速で、安価で、メンテナンスが10倍簡単です。
こちらもおすすめです
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

本番環境のSQLite: WALモード、高並行性、そして実践的なPRAGMA設定
高スループットな本番環境でSQLiteをマスターしましょう。先行書き込みログ(WAL)、busy_timeoutのチューニング、読み書きの同時実行性、そして実用的なベンチマークについて解説します。
Read more
PythonFastAPIを高並行処理向けに最適化する
Uvicorn、Gunicornワーカー、asyncパターン、データベースコネクションプーリングを網羅し、高並行処理環境におけるFastAPIアプリケーションのパフォーマンスを最大化するための詳細な解説。
Read more
高度なPythonデータ構造:リストと辞書を万能薬にするのをやめよう
Pythonの高性能データ構造であるcollections.deque、Counter、defaultdict、heapq、slots dataclassesを使いこなし、10倍高速なコードを実現するための実践的なガイド。
Read more