Polars 入門 — Pandas より高速な DataFrame ライブラリ
Polars は Rust 製の DataFrame ライブラリで、Pandas と比べてメモリ効率・処理速度の面で大幅な改善が見込めます。この記事では基本操作から Lazy API・DuckDB 連携まで実際のコードで解説します。
Polars とは
Polars は 2020 年に登場した Rust 製の DataFrame ライブラリです。Apache Arrow をメモリフォーマットとして採用し、SIMD 最適化・並列処理により Pandas より高速に動作します。
Pandas との比較
| 項目 | Pandas | Polars |
|---|---|---|
| 実装言語 | Python / C | Rust |
| メモリフォーマット | 独自 | Apache Arrow |
| 並列処理 | 限定的(GIL の制約) | ネイティブマルチスレッド |
| Lazy 評価 | なし | あり(クエリ最適化) |
| メモリ効率 | 普通 | 高い |
| 速度(大規模データ) | 普通 | 2〜10 倍高速(ベンチマーク依存) |
| エコシステム | 成熟 | 成長中 |
| Pandas 互換 API | — | polars.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.strptime は str.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 からの移行は段階的に行える。まず小さなデータで試して感覚をつかむのがおすすめ