コンテンツにスキップ

第04回: Polars によるデータ前処理

資料のダウンロード

ファイル 内容
exercise.py ハンズオン演習スクリプト

ヒントの使い方

python exercise.py --hint 1            # 方向性ヒント
python exercise.py --hint 1 --level 2  # より具体的なヒント
python exercise.py --hint 1 --level 3  # コード例
python exercise.py --check             # 提出前の自己チェック

演習問題への取り組み方

  1. 下の講義ノートを読む(事前学習・2時間目安)
  2. exercise.py を実行してグラフを確認する
  3. 演習問題(5問)に取り組む
  4. python exercise.py --check で確認する
  5. exercise.py を提出する(締切は授業後1週間)

詰まったとき

# ヒントを見る
python exercise.py --hint N --level 1

# AIに質問する
python ../../ask.py "質問内容"

解決しない場合は Q&A ページの投稿フォームを使ってください。


講義ノート

1. Polars とは何か

Polars は Rust で書かれた高速データフレームライブラリです。2021年頃から注目を集め、大規模データ処理では pandas を大幅に上回る速度を発揮することが多くのベンチマークで報告されています。

なぜ Polars が速いのか

Polars の速度の源泉は以下の3点です。

1. Apache Arrow メモリレイアウト
列ごとに連続したメモリ領域にデータを置く(列指向ストレージ)。CPU キャッシュに乗りやすく、SIMD 命令で並列計算できる。

2. 遅延評価(Lazy Evaluation)
処理を「宣言」だけしてすぐには実行しない。collect() を呼んだとき、クエリオプティマイザが全体を見てから最適な実行計画を立てる。

3. マルチスレッド並列処理
デフォルトで全 CPU コアを使って並列に列処理を行う。GIL(Global Interpreter Lock)の制約がない Rust だからこそできる設計。


2. LazyFrame の仕組み

Polars には2種類のデータフレームがあります。

種別 生成方法 特徴
DataFrame pl.read_parquet() / pl.DataFrame(...) 即時評価。読んだ瞬間にデータがメモリに入る
LazyFrame pl.scan_parquet() / df.lazy() 遅延評価。処理を「計画」として積み上げる
import polars as pl

# DataFrame: すぐにファイルを読む
df = pl.read_parquet("city_emotion.parquet")

# LazyFrame: まだ読まない。「読む予定」を宣言するだけ
lf = pl.scan_parquet("city_emotion.parquet")

遅延評価の流れ

scan_parquet()          # ステップ1: ファイルを「見る」宣言
  ↓
.filter(...)            # ステップ2: 条件を追加(まだ実行しない)
  ↓
.group_by(...).agg(...) # ステップ3: 集計を追加(まだ実行しない)
  ↓
.sort(...)              # ステップ4: 並び順を追加(まだ実行しない)
  ↓
.collect()              # ← ここで初めてクエリオプティマイザが起動し、実行する

explain() でオプティマイザが立てた実行計画を確認できます。

print(lf.filter(pl.col("noise_level") >= 60).explain(optimized=True))
# FILTER で不要な行を除いてからファイルを読む計画が表示される

3. 主要 API の解説

3.1 Expr(式オブジェクト)

pl.col("列名") は Expr(式) を返します。Expr は計算の「レシピ」であり、実行はされていません。

# Expr の例
pl.col("happiness_score")               # 列を参照する式
pl.col("happiness_score").mean()        # 平均を計算する式
pl.col("happiness_score") - pl.col("stress_score")  # 差を計算する式

Expr を with_columns() や agg() に渡すことで、LazyFrame に処理を積み上げます。

3.2 フィルタリング(filter)

lf.filter(pl.col("noise_level") >= 60)           # 数値比較
lf.filter(pl.col("place_type") == "公園")         # 文字列比較
lf.filter(pl.col("place_type").is_in(["公園", "住宅地"]))  # 複数値
lf.filter(
    (pl.col("noise_level") >= 60) &              # AND
    (pl.col("green_ratio") < 0.3)
)

3.3 新列の追加(with_columns)

lf.with_columns([
    (pl.col("happiness_score") - pl.col("stress_score")).alias("comfort_index"),
    pl.col("week").cast(pl.Datetime).dt.month().alias("month"),
    pl.col("place_type").str.replace("施設", "SC").alias("place_short"),
])

3.4 集計(group_by + agg)

lf.group_by("place_type").agg([
    pl.col("happiness_score").mean().alias("avg_happiness"),
    pl.col("happiness_score").std().alias("std_happiness"),
    pl.col("visit_count").sum().alias("total_visits"),
    pl.len().alias("n"),
])

3.5 ウィンドウ関数(over)

over() は GROUP BY せずに集計値を列として付加します(SQL の OVER PARTITION BY に相当)。

lf.with_columns([
    pl.col("happiness_score")
        .mean()
        .over("place_type")          # 場所種別ごとの平均を全行に付加
        .alias("group_mean"),
    pl.col("happiness_score")
        .fill_null(pl.col("happiness_score").median().over("location_id"))
        .alias("filled_happiness"),  # 地点ごとの中央値で欠損補完
])

3.6 欠損値処理

lf.with_columns([
    pl.col("col").fill_null(0),                      # 定数で補完
    pl.col("col").fill_null(pl.col("col").mean()),   # 全体平均で補完
    pl.col("col").fill_null(
        pl.col("col").mean().over("group_col")       # グループ平均で補完
    ),
    pl.col("col").forward_fill(),                    # 前の値で補完(時系列)
    pl.col("col").backward_fill(),                   # 次の値で補完(時系列)
    pl.col("col").drop_nans(),                       # NaN を除去(fill_null は null のみ)
])

注意: Polars では null(欠損値)と NaN(非数)は別物です。fill_null() は null に、fill_nan() は NaN にそれぞれ作用します。

3.7 ピボット(縦→横変換)

df_pivot = df.pivot(
    on="month",          # 列名になる変数(ex: 月 → 4, 5, 6, ...)
    index="place_type",  # 行インデックス
    values="avg_pm25",   # 集計する値
    aggregate_function="first",
)

4. Pandas vs Polars 比較表

操作 Pandas Polars (LazyFrame)
ファイル読み込み pd.read_parquet("f.parquet") pl.scan_parquet("f.parquet")
行フィルタ df[df["col"] > 5] .filter(pl.col("col") > 5)
新列追加 df["new"] = df["a"] - df["b"] .with_columns([(pl.col("a") - pl.col("b")).alias("new")])
グループ集計 df.groupby("g")["v"].mean() .group_by("g").agg(pl.col("v").mean())
ウィンドウ集計 df.groupby("g")["v"].transform("mean") .with_columns(pl.col("v").mean().over("g"))
欠損補完(グループ平均) df["v"].fillna(df.groupby("g")["v"].transform("mean")) .with_columns(pl.col("v").fill_null(pl.col("v").mean().over("g")))
型変換 df["d"].astype("datetime64[ns]") pl.col("d").cast(pl.Datetime)
月抽出 df["d"].dt.month pl.col("d").dt.month()
実行トリガー (常に即時実行) .collect()

5. 遅延評価が特に効く場面

5.1 大規模ファイルの一部だけ使う

# Polars: フィルタを先にオプティマイザへ渡すので、
# 必要な行だけをファイルから読む(Parquet の述語プッシュダウン)
result = (
    pl.scan_parquet("huge_file.parquet")
    .filter(pl.col("place_type") == "公園")   # フィルタが先に実行される
    .select(["location_id", "happiness_score"])  # 必要な列だけ読む
    .collect()
)

5.2 複数の処理を1回で実行する

# ステップを積み上げるだけなら何段あってもコストはほぼゼロ
# collect() の1回でまとめて実行される
result = (
    lf
    .filter(...)
    .with_columns(...)
    .group_by(...)
    .agg(...)
    .sort(...)
    .limit(100)
    .collect()  # ← これ1回で全ステップを最適化して実行
)

6. matplotlib への接続

Polars DataFrame は .to_pandas() で pandas DataFrame に変換してから matplotlib に渡します。

df_result = lf.collect()                 # Polars DataFrame
df_pd = df_result.to_pandas()            # pandas DataFrame に変換
df_pd.plot(kind="bar", x="place_type", y="avg_happiness")  # matplotlib

また、numpy 配列として取り出すこともできます。

x = df_result["month"].to_numpy()
y = df_result["avg_pm25"].to_numpy()
plt.plot(x, y)

まとめ

  • Polars は Rust 製の高速データフレームライブラリ。Apache Arrow + マルチスレッドで pandas より高速。
  • LazyFrame は処理を「計画」として積み上げ、collect() 時にクエリオプティマイザが最適化してから実行する。
  • Expr(式オブジェクト) が Polars の核心。pl.col() で式を作り、メソッドチェーンで処理を記述する。
  • 欠損値処理は fill_null() / forward_fill() / .over() を組み合わせる。null と NaN は別物に注意。
  • ピボット は df.pivot(on=, index=, values=) で縦→横変換。
  • matplotlib へは .to_pandas() または .to_numpy() で渡す。

次回(第5回)は PCA(主成分分析)で高次元データを2次元に圧縮して可視化する手法を学びます。