在大熊猫数据框中计算每行历史值的最有效方法是啥?
Posted
技术标签:
【中文标题】在大熊猫数据框中计算每行历史值的最有效方法是啥?【英文标题】:What is the most efficient method for calculating per-row historical values in a large pandas dataframe?在大熊猫数据框中计算每行历史值的最有效方法是什么? 【发布时间】:2022-01-18 02:24:34 【问题描述】:假设我有两个 pandas 数据框(df_a 和 df_b),其中每一行代表一个玩具和该玩具的特征。一些伪装功能:
已售出(是/否) 颜色 Size_Group 形状 生产日期假设 df_a 相对较小(几千行的 10s)而 df_b 相对较大(>100 万行)。
然后对于 df_a 中的每一行,我想:
-
从 df_b 中找到所有与 df_a 中相同类型的玩具(例如相同的颜色组)
df_b 玩具也必须在给定的 df_a 玩具之前制作
然后找到已售出的比率(因此计算已售出/计算所有匹配)
进行上述每行计算的最有效方法是什么?
到目前为止,我想出的最好的方法如下所示。 (注意代码可能有一两个错误,因为我是从不同的用例粗略输入的)
cols = ['Color', 'Size_Group', 'Shape']
# Run this calculation for multiple features
for col in cols:
print(col + ' - Started')
# Empty list to build up the calculation in
ratio_list = []
# Start the iteration
for row in df_a.itertuples(index=False):
# Relevant values from df_a
relevant_val = getattr(row, col)
created_date = row.Date_Made
# df to keep the overall prior toy matches
prior_toys = df_b[(df_b.Date_Made < created_date) & (df_b[col] == relevant_val)]
prior_count = len(prior_toys)
# Now find the ones that were sold
prior_sold_count = len(prior_toys[prior_toys.Was_Sold == "Y"])
# Now make the calculation and append to the list
if prior_count == 0:
ratio = 0
else:
ratio = prior_sold_count / prior_count
ratio_list.append(ratio)
# Store the calculation in the original df_a
df_a[col + '_Prior_Sold_Ratio'] = ratio_list
print(col + ' - Finished')
使用.itertuples()
很有用,但这仍然很慢。有没有更有效的方法或我缺少的东西?
编辑 添加了以下脚本,它将模拟上述场景的数据:
import numpy as np
import pandas as pd
colors = ['red', 'green', 'yellow', 'blue']
sizes = ['small', 'medium', 'large']
shapes = ['round', 'square', 'triangle', 'rectangle']
sold = ['Y', 'N']
size_df_a = 200
size_df_b = 2000
date_start = pd.to_datetime('2015-01-01')
date_end = pd.to_datetime('2021-01-01')
def random_dates(start, end, n=10):
start_u = start.value//10**9
end_u = end.value//10**9
return pd.to_datetime(np.random.randint(start_u, end_u, n), unit='s')
df_a = pd.DataFrame(
'Color': np.random.choice(colors, size_df_a),
'Size_Group': np.random.choice(sizes, size_df_a),
'Shape': np.random.choice(shapes, size_df_a),
'Was_Sold': np.random.choice(sold, size_df_a),
'Date_Made': random_dates(date_start, date_end, n=size_df_a)
)
df_b = pd.DataFrame(
'Color': np.random.choice(colors, size_df_b),
'Size_Group': np.random.choice(sizes, size_df_b),
'Shape': np.random.choice(shapes, size_df_b),
'Was_Sold': np.random.choice(sold, size_df_b),
'Date_Made': random_dates(date_start, date_end, n=size_df_b)
)
【问题讨论】:
@Ugurcan 我知道这是最理想的情况,但我不知道如何向量化上述逻辑。 这很难为您提供帮助,因为我们没有数据框示例,也没有列类型,并且提供的示例并不完全完整。拥有一个最小的 reproducible working 示例应该对我们有很大帮助(最后是你)。Some other conditions left out for simplicity
是个问题,因为隐藏条件可能不适用于未来提供的答案
@JérômeRichard 我将编辑掉“其他条件”位。我的观点是强调我可能需要添加更多条件,但我们可以认为这并不重要。至于列类型,我认为它是从提供的示例名称(字符串和日期)中暗示的。我无法提供真实数据(而且它要复杂得多),但我会看看我是否可以提供一个脚本来生成模拟玩具数据。
【参考方案1】:
首先,我认为使用 关系数据库 和 SQL 查询您的计算效率会更高。实际上,过滤器可以通过索引列、执行数据库连接、一些高级过滤和计算结果来完成。优化的关系数据库可以基于简单的 SQL 查询(基于哈希的行分组、二分查找、集合的快速交集等)生成高效算法。遗憾的是,Pandas 不能很好地执行有效这样的高级请求。迭代 pandas 数据帧也很慢,尽管我不确定在这种情况下仅使用 pandas 可以缓解这种情况。希望您可以使用一些 Numpy 和 Python 技巧并(部分)实现快速关系数据库引擎的功能。
此外,纯 Python 对象类型很慢,尤其是(unicode)字符串。因此,**首先将列类型转换为高效的类型可以节省大量时间(和内存)。例如,Was_Sold
列不需要包含“Y”/“N”字符串对象:在这种情况下可以使用 boolean。因此,让我们转换它:
df_b.Was_Sold = df_b.Was_Sold == "Y"
最后,当前算法的复杂性很差:O(Na * Nb)
其中Na
是df_a
中的行数,Nb
是df_b
中的行数。尽管由于条件不平凡,但这并不容易改善。第一个解决方案是提前将df_b
按col
列分组,以避免对df_b
进行昂贵的完整迭代(之前使用df_b[col] == relevant_val
完成)。然后,可以对预先计算的组的日期进行排序,以便稍后执行快速二进制搜索。然后,您可以使用 Numpy 有效地计算布尔值(使用 np.sum
)。
注意prior_toys['Was_Sold']
比prior_toys.Was_Sold
快一点。
这是生成的代码:
cols = ['Color', 'Size_Group', 'Shape']
# Run this calculation for multiple features
for col in cols:
print(col + ' - Started')
# Empty list to build up the calculation in
ratio_list = []
# Split df_b by col and sort each (indexed) group by date
colGroups = grId: grDf.sort_values('Date_Made') for grId, grDf in df_b.groupby(col)
# Start the iteration
for row in df_a.itertuples(index=False):
# Relevant values from df_a
relevant_val = getattr(row, col)
created_date = row.Date_Made
# df to keep the overall prior toy matches
curColGroup = colGroups[relevant_val]
prior_count = np.searchsorted(curColGroup['Date_Made'], created_date)
prior_toys = curColGroup[:prior_count]
# Now find the ones that were sold
prior_sold_count = prior_toys['Was_Sold'].values.sum()
# Now make the calculation and append to the list
if prior_count == 0:
ratio = 0
else:
ratio = prior_sold_count / prior_count
ratio_list.append(ratio)
# Store the calculation in the original df_a
df_a[col + '_Prior_Sold_Ratio'] = ratio_list
print(col + ' - Finished')
这在我的机器上快了 5.5 倍。
pandas 数据框的迭代是导致速度下降的主要原因。实际上,prior_toys['Was_Sold']
需要一半的计算时间,因为 pandas 内部函数调用重复 Na
次的巨大开销......使用 Numba 可能有助于降低缓慢迭代的成本。请注意,可以通过提前将 colGroups
拆分为子组 (O(Na log Nb)
) 来增加复杂性。这应该有助于完全消除prior_sold_count
的开销。生成的程序应该比原来的程序快 10 倍。
【讨论】:
我最初是在 SQL 中启动问题,但发现查询计划仍然在内存中创建了怪物组合。我不相信这在那儿是可行的,除非也进行迭代。不过,预先计算的子组绝对是一个很棒的选择,所以可以接受。虽然有两个后续行动:1)为什么['Was_Sold']
和.Was_Sold
有区别? 2) 我想念prior_toys['Was_Sold']
如何多次调用Na
? (我相信你的符号Na
来自df_a
,但prior_toys['Was_Sold']
应该来自df_b
的过滤版本(除非你只是指Na
次因为循环?)
['Was_Sold']
更快可能是因为运行时定义的属性在解释器本身中引入了开销(要执行的代码多和函数调用多)。对于 (2),您遍历 df_a
,因此循环中的每条指令都被称为 Na
次。我认为prior_toys['Was_Sold']
的总和在O(Nb)
中运行,因为prior_toys
来自df_b
,我认为过滤不会影响复杂性。在实践中它可能要小得多。我刚刚计算了一个上限复杂度。找到下限并不容易。摊销复杂度可能更好(不容易计算)。
明白了!我没有意识到 . vs 括号表示法有这样的差异。我会阅读更多,谢谢!以上是关于在大熊猫数据框中计算每行历史值的最有效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章