A proposal for implementing some date/time types in NumPy

作者:Travis Oliphant
联系:oliphant @ enthought com
日期:2009-06-09

仅从第三个提案稍作修改

作者:Francesc Alted i Abad
联系:faltet @ pytables com
作者:Ivan Vilata i Balaguer
联系:ivan @ selidor net
日期:2008-07-30

Executive summary

在许多需要处理数据集的领域中,日期/时间标记是非常方便的。虽然Python有几个模块定义日期/时间类型(如集成的datetime [1]mx.DateTime [2 ]),NumPy缺少它们。

我们建议添加日期/时间类型来填补这个空白。对所提出的类型的要求有两个方面:1)它们必须能够快速操作; 2)它们必须尽可能与现有的Python的datetime模块兼容。

Types proposed

几乎不可能提出一个单一的日期/时间类型,满足每个用例的需要。因此,我们提出两种一般的日期时间类型:1)timedelta64 - 相对时间,以及2)datetime64 - 绝对时间。

这些时间中的每一个在内部表示为64位有符号整数,其指代特定单位(小时,分钟,微秒等)。有几个预定义的单位以及创建这些单位的理性倍数的能力。还支持表示,使得存储的日期时间整数可以编码特定单元的数目以及针对每个单元跟踪的多个顺序事件。

datetime64表示绝对时间。在内部它被表示为预期时间和时期之间的时间单位数(1970年1月1日上午12:00 - POSIX时间,包括其缺少闰秒)。

Time units

64位整数时间可以表示几个不同的基本单位以及导出单位。基本单位如下表所示:

时间单位 时间跨度 时间跨度(年)
含义 相对时间 绝对时间
Y + - 9.2e18年 [公元前9.2e18,公元9.2e18]
M + - 7.6e17年 [7.6e17 BC,7.6e17 AD]
W + - 1.7e17年 [1.7e17 BC,1.7e17 AD]
B 工作日 + - 3.5e16年 [3.5e16 BC,3.5e16 AD]
D + - 2.5e16年 [2.5e16 BC,2.5e16 AD]
H 小时 + - 1.0e15年 [1.0e15 BC,1.0e15 AD]
m 分钟 + - 1.7e13年 [1.7e13 BC,1.7e13 AD]
s 第二 + - 2.9e12年 [公元前2.9e9年,公元2.9e9]
女士 毫秒 + - 2.9e9年 [公元前2.9e6,公元2.9e6]
我们 微秒 + - 2.9e6年 [290301 BC,294241 AD]
ns 纳秒 + - 292年 [1678 AD,2262 AD]
ps 皮秒 + - 106天 [1969 AD,1970 AD]
fs 飞秒 + - 2.6小时 [1969 AD,1970 AD]
阿特秒 + - 9.2秒 [1969 AD,1970 AD]

时间单位由包含上表中给定的基本类型的字符串指定

除了这些基本代码单元,用户可以创建由任何基本单元的倍数组成的导出单元:100ns,3M,15m等。

任何基本单元的有限数目的除法可以用于产生更高分辨率单位的倍数,只要除数可以均匀地划分为可用的更高分辨率单位的数目。例如:Y / 4只是短号 - >(12M)/ 4 - > 3M和Y / 4将在创建后表示为3M。将选择具有偶数除数的第一下单元(最多3个下单元)。在这种特定情况下使用以下标准化定义来找出可接受的除数

解释为
Y 12M,52W,365D
M 4W,30D,720h
W 5B,7D,168h,10080m
B 24h,1440m,86400s
D 24h,1440m,86400s
H 60m,3600s
m 60s,60000ms

s,ms,us,ns,ps,fs(分别使用下两个可用下位单元的1000和1000000)。

最后,可以创建日期时间数据类型,支持在基本单位内跟踪顺序事件:[D] // 100,[Y] // 4(注意所需的括号)。这些modulo事件单元对日期时间整数提供以下解释:

  • 除数是每个周期中的事件数
  • (整数)商是表示基本单位的整数
  • 余数是期间内的特定事件。

模事件单位可以与任何派生单位组合,但是需要括号。因此,[100ns] // 50其允许每100ns记录50个事件,使得0表示第一个100ns滴答中的第一个事件,1表示第一个100ns滴答中的第二个事件,而50表示第二个100ns滴答中的第一个事件,51表示第二个100ns tick中的第二个事件。

要完全指定日期时间类型,时间单位字符串必须与使用方括号“[]”的datetime64('M8')或timedelta64('m8')的字符串组合。因此,表示日期时间dtype的完全指定的字符串是'M8 [Y]'或(对于更复杂的示例)'M8 [7s / 9] // 5'。

如果未指定时间单位,则默认为[us]。因此,'M8'等价于'M8 [us]'(除非需要模数事件单位 - 即你不能将'M8 [us] // 5'指定为'M8 // 5'或'// 5'

datetime64

此dtype表示绝对时间(即不是相对的)。它在内部实现为int64类型。整数表示来自内部POSIX时期的单位(参见[3])。像POSIX一样,日期的表示不考虑闰秒。

In time unit conversions and time representations (but not in other time computations), the value -2**63 (0x8000000000000000) is interpreted as an invalid or unknown date, Not a Time or NaT. 有关详细信息,请参阅时间单位转换部分。

因此,绝对日期的值是从时期起所经过的所选时间单位的整数个单位。如果整数是负数,则整数的量值表示时期之前的单位数。当工作在工作日,星期六和星期日被忽略从计数(即工作日的第3天不是星期六1970-01-03,但星期一1970-01-05)。

Building a datetime64 dtype

在dtype构造函数中指定时间单位的建议方法是:

使用长字符串符号:

dtype('datetime64[us]')

使用短字符串符号:

dtype('M8[us]')

如果未指定时间单位,则默认为[us]。因此,'M8'相当于'M8 [us]'。

Setting and getting values

具有此dtype的对象可以通过一系列方法设置:

t = numpy.ones(3, dtype='M8[s]')
t[0] = 1199164176    # assign to July 30th, 2008 at 17:31:00
t[1] = datetime.datetime(2008, 7, 30, 17, 31, 01) # with datetime module
t[2] = '2008-07-30T17:31:02'    # with ISO 8601

也可以得到不同的方式:

str(t[0])  -->  2008-07-30T17:31:00
repr(t[1]) -->  datetime64(1199164177, 's')
str(t[0].item()) --> 2008-07-30 17:31:00  # datetime module object
repr(t[0].item()) --> datetime.datetime(2008, 7, 30, 17, 31)  # idem
str(t)  -->  [2008-07-30T17:31:00  2008-07-30T17:31:01  2008-07-30T17:31:02]
repr(t)  -->  array([1199164176, 1199164177, 1199164178],
                    dtype='datetime64[s]')

Comparisons

也将支持比较:

numpy.array(['1980'], 'M8[Y]') == numpy.array(['1979'], 'M8[Y]')
--> [False]

包括应用广播:

numpy.array(['1979', '1980'], 'M8[Y]') == numpy.datetime64('1980', 'Y')
--> [False, True]

以下也应该工作:

numpy.array(['1979', '1980'], 'M8[Y]') == '1980-01-01'
--> [False, True]

因为右手表达式可以广播到dtype'M8 [Y]'的2个元素的数组中。

Compatibility issues

这将与Python的datetime模块的datetime类(仅当使用微秒的时间单位)完全兼容。对于其他时间单位,转换过程将失去精度或将根据需要溢出。从/到datetime对象的转换不考虑闰秒。

timedelta64

它表示相对的时间(即不是绝对的)。它在内部实现为int64类型。

In time unit conversions and time representations (but not in other time computations), the value -2**63 (0x8000000000000000) is interpreted as an invalid or unknown time, Not a Time or NaT. 有关详细信息,请参阅时间单位转换部分。

The value of a time delta is an integer number of units of the chosen time unit.

Building a timedelta64 dtype

在dtype构造函数中指定时间单位的建议方法是:

使用长字符串符号:

dtype('timedelta64[us]')

使用短字符串符号:

dtype('m8[us]')

如果未指定时间单位,则假定缺省值为[us]。因此,'m8'和'm8 [us]'是等价的。

Setting and getting values

具有此dtype的对象可以通过一系列方法设置:

t = numpy.ones(3, dtype='m8[ms]')
t[0] = 12    # assign to 12 ms
t[1] = datetime.timedelta(0, 0, 13000)   # 13 ms
t[2] = '0:00:00.014'    # 14 ms

也可以得到不同的方式:

str(t[0])  -->  0:00:00.012
repr(t[1]) -->  timedelta64(13, 'ms')
str(t[0].item()) --> 0:00:00.012000   # datetime module object
repr(t[0].item()) --> datetime.timedelta(0, 0, 12000)  # idem
str(t)     -->  [0:00:00.012  0:00:00.014  0:00:00.014]
repr(t)    -->  array([12, 13, 14], dtype="timedelta64[ms]")

Comparisons

也将支持比较:

numpy.array([12, 13, 14], 'm8[ms]') == numpy.array([12, 13, 13], 'm8[ms]')
--> [True, True, False]

或通过应用广播:

numpy.array([12, 13, 14], 'm8[ms]') == numpy.timedelta64(13, 'ms')
--> [False, True, False]

以下应该也工作:

numpy.array([12, 13, 14], 'm8[ms]') == '0:00:00.012'
--> [True, False, False]

因为右手表达式可以广播到dtype'm8 [ms]'的3个元素的数组中。

Compatibility issues

这将与Python的datetime模块的timedelta类(仅当使用微秒的时间单位)完全兼容。对于其他单位,转换过程将失去精度或将根据需要溢出。

Examples of use

以下是使用datetime64的示例:

In [5]: numpy.datetime64(42, 'us')
Out[5]: datetime64(42, 'us')

In [6]: print numpy.datetime64(42, 'us')
1970-01-01T00:00:00.000042  # representation in ISO 8601 format

In [7]: print numpy.datetime64(367.7, 'D')  # decimal part is lost
1971-01-02  # still ISO 8601 format

In [8]: numpy.datetime('2008-07-18T12:23:18', 'm')  # from ISO 8601
Out[8]: datetime64(20273063, 'm')

In [9]: print numpy.datetime('2008-07-18T12:23:18', 'm')
Out[9]: 2008-07-18T12:23

In [10]: t = numpy.zeros(5, dtype="datetime64[ms]")

In [11]: t[0] = datetime.datetime.now()  # setter in action

In [12]: print t
[2008-07-16T13:39:25.315  1970-01-01T00:00:00.000
 1970-01-01T00:00:00.000  1970-01-01T00:00:00.000
 1970-01-01T00:00:00.000]

In [13]: repr(t)
Out[13]: array([267859210457, 0, 0, 0, 0], dtype="datetime64[ms]")

In [14]: t[0].item()     # getter in action
Out[14]: datetime.datetime(2008, 7, 16, 13, 39, 25, 315000)

In [15]: print t.dtype
dtype('datetime64[ms]')

在这里,它用于timedelta64的示例:

In [5]: numpy.timedelta64(10, 'us')
Out[5]: timedelta64(10, 'us')

In [6]: print numpy.timedelta64(10, 'us')
0:00:00.000010

In [7]: print numpy.timedelta64(3600.2, 'm')  # decimal part is lost
2 days, 12:00

In [8]: t1 = numpy.zeros(5, dtype="datetime64[ms]")

In [9]: t2 = numpy.ones(5, dtype="datetime64[ms]")

In [10]: t = t2 - t1

In [11]: t[0] = datetime.timedelta(0, 24)  # setter in action

In [12]: print t
[0:00:24.000  0:00:01.000  0:00:01.000  0:00:01.000  0:00:01.000]

In [13]: print repr(t)
Out[13]: array([24000, 1, 1, 1, 1], dtype="timedelta64[ms]")

In [14]: t[0].item()     # getter in action
Out[14]: datetime.timedelta(0, 24)

In [15]: print t.dtype
dtype('timedelta64[s]')

Operating with date/time arrays

datetime64 vs datetime64

绝对日期之间允许的唯一算术运算是减法:

In [10]: numpy.ones(3, "M8[s]") - numpy.zeros(3, "M8[s]")
Out[10]: array([1, 1, 1], dtype=timedelta64[s])

但不是其他操作:

In [11]: numpy.ones(3, "M8[s]") + numpy.zeros(3, "M8[s]")
TypeError: unsupported operand type(s) for +: 'numpy.ndarray' and 'numpy.ndarray'

允许绝对日期之间的比较。

Casting rules

当操作(基本上,只允许减法)两个具有不同单位时间的绝对时间,结果将是引发异常。这是因为不同时间单位的范围和时间跨度可以非常不同,并且根本不清楚什么时间单位对于用户是优选的。例如,应允许:

>>> numpy.ones(3, dtype="M8[Y]") - numpy.zeros(3, dtype="M8[Y]")
array([1, 1, 1], dtype="timedelta64[Y]")

但下一个不应该:

>>> numpy.ones(3, dtype="M8[Y]") - numpy.zeros(3, dtype="M8[ns]")
raise numpy.IncompatibleUnitError  # what unit to choose?

datetime64 vs timedelta64

可以从绝对日期中添加和减去相对时间:

In [10]: numpy.zeros(5, "M8[Y]") + numpy.ones(5, "m8[Y]")
Out[10]: array([1971, 1971, 1971, 1971, 1971], dtype=datetime64[Y])

In [11]: numpy.ones(5, "M8[Y]") - 2 * numpy.ones(5, "m8[Y]")
Out[11]: array([1969, 1969, 1969, 1969, 1969], dtype=datetime64[Y])

但不是其他操作:

In [12]: numpy.ones(5, "M8[Y]") * numpy.ones(5, "m8[Y]")
TypeError: unsupported operand type(s) for *: 'numpy.ndarray' and 'numpy.ndarray'

Casting rules

在这种情况下,绝对时间应该具有确定结果的时间单位的优先级。这将代表人们在大多数时候想要做的事情。例如,这将允许:

>>> series = numpy.array(['1970-01-01', '1970-02-01', '1970-09-01'],
dtype='datetime64[D]')
>>> series2 = series + numpy.timedelta(1, 'Y')  # Add 2 relative years
>>> series2
array(['1972-01-01', '1972-02-01', '1972-09-01'],
dtype='datetime64[D]')  # the 'D'ay time unit has been chosen

timedelta64 vs timedelta64

最后,只要该结果可以转换回timedelta64,就可以以相对时间来操作,就像它们是常规的int64 dtypes

In [10]: numpy.ones(3, 'm8[us]')
Out[10]: array([1, 1, 1], dtype="timedelta64[us]")

In [11]: (numpy.ones(3, 'm8[M]') + 2) ** 3
Out[11]: array([27, 27, 27], dtype="timedelta64[M]")

但:

In [12]: numpy.ones(5, 'm8') + 1j
TypeError: the result cannot be converted into a ``timedelta64``

Casting rules

当使用不同的时间单位组合两个timedelta64类型时,结果将是两者中较短的一个(“保持精度”规则)。例如:

In [10]: numpy.ones(3, 'm8[s]') + numpy.ones(3, 'm8[m]')
Out[10]: array([61, 61, 61],  dtype="timedelta64[s]")

然而,由于不可能知道相对年或相对月的确切持续时间,当这些时间单位出现在一个操作数中时,将不允许该操作:

In [11]: numpy.ones(3, 'm8[Y]') + numpy.ones(3, 'm8[D]')
raise numpy.IncompatibleUnitError  # how to convert relative years to days?

为了能够执行上述操作,提出了称为change_timeunit的新的NumPy函数。其签名将是:

change_timeunit(time_object, new_unit, reference)

其中'time_object'是其单位将被改变的时间对象,'new_unit'是期望的新时间单位,并且'reference'是绝对日期(NumPy datetime64标量),其将用于允许转换相对时间使用具有不确定数量的较小时间单位(相对年份或月份不能用天数表示)的时间单位的情况。

由此,上述操作可以如下进行:

In [10]: t_years = numpy.ones(3, 'm8[Y]')

In [11]: t_days = numpy.change_timeunit(t_years, 'D', '2001-01-01')

In [12]: t_days + numpy.ones(3, 'm8[D]')
Out[12]: array([366, 366, 366],  dtype="timedelta64[D]")

dtype vs time units conversions

为了更改现有数组的日期/时间dtype,我们建议使用.astype()方法。这将主要用于改变时间单位。

例如,对于绝对日期:

In[10]: t1 = numpy.zeros(5, dtype="datetime64[s]")

In[11]: print t1
[1970-01-01T00:00:00  1970-01-01T00:00:00  1970-01-01T00:00:00
 1970-01-01T00:00:00  1970-01-01T00:00:00]

In[12]: print t1.astype('datetime64[D]')
[1970-01-01  1970-01-01  1970-01-01  1970-01-01  1970-01-01]

相对时间:

In[10]: t1 = numpy.ones(5, dtype="timedelta64[s]")

In[11]: print t1
[1 1 1 1 1]

In[12]: print t1.astype('timedelta64[ms]')
[1000 1000 1000 1000 1000]

将不支持从/到相对于/从绝对dtypes直接更改:

In[13]: numpy.zeros(5, dtype="datetime64[s]").astype('timedelta64')
TypeError: data type cannot be converted to the desired type

工作日有一个特点,他们不包括一个连续的时间线(他们在周末有缺口)。因此,当从任何普通时间转换到工作日时,可能发生原始时间是不可表示的。在这种情况下,转换的结果是不是时间NaT):

In[10]: t1 = numpy.arange(5, dtype="datetime64[D]")

In[11]: print t1
[1970-01-01  1970-01-02  1970-01-03  1970-01-04  1970-01-05]

In[12]: t2 = t1.astype("datetime64[B]")

In[13]: print t2  # 1970 begins in a Thursday
[1970-01-01  1970-01-02  NaT  NaT  1970-01-05]

当转换回普通日子时,NaT值保持不变(这发生在所有时间单位转换中):

In[14]: t3 = t2.astype("datetime64[D]")

In[13]: print t3
[1970-01-01  1970-01-02  NaT  NaT  1970-01-05]

Necessary changes to NumPy

为了方便添加日期时间数据类型,对NumPy进行了一些更改:

Addition of metadata to dtypes

所有数据类型现在都有一个元数据字典。它可以在构建对象期间使用metadata关键字设置。

日期时间数据类型将在包含具有以下参数的4元组的元数据字典中放置单词“__frequency__”。

(基本单位字符串(str),
倍数(int),子分割数(int),事件数(int))。

因此,在元数据的“__frequency__”键中,用“D”表示天的简单时间单位将由('D',1,1,1)指定。更复杂的时间单位(如'[2W / 5] // 50')将由('D',2,5,50)表示。

“__frequency__”键为元数据保留,不能使用dtype构造函数设置。

Ufunc interface extension

具有datetime和timedelta参数的ufuncs可以在ufunc调用期间使用Python API(以引发错误)。

有一个新的ufunc C-API调用来为特定的函数指针(对于一组特定的数据类型)设置数据为传递到ufunc的数组的列表。

Array Intervace Extensions

数组接口扩展为处理datetime和timedelta typestr(包括扩展符号)。

元组是('typestr',元数据字典)。

typestr概念的这个扩展扩展到__array_interface__的descr部分。因此,描述数据格式的元组列表的元组中的第二元素本身可以是('typestr',元数据字典)的元组。

Final considerations

Why the fractional time and events: [3Y/12]//50

很难提出足够的单位来满足每一个需要。例如,在Windows上的C#中,时间的基本标记为100ns。多个基本单元易于处理。基本单位的除数更难以任意处理,但通常认为一个月为1/12年,或一天为1/7周。因此,实现了以“较大”单位的分数来指定单位的能力。

添加事件概念(// 50)以解决该NEP的商业赞助者的使用案例。这个想法是允许时间戳携带事件号和时间戳信息。余数携带事件号信息,而商携带时间戳信息。

Why the origin metadata disappeared

在讨论NumPy列表中的日期/时间dtype时,最初发现具有补充绝对datetime64的定义的origin元数据的想法是有用的。

然而,在更多地考虑这一点之后,我们发现绝对datetime64与相对timedelta64的组合确实提供了相同的功能,而不需要额外的origin元数据。这就是为什么我们从这个提案中删除它。

Operations with mixed time units

每当接受具有相同单元的相同类型的两个时间值之间的操作时,具有不同单位的时间值的相同操作应是可能的(例如,以秒为单位增加时间增量,以微秒为单位),从而产生足够的时间单位。这种操作的确切语义在“使用日期/时间数组”部分的“铸造规则”子部分中定义。

由于营业日的特殊性,最有可能不允许将营业日与其他时间单位混合的操作。

[1]http://docs.python.org/lib/module-datetime.html
[2]http://www.egenix.com/products/python/mxBase/mxDateTime
[3]http://en.wikipedia.org/wiki/Unix_time