【问题标题】:Speeding up past-60-day mean in pandas加快大熊猫过去 60 天的平均值
【发布时间】:2016-04-03 12:15:57
【问题描述】:

我使用了过去 kaggle 挑战赛中的数据,这些数据基于多家商店的面板数据,时间跨度为 2.5 年。每个观察都包括给定商店日期的客户数量。对于每个商店日期,我的目标是计算过去 60 天内访问该商店的平均顾客数量。

下面是完全符合我需要的代码。但是,它会永远存在 - 处理 c.800k 行需要一个晚上。我正在寻找一种聪明的方法来更快地实现相同的目标。

我已经包含了对初始数据集的 5 个观察结果以及相关变量:商店 ID(商店)、日期和客户数量(“客户”)。

注意:

  • 对于迭代中的每一行,我最终使用 .loc 而不是例如row["Lagged No of customers"] 因为“row”没有在单元格中写入任何内容。我想知道为什么会这样。
  • 我通常使用“应用,轴 = 1”填充新列,因此我非常感谢任何基于此的解决方案。我发现当对于每一行,使用同一行级别的值跨列进行计算时,“应用”工作正常。但是,我不知道“应用”函数如何涉及不同的行,而这正是这个问题所需要的。到目前为止,我看到的唯一例外是“diff”,它在这里没有用处。

谢谢。


样本数据:

pd.DataFrame({
  'Store': {0: 1, 1: 1, 2: 1, 3: 1, 4: 1},
  'Customers': {0: 668, 1: 578, 2: 619, 3: 635, 4: 785},
  'Date': {
    0: pd.Timestamp('2013-01-02 00:00:00'),
    1: pd.Timestamp('2013-01-03 00:00:00'),
    2: pd.Timestamp('2013-01-04 00:00:00'),
    3: pd.Timestamp('2013-01-05 00:00:00'),
    4: pd.Timestamp('2013-01-07 00:00:00')
  }
})

有效但速度极慢的代码:

import pandas as pd
import numpy as np
data = pd.read_csv("Rossman - no of cust/dataset.csv")
data.Date = pd.to_datetime(data.Date)
data.Customers = data.Customers.astype(int)

for index, row in data.iterrows():
    d = row["Date"]
    store = row["Store"]
    time_condition = (d - data["Date"]<np.timedelta64(60, 'D')) & (d > data["Date"])

    sub_df = data.loc[ time_condition & (data["Store"] == store), :]

    data.loc[ (data["Date"]==d) & (data["Store"] == store), "Lagged No customers"] = sub_df["Customers"].sum()
    data.loc[ (data["Date"]==d) & (data["Store"] == store), "No of days"] = len(sub_df["Customers"])
    if len(sub_df["Customers"]) > 0:
        data.loc[ (data["Date"]==d) & (data["Store"] == store), "Av No of customers"] = int(sub_df["Customers"].sum()/len(sub_df["Customers"]))

【问题讨论】:

  • 为什么要取data的前100行? (data[:100])
  • @Alexander time_condition 只是一个选择正确时间窗口的掩码,随后用于创建 sub_df; @David Z 我需要 100 行而不是整晚等待输出,但目标是获得整个数据集的输出
  • 在这种情况下,我认为最好将[:100] 排除在您问题的代码示例之外。毕竟,您的示例代码应该说明您遇到的问题 - 在这种情况下,太慢了。这没什么大不了的,但是如果您发现自己因为其他原因再次编辑,不妨考虑将其删除。
  • @DavidZ 刚刚更新

标签: python pandas apply


【解决方案1】:

鉴于您的小样本数据,我使用了两天的滚动平均值而不是 60 天。

>>> (pd.rolling_mean(data.pivot(columns='Store', index='Date', values='Customers'), window=2)
     .stack('Store'))
Date        Store
2013-01-03  1        623.0
2013-01-04  1        598.5
2013-01-05  1        627.0
2013-01-07  1        710.0
dtype: float64

通过以日期为索引并以存储为列的数据的轴心,您可以简单地获取滚动平均值。然后,您需要堆叠存储以将数据恢复为正确的形状。

这是最终堆栈之前原始数据的一些示例输出:

Store           1      2      3
Date                           
2015-07-29  541.5  686.5  767.0
2015-07-30  534.5  664.0  769.5
2015-07-31  550.5  613.0  822.0

.stack('Store') 之后,变成:

Date        Store
2015-07-29  1        541.5
            2        686.5
            3        767.0
2015-07-30  1        534.5
            2        664.0
            3        769.5
2015-07-31  1        550.5
            2        613.0
            3        822.0
dtype: float64

假设上面的命名为df,然后您可以将其合并回您的原始数据,如下所示:

data.merge(df.reset_index(), 
           how='left', 
           on=['Date', 'Store'])

编辑: 数据中存在明显的季节性模式,您可能需要对其进行调整。无论如何,您可能希望滚动平均值为 7 的倍数以表示偶数周。在下面的示例中,我使用了 63 天的时间窗口(9 周)。

为了避免丢失刚刚打开的商店(以及时间段开始时的商店)的数据,您可以在滚动平均函数中指定min_periods=1。这将为您提供给定时间窗口内所有可用观察的平均值

df = data.loc[data.Customers > 0, ['Date', 'Store', 'Customers']]
result = (pd.rolling_mean(df.pivot(columns='Store', index='Date', values='Customers'), 
          window=63, min_periods=1)
         .stack('Store'))
result.name = 'Customers_63d_mvg_avg'
df = df.merge(result.reset_index(), on=['Store', 'Date'], how='left')

>>> df.sort_values(['Store', 'Date']).head(8)
              Date  Store  Customers  Customers_63d_mvg_avg
843212  2013-01-02      1        668             668.000000
842103  2013-01-03      1        578             623.000000
840995  2013-01-04      1        619             621.666667
839888  2013-01-05      1        635             625.000000
838763  2013-01-07      1        785             657.000000
837658  2013-01-08      1        654             656.500000
836553  2013-01-09      1        626             652.142857
835448  2013-01-10      1        615             647.500000

为了更清楚地了解发生了什么,这里有一个玩具示例:

s = pd.Series([1,2,3,4,5] + [np.NaN] * 2 + [6])
>>> pd.concat([s, pd.rolling_mean(s, window=4, min_periods=1)], axis=1)
    0    1
0   1  1.0
1   2  1.5
2   3  2.0
3   4  2.5
4   5  3.5
5 NaN  4.0
6 NaN  4.5
7   6  5.5

窗口是四个观察值,但请注意,5.5 的最终值等于 (5 + 6) / 2。4.0 和 4.5 的值是 (3 + 4 + 5) / 3 和 (4 + 5) / 2,分别。

在我们的示例中,数据透视表的 NaN 行不会合并回 df,因为我们进行了左连接,并且 df 中的所有行都有一个或多个客户。

您可以查看滚动数据的图表如下:

df.set_index(['Date', 'Store']).unstack('Store').plot(legend=False)

【讨论】:

  • 谢谢我以前从未见过这个 :) rolling_sum 对于我过去遇到的其他类似问题也很有用。我会尽快在完整的数据集上进行测试以测试速度。
  • 我刚刚在 5.66mb 的训练数据文件上进行了测试。不到半秒。然后你需要把它合并回来。
  • 我也在研究这样的解决方案,但它似乎与问题中的参考实现并不完全相同。取平均值时,该代码除以过去 60 天内数据集中实际存在数据的日期数 - 该数字通常小于 60。(此数字位于“日期数”中" 列)rolling_mean 似乎实际上将数据复制到空行中,然后除以 60,或类似的东西。无论如何,我的测试表明结果并不完全相同。
  • @Alexander:使用 min_period 填充 NaN 是很好的,例如想要消除数据中的季节性。但是,我想使用滚动均值来创建一个特征以输入 ML 模型。我不能将 rolling_mean 用于不在初始数据集中的存储日期,min_period 可以。因此,我遍历 1115 家商店并应用您的解决方案(没有 min_periods),这仍然比我最初的尝试快得多。感谢您的帮助
猜你喜欢
  • 1970-01-01
  • 2020-11-04
  • 2020-09-05
  • 2021-05-19
  • 2018-01-29
  • 1970-01-01
  • 1970-01-01
  • 2021-06-14
  • 1970-01-01
相关资源
最近更新 更多