•8 min read

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

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

Pythonでマルチギガバイトのデータ変換にまだPandasを使っているなら、あなたは「Pandas税」を大量に支払っています。それは、シングルスレッドのCPU実行、積極的なインメモリコピー、そしてOutOfMemoryErrorをスローする前に生データセットサイズの5倍から10倍のRAMを必要とするメモリ肥大化です。

現代のデータパイプラインでは、高性能なシングルノード分析のための標準ツールキットとして、Pandasに代わってPolarsとDuckDBが使われています。どちらもカラムナー(列指向)ストレージ、ベクトル化されたSIMD実行、Apache Arrowを活用し、物理RAMよりも大きなデータセットを処理しながら10倍から50倍の高速化を実現しています。

ここでは、それらがどのように機能し、どのように比較され、本番環境でどのように連携して使用するかを説明します。


Audio Briefing
0:00 / 0:00

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つの根本的なボトルネックがあります。

  1. GILとシングルスレッド: 外部ラッパーを使用しない限り、操作は単一のCPUコアで実行されます。
  2. 積極的な評価(Eager Evaluation): すべての中間ステップで、メモリ内にデータフレームの新しいコピー全体が作成されます。
  3. 欠損データのオーバーヘッド: Pandasは歴史的に、NaNを含む整数列をfloat64にキャストし、メモリ使用量を2倍にしていました。

Advertisement

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)

Advertisement

4. ベンチマーク: 1000万行 (1.2 GB Parquet)

8コア、16GB RAMの開発者向けラップトップで1000万行のデータセットを集計およびフィルタリングした結果です。

操作Pandas 2.2Polars (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倍簡単です。

こちらもおすすめです

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
PythonFastAPIを高並行処理向けに最適化する
python

PythonFastAPIを高並行処理向けに最適化する

Uvicorn、Gunicornワーカー、asyncパターン、データベースコネクションプーリングを網羅し、高並行処理環境におけるFastAPIアプリケーションのパフォーマンスを最大化するための詳細な解説。

Read more