【问题标题】:How to sum over a Pandas dataframe conditionally如何有条件地对 Pandas 数据框求和
【发布时间】:2020-08-06 17:41:51
【问题描述】:

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

数据框:

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 记录,这不是一个可行的解决方案。谁能帮帮我? 非常感谢!

【问题讨论】:

    标签: python pandas sum conditional-statements data-science


    【解决方案1】:

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

    我的解决方案的要点是,对于给定的 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 天。

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

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

    我使用了一些辅助函数。注意a被理解为一个pandas系列,有8行,值为“Orders”,索引中有“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) 和 @987654329 @与group.rolling(61,....)

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

    【讨论】:

    • 感谢您的快速回复!我试过这个,但我收到以下错误消息: AttributeError: ("'numpy.ndarray' object has no attribute 'index'", 'occured at index Orders') 关于这个有什么想法吗?
    • 此外,我实际上稍微简化了问题,假设我可以将解决方案扩展到我的实际问题;-) 在我的真实数据中,我没有日期,而是纪元时间戳。而且我不一定想回顾 7 天(或任何整数),但也可能回顾 123456 秒。我们如何解决这个问题?
    • 关于这个错误,看起来我在 pandas 中使用了一个更新的功能。我有同样的问题,但是在total_last_weekapply 中设置 raw=False 解决了这个问题。提出的FutureWarning 表明 raw=False 将是未来的默认值。我已经更新了上面的代码。
    • 还回复了您上面的不同时间窗口查询。您需要弄清楚的主要问题是:我需要在rolling 窗口中查看多少行数据才能保证我拥有所需的所有信息?在您最初的示例中,我很保守,说 7+1 = 8 行,但在您的实际示例中,我们本可以只看 3 行。这是因为您的日期至少相隔三天。查看 3 行数据是查看日期 - 0 天、日期 - 3 天、日期 - 6 天。第 4 行是日期 - 9 天,超出 1 周 = 7 天的范围。
    • 有趣的解决方案,它有效!性能仍然是一个问题。我用我的数据子集(750 万行)尝试了它,使用了 7 天的范围,并在它运行了一个小时后取消了它。使用 pandasql sqldf 找到了另一个解决方案;也很慢,但它对我有用(有很大的耐心:-))还是谢谢你的帮助!
    猜你喜欢
    • 1970-01-01
    • 2022-01-17
    • 2016-08-08
    • 2021-12-28
    相关资源
    最近更新 更多