たまに必要になると、 dateutil の relativedelta の存在をど忘れしていて何度も調べているので記事にまとめておきます。1ヶ月後の日付が欲しければ relativedelta 使え、で終わる記事なのですがそれだけだとあんまりなので、datetimeモジュールのtimedelta オブジェクトなどの紹介も合わせてまとめていきます。
さて、日付データとかを扱うプログラムを書いていると、n時間後とかn日後の時刻が必要になることはよくあります。Pythonのdatetimeモジュールでは、datetime.datetime.timedelta オブジェクトを使うことで、それを計算することができます。
参考: timedelta オブジェクト
class datetime.timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0)
クラス定義を見てわかる通り、n日後ならdaysにnを指定、n時間後ならhoursにnを指定するなどして使います。
from datetime import datetime
from datetime import timedelta
# 元の時刻
dt1 = datetime(2021, 11, 25, 8, 0, 0)
print(dt1)
# 2021-11-25 08:00:00
# 3日後を計算
dt2 = dt1 + timedelta(days=3)
print(dt2)
# 2021-11-28 08:00:00
# 10時間前を計算
dt3 = dt1 - timedelta(hours=10)
print(dt3)
# 2021-11-24 22:00:00
# 以下の書き方でも可能
dt4 = dt1 + timedelta(hours=-10)
print(dt4)
# 2021-11-24 22:00:00
簡単ですね。ちなみにこのtimedelta、datetimeオブジェクトだけでなく、PandasのDataFrameの datetime型の列やdatetime型のSeriesに対しても演算することができます。
Series でやってみます。元のデータに5時間足しています。
import pandas as pd
sr = pd.to_datetime(pd.Series(["2021-11-01", "2021-11-02", "2021-11-03"]))
print(sr)
"""
0 2021-11-01
1 2021-11-02
2 2021-11-03
dtype: datetime64[ns]
"""
print(sr + timedelta(hours=5))
"""
0 2021-11-01 05:00:00
1 2021-11-02 05:00:00
2 2021-11-03 05:00:00
dtype: datetime64[ns]
"""
weeks などの引数もあるので、3週間後、とか8週間前などの計算も可能です。ただ、僕はもうdays使って21日とか56日として計算してしまうことが多いです。
複数の引数を同時に指定することもでき、 timedelta(days=1, hours=12) とすると要するに36時間後などの計算もできます。 (単純なので実行例のコードは省略。)
前置きが長くなってきたので、続いてnヶ月後やn年後の日付を計算する方法に移ります。
先述の timedelta のclass 定義を見ていただくとわかる通り、これの引数には月や年を表す引数は用意されていません。そのため、timedelta で 1ヶ月後や1年後の日付を算出したい場合は、days=30やdays=365などで代用することになるのですが、当然月によって日数は違いますし、年についても閏年の問題があります。
そこで登場するのが、記事冒頭であげた dateutil のrelativedelta です。
ドキュメントはこちら。
参考: dateutil – powerful extensions to datetime — dateutil 2.8.2 documentation
timedelta と似たような感じの使用感で使え、nヶ月後やnヶ月前、やn年後やn年前の日付を簡単に算出できます。
from dateutil.relativedelta import relativedelta
# 元の時刻
dt1 = datetime(2021, 11, 25, 8, 0, 0)
print(dt1)
# 2021-11-25 08:00:00
# 1ヶ月後
dt2 = dt1 + relativedelta(months=1)
print(dt2)
# 2021-12-25 08:00:00
# 1年前
dt3 = dt1 - relativedelta(years=1)
print(dt3)
# 2020-11-25 08:00:00
1点注意があり、relativedelta は months や years の他に、month や year のような複数形のsがつかない引数も取ることができます。days に対する day なども同様です。このsがつかない方を指定すると、時刻を加算するのではなく、書き換えます。
relativedelta(months=1) は 1ヶ月後を計算しますが、 relativedelta(month=1)は 月を1月にするのです。間違えやすいので気をつけましょう。
# 元の時刻
dt1 = datetime(2021, 11, 25, 8, 0, 0)
print(dt1)
# 2021-11-25 08:00:00
# 2ヶ月後
dt2 = dt1 + relativedelta(months=2)
print(dt2)
# 2022-01-25 08:00:00
# 2月に書き換える
dt3 = dt1 + relativedelta(month=2)
print(dt3)
# 2021-02-25 08:00:00
以前、Presot(トレジャーデータ)で1ヶ月後の日付を算出するとき、月によって日数が違う(1月は31日、2月は28か29日など)ことによって、少し厄介な現象が起きるということを説明した記事を書いたことがあります。
参考: Prestoで1ヶ月後の時刻を求める時に気をつけること
要するにつぎの3点ですね。
- 異なる日付の±nヶ月後が同じ日付になることがある
- ある日付のnヶ月後のnヶ月前が元の日付と異なることがある
- 2つの時間のnヶ月後を計算すると時間の前後関係が入れ替わることがある
これらの現象は、relativedeltaを使っても全く同じように発生します。
dt1 = datetime(2021, 1, 29)
dt2 = datetime(2021, 1, 31)
print(dt1)
# 2021-01-29 00:00:00
print(dt2)
# 2021-01-31 00:00:00
# 1/29 と 1/31 の1ヶ月後はどちらも 2/28
print(dt1 + relativedelta(months=1))
# 2021-02-28 00:00:00
print(dt2 + relativedelta(months=1))
# 2021-02-28 00:00:00
# 1/29 の 1ヶ月後の1ヶ月前は 1/28で元に戻らない
print(dt1 + relativedelta(months=1) - relativedelta(months=1))
# 2021-01-28 00:00:00
dt3 = datetime(2021, 1, 29, 15, 0, 0)
dt4 = datetime(2021, 1, 31, 8, 0, 0)
# dt3 より dt4 の方が新しい時刻
print(dt3 < dt4)
# True
# dt3の1ヶ月後 より dt4の1ヶ月後の方が古い時刻
print(dt3 + relativedelta(months=1) < dt4 + relativedelta(months=1))
# False
月単位ほど頻繁にあることではありませんが、閏年の2/29が絡むと年単位の演算でも似たような現象が発生します。
利用するときは気をつけて使いましょう。機能や分析の要件によっては、1ヶ月後よりも30日後を使った方が安全な場面もあると思います。
relativedelta はもう1つデメリットがあります。これ、Pandas のSeriesに対しては使えないのです。さっきの timedeltaとは対照的ですね。
無理矢理実行すると例外が発生し、サポートされないって言われます。
sr = pd.to_datetime(pd.Series(["2021-11-01", "2021-11-02", "2021-11-03"]))
try:
sr + relativedelta(months=1)
except Exception as e:
print(e)
# unsupported operand type(s) for +: 'DatetimeArray' and 'relativedelta'
PandasのDataFrameの列や、Seriesに対して1ヶ月後の日付を求めたかったら、applyでlambda関数使って対応しましょう。
sr = pd.to_datetime(pd.Series(["2021-11-01", "2021-11-02", "2021-11-03"]))
print(sr.apply(lambda x: x + relativedelta(months=1)))
"""
0 2021-12-01
1 2021-12-02
2 2021-12-03
dtype: datetime64[ns]
"""