时间序列 / 日期功能#

pandas 包含了广泛的能力和特性,适用于所有领域的时间序列数据处理。使用 NumPy 的 datetime64timedelta64 数据类型,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[us]', 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. 日期时间:具有时区支持的特定日期和时间。类似于标准库中的 datetime.datetime

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

  3. 时间跨度:由一个时间点和其相关频率定义的时间段。

  4. 日期偏移量:一个尊重日历算术的相对时间持续时间。类似于 dateutil 包中的 dateutil.relativedelta.relativedelta

概念

Scalar 类

数组类

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

None

None

DateOffset

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

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

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

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

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

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

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

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

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

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

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

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

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

时间戳 vs. 时间段#

时间戳数据是最基本的时间序列数据类型,它将值与时间点关联起来。对于 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[s]', 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[s]

In [45]: pd.to_datetime(["2005/11/23", "2010/12/31"])
Out[45]: DatetimeIndex(['2005-11-23', '2010-12-31'], dtype='datetime64[s]', 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[s]', 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[s]', 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[s]', 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[s]', freq='2D')

提供一个格式参数#

除了必需的日期时间字符串外,还可以传递一个 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 列中组装 datetime#

你也可以传递一个包含整数或字符串列的 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[s]

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 /home/pandas/pandas/core/tools/datetimes.py:1051, in to_datetime(arg, errors, dayfirst, yearfirst, utc, format, exact, unit, origin, cache)
   1049         result = _convert_and_box_cache(argc, cache_array)
   1050     else:
-> 1051         result = convert_listlike(argc, format)
   1052 else:
   1053     result = convert_listlike(np.array([arg]), format)[0]

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

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

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

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

File strptime.pyx:605, 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[s]', freq=None)

Epoch 时间戳#

pandas 支持将整数或浮点数的时间戳转换为 TimestampDatetimeIndex。默认单位是纳秒,因为这是 Timestamp 对象内部存储的方式。然而,时间戳通常以另一种 单位 存储,这可以指定。这些计算从 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 参数相同的字符串 above 。可用单位列在 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)

备注

Epoch 时间将被四舍五入到最近的纳秒。

警告

浮点纪元时间的转换可能导致不准确和意外的结果。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')

从时间戳到纪元#

要反转上述操作,即从 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[us]', 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[us]', 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-06', '2011-12-07', '2011-12-08', '2011-12-09',
               '2011-12-12', '2011-12-13', '2011-12-14', '2011-12-15',
               '2011-12-16', '2011-12-19', '2011-12-20', '2011-12-21',
               '2011-12-22', '2011-12-23', '2011-12-26', '2011-12-27',
               '2011-12-28', '2011-12-29', '2011-12-30'],
              dtype='datetime64[ns]', freq='B')

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

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

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

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

自定义频率范围#

bdate_range 也可以通过使用 weekmaskholidays 参数生成一系列自定义频率的日期。只有在传递自定义频率字符串时,这些参数才会被使用。

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

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

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

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

时间戳限制#

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

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

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

当选择第二分辨率时,可用范围扩大到 +/- 2.9e11 。不同的分辨率可以通过 as_unit 相互转换。

索引#

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

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

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

  • 具有相同频率的重叠 DatetimeIndex 对象的联合操作非常快(这对于快速数据对齐非常重要)。

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

  • snap 这样的正则化函数和非常快的 asof 逻辑。

DatetimeIndex 对象具有常规 Index 对象的所有基本功能,以及大量专门用于时间序列的高级方法,便于频率处理。

备注

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

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

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

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

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

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

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

部分字符串索引#

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

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

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

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

为了方便访问更长时间序列的数据,您还可以传入年份或年份和月份作为字符串:

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

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

这种切片方式在具有 DatetimeIndexDataFrame 上也能工作。由于部分字符串选择是一种标签切片形式,端点 将会被 包含。这包括在包含日期上的匹配时间:

警告

使用带有 getitem 的 单个 字符串索引 DataFrame 行(例如 frame[dtstring])从 pandas 1.2.0 开始已被弃用(由于它索引行还是选择列的歧义),并将在未来版本中移除。使用 .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 对象有精确的 小时, 分钟, ,即使它们没有被明确指定(它们是 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 这样的时间戳集合中访问。

属性

描述

datetime 的年份

datetime 的月份

datetime 的日期

小时

datetime 的小时

minute

datetime 的记录

second

datetime 的秒数

微秒

datetime 的微秒

纳秒

datetime 的纳秒

日期

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

时间

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

timetz

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

dayofyear

一年中的第几天

day_of_year

一年中的第几天

dayofweek

星期一=0,星期日=6 的星期几的数字

day_of_week

星期一=0,星期日=6 的星期几的数字

工作日

星期一=0,星期日=6 的星期几的数字

季度

日期的季度:1月-3月 = 1,4月-6月 = 2,等等。

days_in_month

datetime 月份中的天数

is_month_start

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

is_month_end

逻辑指示是否为月末(由频率定义)

is_quarter_start

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

is_quarter_end

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

is_year_start

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

is_year_end

逻辑指示是否为年末(由频率定义)

is_leap_year

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

备注

你可以使用 DatetimeIndex.isocalendar().week 来访问年份中的周信息。

此外,如果你有一个包含类似日期时间值的 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 小时,由于夏令时。然而,所有小时或更小的 DateOffset 子类(Hour, Minute, Second, Milli, Micro, Nano)表现得像 Timedelta 并尊重绝对时间。

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

# 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

None

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

BDayBusinessDay

'B'

工作日(平日)

CDayCustomBusinessDay

'C'

自定义营业日

Week

'W'

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

WeekOfMonth

'WOM'

每个月的第 y 周的第 x 天

LastWeekOfMonth

'LWOM'

每个月最后一周的第x天

MonthEnd

'ME'

calendar month end

MonthBegin

'MS'

日历月开始

BMonthEndBusinessMonthEnd

'BME'

业务月末

BMonthBeginBusinessMonthBegin

'BMS'

业务月开始

CBMonthEndCustomBusinessMonthEnd

'CBME'

自定义业务月末

CBMonthBeginCustomBusinessMonthBegin

'CBMS'

自定义业务月开始

SemiMonthEnd

'SME'

15日(或其他day_of_month)和月末

SemiMonthBegin

'SMS'

15日(或其他日期)和日历月开始

QuarterEnd

'QE'

calendar quarter end

QuarterBegin

'QS'

calendar quarter begin

BQuarterEnd

'BQE

业务季度末

BQuarterBegin

'BQS'

业务季度开始

FY5253Quarter

'REQ'

零售(即52-53周)季度

YearEnd

'YE'

calendar year end

YearBegin

'YS''BYS'

calendar year begin

BYearEnd

'BYE'

业务年度结束

BYearBegin

'BYS'

业务年度开始

FY5253

'RE'

零售(即52-53周)年

Easter

None

复活节假期

BusinessHour

'bh'

营业时间

CustomBusinessHour

'cbh'

自定义营业时间

Day

'D'

一整天

Hour

'h'

一小时

Minute

'min'

一分钟

Second

's'

一秒钟

Milli

'ms'

一毫秒

Micro

'us'

一微秒

Nano

'ns'

一纳秒

DateOffsets 另外还有 rollforward()rollback() 方法,用于分别将日期向前或向后移动到相对于偏移量的有效偏移日期。例如,业务偏移量会将落在周末(周六和周日)的日期向前滚动到周一,因为业务偏移量在工作日操作。

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

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

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

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

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

这些操作默认保留时间(小时、分钟等)信息。要将时间重置为午夜,请在应用操作之前或之后使用 :meth:`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]

如果偏移类直接映射到一个 TimedeltaDay, Hour, Minute, Second, Micro, Milli, Nano),它可以像 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 表示的 str 或者一个 datetime.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

每周频率

月末频率

SME

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

BME

业务月末频率

CBME

自定义业务月末频率

MS

月开始频率

SMS

半月开始频率(1号和15号)

BMS

业务月开始频率

CBMS

自定义业务月开始频率

QE

季度末频率

BQE

业务季度末频率

QS

季度开始频率

BQS

业务季度开始频率

YE

年终频率

再见

业务年度结束频率

YS

年 开始 频率

BYS

业务年度开始频率

h

每小时频率

bh

营业时间频率

cbh

自定义营业时间频率

最小

每分钟频率

s

其次 频率

ms

毫秒

us

微秒

ns

纳秒

自 2.2.0 版本弃用: 别名 H, BH, CBH, T, S, L, U, 和 N 已被弃用,取而代之的是别名 h, bh, cbh, min, s, ms, us, 和 ns

别名 Y, M, 和 Q 已被弃用,取而代之的是别名 YE, ME, QE

备注

使用上述偏移别名时,需要注意的是,诸如 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

每小时频率

最小

每分钟频率

s

其次 频率

ms

毫秒

us

微秒

ns

纳秒

自 2.2.0 版本弃用: 别名 H, T, S, L, U, 和 N 已被弃用,取而代之的是别名 h, min, s, ms, us, 和 ns

结合别名#

正如我们之前所见,别名和偏移实例在大多数功能中是可互换的:

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

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

你可以将日和日内偏移量结合起来:

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

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

锚定的偏移#

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

别名

描述

W-SUN

每周频率(星期日)。与 ‘W’ 相同

W-MON

每周频率(周一)

W-TUE

每周频率(周二)

W-WED

每周频率(周三)

W-THU

每周频率(星期四)

W-FRI

每周频率(周五)

W-SAT

每周频率(周六)

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

季度频率,年末在12月。与 ‘QE’ 相同

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

季度频率,年份结束于一月

(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

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

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

年度频率,固定在12月底。与 ‘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

年度频率,固定在11月底

这些可以作为 date_rangebdate_range 的参数,用于 DatetimeIndex 的构造函数,以及 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日)发生的节日,如果节日落在周末或其他非观察日,观察规则决定了该节日的观察日期。定义的观察规则有:

规则

描述

next_workday

将周六和周日移至周一

previous_workday

将周六和周日移到周五

nearest_workday

将星期六移到星期五,星期日移到星期一

before_nearest_workday

应用 nearest_workday 然后在该日之前移至前一个工作日

after_nearest_workday

应用 nearest_workday 然后在那一天之后移到下一个工作日

sunday_to_monday

将星期日移到下一个星期一

next_monday_or_tuesday

将星期六移到星期一,星期日/星期一移到星期二

previous_friday

将周六和周日移到上一个周五

next_monday

将周六和周日移至接下来的周一

weekend_to_monday

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 0xffff04745bd0>),
 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 0xffff04745bd0>),
 Holiday: Columbus Day (month=10, day=1, offset=<DateOffset: weekday=MO(+2)>)]

重采样#

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

resample() 是一个基于时间的分组,然后对其每个组进行缩减方法。查看一些 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 可用的内置方法都可以作为返回对象的方法使用,包括 summeanstdsemmaxminmedianfirstlastohlc

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 的默认值对于所有频率偏移都是 ‘left’,除了 ‘ME’, ‘YE’, ‘QE’, ‘BME’, ‘BYE’, ‘BQE’, 和 ‘W’ 这些都有默认值 ‘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 是一个基于时间的分组,以下是一种仅对非全部为 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 实例。类似于 聚合 API分组 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                    B                    C          
                           sum      mean        sum      mean        sum      mean
2012-01-01 00:00:00  -6.088060 -0.033823 -21.872530 -0.121514 -14.660515 -0.081447
2012-01-01 00:03:00  10.243678  0.056909  26.411633  0.146731  -4.377642 -0.024320
2012-01-01 00:06:00 -10.590584 -0.058837   8.468289  0.047046  -9.363825 -0.052021
2012-01-01 00:09:00  11.362228  0.063123  -4.708526 -0.026158 -11.975895 -0.066533
2012-01-01 00:12:00  33.541257  0.186340  -0.565895 -0.003144  13.455299  0.074752
2012-01-01 00:15:00  -8.595393 -0.085954  -1.628689 -0.016287  -5.004580 -0.050046

通过将字典传递给 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("MS", on="date")[["a"]].sum()
Out[327]: 
            a
date         
2015-01-01  6
2015-02-01  4

同样地,如果你想按 MultiIndex 的日期时间级别重新采样,可以将它的名称或位置传递给 level 关键字。

In [328]: df.resample("MS", level="d")[["a"]].sum()
Out[328]: 
            a
d            
2015-01-01  6
2015-02-01  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

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

向后重采样#

Added in version 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, us, 和 ns),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 period.pyx:1827, in pandas._libs.tslibs.period._Period._add_timedeltalike_scalar()

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

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

ValueError: Cannot losslessly convert units

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

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

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

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

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

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

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

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

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

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

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

IncompatibleFrequency: Input has different freq=3MS 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,请参阅 周期数据类型

周期数据类型#

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

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(...) 中使用。它允许更改 PeriodIndexfreq,类似于 .asfreq(),并将 DatetimeIndex 转换为 PeriodIndex,类似于 to_period()

# 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 提供了对使用 zoneinfopytzdateutil 库或标准库中的 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 关键字参数。您可以传递 zoneinfopytzdateutil 时区对象或 Olson 时区数据库字符串。默认情况下,Olson 时区字符串将返回 pytz 时区对象。要返回 dateutil 时区对象,请在字符串前加上 dateutil/

  • 对于 zoneinfo,可以从 zoneinfo.available_timezones() 获取可用时区列表。

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

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

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]: zoneinfo.ZoneInfo(key='Europe/London')

# 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]: zoneinfo.ZoneInfo(key='US/Pacific')

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

In [458]: ts.tz
Out[458]: zoneinfo.ZoneInfo(key='US/Pacific')

警告

注意库之间的转换。对于某些时区,pytzdateutil 对时区的定义不同。这对于不寻常的时区比对像 US/Eastern 这样的’标准’时区问题更大。

警告

请注意,不同版本的时间区域库中的时间区域定义可能不被视为相等。这可能会在使用一个版本本地化的存储数据与使用不同版本操作时引起问题。请参阅 这里 了解如何处理这种情况。

警告

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

警告

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

警告

如果你在使用 pytz 时使用超过 2038-01-18 的日期,由于当前底层库因 2038 年问题导致的缺陷,时区感知日期的夏令时(DST)调整将不会被应用。当底层库被修复时,DST 转换将被应用。

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

In [459]: import pytz

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

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

In [462]: DST = pytz.timezone("Europe/London")

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

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

在底层,所有时间戳都存储在UTC中。来自时区感知 DatetimeIndexTimestamp 的值将使其字段(日、小时、分钟等)本地化为时区。然而,即使它们在不同时区,具有相同UTC值的时间戳仍被认为是相等的:

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

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

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

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

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

不同时区中的 Series 之间的操作将产生 UTC Series,将数据对齐到 UTC 时间戳:

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

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

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

In [473]: result = eastern + berlin

In [474]: result
Out[474]: 
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 [475]: result.index
Out[475]: 
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 [476]: didx = pd.date_range(start="2014-08-01 09:00", freq="h", periods=3, tz="US/Eastern")

In [477]: didx
Out[477]: 
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 [478]: didx.tz_localize(None)
Out[478]: 
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 [479]: didx.tz_convert(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='h')

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

Fold#

对于模糊的时间,pandas 支持显式指定仅限关键字的 fold 参数。由于夏令时,当从夏季时间转换到冬季时间时,一个挂钟时间可能会出现两次;fold 描述了 datetime-like 对应的是挂钟第一次(0)还是第二次(1)击中模糊时间。fold 仅在从天真的 datetime.datetime 构建时支持(详见 datetime 文档),或从 Timestamp 构建,或从组件构建(见下文)。仅支持 dateutil 时区(详见 dateutil 文档 中处理模糊日期时间的方法),因为 pytz 时区不支持 fold(详见 pytz 文档pytz 如何处理模糊日期时间的细节)。要使用 pytz 本地化模糊的日期时间,请使用 Timestamp.tz_localize()。一般来说,我们建议在需要直接控制如何处理模糊日期时间时,依赖 Timestamp.tz_localize()

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

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

本地化时的模糊时间#

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

  • 'raise': 引发一个 ValueError (默认行为)

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

  • 'NaT': 用 NaT 替换不明确的时间

  • bool: True 表示夏令时时间,False 表示非夏令时时间。支持 bool 值的类数组序列用于一系列时间。

In [483]: 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 [484]: rng_hourly.tz_localize('US/Eastern')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[484], line 1
----> 1 rng_hourly.tz_localize('US/Eastern')

File /home/pandas/pandas/core/indexes/datetimes.py:280, in DatetimeIndex.tz_localize(self, tz, ambiguous, nonexistent)
    273 @doc(DatetimeArray.tz_localize)
    274 def tz_localize(
    275     self,
   (...)
    278     nonexistent: TimeNonexistent = "raise",
    279 ) -> Self:
--> 280     arr = self._data.tz_localize(tz, ambiguous, nonexistent)
    281     return type(self)._simple_new(arr, name=self.name)

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

File /home/pandas/pandas/core/arrays/datetimes.py:1097, in DatetimeArray.tz_localize(self, tz, ambiguous, nonexistent)
   1094     tz = timezones.maybe_get_tz(tz)
   1095     # Convert to UTC
-> 1097     new_dates = tzconversion.tz_localize_to_utc(
   1098         self.asi8,
   1099         tz,
   1100         ambiguous=ambiguous,
   1101         nonexistent=nonexistent,
   1102         creso=self._creso,
   1103     )
   1104 new_dates_dt64 = new_dates.view(f"M8[{self.unit}]")
   1105 dtype = tz_to_dtype(tz, unit=self.unit)

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

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

通过指定以下内容来处理这些模棱两可的时刻。

In [485]: rng_hourly.tz_localize("US/Eastern", ambiguous="infer")
Out[485]: 
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[s, US/Eastern]', freq=None)

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

In [487]: rng_hourly.tz_localize("US/Eastern", ambiguous=[True, True, False, False])
Out[487]: 
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[s, US/Eastern]', freq=None)

本地化时的不存在时间#

夏令时转换也可能将本地时间提前1小时,创造出不存在的本地时间(”时钟向前跳”)。通过 nonexistent 参数可以控制本地化一个包含不存在时间的时序的行为。以下选项可用:

  • 'raise': 引发一个 ValueError (默认行为)

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

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

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

  • timedelta 对象:将不存在的时间按 timedelta 时长移动

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

# 2:30 is a nonexistent time

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

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

File /home/pandas/pandas/core/indexes/datetimes.py:280, in DatetimeIndex.tz_localize(self, tz, ambiguous, nonexistent)
    273 @doc(DatetimeArray.tz_localize)
    274 def tz_localize(
    275     self,
   (...)
    278     nonexistent: TimeNonexistent = "raise",
    279 ) -> Self:
--> 280     arr = self._data.tz_localize(tz, ambiguous, nonexistent)
    281     return type(self)._simple_new(arr, name=self.name)

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

File /home/pandas/pandas/core/arrays/datetimes.py:1097, in DatetimeArray.tz_localize(self, tz, ambiguous, nonexistent)
   1094     tz = timezones.maybe_get_tz(tz)
   1095     # Convert to UTC
-> 1097     new_dates = tzconversion.tz_localize_to_utc(
   1098         self.asi8,
   1099         tz,
   1100         ambiguous=ambiguous,
   1101         nonexistent=nonexistent,
   1102         creso=self._creso,
   1103     )
   1104 new_dates_dt64 = new_dates.view(f"M8[{self.unit}]")
   1105 dtype = tz_to_dtype(tz, unit=self.unit)

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

ValueError: 2015-03-29 02:30:00 is a nonexistent time due to daylight savings time. Try using the 'nonexistent' argument.

将不存在的时间转换为 NaT 或移动时间。

In [490]: dti
Out[490]: 
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 [491]: dti.tz_localize("Europe/Warsaw", nonexistent="shift_forward")
Out[491]: 
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 [492]: dti.tz_localize("Europe/Warsaw", nonexistent="shift_backward")
Out[492]: 
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 [493]: dti.tz_localize("Europe/Warsaw", nonexistent=pd.Timedelta(1, unit="h"))
Out[493]: 
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 [494]: dti.tz_localize("Europe/Warsaw", nonexistent="NaT")
Out[494]: 
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)

时区系列操作#

一个带有时间区**naive**值的 Seriesdatetime64[ns] 的 dtype 表示。

In [495]: s_naive = pd.Series(pd.date_range("20130101", periods=3))

In [496]: s_naive
Out[496]: 
0   2013-01-01
1   2013-01-02
2   2013-01-03
dtype: datetime64[ns]

一个带有时间戳 aware 值的 Seriesdatetime64[ns, tz] 的 dtype 表示,其中 tz 是时区

In [497]: s_aware = pd.Series(pd.date_range("20130101", periods=3, tz="US/Eastern"))

In [498]: s_aware
Out[498]: 
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 [499]: s_naive.dt.tz_localize("UTC").dt.tz_convert("US/Eastern")
Out[499]: 
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 方法进行操作。此方法可以在不同的时区感知 dtypes 之间进行转换。

# convert to a new time zone
In [500]: s_aware.astype("datetime64[ns, CET]")
Out[500]: 
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 目前不支持时区(即使它在本地时区 打印 !),因此对于有时区意识的数据,返回的是时间戳的对象数组:

In [501]: s_naive.to_numpy()
Out[501]: 
array(['2013-01-01T00:00:00.000000000', '2013-01-02T00:00:00.000000000',
       '2013-01-03T00:00:00.000000000'], dtype='datetime64[ns]')

In [502]: s_aware.to_numpy()
Out[502]: 
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)

通过转换为时间戳对象数组,它会保留时区信息。例如,当转换回一个系列时:

In [503]: pd.Series(s_aware.to_numpy())
Out[503]: 
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 [504]: s_aware.to_numpy(dtype="datetime64[ns]")
Out[504]: 
array(['2013-01-01T05:00:00.000000000', '2013-01-02T05:00:00.000000000',
       '2013-01-03T05:00:00.000000000'], dtype='datetime64[ns]')