在大型 pandas 数据框中计算每行历史值的最有效方法是什么?
What is the most efficient method for calculating per-row historical values in a large pandas dataframe?
假设我有两个 pandas 数据框(df_a 和 df_b),其中每一行代表一个玩具和该玩具的特征。一些假装功能:
- Was_Sold (Y/N)
- 颜色
- Size_Group
- 形状
- Date_Made
说 df_a 相对较小(几千行)而 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)
}
)
首先,我认为使用 关系数据库 和 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 倍。
假设我有两个 pandas 数据框(df_a 和 df_b),其中每一行代表一个玩具和该玩具的特征。一些假装功能:
- Was_Sold (Y/N)
- 颜色
- Size_Group
- 形状
- Date_Made
说 df_a 相对较小(几千行)而 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)
}
)
首先,我认为使用 关系数据库 和 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 倍。