numpy.datetime64
からdatetime.datetime
への変換について考察してみたので、メモとしてまとめておく。
ここではtimezoneを考慮していない。numpy.datetime64
のtimezoneは非推奨である為。
結論
文字列経由で変換するのが無難である。
from datetime import datetime
from numpy import datetime64
def ts2dt(ts):
'''
ts2dt(): datetime64 -> datetime
datetime64('year'), datetime64('year-month') はエラーとなる
'''
return datetime.fromisoformat(str(ts)[0:26])
_date_re=re.compile('^[0-9]{4}-[0-9]{2}-[0-9]{2}')
_month_re=re.compile('^[0-9]{4}-[0-9]{2}$')
_year_re=re.compile('^[0-9]{4}$')
def safer_ts2dt(ts):
'''
safer_ts2dt(): datetime64 -> datetime
datetime64('year'), datetime64('year-month') を変換可能
'''
ts_str=str(ts)[0:26]
if not _date_re.match(ts_str):
if _month_re.match(ts_str):
ts_str += '-01'
elif _year_re.match(ts_str):
ts_str += '-01-01'
else:
raise(ValueError(ts_str))
return datetime.fromisoformat(ts_str)
# datetime -> datetime64
def dt2ts(dt):
return np.datetime64(dt.isoformat())
#
# 実行例:
print(dt2ts(ts2dt(datetime64('2022-08-01T09:00:00.123456'))))
print(ts2dt(dt2ts(datetime(2022, 8, 1, 9, 0, 0, 123456))))
print()
print(dt2ts(safer_ts2dt(datetime64('2022-08'))))
print(dt2ts(safer_ts2dt(datetime64('2022'))))
2022-08-01T09:00:00.123456
2022-08-01 09:00:00.123456
2022-08-01T00:00:00
2022-01-01T00:00:00
各種変換方法
datetime64.astype(datetime)
で変換する方法
datetime64
の精度が micro second までなら、datetime64.astype(datetime)
でdatetime
に変換できる(リスト2: micro_ts-> micro_dt
)。
ところがdatetime64
の精度が nano second まであると変換できず、
int
になってしまう(リスト2: nano_ts->nano_dt
)。
micro_ts = datetime64('2022-08-01T09:00:00.123456')
micro_dt = micro_ts.astype(datetime)
print(micro_ts, micro_dt, type(micro_dt), sep='\n')
print()
nano_ts = datetime64('2022-08-01T09:00:00.1234560')
nano_dt = nano_ts.astype(datetime)
print(nano_ts, nano_dt, type(nano_dt), sep='\n')
2022-08-01T09:00:00.123456
2022-08-01 09:00:00.123456
<class 'datetime.datetime'>
2022-08-01T09:00:00.123456000
1659344400123456000
<class 'int'>
datetime64.astype(int)
で一旦int
にした上で、datetime.fromtimestamp()
を使う方法
datetime64.astype(int)
を使えば、精度に関わらずint
になる。これを使えば良いと思ったが...
year_ts = datetime64('2022')
print(year_ts, year_ts.astype(int))
month_ts = datetime64('2022-08')
print(month_ts, month_ts.astype(int))
day_ts = datetime64('2022-08-01')
print(day_ts, day_ts.astype(int))
hour_ts = datetime64('2022-08-01T09')
print(hour_ts, hour_ts.astype(int))
minute_ts = datetime64('2022-08-01T09:08')
print(minute_ts, minute_ts.astype(int))
second_ts = datetime64('2022-08-01T09:08:07')
print(second_ts, second_ts.astype(int))
micro_ts = datetime64('2022-08-01T09:08:07.654321')
print(micro_ts, micro_ts.astype(int))
nano_ts = datetime64('2022-08-01T09:08:07.6543210')
print(nano_ts, nano_ts.astype(int))
2022 52
2022-08 631
2022-08-01 19205
2022-08-01T09 460929
2022-08-01T09:08 27655748
2022-08-01T09:08:07 1659344887
2022-08-01T09:08:07.654321 1659344887654321
2022-08-01T09:08:07.654321000 1659344887654321000
上記の通り、datetime64
の精度によりint
に変換した場合の桁が異なる。
文字列(isoformat)を介して変換する方法
isoformatの文字列を介して変換すれば問題は起きないと考え、実行してみた。
def ts2dt(ts):
try:
return(datetime.fromisoformat(str(ts)[0:26]))
except:
return('### error ###')
year_ts = datetime64('2022')
print(year_ts, str(year_ts), ts2dt(year_ts))
month_ts = datetime64('2022-08')
print(month_ts, str(month_ts), ts2dt(month_ts))
day_ts = datetime64('2022-08-01')
print(day_ts, str(day_ts), ts2dt(day_ts))
hour_ts = datetime64('2022-08-01T09')
print(hour_ts, str(hour_ts), ts2dt(hour_ts))
minute_ts = datetime64('2022-08-01T09:08')
print(minute_ts, str(minute_ts), ts2dt(minute_ts))
second_ts = datetime64('2022-08-01T09:08:07')
print(second_ts, str(second_ts), ts2dt(second_ts))
micro_ts = datetime64('2022-08-01T09:08:07.654321')
print(micro_ts, str(micro_ts), ts2dt(micro_ts))
nano_ts = datetime64('2022-08-01T09:08:07.6543210')
print(nano_ts, str(nano_ts), ts2dt(nano_ts))
2022 2022 ### error ###
2022-08 2022-08 ### error ###
2022-08-01 2022-08-01 2022-08-01 00:00:00
2022-08-01T09 2022-08-01T09 2022-08-01 09:00:00
2022-08-01T09:00 2022-08-01T09:00 2022-08-01 09:00:00
2022-08-01T09:00:00 2022-08-01T09:00:00 2022-08-01 09:00:00
2022-08-01T09:00:00.123456 2022-08-01T09:00:00.123456 2022-08-01 09:00:00.123456
2022-08-01T09:00:00.123456000 2022-08-01T09:00:00.123456000 2022-08-01 09:00:00.123456
上記から分かる通り、datetime64
が"年"あるいは"年-月"の場合、datetime.fromisoformat()
がエラーとなってしまう。ただしそのようなdatetime64
が現れない事も多い。よってリスト1にある通り、2通りの関数とした。
リスト1のsafer_ts2dt()
では、datetime64
を文字列に変換した際に'年`, '年-月'であった場合、'年-月-日'となるように補完している。
benchmark
以下のようなスクリプトを用意し、pytest-benchmark
にてベンチマークを取ってみた。
#!/usr/bin/env python3
from datetime import datetime
from numpy import datetime64
import pytest
import re
#
# astype_ts2dt()
# datetime64.astype(datetime)で変換
def astype_ts2dt(ts=datetime64('2022-04-01T09:00:00')):
return ts.astype(datetime)
def test_astype_ts2dt(benchmark):
# テスト対象を引数として benchmark を実行する
ret = benchmark(astype_ts2dt)
# 返り値を検証する
assert type(ret) is datetime
#
# str_ts2dt()
# 文字列経由で変換
def str_ts2dt(ts=datetime64('2022-04-01T09:00:00')):
return datetime.fromisoformat(str(ts)[0:26])
def test_str_ts2dt(benchmark):
# テスト対象を引数として benchmark を実行する
ret = benchmark(str_ts2dt)
# 返り値を検証する
assert type(ret) is datetime
#
# safer_ts2dt()
# 文字列経由で変換。datetime64('年'), datetime64('年-月')対応
_date_re=re.compile('^[0-9]{4}-[0-9]{2}-[0-9]{2}')
_month_re=re.compile('^[0-9]{4}-[0-9]{2}$')
_year_re=re.compile('^[0-9]{4}$')
def safer_ts2dt(ts=datetime64('2022-04-01T09:00:00')):
ts_str=str(ts)[0:26]
if not _date_re.match(ts_str):
if _month_re.match(ts_str):
ts_str += '-01'
elif _year_re.match(ts_str):
ts_str += '-01-01'
else:
raise(ValueError(ts_str))
return datetime.fromisoformat(ts_str)
def test_safer_ts2dt(benchmark):
# テスト対象を引数として benchmark を実行する
ret = benchmark(safer_ts2dt)
# 返り値を検証する
assert type(ret) is datetime
#
# main
if __name__ == '__main__':
pytest.main(['-v', __file__])
結果は意外にも、datetime64.astype()
を用いる方が文字列経由で変換するより遅いと言う結果になった。
$ pytest --benchmark-disable-gc +[master]
========================================================================================== test session starts ===========================================================================================
platform darwin -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
benchmark: 3.4.1 (defaults: timer=time.perf_counter disable_gc=True min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /Users/akihiro/src/Project/mpl/cputemp/tests2
plugins: benchmark-3.4.1
collected 3 items
test_ts2dt.py ... [100%]
---------------------------------------------------------------------------------------- benchmark: 3 tests ----------------------------------------------------------------------------------------
Name (time in ns) Min Max Mean StdDev Median IQR Outliers OPS (Mops/s) Rounds Iterations
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_str_ts2dt 254.1500 (1.0) 3,604.2000 (1.0) 264.9232 (1.0) 16.4015 (1.0) 262.5000 (1.0) 4.1500 (1.0) 2428;17735 3.7747 (1.0) 152882 20
test_safer_ts2dt 416.0000 (1.64) 19,209.0000 (5.33) 479.8195 (1.81) 91.7862 (5.60) 459.0000 (1.75) 42.0000 (10.12) 793;793 2.0841 (0.55) 76676 1
test_astype_ts2dt 750.0000 (2.95) 45,542.0000 (12.64) 840.0710 (3.17) 410.1020 (25.00) 833.0000 (3.17) 42.0000 (10.12) 62;1482 1.1904 (0.32) 31373 1
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Legend:
Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
OPS: Operations Per Second, computed as 1 / Mean
=========================================================================================== 3 passed in 2.58s ============================================================================================
なお上記は実行例の一つに過ぎないが、何度か実行してみた範囲ではこの順序は変わらなかった。--benchmark-disable-gc
無しで実行しても、最小値(Min)と平均(Mean)は常にstr_ts2dt() < safer_ts2dt() < astype_ts2dt()
であった。
よってstr_ts2dt()
またはsafer_ts2dt()
を使う方が望ましいと言えるだろう。
参考