NEP 4 — NumPy 中实现某些日期/时间类型的 (第三个) 提案#
- 作者:
Francesc Alted i Abad
- 联系方式:
- 作者:
Ivan Vilata i Balaguer
- 联系方式:
- 日期:
2008-07-30
- 状态:
已延迟
执行摘要#
日期/时间标记在许多需要处理数据集的领域都非常有用。虽然 Python 有几个定义日期/时间类型的模块(例如集成的 datetime
[1] 或 mx.DateTime
[2]),但 NumPy 缺乏这些类型。
本文档中,我们提议添加一系列日期/时间类型来填补这一空白。对所提议类型的要求是双重的:1)它们必须能够快速操作,以及 2)它们必须尽可能与 Python 自带的现有 datetime
模块兼容。
提议的类型#
首先,提出一种能够满足所有用例需求的单一日期/时间类型实际上是不可能的。因此,在思考了不同的可能性之后,我们坚持使用两种不同的类型,即 datetime64
和 timedelta64
(这些名称是初步的,可以更改),它们可以具有不同的时间单位以满足不同的需求。
重要
此处的时间单位被认为是补充日期/时间 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# |
刻度(100ns) |
[公元前 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]
因为右侧表达式可以广播到 dtype 为 'M8[Y]' 的 2 个元素的数组中。
兼容性问题#
仅当使用微秒的时间单位时,此功能才能与 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# |
刻度(100ns) |
+- 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]
因为右侧表达式可以广播到 dtype 为 'm8[ms]' 的 3 个元素的数组中。
兼容性问题#
仅当使用微秒的时间单位时,此功能才能与 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
#
最后,只要结果可以转换回 timedelta64
,就可以像操作常规 int64 dtype 一样操作相对时间。
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?
为了能够执行上述操作,建议使用一个名为 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 与时间单位转换#
为了更改现有数组的日期/时间 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]
最终考虑#
为什么 origin
元数据消失了#
在NumPy列表中讨论日期/时间数据类型时,最初发现拥有一个补充绝对datetime64
定义的origin
元数据很有用。
但是,在进一步思考之后,我们发现将绝对datetime64
与相对timedelta64
结合使用,可以提供相同的功能,同时避免了对额外origin
元数据的需求。这就是我们从该提案中删除它的原因。
混合时间单位的操作#
只要接受相同数据类型且具有相同单位的两个时间值之间的操作,就应该允许具有不同单位的时间值进行相同操作(例如,将秒时间增量和微秒时间增量相加),从而产生适当的时间单位。此类操作的确切语义在“使用日期/时间数组进行操作”部分的“转换规则”子部分中定义。
由于工作日的一些特殊性,混合工作日和其他时间单位的操作很可能不被允许。
为什么没有quarter
时间单位?#
此提案试图专注于最常用的时间单位集进行操作,而quarter
可以被认为是派生单位。此外,使用quarter
通常需要它能够从一年中的任何月份开始,并且由于我们不包含对时间origin
元数据的支持,因此这里不可行。最后,如果我们要添加quarter
,那么人们应该期望找到biweekly
、semester
或biyearly
,仅仅举一些其他派生单位的例子,我们认为这对该提案的目的来说有点过于繁重。