在大型 pandas 数据框中计算每行历史值的最有效方法是什么?

What is the most efficient method for calculating per-row historical values in a large pandas dataframe?

假设我有两个 pandas 数据框(df_a 和 df_b),其中每一行代表一个玩具和该玩具的特征。一些假装功能:

说 df_a 相对较小(几千行)而 df_b 相对较大(>100 万行)。

然后 df_a 中的每一行,我想:

  1. 找出 df_b 中所有与 df_a 中相同类型的玩具(例如相同颜色组)
  2. df_b玩具也必须在给定的df_a玩具
  3. 之前制作
  4. 然后找出已售出的比率(所以计算已售出/计算所有匹配)

进行上述每行计算的最有效方法是什么?

到目前为止,我想到的最好的方法如下所示。 (注意代码可能有一个或两个错误,因为我是从不同的用例中粗略输入的)

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) 其中 Nadf_aNbdf_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 倍