NEP 7 — 在NumPy中实现日期/时间类型的一项提案#
- 作者:
Travis Oliphant
- 联系方式:
- 日期:
2009-06-09
- 状态:
最终
由以下人员在第三项提案基础上略作修订
- 作者:
Francesc Alted i Abad
- 联系方式:
- 作者:
Ivan Vilata i Balaguer
- 联系方式:
- 日期:
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(注意所需的方括号)。这些取模
事件单位为日期时间整数提供了以下解释
除数是每个周期中的事件数量
(整数)商是表示基本单位的整数
余数是该周期中的特定事件。
取模事件单位可以与任何派生单位结合,但需要方括号。例如 [100ns]//50,它允许每100ns记录50个事件,这样0表示第一个100ns刻度中的第一个事件,1表示第一个100ns刻度中的第二个事件,而50表示第二个100ns刻度中的第一个事件,51表示第二个100ns刻度中的第二个事件。
为了完整指定日期时间类型,时间单位字符串必须与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一样,日期的表示不考虑闰秒。
在时间单位转换和时间表示中(但在其他时间计算中不适用),值-2**63 (0x8000000000000000) 被解释为无效或未知日期,即非时间或NaT。有关更多信息,请参阅时间单位转换一节。
因此,一个绝对日期表示从纪元以来所选时间单位的整数个单位。如果整数是负数,则整数的绝对值表示在纪元之前的单位数量。在使用工作日时,周六和周日将从计数中简单忽略(即,工作日中的第3天不是1970年1月3日星期六,而是1970年1月5日星期一)。
构建datetime64
dtype#
在dtype构造函数中指定时间单位的建议方式是
使用长字符串表示法
dtype('datetime64[us]')
使用短字符串表示法
dtype('M8[us]')
如果未指定时间单位,则默认为[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。有关更多信息,请参阅时间单位转换一节。
时间差的值是所选时间单位的整数个单位。
构建timedelta64
dtype#
在dtype构造函数中指定时间单位的建议方式是
使用长字符串表示法
dtype('timedelta64[us]')
使用短字符串表示法
dtype('m8[us]')
如果未指定时间单位,则假定默认为[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]')
使用日期/时间数组进行操作#
datetime64
对比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
对比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
对比timedelta64
#
最后,将可能像操作常规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之间进行转换
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进行了一些更改
向dtype添加元数据#
所有数据类型现在都有一个元数据字典。它可以在对象构造期间使用metadata关键字进行设置。
日期时间数据类型将在元数据字典中放置“__frequency__”键,其中包含一个带有以下参数的4元组。
- (基本单位字符串(str),
倍数(int),子分区数(int),事件数(int))。
因此,像表示天数的‘D’这样的简单时间单位将通过元数据中“__frequency__”键的(‘D’, 1, 1, 1)来指定。更复杂的时间单位(例如‘[2W/5]//50’)将通过(‘D’, 2, 5, 50)来表示。
“__frequency__”键保留用于元数据,不能通过dtype构造函数设置。
Ufunc接口扩展#
具有datetime和timedelta参数的ufunc可以在ufunc调用期间使用Python API(以引发错误)。
有一个新的ufunc C-API调用,用于将特定函数指针(针对特定数据类型集)的数据设置为传递给ufunc的数组列表。
数组接口扩展#
数组接口已扩展,可以处理datetime和timedelta typestr(包括扩展表示法)。
此外,只要版本字符串为4,__array_interface__的typestr元素就可以是元组。该元组是(‘typestr’,元数据字典)。
对typestr概念的此扩展延伸到__array_interface__的descr部分。因此,描述数据格式的元组列表中的第二个元组元素本身可以是(‘typestr’,元数据字典)的元组。
最终考量#
为什么会有分数时间与事件:[3Y/12]//50#
很难想出足够的单位来满足所有需求。例如,在Windows上的C#中,基本时间刻度是100ns。基本单位的倍数很容易处理。基本单位的除数更难任意处理,但通常会将一个月视为一年的1/12,或将一天视为一周的1/7。因此,实现了以“更大”单位的分数形式指定单位的能力。
添加事件概念(//50)是为了解决本NEP一位商业赞助商的用例。其思想是允许时间戳同时携带事件编号和时间戳信息。余数携带事件编号信息,而商携带时间戳信息。
为什么origin
元数据消失了#
在NumPy邮件列表中讨论日期/时间dtype时,最初认为拥有一个补充绝对datetime64
定义的origin
元数据很有用。
然而,经过进一步思考,我们发现绝对datetime64
与相对timedelta64
的组合提供了相同的功能,同时消除了对额外origin
元数据的需求。这就是我们将其从本提案中移除的原因。
混合时间单位的操作#
只要接受相同dtype和相同单位的两个时间值之间的操作,那么不同单位的时间值之间的相同操作也应可能(例如,添加一个以秒计的时间差和一个以微秒计的时间差),从而得到一个合适的时间单位。此类操作的确切语义在“使用日期/时间数组进行操作”部分的“转换规则”小节中定义。
由于工作日的特殊性,混合工作日与其他时间单位的操作很可能不被允许。