怎么用Pandas聚合时间序列数据?

怎么用Pandas聚合时间序列数据?

处理时间序列数据本身就是一个挑战。时间序列是一种特殊的数据,其数据点在时间上存在相关性。在分析时间序列数据时,你得到结论的效率很大程度上取决于处理时间维度的能力。

通常,时间序列数据是需要长期收集的,尤其是当这些数据是来自硬件设备、或表示金融交易的时候。此外,就算数据集中没有字段为“null”,如果时间戳的间隔不规律,或出现移位、丢失或不一致,数据仍然可能存在问题。

《等待一个美好的时刻,波兰 2021》图片由作者提供

如果你想要从时间相关的数据中学习有用信息,有一个技能非常重要,那就是有效地进行聚合。这样,不仅可以大大减少处理的数据总量,还有助于更快地发现有趣的的的事实。

在本文中,我想介绍几种方法,用于分析当前最流行的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