NEP 4 — 在NumPy中实现日期/时间类型的一项(第三份)提案#

作者:

Francesc Alted i Abad

联系方式:

faltet@pytables.com

作者:

Ivan Vilata i Balaguer

联系方式:

ivan@selidor.net

日期:

2008-07-30

状态:

推迟

执行摘要#

日期/时间标记在许多处理数据集的领域都非常方便。虽然Python有几个定义日期/时间类型的模块(如集成的datetime [1]mx.DateTime [2]),但NumPy却缺乏这些类型。

在本文档中,我们建议添加一系列日期/时间类型以填补这一空白。对所提议类型的要求有两个方面:1) 它们必须操作快速,2) 它们必须尽可能与Python自带的现有datetime模块兼容。

提议的类型#

首先,几乎不可能提出一种单一的日期/时间类型来满足所有用例的需求。因此,在考虑了各种可能性后,我们选择了两种不同的类型,即datetime64timedelta64(这些名称是初步的,可能会更改),它们可以具有不同的时间单位,以满足不同的需求。

重要提示

这里的时间单位被认为是补充日期/时间dtype的元数据,而不改变其基本类型。它提供了关于存储数字的含义,而不是它们的结构

以下是所提议类型的详细描述。

datetime64#

它表示一个绝对时间(即非相对时间)。它在内部被实现为int64类型。内部纪元是POSIX纪元(参见[3])。与POSIX一样,日期的表示不考虑闰秒。

在时间单位转换和时间表示(但不包括其他时间计算)中,值 -2**63 (0x8000000000000000) 被解释为无效或未知日期,即非时间NaT。有关更多信息,请参阅时间单位转换部分。

时间单位#

它接受不同的时间单位,每个单位都意味着不同的时间跨度。下表描述了支持的时间单位及其对应的时间跨度。

时间单位

时间跨度(年)

代码

含义

Y

[公元前9.2e18年, 公元9.2e18年]

M

[公元前7.6e17年, 公元7.6e17年]

W

[公元前1.7e17年, 公元1.7e17年]

B

工作日

[公元前3.5e16年, 公元3.5e16年]

D

[公元前2.5e16年, 公元2.5e16年]

h

小时

[公元前1.0e15年, 公元1.0e15年]

m

分钟

[公元前1.7e13年, 公元1.7e13年]

s

[公元前2.9e9年, 公元2.9e9年]

ms

毫秒

[公元前2.9e6年, 公元2.9e6年]

us

微秒

[公元前290301年, 公元294241年]

c

时钟周期 (100纳秒)

[公元前2757年, 公元31197年]

ns

纳秒

[公元1678年, 公元2262年]

因此,绝对日期的值是自内部纪元以来所选时间单位的整数个单位。在使用工作日时,周六和周日会简单地从计数中忽略(即,工作日中的第3天不是1970-01-03星期六,而是1970-01-05星期一)。

构建datetime64 dtype#

在dtype构造函数中指定时间单位的建议方式有

使用长字符串表示法

dtype('datetime64[us]')

使用短字符串表示法

dtype('M8[us]')

如果未指定时间单位,则默认值为微秒。因此,‘M8’等同于‘M8[us]’

设置和获取值#

具有此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]')

比较#

比较也将得到支持

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]

因为右侧表达式可以广播成一个包含2个元素、dtype为‘M8[Y]’的数组。

兼容性问题#

只有当时间单位为微秒时,这才能与Python datetime模块的datetime类完全兼容。对于其他时间单位,转换过程将根据需要损失精度或溢出。从datetime对象转换时,不考虑闰秒。

timedelta64#

它表示一个相对时间(即非绝对时间)。它在内部被实现为int64类型。

在时间单位转换和时间表示(但不包括其他时间计算)中,值 -2**63 (0x8000000000000000) 被解释为无效或未知时间,即非时间NaT。有关更多信息,请参阅时间单位转换部分。

时间单位#

它接受不同的时间单位,每个单位都意味着不同的时间跨度。下表描述了支持的时间单位及其对应的时间跨度。

时间单位

时间跨度

代码

含义

Y

+- 9.2e18 年

M

+- 7.6e17 年

W

+- 1.7e17 年

B

工作日

+- 3.5e16 年

D

+- 2.5e16 年

h

小时

+- 1.0e15 年

m

分钟

+- 1.7e13 年

s

+- 2.9e12 年

ms

毫秒

+- 2.9e9 年

us

微秒

+- 2.9e6 年

c

时钟周期 (100纳秒)

+- 2.9e4 年

ns

纳秒

+- 292 年

ps

皮秒

+- 106 天

fs

飞秒

+- 2.6 小时

as

阿秒

+- 9.2 秒

因此,时间差的值是所选时间单位的整数个单位

构建timedelta64 dtype#

在dtype构造函数中指定时间单位的建议方式有

使用长字符串表示法

dtype('timedelta64[us]')

使用短字符串表示法

dtype('m8[us]')

如果未指定默认值,则默认值为微秒:‘m8’等同于‘m8[us]’

设置和获取值#

具有此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]")

比较#

比较也将得到支持

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]

因为右侧表达式可以广播成一个包含3个元素、dtype为‘m8[ms]’的数组。

兼容性问题#

只有当时间单位为微秒时,这才能与Python datetime模块的timedelta类完全兼容。对于其他单位,转换过程将根据需要损失精度或溢出。

使用示例#

以下是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]')

日期/时间数组操作#

datetime64datetime64#

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

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'

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

类型转换规则#

当对两个具有不同时间单位的绝对时间进行操作时(基本上,只允许减法),结果将是引发异常。这是因为不同时间单位的范围和时间跨度可能差异很大,而且完全不清楚用户会偏好哪个时间单位。例如,这应该被允许

>>> 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?

datetime64timedelta64#

可以将相对时间添加到绝对日期或从绝对日期中减去

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'

类型转换规则#

在这种情况下,绝对时间应该优先决定结果的时间单位。这通常代表了人们想要进行的操作。例如,这将允许进行

>>> 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

timedelta64timedelta64#

最后,将可以像操作常规int64 dtype一样操作相对时间,只要结果可以转换回timedelta64

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``

类型转换规则#

当组合两个具有不同时间单位的timedelta64 dtype时,结果将是两者中较短的时间单位(“保持精度”规则)。例如

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?

为了能够执行上述操作,建议新增一个NumPy函数,名为change_timeunit。其函数签名将是

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与时间单位转换#

为了更改现有数组的日期/时间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]

不支持直接从相对dtype更改为绝对dtype或反向更改

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]

最终考量#

为什么origin元数据消失了#

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

然而,经过进一步思考,我们发现将绝对datetime64与相对timedelta64结合起来,提供了相同的功能,同时消除了对额外origin元数据的需求。这就是我们将其从本提案中移除的原因。

混合时间单位的操作#

当两个相同dtype、相同单位的时间值之间的操作被接受时,不同单位的时间值之间的相同操作也应可能(例如,将以秒计的时间差与以微秒计的时间差相加),从而产生一个合适的时间单位。这类操作的确切语义在“日期/时间数组操作”部分的“类型转换规则”小节中定义。

由于工作日的特殊性,混合工作日与其他时间单位的操作很可能不被允许。

为什么没有quarter(季度)时间单位?#

本提案旨在关注最常用的时间单位集进行操作,而quarter(季度)更多地可以被视为一个派生单位。此外,使用quarter通常要求它可以从一年中的任何月份开始,由于我们不包括对时间origin元数据的支持,这在这里不是一个可行的方向。最后,如果我们添加了quarter,那么人们可能会期望找到biweekly(双周)、semester(学期)或biyearly(两年一次)等其他派生单位的示例,我们认为这对于本提案的目的来说有点过于繁重了。