NEP 7 — 關於在 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

摘要#

在許多必須處理資料集的領域中,日期/時間標記是非常方便的工具。雖然 Python 有幾個模組定義了日期/時間類型(例如整合的 datetime [1]mx.DateTime [2]),但 NumPy 卻缺乏這些類型。

我們建議新增日期/時間類型以填補此缺口。擬議類型的要求有兩個:1) 它們必須運算快速,且 2) 它們必須盡可能與 Python 隨附的現有 datetime 模組相容。

擬議類型#

幾乎不可能提出單一日期/時間類型來滿足每個使用案例的需求。因此,我們提出兩種通用日期時間類型:1) timedelta64 – 相對時間,以及 2) datetime64 – 絕對時間。

這些時間類型在內部都表示為 64 位元帶正負號整數,指向特定單位(小時、分鐘、微秒等)。有多個預定義單位,以及建立這些單位的有理數倍數的能力。也支援一種表示法,讓儲存的日期時間整數可以編碼特定單位的數量,以及追蹤每個單位的循序事件數。

datetime64 表示絕對時間。在內部,它表示為預定時間與 epoch(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 事件單位為日期時間整數提供以下解讀

  • 除數是每個期間的事件數

  • (整數)商數是表示基本單位的整數

  • 餘數是期間中的特定事件。

Modulo 事件單位可以與任何衍生單位組合,但必須使用括號。因此 [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]'(除非需要 modulo 事件單位 – 也就是說,您不能將 'M8[us]//5' 指定為 'M8//5' 或 '//5'

datetime64#

此 dtype 表示絕對時間(即非相對時間)。它在內部實作為 int64 類型。整數表示自內部 POSIX epoch 以來的單位(請參閱 [3])。與 POSIX 類似,日期的表示法未將閏秒納入考量。

在時間單位轉換和時間表示法中(但在其他時間計算中則否),值 -2**63 (0x8000000000000000) 解讀為無效或未知日期,非時間NaT。如需更多資訊,請參閱時間單位轉換章節。

因此,絕對日期的值是自 epoch 以來經過的所選時間單位的整數。如果整數為負數,則整數的量值表示 epoch 之前的單位數。使用營業日時,星期六和星期日會直接從計數中忽略(即營業日中的第 3 天不是 1970-01-03 星期六,而是 1970-01-05 星期一)。

建置 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]

因為右手邊的運算式可以廣播到 dtype 為 'M8[Y]' 的 2 個元素陣列中。

相容性問題#

只有在使用微秒的時間單位時,才會與 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]

因為右手邊的運算式可以廣播到 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]')

使用日期/時間陣列#

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]

NumPy 的必要變更#

為了促進新增日期時間資料類型,對 NumPy 進行了一些變更

將中繼資料新增至 dtype#

所有資料類型現在都有中繼資料字典。可以在物件建構期間使用中繼資料關鍵字來設定它。

日期時間資料類型會將單字「__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(包括擴展表示法)。

此外,__array_interface__ 的 typestr 元素可以是元組,只要版本字串為 4 即可。元組為 ('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 的兩個時間值之間的運算時,也應可以進行具有不同單位之時間值的相同運算(例如,以秒為單位和以微秒為單位新增時間差),從而產生適當的時間單位。這類運算的確切語意在「使用日期/時間陣列」章節的「轉換規則」小節中定義。

由於營業日的特殊性,很可能不允許混合營業日與其他時間單位的運算。