[NumPy超入門]日付や時刻、その差分の扱い:Pythonデータ処理入門
NumPyには日付を扱うためのnumpy.datetime64クラスとnumpy.timedelta64クラスがあります。これら2つのクラスを使った日付の扱い方を紹介しましょう。
連載概要
本連載はPythonについての知識を既にある程度は身に付けている方を対象として、Pythonでデータ処理を行う上で必須ともいえるNumPyやpandas、Matplotlibなどの各種ライブラリの基本的な使い方を学んでいくものです。そして、それらの使い方をある程度覚えた上で、それらを活用してデータ処理を行うための第一歩を踏み出すことを目的としています。
前回はカリフォルニア州の住宅価格を予想するモデルを単回帰分析という手法を用いて作成してみました。今回は話をガラリと変えて、NumPyにおける日付の扱いを紹介します。
NumPyで日付を操作する
NumPyには日付や時刻、その差分を扱うために次の2つのクラスが用意されています。
- numpy.datetime64クラス(以下、datetime64クラスと表記)
- numpy.timedelta64クラス(以下、timedelta64クラスと表記)
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']
最初の例では「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']
このとき、'datetime64'の省略形として'M8'を指定することも可能です。
month = np.array([0], dtype='M8[M]')
print(month) # ['1970-01']
日付単位か時刻単位のどちらを指定するかで、意味が変わってくることもあるので注意はしてください。以下に例を示します。
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
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']
最初の例では日付を表す文字列を初期値と終わり値に指定して、それらの範囲に収まる日付を作成するように「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クラスのインスタンスを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
この例では整数値「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
最初の例ではこの値を日付に加算をしています。これにより「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
ここで第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オブジェクトになります。
d0 = np.datetime64('2024-01-26')
print(d0 + nat1) # NaT
d_delta = np.timedelta64(1, 'D')
print(d_delta + nat1) # 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.
そのまま、読み込むともちろん例外が発生します。
data = np.loadtxt('test.csv', delimiter=',', skiprows=1) # ValueError
これはNumPyの配列は全てが同じデータ型である必要があり、第0列の日付データを浮動小数点には変換できないからです。こうしたデータを使用するにはどうすればよいか。それを次回の話題としましょう。
Copyright© Digital Advantage Corp. All Rights Reserved.