📑

polarsで曜日操作を行う際の落とし穴

に公開

はじめに

polarsで曜日を用いた特徴量を作成して出力を検証したところ、Python標準のdatetimeとpolarsで曜日の扱いに違いがあることに気づきました。
本記事では、曜日の扱いに関する各ライブラリの仕様の違いと、polars特有の落とし穴、そしてその具体的な解決策について整理します。

この記事でわかること

  • Python / pandas / polars における曜日の扱いの違い
  • polarsで曜日を使う際の注意点と回避策

検証環境

  • Python: 3.11.11
  • pandas: 2.2.3
  • polars: 1.27.0
  • OS: macOS

結論

各ライブラリでの曜日の返り値は以下の通り:

ライブラリ 関数 返り値 備考
Python datetime.weekday() 0 (月) 〜 6 (日) -
pandas dt.dayofweek 0 (月) 〜 6 (日) Pythonと同じ
polars dt.weekday() 1 (月) 〜 7 (日) 1始まりのインデックス

検証

Pythonコード

from datetime import datetime
import pandas as pd
import polars as pl

def compare_weekday_calculations():
    dates = [
        datetime(2023, 1, 1),  # 日曜日
        datetime(2023, 1, 2),  # 月曜日
        datetime(2023, 1, 3),  # 火曜日
        datetime(2023, 1, 4),  # 水曜日
        datetime(2023, 1, 5),  # 木曜日
        datetime(2023, 1, 6),  # 金曜日
        datetime(2023, 1, 7),  # 土曜日
    ]

    df = pl.DataFrame({"date": dates})
    pdf = pd.DataFrame({"date": dates})

    df_polars_weekday = df.with_columns(pl.col("date").dt.weekday().alias("polars_weekday"))
    pandas_weekdays = pdf["date"].dt.dayofweek.tolist()

    print(f"{'日付':<12}{'曜日':<8}{'Python':<10}{'pandas':<10}{'Polars':<10}{'調整後':<10}")
    day_names = ["月", "火", "水", "木", "金", "土", "日"]

    for i, date in enumerate(dates):
        python_wd = date.weekday()
        pandas_wd = pandas_weekdays[i]
        polars_wd = df_polars_weekday[i, "polars_weekday"]
        day_name = day_names[python_wd] if python_wd < 6 else "日"
        print(f"{date.strftime('%Y-%m-%d')}  {day_name:<6}{python_wd:<10}{pandas_wd:<10}{polars_wd:<10}{adjusted_wd:<10}")

出力結果

日付          曜日    Python    pandas    polars
2023-01-01    日       6         6         7   
2023-01-02    月       0         0         1   
2023-01-03    火       1         1         2    
2023-01-04    水       2         2         3    
2023-01-05    木       3         3         4         
2023-01-06    金       4         4         5         
2023-01-07    土       5         5         6       

polarsのみ出力結果が違う。

なぜpolarsだけ違うのか?

polarsはRustで実装されていて、日時処理にはRustの chrono ライブラリが使われている。

chrono の weekday().number_from_monday() は、1 = 月曜日, 7 = 日曜日になっていて、この仕様がpolarsにそのまま反映されている様子。

use chrono::{Datelike, NaiveDate};

fn main() {
    let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
    println!("{}", date.weekday().number_from_monday()); // 7 (日曜日)
}

対処法

  1. 曜日をpolarsで処理した上でPython/pandasと互換性のある形式で使いたい場合は、返り値から -1 することで調整する。
df = df.with_columns((pl.col("date").dt.weekday() - 1).alias("weekday_fixed"))
  1. 曜日をLabelとして処理を行う。(scikit-learnのLabelEncoderを使って処理する。)

まとめ

  • polarsの dt.weekday() は 1〜7(月〜日) を返すため、Pythonやpandasと異なる。
  • 特徴量エンジニアリングや曜日ごとの集計処理においては インデックスの違いに注意 が必要。
  • 以下の条件を両方満たす場合では明示的な調整が推奨される。
    • 曜日を数値として扱う場合
    • polars以外のパッケージ(NumPyなど)と併用する場合

Reference

polars document
pandas document
Rust chrono crate

Discussion