polarsのDatetime操作にはoffset_byという話

皆さんこんにちは。機械学習エンジニアのwasatingです。

今回も皆さんおなじみpolarsのtips話をしたいと思います。というかタイトルに書いてある通りなんですが…

データ分析をする際や、時系列でtrain, testを分割する際に、最新の日付から一定期間等の区切りをつけたりしたいですよね。
こんな時、日単位での指定であれば単純にdatetimetimedeltaを使うことが多いと思います。

しかし、月単位の場合そうもいかないため、dateutilrelativedelta.relativedeltaを使うことで、月単位での計算をすることがあると思います。

が、技術ブログのためpolarsで全部完結させたい、そのほうが可読性等の面からよさそうということで、上記のものを使わずに対応する話をしていきたいと思います。

が、その前に

実行環境

import polars as pl
pl.show_versions()

--------Version info---------
Polars:              0.18.15
Index type:          UInt32
Platform:            Linux-5.10.16.3-microsoft-standard-WSL2-x86_64-with-glibc2.31
Python:              3.9.11 (main, Mar 18 2022, 16:45:24) 
[GCC 10.2.1 20210110]

----Optional dependencies----
adbc_driver_sqlite:  <not installed>
cloudpickle:         <not installed>
connectorx:          <not installed>
deltalake:           <not installed>
fsspec:              2022.11.0
matplotlib:          3.6.0
numpy:               1.23.4
pandas:              1.5.1
pyarrow:             12.0.1
pydantic:            1.10.12
sqlalchemy:          <not installed>
xlsx2csv:            <not installed>
xlsxwriter:          <not installed>

本題

まずは以下のようなDataFrameを用意します。

import polars as pl
from datetime import datetime
df = pl.DataFrame(data={'timestamp': pl.date_range(start=datetime(2023, 1, 1), end=datetime(2024, 12, 31), eager=True)})
df.head()

shape: (5, 1)
┌─────────────────────┐
│ timestamp           │
│ ---                 │
│ datetime[μs]        │
╞═════════════════════╡
│ 2023-01-01 00:00:00 │
│ 2023-01-02 00:00:00 │
│ 2023-01-03 00:00:00 │
│ 2023-01-04 00:00:00 │
│ 2023-01-05 00:00:00 │
└─────────────────────┘

では、このtimestampに対して、datetime, pl.Duration, pl.Expr.dt.offset_byそれぞれを使って日付の計算をしていきます。

#1日減算
from datetime import timedelta
df.with_columns(
    with_tdelta=pl.col('timestamp').sub(timedelta(days=1)),
    with_dur=pl.col('timestamp').sub(pl.duration(days=1)),
    with_offset=pl.col('timestamp').dt.offset_by(by='-1d')
)

shape: (731, 4)
┌─────────────────────┬─────────────────────┬─────────────────────┬─────────────────────┐
│ timestamp           ┆ with_tdelta         ┆ with_dur            ┆ with_offset         │
│ ---                 ┆ ---                 ┆ ---                 ┆ ---                 │
│ datetime[μs]        ┆ datetime[μs]        ┆ datetime[μs]        ┆ datetime[μs]        │
╞═════════════════════╪═════════════════════╪═════════════════════╪═════════════════════╡
│ 2023-01-01 00:00:002022-12-31 00:00:002022-12-31 00:00:002022-12-31 00:00:00 │
│ 2023-01-02 00:00:002023-01-01 00:00:002023-01-01 00:00:002023-01-01 00:00:00 │
│ 2023-01-03 00:00:002023-01-02 00:00:002023-01-02 00:00:002023-01-02 00:00:00 │
│ 2023-01-04 00:00:002023-01-03 00:00:002023-01-03 00:00:002023-01-03 00:00:00 │
│ …                   ┆ …                   ┆ …                   ┆ …                   │
│ 2024-12-28 00:00:002024-12-27 00:00:002024-12-27 00:00:002024-12-27 00:00:00 │
│ 2024-12-29 00:00:002024-12-28 00:00:002024-12-28 00:00:002024-12-28 00:00:00 │
│ 2024-12-30 00:00:002024-12-29 00:00:002024-12-29 00:00:002024-12-29 00:00:00 │
│ 2024-12-31 00:00:002024-12-30 00:00:002024-12-30 00:00:002024-12-30 00:00:00 │
└─────────────────────┴─────────────────────┴─────────────────────┴─────────────────────┘

上記のように、それぞれ同じ結果が得られましたね。
ちなみにpl.Expr.dt.offset_byで減算をする場合はprefixとして-を付ける必要があることに注意してください。

では最初に問題に挙げた月単位での計算に着目してみましょう。 残念ながら、timedelta, pl.durationでは日単位でしか指定できないので、次では省略します。

df.with_columns(
    with_offset=pl.col('timestamp').dt.offset_by(by='-1mo_saturating'),
)
shape: (731, 2)
┌─────────────────────┬─────────────────────┐
│ timestamp           ┆ with_offset         │
│ ---                 ┆ ---                 │
│ datetime[μs]        ┆ datetime[μs]        │
╞═════════════════════╪═════════════════════╡
│ 2023-01-01 00:00:002022-12-01 00:00:00 │
│ 2023-01-02 00:00:002022-12-02 00:00:00 │
│ 2023-01-03 00:00:002022-12-03 00:00:00 │
│ 2023-01-04 00:00:002022-12-04 00:00:00 │
│ …                   ┆ …                   │
│ 2024-12-28 00:00:002024-11-28 00:00:00 │
│ 2024-12-29 00:00:002024-11-29 00:00:00 │
│ 2024-12-30 00:00:002024-11-30 00:00:00 │
│ 2024-12-31 00:00:002024-11-30 00:00:00 │
└─────────────────────┴─────────────────────┘

これにより、月単位での計算ができました。
ただし、出力を見ればわかる通り、11/31は存在しないため、11/30に丸め込まれている点は要注意です。

また、_saturatingはうるう年に対応させるためにつけるsuffixですが、polars v0.19以降は不要になっているそうです。

おわりに

以上、polrasのExpr.dt.offset_byを使ってdatetimeの計算をする方法について紹介しました。

最近、日本語でpolarsについて議論、情報交換するコミュニティである、polars-jaが立ち上がったこともあり、今後ML/DS界隈でのデファクトスタンダードになっていきそうなpolarsを是非皆さんも触って良い分析ライフを送っていきましょう!!