怎么用Pandas聚合时间序列数据?
处理时间序列数据本身就是一个挑战。时间序列是一种特殊的数据,其数据点在时间上存在相关性。在分析时间序列数据时,你得到结论的效率很大程度上取决于处理时间维度的能力。
通常,时间序列数据是需要长期收集的,尤其是当这些数据是来自硬件设备、或表示金融交易的时候。此外,就算数据集中没有字段为“null”,如果时间戳的间隔不规律,或出现移位、丢失或不一致,数据仍然可能存在问题。
如果你想要从时间相关的数据中学习有用信息,有一个技能非常重要,那就是有效地进行聚合。这样,不仅可以大大减少处理的数据总量,还有助于更快地发现有趣的的的事实。
在本文中,我想介绍几种方法,用于分析当前最流行的Python数据处理库—Pandas 是如何帮助你执行这些聚合的,以及在处理时间时,有什么特别之处。除此之外,我还会列出一个 SQL 的等价语法以供你参考。 如果你想了解更多数据分析相关内容,可以阅读以下这些文章:
如何在Pandas里写SQL查询语句?
如何用Pandas 三步清洗数据?
一文上手用Pandas给数据加标签
SQL和Pandas同时掉到河里,你先救谁?
数据
The data
为了演示,我使用了 Kaggle 的信用卡交易数据集。尽管我们也可以聚合更多条件,但为了简便起见,我会把重点放在“金额”这一列上,并按照单个用户对其进行筛选。有关时间的信息分布在“Year”、“Month”、“Day”和“Time”列中,因此,使用单个列来表示时间是更有意义的。
由于整个数据集约 2.35 GB,我们可以以较小的批次动态转换数据。
import pandas as pd
import numpy as np
from tqdm import tqdm
from pathlib import Path
SRC = Path("data/credit_card_transactions-ibm_v2.csv")
DST = Path("data/transactions.csv")
USER = 0
def load(filepath=SRC):
data = pd.read_csv(
filepath,
iterator=True,
chunksize=10000,
usecols=["Year", "Month", "Day", "Time", "Amount"],
)
for df in tqdm(data):
yield df
def process(df):
_df = df.query("User == @USER")
ts = _df.apply(
lambda x: f"{x['Year']}{x['Month']:02d}{x['Day']:02d} {x['Time']}",
axis=1,
)
_df["timestmap"] = pd.to_datetime(ts)
_df["amount"] = df["Amount"].str.strip("$").astype(float)
return _df.get(["timestamp", "amount"])
def main():
for i, df in enumerate(load()):
df = process(df)
df.to_csv(
DST,
mode="a" if i else "w",
header=not(bool(i)),
index=False,
)
if __name__ == "__main__":
main()
…以上代码会得出:
| timestamp | amount |
|:--------------------|---------:|
| 2002-09-01 06:21:00 | 134.09 |
| 2002-09-01 06:42:00 | 38.48 |
| 2002-09-02 06:22:00 | 120.34 |
| 2002-09-02 17:45:00 | 128.95 |
| 2002-09-03 06:23:00 | 104.71 |
根据这个数据的“head”,我们得出上面的表格。对于单个用户(此处 USER = 0),我们有近 2 万个时间戳,以一分钟的频率标记 2002 年到 2020 年之间的交易。
多亏用了第 31 行中的 pd.to_datetime,我们合并了四列的数据,并将其存储为 np.datetime64 变量,该变量能统一描述时间。
什么是 np.datetime64?
What is np.datetime64?
np.datetime64 是 pythonic datetime.datetime 对象的 numpy 版本。它是向量化的,因此可以快速对整个数组执行操作。同时,该对象可以识别典型的datetime,这有助于系统自然地操作这些值。
Pandas中,时间相关的对象有Timestamp、Timedelta和Period(与DatetimeIndex、TimedeltaIndex和PeriodIndex相对应),它们分别描述了时间、时间偏移和时间跨度上的时刻。当然还有 np.datetime64s(以及类似的 np.timedelta64 s),都非常方便使用。
分析时间序列,需要先将时间相关的值转换为这些对象,这些方法既方便又快捷。
基本重采样
Basic resampling
最简单的时间序列聚合形式,就是使用聚合函数,将值输入等间隔的容器中,有助于调整分辨率和数据量。
以下代码展示了如何使用sum 和 count两个函数来重采样天数:
SELECT
sum(amount),
count(amount),
DATE(timestamp) AS dt
FROM transactions
GROUP BY dt;
Pandas 给我们提供了至少两种方法来达到同样的结果:
# option 1
df["amount"].resample("D").agg(["sum", "count"])
# option 2
df["amount"].groupby(pd.Grouper(level=0, freq="D")) \
.agg(["sum", "count"])
这两个选项是等价的。第一个选项更简单,因为timestamp列是这个数据的index,当然,也可以使用可选参数来指向其他的列。第二个使用更通用的聚合函数—pd.Grouper 和 .groupby 方法。这个方法具备高度的可定制性,可以同时具有许多可选参数。在这里,我使用的是level而不是key,因为timestamp是index。此外,freq=”D” 代表天数。你也可以使用其他代码,尽管类似的 SQL 语句可能更复杂。
多个时间跨度的聚合
Aggregations over several time spans
假设你想要聚合时间戳中多个部分的数据,例如(年、周)或(月、星期几、小时)。由于时间戳是 np.datetime64 类型,因此,你可以使用 .dt 访问器生成聚合指令。
在 SQL 中,操作如下:
SELECT
AVG(amount),
STRFTIME('%Y %W', timestamp) AS yearweek
FROM transactions
GROUP BY yearweek
在Pandas中,有两种方法,操作如下:
df = df.reset_index() # if we want `timestamp` to be a column
df["amount"].groupby(by=[
df["timestamp"].dt.year,
df["timestamp"].dt.isocalendar().week
]).mean()
df = df.set_index("timestamp") # if we want `timestamp` to be index
df["amount"].groupby(by=[
df.index.year,
df.index.isocalendar().week,
]).mean()
它们做的事情是一样的。
| | amount |
|:-----------|---------:|
| (2002, 1) | 40.7375 |
| (2002, 35) | 86.285 |
| (2002, 36) | 82.3733 |
| (2002, 37) | 72.2048 |
| (2002, 38) | 91.8647 |
值得一提的是,.groupby 方法并不会强制使用聚合函数。这种方法只是将数据框切成许多的小部分。你可能还想使用单独的“子框”,然后直接进行转换。在这种情况下,只需加上以下指令:
for key, group in df.groupby(by=[
df.index.year,
df.index.isocalendar().week
]):
pass
这里的key是(年,周),而group是子框。
评述
Remark
值得一提的是, SQL 和 Pandas 风格不同,所以时间窗口的边界的定义也可能不同。当使用 SQLite 进行比较时,每种方法得出的结果略有不同。
SQL:
SELECT
STRFTIME('%Y %W %w', timestamp),
timestamp
FROM TRANSACTIONS
LIMIT 5;
--gives:
| timestamp | year | week | day |
|:--------------------|-----:|-----:|----:|
| 2002-09-01 06:21:00 | 2002 | 34 | 0 |
| 2002-09-01 06:42:00 | 2002 | 34 | 0 |
| 2002-09-02 06:22:00 | 2002 | 35 | 1 |
| 2002-09-02 17:45:00 | 2002 | 35 | 1 |
| 2002-09-03 06:23:00 | 2002 | 35 | 2 |
Pandas:
df.index.isocalendar().head()
# gives:
| timestamp | year | week | day |
|:--------------------|-------:|-------:|------:|
| 2002-09-01 06:21:00 | 2002 | 35 | 7 |
| 2002-09-01 06:42:00 | 2002 | 35 | 7 |
| 2002-09-02 06:22:00 | 2002 | 36 | 1 |
| 2002-09-02 17:45:00 | 2002 | 36 | 1 |
| 2002-09-03 06:23:00 | 2002 | 36 | 2 |
这两者概念是一样的,只是时间的参考不同。
窗口函数
Window Functions
最后一种通常用于时间数据的聚合函数就是滚动窗口(Rolling Window)。与按某些列的值来分组行相反,此方法定义了行间隔,以选取子表、移动窗口、并再次进行此操作。
让我们看一个计算连续5行移动平均值的示例。在 SQL 中,语法如下:
SELECT
timestamp,
AVG(amount) OVER (
ORDER BY timestamp
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
) rolling_avg
FROM transactions;
Pandas 中的语法更简单:
# applying mean immediatiely
df["amount"].rolling(5).mean()
# accessing the chunks directly
for chunk in df["amount"].rolling(5):
pass
同样,在 Pandas 中,你可以通过可选参数进行不同的调整。窗口的大小由 window 的属性决定,在SQL中,这是由一系列语句实现的(第5行)。 此外,我们可能还想把将窗口居中,或使用不同的窗口,例如:加权平均,或进行可选的数据清理。但是,.rolling 方法返回的 pd.Rolling 对象的用法,在某种意义上与 pd.DataFrameGroupBy 对象类似。
结论
Conclusions
本文介绍了在处理时间序列数据时经常使用的三种聚合。虽然并不是所有包含时间信息的数据都是时间序列,但对于时间序列来说,将时间信息转换为 pd.Timestamp 或其他类似的对象大部分时候都是有帮助的,这些对象在下面实现了 numpy 的 np.datetime64 对象。你能看到,这使得跨越不同时间属性的聚合变得非常方便、直观和有趣。感谢你的阅读!你还可以订阅我们的YouTube频道,观看大量数据科学相关公开课:https://www.youtube.com/channel/UCa8NLpvi70mHVsW4J_x9OeQ;在LinkedIn上关注我们,扩展你的人际网络!https://www.linkedin.com/company/dataapplab/
最初发布于 https://zerowithdot.com.
原文作者:Oleg Zero
翻译作者:Lia
美工编辑:过儿
校对审稿:Jiawei Tong
原文链接:https://towardsdatascience.com/aggregations-on-time-series-data-with-pandas-5c79cc24a449