皆さんこんにちは。機械学習エンジニアのwasatingです。
今回も皆さんおなじみpolarsのtips話をしたいと思います。というかタイトルに書いてある通りなんですが…
データ分析をする際や、時系列でtrain, testを分割する際に、最新の日付から一定期間等の区切りをつけたりしたいですよね。
こんな時、日単位での指定であれば単純にdatetime
のtimedelta
を使うことが多いと思います。
しかし、月単位の場合そうもいかないため、dateutil
の relativedelta.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:00 ┆ 2022-12-31 00:00:00 ┆ 2022-12-31 00:00:00 ┆ 2022-12-31 00:00:00 │ │ 2023-01-02 00:00:00 ┆ 2023-01-01 00:00:00 ┆ 2023-01-01 00:00:00 ┆ 2023-01-01 00:00:00 │ │ 2023-01-03 00:00:00 ┆ 2023-01-02 00:00:00 ┆ 2023-01-02 00:00:00 ┆ 2023-01-02 00:00:00 │ │ 2023-01-04 00:00:00 ┆ 2023-01-03 00:00:00 ┆ 2023-01-03 00:00:00 ┆ 2023-01-03 00:00:00 │ │ … ┆ … ┆ … ┆ … │ │ 2024-12-28 00:00:00 ┆ 2024-12-27 00:00:00 ┆ 2024-12-27 00:00:00 ┆ 2024-12-27 00:00:00 │ │ 2024-12-29 00:00:00 ┆ 2024-12-28 00:00:00 ┆ 2024-12-28 00:00:00 ┆ 2024-12-28 00:00:00 │ │ 2024-12-30 00:00:00 ┆ 2024-12-29 00:00:00 ┆ 2024-12-29 00:00:00 ┆ 2024-12-29 00:00:00 │ │ 2024-12-31 00:00:00 ┆ 2024-12-30 00:00:00 ┆ 2024-12-30 00:00:00 ┆ 2024-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:00 ┆ 2022-12-01 00:00:00 │ │ 2023-01-02 00:00:00 ┆ 2022-12-02 00:00:00 │ │ 2023-01-03 00:00:00 ┆ 2022-12-03 00:00:00 │ │ 2023-01-04 00:00:00 ┆ 2022-12-04 00:00:00 │ │ … ┆ … │ │ 2024-12-28 00:00:00 ┆ 2024-11-28 00:00:00 │ │ 2024-12-29 00:00:00 ┆ 2024-11-29 00:00:00 │ │ 2024-12-30 00:00:00 ┆ 2024-11-30 00:00:00 │ │ 2024-12-31 00:00:00 ┆ 2024-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を是非皆さんも触って良い分析ライフを送っていきましょう!!