日期时间与时间差#

从 NumPy 1.7 开始,核心数组数据类型原生支持日期时间功能。这种数据类型被称为 datetime64,之所以这样命名,是因为 Python 标准库已经使用了 datetime

Datetime64 约定与假设#

与 Python 的 date 类类似,日期以当前的公历表示,并无限期地向前和向后延伸。 [1] 与仅支持公元 1 年至公元 9999 年的 Python date 不同,datetime64 也允许公元前日期;公元前年份遵循天文纪年法约定,即公元前 2 年被编号为 -1,公元前 1 年被编号为 0,公元 1 年被编号为 1。

时间点,例如 16:23:32.234,表示为从午夜开始计算的小时、分钟、秒和秒的小数部分:即 00:00:00.000 是午夜,12:00:00.000 是中午等。每个日历日恰好有 86400 秒。这是一种“朴素”时间,没有明确的时区或特定时间尺度(UT1、UTC、TAI 等)概念。 [2]

基本日期时间#

创建日期时间最基本的方式是使用 ISO 8601 日期或日期时间格式的字符串。也可以通过相对于 Unix 纪元(1970 年 1 月 1 日 00:00:00 UTC)的偏移量整数来创建日期时间。内部存储的单位会根据字符串的形式自动选择,可以是日期单位时间单位。日期单位是年(‘Y’)、月(‘M’)、周(‘W’)和天(‘D’),而时间单位是小时(‘h’)、分钟(‘m’)、秒(‘s’)、毫秒(‘ms’)以及一些其他基于 SI 前缀的秒单位。 datetime64 数据类型也接受字符串“NAT”(任意大小写组合),表示“非时间”值。

示例

一个简单的 ISO 日期

>>> import numpy as np
>>> np.datetime64('2005-02-25')
np.datetime64('2005-02-25')

从整数和日期单位创建,距 UNIX 纪元 1 年

>>> np.datetime64(1, 'Y')
np.datetime64('1971')

使用月份作为单位

>>> np.datetime64('2005-02')
np.datetime64('2005-02')

只指定月份,但强制使用“天”单位

>>> np.datetime64('2005-02', 'D')
np.datetime64('2005-02-01')

从日期和时间

>>> np.datetime64('2005-02-25T03:30')
np.datetime64('2005-02-25T03:30')

NAT (非时间)

>>> np.datetime64('nat')
np.datetime64('NaT')

从字符串创建日期时间数组时,仍然可以通过使用通用单位的日期时间类型,自动从输入中选择单位。

示例

>>> import numpy as np
>>> np.array(['2007-07-13', '2006-01-13', '2010-08-13'], dtype='datetime64')
array(['2007-07-13', '2006-01-13', '2010-08-13'], dtype='datetime64[D]')
>>> np.array(['2001-01-01T12:00', '2002-02-03T13:56:03.172'], dtype='datetime64')
array(['2001-01-01T12:00:00.000', '2002-02-03T13:56:03.172'],
      dtype='datetime64[ms]')

日期时间数组可以通过表示 POSIX 时间戳的整数和给定单位来构造。

示例

>>> import numpy as np
>>> np.array([0, 1577836800], dtype='datetime64[s]')
array(['1970-01-01T00:00:00', '2020-01-01T00:00:00'],
      dtype='datetime64[s]')
>>> np.array([0, 1577836800000]).astype('datetime64[ms]')
array(['1970-01-01T00:00:00.000', '2020-01-01T00:00:00.000'],
      dtype='datetime64[ms]')

日期时间类型可与许多常见的 NumPy 函数配合使用,例如 arange 可用于生成日期范围。

示例

某一个月的所有日期

>>> import numpy as np
>>> np.arange('2005-02', '2005-03', dtype='datetime64[D]')
array(['2005-02-01', '2005-02-02', '2005-02-03', '2005-02-04',
       '2005-02-05', '2005-02-06', '2005-02-07', '2005-02-08',
       '2005-02-09', '2005-02-10', '2005-02-11', '2005-02-12',
       '2005-02-13', '2005-02-14', '2005-02-15', '2005-02-16',
       '2005-02-17', '2005-02-18', '2005-02-19', '2005-02-20',
       '2005-02-21', '2005-02-22', '2005-02-23', '2005-02-24',
       '2005-02-25', '2005-02-26', '2005-02-27', '2005-02-28'],
      dtype='datetime64[D]')

日期时间对象表示时间中的一个单一时刻。如果两个日期时间具有不同的单位,它们仍可能表示同一时间点,并且从月份等较大单位转换为天等较小单位被认为是“安全”的转换,因为时间点仍然被精确地表示。

示例

>>> import numpy as np
>>> np.datetime64('2005') == np.datetime64('2005-01-01')
True
>>> np.datetime64('2010-03-14T15') == np.datetime64('2010-03-14T15:00:00.00')
True

自版本 1.11.0 起已废弃:NumPy 不存储时区信息。为了向后兼容,datetime64 仍然会解析时区偏移量,并通过转换为 UTC±00:00(祖鲁时间)来处理。此行为已被废弃,并将在未来引发错误。

日期时间与时间差算术运算#

NumPy 允许两个日期时间值相减,该操作会产生一个带时间单位的数字。因为 NumPy 的核心中没有物理量系统,所以创建了 timedelta64 数据类型来补充 datetime64timedelta64 的参数是一个数字(表示单位的数量)和一个日期/时间单位,例如天 (D)、月 (M)、年 (Y)、小时 (h)、分钟 (m) 或秒 (s)。timedelta64 数据类型也接受字符串“NAT”来代替数字,表示“非时间”值。

示例

>>> import numpy as np
>>> np.timedelta64(1, 'D')
np.timedelta64(1,'D')
>>> np.timedelta64(4, 'h')
np.timedelta64(4,'h')
>>> np.timedelta64('nAt')
np.timedelta64('NaT')

日期时间与时间差共同提供简单日期时间计算的方法。

示例

>>> import numpy as np
>>> np.datetime64('2009-01-01') - np.datetime64('2008-01-01')
np.timedelta64(366,'D')
>>> np.datetime64('2009') + np.timedelta64(20, 'D')
np.datetime64('2009-01-21')
>>> np.datetime64('2011-06-15T00:00') + np.timedelta64(12, 'h')
np.datetime64('2011-06-15T12:00')
>>> np.timedelta64(1,'W') / np.timedelta64(1,'D')
7.0
>>> np.timedelta64(1,'W') % np.timedelta64(10,'D')
np.timedelta64(7,'D')
>>> np.datetime64('nat') - np.datetime64('2009-01-01')
np.timedelta64('NaT','D')
>>> np.datetime64('2009-01-01') + np.timedelta64('nat')
np.datetime64('NaT')

有两种时间差单位(“Y”年和“M”月)被特殊处理,因为它们所代表的时间量会根据使用它们的上下文而变化。虽然时间差的天单位等同于 24 小时,但月和年单位无法在不进行“不安全”转换的情况下直接转换为天。

numpy.ndarray.astype 方法可用于将月份/年份不安全地转换为天。此转换遵循从 400 年闰年周期计算平均值的方式。

示例

>>> import numpy as np
>>> a = np.timedelta64(1, 'Y')
>>> np.timedelta64(a, 'M')
numpy.timedelta64(12,'M')
>>> np.timedelta64(a, 'D')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Cannot cast NumPy timedelta64 scalar from metadata [Y] to [D] according to the rule 'same_kind'

日期时间单位#

日期时间(Datetime)和时间差(Timedelta)数据类型支持大量的时间单位,以及可以根据输入数据被强制转换为任何其他单位的通用单位。

日期时间始终以 1970-01-01T00:00 作为纪元进行存储。这意味着支持的日期始终是围绕该纪元的对称区间,在下表中称之为“时间跨度”。

跨度的长度是 64 位整数的范围乘以日期或单位的长度。例如,“W”(周)的时间跨度恰好是“D”(天)时间跨度的 7 倍,而“D”(天)的时间跨度恰好是“h”(小时)时间跨度的 24 倍。

以下是日期单位

代码

含义

时间跨度(相对)

时间跨度(绝对)

Y

+/- 9.2e18 年

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

M

+/- 7.6e17 年

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

W

+/- 1.7e17 年

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

D

+/- 2.5e16 年

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

以下是时间单位

代码

含义

时间跨度(相对)

时间跨度(绝对)

h

小时

+/- 1.0e15 年

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

m

分钟

+/- 1.7e13 年

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

s

+/- 2.9e11 年

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

ms

毫秒

+/- 2.9e8 年

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

us / μs

微秒

+/- 2.9e5 年

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

ns

纳秒

+/- 292 年

[公元 1678 年, 公元 2262 年]

ps

皮秒

+/- 106 天

[公元 1969 年, 公元 1970 年]

fs

飞秒

+/- 2.6 小时

[公元 1969 年, 公元 1970 年]

as

阿秒

+/- 9.2 秒

[公元 1969 年, 公元 1970 年]

工作日功能#

为了让日期时间能在只特定周几有效的情境中使用,NumPy 包含一组“工作日”(busday)函数。

工作日函数的默认设置是只有周一到周五(通常的工作日)是有效日期。其实现基于一个包含 7 个布尔标志的“周掩码”(weekmask)来指示有效日期;也可以自定义周掩码以指定其他有效日期集合。

“工作日”函数还可以额外检查一个“节假日”日期列表,即那些不是有效日期的特定日期。

函数 busday_offset 允许您将以工作日指定的偏移量应用于单位为“D”(天)的日期时间。

示例

>>> import numpy as np
>>> np.busday_offset('2011-06-23', 1)
np.datetime64('2011-06-24')
>>> np.busday_offset('2011-06-23', 2)
np.datetime64('2011-06-27')

当输入日期落在周末或节假日时,busday_offset 会首先应用一条规则将日期滚动到有效的工作日,然后应用偏移量。默认规则是“raise”(引发),即简单地引发异常。最常用的规则是“forward”(向前)和“backward”(向后)。

示例

>>> import numpy as np
>>> np.busday_offset('2011-06-25', 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Non-business day date in busday_offset
>>> np.busday_offset('2011-06-25', 0, roll='forward')
np.datetime64('2011-06-27')
>>> np.busday_offset('2011-06-25', 2, roll='forward')
np.datetime64('2011-06-29')
>>> np.busday_offset('2011-06-25', 0, roll='backward')
np.datetime64('2011-06-24')
>>> np.busday_offset('2011-06-25', 2, roll='backward')
np.datetime64('2011-06-28')

在某些情况下,需要适当地使用滚动规则和偏移量才能得到期望的结果。

示例

某个日期当天或之后的第一个工作日

>>> import numpy as np
>>> np.busday_offset('2011-03-20', 0, roll='forward')
np.datetime64('2011-03-21')
>>> np.busday_offset('2011-03-22', 0, roll='forward')
np.datetime64('2011-03-22')

某个日期严格之后的第一个工作日

>>> np.busday_offset('2011-03-20', 1, roll='backward')
np.datetime64('2011-03-21')
>>> np.busday_offset('2011-03-22', 1, roll='backward')
np.datetime64('2011-03-23')

该函数对于计算某些类型的日期(如节假日)也很有用。在加拿大和美国,母亲节是五月的第二个星期日,这可以通过自定义周掩码来计算。

示例

>>> import numpy as np
>>> np.busday_offset('2012-05', 1, roll='forward', weekmask='Sun')
np.datetime64('2012-05-13')

当处理大量工作日数据且需要特定周掩码和节假日配置时,为了性能考量,存在一个 busdaycalendar 对象,它以优化形式存储所需数据。

np.is_busday():#

要测试一个 datetime64 值是否是有效日期,请使用 is_busday

示例

>>> import numpy as np
>>> np.is_busday(np.datetime64('2011-07-15'))  # a Friday
True
>>> np.is_busday(np.datetime64('2011-07-16')) # a Saturday
False
>>> np.is_busday(np.datetime64('2011-07-16'), weekmask="Sat Sun")
True
>>> a = np.arange(np.datetime64('2011-07-11'), np.datetime64('2011-07-18'))
>>> np.is_busday(a)
array([ True,  True,  True,  True,  True, False, False])

np.busday_count():#

要查找指定日期范围内的有效工作日数量,请使用 busday_count

示例

>>> import numpy as np
>>> np.busday_count(np.datetime64('2011-07-11'), np.datetime64('2011-07-18'))
5
>>> np.busday_count(np.datetime64('2011-07-18'), np.datetime64('2011-07-11'))
-5

如果你有一个 datetime64 天值数组,并且想计算其中有多少是有效日期,你可以这样做:

示例

>>> import numpy as np
>>> a = np.arange(np.datetime64('2011-07-11'), np.datetime64('2011-07-18'))
>>> np.count_nonzero(np.is_busday(a))
5

自定义周掩码#

以下是几个自定义周掩码值的示例。这些示例指定了“工作日”默认的周一至周五为有效日期。

一些示例

# Positional sequences; positions are Monday through Sunday.
# Length of the sequence must be exactly 7.
weekmask = [1, 1, 1, 1, 1, 0, 0]
# list or other sequence; 0 == invalid day, 1 == valid day
weekmask = "1111100"
# string '0' == invalid day, '1' == valid day

# string abbreviations from this list: Mon Tue Wed Thu Fri Sat Sun
weekmask = "Mon Tue Wed Thu Fri"
# any amount of whitespace is allowed; abbreviations are case-sensitive.
weekmask = "MonTue Wed  Thu\tFri"

Datetime64 的局限性#

所有日期都恰好是 86400 秒长的假设使得 datetime64 在很大程度上与 Python datetime 和“POSIX 时间”语义兼容;因此,它们在 UTC 时间尺度和历史时间确定方面都存在相同的已知局限性。下面给出了一个简短的非详尽总结。

  • 无法解析在正闰秒期间发生的有效 UTC 时间戳。

    示例

    “2016-12-31 23:59:60 UTC”是一个闰秒,因此“2016-12-31 23:59:60.450 UTC”是一个有效的日期时间戳,但 datetime64 无法解析。

    >>> import numpy as np
    
    >>> np.datetime64("2016-12-31 23:59:60.450")
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ValueError: Seconds out of range in datetime string "2016-12-31 23:59:60.450"
    
  • 两个 UTC 日期之间的 Timedelta64 计算可能会因 SI 秒的整数倍而产生错误。

    示例

    计算“2021-01-01 12:56:23.423 UTC”和“2001-01-01 00:00:00.000 UTC”之间的 SI 秒数

    >>> import numpy as np
    
    >>> (
    ...   np.datetime64("2021-01-01 12:56:23.423")
    ...   - np.datetime64("2001-01-01")
    ... ) / np.timedelta64(1, "s")
    631198583.423
    

    然而,正确答案是 631198588.423 SI 秒,因为 2001 年至 2021 年间存在 5 个闰秒。

  • 过去日期的 Timedelta64 计算不会返回 SI 秒,这与预期不符。

    示例

    计算“000-01-01 UT”和“1600-01-01 UT”之间的秒数,其中 UT 是世界时

    >>> import numpy as np
    
    >>> a = np.datetime64("0000-01-01", "us")
    >>> b = np.datetime64("1600-01-01", "us")
    >>> b - a
    numpy.timedelta64(50491123200000000,'us')
    

    计算结果为 50491123200 秒,这是通过已过去的天数(584388)乘以 86400 秒得到的;这是与地球自转同步的时钟的秒数。SI 秒的精确值只能估算,例如,使用 Stephenson 等人发表于 2016 年皇家学会会刊 A 472 期中的《地球自转测量:公元前 720 年至公元 2015 年》的数据。一个合理的估算值是 50491112870 ± 90 秒,相差 10330 秒。