在大熊猫数据框中计算每行历史值的最有效方法是啥?

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) 其中Nadf_a 中的行数,Nbdf_b 中的行数。尽管由于条件不平凡,但这并不容易改善。第一个解决方案是提前将df_bcol 列分组,以避免对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 括号表示法有这样的差异。我会阅读更多,谢谢!

以上是关于在大熊猫数据框中计算每行历史值的最有效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

从数据框中的所有列中获取值的最简单方法是啥[重复]

在pyspark中将值随机更改为空值的最有效方法是啥?

在javascript中设置默认参数值的最有效方法是啥?

通过某些(索引)参数将值插入熊猫数据框中“适当”位置的最佳方法是啥?

如何计算熊猫数据框中每一列的唯一性?

从地理坐标计算本地用户的最有效方法是啥?