[NumPy超入門]日付や時刻、その差分の扱いPythonデータ処理入門

NumPyには日付を扱うためのnumpy.datetime64クラスとnumpy.timedelta64クラスがあります。これら2つのクラスを使った日付の扱い方を紹介しましょう。

» 2024年01月26日 05時00分 公開
[かわさきしんじDeep Insider編集部]
「Pythonデータ処理入門」のインデックス

連載目次

連載概要

 本連載はPythonについての知識を既にある程度は身に付けている方を対象として、Pythonでデータ処理を行う上で必須ともいえるNumPyやpandas、Matplotlibなどの各種ライブラリの基本的な使い方を学んでいくものです。そして、それらの使い方をある程度覚えた上で、それらを活用してデータ処理を行うための第一歩を踏み出すことを目的としています。


 前回はカリフォルニア州の住宅価格を予想するモデルを単回帰分析という手法を用いて作成してみました。今回は話をガラリと変えて、NumPyにおける日付の扱いを紹介します。

NumPyで日付を操作する

 NumPyには日付や時刻、その差分を扱うために次の2つのクラスが用意されています。

 datetime64クラスは西暦(グレゴリオ暦)を扱うものですが、Pythonに標準で添付されるdatetimeクラス(datetimeモジュール)とは異なり、紀元前の年もサポートしています。また、datetime64クラスでは日付と時刻は全て「naive」な(=UTCやJSTなどのタイムゾーン情報は持たない)ものである点には注意してください。

日付と時刻を表すオブジェクト:datetime64クラスのインスタンス

 datetime64クラスのインスタンスは簡単に作成できます。以下に例を示します。

import numpy as np

date = np.datetime64('2024-01-27T05:00:00.123')
print(date)  # 2024-01-27T05:00:00.123
date = np.datetime64('2024-01-27')
print(date)  # 2024-01-27

日付と時刻を表すオブジェクトの作成

 最初の例では「YYYY-MM-DDThh:mm:ss.xxx」という形式で日付と時刻を記述した文字列を与えて、datetime64クラスのインスタンスを作成しています。これにより、ミリ秒単位までの時刻を含んだオブジェクトが作成されました。次の例では「YYYY-MM-DD」形式の文字列を与えています。このときには、特定の日付だけを表すインスタンスが作成されます。

 ただし、時刻だけを表すオブジェクトは作成できません。

t0 = np.datetime64('05:00:00'# ValueError:時刻だけを表すことはできない

時刻だけを表すオブジェクトは作成できない

 datetime64クラスの作成時に渡せる文字列はISO 8601形式で日付または日付と時刻を表記する必要があります。簡単にいえば、上で述べたような「YYYY-MM-DD」形式の日付(月と日は省略可能)か、日付の後に「T」または空白文字、そして「hh:mm:ss.xxx」形式の時刻(分、秒、ミリ秒などは省略可能)を付加した文字列だと考えてください。以下はその例です。

year = np.datetime64('2024'# OK
print(year)  # 2024
date_hour = np.datetime64('2024-01-26 05'# 日付と時刻は空白文字で区切ってもOK
print(date_hour)  # 2024-01-26T05

日付や時刻の一部を表すオブジェクトの作成

 最初の例では年だけを示すオブジェクトを、次の例では年月日に加えて何時かまでを示すオブジェクトを作成しています。

 あるいは、日付単位(date unit)もしくは時刻単位(time unit)を与えることで、日付や時刻をどのレベルまで表すかを指定できます。以下に例を示します。

Y = np.datetime64('2027-01-26T05:00:00.123', 'Y')
print(Y)  # 2027
M = np.datetime64('2027-01-26T05:00:00.123', 'M')
print(M)  # 2027-01
D = np.datetime64('2027-01-26T05:00:00.123', 'D')
print(D)  # 2027-01-26
h = np.datetime64('2027-01-26T05:00:00.123', 'h')
print(h)  # 2027-01-26T05
m = np.datetime64('2027-01-26T05:00:00.123', 'm')
print(m)  # 2027-01-26T05:00
s = np.datetime64('2027-01-26T05:00:00.123', 's')
print(s)  # 2027-01-26T05:00:00
ms = np.datetime64('2027-01-26T05:00:00.123', 'ms')
print(ms)  # 2027-01-26T05:00:00.123

日付単位や時刻単位を指定してインスタンスを作成

 第2引数に指定している'Y'や'M'が日付単位(date unit)、'h'や'm'が時刻単位(time unit)です。日付単位として指定できるのは以下です。

単位 意味
'Y'
'M'
'W'
'D'
指定可能な日付単位

 時刻単位として指定できるのは以下です(ミリ秒より小さな単位として'us'なども指定可能ですが、ここでは省略します)。

単位 意味
'h'
'm'
's'
'ms' ミリ秒
指定可能な時刻単位

 ここで気を付けておきたいのは、上のように単位を指定すると、インスタンス生成時に指定した日付や時刻で日付単位または時刻単位よりも小さな単位の値についての情報が失われる点です。以下のコードを見てください。

M = np.datetime64('2027-01-26T05:00:00.123', 'M')
print(M)  # 2027-01
N = np.datetime64(M, 's')
print(N)  # 2027-01-01T00:00:00

指定した単位よりも小さな単位の値は失われる

 最初のコードでは、日付と時刻を詳細に記述しながら、日付単位として'M'を指定しています。このため、作成されたのは2027年1月を表すオブジェクトになっています。そして、次の「N = np.datetime64(M, 's')」というコードでは今作成したオブジェクトを基に今度は時刻単位として's'を指定してオブジェクトを作成しています。すると、日や時刻という情報が最初にオブジェクトを作成したときに指定した「2027-01-26T05:00:00.123」とは違うものになりました。こうした特性があることは覚えておいた方がよいかもしれませんね。

 なお、ここまでに見てきたやり方だけではなく、numpy.array関数を使って、datetime64クラスのインスタンスを格納する配列を作成することも可能です。このときには、以下の例に示すようにdtypeパラメーターに格納する値がdatetime64クラスのインスタンスであることを指定します。

date = np.array(['2024-01-26'], dtype='datetime64')
print(date)  # ['2024-01-26']

sec = np.array(['2024-01-26'], dtype='datetime64[s]')
print(sec)  # ['2024-01-26T00:00:00']

numpy.array関数を使ってdatetime64クラスのインスタンスを格納する配列を作成

 最初の例では「dtype='datetime64'」と指定することで、「'2024-01-26'」という日付だけで構成される文字列から日付単位のオブジェクトが作成されています。その次の例では「dtype='datetime64[s]'」と角かっこの中に時刻単位の「s」を含めることで秒単位の情報を含んだオブジェクトが作成されているのが分かります。

 このように文字列で特定の日付や時刻を指定するだけではなく、整数値を指定することも可能です。この値はいわゆるUNIX時間と呼ばれる値で、1970年1月1日0時0分0秒を「0」とします。

date = np.array([0], dtype='datetime64[D]')
print(date)  # ['1970-01-01']
sec = np.array([0], dtype='datetime64[s]')
print(sec)  # ['1970-01-01T00:00:00']

numpy.array関数のdtypeパラメーターに指定をしてdatetime64クラスのインスタンスを作成

 このとき、'datetime64'の省略形として'M8'を指定することも可能です。

month = np.array([0], dtype='M8[M]')
print(month)  # ['1970-01']

'datetime64'の省略形として'M8'を指定することも可能

 日付単位か時刻単位のどちらを指定するかで、意味が変わってくることもあるので注意はしてください。以下に例を示します。

a = np.array([-1, 0, 1], dtype='datetime64[s]')
for item in a:
    print(f'{item.astype(int): 2}: {item}')
# 出力結果:
#-1: 1969-12-31T23:59:59
# 0: 1970-01-01T00:00:00
# 1: 1970-01-01T00:00:01

b = np.array([-1, 0, 1], dtype='datetime64[D]')
for item in b:
    print(f'{item.astype(int): 2}: {item}')
# 出力結果:
#-1: 1969-12-31
# 0: 1970-01-01
# 1: 1970-01-02

dtypeパラメーターに日付単位か時刻単位のどちらを指定するかで意味が変わる

 2つの例は共に-1、0、1を要素とするリストを使ってdatetime64クラスのインスタンスを作成していますが、numpy.array関数のdtypeパラメーターに時刻単位の'datetime64[s]'と日付単位の'datetime64[D]'を渡している点が異なります。前者では3つの整数値は1970年1月1日0時0分0秒の1秒前後の時刻になっていますが、後者では1970年1月1日の前後の日付になっています。

 NumPyが提供する他の関数でもdtypeパラメーターに指定をすれば、datetime64クラスのインスタンスを含んだ配列を作成できるものがあります。例えば、numpy.arange関数がそうです。

dates = np.arange('2024-02-01', '2024-02-04', dtype='datetime64[D]')
print(dates)  # ['2024-02-01' '2024-02-02' '2024-02-03']
print(type(dates))  # <class 'numpy.ndarray'>
print(type(dates[0]))  # <class 'numpy.datetime64'>

secs = np.arange('2024-02-01 00:00:00', '2024-02-01 00:00:05',
                 dtype='datetime64[s]')
print(secs)
# 出力結果:
#['2024-02-01T00:00:00' '2024-02-01T00:00:01' '2024-02-01T00:00:02'
# '2024-02-01T00:00:03' '2024-02-01T00:00:04']

numpy.arange関数で連続する日付や時刻を要素とする配列を作成

 最初の例では日付を表す文字列を初期値と終わり値に指定して、それらの範囲に収まる日付を作成するように「dtype='datetime64[D]'」を指定しています。次の例では、同様にして連続する時刻を作成しています。

 このようにして作成したdatetime64クラスのインスタンスは(それに意味があるのであれば)演算が可能です。例えば、ある日付から別の日付を減算すれば、その間隔(何日か)を調べられるでしょう。以下に例を示します。

date0 = np.datetime64('2024-01-27T05:00:00')
date1 = np.datetime64('2024-01-26T05:00:00')
delta = date0 - date1
print(delta)  # 86400 seconds
print(type(delta))  # <class 'numpy.timedelta64'>

date2 = date0 + delta
print(date2)  # 2024-01-28T05:00:00

datetime64クラスのオブジェクトの減算

 この例ではdatetime64クラスのインスタンスを2つ作成し、減算をしています。変数deltaにはある日付から別の日付を減算した差分が格納されています。その値はprint関数の結果を見れば分かる通り、「86400」秒です(24時間の秒数)。また、上の例を見ると、減算結果の型がtimedelta64クラスになっていることも分かります。

 時刻と時間差は加算が可能です(2024年1月27日の5時0分0秒に1日を足すとどうなるでしょう)。ということをしているのが最後の部分です。これもちゃんと計算できていますね。

時間差を表すオブジェクト:numpy.timedelta64クラスのインスタンス

 timedelta64クラスのインスタンスは時間差を表す値を与えることで作成できます。

t_delta0 = np.timedelta64(1)
print(t_delta0)  # 1 generic time units

timedelta64クラスのインスタンスの作成

 この例では整数値「1」を与えていますが、それにより作成されたtimedelta64クラスが表す時間差は「1 generic time units」(汎用《はんよう》の時刻単位1単位)となっています。これが意味するのは何でしょう。以下は今作成したtimedelta64クラスのオブジェクトを日付と時刻に加算してみるコードです。

t_delta0 = np.timedelta64(1)
print(t_delta0)  # 1 generic time units

date0 = np.datetime64('2024-01-26')
print(date0)  # 2024-01-26
print(date0 + t_delta0)  # 2024-01-27

time0 = np.datetime64('2024-01-26T05:00:00')
print(time0)  # 2024-01-26T05:00:00
print(time0 + t_delta0)  # 2024-01-26T05:00:01

汎用の時刻単位1単位を日付と時刻に加算

 最初の例ではこの値を日付に加算をしています。これにより「2024-01-26」から「2024-01-27」へと日付が1日先に進んだことが分かります。次の例ではそれを時刻に加算をしています。すると、今度は「2024-01-26T05:00:00」から「2024-01-26T05:00:01」へと時刻が1秒進んだことが分かります。特に単位を指定せずに作成したtimedelta64クラスのインスタンスを使って日付や時刻との演算を行うと、演算対象のdatetime64クラスのインスタンスが表しているものが何かによって、その結果も変わってくるということです。

 これが使いやすいこともあれば、もっとちゃんと単位を指定したいこともあるでしょう。そうしたときには、datetime64クラスのインスタンス作成と同様に、日付単位や時刻単位を指定します。

t_delta = np.timedelta64(60 * 60 * 12 + 1, 's')
print(t_delta)  # 43201 seconds

単位を指定してtimedelta64クラスのインスタンスを作成

 ここで第2引数に指定する単位は先ほどdatetime64クラスのインスタンス生成で使用したものと同様です。上の例では12時間+1秒の時刻差を表すオブジェクトを作成しています。

 これを使って、ある時刻に時間差を加算してみましょう。

dt = np.datetime64('2024-01-26 00:00:00')
print(dt + t_delta)  # 2024-01-26T12:00:01

日付と時刻を含むオブジェクトに加算

 ここでは、2024年1月26日0時0分0秒を表すdatetime64クラスのオブジェクトを作成して、それに先ほどのtimedelta64クラスのオブジェクトを加算しているので、結果は2024年1月26日12時0分1秒になりました。

 今度は日付のみを表すdatetime64クラスのオブジェクトに、同じtimedelta64クラスのオブジェクトを加算してみます。

dt = np.datetime64('2024-01-26')
print(dt + t_delta)  # 2024-01-26T12:00:01

日付のみを含むオブジェクトに加算

 結果を見ると、0時0分0秒を基点としてそこに12時間と1秒が加算されています。このように、日付や時刻に関して特に指定がない部分に対する演算は1月、1日、0時、0分、0秒などを基点として行われます。

 また、あるtimedelta64クラスのオブジェクトと日付単位や時刻単位を指定して、新たにtimedelta64クラスのオブジェクトを作成することで単位が変換できます。以下に例を示します。

d_delta = np.timedelta64(1, 'D'# 1日を表すtimedelta64オブジェクト
print(d_delta)  # 1 days
t_delta = np.timedelta64(d_delta, 'h'# それは何時間か
print(t_delta)  # 24 hours

単位の変換

 ここでは1日を表すtimedelta64クラスのオブジェクトを基に、24時間を表すオブジェクトを作成しました(つまり、単位の変換ができました)。

 ただし、1カ月が何日かはそれが何月かによって異なります。そうした値に関しては例外が発生することには注意してください。

m_delta = np.timedelta64(1, 'M')
d_delta = np.timedelta64(m_delta, 'D'# TypeError

間隔が不定なものは単位変換できない

日付や時刻でないことを表すNaT(Not a Time)

 datetime64クラスとtimedelta64クラスでは、'NaT'を与えて、インスタンスを作成することで、それが日付や時刻ではないことを表すことも可能です。

nat0 = np.datetime64('nat')
print(nat0)  # NaT
nat1 = np.timedelta64('NaT')
print(nat1)  # NaT

NaTオブジェクトの作成

 「'nat'」「'NaT'」のどちらを指定してもこのオブジェクトが作成できていることから分かるように、大文字小文字は区別されません。

 また、NaTオブジェクトを含んだ演算の結果はNaTオブジェクトになります。

d0 = np.datetime64('2024-01-26')
print(d0 + nat1)  # NaT

d_delta = np.timedelta64(1, 'D')
print(d_delta + nat1)  # NaT

NaTを含む演算の結果はNaTになる

 ところで、次のようなCSVファイルがあったとしましょう。

from pathlib import Path

print(Path('test.csv').read_text())
# 出力結果:
#date,value0,value1
#2024-01-01,5.,2.
#2024-01-02,8.,7.
#2024-01-03,1.,10.
#2024-01-04,6.,2.

データに日付を含むCSVファイル

 そのまま、読み込むともちろん例外が発生します。

data = np.loadtxt('test.csv', delimiter=',', skiprows=1# ValueError

そのまま読み込むと例外

 これはNumPyの配列は全てが同じデータ型である必要があり、第0列の日付データを浮動小数点には変換できないからです。こうしたデータを使用するにはどうすればよいか。それを次回の話題としましょう。

「Pythonデータ処理入門」のインデックス

Pythonデータ処理入門

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。