Polars 入門 — Pandas より高速な DataFrame ライブラリ

Polars は Rust 製の DataFrame ライブラリで、Pandas と比べてメモリ効率・処理速度の面で大幅な改善が見込めます。この記事では基本操作から Lazy API・DuckDB 連携まで実際のコードで解説します。

Polars とは

Polars は 2020 年に登場した Rust 製の DataFrame ライブラリです。Apache Arrow をメモリフォーマットとして採用し、SIMD 最適化・並列処理により Pandas より高速に動作します。

Pandas との比較

項目PandasPolars
実装言語Python / CRust
メモリフォーマット独自Apache Arrow
並列処理限定的(GIL の制約)ネイティブマルチスレッド
Lazy 評価なしあり(クエリ最適化)
メモリ効率普通高い
速度(大規模データ)普通2〜10 倍高速(ベンチマーク依存)
エコシステム成熟成長中
Pandas 互換 APIpolars.pandas_compat で一部対応

インストール

pip install polars

# 追加機能(Excel 読み込み・Parquet 等)
pip install polars[all]

基本操作

DataFrame の作成

import polars as pl

# 辞書から作成
df = pl.DataFrame({
    "name": ["Alice", "Bob", "Charlie", "David"],
    "age": [25, 30, 35, 28],
    "sales": [120.5, 85.0, 200.3, 150.7],
    "region": ["East", "West", "East", "North"],
})

print(df)
# shape: (4, 4)
# ┌─────────┬─────┬───────┬────────┐
# │ name    ┆ age ┆ sales ┆ region │
# ╞═════════╪═════╪═══════╪════════╡
# │ Alice   ┆ 25  ┆ 120.5 ┆ East   │
# ...

CSV / Parquet の読み込み

# CSV
df = pl.read_csv("data/sales.csv")

# CSV(型を明示)
df = pl.read_csv(
    "data/sales.csv",
    schema_overrides={"date": pl.Date, "amount": pl.Float64},
)

# Parquet
df = pl.read_parquet("data/events.parquet")

# 複数ファイル(glob)
df = pl.read_parquet("data/*.parquet")

基本的な操作

# 行数・列数
print(df.shape)     # (4, 4)
print(df.height)    # 4
print(df.width)     # 4

# 列の型確認
print(df.schema)
# Schema({'name': String, 'age': Int64, 'sales': Float64, 'region': String})

# 先頭・末尾
df.head(3)
df.tail(3)

# 列の選択
df.select("name", "sales")
df.select(pl.col("name"), pl.col("sales"))

# 列の追加
df = df.with_columns(
    (pl.col("sales") * 1.1).alias("sales_with_tax")
)

フィルタリング

# 条件フィルタ
df.filter(pl.col("age") > 28)

# 複数条件(AND)
df.filter(
    (pl.col("age") > 25) & (pl.col("region") == "East")
)

# 複数条件(OR)
df.filter(
    (pl.col("region") == "East") | (pl.col("region") == "West")
)

# in 条件
df.filter(pl.col("region").is_in(["East", "North"]))

# null を除外
df.filter(pl.col("sales").is_not_null())

# 文字列の部分一致
df.filter(pl.col("name").str.starts_with("A"))

集計

# グループ集計
df.group_by("region").agg(
    pl.col("sales").sum().alias("total_sales"),
    pl.col("sales").mean().alias("avg_sales"),
    pl.col("sales").count().alias("count"),
)

# 複数列でグループ化
df.group_by("region", "age").agg(
    pl.col("sales").sum()
)

# 全列の基本統計
df.describe()

# 単一集計
df["sales"].sum()
df["sales"].mean()
df["sales"].std()

Pandas との文法比較

よく使う操作の対応表です。

import pandas as pd
import polars as pl

# ---- フィルタリング ----
# Pandas
pd_df[pd_df["age"] > 28]

# Polars
pl_df.filter(pl.col("age") > 28)

# ---- 列の追加 ----
# Pandas
pd_df["tax"] = pd_df["sales"] * 0.1

# Polars(イミュータブル)
pl_df = pl_df.with_columns(
    (pl.col("sales") * 0.1).alias("tax")
)

# ---- グループ集計 ----
# Pandas
pd_df.groupby("region")["sales"].sum().reset_index()

# Polars
pl_df.group_by("region").agg(pl.col("sales").sum())

# ---- 欠損値の埋め ----
# Pandas
pd_df["sales"].fillna(0)

# Polars
pl_df.with_columns(
    pl.col("sales").fill_null(0)
)

# ---- 文字列操作 ----
# Pandas
pd_df["name"].str.upper()

# Polars
pl_df.with_columns(
    pl.col("name").str.to_uppercase()
)

# ---- 日付操作 ----
# Pandas
pd_df["date"].dt.year

# Polars
pl_df.with_columns(
    pl.col("date").dt.year().alias("year")
)

Lazy API — クエリ最適化で大規模データを効率処理

Polars の Lazy API は SQL のクエリプランナーのように、実際に計算する前にクエリ全体を最適化します。大規模データでは特に効果的です。

Eager(即時評価)vs Lazy(遅延評価)

# Eager: 各ステップで即座に計算(小〜中規模データ向け)
result = (
    df
    .filter(pl.col("sales") > 100)
    .group_by("region")
    .agg(pl.col("sales").sum())
)

# Lazy: クエリを構築してから一括実行(大規模データ向け)
result = (
    df.lazy()                               # LazyFrame に変換
    .filter(pl.col("sales") > 100)
    .group_by("region")
    .agg(pl.col("sales").sum())
    .collect()                              # ここで初めて計算実行
)

ファイルから直接 Lazy 読み込み

# scan_* はファイルを Lazy で読み込む(メモリに全部載せない)
result = (
    pl.scan_csv("data/large_sales.csv")     # 数 GB のファイルも OK
    .filter(pl.col("region") == "East")
    .group_by("date")
    .agg(pl.col("sales").sum())
    .sort("date")
    .collect()
)

# Parquet も同様
result = (
    pl.scan_parquet("data/*.parquet")
    .filter(pl.col("date") >= "2025-01-01")
    .select("date", "product_id", "sales")
    .collect()
)

クエリプランの確認

lf = (
    pl.scan_csv("data/large_sales.csv")
    .filter(pl.col("sales") > 100)
    .group_by("region")
    .agg(pl.col("sales").sum())
)

# 最適化前後のプランを確認
print(lf.explain())                 # 最適化後
print(lf.explain(optimized=False))  # 最適化前

DuckDB との連携

Polars と DuckDB は互いにデータを渡し合えます。DuckDB の SQL 表現力と Polars の高速処理を組み合わせるのが強力なパターンです。

import polars as pl
import duckdb

# Polars DataFrame を作成
df = pl.read_parquet("data/sales.parquet")

# DuckDB で SQL クエリ(Polars DataFrame を直接参照可能)
result = duckdb.sql("""
    SELECT
        region,
        DATE_TRUNC('month', sale_date) AS month,
        SUM(sales) AS total_sales
    FROM df
    WHERE sale_date >= '2025-01-01'
    GROUP BY region, month
    ORDER BY month, total_sales DESC
""").pl()  # .pl() で Polars DataFrame として返す

print(type(result))  # <class 'polars.dataframe.frame.DataFrame'>

Arrow を介した高速データ受け渡し

# Polars → DuckDB(Arrow 経由でゼロコピー)
conn = duckdb.connect()
conn.register("sales_df", df.to_arrow())

result = conn.execute("""
    SELECT * FROM sales_df WHERE sales > 1000
""").fetchdf()

# DuckDB → Polars
pl_result = pl.from_arrow(
    conn.execute("SELECT * FROM sales_df").fetch_arrow_table()
)

ハマりやすいポイント

Polars の DataFrame はイミュータブル

Pandas では df["col"] = value で直接代入できますが、Polars では with_columns を使います。

# NG: Polars では動かない
df["tax"] = df["sales"] * 0.1

# OK
df = df.with_columns(
    (pl.col("sales") * 0.1).alias("tax")
)

group_by の結果は順序が不定

Polars の group_by は並列処理のため、結果の行順が毎回異なります。順序を保証したい場合は sort を明示します。

result = (
    df
    .group_by("region")
    .agg(pl.col("sales").sum())
    .sort("region")   # 明示的にソート
)

日付文字列のパースは str.to_date を使う(str.strptime は deprecated 予定)

Polars 1.x 以降、str.strptimestr.to_date / str.to_datetime / str.to_time に置き換えられつつあります。

# Pandas
pd_df["date"] = pd.to_datetime(pd_df["date"], format="%Y/%m/%d")

# Polars(推奨: str.to_date)
pl_df = pl_df.with_columns(
    pl.col("date").str.to_date(format="%Y/%m/%d")
)

# 日時(datetime)の場合
pl_df = pl_df.with_columns(
    pl.col("datetime").str.to_datetime(format="%Y-%m-%d %H:%M:%S")
)

Lazy API で .collect() を忘れる

scan_*.lazy() を使った場合、.collect() を呼ばないと LazyFrame のまま返ります。

result = pl.scan_csv("data.csv").filter(pl.col("age") > 25)
print(type(result))  # <class 'polars.LazyFrame'> ← collect() が必要

result = result.collect()
print(type(result))  # <class 'polars.DataFrame'>

まとめ

  • Polars は Rust 製・Apache Arrow ベースの高速 DataFrame ライブラリ
  • 基本 API は Pandas に似ているが、イミュータブル・Lazy 評価などの違いがある
  • scan_* + Lazy API で数 GB のファイルをメモリ効率よく処理できる
  • DuckDB と Arrow 経由でゼロコピー連携でき、SQL と DataFrame 処理を使い分けられる
  • Pandas からの移行は段階的に行える。まず小さなデータで試して感覚をつかむのがおすすめ