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# |
刻度 (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]
因為右手邊的運算式可以廣播到 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# |
刻度 (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]
因為右手邊的運算式可以廣播到 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
#
最後,可以像處理一般 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 期間,最初發現使用 origin
元資料來補充絕對 datetime64
的定義很有用。
但是,在對此進行更多思考之後,我們發現絕對 datetime64
與相對 timedelta64
的組合確實提供了相同的功能,同時消除了對額外 origin
元資料的需求。這就是我們從此提案中移除它的原因。
混合時間單位的運算#
每當接受具有相同單位的相同 dtype 的兩個時間值之間的運算時,也應該可以進行具有不同單位時間值的相同運算(例如,以秒為單位和以微秒為單位新增時間差),從而產生適當的時間單位。此類運算的確切語意在「使用日期/時間陣列運算」章節的「型別轉換規則」子章節中定義。
由於營業日的特殊性,混合營業日與其他時間單位的運算很可能不被允許。
為什麼沒有 quarter
時間單位?#
此提案試圖著重於最常用的時間單位集進行運算,而 quarter
可以被視為更衍生的單位。此外,quarter
的使用通常要求它可以從一年中的任何月份開始,而且由於我們不包含對時間 origin
元資料的支援,因此這在這裡是不可行的方法。最後,如果我們要新增 quarter
,那麼人們應該期望找到 biweekly
、semester
或 biyearly
,僅舉例說明其他衍生單位,我們發現這對於此提案的目的來說有點過於繁瑣。