NEP 7 — NumPy 日期/时间类型实现提案#

作者:

Travis Oliphant

联系方式:

oliphant@enthought.com

日期:

2009-06-09

状态:

最终版

仅对 Francesc Alted i Abad 的第三个提案进行了略微修改

作者:

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 模块兼容。

建议的类型#

几乎不可能找到一种单一的日期/时间类型来满足所有用例的需求。因此,我们建议使用两种通用的日期时间类型:1) timedelta64 – 相对时间;2) datetime64 – 绝对时间。

这些时间分别在内部表示为 64 位有符号整数,这些整数引用特定的单位(小时、分钟、微秒等)。有几个预定义的单位,以及创建这些单位的有理倍数的能力。还支持一种表示法,其中存储的日期时间整数可以同时编码特定单位的数量以及为每个单位跟踪的顺序事件的数量。

datetime64 表示绝对时间。它在内部表示为目标时间和纪元(1970 年 1 月 1 日凌晨 12:00 — POSIX 时间,包括其缺乏闰秒)之间的时间单位数。

时间单位#

64 位整数时间可以表示几个不同的基本单位以及派生单位。基本单位列在下表中

时间单位

时间跨度

时间跨度(年)

代码

含义

相对时间

绝对时间

Y

+- 9.2e18 年

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

M

+- 7.6e17 年

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

W

+- 1.7e17 年

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

B

工作日

+- 3.5e16 年

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

D

+- 2.5e16 年

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

h

小时

+- 1.0e15 年

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

m

分钟

+- 1.7e13 年

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

s

+- 2.9e12 年

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

ms

毫秒

+- 2.9e9 年

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

us

微秒

+- 2.9e6 年

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

ns

纳秒

+- 292 年

[公元 1678 年,公元 2262 年]

ps

皮秒

+- 106 天

[公元 1969 年,公元 1970 年]

fs

飞秒

+- 2.6 小时

[公元 1969 年,公元 1970 年]

as

阿秒

+- 9.2 秒

[公元 1969 年,公元 1970 年]

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

除了这些基本代码单位外,用户还可以创建由任何基本单位的倍数组成的派生单位: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 刻度中的第二个事件。

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

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

datetime64#

此数据类型表示绝对时间(即非相对时间)。它在内部实现为 int64 类型。整数表示从内部 POSIX 纪元开始的单位(参见 [3])。与 POSIX 一样,日期的表示不考虑闰秒。

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

因此,绝对日期的值是自纪元以来经过的 *所选时间单位的整数单位数*。如果整数为负数,则整数的绝对值表示纪元之前的单位数。处理工作日时,星期六和星期日将被简单地忽略(即,工作日中的第 3 天不是 1970-01-03 星期六,而是 1970-01-05 星期一)。

构建 datetime64 数据类型#

建议在数据类型构造函数中指定时间单位的方法有:

使用长字符串表示法

dtype('datetime64[us]')

使用短字符串表示法

dtype('M8[us]')

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

设置和获取值#

具有此数据类型的对象可以通过一系列方式设置

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 个 'M8[Y]' 类型元素的数组中。

兼容性问题#

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

timedelta64#

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

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

时间增量的值是 *所选时间单位的整数单位数*。

构建 timedelta64 数据类型#

建议在数据类型构造函数中指定时间单位的方法有:

使用长字符串表示法

dtype('timedelta64[us]')

使用短字符串表示法

dtype('m8[us]')

如果未指定时间单位,则假定默认为 [us]。因此,'m8' 和 'm8[us]' 等效。

设置和获取值#

具有此数据类型的对象可以通过一系列方式设置

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 个 '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]')

使用日期/时间数组进行运算#

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'

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

强制转换规则#

当对具有不同时间单位的两个绝对时间进行运算(基本上,只允许减法)时,结果将引发异常。这是因为不同时间单位的范围和时间跨度可能大相径庭,并且根本不清楚用户将偏好哪种时间单位。例如,这应该是允许的

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

强制转换规则#

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

>>> 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 数据类型一样处理相对时间。

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 数据类型时,结果将是两者中较短的一个(“保持精度”规则)。例如

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]")

数据类型与时间单位转换#

为了更改现有数组的日期/时间数据类型,我们建议使用 .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]

将不支持直接从/到相对类型到/从绝对类型的更改

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]

对 NumPy 的必要更改#

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

向数据类型添加元数据#

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

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

(基本单位字符串 (str),

倍数 (int),子划分数 (int),事件数 (int))。

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

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

ufunc 接口扩展#

具有日期时间和时间增量参数的 ufunc 可以在 ufunc 调用期间使用 Python API(以引发错误)。

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

数组接口扩展#

数组接口扩展到处理日期时间和时间增量 typestr(包括扩展表示法)。

此外,__array_interface__ 的 typestr 元素可以是元组,只要版本字符串为 4。元组为('typestr',元数据字典)。

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

最终考虑#

为什么使用分数时间和事件:[3Y/12]//50#

要找到足够的单位来满足所有需求是困难的。例如,在 Windows 上的 C# 中,时间的基本单位是 100ns。基本单位的倍数很容易处理。基本单位的除数则难以任意处理,但人们通常会将一个月视为一年的 1/12,或一天视为一周的 1/7。因此,实现了根据“更大”单位的分数来指定单位的功能。

事件概念 (//50) 是为了解决该 NEP 的商业赞助商的一个用例而添加的。其思想是允许时间戳同时携带事件编号和时间戳信息。余数携带事件编号信息,而商携带时间戳信息。

为什么origin 元数据消失了#

在 NumPy 列表中讨论日期/时间数据类型时,最初发现使用补充绝对datetime64 定义的origin 元数据非常有用。

但是,经过进一步思考,我们发现绝对datetime64 与相对timedelta64 的组合提供了相同的功能,同时无需额外的origin 元数据。这就是我们将其从该提案中移除的原因。

混合时间单位的运算#

只要接受相同 dtype 和相同单位的两个时间值之间的运算,就应该允许使用不同单位的时间值进行相同的运算(例如,将秒时间增量和微秒时间增量相加),从而得到适当的时间单位。这种运算的确切语义在“使用日期/时间数组”部分的“转换规则”小节中定义。

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