python - 如何有条件地对 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 记录,这不是一个可行的解决方案。谁能帮帮我?非常感激!
解决方案
我假设您的数据框名为df
. 我还将假设给定的日期不重复ClientID
,并且按升序排列(如果不是这种情况,请执行 groupby sum 并对结果进行排序,以便它是)。
我的解决方案的要点是,对于给定的 ClientID 和 Date。
- 使用 groupby.transform 按 ClientID 拆分此问题。
- 用于
rolling
检查接下来的 7 行中是否存在 1 周时间跨度内的日期。 - 在这 7 行中,时间跨度内的日期标记为 True (=1)。未标记的日期为 False (=0)。
- 在这 7 行中,将 Orders 列乘以日期的 True/False 标记。
- 对结果求和。
实际上,我们使用 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 循环可能比这种部分矢量化的解决方案运行得更快。
推荐阅读
- php-curl - 仅显示使用 curl 从 url 读取的参数之一
- node.js - AWS CDK 以及如何在不卡住的情况下更新 ECS
- jmeter - 在 Jmeter 中使用 HTTP 请求默认值时,它在运行时遇到错误的 URL
- spring-boot - 如何在 Kotlin 对象中动态添加键值对?
- javascript - 用下拉值替换函数变量(lodash)
- flutter - RenderIndexedSemantics 对象在布局期间被赋予无限大小
- laravel - Laravel Lighthouse:CanDirective:如何定义自定义错误消息?
- windows-terminal - WT 与配置文件最大化
- css - 带类的 Css ID - 父选择器
- asp.net - 在 asp.net 核心中连接到 neo4j