时间序列 / 日期功能#

pandas 包含了处理各种领域时间序列数据的广泛功能和特性。通过使用 NumPy 的 datetime64timedelta64 数据类型,pandas 整合了 scikits.timeseries 等其他 Python 库的许多功能,并为操作时间序列数据创建了大量新功能。

例如,pandas 支持

从各种来源和格式解析时间序列信息

In [1]: import datetime

In [2]: dti = pd.to_datetime(
   ...:     ["1/1/2018", np.datetime64("2018-01-01"), datetime.datetime(2018, 1, 1)]
   ...: )
   ...: 

In [3]: dti
Out[3]: DatetimeIndex(['2018-01-01', '2018-01-01', '2018-01-01'], dtype='datetime64[ns]', freq=None)

生成固定频率的日期和时间跨度序列

In [4]: dti = pd.date_range("2018-01-01", periods=3, freq="h")

In [5]: dti
Out[5]: 
DatetimeIndex(['2018-01-01 00:00:00', '2018-01-01 01:00:00',
               '2018-01-01 02:00:00'],
              dtype='datetime64[ns]', freq='h')

处理和转换带时区信息的日期时间

In [6]: dti = dti.tz_localize("UTC")

In [7]: dti
Out[7]: 
DatetimeIndex(['2018-01-01 00:00:00+00:00', '2018-01-01 01:00:00+00:00',
               '2018-01-01 02:00:00+00:00'],
              dtype='datetime64[ns, UTC]', freq='h')

In [8]: dti.tz_convert("US/Pacific")
Out[8]: 
DatetimeIndex(['2017-12-31 16:00:00-08:00', '2017-12-31 17:00:00-08:00',
               '2017-12-31 18:00:00-08:00'],
              dtype='datetime64[ns, US/Pacific]', freq='h')

将时间序列重新采样或转换为特定频率

In [9]: idx = pd.date_range("2018-01-01", periods=5, freq="h")

In [10]: ts = pd.Series(range(len(idx)), index=idx)

In [11]: ts
Out[11]: 
2018-01-01 00:00:00    0
2018-01-01 01:00:00    1
2018-01-01 02:00:00    2
2018-01-01 03:00:00    3
2018-01-01 04:00:00    4
Freq: h, dtype: int64

In [12]: ts.resample("2h").mean()
Out[12]: 
2018-01-01 00:00:00    0.5
2018-01-01 02:00:00    2.5
2018-01-01 04:00:00    4.0
Freq: 2h, dtype: float64

使用绝对或相对时间增量执行日期和时间算术运算

In [13]: friday = pd.Timestamp("2018-01-05")

In [14]: friday.day_name()
Out[14]: 'Friday'

# Add 1 day
In [15]: saturday = friday + pd.Timedelta("1 day")

In [16]: saturday.day_name()
Out[16]: 'Saturday'

# Add 1 business day (Friday --> Monday)
In [17]: monday = friday + pd.offsets.BDay()

In [18]: monday.day_name()
Out[18]: 'Monday'

pandas 提供了一套相对紧凑且自包含的工具,用于执行上述任务以及更多功能。

概述#

pandas 包含 4 个与时间相关的通用概念

  1. 日期时间 (Date times): 一个带时区支持的特定日期和时间。类似于标准库中的 datetime.datetime

  2. 时间差 (Time deltas): 一个绝对的时间持续时间。类似于标准库中的 datetime.timedelta

  3. 时间跨度 (Time spans): 由时间点及其相关频率定义的时间范围。

  4. 日期偏移 (Date offsets): 一种遵循日历算术的相对时间持续时间。类似于 dateutil 包中的 dateutil.relativedelta.relativedelta

概念

标量类

数组类

pandas 数据类型

主要创建方法

日期时间

Timestamp

DatetimeIndex

datetime64[ns]datetime64[ns, tz]

to_datetimedate_range

时间差

Timedelta

TimedeltaIndex

timedelta64[ns]

to_timedeltatimedelta_range

时间跨度

Period

PeriodIndex

period[freq]

Periodperiod_range

日期偏移

DateOffset

DateOffset

对于时间序列数据,通常将时间分量表示在 SeriesDataFrame 的索引中,以便可以相对于时间元素执行操作。

In [19]: pd.Series(range(3), index=pd.date_range("2000", freq="D", periods=3))
Out[19]: 
2000-01-01    0
2000-01-02    1
2000-01-03    2
Freq: D, dtype: int64

然而,SeriesDataFrame 也可以直接支持将时间分量作为数据本身。

In [20]: pd.Series(pd.date_range("2000", freq="D", periods=3))
Out[20]: 
0   2000-01-01
1   2000-01-02
2   2000-01-03
dtype: datetime64[ns]

SeriesDataFrame 在传递给其构造函数时,对 datetimetimedeltaPeriod 数据具有扩展的数据类型支持和功能。然而,DateOffset 数据将被存储为 object 数据。

In [21]: pd.Series(pd.period_range("1/1/2011", freq="M", periods=3))
Out[21]: 
0    2011-01
1    2011-02
2    2011-03
dtype: period[M]

In [22]: pd.Series([pd.DateOffset(1), pd.DateOffset(2)])
Out[22]: 
0         <DateOffset>
1    <2 * DateOffsets>
dtype: object

In [23]: pd.Series(pd.date_range("1/1/2011", freq="ME", periods=3))
Out[23]: 
0   2011-01-31
1   2011-02-28
2   2011-03-31
dtype: datetime64[ns]

最后,pandas 将空日期时间、时间差和时间跨度表示为 NaT,这对于表示缺失或空的日期类值非常有用,其行为类似于 np.nan 对于浮点数据的行为。

In [24]: pd.Timestamp(pd.NaT)
Out[24]: NaT

In [25]: pd.Timedelta(pd.NaT)
Out[25]: NaT

In [26]: pd.Period(pd.NaT)
Out[26]: NaT

# Equality acts as np.nan would
In [27]: pd.NaT == pd.NaT
Out[27]: False

时间戳与时间跨度#

时间戳数据是时间序列数据最基本的类型,它将值与时间点相关联。对于 pandas 对象,这意味着使用时间点。

In [28]: import datetime

In [29]: pd.Timestamp(datetime.datetime(2012, 5, 1))
Out[29]: Timestamp('2012-05-01 00:00:00')

In [30]: pd.Timestamp("2012-05-01")
Out[30]: Timestamp('2012-05-01 00:00:00')

In [31]: pd.Timestamp(2012, 5, 1)
Out[31]: Timestamp('2012-05-01 00:00:00')

然而,在许多情况下,将变化变量等事物与时间跨度相关联更为自然。Period 表示的时间跨度可以显式指定,或从日期时间字符串格式中推断出来。

例如

In [32]: pd.Period("2011-01")
Out[32]: Period('2011-01', 'M')

In [33]: pd.Period("2012-05", freq="D")
Out[33]: Period('2012-05-01', 'D')

TimestampPeriod 可以作为索引。由 TimestampPeriod 组成的列表会自动强制转换为 DatetimeIndexPeriodIndex

In [34]: dates = [
   ....:     pd.Timestamp("2012-05-01"),
   ....:     pd.Timestamp("2012-05-02"),
   ....:     pd.Timestamp("2012-05-03"),
   ....: ]
   ....: 

In [35]: ts = pd.Series(np.random.randn(3), dates)

In [36]: type(ts.index)
Out[36]: pandas.core.indexes.datetimes.DatetimeIndex

In [37]: ts.index
Out[37]: DatetimeIndex(['2012-05-01', '2012-05-02', '2012-05-03'], dtype='datetime64[ns]', freq=None)

In [38]: ts
Out[38]: 
2012-05-01    0.469112
2012-05-02   -0.282863
2012-05-03   -1.509059
dtype: float64

In [39]: periods = [pd.Period("2012-01"), pd.Period("2012-02"), pd.Period("2012-03")]

In [40]: ts = pd.Series(np.random.randn(3), periods)

In [41]: type(ts.index)
Out[41]: pandas.core.indexes.period.PeriodIndex

In [42]: ts.index
Out[42]: PeriodIndex(['2012-01', '2012-02', '2012-03'], dtype='period[M]')

In [43]: ts
Out[43]: 
2012-01   -1.135632
2012-02    1.212112
2012-03   -0.173215
Freq: M, dtype: float64

pandas 允许您捕获这两种表示形式并在它们之间进行转换。在底层,pandas 使用 Timestamp 的实例表示时间戳,使用 DatetimeIndex 的实例表示时间戳序列。对于规则时间跨度,pandas 使用 Period 对象表示标量值,使用 PeriodIndex 表示跨度序列。未来版本将提供更好的支持,用于处理具有任意起点和终点的不规则间隔。

转换为时间戳#

要将 Series 或类列表对象(例如字符串、纪元或它们的混合)转换为日期类对象,可以使用 to_datetime 函数。当传递一个 Series 时,它返回一个 Series(具有相同的索引),而类列表对象则转换为 DatetimeIndex

In [44]: pd.to_datetime(pd.Series(["Jul 31, 2009", "Jan 10, 2010", None]))
Out[44]: 
0   2009-07-31
1   2010-01-10
2          NaT
dtype: datetime64[ns]

In [45]: pd.to_datetime(["2005/11/23", "2010/12/31"])
Out[45]: DatetimeIndex(['2005-11-23', '2010-12-31'], dtype='datetime64[ns]', freq=None)

如果您使用的日期格式是日优先(即欧洲风格),可以传递 dayfirst 标志

In [46]: pd.to_datetime(["04-01-2012 10:00"], dayfirst=True)
Out[46]: DatetimeIndex(['2012-01-04 10:00:00'], dtype='datetime64[ns]', freq=None)

In [47]: pd.to_datetime(["04-14-2012 10:00"], dayfirst=True)
Out[47]: DatetimeIndex(['2012-04-14 10:00:00'], dtype='datetime64[ns]', freq=None)

警告

您在上面的例子中看到 dayfirst 并不严格。如果一个日期无法按照日优先解析,它将像 dayfirstFalse 那样进行解析,并且会发出警告。

如果您将单个字符串传递给 to_datetime,它将返回一个 TimestampTimestamp 也可以接受字符串输入,但它不接受 dayfirstformat 等字符串解析选项,因此如果需要这些选项,请使用 to_datetime

In [48]: pd.to_datetime("2010/11/12")
Out[48]: Timestamp('2010-11-12 00:00:00')

In [49]: pd.Timestamp("2010/11/12")
Out[49]: Timestamp('2010-11-12 00:00:00')

您也可以直接使用 DatetimeIndex 构造函数

In [50]: pd.DatetimeIndex(["2018-01-01", "2018-01-03", "2018-01-05"])
Out[50]: DatetimeIndex(['2018-01-01', '2018-01-03', '2018-01-05'], dtype='datetime64[ns]', freq=None)

可以传入字符串 ‘infer’,以便在创建时将索引的频率设置为推断出的频率

In [51]: pd.DatetimeIndex(["2018-01-01", "2018-01-03", "2018-01-05"], freq="infer")
Out[51]: DatetimeIndex(['2018-01-01', '2018-01-03', '2018-01-05'], dtype='datetime64[ns]', freq='2D')

提供 format 参数#

除了必需的日期时间字符串外,还可以传递 format 参数以确保特定的解析。这还可能显著加快转换速度。

In [52]: pd.to_datetime("2010/11/12", format="%Y/%m/%d")
Out[52]: Timestamp('2010-11-12 00:00:00')

In [53]: pd.to_datetime("12-11-2010 00:00", format="%d-%m-%Y %H:%M")
Out[53]: Timestamp('2010-11-12 00:00:00')

有关指定 format 选项时可用的更多信息,请参见 Python 日期时间文档

从多个 DataFrame 列组装日期时间#

您还可以传递一个由整数或字符串列组成的 DataFrame,将其组装成一个由 Timestamps 组成的 Series

In [54]: df = pd.DataFrame(
   ....:     {"year": [2015, 2016], "month": [2, 3], "day": [4, 5], "hour": [2, 3]}
   ....: )
   ....: 

In [55]: pd.to_datetime(df)
Out[55]: 
0   2015-02-04 02:00:00
1   2016-03-05 03:00:00
dtype: datetime64[ns]

您可以仅传递需要组装的列。

In [56]: pd.to_datetime(df[["year", "month", "day"]])
Out[56]: 
0   2015-02-04
1   2016-03-05
dtype: datetime64[ns]

pd.to_datetime 在列名中查找日期时间分量的标准名称,包括

  • 必需: year, month, day

  • 可选: hour, minute, second, millisecond, microsecond, nanosecond

无效数据#

默认行为 errors='raise' 是在无法解析时引发错误

In [57]: pd.to_datetime(['2009/07/31', 'asd'], errors='raise')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[57], line 1
----> 1 pd.to_datetime(['2009/07/31', 'asd'], errors='raise')

File ~/work/pandas/pandas/pandas/core/tools/datetimes.py:1099, in to_datetime(arg, errors, dayfirst, yearfirst, utc, format, exact, unit, infer_datetime_format, origin, cache)
   1097         result = _convert_and_box_cache(argc, cache_array)
   1098     else:
-> 1099         result = convert_listlike(argc, format)
   1100 else:
   1101     result = convert_listlike(np.array([arg]), format)[0]

File ~/work/pandas/pandas/pandas/core/tools/datetimes.py:433, in _convert_listlike_datetimes(arg, format, name, utc, unit, errors, dayfirst, yearfirst, exact)
    431 # `format` could be inferred, or user didn't ask for mixed-format parsing.
    432 if format is not None and format != "mixed":
--> 433     return _array_strptime_with_fallback(arg, name, utc, format, exact, errors)
    435 result, tz_parsed = objects_to_datetime64(
    436     arg,
    437     dayfirst=dayfirst,
   (...)
    441     allow_object=True,
    442 )
    444 if tz_parsed is not None:
    445     # We can take a shortcut since the datetime64 numpy array
    446     # is in UTC

File ~/work/pandas/pandas/pandas/core/tools/datetimes.py:467, in _array_strptime_with_fallback(arg, name, utc, fmt, exact, errors)
    456 def _array_strptime_with_fallback(
    457     arg,
    458     name,
   (...)
    462     errors: str,
    463 ) -> Index:
    464     """
    465     Call array_strptime, with fallback behavior depending on 'errors'.
    466     """
--> 467     result, tz_out = array_strptime(arg, fmt, exact=exact, errors=errors, utc=utc)
    468     if tz_out is not None:
    469         unit = np.datetime_data(result.dtype)[0]

File strptime.pyx:501, in pandas._libs.tslibs.strptime.array_strptime()

File strptime.pyx:451, in pandas._libs.tslibs.strptime.array_strptime()

File strptime.pyx:583, in pandas._libs.tslibs.strptime._parse_with_format()

ValueError: time data "asd" doesn't match format "%Y/%m/%d", at position 1. You might want to try:
    - passing `format` if your strings have a consistent format;
    - passing `format='ISO8601'` if your strings are all ISO8601 but not necessarily in exactly the same format;
    - passing `format='mixed'`, and the format will be inferred for each element individually. You might want to use `dayfirst` alongside this.

传递 errors='coerce' 将无法解析的数据转换为 NaT (不是时间)

In [58]: pd.to_datetime(["2009/07/31", "asd"], errors="coerce")
Out[58]: DatetimeIndex(['2009-07-31', 'NaT'], dtype='datetime64[ns]', freq=None)

纪元时间戳#

pandas 支持将整数或浮点纪元时间转换为 TimestampDatetimeIndex。默认单位是纳秒,因为 Timestamp 对象在内部就是这样存储的。然而,纪元通常存储在另一个可以指定的 unit 中。这些是根据 origin 参数指定的起点计算的。

In [59]: pd.to_datetime(
   ....:     [1349720105, 1349806505, 1349892905, 1349979305, 1350065705], unit="s"
   ....: )
   ....: 
Out[59]: 
DatetimeIndex(['2012-10-08 18:15:05', '2012-10-09 18:15:05',
               '2012-10-10 18:15:05', '2012-10-11 18:15:05',
               '2012-10-12 18:15:05'],
              dtype='datetime64[ns]', freq=None)

In [60]: pd.to_datetime(
   ....:     [1349720105100, 1349720105200, 1349720105300, 1349720105400, 1349720105500],
   ....:     unit="ms",
   ....: )
   ....: 
Out[60]: 
DatetimeIndex(['2012-10-08 18:15:05.100000', '2012-10-08 18:15:05.200000',
               '2012-10-08 18:15:05.300000', '2012-10-08 18:15:05.400000',
               '2012-10-08 18:15:05.500000'],
              dtype='datetime64[ns]', freq=None)

注意

unit 参数使用的字符串与 上面 讨论的 format 参数使用的字符串不同)。可用的单位列在 pandas.to_datetime() 的文档中。

使用指定了 tz 参数的纪元时间戳构造 TimestampDatetimeIndex 将引发 ValueError。如果您在另一个时区有墙上时间的纪元,可以将这些纪元读取为时区天真的时间戳,然后本地化到相应的时区

In [61]: pd.Timestamp(1262347200000000000).tz_localize("US/Pacific")
Out[61]: Timestamp('2010-01-01 12:00:00-0800', tz='US/Pacific')

In [62]: pd.DatetimeIndex([1262347200000000000]).tz_localize("US/Pacific")
Out[62]: DatetimeIndex(['2010-01-01 12:00:00-08:00'], dtype='datetime64[ns, US/Pacific]', freq=None)

注意

纪元时间将四舍五入到最接近的纳秒。

警告

浮点纪元时间的转换可能导致不准确和意想不到的结果。Python 浮点数 在十进制中有大约 15 位精度。从浮点数转换为高精度 Timestamp 期间的四舍五入是不可避免的。获得精确精度的唯一方法是使用固定宽度类型(例如 int64)。

In [63]: pd.to_datetime([1490195805.433, 1490195805.433502912], unit="s")
Out[63]: DatetimeIndex(['2017-03-22 15:16:45.433000088', '2017-03-22 15:16:45.433502913'], dtype='datetime64[ns]', freq=None)

In [64]: pd.to_datetime(1490195805433502912, unit="ns")
Out[64]: Timestamp('2017-03-22 15:16:45.433502912')

另请参阅

使用 origin 参数

从时间戳到纪元#

要执行与上面相反的操作,即将 Timestamp 转换为“unix”纪元

In [65]: stamps = pd.date_range("2012-10-08 18:15:05", periods=4, freq="D")

In [66]: stamps
Out[66]: 
DatetimeIndex(['2012-10-08 18:15:05', '2012-10-09 18:15:05',
               '2012-10-10 18:15:05', '2012-10-11 18:15:05'],
              dtype='datetime64[ns]', freq='D')

我们减去纪元(UTC 1970 年 1 月 1 日午夜),然后向下取整除以“单位”(1 秒)。

In [67]: (stamps - pd.Timestamp("1970-01-01")) // pd.Timedelta("1s")
Out[67]: Index([1349720105, 1349806505, 1349892905, 1349979305], dtype='int64')

使用 origin 参数#

使用 origin 参数,可以指定创建 DatetimeIndex 的替代起点。例如,使用 1960-01-01 作为起始日期

In [68]: pd.to_datetime([1, 2, 3], unit="D", origin=pd.Timestamp("1960-01-01"))
Out[68]: DatetimeIndex(['1960-01-02', '1960-01-03', '1960-01-04'], dtype='datetime64[ns]', freq=None)

默认设置为 origin='unix',默认为 1970-01-01 00:00:00。通常称为“Unix 纪元”或 POSIX 时间。

In [69]: pd.to_datetime([1, 2, 3], unit="D")
Out[69]: DatetimeIndex(['1970-01-02', '1970-01-03', '1970-01-04'], dtype='datetime64[ns]', freq=None)

生成时间戳范围#

要生成带时间戳的索引,可以使用 DatetimeIndexIndex 构造函数,并传入日期时间对象列表

In [70]: dates = [
   ....:     datetime.datetime(2012, 5, 1),
   ....:     datetime.datetime(2012, 5, 2),
   ....:     datetime.datetime(2012, 5, 3),
   ....: ]
   ....: 

# Note the frequency information
In [71]: index = pd.DatetimeIndex(dates)

In [72]: index
Out[72]: DatetimeIndex(['2012-05-01', '2012-05-02', '2012-05-03'], dtype='datetime64[ns]', freq=None)

# Automatically converted to DatetimeIndex
In [73]: index = pd.Index(dates)

In [74]: index
Out[74]: DatetimeIndex(['2012-05-01', '2012-05-02', '2012-05-03'], dtype='datetime64[ns]', freq=None)

在实践中,这会变得非常繁琐,因为我们通常需要一个非常长的、包含大量时间戳的索引。如果我们需要规则频率的时间戳,可以使用 date_range()bdate_range() 函数来创建 DatetimeIndexdate_range 的默认频率是日历日,而 bdate_range 的默认频率是工作日

In [75]: start = datetime.datetime(2011, 1, 1)

In [76]: end = datetime.datetime(2012, 1, 1)

In [77]: index = pd.date_range(start, end)

In [78]: index
Out[78]: 
DatetimeIndex(['2011-01-01', '2011-01-02', '2011-01-03', '2011-01-04',
               '2011-01-05', '2011-01-06', '2011-01-07', '2011-01-08',
               '2011-01-09', '2011-01-10',
               ...
               '2011-12-23', '2011-12-24', '2011-12-25', '2011-12-26',
               '2011-12-27', '2011-12-28', '2011-12-29', '2011-12-30',
               '2011-12-31', '2012-01-01'],
              dtype='datetime64[ns]', length=366, freq='D')

In [79]: index = pd.bdate_range(start, end)

In [80]: index
Out[80]: 
DatetimeIndex(['2011-01-03', '2011-01-04', '2011-01-05', '2011-01-06',
               '2011-01-07', '2011-01-10', '2011-01-11', '2011-01-12',
               '2011-01-13', '2011-01-14',
               ...
               '2011-12-19', '2011-12-20', '2011-12-21', '2011-12-22',
               '2011-12-23', '2011-12-26', '2011-12-27', '2011-12-28',
               '2011-12-29', '2011-12-30'],
              dtype='datetime64[ns]', length=260, freq='B')

date_rangebdate_range 等便捷函数可以利用各种 频率别名

In [81]: pd.date_range(start, periods=1000, freq="ME")
Out[81]: 
DatetimeIndex(['2011-01-31', '2011-02-28', '2011-03-31', '2011-04-30',
               '2011-05-31', '2011-06-30', '2011-07-31', '2011-08-31',
               '2011-09-30', '2011-10-31',
               ...
               '2093-07-31', '2093-08-31', '2093-09-30', '2093-10-31',
               '2093-11-30', '2093-12-31', '2094-01-31', '2094-02-28',
               '2094-03-31', '2094-04-30'],
              dtype='datetime64[ns]', length=1000, freq='ME')

In [82]: pd.bdate_range(start, periods=250, freq="BQS")
Out[82]: 
DatetimeIndex(['2011-01-03', '2011-04-01', '2011-07-01', '2011-10-03',
               '2012-01-02', '2012-04-02', '2012-07-02', '2012-10-01',
               '2013-01-01', '2013-04-01',
               ...
               '2071-01-01', '2071-04-01', '2071-07-01', '2071-10-01',
               '2072-01-01', '2072-04-01', '2072-07-01', '2072-10-03',
               '2073-01-02', '2073-04-03'],
              dtype='datetime64[ns]', length=250, freq='BQS-JAN')

date_rangebdate_range 可以轻松地使用 startendperiodsfreq 等参数的各种组合来生成日期范围。起始日期和结束日期是严格包含的,因此不会生成指定范围之外的日期

In [83]: pd.date_range(start, end, freq="BME")
Out[83]: 
DatetimeIndex(['2011-01-31', '2011-02-28', '2011-03-31', '2011-04-29',
               '2011-05-31', '2011-06-30', '2011-07-29', '2011-08-31',
               '2011-09-30', '2011-10-31', '2011-11-30', '2011-12-30'],
              dtype='datetime64[ns]', freq='BME')

In [84]: pd.date_range(start, end, freq="W")
Out[84]: 
DatetimeIndex(['2011-01-02', '2011-01-09', '2011-01-16', '2011-01-23',
               '2011-01-30', '2011-02-06', '2011-02-13', '2011-02-20',
               '2011-02-27', '2011-03-06', '2011-03-13', '2011-03-20',
               '2011-03-27', '2011-04-03', '2011-04-10', '2011-04-17',
               '2011-04-24', '2011-05-01', '2011-05-08', '2011-05-15',
               '2011-05-22', '2011-05-29', '2011-06-05', '2011-06-12',
               '2011-06-19', '2011-06-26', '2011-07-03', '2011-07-10',
               '2011-07-17', '2011-07-24', '2011-07-31', '2011-08-07',
               '2011-08-14', '2011-08-21', '2011-08-28', '2011-09-04',
               '2011-09-11', '2011-09-18', '2011-09-25', '2011-10-02',
               '2011-10-09', '2011-10-16', '2011-10-23', '2011-10-30',
               '2011-11-06', '2011-11-13', '2011-11-20', '2011-11-27',
               '2011-12-04', '2011-12-11', '2011-12-18', '2011-12-25',
               '2012-01-01'],
              dtype='datetime64[ns]', freq='W-SUN')

In [85]: pd.bdate_range(end=end, periods=20)
Out[85]: 
DatetimeIndex(['2011-12-05', '2011-12-06', '2011-12-07', '2011-12-08',
               '2011-12-09', '2011-12-12', '2011-12-13', '2011-12-14',
               '2011-12-15', '2011-12-16', '2011-12-19', '2011-12-20',
               '2011-12-21', '2011-12-22', '2011-12-23', '2011-12-26',
               '2011-12-27', '2011-12-28', '2011-12-29', '2011-12-30'],
              dtype='datetime64[ns]', freq='B')

In [86]: pd.bdate_range(start=start, periods=20)
Out[86]: 
DatetimeIndex(['2011-01-03', '2011-01-04', '2011-01-05', '2011-01-06',
               '2011-01-07', '2011-01-10', '2011-01-11', '2011-01-12',
               '2011-01-13', '2011-01-14', '2011-01-17', '2011-01-18',
               '2011-01-19', '2011-01-20', '2011-01-21', '2011-01-24',
               '2011-01-25', '2011-01-26', '2011-01-27', '2011-01-28'],
              dtype='datetime64[ns]', freq='B')

指定 startendperiods 将生成一个从 startend 的均匀间隔日期范围(包含两端),结果 DatetimeIndex 中包含 periods 个元素

In [87]: pd.date_range("2018-01-01", "2018-01-05", periods=5)
Out[87]: 
DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04',
               '2018-01-05'],
              dtype='datetime64[ns]', freq=None)

In [88]: pd.date_range("2018-01-01", "2018-01-05", periods=10)
Out[88]: 
DatetimeIndex(['2018-01-01 00:00:00', '2018-01-01 10:40:00',
               '2018-01-01 21:20:00', '2018-01-02 08:00:00',
               '2018-01-02 18:40:00', '2018-01-03 05:20:00',
               '2018-01-03 16:00:00', '2018-01-04 02:40:00',
               '2018-01-04 13:20:00', '2018-01-05 00:00:00'],
              dtype='datetime64[ns]', freq=None)

自定义频率范围#

bdate_range 还可以通过使用 weekmaskholidays 参数生成自定义频率日期范围。这些参数仅在传递自定义频率字符串时使用。

In [89]: weekmask = "Mon Wed Fri"

In [90]: holidays = [datetime.datetime(2011, 1, 5), datetime.datetime(2011, 3, 14)]

In [91]: pd.bdate_range(start, end, freq="C", weekmask=weekmask, holidays=holidays)
Out[91]: 
DatetimeIndex(['2011-01-03', '2011-01-07', '2011-01-10', '2011-01-12',
               '2011-01-14', '2011-01-17', '2011-01-19', '2011-01-21',
               '2011-01-24', '2011-01-26',
               ...
               '2011-12-09', '2011-12-12', '2011-12-14', '2011-12-16',
               '2011-12-19', '2011-12-21', '2011-12-23', '2011-12-26',
               '2011-12-28', '2011-12-30'],
              dtype='datetime64[ns]', length=154, freq='C')

In [92]: pd.bdate_range(start, end, freq="CBMS", weekmask=weekmask)
Out[92]: 
DatetimeIndex(['2011-01-03', '2011-02-02', '2011-03-02', '2011-04-01',
               '2011-05-02', '2011-06-01', '2011-07-01', '2011-08-01',
               '2011-09-02', '2011-10-03', '2011-11-02', '2011-12-02'],
              dtype='datetime64[ns]', freq='CBMS')

另请参阅

自定义工作日

时间戳限制#

时间戳表示的限制取决于所选的分辨率。对于纳秒分辨率,使用 64 位整数表示的时间跨度限制在约 584 年

In [93]: pd.Timestamp.min
Out[93]: Timestamp('1677-09-21 00:12:43.145224193')

In [94]: pd.Timestamp.max
Out[94]: Timestamp('2262-04-11 23:47:16.854775807')

选择秒分辨率时,可用范围增加到 +/- 2.9e11 。不同的分辨率可以通过 as_unit 互相转换。

索引#

DatetimeIndex 的主要用途之一是作为 pandas 对象的索引。DatetimeIndex 类包含许多与时间序列相关的优化

  • 各种偏移量的大范围日期在底层预先计算并缓存,以便使后续日期范围的生成非常快速(只需获取一个切片)。

  • 使用 pandas 对象的 shift 方法进行快速移动。

  • 具有相同频率的重叠 DatetimeIndex 对象的并集运算非常快(对于快速数据对齐很重要)。

  • 通过 year, month 等属性快速访问日期字段。

  • 规则化函数如 snap 以及非常快速的 asof 逻辑。

DatetimeIndex 对象具有普通 Index 对象的所有基本功能,以及一系列用于轻松处理频率的高级时间序列特定方法。

另请参阅

重新索引方法

注意

虽然 pandas 不强制要求您拥有一个有序的日期索引,但如果日期未排序,其中一些方法的行为可能会出乎意料或不正确。

DatetimeIndex 可以像普通索引一样使用,并提供所有智能功能,如选择、切片等。

In [95]: rng = pd.date_range(start, end, freq="BME")

In [96]: ts = pd.Series(np.random.randn(len(rng)), index=rng)

In [97]: ts.index
Out[97]: 
DatetimeIndex(['2011-01-31', '2011-02-28', '2011-03-31', '2011-04-29',
               '2011-05-31', '2011-06-30', '2011-07-29', '2011-08-31',
               '2011-09-30', '2011-10-31', '2011-11-30', '2011-12-30'],
              dtype='datetime64[ns]', freq='BME')

In [98]: ts[:5].index
Out[98]: 
DatetimeIndex(['2011-01-31', '2011-02-28', '2011-03-31', '2011-04-29',
               '2011-05-31'],
              dtype='datetime64[ns]', freq='BME')

In [99]: ts[::2].index
Out[99]: 
DatetimeIndex(['2011-01-31', '2011-03-31', '2011-05-31', '2011-07-29',
               '2011-09-30', '2011-11-30'],
              dtype='datetime64[ns]', freq='2BME')

部分字符串索引#

可以将日期和能解析为时间戳的字符串作为索引参数传递

In [100]: ts["1/31/2011"]
Out[100]: 0.11920871129693428

In [101]: ts[datetime.datetime(2011, 12, 25):]
Out[101]: 
2011-12-30    0.56702
Freq: BME, dtype: float64

In [102]: ts["10/31/2011":"12/31/2011"]
Out[102]: 
2011-10-31    0.271860
2011-11-30   -0.424972
2011-12-30    0.567020
Freq: BME, dtype: float64

为了方便访问较长的时间序列,您还可以将年份或年份和月份作为字符串传递

In [103]: ts["2011"]
Out[103]: 
2011-01-31    0.119209
2011-02-28   -1.044236
2011-03-31   -0.861849
2011-04-29   -2.104569
2011-05-31   -0.494929
2011-06-30    1.071804
2011-07-29    0.721555
2011-08-31   -0.706771
2011-09-30   -1.039575
2011-10-31    0.271860
2011-11-30   -0.424972
2011-12-30    0.567020
Freq: BME, dtype: float64

In [104]: ts["2011-6"]
Out[104]: 
2011-06-30    1.071804
Freq: BME, dtype: float64

这种类型的切片也适用于具有 DatetimeIndexDataFrame。由于部分字符串选择是一种标签切片形式,因此端点将被包含在内。这包括匹配包含日期上的时间

警告

从 pandas 1.2.0 开始,使用 getitem(例如 frame[dtstring])使用单个字符串索引 DataFrame 的行已被弃用(考虑到是索引行还是选择列的歧义性),并将在未来版本中移除。使用 .loc 的等效方法(例如 frame.loc[dtstring])仍然受支持。

In [105]: dft = pd.DataFrame(
   .....:     np.random.randn(100000, 1),
   .....:     columns=["A"],
   .....:     index=pd.date_range("20130101", periods=100000, freq="min"),
   .....: )
   .....: 

In [106]: dft
Out[106]: 
                            A
2013-01-01 00:00:00  0.276232
2013-01-01 00:01:00 -1.087401
2013-01-01 00:02:00 -0.673690
2013-01-01 00:03:00  0.113648
2013-01-01 00:04:00 -1.478427
...                       ...
2013-03-11 10:35:00 -0.747967
2013-03-11 10:36:00 -0.034523
2013-03-11 10:37:00 -0.201754
2013-03-11 10:38:00 -1.509067
2013-03-11 10:39:00 -1.693043

[100000 rows x 1 columns]

In [107]: dft.loc["2013"]
Out[107]: 
                            A
2013-01-01 00:00:00  0.276232
2013-01-01 00:01:00 -1.087401
2013-01-01 00:02:00 -0.673690
2013-01-01 00:03:00  0.113648
2013-01-01 00:04:00 -1.478427
...                       ...
2013-03-11 10:35:00 -0.747967
2013-03-11 10:36:00 -0.034523
2013-03-11 10:37:00 -0.201754
2013-03-11 10:38:00 -1.509067
2013-03-11 10:39:00 -1.693043

[100000 rows x 1 columns]

这从当月的最早时间开始,并包括当月的最后日期和时间

In [108]: dft["2013-1":"2013-2"]
Out[108]: 
                            A
2013-01-01 00:00:00  0.276232
2013-01-01 00:01:00 -1.087401
2013-01-01 00:02:00 -0.673690
2013-01-01 00:03:00  0.113648
2013-01-01 00:04:00 -1.478427
...                       ...
2013-02-28 23:55:00  0.850929
2013-02-28 23:56:00  0.976712
2013-02-28 23:57:00 -2.693884
2013-02-28 23:58:00 -1.575535
2013-02-28 23:59:00 -1.573517

[84960 rows x 1 columns]

这指定了一个结束时间,该时间包括最后一天的所有时间

In [109]: dft["2013-1":"2013-2-28"]
Out[109]: 
                            A
2013-01-01 00:00:00  0.276232
2013-01-01 00:01:00 -1.087401
2013-01-01 00:02:00 -0.673690
2013-01-01 00:03:00  0.113648
2013-01-01 00:04:00 -1.478427
...                       ...
2013-02-28 23:55:00  0.850929
2013-02-28 23:56:00  0.976712
2013-02-28 23:57:00 -2.693884
2013-02-28 23:58:00 -1.575535
2013-02-28 23:59:00 -1.573517

[84960 rows x 1 columns]

这指定了一个精确的结束时间(与上面不同)

In [110]: dft["2013-1":"2013-2-28 00:00:00"]
Out[110]: 
                            A
2013-01-01 00:00:00  0.276232
2013-01-01 00:01:00 -1.087401
2013-01-01 00:02:00 -0.673690
2013-01-01 00:03:00  0.113648
2013-01-01 00:04:00 -1.478427
...                       ...
2013-02-27 23:56:00  1.197749
2013-02-27 23:57:00  0.720521
2013-02-27 23:58:00 -0.072718
2013-02-27 23:59:00 -0.681192
2013-02-28 00:00:00 -0.557501

[83521 rows x 1 columns]

由于它属于索引的一部分,我们将在包含的端点处停止

In [111]: dft["2013-1-15":"2013-1-15 12:30:00"]
Out[111]: 
                            A
2013-01-15 00:00:00 -0.984810
2013-01-15 00:01:00  0.941451
2013-01-15 00:02:00  1.559365
2013-01-15 00:03:00  1.034374
2013-01-15 00:04:00 -1.480656
...                       ...
2013-01-15 12:26:00  0.371454
2013-01-15 12:27:00 -0.930806
2013-01-15 12:28:00 -0.069177
2013-01-15 12:29:00  0.066510
2013-01-15 12:30:00 -0.003945

[751 rows x 1 columns]

具有 MultiIndexDataFrame 也支持 DatetimeIndex 部分字符串索引

In [112]: dft2 = pd.DataFrame(
   .....:     np.random.randn(20, 1),
   .....:     columns=["A"],
   .....:     index=pd.MultiIndex.from_product(
   .....:         [pd.date_range("20130101", periods=10, freq="12h"), ["a", "b"]]
   .....:     ),
   .....: )
   .....: 

In [113]: dft2
Out[113]: 
                              A
2013-01-01 00:00:00 a -0.298694
                    b  0.823553
2013-01-01 12:00:00 a  0.943285
                    b -1.479399
2013-01-02 00:00:00 a -1.643342
...                         ...
2013-01-04 12:00:00 b  0.069036
2013-01-05 00:00:00 a  0.122297
                    b  1.422060
2013-01-05 12:00:00 a  0.370079
                    b  1.016331

[20 rows x 1 columns]

In [114]: dft2.loc["2013-01-05"]
Out[114]: 
                              A
2013-01-05 00:00:00 a  0.122297
                    b  1.422060
2013-01-05 12:00:00 a  0.370079
                    b  1.016331

In [115]: idx = pd.IndexSlice

In [116]: dft2 = dft2.swaplevel(0, 1).sort_index()

In [117]: dft2.loc[idx[:, "2013-01-05"], :]
Out[117]: 
                              A
a 2013-01-05 00:00:00  0.122297
  2013-01-05 12:00:00  0.370079
b 2013-01-05 00:00:00  1.422060
  2013-01-05 12:00:00  1.016331

使用字符串索引进行切片也遵循 UTC 偏移。

In [118]: df = pd.DataFrame([0], index=pd.DatetimeIndex(["2019-01-01"], tz="US/Pacific"))

In [119]: df
Out[119]: 
                           0
2019-01-01 00:00:00-08:00  0

In [120]: df["2019-01-01 12:00:00+04:00":"2019-01-01 13:00:00+04:00"]
Out[120]: 
                           0
2019-01-01 00:00:00-08:00  0

切片与精确匹配#

用作索引参数的同一个字符串可以被视为切片或精确匹配,这取决于索引的分辨率。如果字符串的精度低于索引,它将被视为切片;否则,将被视为精确匹配。

考虑一个索引分辨率为分钟的 Series 对象

In [121]: series_minute = pd.Series(
   .....:     [1, 2, 3],
   .....:     pd.DatetimeIndex(
   .....:         ["2011-12-31 23:59:00", "2012-01-01 00:00:00", "2012-01-01 00:02:00"]
   .....:     ),
   .....: )
   .....: 

In [122]: series_minute.index.resolution
Out[122]: 'minute'

精度低于分钟的时间戳字符串会得到一个 Series 对象。

In [123]: series_minute["2011-12-31 23"]
Out[123]: 
2011-12-31 23:59:00    1
dtype: int64

具有分钟分辨率(或更精确)的时间戳字符串会得到一个标量,也就是说,它不会被转换为切片。

In [124]: series_minute["2011-12-31 23:59"]
Out[124]: 1

In [125]: series_minute["2011-12-31 23:59:00"]
Out[125]: 1

如果索引分辨率是秒,那么分钟精度的时间戳会得到一个 Series

In [126]: series_second = pd.Series(
   .....:     [1, 2, 3],
   .....:     pd.DatetimeIndex(
   .....:         ["2011-12-31 23:59:59", "2012-01-01 00:00:00", "2012-01-01 00:00:01"]
   .....:     ),
   .....: )
   .....: 

In [127]: series_second.index.resolution
Out[127]: 'second'

In [128]: series_second["2011-12-31 23:59"]
Out[128]: 
2011-12-31 23:59:59    1
dtype: int64

如果时间戳字符串被视为切片,它也可以用于使用 .loc[] 索引 DataFrame

In [129]: dft_minute = pd.DataFrame(
   .....:     {"a": [1, 2, 3], "b": [4, 5, 6]}, index=series_minute.index
   .....: )
   .....: 

In [130]: dft_minute.loc["2011-12-31 23"]
Out[130]: 
                     a  b
2011-12-31 23:59:00  1  4

警告

然而,如果字符串被视为精确匹配,DataFrame[] 中的选择将是列级别的而不是行级别的,参见 索引基础。例如,dft_minute['2011-12-31 23:59'] 将引发 KeyError,因为 '2012-12-31 23:59' 与索引具有相同的分辨率,且没有名为该字符串的列

为了始终获得无歧义的选择,无论行被视为切片还是单个选择,请使用 .loc

In [131]: dft_minute.loc["2011-12-31 23:59"]
Out[131]: 
a    1
b    4
Name: 2011-12-31 23:59:00, dtype: int64

另请注意,DatetimeIndex 的分辨率不能低于天。

In [132]: series_monthly = pd.Series(
   .....:     [1, 2, 3], pd.DatetimeIndex(["2011-12", "2012-01", "2012-02"])
   .....: )
   .....: 

In [133]: series_monthly.index.resolution
Out[133]: 'day'

In [134]: series_monthly["2011-12"]  # returns Series
Out[134]: 
2011-12-01    1
dtype: int64

精确索引#

如前一节所述,使用部分字符串索引 DatetimeIndex 取决于时期的“精度”,换句话说,即间隔相对于索引分辨率的具体程度。相比之下,使用 Timestampdatetime 对象进行索引是精确的,因为这些对象具有精确的含义。它们也遵循包含两个端点的语义。

这些 Timestampdatetime 对象具有精确的 hours, minutes,seconds,即使它们没有被显式指定(它们为 0)。

In [135]: dft[datetime.datetime(2013, 1, 1): datetime.datetime(2013, 2, 28)]
Out[135]: 
                            A
2013-01-01 00:00:00  0.276232
2013-01-01 00:01:00 -1.087401
2013-01-01 00:02:00 -0.673690
2013-01-01 00:03:00  0.113648
2013-01-01 00:04:00 -1.478427
...                       ...
2013-02-27 23:56:00  1.197749
2013-02-27 23:57:00  0.720521
2013-02-27 23:58:00 -0.072718
2013-02-27 23:59:00 -0.681192
2013-02-28 00:00:00 -0.557501

[83521 rows x 1 columns]

无默认值时。

In [136]: dft[
   .....:     datetime.datetime(2013, 1, 1, 10, 12, 0): datetime.datetime(
   .....:         2013, 2, 28, 10, 12, 0
   .....:     )
   .....: ]
   .....: 
Out[136]: 
                            A
2013-01-01 10:12:00  0.565375
2013-01-01 10:13:00  0.068184
2013-01-01 10:14:00  0.788871
2013-01-01 10:15:00 -0.280343
2013-01-01 10:16:00  0.931536
...                       ...
2013-02-28 10:08:00  0.148098
2013-02-28 10:09:00 -0.388138
2013-02-28 10:10:00  0.139348
2013-02-28 10:11:00  0.085288
2013-02-28 10:12:00  0.950146

[83521 rows x 1 columns]

截断与高级索引#

提供了一个便捷函数 truncate(),其功能类似于切片。注意,truncateDatetimeIndex 中任何未指定的日期分量假定为 0 值,这与切片返回任何部分匹配日期不同

In [137]: rng2 = pd.date_range("2011-01-01", "2012-01-01", freq="W")

In [138]: ts2 = pd.Series(np.random.randn(len(rng2)), index=rng2)

In [139]: ts2.truncate(before="2011-11", after="2011-12")
Out[139]: 
2011-11-06    0.437823
2011-11-13   -0.293083
2011-11-20   -0.059881
2011-11-27    1.252450
Freq: W-SUN, dtype: float64

In [140]: ts2["2011-11":"2011-12"]
Out[140]: 
2011-11-06    0.437823
2011-11-13   -0.293083
2011-11-20   -0.059881
2011-11-27    1.252450
2011-12-04    0.046611
2011-12-11    0.059478
2011-12-18   -0.286539
2011-12-25    0.841669
Freq: W-SUN, dtype: float64

即使是破坏 DatetimeIndex 频率规则性的复杂高级索引,结果仍将是 DatetimeIndex,尽管频率会丢失

In [141]: ts2.iloc[[0, 2, 6]].index
Out[141]: DatetimeIndex(['2011-01-02', '2011-01-16', '2011-02-13'], dtype='datetime64[ns]', freq=None)

时间/日期分量#

可以从 Timestamp 或像 DatetimeIndex 这样的时间戳集合中访问几个时间/日期属性。

属性

描述

year (年)

日期时间的年份

month (月)

日期时间的月份

day (日)

日期时间的日期

hour (时)

日期时间的小时

minute (分)

日期时间的分钟

second (秒)

日期时间的秒钟

microsecond (微秒)

日期时间的微秒

nanosecond (纳秒)

日期时间的纳秒

date (日期)

返回 datetime.date (不包含时区信息)

time (时间)

返回 datetime.time (不包含时区信息)

timetz (带时区的时间)

以本地时间返回带有时区信息的 datetime.time

dayofyear (一年中的天)

一年中的序号日

day_of_year (一年中的天)

一年中的序号日

weekofyear (一年中的周)

一年中的周序号

week (周)

一年中的周序号

dayofweek (一周中的天)

一周中的天数,周一=0,周日=6

day_of_week (一周中的天)

一周中的天数,周一=0,周日=6

weekday (工作日)

一周中的天数,周一=0,周日=6

quarter (季度)

日期的季度:一月-三月 = 1,四月-六月 = 2,等等。

days_in_month (月中天数)

日期时间所在月份的天数

is_month_start (是否月初)

逻辑值,指示是否为月份的第一天(由频率定义)

is_month_end (是否月末)

逻辑值,指示是否为月份的最后一天(由频率定义)

is_quarter_start (是否季度初)

逻辑值,指示是否为季度的第一天(由频率定义)

is_quarter_end (是否季度末)

逻辑值,指示是否为季度的最后一天(由频率定义)

is_year_start (是否年初)

逻辑值,指示是否为年份的第一天(由频率定义)

is_year_end

逻辑值,指示是否为年份的最后一天(由频率定义)

is_leap_year

逻辑值,指示日期是否属于闰年

此外,如果您有一个包含日期时间类值(datetimelike)的 Series,则可以通过 .dt 访问器来访问这些属性,具体细节请参阅 .dt 访问器 一节。

您可以从 ISO 8601 标准中获取 ISO 年的年、周和日组成部分

In [142]: idx = pd.date_range(start="2019-12-29", freq="D", periods=4)

In [143]: idx.isocalendar()
Out[143]: 
            year  week  day
2019-12-29  2019    52    7
2019-12-30  2020     1    1
2019-12-31  2020     1    2
2020-01-01  2020     1    3

In [144]: idx.to_series().dt.isocalendar()
Out[144]: 
            year  week  day
2019-12-29  2019    52    7
2019-12-30  2020     1    1
2019-12-31  2020     1    2
2020-01-01  2020     1    3

DateOffset 对象#

在前面的示例中,频率字符串(例如 'D')用于指定频率,该频率定义了

这些频率字符串映射到一个 DateOffset 对象及其子类。DateOffset 类似于 Timedelta,后者表示一段持续时间,但 DateOffset 遵循特定的日历持续时间规则。例如,Timedelta 的一天总是将 datetimes 增加 24 小时,而 DateOffset 的一天会将 datetimes 增加到下一天的同一时间,无论由于夏令时一天是 23、24 还是 25 小时。然而,所有小于或等于一小时的 DateOffset 子类(HourMinuteSecondMilliMicroNano)的行为类似于 Timedelta 并遵守绝对时间。

基本的 DateOffset 类似于 dateutil.relativedeltarelativedelta 文档),后者按指定的相应日历持续时间移动日期时间。可以使用算术运算符(+)来执行移动。

# This particular day contains a day light savings time transition
In [145]: ts = pd.Timestamp("2016-10-30 00:00:00", tz="Europe/Helsinki")

# Respects absolute time
In [146]: ts + pd.Timedelta(days=1)
Out[146]: Timestamp('2016-10-30 23:00:00+0200', tz='Europe/Helsinki')

# Respects calendar time
In [147]: ts + pd.DateOffset(days=1)
Out[147]: Timestamp('2016-10-31 00:00:00+0200', tz='Europe/Helsinki')

In [148]: friday = pd.Timestamp("2018-01-05")

In [149]: friday.day_name()
Out[149]: 'Friday'

# Add 2 business days (Friday --> Tuesday)
In [150]: two_business_days = 2 * pd.offsets.BDay()

In [151]: friday + two_business_days
Out[151]: Timestamp('2018-01-09 00:00:00')

In [152]: (friday + two_business_days).day_name()
Out[152]: 'Tuesday'

大多数 DateOffsets 都有相关的频率字符串或偏移别名,可以作为 freq 关键字参数传入。可用的日期偏移量和相关的频率字符串如下所示

日期偏移量

频率字符串

描述

DateOffset

通用偏移类,默认为绝对 24 小时

BDayBusinessDay

'B'

工作日(周一至周五)

CDayCustomBusinessDay

'C'

自定义工作日

Week

'W'

一周,可选择锚定到一周中的某一天

WeekOfMonth

'WOM'

每月第 y 周的第 x 天

LastWeekOfMonth

'LWOM'

每月最后一周的第 x 天

MonthEnd

'ME'

日历月末

MonthBegin

'MS'

日历月初

BMonthEndBusinessMonthEnd

'BME'

工作月末

BMonthBeginBusinessMonthBegin

'BMS'

工作月初

CBMonthEndCustomBusinessMonthEnd

'CBME'

自定义工作月末

CBMonthBeginCustomBusinessMonthBegin

'CBMS'

自定义工作月初

SemiMonthEnd

'SME'

每月的 15 日(或其他指定日期)和日历月末

SemiMonthBegin

'SMS'

每月的 15 日(或其他指定日期)和日历月初

QuarterEnd

'QE'

日历季度末

QuarterBegin

'QS'

日历季度初

BQuarterEnd

'BQE

工作季度末

BQuarterBegin

'BQS'

工作季度初

FY5253Quarter

'REQ'

零售业(又称 52-53 周)季度

YearEnd

'YE'

日历年末

YearBegin

'YS''BYS'

日历年初

BYearEnd

'BYE'

工作年末

BYearBegin

'BYS'

工作年初

FY5253

'RE'

零售业(又称 52-53 周)年

Easter

复活节假日

BusinessHour

'bh'

工作时

CustomBusinessHour

'cbh'

自定义工作时

Day

'D'

一个绝对日期

Hour

'h'

一小时

Minute

'min'

一分钟

Second

's'

一秒

Milli

'ms'

一毫秒

Micro

'us'

一微秒

Nano

'ns'

一纳秒

DateOffsets 还提供了 rollforward()rollback() 方法,分别用于相对于偏移量将日期向前或向后移动到有效的偏移日期。例如,由于工作日偏移量仅在工作日运行,它会将落在周末(周六和周日)的日期向前滚动到周一。

In [153]: ts = pd.Timestamp("2018-01-06 00:00:00")

In [154]: ts.day_name()
Out[154]: 'Saturday'

# BusinessHour's valid offset dates are Monday through Friday
In [155]: offset = pd.offsets.BusinessHour(start="09:00")

# Bring the date to the closest offset date (Monday)
In [156]: offset.rollforward(ts)
Out[156]: Timestamp('2018-01-08 09:00:00')

# Date is brought to the closest offset date first and then the hour is added
In [157]: ts + offset
Out[157]: Timestamp('2018-01-08 10:00:00')

这些操作默认会保留时间(小时、分钟等)信息。要将时间重置为午夜,请在应用操作之前或之后使用 normalize()(取决于您是否希望在操作中包含时间信息)。

In [158]: ts = pd.Timestamp("2014-01-01 09:00")

In [159]: day = pd.offsets.Day()

In [160]: day + ts
Out[160]: Timestamp('2014-01-02 09:00:00')

In [161]: (day + ts).normalize()
Out[161]: Timestamp('2014-01-02 00:00:00')

In [162]: ts = pd.Timestamp("2014-01-01 22:00")

In [163]: hour = pd.offsets.Hour()

In [164]: hour + ts
Out[164]: Timestamp('2014-01-01 23:00:00')

In [165]: (hour + ts).normalize()
Out[165]: Timestamp('2014-01-01 00:00:00')

In [166]: (hour + pd.Timestamp("2014-01-01 23:30")).normalize()
Out[166]: Timestamp('2014-01-02 00:00:00')

参数化偏移量#

有些偏移量在创建时可以进行“参数化”,从而产生不同的行为。例如,用于生成周数据的 Week 偏移量接受一个 weekday 参数,这使得生成的日期总是落在周中的特定一天。

In [167]: d = datetime.datetime(2008, 8, 18, 9, 0)

In [168]: d
Out[168]: datetime.datetime(2008, 8, 18, 9, 0)

In [169]: d + pd.offsets.Week()
Out[169]: Timestamp('2008-08-25 09:00:00')

In [170]: d + pd.offsets.Week(weekday=4)
Out[170]: Timestamp('2008-08-22 09:00:00')

In [171]: (d + pd.offsets.Week(weekday=4)).weekday()
Out[171]: 4

In [172]: d - pd.offsets.Week()
Out[172]: Timestamp('2008-08-11 09:00:00')

normalize 选项在加法和减法中有效。

In [173]: d + pd.offsets.Week(normalize=True)
Out[173]: Timestamp('2008-08-25 00:00:00')

In [174]: d - pd.offsets.Week(normalize=True)
Out[174]: Timestamp('2008-08-11 00:00:00')

另一个例子是使用特定的结束月份对 YearEnd 进行参数化

In [175]: d + pd.offsets.YearEnd()
Out[175]: Timestamp('2008-12-31 09:00:00')

In [176]: d + pd.offsets.YearEnd(month=6)
Out[176]: Timestamp('2009-06-30 09:00:00')

将偏移量用于 Series / DatetimeIndex#

偏移量可以与 SeriesDatetimeIndex 一起使用,将偏移量应用于每个元素。

In [177]: rng = pd.date_range("2012-01-01", "2012-01-03")

In [178]: s = pd.Series(rng)

In [179]: rng
Out[179]: DatetimeIndex(['2012-01-01', '2012-01-02', '2012-01-03'], dtype='datetime64[ns]', freq='D')

In [180]: rng + pd.DateOffset(months=2)
Out[180]: DatetimeIndex(['2012-03-01', '2012-03-02', '2012-03-03'], dtype='datetime64[ns]', freq=None)

In [181]: s + pd.DateOffset(months=2)
Out[181]: 
0   2012-03-01
1   2012-03-02
2   2012-03-03
dtype: datetime64[ns]

In [182]: s - pd.DateOffset(months=2)
Out[182]: 
0   2011-11-01
1   2011-11-02
2   2011-11-03
dtype: datetime64[ns]

如果偏移量类直接映射到 TimedeltaDayHourMinuteSecondMicroMilliNano),则可以像 Timedelta 一样使用它 - 更多示例请参阅 Timedelta 一节

In [183]: s - pd.offsets.Day(2)
Out[183]: 
0   2011-12-30
1   2011-12-31
2   2012-01-01
dtype: datetime64[ns]

In [184]: td = s - pd.Series(pd.date_range("2011-12-29", "2011-12-31"))

In [185]: td
Out[185]: 
0   3 days
1   3 days
2   3 days
dtype: timedelta64[ns]

In [186]: td + pd.offsets.Minute(15)
Out[186]: 
0   3 days 00:15:00
1   3 days 00:15:00
2   3 days 00:15:00
dtype: timedelta64[ns]

请注意,某些偏移量(例如 BQuarterEnd)没有向量化实现。它们仍然可以使用,但计算速度可能会慢很多,并且会显示 PerformanceWarning

In [187]: rng + pd.offsets.BQuarterEnd()
Out[187]: DatetimeIndex(['2012-03-30', '2012-03-30', '2012-03-30'], dtype='datetime64[ns]', freq=None)

自定义工作日#

CDayCustomBusinessDay 类提供了一个参数化的 BusinessDay 类,可用于创建考虑当地假日和周末习俗的自定义工作日日历。

举一个有趣的例子,看看埃及,那里实行周五至周六的周末制。

In [188]: weekmask_egypt = "Sun Mon Tue Wed Thu"

# They also observe International Workers' Day so let's
# add that for a couple of years
In [189]: holidays = [
   .....:     "2012-05-01",
   .....:     datetime.datetime(2013, 5, 1),
   .....:     np.datetime64("2014-05-01"),
   .....: ]
   .....: 

In [190]: bday_egypt = pd.offsets.CustomBusinessDay(
   .....:     holidays=holidays,
   .....:     weekmask=weekmask_egypt,
   .....: )
   .....: 

In [191]: dt = datetime.datetime(2013, 4, 30)

In [192]: dt + 2 * bday_egypt
Out[192]: Timestamp('2013-05-05 00:00:00')

让我们映射到工作日名称

In [193]: dts = pd.date_range(dt, periods=5, freq=bday_egypt)

In [194]: pd.Series(dts.weekday, dts).map(pd.Series("Mon Tue Wed Thu Fri Sat Sun".split()))
Out[194]: 
2013-04-30    Tue
2013-05-02    Thu
2013-05-05    Sun
2013-05-06    Mon
2013-05-07    Tue
Freq: C, dtype: object

假日日历可用于提供假日列表。更多信息请参阅 假日日历 一节。

In [195]: from pandas.tseries.holiday import USFederalHolidayCalendar

In [196]: bday_us = pd.offsets.CustomBusinessDay(calendar=USFederalHolidayCalendar())

# Friday before MLK Day
In [197]: dt = datetime.datetime(2014, 1, 17)

# Tuesday after MLK Day (Monday is skipped because it's a holiday)
In [198]: dt + bday_us
Out[198]: Timestamp('2014-01-21 00:00:00')

遵守特定假日日历的月度偏移量可以按常规方式定义。

In [199]: bmth_us = pd.offsets.CustomBusinessMonthBegin(calendar=USFederalHolidayCalendar())

# Skip new years
In [200]: dt = datetime.datetime(2013, 12, 17)

In [201]: dt + bmth_us
Out[201]: Timestamp('2014-01-02 00:00:00')

# Define date index with custom offset
In [202]: pd.date_range(start="20100101", end="20120101", freq=bmth_us)
Out[202]: 
DatetimeIndex(['2010-01-04', '2010-02-01', '2010-03-01', '2010-04-01',
               '2010-05-03', '2010-06-01', '2010-07-01', '2010-08-02',
               '2010-09-01', '2010-10-01', '2010-11-01', '2010-12-01',
               '2011-01-03', '2011-02-01', '2011-03-01', '2011-04-01',
               '2011-05-02', '2011-06-01', '2011-07-01', '2011-08-01',
               '2011-09-01', '2011-10-03', '2011-11-01', '2011-12-01'],
              dtype='datetime64[ns]', freq='CBMS')

注意

频率字符串“C”用于指示使用了 CustomBusinessDay DateOffset,需要注意的是,CustomBusinessDay 是一个参数化类型,CustomBusinessDay 的实例可能有所不同,而这无法从频率字符串“C”中检测出来。因此,用户需要确保在应用程序中一致地使用频率字符串“C”。

工作时#

BusinessHour 类在 BusinessDay 上提供工作时表示,允许使用特定的开始和结束时间。

默认情况下,BusinessHour 使用 9:00 - 17:00 作为工作时间。添加 BusinessHour 将以小时频率增加 Timestamp。如果目标 Timestamp 不在工作时间内,则移至下一个工作时间然后增加。如果结果超出工作时间的结束,剩余的小时数将添加到下一个工作日。

In [203]: bh = pd.offsets.BusinessHour()

In [204]: bh
Out[204]: <BusinessHour: bh=09:00-17:00>

# 2014-08-01 is Friday
In [205]: pd.Timestamp("2014-08-01 10:00").weekday()
Out[205]: 4

In [206]: pd.Timestamp("2014-08-01 10:00") + bh
Out[206]: Timestamp('2014-08-01 11:00:00')

# Below example is the same as: pd.Timestamp('2014-08-01 09:00') + bh
In [207]: pd.Timestamp("2014-08-01 08:00") + bh
Out[207]: Timestamp('2014-08-01 10:00:00')

# If the results is on the end time, move to the next business day
In [208]: pd.Timestamp("2014-08-01 16:00") + bh
Out[208]: Timestamp('2014-08-04 09:00:00')

# Remainings are added to the next day
In [209]: pd.Timestamp("2014-08-01 16:30") + bh
Out[209]: Timestamp('2014-08-04 09:30:00')

# Adding 2 business hours
In [210]: pd.Timestamp("2014-08-01 10:00") + pd.offsets.BusinessHour(2)
Out[210]: Timestamp('2014-08-01 12:00:00')

# Subtracting 3 business hours
In [211]: pd.Timestamp("2014-08-01 10:00") + pd.offsets.BusinessHour(-3)
Out[211]: Timestamp('2014-07-31 15:00:00')

您还可以通过关键字指定 startend 时间。参数必须是表示 hour:minutestrdatetime.time 实例。将秒、微秒和纳秒指定为工作时会导致 ValueError

In [212]: bh = pd.offsets.BusinessHour(start="11:00", end=datetime.time(20, 0))

In [213]: bh
Out[213]: <BusinessHour: bh=11:00-20:00>

In [214]: pd.Timestamp("2014-08-01 13:00") + bh
Out[214]: Timestamp('2014-08-01 14:00:00')

In [215]: pd.Timestamp("2014-08-01 09:00") + bh
Out[215]: Timestamp('2014-08-01 12:00:00')

In [216]: pd.Timestamp("2014-08-01 18:00") + bh
Out[216]: Timestamp('2014-08-01 19:00:00')

传递晚于 endstart 时间表示跨午夜的工作时间。在这种情况下,工作时间会跨越午夜并延伸到第二天。有效的工作时间通过其是否从有效的 BusinessDay 开始来区分。

In [217]: bh = pd.offsets.BusinessHour(start="17:00", end="09:00")

In [218]: bh
Out[218]: <BusinessHour: bh=17:00-09:00>

In [219]: pd.Timestamp("2014-08-01 17:00") + bh
Out[219]: Timestamp('2014-08-01 18:00:00')

In [220]: pd.Timestamp("2014-08-01 23:00") + bh
Out[220]: Timestamp('2014-08-02 00:00:00')

# Although 2014-08-02 is Saturday,
# it is valid because it starts from 08-01 (Friday).
In [221]: pd.Timestamp("2014-08-02 04:00") + bh
Out[221]: Timestamp('2014-08-02 05:00:00')

# Although 2014-08-04 is Monday,
# it is out of business hours because it starts from 08-03 (Sunday).
In [222]: pd.Timestamp("2014-08-04 04:00") + bh
Out[222]: Timestamp('2014-08-04 18:00:00')

BusinessHour.rollforwardrollback 应用于非工作时间,结果会是下一个工作时间的开始或前一天的结束。与其他偏移量不同,根据定义,BusinessHour.rollforward 的输出可能与 apply 的结果不同。

这是因为一天的工作时间结束等于下一天的工作时间开始。例如,在默认工作时间(9:00 - 17:00)下,2014-08-01 17:002014-08-04 09:00 之间没有间隔(0 分钟)。

# This adjusts a Timestamp to business hour edge
In [223]: pd.offsets.BusinessHour().rollback(pd.Timestamp("2014-08-02 15:00"))
Out[223]: Timestamp('2014-08-01 17:00:00')

In [224]: pd.offsets.BusinessHour().rollforward(pd.Timestamp("2014-08-02 15:00"))
Out[224]: Timestamp('2014-08-04 09:00:00')

# It is the same as BusinessHour() + pd.Timestamp('2014-08-01 17:00').
# And it is the same as BusinessHour() + pd.Timestamp('2014-08-04 09:00')
In [225]: pd.offsets.BusinessHour() + pd.Timestamp("2014-08-02 15:00")
Out[225]: Timestamp('2014-08-04 10:00:00')

# BusinessDay results (for reference)
In [226]: pd.offsets.BusinessHour().rollforward(pd.Timestamp("2014-08-02"))
Out[226]: Timestamp('2014-08-04 09:00:00')

# It is the same as BusinessDay() + pd.Timestamp('2014-08-01')
# The result is the same as rollworward because BusinessDay never overlap.
In [227]: pd.offsets.BusinessHour() + pd.Timestamp("2014-08-02")
Out[227]: Timestamp('2014-08-04 10:00:00')

BusinessHour 将周六和周日视为假日。要使用任意假日,可以使用 CustomBusinessHour 偏移量,如下一小节所述。

自定义工作时#

CustomBusinessHourBusinessHourCustomBusinessDay 的结合,允许您指定任意假日。CustomBusinessHour 的工作方式与 BusinessHour 相同,只是它会跳过指定的自定义假日。

In [228]: from pandas.tseries.holiday import USFederalHolidayCalendar

In [229]: bhour_us = pd.offsets.CustomBusinessHour(calendar=USFederalHolidayCalendar())

# Friday before MLK Day
In [230]: dt = datetime.datetime(2014, 1, 17, 15)

In [231]: dt + bhour_us
Out[231]: Timestamp('2014-01-17 16:00:00')

# Tuesday after MLK Day (Monday is skipped because it's a holiday)
In [232]: dt + bhour_us * 2
Out[232]: Timestamp('2014-01-21 09:00:00')

您可以使用 BusinessHourCustomBusinessDay 都支持的关键字参数。

In [233]: bhour_mon = pd.offsets.CustomBusinessHour(start="10:00", weekmask="Tue Wed Thu Fri")

# Monday is skipped because it's a holiday, business hour starts from 10:00
In [234]: dt + bhour_mon * 2
Out[234]: Timestamp('2014-01-21 10:00:00')

偏移别名#

许多常用的时间序列频率都有字符串别名。我们将这些别名称为 偏移别名

别名

描述

B

工作日频率

C

自定义工作日频率

D

日历日频率

W

每周频率

ME

月末频率

SME

半月月末频率(每月 15 日和月末)

BME

工作月末频率

CBME

自定义工作月末频率

MS

月初频率

SMS

半月月初频率(每月 1 日和 15 日)

BMS

工作月初频率

CBMS

自定义工作月初频率

QE

季度末频率

BQE

工作季度末频率

QS

季度初频率

BQS

工作季度初频率

YE

年末频率

BYE

工作年末频率

YS

年初频率

BYS

工作年初频率

h

每小时频率

bh

工作时频率

cbh

自定义工作时频率

min

每分钟频率

s

每秒频率

ms

毫秒

us

微秒

ns

纳秒

自 2.2.0 版本弃用: 别名 HBHCBHTSLUN 已弃用,推荐使用别名 hbhcbhminsmsusns

注意

使用上述偏移别名时,应注意 date_range()bdate_range() 等函数只会返回在由 start_dateend_date 定义的区间内的 timestamp。如果 start_date 不对应于频率,则返回的 timestamp 将从下一个有效的 timestamp 开始;对于 end_date 也是如此,返回的 timestamp 将在之前一个有效的 timestamp 处停止。

例如,对于偏移量 MS,如果 start_date 不是月份的第一天,返回的 timestamp 将从下一个月的第一天开始。如果 end_date 不是月份的第一天,最后一个返回的 timestamp 将是对应月份的第一天。

In [235]: dates_lst_1 = pd.date_range("2020-01-06", "2020-04-03", freq="MS")

In [236]: dates_lst_1
Out[236]: DatetimeIndex(['2020-02-01', '2020-03-01', '2020-04-01'], dtype='datetime64[ns]', freq='MS')

In [237]: dates_lst_2 = pd.date_range("2020-01-01", "2020-04-01", freq="MS")

In [238]: dates_lst_2
Out[238]: DatetimeIndex(['2020-01-01', '2020-02-01', '2020-03-01', '2020-04-01'], dtype='datetime64[ns]', freq='MS')

从上面的示例可以看出,date_range()bdate_range() 只会返回 start_dateend_date 之间的有效 timestamp。如果这些日期对于给定的频率不是有效的 timestamp,start_date 将向前滚动到下一个有效值(end_date 则向后滚动到前一个有效值)。

周期别名#

许多常用的时间序列频率都有字符串别名。我们将这些别名称为 周期别名

别名

描述

B

工作日频率

D

日历日频率

W

每周频率

M

每月频率

Q

每季度频率

Y

每年频率

h

每小时频率

min

每分钟频率

s

每秒频率

ms

毫秒

us

微秒

ns

纳秒

自 2.2.0 版本弃用: 别名 AHTSLUN 已弃用,推荐使用别名 Yhminsmsusns

组合别名#

如前所述,别名和偏移量实例在大多数函数中可以互换使用

In [239]: pd.date_range(start, periods=5, freq="B")
Out[239]: 
DatetimeIndex(['2011-01-03', '2011-01-04', '2011-01-05', '2011-01-06',
               '2011-01-07'],
              dtype='datetime64[ns]', freq='B')

In [240]: pd.date_range(start, periods=5, freq=pd.offsets.BDay())
Out[240]: 
DatetimeIndex(['2011-01-03', '2011-01-04', '2011-01-05', '2011-01-06',
               '2011-01-07'],
              dtype='datetime64[ns]', freq='B')

您可以组合使用日期和日内偏移量

In [241]: pd.date_range(start, periods=10, freq="2h20min")
Out[241]: 
DatetimeIndex(['2011-01-01 00:00:00', '2011-01-01 02:20:00',
               '2011-01-01 04:40:00', '2011-01-01 07:00:00',
               '2011-01-01 09:20:00', '2011-01-01 11:40:00',
               '2011-01-01 14:00:00', '2011-01-01 16:20:00',
               '2011-01-01 18:40:00', '2011-01-01 21:00:00'],
              dtype='datetime64[ns]', freq='140min')

In [242]: pd.date_range(start, periods=10, freq="1D10us")
Out[242]: 
DatetimeIndex([       '2011-01-01 00:00:00', '2011-01-02 00:00:00.000010',
               '2011-01-03 00:00:00.000020', '2011-01-04 00:00:00.000030',
               '2011-01-05 00:00:00.000040', '2011-01-06 00:00:00.000050',
               '2011-01-07 00:00:00.000060', '2011-01-08 00:00:00.000070',
               '2011-01-09 00:00:00.000080', '2011-01-10 00:00:00.000090'],
              dtype='datetime64[ns]', freq='86400000010us')

锚定偏移量#

对于某些频率,您可以指定锚定后缀

别名

描述

W-SUN

每周频率(星期日)。与“W”相同

W-MON

每周频率(星期一)

W-TUE

每周频率(星期二)

W-WED

每周频率(星期三)

W-THU

每周频率(星期四)

W-FRI

每周频率(星期五)

W-SAT

每周频率(星期六)

(B)Q(E)(S)-DEC

每季度频率,年度在 12 月结束。与“QE”相同

(B)Q(E)(S)-JAN

每季度频率,年度在 1 月结束

(B)Q(E)(S)-FEB

每季度频率,年度在 2 月结束

(B)Q(E)(S)-MAR

每季度频率,年度在 3 月结束

(B)Q(E)(S)-APR

每季度频率,年度在 4 月结束

(B)Q(E)(S)-MAY

每季度频率,年度在 5 月结束

(B)Q(E)(S)-JUN

每季度频率,年度在 6 月结束

(B)Q(E)(S)-JUL

每季度频率,年度在 7 月结束

(B)Q(E)(S)-AUG

每季度频率,年度在 8 月结束

(B)Q(E)(S)-SEP

每季度频率,年度在 9 月结束

(B)Q(E)(S)-OCT

每季度频率,年度在 10 月结束

(B)Q(E)(S)-NOV

每季度频率,年度在 11 月结束

(B)Y(E)(S)-DEC

每年频率,锚定在 12 月末。与“YE”相同

(B)Y(E)(S)-JAN

每年频率,锚定在 1 月末

(B)Y(E)(S)-FEB

每年频率,锚定在 2 月末

(B)Y(E)(S)-MAR

每年频率,锚定在 3 月末

(B)Y(E)(S)-APR

每年频率,锚定在 4 月末

(B)Y(E)(S)-MAY

每年频率,锚定在 5 月末

(B)Y(E)(S)-JUN

每年频率,锚定在 6 月末

(B)Y(E)(S)-JUL

每年频率,锚定在 7 月末

(B)Y(E)(S)-AUG

每年频率,锚定在 8 月末

(B)Y(E)(S)-SEP

每年频率,锚定在 9 月末

(B)Y(E)(S)-OCT

每年频率,锚定在 10 月末

(B)Y(E)(S)-NOV

每年频率,锚定在 11 月末

这些可以用作 date_rangebdate_rangeDatetimeIndex 的构造函数以及 pandas 中各种其他时间序列相关函数的参数。

锚定偏移量语义#

对于那些锚定在特定频率(MonthEndMonthBeginWeekEnd 等)的开始或结束的偏移量,向前和向后滚动时适用以下规则。

n 不为 0 时,如果给定的日期不在锚定点上,它会捕捉到下一个(上一个)锚定点,然后额外向前或向后移动 |n|-1 步。

In [243]: pd.Timestamp("2014-01-02") + pd.offsets.MonthBegin(n=1)
Out[243]: Timestamp('2014-02-01 00:00:00')

In [244]: pd.Timestamp("2014-01-02") + pd.offsets.MonthEnd(n=1)
Out[244]: Timestamp('2014-01-31 00:00:00')

In [245]: pd.Timestamp("2014-01-02") - pd.offsets.MonthBegin(n=1)
Out[245]: Timestamp('2014-01-01 00:00:00')

In [246]: pd.Timestamp("2014-01-02") - pd.offsets.MonthEnd(n=1)
Out[246]: Timestamp('2013-12-31 00:00:00')

In [247]: pd.Timestamp("2014-01-02") + pd.offsets.MonthBegin(n=4)
Out[247]: Timestamp('2014-05-01 00:00:00')

In [248]: pd.Timestamp("2014-01-02") - pd.offsets.MonthBegin(n=4)
Out[248]: Timestamp('2013-10-01 00:00:00')

如果给定的日期 *正好在* 锚定点上,它会向前或向后移动 |n| 点。

In [249]: pd.Timestamp("2014-01-01") + pd.offsets.MonthBegin(n=1)
Out[249]: Timestamp('2014-02-01 00:00:00')

In [250]: pd.Timestamp("2014-01-31") + pd.offsets.MonthEnd(n=1)
Out[250]: Timestamp('2014-02-28 00:00:00')

In [251]: pd.Timestamp("2014-01-01") - pd.offsets.MonthBegin(n=1)
Out[251]: Timestamp('2013-12-01 00:00:00')

In [252]: pd.Timestamp("2014-01-31") - pd.offsets.MonthEnd(n=1)
Out[252]: Timestamp('2013-12-31 00:00:00')

In [253]: pd.Timestamp("2014-01-01") + pd.offsets.MonthBegin(n=4)
Out[253]: Timestamp('2014-05-01 00:00:00')

In [254]: pd.Timestamp("2014-01-31") - pd.offsets.MonthBegin(n=4)
Out[254]: Timestamp('2013-10-01 00:00:00')

n=0 时,如果日期在锚定点上则不移动,否则向前滚动到下一个锚定点。

In [255]: pd.Timestamp("2014-01-02") + pd.offsets.MonthBegin(n=0)
Out[255]: Timestamp('2014-02-01 00:00:00')

In [256]: pd.Timestamp("2014-01-02") + pd.offsets.MonthEnd(n=0)
Out[256]: Timestamp('2014-01-31 00:00:00')

In [257]: pd.Timestamp("2014-01-01") + pd.offsets.MonthBegin(n=0)
Out[257]: Timestamp('2014-01-01 00:00:00')

In [258]: pd.Timestamp("2014-01-31") + pd.offsets.MonthEnd(n=0)
Out[258]: Timestamp('2014-01-31 00:00:00')

假日 / 假日日历#

假日和日历提供了一种简单的方式来定义假日规则,以便与 CustomBusinessDay 一起使用或用于其他需要预定义假日集的分析。AbstractHolidayCalendar 类提供了返回假日列表所需的所有方法,只需在特定的假日日历类中定义 rules。此外,start_dateend_date 类属性决定了生成假日的日期范围。这些属性应在 AbstractHolidayCalendar 类上被覆盖,以便该范围适用于所有日历子类。USFederalHolidayCalendar 是唯一存在的日历,主要用作开发其他日历的示例。

对于固定日期的假日(例如美国阵亡将士纪念日或 7 月 4 日),如果假日落在周末或其他非工作日,则观察规则决定何时庆祝该假日。已定义的观察规则有

规则

描述

nearest_workday

将周六移至周五,将周日移至周一

sunday_to_monday

将周日移至下一个周一

next_monday_or_tuesday

将周六移至周一,将周日/周一移至周二

previous_friday

将周六和周日移至前一个周五”

next_monday

将周六和周日移至下一个周一

假日和假日日历的定义示例

In [259]: from pandas.tseries.holiday import (
   .....:     Holiday,
   .....:     USMemorialDay,
   .....:     AbstractHolidayCalendar,
   .....:     nearest_workday,
   .....:     MO,
   .....: )
   .....: 

In [260]: class ExampleCalendar(AbstractHolidayCalendar):
   .....:     rules = [
   .....:         USMemorialDay,
   .....:         Holiday("July 4th", month=7, day=4, observance=nearest_workday),
   .....:         Holiday(
   .....:             "Columbus Day",
   .....:             month=10,
   .....:             day=1,
   .....:             offset=pd.DateOffset(weekday=MO(2)),
   .....:         ),
   .....:     ]
   .....: 

In [261]: cal = ExampleCalendar()

In [262]: cal.holidays(datetime.datetime(2012, 1, 1), datetime.datetime(2012, 12, 31))
Out[262]: DatetimeIndex(['2012-05-28', '2012-07-04', '2012-10-08'], dtype='datetime64[ns]', freq=None)
提示:

weekday=MO(2)2 * Week(weekday=2) 相同

使用此日历,创建索引或执行偏移量算术运算会跳过周末和假日(即阵亡将士纪念日/7 月 4 日)。例如,下面使用 ExampleCalendar 定义了一个自定义工作日偏移量。与任何其他偏移量一样,它可用于创建 DatetimeIndex 或添加到 datetimeTimestamp 对象。

In [263]: pd.date_range(
   .....:     start="7/1/2012", end="7/10/2012", freq=pd.offsets.CDay(calendar=cal)
   .....: ).to_pydatetime()
   .....: 
Out[263]: 
array([datetime.datetime(2012, 7, 2, 0, 0),
       datetime.datetime(2012, 7, 3, 0, 0),
       datetime.datetime(2012, 7, 5, 0, 0),
       datetime.datetime(2012, 7, 6, 0, 0),
       datetime.datetime(2012, 7, 9, 0, 0),
       datetime.datetime(2012, 7, 10, 0, 0)], dtype=object)

In [264]: offset = pd.offsets.CustomBusinessDay(calendar=cal)

In [265]: datetime.datetime(2012, 5, 25) + offset
Out[265]: Timestamp('2012-05-29 00:00:00')

In [266]: datetime.datetime(2012, 7, 3) + offset
Out[266]: Timestamp('2012-07-05 00:00:00')

In [267]: datetime.datetime(2012, 7, 3) + 2 * offset
Out[267]: Timestamp('2012-07-06 00:00:00')

In [268]: datetime.datetime(2012, 7, 6) + offset
Out[268]: Timestamp('2012-07-09 00:00:00')

日期范围由 AbstractHolidayCalendarstart_dateend_date 类属性定义。默认值如下所示。

In [269]: AbstractHolidayCalendar.start_date
Out[269]: Timestamp('1970-01-01 00:00:00')

In [270]: AbstractHolidayCalendar.end_date
Out[270]: Timestamp('2200-12-31 00:00:00')

通过将属性设置为 datetime/Timestamp/字符串,可以覆盖这些日期。

In [271]: AbstractHolidayCalendar.start_date = datetime.datetime(2012, 1, 1)

In [272]: AbstractHolidayCalendar.end_date = datetime.datetime(2012, 12, 31)

In [273]: cal.holidays()
Out[273]: DatetimeIndex(['2012-05-28', '2012-07-04', '2012-10-08'], dtype='datetime64[ns]', freq=None)

可以使用 get_calendar 函数按名称访问每个日历类,该函数返回一个假日类实例。任何导入的日历类都将通过此函数自动可用。HolidayCalendarFactory 还提供了一个简单的接口,用于创建日历的组合日历或具有附加规则的日历。

In [274]: from pandas.tseries.holiday import get_calendar, HolidayCalendarFactory, USLaborDay

In [275]: cal = get_calendar("ExampleCalendar")

In [276]: cal.rules
Out[276]: 
[Holiday: Memorial Day (month=5, day=31, offset=<DateOffset: weekday=MO(-1)>),
 Holiday: July 4th (month=7, day=4, observance=<function nearest_workday at 0x7fe8b0553c70>),
 Holiday: Columbus Day (month=10, day=1, offset=<DateOffset: weekday=MO(+2)>)]

In [277]: new_cal = HolidayCalendarFactory("NewExampleCalendar", cal, USLaborDay)

In [278]: new_cal.rules
Out[278]: 
[Holiday: Labor Day (month=9, day=1, offset=<DateOffset: weekday=MO(+1)>),
 Holiday: Memorial Day (month=5, day=31, offset=<DateOffset: weekday=MO(-1)>),
 Holiday: July 4th (month=7, day=4, observance=<function nearest_workday at 0x7fe8b0553c70>),
 Holiday: Columbus Day (month=10, day=1, offset=<DateOffset: weekday=MO(+2)>)]

重采样#

pandas 提供了一个简单、强大且高效的功能,用于在频率转换期间执行重采样操作(例如,将秒级数据转换为 5 分钟级数据)。这在金融应用中极其常见,但不限于此。

resample() 是一个基于时间的 groupby,然后在每个分组上应用一个归约方法。请参阅一些 cookbook 示例 以了解一些高级策略。

resample() 方法可以直接从 DataFrameGroupBy 对象使用,请参阅 groupby 文档

基础知识#

In [290]: rng = pd.date_range("1/1/2012", periods=100, freq="s")

In [291]: ts = pd.Series(np.random.randint(0, 500, len(rng)), index=rng)

In [292]: ts.resample("5Min").sum()
Out[292]: 
2012-01-01    25103
Freq: 5min, dtype: int64

resample 函数非常灵活,允许你指定许多不同的参数来控制频率转换和重采样操作。

通过 GroupBy 可用的任何内置方法都可以作为返回对象的方法使用,包括 sum, mean, std, sem, max, min, median, first, last, ohlc

In [293]: ts.resample("5Min").mean()
Out[293]: 
2012-01-01    251.03
Freq: 5min, dtype: float64

In [294]: ts.resample("5Min").ohlc()
Out[294]: 
            open  high  low  close
2012-01-01   308   460    9    205

In [295]: ts.resample("5Min").max()
Out[295]: 
2012-01-01    460
Freq: 5min, dtype: int64

对于向下采样,可以将 closed 设置为 'left' 或 'right' 以指定间隔的哪一端是闭合的。

In [296]: ts.resample("5Min", closed="right").mean()
Out[296]: 
2011-12-31 23:55:00    308.000000
2012-01-01 00:00:00    250.454545
Freq: 5min, dtype: float64

In [297]: ts.resample("5Min", closed="left").mean()
Out[297]: 
2012-01-01    251.03
Freq: 5min, dtype: float64

label 等参数用于操作结果标签。label 指定结果是用间隔的开始还是结束进行标记。

In [298]: ts.resample("5Min").mean()  # by default label='left'
Out[298]: 
2012-01-01    251.03
Freq: 5min, dtype: float64

In [299]: ts.resample("5Min", label="left").mean()
Out[299]: 
2012-01-01    251.03
Freq: 5min, dtype: float64

警告

除了 'ME', 'YE', 'QE', 'BME', 'BYE', 'BQE' 和 'W' 之外,所有频率偏移的 labelclosed 的默认值都是 '**left**',这些频率的默认值都是 'right'。

这可能会无意中导致“向前看”,即稍后时间的值被拉回到之前的时间,如下面的使用 BusinessDay 频率的示例所示。

In [300]: s = pd.date_range("2000-01-01", "2000-01-05").to_series()

In [301]: s.iloc[2] = pd.NaT

In [302]: s.dt.day_name()
Out[302]: 
2000-01-01     Saturday
2000-01-02       Sunday
2000-01-03          NaN
2000-01-04      Tuesday
2000-01-05    Wednesday
Freq: D, dtype: object

# default: label='left', closed='left'
In [303]: s.resample("B").last().dt.day_name()
Out[303]: 
1999-12-31       Sunday
2000-01-03          NaN
2000-01-04      Tuesday
2000-01-05    Wednesday
Freq: B, dtype: object

注意星期日的值是如何被拉回到之前的星期五的。要获得星期日的值被推迟到星期一的行为,请改用

In [304]: s.resample("B", label="right", closed="right").last().dt.day_name()
Out[304]: 
2000-01-03       Sunday
2000-01-04      Tuesday
2000-01-05    Wednesday
2000-01-06          NaN
Freq: B, dtype: object

axis 参数可以设置为 0 或 1,它允许你对 DataFrame 的指定轴进行重采样。

kind 可以设置为 'timestamp' 或 'period',用于将结果索引转换为 timestamp 或 time span 表示,或从 timestamp 或 time span 表示转换。默认情况下,resample 保留输入表示。

重采样 period 数据时(详见下文),convention 可以设置为 'start' 或 'end'。它指定了低频率周期如何转换为高频率周期。

向上采样#

对于向上采样,你可以指定向上采样的方式以及使用 limit 参数对创建的间隙进行插值。

# from secondly to every 250 milliseconds
In [305]: ts[:2].resample("250ms").asfreq()
Out[305]: 
2012-01-01 00:00:00.000    308.0
2012-01-01 00:00:00.250      NaN
2012-01-01 00:00:00.500      NaN
2012-01-01 00:00:00.750      NaN
2012-01-01 00:00:01.000    204.0
Freq: 250ms, dtype: float64

In [306]: ts[:2].resample("250ms").ffill()
Out[306]: 
2012-01-01 00:00:00.000    308
2012-01-01 00:00:00.250    308
2012-01-01 00:00:00.500    308
2012-01-01 00:00:00.750    308
2012-01-01 00:00:01.000    204
Freq: 250ms, dtype: int64

In [307]: ts[:2].resample("250ms").ffill(limit=2)
Out[307]: 
2012-01-01 00:00:00.000    308.0
2012-01-01 00:00:00.250    308.0
2012-01-01 00:00:00.500    308.0
2012-01-01 00:00:00.750      NaN
2012-01-01 00:00:01.000    204.0
Freq: 250ms, dtype: float64

稀疏重采样#

稀疏时间序列是指相对于你要重采样的时间跨度,你拥有的数据点非常少。对稀疏序列进行简单的向上采样可能会生成大量中间值。当你不想使用方法填充这些值时,例如 fill_methodNone 时,中间值将被填充为 NaN

由于 resample 是一个基于时间的 groupby,下面是一种仅对不全是 NaN 的分组进行高效重采样的方法。

In [308]: rng = pd.date_range("2014-1-1", periods=100, freq="D") + pd.Timedelta("1s")

In [309]: ts = pd.Series(range(100), index=rng)

如果我们想对序列的完整范围进行重采样

In [310]: ts.resample("3min").sum()
Out[310]: 
2014-01-01 00:00:00     0
2014-01-01 00:03:00     0
2014-01-01 00:06:00     0
2014-01-01 00:09:00     0
2014-01-01 00:12:00     0
                       ..
2014-04-09 23:48:00     0
2014-04-09 23:51:00     0
2014-04-09 23:54:00     0
2014-04-09 23:57:00     0
2014-04-10 00:00:00    99
Freq: 3min, Length: 47521, dtype: int64

我们可以转而仅对那些包含数据点的分组进行重采样,如下所示

In [311]: from functools import partial

In [312]: from pandas.tseries.frequencies import to_offset

In [313]: def round(t, freq):
   .....:     freq = to_offset(freq)
   .....:     td = pd.Timedelta(freq)
   .....:     return pd.Timestamp((t.value // td.value) * td.value)
   .....: 

In [314]: ts.groupby(partial(round, freq="3min")).sum()
Out[314]: 
2014-01-01     0
2014-01-02     1
2014-01-03     2
2014-01-04     3
2014-01-05     4
              ..
2014-04-06    95
2014-04-07    96
2014-04-08    97
2014-04-09    98
2014-04-10    99
Length: 100, dtype: int64

聚合#

resample() 方法返回一个 pandas.api.typing.Resampler 实例。与 aggregating APIgroupby APIwindow API 类似,Resampler 可以选择性地进行重采样。

DataFrame 进行重采样时,默认情况下会对所有列使用相同的函数。

In [315]: df = pd.DataFrame(
   .....:     np.random.randn(1000, 3),
   .....:     index=pd.date_range("1/1/2012", freq="s", periods=1000),
   .....:     columns=["A", "B", "C"],
   .....: )
   .....: 

In [316]: r = df.resample("3min")

In [317]: r.mean()
Out[317]: 
                            A         B         C
2012-01-01 00:00:00 -0.033823 -0.121514 -0.081447
2012-01-01 00:03:00  0.056909  0.146731 -0.024320
2012-01-01 00:06:00 -0.058837  0.047046 -0.052021
2012-01-01 00:09:00  0.063123 -0.026158 -0.066533
2012-01-01 00:12:00  0.186340 -0.003144  0.074752
2012-01-01 00:15:00 -0.085954 -0.016287 -0.050046

我们可以使用标准的 getitem 选择一个或多个特定的列。

In [318]: r["A"].mean()
Out[318]: 
2012-01-01 00:00:00   -0.033823
2012-01-01 00:03:00    0.056909
2012-01-01 00:06:00   -0.058837
2012-01-01 00:09:00    0.063123
2012-01-01 00:12:00    0.186340
2012-01-01 00:15:00   -0.085954
Freq: 3min, Name: A, dtype: float64

In [319]: r[["A", "B"]].mean()
Out[319]: 
                            A         B
2012-01-01 00:00:00 -0.033823 -0.121514
2012-01-01 00:03:00  0.056909  0.146731
2012-01-01 00:06:00 -0.058837  0.047046
2012-01-01 00:09:00  0.063123 -0.026158
2012-01-01 00:12:00  0.186340 -0.003144
2012-01-01 00:15:00 -0.085954 -0.016287

你可以传递函数列表或字典进行聚合,输出一个 DataFrame

In [320]: r["A"].agg(["sum", "mean", "std"])
Out[320]: 
                           sum      mean       std
2012-01-01 00:00:00  -6.088060 -0.033823  1.043263
2012-01-01 00:03:00  10.243678  0.056909  1.058534
2012-01-01 00:06:00 -10.590584 -0.058837  0.949264
2012-01-01 00:09:00  11.362228  0.063123  1.028096
2012-01-01 00:12:00  33.541257  0.186340  0.884586
2012-01-01 00:15:00  -8.595393 -0.085954  1.035476

对重采样的 DataFrame,你可以传递函数列表应用到每一列,这将生成具有层次化索引的聚合结果。

In [321]: r.agg(["sum", "mean"])
Out[321]: 
                             A            ...          C          
                           sum      mean  ...        sum      mean
2012-01-01 00:00:00  -6.088060 -0.033823  ... -14.660515 -0.081447
2012-01-01 00:03:00  10.243678  0.056909  ...  -4.377642 -0.024320
2012-01-01 00:06:00 -10.590584 -0.058837  ...  -9.363825 -0.052021
2012-01-01 00:09:00  11.362228  0.063123  ... -11.975895 -0.066533
2012-01-01 00:12:00  33.541257  0.186340  ...  13.455299  0.074752
2012-01-01 00:15:00  -8.595393 -0.085954  ...  -5.004580 -0.050046

[6 rows x 6 columns]

通过向 aggregate 传递一个字典,你可以对 DataFrame 的列应用不同的聚合。

In [322]: r.agg({"A": "sum", "B": lambda x: np.std(x, ddof=1)})
Out[322]: 
                             A         B
2012-01-01 00:00:00  -6.088060  1.001294
2012-01-01 00:03:00  10.243678  1.074597
2012-01-01 00:06:00 -10.590584  0.987309
2012-01-01 00:09:00  11.362228  0.944953
2012-01-01 00:12:00  33.541257  1.095025
2012-01-01 00:15:00  -8.595393  1.035312

函数名称也可以是字符串。要使字符串有效,它必须在重采样对象上实现。

In [323]: r.agg({"A": "sum", "B": "std"})
Out[323]: 
                             A         B
2012-01-01 00:00:00  -6.088060  1.001294
2012-01-01 00:03:00  10.243678  1.074597
2012-01-01 00:06:00 -10.590584  0.987309
2012-01-01 00:09:00  11.362228  0.944953
2012-01-01 00:12:00  33.541257  1.095025
2012-01-01 00:15:00  -8.595393  1.035312

此外,你还可以分别为每一列指定多个聚合函数。

In [324]: r.agg({"A": ["sum", "std"], "B": ["mean", "std"]})
Out[324]: 
                             A                   B          
                           sum       std      mean       std
2012-01-01 00:00:00  -6.088060  1.043263 -0.121514  1.001294
2012-01-01 00:03:00  10.243678  1.058534  0.146731  1.074597
2012-01-01 00:06:00 -10.590584  0.949264  0.047046  0.987309
2012-01-01 00:09:00  11.362228  1.028096 -0.026158  0.944953
2012-01-01 00:12:00  33.541257  0.884586 -0.003144  1.095025
2012-01-01 00:15:00  -8.595393  1.035476 -0.016287  1.035312

如果 DataFrame 没有类似 datetime 的索引,但你希望基于 frame 中的类似 datetime 的列进行重采样,则可以将其传递给 on 关键字。

In [325]: df = pd.DataFrame(
   .....:     {"date": pd.date_range("2015-01-01", freq="W", periods=5), "a": np.arange(5)},
   .....:     index=pd.MultiIndex.from_arrays(
   .....:         [[1, 2, 3, 4, 5], pd.date_range("2015-01-01", freq="W", periods=5)],
   .....:         names=["v", "d"],
   .....:     ),
   .....: )
   .....: 

In [326]: df
Out[326]: 
                   date  a
v d                       
1 2015-01-04 2015-01-04  0
2 2015-01-11 2015-01-11  1
3 2015-01-18 2015-01-18  2
4 2015-01-25 2015-01-25  3
5 2015-02-01 2015-02-01  4

In [327]: df.resample("ME", on="date")[["a"]].sum()
Out[327]: 
            a
date         
2015-01-31  6
2015-02-28  4

同样,如果你想通过 MultiIndex 中类似 datetime 的层级进行重采样,可以将其名称或位置传递给 level 关键字。

In [328]: df.resample("ME", level="d")[["a"]].sum()
Out[328]: 
            a
d            
2015-01-31  6
2015-02-28  4

迭代分组#

有了 Resampler 对象,迭代分组数据就非常自然,其功能类似于 itertools.groupby()

In [329]: small = pd.Series(
   .....:     range(6),
   .....:     index=pd.to_datetime(
   .....:         [
   .....:             "2017-01-01T00:00:00",
   .....:             "2017-01-01T00:30:00",
   .....:             "2017-01-01T00:31:00",
   .....:             "2017-01-01T01:00:00",
   .....:             "2017-01-01T03:00:00",
   .....:             "2017-01-01T03:05:00",
   .....:         ]
   .....:     ),
   .....: )
   .....: 

In [330]: resampled = small.resample("h")

In [331]: for name, group in resampled:
   .....:     print("Group: ", name)
   .....:     print("-" * 27)
   .....:     print(group, end="\n\n")
   .....: 
Group:  2017-01-01 00:00:00
---------------------------
2017-01-01 00:00:00    0
2017-01-01 00:30:00    1
2017-01-01 00:31:00    2
dtype: int64

Group:  2017-01-01 01:00:00
---------------------------
2017-01-01 01:00:00    3
dtype: int64

Group:  2017-01-01 02:00:00
---------------------------
Series([], dtype: int64)

Group:  2017-01-01 03:00:00
---------------------------
2017-01-01 03:00:00    4
2017-01-01 03:05:00    5
dtype: int64

更多信息请参见 迭代分组Resampler.__iter__

使用 originoffset 调整分箱的开始#

分组的分箱根据时间序列起始点的当天的开始进行调整。这对于一天倍数的频率(例如 30D)或能整除一天的频率(例如 90s1min)工作良好。这可能会与某些不符合此标准的频率产生不一致。要改变此行为,你可以使用参数 origin 指定一个固定的 Timestamp。

例如

In [332]: start, end = "2000-10-01 23:30:00", "2000-10-02 00:30:00"

In [333]: middle = "2000-10-02 00:00:00"

In [334]: rng = pd.date_range(start, end, freq="7min")

In [335]: ts = pd.Series(np.arange(len(rng)) * 3, index=rng)

In [336]: ts
Out[336]: 
2000-10-01 23:30:00     0
2000-10-01 23:37:00     3
2000-10-01 23:44:00     6
2000-10-01 23:51:00     9
2000-10-01 23:58:00    12
2000-10-02 00:05:00    15
2000-10-02 00:12:00    18
2000-10-02 00:19:00    21
2000-10-02 00:26:00    24
Freq: 7min, dtype: int64

这里我们可以看到,当使用默认值 (`'start_day'`) 的 origin 时,'2000-10-02 00:00:00' 之后的结果会因时间序列的起始点而不同。

In [337]: ts.resample("17min", origin="start_day").sum()
Out[337]: 
2000-10-01 23:14:00     0
2000-10-01 23:31:00     9
2000-10-01 23:48:00    21
2000-10-02 00:05:00    54
2000-10-02 00:22:00    24
Freq: 17min, dtype: int64

In [338]: ts[middle:end].resample("17min", origin="start_day").sum()
Out[338]: 
2000-10-02 00:00:00    33
2000-10-02 00:17:00    45
Freq: 17min, dtype: int64

这里我们可以看到,当将 origin 设置为 `'epoch'` 时,'2000-10-02 00:00:00' 之后的结果会因时间序列的起始点而相同。

In [339]: ts.resample("17min", origin="epoch").sum()
Out[339]: 
2000-10-01 23:18:00     0
2000-10-01 23:35:00    18
2000-10-01 23:52:00    27
2000-10-02 00:09:00    39
2000-10-02 00:26:00    24
Freq: 17min, dtype: int64

In [340]: ts[middle:end].resample("17min", origin="epoch").sum()
Out[340]: 
2000-10-01 23:52:00    15
2000-10-02 00:09:00    39
2000-10-02 00:26:00    24
Freq: 17min, dtype: int64

如果需要,你可以为 origin 使用一个自定义的时间戳。

In [341]: ts.resample("17min", origin="2001-01-01").sum()
Out[341]: 
2000-10-01 23:30:00     9
2000-10-01 23:47:00    21
2000-10-02 00:04:00    54
2000-10-02 00:21:00    24
Freq: 17min, dtype: int64

In [342]: ts[middle:end].resample("17min", origin=pd.Timestamp("2001-01-01")).sum()
Out[342]: 
2000-10-02 00:04:00    54
2000-10-02 00:21:00    24
Freq: 17min, dtype: int64

如果需要,你只需使用一个 offset Timedelta 来调整分箱,该 Timedelta 将添加到默认的 origin 中。这两个示例对于这个时间序列是等效的。

In [343]: ts.resample("17min", origin="start").sum()
Out[343]: 
2000-10-01 23:30:00     9
2000-10-01 23:47:00    21
2000-10-02 00:04:00    54
2000-10-02 00:21:00    24
Freq: 17min, dtype: int64

In [344]: ts.resample("17min", offset="23h30min").sum()
Out[344]: 
2000-10-01 23:30:00     9
2000-10-01 23:47:00    21
2000-10-02 00:04:00    54
2000-10-02 00:21:00    24
Freq: 17min, dtype: int64

注意在最后一个示例中 origin 使用了 `'start'`。在这种情况下,origin 将被设置为时间序列的第一个值。

向后重采样#

在 1.3.0 版本中新增。

有时,我们不是调整分箱的开始,而是需要固定分箱的结束,以使用给定的 freq 进行向后重采样。向后重采样默认将 closed 设置为 `'right'`,因为最后一个值应被视为最后一个分箱的边缘点。

我们可以将 origin 设置为 `'end'`。特定 Timestamp 索引对应的值表示从当前 Timestamp 减去 freq 到当前 Timestamp(右闭合)的重采样结果。

In [345]: ts.resample('17min', origin='end').sum()
Out[345]: 
2000-10-01 23:35:00     0
2000-10-01 23:52:00    18
2000-10-02 00:09:00    27
2000-10-02 00:26:00    63
Freq: 17min, dtype: int64

此外,与 `'start_day'` 选项不同,`end_day` 受到支持。这将把 origin 设置为最大 Timestamp 的向上取整午夜时间。

In [346]: ts.resample('17min', origin='end_day').sum()
Out[346]: 
2000-10-01 23:38:00     3
2000-10-01 23:55:00    15
2000-10-02 00:12:00    45
2000-10-02 00:29:00    45
Freq: 17min, dtype: int64

上述结果使用 '2000-10-02 00:29:00' 作为最后一个分箱的右边缘,因为进行了以下计算。

In [347]: ceil_mid = rng.max().ceil('D')

In [348]: freq = pd.offsets.Minute(17)

In [349]: bin_res = ceil_mid - freq * ((ceil_mid - rng.max()) // freq)

In [350]: bin_res
Out[350]: Timestamp('2000-10-02 00:29:00')

时间跨度表示#

pandas 中,规律的时间间隔由 Period 对象表示,而 Period 对象的序列则收集在 PeriodIndex 中,可以使用便捷函数 period_range 创建。

Period#

Period 代表一段时间跨度(例如,一天、一个月、一个季度等)。你可以使用 freq 关键字指定跨度,使用如下的频率别名。由于 freq 代表 Period 的跨度,它不能像“-3D”那样为负。

In [351]: pd.Period("2012", freq="Y-DEC")
Out[351]: Period('2012', 'Y-DEC')

In [352]: pd.Period("2012-1-1", freq="D")
Out[352]: Period('2012-01-01', 'D')

In [353]: pd.Period("2012-1-1 19:00", freq="h")
Out[353]: Period('2012-01-01 19:00', 'h')

In [354]: pd.Period("2012-1-1 19:00", freq="5h")
Out[354]: Period('2012-01-01 19:00', '5h')

对 period 加上或减去整数会按照 period 自身的频率移动 period。不同 freq (跨度) 的 Period 之间不允许进行算术运算。

In [355]: p = pd.Period("2012", freq="Y-DEC")

In [356]: p + 1
Out[356]: Period('2013', 'Y-DEC')

In [357]: p - 3
Out[357]: Period('2009', 'Y-DEC')

In [358]: p = pd.Period("2012-01", freq="2M")

In [359]: p + 2
Out[359]: Period('2012-05', '2M')

In [360]: p - 1
Out[360]: Period('2011-11', '2M')

In [361]: p == pd.Period("2012-01", freq="3M")
Out[361]: False

如果 Period 的频率是天或更高频率(D, h, min, s, ms, usns),如果结果可以具有相同的频率,则可以添加 offsets 和类似 timedelta 的对象。否则,将引发 ValueError

In [362]: p = pd.Period("2014-07-01 09:00", freq="h")

In [363]: p + pd.offsets.Hour(2)
Out[363]: Period('2014-07-01 11:00', 'h')

In [364]: p + datetime.timedelta(minutes=120)
Out[364]: Period('2014-07-01 11:00', 'h')

In [365]: p + np.timedelta64(7200, "s")
Out[365]: Period('2014-07-01 11:00', 'h')
In [366]: p + pd.offsets.Minute(5)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File period.pyx:1824, in pandas._libs.tslibs.period._Period._add_timedeltalike_scalar()

File timedeltas.pyx:278, in pandas._libs.tslibs.timedeltas.delta_to_nanoseconds()

File np_datetime.pyx:661, in pandas._libs.tslibs.np_datetime.convert_reso()

ValueError: Cannot losslessly convert units

The above exception was the direct cause of the following exception:

IncompatibleFrequency                     Traceback (most recent call last)
Cell In[366], line 1
----> 1 p + pd.offsets.Minute(5)

File period.pyx:1845, in pandas._libs.tslibs.period._Period.__add__()

File period.pyx:1826, in pandas._libs.tslibs.period._Period._add_timedeltalike_scalar()

IncompatibleFrequency: Input cannot be converted to Period(freq=h)

如果 Period 具有其他频率,则只能添加相同的 offsets。否则,将引发 ValueError

In [367]: p = pd.Period("2014-07", freq="M")

In [368]: p + pd.offsets.MonthEnd(3)
Out[368]: Period('2014-10', 'M')
In [369]: p + pd.offsets.MonthBegin(3)
---------------------------------------------------------------------------
IncompatibleFrequency                     Traceback (most recent call last)
Cell In[369], line 1
----> 1 p + pd.offsets.MonthBegin(3)

File period.pyx:1847, in pandas._libs.tslibs.period._Period.__add__()

File period.pyx:1837, in pandas._libs.tslibs.period._Period._add_offset()

File period.pyx:1732, in pandas._libs.tslibs.period.PeriodMixin._require_matching_freq()

IncompatibleFrequency: Input has different freq=3M from Period(freq=M)

计算具有相同频率的 Period 实例之间的差值将返回它们之间的频率单位数量。

In [370]: pd.Period("2012", freq="Y-DEC") - pd.Period("2002", freq="Y-DEC")
Out[370]: <10 * YearEnds: month=12>

PeriodIndex 和 period_range#

规律的 Period 对象序列可以收集在 PeriodIndex 中,可以使用 period_range 便利函数进行构建。

In [371]: prng = pd.period_range("1/1/2011", "1/1/2012", freq="M")

In [372]: prng
Out[372]: 
PeriodIndex(['2011-01', '2011-02', '2011-03', '2011-04', '2011-05', '2011-06',
             '2011-07', '2011-08', '2011-09', '2011-10', '2011-11', '2011-12',
             '2012-01'],
            dtype='period[M]')

也可以直接使用 PeriodIndex 构造函数。

In [373]: pd.PeriodIndex(["2011-1", "2011-2", "2011-3"], freq="M")
Out[373]: PeriodIndex(['2011-01', '2011-02', '2011-03'], dtype='period[M]')

传递乘数频率会输出一个具有倍数跨度的 Period 序列。

In [374]: pd.period_range(start="2014-01", freq="3M", periods=4)
Out[374]: PeriodIndex(['2014-01', '2014-04', '2014-07', '2014-10'], dtype='period[3M]')

如果 startendPeriod 对象,它们将被用作 PeriodIndex 的锚定端点,其频率与 PeriodIndex 构造函数的频率匹配。

In [375]: pd.period_range(
   .....:     start=pd.Period("2017Q1", freq="Q"), end=pd.Period("2017Q2", freq="Q"), freq="M"
   .....: )
   .....: 
Out[375]: PeriodIndex(['2017-03', '2017-04', '2017-05', '2017-06'], dtype='period[M]')

就像 DatetimeIndex 一样,PeriodIndex 也可以用于索引 pandas 对象。

In [376]: ps = pd.Series(np.random.randn(len(prng)), prng)

In [377]: ps
Out[377]: 
2011-01   -2.916901
2011-02    0.514474
2011-03    1.346470
2011-04    0.816397
2011-05    2.258648
2011-06    0.494789
2011-07    0.301239
2011-08    0.464776
2011-09   -1.393581
2011-10    0.056780
2011-11    0.197035
2011-12    2.261385
2012-01   -0.329583
Freq: M, dtype: float64

PeriodIndex 支持与 Period 相同的加法和减法规则。

In [378]: idx = pd.period_range("2014-07-01 09:00", periods=5, freq="h")

In [379]: idx
Out[379]: 
PeriodIndex(['2014-07-01 09:00', '2014-07-01 10:00', '2014-07-01 11:00',
             '2014-07-01 12:00', '2014-07-01 13:00'],
            dtype='period[h]')

In [380]: idx + pd.offsets.Hour(2)
Out[380]: 
PeriodIndex(['2014-07-01 11:00', '2014-07-01 12:00', '2014-07-01 13:00',
             '2014-07-01 14:00', '2014-07-01 15:00'],
            dtype='period[h]')

In [381]: idx = pd.period_range("2014-07", periods=5, freq="M")

In [382]: idx
Out[382]: PeriodIndex(['2014-07', '2014-08', '2014-09', '2014-10', '2014-11'], dtype='period[M]')

In [383]: idx + pd.offsets.MonthEnd(3)
Out[383]: PeriodIndex(['2014-10', '2014-11', '2014-12', '2015-01', '2015-02'], dtype='period[M]')

PeriodIndex 有其自己的名为 period 的 dtype,请参见 Period Dtypes

Period Dtypes#

PeriodIndex 有一个自定义的 period dtype。这是一个 pandas 扩展 dtype,类似于 时区感知 dtype (datetime64[ns, tz])。

period dtype 保存 freq 属性,并使用 period[freq] 表示,例如 period[D]period[M],使用 频率字符串

In [384]: pi = pd.period_range("2016-01-01", periods=3, freq="M")

In [385]: pi
Out[385]: PeriodIndex(['2016-01', '2016-02', '2016-03'], dtype='period[M]')

In [386]: pi.dtype
Out[386]: period[M]

period dtype 可以在 .astype(...) 中使用。它允许像 .asfreq() 一样改变 PeriodIndexfreq,以及像 to_period() 一样将 DatetimeIndex 转换为 PeriodIndex

# change monthly freq to daily freq
In [387]: pi.astype("period[D]")
Out[387]: PeriodIndex(['2016-01-31', '2016-02-29', '2016-03-31'], dtype='period[D]')

# convert to DatetimeIndex
In [388]: pi.astype("datetime64[ns]")
Out[388]: DatetimeIndex(['2016-01-01', '2016-02-01', '2016-03-01'], dtype='datetime64[ns]', freq='MS')

# convert to PeriodIndex
In [389]: dti = pd.date_range("2011-01-01", freq="ME", periods=3)

In [390]: dti
Out[390]: DatetimeIndex(['2011-01-31', '2011-02-28', '2011-03-31'], dtype='datetime64[ns]', freq='ME')

In [391]: dti.astype("period[M]")
Out[391]: PeriodIndex(['2011-01', '2011-02', '2011-03'], dtype='period[M]')

PeriodIndex 部分字符串索引#

PeriodIndex 现在支持对非单调索引进行部分字符串切片。

你可以将日期和字符串传递给带有 PeriodIndexSeriesDataFrame,方式与 DatetimeIndex 相同。有关详细信息,请参阅 DatetimeIndex 部分字符串索引

In [392]: ps["2011-01"]
Out[392]: -2.9169013294054507

In [393]: ps[datetime.datetime(2011, 12, 25):]
Out[393]: 
2011-12    2.261385
2012-01   -0.329583
Freq: M, dtype: float64

In [394]: ps["10/31/2011":"12/31/2011"]
Out[394]: 
2011-10    0.056780
2011-11    0.197035
2011-12    2.261385
Freq: M, dtype: float64

传递一个代表比 PeriodIndex 更低频率的字符串会返回部分切片数据。

In [395]: ps["2011"]
Out[395]: 
2011-01   -2.916901
2011-02    0.514474
2011-03    1.346470
2011-04    0.816397
2011-05    2.258648
2011-06    0.494789
2011-07    0.301239
2011-08    0.464776
2011-09   -1.393581
2011-10    0.056780
2011-11    0.197035
2011-12    2.261385
Freq: M, dtype: float64

In [396]: dfp = pd.DataFrame(
   .....:     np.random.randn(600, 1),
   .....:     columns=["A"],
   .....:     index=pd.period_range("2013-01-01 9:00", periods=600, freq="min"),
   .....: )
   .....: 

In [397]: dfp
Out[397]: 
                         A
2013-01-01 09:00 -0.538468
2013-01-01 09:01 -1.365819
2013-01-01 09:02 -0.969051
2013-01-01 09:03 -0.331152
2013-01-01 09:04 -0.245334
...                    ...
2013-01-01 18:55  0.522460
2013-01-01 18:56  0.118710
2013-01-01 18:57  0.167517
2013-01-01 18:58  0.922883
2013-01-01 18:59  1.721104

[600 rows x 1 columns]

In [398]: dfp.loc["2013-01-01 10h"]
Out[398]: 
                         A
2013-01-01 10:00 -0.308975
2013-01-01 10:01  0.542520
2013-01-01 10:02  1.061068
2013-01-01 10:03  0.754005
2013-01-01 10:04  0.352933
...                    ...
2013-01-01 10:55 -0.865621
2013-01-01 10:56 -1.167818
2013-01-01 10:57 -2.081748
2013-01-01 10:58 -0.527146
2013-01-01 10:59  0.802298

[60 rows x 1 columns]

DatetimeIndex 一样,端点将包含在结果中。下面的例子切片了从 10:00 到 11:59 的数据。

In [399]: dfp["2013-01-01 10h":"2013-01-01 11h"]
Out[399]: 
                         A
2013-01-01 10:00 -0.308975
2013-01-01 10:01  0.542520
2013-01-01 10:02  1.061068
2013-01-01 10:03  0.754005
2013-01-01 10:04  0.352933
...                    ...
2013-01-01 11:55 -0.590204
2013-01-01 11:56  1.539990
2013-01-01 11:57 -1.224826
2013-01-01 11:58  0.578798
2013-01-01 11:59 -0.685496

[120 rows x 1 columns]

使用 PeriodIndex 进行频率转换和重采样#

PeriodPeriodIndex 的频率可以通过 asfreq 方法进行转换。让我们从结束于 12 月的 2011 财年开始。

In [400]: p = pd.Period("2011", freq="Y-DEC")

In [401]: p
Out[401]: Period('2011', 'Y-DEC')

我们可以将其转换为月度频率。使用 how 参数,我们可以指定是返回起始月份还是结束月份。

In [402]: p.asfreq("M", how="start")
Out[402]: Period('2011-01', 'M')

In [403]: p.asfreq("M", how="end")
Out[403]: Period('2011-12', 'M')

为了方便起见,提供了缩写 's' 和 'e'。

In [404]: p.asfreq("M", "s")
Out[404]: Period('2011-01', 'M')

In [405]: p.asfreq("M", "e")
Out[405]: Period('2011-12', 'M')

转换为“超周期”(例如,年度频率是季度频率的超周期)会自动返回包含输入 period 的超周期。

In [406]: p = pd.Period("2011-12", freq="M")

In [407]: p.asfreq("Y-NOV")
Out[407]: Period('2012', 'Y-NOV')

注意,由于我们转换为以 11 月结束的年度频率,2011 年 12 月的月度 period 实际上属于 2012 Y-NOV period。

带有锚定频率的 Period 转换对于处理经济学、商业及其他领域常见的各种季度数据特别有用。许多组织根据其财年开始和结束的月份来定义季度。因此,2011 年第一季度可能开始于 2010 年或 2011 年的几个月后。通过锚定频率,pandas 支持所有季度频率,从 Q-JANQ-DEC

Q-DEC 定义常规日历季度。

In [408]: p = pd.Period("2012Q1", freq="Q-DEC")

In [409]: p.asfreq("D", "s")
Out[409]: Period('2012-01-01', 'D')

In [410]: p.asfreq("D", "e")
Out[410]: Period('2012-03-31', 'D')

Q-MAR 定义财年结束于 3 月。

In [411]: p = pd.Period("2011Q4", freq="Q-MAR")

In [412]: p.asfreq("D", "s")
Out[412]: Period('2011-01-01', 'D')

In [413]: p.asfreq("D", "e")
Out[413]: Period('2011-03-31', 'D')

表示之间的转换#

带时间戳的数据可以使用 to_period 转换为 PeriodIndex-ed 数据,反之则使用 to_timestamp

In [414]: rng = pd.date_range("1/1/2012", periods=5, freq="ME")

In [415]: ts = pd.Series(np.random.randn(len(rng)), index=rng)

In [416]: ts
Out[416]: 
2012-01-31    1.931253
2012-02-29   -0.184594
2012-03-31    0.249656
2012-04-30   -0.978151
2012-05-31   -0.873389
Freq: ME, dtype: float64

In [417]: ps = ts.to_period()

In [418]: ps
Out[418]: 
2012-01    1.931253
2012-02   -0.184594
2012-03    0.249656
2012-04   -0.978151
2012-05   -0.873389
Freq: M, dtype: float64

In [419]: ps.to_timestamp()
Out[419]: 
2012-01-01    1.931253
2012-02-01   -0.184594
2012-03-01    0.249656
2012-04-01   -0.978151
2012-05-01   -0.873389
Freq: MS, dtype: float64

记住可以使用 's' 和 'e' 返回 period 开始或结束时的时间戳。

In [420]: ps.to_timestamp("D", how="s")
Out[420]: 
2012-01-01    1.931253
2012-02-01   -0.184594
2012-03-01    0.249656
2012-04-01   -0.978151
2012-05-01   -0.873389
Freq: MS, dtype: float64

在 period 和 timestamp 之间进行转换可以使用一些方便的算术函数。在下面的示例中,我们将一个财年结束于 11 月的季度频率转换为季度结束后的次月月末上午 9 点。

In [421]: prng = pd.period_range("1990Q1", "2000Q4", freq="Q-NOV")

In [422]: ts = pd.Series(np.random.randn(len(prng)), prng)

In [423]: ts.index = (prng.asfreq("M", "e") + 1).asfreq("h", "s") + 9

In [424]: ts.head()
Out[424]: 
1990-03-01 09:00   -0.109291
1990-06-01 09:00   -0.637235
1990-09-01 09:00   -1.735925
1990-12-01 09:00    2.096946
1991-03-01 09:00   -1.039926
Freq: h, dtype: float64

表示超出边界的时间跨度#

如果你的数据超出了 Timestamp 的范围,请参见 Timestamp limitations,则可以使用 PeriodIndex 和/或由 Period 组成的 Series 进行计算。

In [425]: span = pd.period_range("1215-01-01", "1381-01-01", freq="D")

In [426]: span
Out[426]: 
PeriodIndex(['1215-01-01', '1215-01-02', '1215-01-03', '1215-01-04',
             '1215-01-05', '1215-01-06', '1215-01-07', '1215-01-08',
             '1215-01-09', '1215-01-10',
             ...
             '1380-12-23', '1380-12-24', '1380-12-25', '1380-12-26',
             '1380-12-27', '1380-12-28', '1380-12-29', '1380-12-30',
             '1380-12-31', '1381-01-01'],
            dtype='period[D]', length=60632)

从基于 int64 的 YYYYMMDD 表示进行转换。

In [427]: s = pd.Series([20121231, 20141130, 99991231])

In [428]: s
Out[428]: 
0    20121231
1    20141130
2    99991231
dtype: int64

In [429]: def conv(x):
   .....:     return pd.Period(year=x // 10000, month=x // 100 % 100, day=x % 100, freq="D")
   .....: 

In [430]: s.apply(conv)
Out[430]: 
0    2012-12-31
1    2014-11-30
2    9999-12-31
dtype: period[D]

In [431]: s.apply(conv)[2]
Out[431]: Period('9999-12-31', 'D')

这些可以轻松转换为 PeriodIndex

In [432]: span = pd.PeriodIndex(s.apply(conv))

In [433]: span
Out[433]: PeriodIndex(['2012-12-31', '2014-11-30', '9999-12-31'], dtype='period[D]')

时区处理#

pandas 使用 pytzdateutil 库或标准库中的 datetime.timezone 对象,为处理不同时区的 timestamp 提供了丰富的支持。

使用时区#

默认情况下,pandas 对象是时区无感知的。

In [434]: rng = pd.date_range("3/6/2012 00:00", periods=15, freq="D")

In [435]: rng.tz is None
Out[435]: True

要将这些日期本地化到特定时区(为无感知日期分配特定时区),可以使用 tz_localize 方法或 date_range()TimestampDatetimeIndex 中的 tz 关键字参数。你可以传递 pytzdateutil 时区对象,或者 Olson 时区数据库字符串。Olson 时区字符串默认返回 pytz 时区对象。要返回 dateutil 时区对象,请在字符串前加上 dateutil/

  • pytz 中,你可以使用 from pytz import common_timezones, all_timezones 找到常见(和不太常见)时区的列表。

  • dateutil 使用操作系统时区,因此没有固定的可用列表。对于常见时区,名称与 pytz 相同。

In [436]: import dateutil

# pytz
In [437]: rng_pytz = pd.date_range("3/6/2012 00:00", periods=3, freq="D", tz="Europe/London")

In [438]: rng_pytz.tz
Out[438]: <DstTzInfo 'Europe/London' LMT-1 day, 23:59:00 STD>

# dateutil
In [439]: rng_dateutil = pd.date_range("3/6/2012 00:00", periods=3, freq="D")

In [440]: rng_dateutil = rng_dateutil.tz_localize("dateutil/Europe/London")

In [441]: rng_dateutil.tz
Out[441]: tzfile('/usr/share/zoneinfo/Europe/London')

# dateutil - utc special case
In [442]: rng_utc = pd.date_range(
   .....:     "3/6/2012 00:00",
   .....:     periods=3,
   .....:     freq="D",
   .....:     tz=dateutil.tz.tzutc(),
   .....: )
   .....: 

In [443]: rng_utc.tz
Out[443]: tzutc()
# datetime.timezone
In [444]: rng_utc = pd.date_range(
   .....:     "3/6/2012 00:00",
   .....:     periods=3,
   .....:     freq="D",
   .....:     tz=datetime.timezone.utc,
   .....: )
   .....: 

In [445]: rng_utc.tz
Out[445]: datetime.timezone.utc

注意,UTC 时区在 dateutil 中是一个特殊情况,应该显式构造为 dateutil.tz.tzutc 的实例。你也可以先显式构造其他时区对象。

In [446]: import pytz

# pytz
In [447]: tz_pytz = pytz.timezone("Europe/London")

In [448]: rng_pytz = pd.date_range("3/6/2012 00:00", periods=3, freq="D")

In [449]: rng_pytz = rng_pytz.tz_localize(tz_pytz)

In [450]: rng_pytz.tz == tz_pytz
Out[450]: True

# dateutil
In [451]: tz_dateutil = dateutil.tz.gettz("Europe/London")

In [452]: rng_dateutil = pd.date_range("3/6/2012 00:00", periods=3, freq="D", tz=tz_dateutil)

In [453]: rng_dateutil.tz == tz_dateutil
Out[453]: True

要将一个时区感知的 pandas 对象从一个时区转换为另一个时区,可以使用 tz_convert 方法。

In [454]: rng_pytz.tz_convert("US/Eastern")
Out[454]: 
DatetimeIndex(['2012-03-05 19:00:00-05:00', '2012-03-06 19:00:00-05:00',
               '2012-03-07 19:00:00-05:00'],
              dtype='datetime64[ns, US/Eastern]', freq=None)

注意

使用 pytz 时区时,对于相同的时区输入,DatetimeIndex 将构造一个与 Timestamp 不同的时区对象。一个 DatetimeIndex 可以包含可能具有不同 UTC 偏移量的 Timestamp 对象集合,无法用一个 pytz 时区实例简洁地表示,而一个 Timestamp 代表一个具有特定 UTC 偏移量的时间点。

In [455]: dti = pd.date_range("2019-01-01", periods=3, freq="D", tz="US/Pacific")

In [456]: dti.tz
Out[456]: <DstTzInfo 'US/Pacific' LMT-1 day, 16:07:00 STD>

In [457]: ts = pd.Timestamp("2019-01-01", tz="US/Pacific")

In [458]: ts.tz
Out[458]: <DstTzInfo 'US/Pacific' PST-1 day, 16:00:00 STD>

警告

注意库之间的转换。对于某些时区,pytzdateutil 对时区的定义不同。这对于不常见的时区来说问题更大,而不是对于像 US/Eastern 这样的“标准”时区。

警告

请注意,不同版本的时区库中的时区定义可能不被视为相等。这可能会在使用一个版本本地化存储数据,然后使用另一个不同版本对数据进行操作时导致问题。有关如何处理这种情况,请参阅此处

警告

对于 pytz 时区,将时区对象直接传递到 datetime.datetime 构造函数是错误的(例如,datetime.datetime(2011, 1, 1, tzinfo=pytz.timezone('US/Eastern')))。相反,需要使用 pytz 时区对象上的 localize 方法来本地化 datetime。

警告

请注意,对于未来的时间,任何时区库都无法保证时区之间(以及与 UTC 之间)的正确转换,因为时区相对于 UTC 的偏移量可能会被相关政府更改。

警告

如果您使用的日期超出 2038-01-18,由于底层库目前存在由 2038 年问题导致的缺陷,日光节约时间 (DST) 对时区感知日期的调整将不会生效。如果底层库得到修复,DST 过渡将生效。

例如,对于两个属于英国夏令时(因此通常为 GMT+1)的日期,以下断言都评估为 True:

In [459]: d_2037 = "2037-03-31T010101"

In [460]: d_2038 = "2038-03-31T010101"

In [461]: DST = "Europe/London"

In [462]: assert pd.Timestamp(d_2037, tz=DST) != pd.Timestamp(d_2037, tz="GMT")

In [463]: assert pd.Timestamp(d_2038, tz=DST) == pd.Timestamp(d_2038, tz="GMT")

在底层,所有时间戳都以 UTC 格式存储。来自时区感知 DatetimeIndexTimestamp 的值将其字段(日、小时、分钟等)本地化到相应的时区。然而,具有相同 UTC 值的时间戳即使位于不同时区,仍被视为相等。

In [464]: rng_eastern = rng_utc.tz_convert("US/Eastern")

In [465]: rng_berlin = rng_utc.tz_convert("Europe/Berlin")

In [466]: rng_eastern[2]
Out[466]: Timestamp('2012-03-07 19:00:00-0500', tz='US/Eastern')

In [467]: rng_berlin[2]
Out[467]: Timestamp('2012-03-08 01:00:00+0100', tz='Europe/Berlin')

In [468]: rng_eastern[2] == rng_berlin[2]
Out[468]: True

在不同时区中的 Series 之间进行操作将产生 UTC Series,数据将基于 UTC 时间戳对齐。

In [469]: ts_utc = pd.Series(range(3), pd.date_range("20130101", periods=3, tz="UTC"))

In [470]: eastern = ts_utc.tz_convert("US/Eastern")

In [471]: berlin = ts_utc.tz_convert("Europe/Berlin")

In [472]: result = eastern + berlin

In [473]: result
Out[473]: 
2013-01-01 00:00:00+00:00    0
2013-01-02 00:00:00+00:00    2
2013-01-03 00:00:00+00:00    4
Freq: D, dtype: int64

In [474]: result.index
Out[474]: 
DatetimeIndex(['2013-01-01 00:00:00+00:00', '2013-01-02 00:00:00+00:00',
               '2013-01-03 00:00:00+00:00'],
              dtype='datetime64[ns, UTC]', freq='D')

要删除时区信息,请使用 tz_localize(None)tz_convert(None)tz_localize(None) 将删除时区并产生本地时间表示。tz_convert(None) 将在转换为 UTC 时间后删除时区。

In [475]: didx = pd.date_range(start="2014-08-01 09:00", freq="h", periods=3, tz="US/Eastern")

In [476]: didx
Out[476]: 
DatetimeIndex(['2014-08-01 09:00:00-04:00', '2014-08-01 10:00:00-04:00',
               '2014-08-01 11:00:00-04:00'],
              dtype='datetime64[ns, US/Eastern]', freq='h')

In [477]: didx.tz_localize(None)
Out[477]: 
DatetimeIndex(['2014-08-01 09:00:00', '2014-08-01 10:00:00',
               '2014-08-01 11:00:00'],
              dtype='datetime64[ns]', freq=None)

In [478]: didx.tz_convert(None)
Out[478]: 
DatetimeIndex(['2014-08-01 13:00:00', '2014-08-01 14:00:00',
               '2014-08-01 15:00:00'],
              dtype='datetime64[ns]', freq='h')

# tz_convert(None) is identical to tz_convert('UTC').tz_localize(None)
In [479]: didx.tz_convert("UTC").tz_localize(None)
Out[479]: 
DatetimeIndex(['2014-08-01 13:00:00', '2014-08-01 14:00:00',
               '2014-08-01 15:00:00'],
              dtype='datetime64[ns]', freq=None)

Fold#

对于有歧义的时间,pandas 支持明确指定仅限关键字的 fold 参数。由于夏令时,当从夏季切换到冬季时,一个挂钟时间可能会出现两次;fold 描述了 datetime-like 对象是对应于挂钟第一次 (0) 还是第二次 (1) 达到有歧义时间。Fold 仅支持从朴素 (naive) 的 datetime.datetime 构造(详情请参阅 datetime 文档),或从 Timestamp 构造,或从组件构造(见下文)。仅支持 dateutil 时区(有关处理有歧义 datetime 的 dateutil 方法,请参阅 dateutil 文档),因为 pytz 时区不支持 fold(有关 pytz 如何处理有歧义 datetime 的详细信息,请参阅 pytz 文档)。要使用 pytz 本地化有歧义的 datetime,请使用 Timestamp.tz_localize()。总的来说,如果您需要直接控制如何处理有歧义的 datetime,我们建议在本地化时依赖 Timestamp.tz_localize()

In [480]: pd.Timestamp(
   .....:     datetime.datetime(2019, 10, 27, 1, 30, 0, 0),
   .....:     tz="dateutil/Europe/London",
   .....:     fold=0,
   .....: )
   .....: 
Out[480]: Timestamp('2019-10-27 01:30:00+0100', tz='dateutil//usr/share/zoneinfo/Europe/London')

In [481]: pd.Timestamp(
   .....:     year=2019,
   .....:     month=10,
   .....:     day=27,
   .....:     hour=1,
   .....:     minute=30,
   .....:     tz="dateutil/Europe/London",
   .....:     fold=1,
   .....: )
   .....: 
Out[481]: Timestamp('2019-10-27 01:30:00+0000', tz='dateutil//usr/share/zoneinfo/Europe/London')

本地化时的有歧义时间#

tz_localize 可能无法确定时间戳的 UTC 偏移量,因为本地时区的夏令时 (DST) 导致某些时间一天内出现两次(“时钟回拨”)。以下选项可用:

  • 'raise':抛出 pytz.AmbiguousTimeError(默认行为)

  • 'infer':尝试根据时间戳的单调性确定正确的偏移量

  • 'NaT':将有歧义的时间替换为 NaT

  • boolTrue 表示 DST 时间,False 表示非 DST 时间。对于时间序列,支持布尔值数组。

In [482]: rng_hourly = pd.DatetimeIndex(
   .....:     ["11/06/2011 00:00", "11/06/2011 01:00", "11/06/2011 01:00", "11/06/2011 02:00"]
   .....: )
   .....: 

这将会失败,因为存在有歧义的时间('11/06/2011 01:00')。

In [483]: rng_hourly.tz_localize('US/Eastern')
---------------------------------------------------------------------------
AmbiguousTimeError                        Traceback (most recent call last)
Cell In[483], line 1
----> 1 rng_hourly.tz_localize('US/Eastern')

File ~/work/pandas/pandas/pandas/core/indexes/datetimes.py:293, in DatetimeIndex.tz_localize(self, tz, ambiguous, nonexistent)
    286 @doc(DatetimeArray.tz_localize)
    287 def tz_localize(
    288     self,
   (...)
    291     nonexistent: TimeNonexistent = "raise",
    292 ) -> Self:
--> 293     arr = self._data.tz_localize(tz, ambiguous, nonexistent)
    294     return type(self)._simple_new(arr, name=self.name)

File ~/work/pandas/pandas/pandas/core/arrays/_mixins.py:81, in ravel_compat.<locals>.method(self, *args, **kwargs)
     78 @wraps(meth)
     79 def method(self, *args, **kwargs):
     80     if self.ndim == 1:
---> 81         return meth(self, *args, **kwargs)
     83     flags = self._ndarray.flags
     84     flat = self.ravel("K")

File ~/work/pandas/pandas/pandas/core/arrays/datetimes.py:1088, in DatetimeArray.tz_localize(self, tz, ambiguous, nonexistent)
   1085     tz = timezones.maybe_get_tz(tz)
   1086     # Convert to UTC
-> 1088     new_dates = tzconversion.tz_localize_to_utc(
   1089         self.asi8,
   1090         tz,
   1091         ambiguous=ambiguous,
   1092         nonexistent=nonexistent,
   1093         creso=self._creso,
   1094     )
   1095 new_dates_dt64 = new_dates.view(f"M8[{self.unit}]")
   1096 dtype = tz_to_dtype(tz, unit=self.unit)

File tzconversion.pyx:371, in pandas._libs.tslibs.tzconversion.tz_localize_to_utc()

AmbiguousTimeError: Cannot infer dst time from 2011-11-06 01:00:00, try using the 'ambiguous' argument

通过指定以下选项来处理这些有歧义的时间。

In [484]: rng_hourly.tz_localize("US/Eastern", ambiguous="infer")
Out[484]: 
DatetimeIndex(['2011-11-06 00:00:00-04:00', '2011-11-06 01:00:00-04:00',
               '2011-11-06 01:00:00-05:00', '2011-11-06 02:00:00-05:00'],
              dtype='datetime64[ns, US/Eastern]', freq=None)

In [485]: rng_hourly.tz_localize("US/Eastern", ambiguous="NaT")
Out[485]: 
DatetimeIndex(['2011-11-06 00:00:00-04:00', 'NaT', 'NaT',
               '2011-11-06 02:00:00-05:00'],
              dtype='datetime64[ns, US/Eastern]', freq=None)

In [486]: rng_hourly.tz_localize("US/Eastern", ambiguous=[True, True, False, False])
Out[486]: 
DatetimeIndex(['2011-11-06 00:00:00-04:00', '2011-11-06 01:00:00-04:00',
               '2011-11-06 01:00:00-05:00', '2011-11-06 02:00:00-05:00'],
              dtype='datetime64[ns, US/Eastern]', freq=None)

本地化时不存在的时间#

夏令时 (DST) 过渡也可能将本地时间提前 1 小时,从而产生不存在的本地时间(“时钟向前拨”)。通过 nonexistent 参数可以控制本地化具有不存在时间的时间序列的行为。以下选项可用:

  • 'raise':抛出 pytz.NonExistentTimeError(默认行为)

  • 'NaT':将不存在的时间替换为 NaT

  • 'shift_forward':将不存在的时间向前移动到最接近的真实时间

  • 'shift_backward':将不存在的时间向后移动到最接近的真实时间

  • timedelta 对象:按 timedelta 持续时间移动不存在的时间

In [487]: dti = pd.date_range(start="2015-03-29 02:30:00", periods=3, freq="h")

# 2:30 is a nonexistent time

默认情况下,本地化不存在的时间会引发错误。

In [488]: dti.tz_localize('Europe/Warsaw')
---------------------------------------------------------------------------
NonExistentTimeError                      Traceback (most recent call last)
Cell In[488], line 1
----> 1 dti.tz_localize('Europe/Warsaw')

File ~/work/pandas/pandas/pandas/core/indexes/datetimes.py:293, in DatetimeIndex.tz_localize(self, tz, ambiguous, nonexistent)
    286 @doc(DatetimeArray.tz_localize)
    287 def tz_localize(
    288     self,
   (...)
    291     nonexistent: TimeNonexistent = "raise",
    292 ) -> Self:
--> 293     arr = self._data.tz_localize(tz, ambiguous, nonexistent)
    294     return type(self)._simple_new(arr, name=self.name)

File ~/work/pandas/pandas/pandas/core/arrays/_mixins.py:81, in ravel_compat.<locals>.method(self, *args, **kwargs)
     78 @wraps(meth)
     79 def method(self, *args, **kwargs):
     80     if self.ndim == 1:
---> 81         return meth(self, *args, **kwargs)
     83     flags = self._ndarray.flags
     84     flat = self.ravel("K")

File ~/work/pandas/pandas/pandas/core/arrays/datetimes.py:1088, in DatetimeArray.tz_localize(self, tz, ambiguous, nonexistent)
   1085     tz = timezones.maybe_get_tz(tz)
   1086     # Convert to UTC
-> 1088     new_dates = tzconversion.tz_localize_to_utc(
   1089         self.asi8,
   1090         tz,
   1091         ambiguous=ambiguous,
   1092         nonexistent=nonexistent,
   1093         creso=self._creso,
   1094     )
   1095 new_dates_dt64 = new_dates.view(f"M8[{self.unit}]")
   1096 dtype = tz_to_dtype(tz, unit=self.unit)

File tzconversion.pyx:431, in pandas._libs.tslibs.tzconversion.tz_localize_to_utc()

NonExistentTimeError: 2015-03-29 02:30:00

将不存在的时间转换为 NaT 或移动时间。

In [489]: dti
Out[489]: 
DatetimeIndex(['2015-03-29 02:30:00', '2015-03-29 03:30:00',
               '2015-03-29 04:30:00'],
              dtype='datetime64[ns]', freq='h')

In [490]: dti.tz_localize("Europe/Warsaw", nonexistent="shift_forward")
Out[490]: 
DatetimeIndex(['2015-03-29 03:00:00+02:00', '2015-03-29 03:30:00+02:00',
               '2015-03-29 04:30:00+02:00'],
              dtype='datetime64[ns, Europe/Warsaw]', freq=None)

In [491]: dti.tz_localize("Europe/Warsaw", nonexistent="shift_backward")
Out[491]: 
DatetimeIndex(['2015-03-29 01:59:59.999999999+01:00',
                         '2015-03-29 03:30:00+02:00',
                         '2015-03-29 04:30:00+02:00'],
              dtype='datetime64[ns, Europe/Warsaw]', freq=None)

In [492]: dti.tz_localize("Europe/Warsaw", nonexistent=pd.Timedelta(1, unit="h"))
Out[492]: 
DatetimeIndex(['2015-03-29 03:30:00+02:00', '2015-03-29 03:30:00+02:00',
               '2015-03-29 04:30:00+02:00'],
              dtype='datetime64[ns, Europe/Warsaw]', freq=None)

In [493]: dti.tz_localize("Europe/Warsaw", nonexistent="NaT")
Out[493]: 
DatetimeIndex(['NaT', '2015-03-29 03:30:00+02:00',
               '2015-03-29 04:30:00+02:00'],
              dtype='datetime64[ns, Europe/Warsaw]', freq=None)

时区 Series 操作#

具有时区朴素值(naive values)的 Series 表示为 dtype datetime64[ns]

In [494]: s_naive = pd.Series(pd.date_range("20130101", periods=3))

In [495]: s_naive
Out[495]: 
0   2013-01-01
1   2013-01-02
2   2013-01-03
dtype: datetime64[ns]

具有时区感知值(aware values)的 Series 表示为 dtype datetime64[ns, tz],其中 tz 是时区。

In [496]: s_aware = pd.Series(pd.date_range("20130101", periods=3, tz="US/Eastern"))

In [497]: s_aware
Out[497]: 
0   2013-01-01 00:00:00-05:00
1   2013-01-02 00:00:00-05:00
2   2013-01-03 00:00:00-05:00
dtype: datetime64[ns, US/Eastern]

这两种 Series 的时区信息都可以通过 .dt 访问器进行操作,请参阅dt 访问器部分

例如,将朴素时间戳本地化并转换为时区感知。

In [498]: s_naive.dt.tz_localize("UTC").dt.tz_convert("US/Eastern")
Out[498]: 
0   2012-12-31 19:00:00-05:00
1   2013-01-01 19:00:00-05:00
2   2013-01-02 19:00:00-05:00
dtype: datetime64[ns, US/Eastern]

时区信息也可以使用 astype 方法进行操作。此方法可以在不同的时区感知 dtype 之间进行转换。

# convert to a new time zone
In [499]: s_aware.astype("datetime64[ns, CET]")
Out[499]: 
0   2013-01-01 06:00:00+01:00
1   2013-01-02 06:00:00+01:00
2   2013-01-03 06:00:00+01:00
dtype: datetime64[ns, CET]

注意

Series 使用 Series.to_numpy() 返回数据的 NumPy 数组。NumPy 目前不支持时区(尽管它以本地时区进行*打印*!),因此对于时区感知数据,返回的是 Timestamps 的对象数组。

In [500]: s_naive.to_numpy()
Out[500]: 
array(['2013-01-01T00:00:00.000000000', '2013-01-02T00:00:00.000000000',
       '2013-01-03T00:00:00.000000000'], dtype='datetime64[ns]')

In [501]: s_aware.to_numpy()
Out[501]: 
array([Timestamp('2013-01-01 00:00:00-0500', tz='US/Eastern'),
       Timestamp('2013-01-02 00:00:00-0500', tz='US/Eastern'),
       Timestamp('2013-01-03 00:00:00-0500', tz='US/Eastern')],
      dtype=object)

通过转换为 Timestamps 的对象数组,可以保留时区信息。例如,当转换回 Series 时:

In [502]: pd.Series(s_aware.to_numpy())
Out[502]: 
0   2013-01-01 00:00:00-05:00
1   2013-01-02 00:00:00-05:00
2   2013-01-03 00:00:00-05:00
dtype: datetime64[ns, US/Eastern]

然而,如果您想要一个实际的 NumPy datetime64[ns] 数组(值已转换为 UTC),而不是对象数组,您可以指定 dtype 参数。

In [503]: s_aware.to_numpy(dtype="datetime64[ns]")
Out[503]: 
array(['2013-01-01T05:00:00.000000000', '2013-01-02T05:00:00.000000000',
       '2013-01-03T05:00:00.000000000'], dtype='datetime64[ns]')