时间序列 / 日期功能#

pandas 包含用于处理所有领域时间序列数据的广泛功能和特性。通过使用 NumPy 的 datetime64timedelta64 dtypes,pandas 整合了来自其他 Python 库(如 scikits.timeseries)的大量功能,并创建了大量用于操纵时间序列数据的新功能。

例如,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]

datetimetimedeltaPeriod 数据传递给其构造函数时,SeriesDataFrame 具有扩展的数据类型支持和功能。然而,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 datetime 文档

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

您还可以传入一个整数或字符串列的 DataFrame,将其组装成一个 TimestampsSeries

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:1104, in to_datetime(arg, errors, dayfirst, yearfirst, utc, format, exact, unit, infer_datetime_format, origin, cache)
   1102         result = _convert_and_box_cache(argc, cache_array)
   1103     else:
-> 1104         result = convert_listlike(argc, format)
   1105 else:
   1106     result = convert_listlike(np.array([arg]), format)[0]

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

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

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

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

File ~/work/pandas/pandas/pandas/_libs/tslibs/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')

我们减去纪元(1970 年 1 月 1 日 UTC 午夜),然后对“单位”(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 对象的并集速度非常快(对于快速数据对齐很重要)。

  • 通过 yearmonth 等属性快速访问日期字段。

  • 正则化函数如 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]

DatetimeIndex 部分字符串索引也适用于带有 MultiIndexDataFrame

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

切片 vs. 精确匹配#

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

考虑一个具有分钟分辨率索引的 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() 便捷函数,它类似于切片。请注意,truncate 对于 DatetimeIndex 中任何未指定的日期组件假定值为 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-3月=1,4-6月=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

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

此外,如果您有一个包含日期时间类值的 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,它表示一段时间的持续时间,但遵循特定的日历持续时间规则。例如,一个 Timedelta 天将始终将 datetimes 增加 24 小时,而 DateOffset 天将使 datetimes 增加到下一天的相同时间,无论一天由于夏令时代表 23、24 还是 25 小时。然而,所有小时或更小(HourMinuteSecondMilliMicroNano)的 DateOffset 子类都表现得像 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'

工作日频率 (weekday)

CDayCustomBusinessDay

'C'

自定义工作日

'W'

一周,可选地锚定在一周中的某一天

月份的第几周

'WOM'

每月第y周的第x天

月份的最后一周

'LWOM'

每月最后一周的第x天

月末

'ME'

日历月末

月初

'MS'

日历月初

BMonthEndBusinessMonthEnd

'BME'

营业月月末

BMonthBeginBusinessMonthBegin

'BMS'

营业月月初

CBMonthEndCustomBusinessMonthEnd

'CBME'

自定义营业月月末

CBMonthBeginCustomBusinessMonthBegin

'CBMS'

自定义营业月月初

半月末

'SME'

15日(或其他day_of_month)和日历月末

半月起始

'SMS'

15日(或其他day_of_month)和日历月初

季末

'QE'

日历季末

季初

'QS'

日历季初

BQuarterEnd

'BQE

营业季末

BQuarterBegin

'BQS'

营业季初

FY5253季度

'REQ'

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

年末

'YE'

日历年末

年初

'YS''BYS'

日历年初

营业年末

'BYE'

营业年末

营业年初

'BYS'

营业年初

FY5253

'RE'

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

复活节

复活节假期

营业时间

'bh'

营业小时

自定义营业时间

'cbh'

自定义营业小时

'D'

一个绝对日

小时

'h'

一小时

分钟

'min'

一分钟

's'

一秒

毫秒

'ms'

一毫秒

微秒

'us'

一微秒

纳秒

'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:minute 表示的 strdatetime.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')

start 时间传递晚于 end 时间表示午夜营业时间。在这种情况下,营业时间会跨越午夜并与第二天重叠。有效营业时间根据是否从有效 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 定义的区间内的时间戳。如果 start_date 不对应频率,返回的时间戳将从下一个有效时间戳开始;对于 end_date,返回的时间戳将停止在前一个有效时间戳。

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

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 之间的有效时间戳。如果这些时间戳对于给定的频率无效,它将滚动到 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

季度频率,年末在十二月。与“QE”相同

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

季度频率,年末在一月

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

季度频率,年末在二月

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

季度频率,年末在三月

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

季度频率,年末在四月

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

季度频率,年末在五月

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

季度频率,年末在六月

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

季度频率,年末在七月

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

季度频率,年末在八月

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

季度频率,年末在九月

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

季度频率,年末在十月

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

季度频率,年末在十一月

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

年度频率,锚定在十二月末。与“YE”相同

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

年度频率,锚定在一月末

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

年度频率,锚定在二月末

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

年度频率,锚定在三月末

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

年度频率,锚定在四月末

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

年度频率,锚定在五月末

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

年度频率,锚定在六月末

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

年度频率,锚定在七月末

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

年度频率,锚定在八月末

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

年度频率,锚定在九月末

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

年度频率,锚定在十月末

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

年度频率,锚定在十一月末

这些可以作为参数传递给 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/string 来覆盖。

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 0x7f9491497c70>),
 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 0x7f9491497c70>),
 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

警告

labelclosed 的默认值对于所有频率偏移(除了 'ME', 'YE', 'QE', 'BME', 'BYE', 'BQE' 和 'W' 以外)都是 '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',用于将结果索引转换为时间戳和时间跨度表示,或反之。默认情况下,resample 会保留输入表示。

当重采样周期数据时(详情见下文),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 实例。与聚合 APIgroupby API窗口 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 没有日期时间索引,但您想根据帧中的日期时间列进行重采样,则可以将其传递给 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 的日期时间级别进行重采样,其名称或位置可以传递给 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 指定一个固定的时间戳。

例如:

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

在这里我们可以看到,当使用 origin 及其默认值 ('start_day') 时,在 '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 也受支持。这将把原点设置为最大 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 表示一个时间跨度(例如,一天、一个月、一个季度等)。您可以使用频率别名(如下所示)通过 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')

对周期进行整数加减会使其按自身频率移动。具有不同 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),则可以添加 offsetstimedelta 类型,如果结果可以具有相同的频率。否则,将引发 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 ~/work/pandas/pandas/pandas/_libs/tslibs/period.pyx:1824, in pandas._libs.tslibs.period._Period._add_timedeltalike_scalar()

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

File ~/work/pandas/pandas/pandas/_libs/tslibs/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 ~/work/pandas/pandas/pandas/_libs/tslibs/period.pyx:1845, in pandas._libs.tslibs.period._Period.__add__()

File ~/work/pandas/pandas/pandas/_libs/tslibs/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 ~/work/pandas/pandas/pandas/_libs/tslibs/period.pyx:1847, in pandas._libs.tslibs.period._Period.__add__()

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

File ~/work/pandas/pandas/pandas/_libs/tslibs/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,请参阅周期数据类型

周期数据类型#

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

period 数据类型保存 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 数据类型可以在 .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 方法进行转换。让我们从 2011 财年开始,该财年于 12 月结束。

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')

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

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

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

请注意,由于我们转换为年频率并在 11 月结束,因此 2011 年 12 月的月周期实际上在 2012 年 Y-NOV 周期中。

带锚定频率的周期转换对于处理经济学、商业和其他领域常见的各种季度数据特别有用。许多组织根据其财政年度开始和结束的月份定义季度。因此,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 定义财政年度在三月结束。

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 索引的数据,反之则可以使用 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' 来返回周期的开始或结束时间戳。

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

周期和时间戳之间的转换使得一些便捷的算术函数得以使用。在下面的示例中,我们将一个财政年度于 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 的范围,请参阅时间戳限制,那么您可以使用 PeriodIndex 和/或 PeriodsSeries 进行计算。

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 对象,为处理不同时区的时间戳提供了丰富的支持。

使用时区#

默认情况下,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 可以包含一组 Timestamp 对象,这些对象可能具有不同的 UTC 偏移量,不能由一个 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 方法进行本地化。

警告

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

警告

如果您使用 2038-01-18 之后的日期,由于年 2038 问题导致底层库存在缺陷,夏令时 (DST) 调整将不会应用于时区感知日期。如果底层库得到修复,DST 转换将被应用。

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

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)

折叠#

对于模糊时间,pandas 支持显式指定仅关键字参数 fold。由于夏令时,从夏季到冬季时间转换时,一个挂钟时间可能会出现两次;fold 描述日期时间类是对应于挂钟第一次(0)还是第二次(1)到达模糊时间。Fold 仅支持从朴素的 datetime.datetime(参见 datetime 文档了解详细信息)或从 Timestamp 构造,或从组件构造(参见下文)。仅支持 dateutil 时区(参见 dateutil 文档了解处理模糊日期时间的 dateutil 方法),因为 pytz 时区不支持 fold(参见 pytz 文档了解 pytz 如何处理模糊日期时间的详细信息)。要使用 pytz 本地化模糊日期时间,请使用 Timestamp.tz_localize()。一般来说,如果您需要直接控制如何处理模糊日期时间,我们建议依赖 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 表示夏令时,False 表示非夏令时。序列时间支持布尔值数组。

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:1090, in DatetimeArray.tz_localize(self, tz, ambiguous, nonexistent)
   1087     tz = timezones.maybe_get_tz(tz)
   1088     # Convert to UTC
-> 1090     new_dates = tzconversion.tz_localize_to_utc(
   1091         self.asi8,
   1092         tz,
   1093         ambiguous=ambiguous,
   1094         nonexistent=nonexistent,
   1095         creso=self._creso,
   1096     )
   1097 new_dates_dt64 = new_dates.view(f"M8[{self.unit}]")
   1098 dtype = tz_to_dtype(tz, unit=self.unit)

File ~/work/pandas/pandas/pandas/_libs/tslibs/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:1090, in DatetimeArray.tz_localize(self, tz, ambiguous, nonexistent)
   1087     tz = timezones.maybe_get_tz(tz)
   1088     # Convert to UTC
-> 1090     new_dates = tzconversion.tz_localize_to_utc(
   1091         self.asi8,
   1092         tz,
   1093         ambiguous=ambiguous,
   1094         nonexistent=nonexistent,
   1095         creso=self._creso,
   1096     )
   1097 new_dates_dt64 = new_dates.view(f"M8[{self.unit}]")
   1098 dtype = tz_to_dtype(tz, unit=self.unit)

File ~/work/pandas/pandas/pandas/_libs/tslibs/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 操作#

一个具有时区朴素值的 Series 表示为 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]

一个具有时区感知值的 Series 表示为 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 方法进行操作。此方法可以在不同的时区感知数据类型之间进行转换。

# 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)

通过转换为 Timestamp 的对象数组,可以保留时区信息。例如,当转换回 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]')