在pandas中对时间序列数据进行索引

2,737 阅读13分钟

很多时候,我们想要分析的数据有一个基于时间的部分。想想像每日温度或降雨量、股票价格、销售数据、学生出勤率等数据,或者像网络应用程序的点击或浏览等事件。数据的来源并不缺乏,而且新的来源一直在增加。因此,大多数pandas用户都需要在某个时候熟悉时间序列数据。

一个时间序列只是一个pandasDataFrameSeries ,它有一个基于时间的索引。时间序列中的值可以是其他任何可以包含在容器中的东西,它们只是用日期或时间值来访问。在pandas中,一个时间序列容器可以用很多方式来操作,但在这篇文章中,我将只关注索引的基础知识。首先了解索引的工作原理对于数据探索和使用更高级的功能非常重要。

日期索引(DatetimeIndex

在pandas中,DatetimeIndex 是用来为pandasSeriesDataFrames提供索引的,其工作原理与其他Index 类型一样,但为时间序列操作提供了特殊功能。我们将首先介绍与其他Index 类型的共同功能,然后谈谈部分字符串索引的基础知识。

在我们开始之前有一个警告。你的索引必须要排序,否则你可能会得到一些奇怪的结果。

例子

为了显示这个功能是如何工作的,让我们创建一些具有不同时间分辨率的样本时间序列数据。

import pandas as pd
import numpy as np

import datetime

# this is an easy way to create a DatetimeIndex
# both dates are inclusive
d_range = pd.date_range("2021-01-01", "2021-01-20")

# this creates another DatetimeIndex, 10000 minutes long
m_range = pd.date_range("2021-01-01", periods=10000, freq="T")

# daily data in a Series
daily = pd.Series(np.random.rand(len(d_range)), index=d_range)
# minute data in a DataFrame
minute = pd.DataFrame(np.random.rand(len(m_range), 1),
                      columns=["value"],
                      index=m_range)

# time boundaries not on the minute boundary, add some random jitter
mr_range = m_range + pd.Series([pd.Timedelta(microseconds=1_000_000.0 * s)
                                for s in np.random.rand(len(m_range))]) 
# minute data in a DataFrame, but at a higher resolution
minute2 = pd.DataFrame(np.random.rand(len(mr_range), 1),
                       columns=["value"],
                       index=mr_range)
daily.head()
2021-01-01    0.293300
2021-01-02    0.921466
2021-01-03    0.040813
2021-01-04    0.107230
2021-01-05    0.201100
Freq: D, dtype: float64
minute.head()
                        value
2021-01-01 00:00:00  0.124186
2021-01-01 00:01:00  0.542545
2021-01-01 00:02:00  0.557347
2021-01-01 00:03:00  0.834881
2021-01-01 00:04:00  0.732195
minute2.head()
                               value
2021-01-01 00:00:00.641049  0.527961
2021-01-01 00:01:00.088244  0.142192
2021-01-01 00:02:00.976195  0.269042
2021-01-01 00:03:00.922019  0.509333
2021-01-01 00:04:00.452614  0.646703

分辨率

一个DatetimeIndex 有一个分辨率,表明Index 对数据进行索引的水平。上面创建的三个指数有不同的分辨率。这将对我们以后的索引方式产生影响。

print("daily:", daily.index.resolution)
print("minute:", minute.index.resolution)
print("randomized minute:", minute2.index.resolution)
daily: day
minute: minute
randomized minute: microsecond

典型的索引编制

在我们进入一些 "特殊 "的方式来为pandasSeriesDataFrameDatetimeIndex 建立索引之前,让我们先看看一些典型的索引功能。

基础知识

我之前已经介绍了索引的基础知识,所以我不会在这里介绍太多的细节。然而,重要的是要意识到,DatetimeIndex ,就像pandas中的其他索引一样工作,但有额外的功能。额外的功能可能更有用,更方便,但请抓紧时间,这些细节是下一步要做的)。如果你已经了解了基本的索引,你可能想略过,直到你得到部分字符串的索引。如果你没有读过我关于索引的文章,你应该从基本知识开始,然后从那里开始。

使用类似datetime 的对象对DatetimeIndex 进行索引将使用精确索引

getitem 又称数组索引运算符 ([])

当使用datetime-like对象进行索引时,我们需要匹配索引的分辨率。

对于我们的每日时间序列来说,这最终看起来相当明显。

daily[pd.Timestamp("2021-01-01")]
0.29330017699861666
try:
    minute[pd.Timestamp("2021-01-01 00:00:00")]
except KeyError as ke:
    print(ke)
Timestamp('2021-01-01 00:00:00')

这个KeyError ,因为在DataFrame ,使用单一参数的[] 操作符将寻找一个_列_,而不是一个行。在我们的DataFrame ,我们有一个名为value 的单列,所以上面的代码正在寻找一个列。因为没有一个叫这个名字的列,所以有一个KeyError 。我们将使用其他方法对DataFrame 中的行进行索引。

.iloc 索引

由于iloc 索引器是基于整数偏移的,所以它的工作原理很清楚,这里就不多说了。它对所有分辨率的工作原理都是一样的。

daily.iloc[0]
0.29330017699861666
minute.iloc[-1]
value    0.999354
Name: 2021-01-07 22:39:00, dtype: float64
minute2.iloc[4]
value    0.646703
Name: 2021-01-01 00:04:00.452614, dtype: float64

.loc 索引

当使用datetime-样的对象时,你需要对单个索引进行精确匹配。重要的是要认识到,当你制作datetimepd.Timestamp 对象时,所有你没有明确指定的字段都将默认为0。

jan1 = datetime.datetime(2021, 1, 1)
daily.loc[jan1]
0.29330017699861666
minute.loc[jan1]  # the defaults for hour, minute, second make this work
value    0.124186
Name: 2021-01-01 00:00:00, dtype: float64
try:
    # we don't have that exact time, due to the jitter
    minute2.loc[jan1] 
except KeyError as ke:
    print("Missing in index: ", ke)
# but we do have a value on that day
# we could construct it manually to the microsecond if needed
jan1_ms = datetime.datetime(2021, 1, 1, 0, 0, 0, microsecond=minute2.index[0].microsecond)
minute2.loc[jan1_ms] 
Missing in index:  datetime.datetime(2021, 1, 1, 0, 0)
value    0.527961
Name: 2021-01-01 00:00:00.641049, dtype: float64

分片

用整数切片的工作方式和预期的一样,你可以在这里阅读更多关于常规切片的信息。但这里有几个 "常规 "切片的例子,它与数组索引操作符 ([]) 或.iloc 索引器一起工作。

daily[0:2] # first two, end is not inclusive
2021-01-01    0.293300
2021-01-02    0.921466
Freq: D, dtype: float64
minute[0:2] # same
                        value
2021-01-01 00:00:00  0.124186
2021-01-01 00:01:00  0.542545
minute2[1:5:2]  # every other
                               value
2021-01-01 00:01:00.088244  0.142192
2021-01-01 00:03:00.922019  0.509333
minute2.iloc[1:5:2] # works with the iloc indexer as well
                               value
2021-01-01 00:01:00.088244  0.142192
2021-01-01 00:03:00.922019  0.509333

datetime-样的对象进行切片也可以。请注意,结束项是包括在内的,小时、分钟、秒和微秒的默认值将在分钟边界上设置随机数据的截止点(在我们的例子中)。

daily[datetime.date(2021,1,1):datetime.date(2021, 1,3)] # end is inclusive
2021-01-01    0.293300
2021-01-02    0.921466
2021-01-03    0.040813
Freq: D, dtype: float64
minute[datetime.datetime(2021, 1, 1): datetime.datetime(2021, 1, 1, 0, 2, 0)]
                        value
2021-01-01 00:00:00  0.124186
2021-01-01 00:01:00  0.542545
2021-01-01 00:02:00  0.557347
minute2[datetime.datetime(2021, 1, 1): datetime.datetime(2021, 1, 1, 0, 2, 0)]
                               value
2021-01-01 00:00:00.641049  0.527961
2021-01-01 00:01:00.088244  0.142192

这种切分在[].loc ,但不包括.iloc ,正如预期的那样。记住,.iloc 是用于整数偏移量的索引。

minute2.loc[datetime.datetime(2021, 1, 1): datetime.datetime(2021, 1, 1, 0, 2, 0)]
                               value
2021-01-01 00:00:00.641049  0.527961
2021-01-01 00:01:00.088244  0.142192
try:
    # no! use integers with iloc
    minute2.iloc[datetime.datetime(2021, 1, 1): datetime.datetime(2021, 1, 1, 0, 2, 0)]
except TypeError as te:
    print(te)
cannot do positional indexing on DatetimeIndex with these indexers [2021-01-01 00:00:00] of type datetime

字符串的特殊索引

现在事情变得非常有趣和有用。当处理时间序列数据时,部分字符串索引可能非常有用,而且比使用datetime 对象要省事得多。我知道我们从对象开始,但现在你看到,对于交互式使用和探索,字符串是非常有用的。你可以传入一个可以被解析为完整日期的字符串,并且它可以用于索引。

daily["2021-01-04"]
0.10723013753233923
minute.loc["2021-01-01 00:03:00"]
value    0.834881
Name: 2021-01-01 00:03:00, dtype: float64

字符串也可以用于分片。

minute.loc["2021-01-01 00:03:00":"2021-01-01 00:05:00"] # end is inclusive
                        value
2021-01-01 00:03:00  0.834881
2021-01-01 00:04:00  0.732195
2021-01-01 00:05:00  0.291089

部分字符串的索引

也可以使用部分字符串,所以你只需要指定部分数据。这对于从一个较长的数据集中提取单个年份、月份或日期是很有用的。

daily["2021"]    # all items match (since they were all in 2021)
daily["2021-01"] # this one as well (and only in January for our data)
2021-01-01    0.293300
2021-01-02    0.921466
2021-01-03    0.040813
2021-01-04    0.107230
2021-01-05    0.201100
2021-01-06    0.534822
2021-01-07    0.070303
2021-01-08    0.413683
2021-01-09    0.316605
2021-01-10    0.438853
2021-01-11    0.258554
2021-01-12    0.473523
2021-01-13    0.497695
2021-01-14    0.250582
2021-01-15    0.861521
2021-01-16    0.589558
2021-01-17    0.574399
2021-01-18    0.951196
2021-01-19    0.967695
2021-01-20    0.082931
Freq: D, dtype: float64

你也可以在DataFrame 上这样做。

minute["2021-01-01"]
<ipython-input-67-96027d36d9fe>:1: FutureWarning: Indexing a DataFrame with a datetimelike index using a single string to slice the rows, like `frame[string]`, is deprecated and will be removed in a future version. Use `frame.loc[string]` instead.
  minute["2021-01-01"]
                        value
2021-01-01 00:00:00  0.124186
2021-01-01 00:01:00  0.542545
2021-01-01 00:02:00  0.557347
2021-01-01 00:03:00  0.834881
2021-01-01 00:04:00  0.732195
...                       ...
2021-01-01 23:55:00  0.687931
2021-01-01 23:56:00  0.001978
2021-01-01 23:57:00  0.770587
2021-01-01 23:58:00  0.154300
2021-01-01 23:59:00  0.777973

[1440 rows x 1 columns]

看到那个废弃警告了吗?你不应该再使用[] 来进行DataFrame 字符串索引(正如我们在上面看到的,[] 应该用于列访问,而不是行)。根据是否在索引中找到该值,你可能会得到一个错误或一个警告。使用.loc ,这样你就可以避免混淆。

minute2.loc["2021-01-01"]
                               value
2021-01-01 00:00:00.641049  0.527961
2021-01-01 00:01:00.088244  0.142192
2021-01-01 00:02:00.976195  0.269042
2021-01-01 00:03:00.922019  0.509333
2021-01-01 00:04:00.452614  0.646703
...                              ...
2021-01-01 23:55:00.642728  0.749619
2021-01-01 23:56:00.238864  0.053027
2021-01-01 23:57:00.168598  0.598910
2021-01-01 23:58:00.103543  0.107069
2021-01-01 23:59:00.687053  0.941584

[1440 rows x 1 columns]

如果使用字符串切分,终点包括_一天中的所有时间_。

minute2.loc["2021-01-01":"2021-01-02"]
                               value
2021-01-01 00:00:00.641049  0.527961
2021-01-01 00:01:00.088244  0.142192
2021-01-01 00:02:00.976195  0.269042
2021-01-01 00:03:00.922019  0.509333
2021-01-01 00:04:00.452614  0.646703
...                              ...
2021-01-02 23:55:00.604411  0.987777
2021-01-02 23:56:00.134674  0.159338
2021-01-02 23:57:00.508329  0.973378
2021-01-02 23:58:00.573397  0.223098
2021-01-02 23:59:00.751779  0.685637

[2880 rows x 1 columns]

但是,如果我们包括时间,它将包括部分时期,如果指定了微秒,就会切断终点,直到微秒。

minute2.loc["2021-01-01":"2021-01-02 13:32:01"]
                               value
2021-01-01 00:00:00.641049  0.527961
2021-01-01 00:01:00.088244  0.142192
2021-01-01 00:02:00.976195  0.269042
2021-01-01 00:03:00.922019  0.509333
2021-01-01 00:04:00.452614  0.646703
...                              ...
2021-01-02 13:28:00.925951  0.969213
2021-01-02 13:29:00.037827  0.758476
2021-01-02 13:30:00.309543  0.473163
2021-01-02 13:31:00.363813  0.846199
2021-01-02 13:32:00.867343  0.007899

[2253 rows x 1 columns]

切片与精确匹配

我们的三个数据集在其索引中具有不同的分辨率:分别是日、分钟和微秒。如果我们传入一个字符串索引参数,而字符串的分辨率_不如_索引精确,它将被视为一个切片。如果它相同或更精确,就会被视为完全匹配。让我们使用我们的微秒 (minute2) 和分钟 (minute) 分辨率的数据例子。请注意,每当你得到一个切片DataFrame ,返回的值就是一个DataFrame 。当它是完全匹配时,它是一个Series

minute2.loc["2021-01-01"]          # slice - the entire day
minute2.loc["2021-01-01 00"]       # slice - the first hour of the day
minute2.loc["2021-01-01 00:00"]    # slice - the first minute of the day
minute2.loc["2021-01-01 00:00:00"] # slice - the first minute and second of the day
                               value
2021-01-01 00:00:00.641049  0.527961
print(str(minute2.index[0]))       # note the string representation include the full microseconds
minute2.loc[str(minute2.index[0])] # slice - this seems incorrect to me, should return Series not DataFrame
minute2.loc[minute2.index[0]]      # exact match
2021-01-01 00:00:00.641049
value    0.527961
Name: 2021-01-01 00:00:00.641049, dtype: float64
minute.loc["2021-01-01"]       # slice - the entire day
minute.loc["2021-01-01 00"]    # slice - the first hour of the day
minute.loc["2021-01-01 00:00"] # exact match
value    0.124186
Name: 2021-01-01 00:00:00, dtype: float64

请注意,对于微秒分辨率的字符串匹配,我没有看到精确匹配(返回值是Series ),而是一个片断匹配(因为返回值是DataFrame )。在分钟分辨率的DataFrame ,它如我预期的那样工作。

如同

处理这类问题的一个方法是使用asof 。通常情况下,当你的数据在时间上是随机的,或者可能有缺失的值,获取截至某一时间的最新值是最好的。你可以自己做这件事,但使用asof ,看起来更干净一些。

minute2.loc[:"2021-01-01 00:00:03"].iloc[-1]
# vs
minute2.asof("2021-01-01 00:00:03")
value    0.527961
Name: 2021-01-01 00:00:03, dtype: float64

截断

你也可以使用truncate ,这有点像切分。你指定一个beforeafter (或两者)的值来表示数据的截断。与切片不同的是,truncate 包括所有与日期部分匹配的值,对于任何未指定的日期值都假定为0。

minute2.truncate(after="2021-01-01 00:00:03")
                               value
2021-01-01 00:00:00.641049  0.527961

总结

现在你可以看到,时间序列数据的索引与pandas中其他类型的Index ,有一些不同。了解了时间序列的切分,你就可以快速浏览时间序列数据,并迅速进入更高级的时间序列分析。

The postIndexing time series data in pandasappeared first onwrighters.io.