LoginSignup
1
0

More than 1 year has passed since last update.

numpy.datetime64からdatetime.datetimeへの変換

Last updated at Posted at 2022-08-09

numpy.datetime64からdatetime.datetimeへの変換について考察してみたので、メモとしてまとめておく。

ここではtimezoneを考慮していない。numpy.datetime64timezoneは非推奨である為。

結論

文字列経由で変換するのが無難である。

リスト1
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'))))

実行結果1
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)。

リスト2
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')
実行結果2
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になる。これを使えば良いと思ったが...

リスト3
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))
実行結果3
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の文字列を介して変換すれば問題は起きないと考え、実行してみた。

リスト4
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通りの関数とした。
リスト1safer_ts2dt()では、datetime64を文字列に変換した際に'年`, '年-月'であった場合、'年-月-日'となるように補完している。

benchmark

以下のようなスクリプトを用意し、pytest-benchmarkにてベンチマークを取ってみた。

test_ts2dt.py
#!/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()を使う方が望ましいと言えるだろう。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0