首页 > 解决方案 > 如何有条件地对 Pandas 数据框求和

问题描述

我正在寻找一种有效的方法(不循环)将一列添加到数据框中,包含同一数据框的一列的总和,并由行中的某些值过滤。例子:

数据框:

ClientID   Date           Orders
123        2020-03-01     23
123        2020-03-05     10
123        2020-03-10     7
456        2020-02-22     3
456        2020-02-25     15
456        2020-02-28     5
...

我想添加一个列“orders_last_week”,其中包含给定日期前 7 天内该特定客户的订单总数。Excel 等价物类似于:

SUMIFS([orders],[ClientID],ClientID,[Date]>=Date-7,[Date]<Date)

所以这将是结果:

ClientID   Date           Orders  Orders_Last_Week
123        2020-03-01     23      0
123        2020-03-05     10      23
123        2020-03-10     7       10
456        2020-02-22     3       0
456        2020-02-25     15      3
456        2020-02-28     5       18
...

我可以通过循环解决这个问题,但由于我的数据框包含 >20M 记录,这不是一个可行的解决方案。谁能帮帮我?非常感激!

标签: pythonpandassumconditional-statementsdata-science

解决方案


我假设您的数据框名为df. 我还将假设给定的日期不重复ClientID,并且按升序排列(如果不是这种情况,请执行 groupby sum 并对结果进行排序,以便它是)。

我的解决方案的要点是,对于给定的 ClientID 和 Date。

  1. 使用 groupby.transform 按 ClientID 拆分此问题。
  2. 用于rolling检查接下来的 7 行中是否存在 1 周时间跨度内的日期。
  3. 在这 7 行中,时间跨度内的日期标记为 True (=1)。未标记的日期为 False (=0)。
  4. 在这 7 行中,将 Orders 列乘以日期的 True/False 标记。
  5. 对结果求和。

实际上,我们使用 8 行,因为例如 SuMoTuWeThFrSaSu 有 8 天。

使这变得困难的是滚动聚合一次一个列,因此显然不允许您在聚合时使用多个列。如果是这样,您可以使用日期列进行过滤,并使用它来汇总订单。

但是有一个漏洞:如果您愿意通过索引将它们偷运进来,您可以使用多个列!

我使用了一些辅助函数。Notea被理解为具有 8 行和值“Orders”的 pandas 系列,索引中带有“Date”。

很想知道真实数据的性能如何。

import pandas as pd

data =  {
    'ClientID': {0: 123, 1: 123, 2: 123, 3: 456, 4: 456, 5: 456},
    'Date': {0: '2020-03-01', 1: '2020-03-05', 2: '2020-03-10',
             3: '2020-02-22', 4: '2020-02-25', 5: '2020-02-28'},
 'Orders': {0: 23, 1: 10, 2: 7, 3: 3, 4: 15, 5: 5}
}

df = pd.DataFrame(data)

# Make sure the dates are datetimes
df['Date'] = pd.to_datetime(df['Date'])

# Put into index so we can smuggle them through "rolling"
df = df.set_index(['ClientID', 'Date'])


def date(a):
    # get the "Date" index-column from the dataframe 
    return a.index.get_level_values('Date')

def previous_week(a):
    # get a column of 0s and 1s identifying the previous week, 
    # (compared to the date in the last row in a).
    return (date(a) >= date(a)[-1] - pd.DateOffset(days=7)) * (date(a) < date(a)[-1]) 

def previous_week_order_total(a):
    #compute the order total for the previous week
    return sum(previous_week(a) * a)

def total_last_week(group):
    # for a "ClientID" compute all the "previous week order totals"
    return group.rolling(8, min_periods=1).apply(previous_week_order_total, raw=False)

# Ok, actually compute this
df['Orders_Last_Week'] = df.groupby(['ClientID']).transform(total_last_week)

# Reset the index back so you can have the ClientID and Date columns back
df = df.reset_index()

上面的代码依赖于过去一周最多包含 7 行数据的事实,即一周中的 7 天(尽管在您的示例中,它实际上小于 7)

如果您的时间窗口不是一周,您需要根据时间戳的最佳划分替换所有对一周长度的引用。

例如,如果您的日期时间戳间隔不小于 1 秒,并且您对 1 分钟的时间窗口感兴趣(例如,“Orders_last_minute”),请替换pd.DateOffset(days=7)pd.DateOffset(seconds=60),group.rolling(8,...group.rolling(61,....)

显然,这段代码有点悲观:对于每一行,它总是查看 61 行,在这种情况下。不幸的是rolling,没有提供合适的可变窗口大小功能。我怀疑在某些情况下,利用数据帧按日期排序这一事实的 python 循环可能比这种部分矢量化的解决方案运行得更快。


推荐阅读