在 pandas DataFrame 中查找值的更快方法?

Faster way to look for a value in pandas DataFrame?

我正在尝试将我的一些 R 脚本“翻译”为 Python,但我注意到,在 Python 中处理数据帧比在 R 中处理要慢得多,例如根据条件提取细胞

我做了一点调查,这是在 Python 中查找特定值所花费的时间:

import pandas as pd
from timeit import default_timer as timer

code = 145896

# real df is way bigger
df = pd.DataFrame(data={
    'code1': [145896, 800175, 633974, 774521, 416109],
    'code2': [100, 800, 600, 700, 400],
    'code3': [1, 8, 6, 7, 4]}
    )

start = timer()
for _ in range(100000):
    desired = df.loc[df['code1']==code, 'code2'][0]
print(timer() - start) # 19.866242500000226 (sec)

在 R 中:

code <- 145896

df <- data.frame("code1" = c(145896, 800175, 633974, 774521, 416109),
           "code2" = c(100, 800, 600, 700, 400),
           "code3" = c(1, 8, 6, 7, 4))

start <- Sys.time()
for (i in 1:100000) {
  desired <- df[df$code1 == code, "code2"]
}
print(Sys.time() - start) # Time difference of 1.140949 secs

我对 Python 比较陌生,我可能遗漏了一些东西。有什么方法可以加快这个过程吗?也许将此脚本转移到 Python 的整个想法毫无意义?在其他操作中 Python 更快(即处理字符串),一旦需要处理数据帧,在两个或多个脚本之间跳转将非常不方便。请问有什么帮助吗?

更新 真正的脚本块迭代初始数据帧的行(相当大,500-1500k 行)并创建一个新的行,包含来自原始列“code1”的值和对应的代码,来自另一个数据帧,和许多其他新创建的值。我相信,我可以用图片澄清它:

在脚本的后面,我也需要根据不同的条件在循环中搜索特定的值。所以搜索的速度是必不可少的。

只需重复使用过滤器表达式,您就可以将其减少一半左右。

In [1]: import pandas as pd

In [2]: code = 145896
   ...: df = pd.DataFrame(data={
   ...:     'code1': [145896, 800175, 633974, 774521, 416109],
   ...:     'code2': [100, 800, 600, 700, 400],
   ...:     'code3': [1, 8, 6, 7, 4]
   ...: })

In [3]: %timeit df.loc[df['code1'] == code, 'code2'][0]
197 µs ± 5.14 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [4]: filter_expr = df['code1'] == code

In [5]: %timeit df.loc[filter_expr, 'code2'][0]
106 µs ± 3.3 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

通过键列索引数据帧(假设查找频繁)应该是可行的方法,因为数据帧的索引是散列 table(有关详细信息,请参阅 this answer and these slides)。

In [6]: df_idx = df.set_index('code1')

In [7]: %timeit df_idx.loc[code]['code2']
72.7 µs ± 1.58 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

并且根据您拥有的其他用例,拥有真正的嵌入式(内存中)数据库、SQLite 或 DuckDB 可以 运行 直接查询 Pandas data without ever importing or copying any data), 也可能是一个解决方案。

由于您正在寻找 select 来自 DataFrame 的 单个 值,因此您可以采取一些措施来提高性能。

  1. 使用 .item() 而不是 [0],它有一个小但不错的改进,尤其是对于较小的数据帧。
  2. 屏蔽 整个 DataFrame 只是为了 select 一个已知系列是一种浪费。而是仅屏蔽系列和 select 值。尽管您可能会认为“哦,这是被链接的——被禁止的 ][”,但它只是被链接的 assignment,这令人担忧,而不是被链接的 selection.
  3. 使用 numpy。由于索引和对齐,Pandas 有很多开销。但是您只想 select 来自矩形数据结构的单个值,因此下降到 numpy 会更快。

下面是 select 数据的不同方式的时间说明 [下面每种方式都有自己的方法]。使用 numpy 是迄今为止最快的,特别是对于像您的示例中的较小数据帧。对于那些,它将比您获取 select 数据的原始方式快 20 倍以上,查看您与 R 的初步比较应该使它 比 select 略快 ] 在 R 中处理数据。随着 DataFrame 变大,numpy 解决方案的相对性能不那么好,但它仍然是最快的方法(此处显示)。

import perfplot
import pandas as pd
import numpy as np

def DataFrame_Slice(df, code=0):
    return df.loc[df['code1'] == code, 'code2'].iloc[0]

def DataFrame_Slice_Item(df, code=0):
    return df.loc[df['code1'] == code, 'code2'].item()

def Series_Slice_Item(df, code=0):
    return df['code2'][df['code1'] == code].item()

def with_numpy(df, code=0):
    return df['code2'].to_numpy()[df['code1'].to_numpy() == code].item()


perfplot.show(
    setup=lambda N: pd.DataFrame({'code1': range(N),
                                  'code2': range(50, N+50),
                                  'code3': range(100, N+100)}),
    kernels=[
        lambda df: DataFrame_Slice(df),
        lambda df: DataFrame_Slice_Item(df),
        lambda df: Series_Slice_Item(df),
        lambda df: with_numpy(df)
    ],
    labels=['DataFrame_Slice', 'DataFrame_Slice_Item', 'Series_Slice_Item', 'with_numpy'],
    n_range=[2 ** k for k in range(1, 21)],
    equality_check=np.allclose,  
    relative_to=3,
    xlabel='len(df)'
)

R-lang 可能就是围绕这种操作设计的。 Pandas 是 python 中的第三方库,因此它有额外的限制和开销需要解决。也就是说,在每个中间步骤中都会生成一个新的数据帧或系列。几乎每个等号或括号都是一个中间步骤。

如果你真的想从单个列中一次提取单个元素,你可以尝试设置索引:

df2 = df.set_index('code1')
def getel(df2,code):
    desired = None
    if code in df2.index: 
        desired = df2['code2'][code]
        if isinstance(desired, pd.Series):
            desired = desired.iloc[0]
    return code

如果值是重复的,这比原来快三倍。如果该值是唯一的,则 desired = df2['code2'][code] 不会生成新系列,代码比原始代码快 17 倍。另外,请注意,在选择其他所有内容之前选择列往往会显着减少不必要的工作 pandas。

如果您想对不同的值进行类似的操作,那么您应该看看 groupby。或者至少一次过滤所有要处理的值:

codes = {145896,774521}
pad = df['code1'].apply(lambda x: x in codes) #this is good when there are many codes
#or
pad = df['code1'].isin(codes) #this is linear time in len(codes)
result = df[pad].apply(do_stuff, axis = 1)
#or df[pad].transform(do_stuff, axis = 1)

一种方法是通过首先获取底层数组然后循环来回退到 numpy:

import pandas as pd
from timeit import default_timer as timer

code = 145896

# real df is way bigger
df = pd.DataFrame(data={
    'code1': [145896, 800175, 633974, 774521, 416109],
    'code2': [100, 800, 600, 700, 400],
    'code3': [1, 8, 6, 7, 4]}
    )

start = timer()


code1 = df['code1'].values
code2 = df['code2'].values
code3 = df['code3'].values

for _ in range(100000):
    desired = code1 == code
    desired_code2 = code2[desired][0]
    desired_code3 = code3[desired][0]

print(timer() - start) # 0.26 (sec)

以下代码改编自 ALollz 的回答,说明了不同技术在性能上的差异,其中添加了几种技术并增加了数据集大小范围,显示了受数量级影响的变化行为。

import perfplot
import pandas as pd
import numpy as np

def DataFrame_Slice(df, code=0):
    return df.loc[df['code1'] == code, 'code2'].iloc[0]

def DataFrame_Slice_Item(df, code=0):
    return df.loc[df['code1'] == code, 'code2'].item()

def Series_Slice_Item(df, code=0):
    return df['code2'][df['code1'] == code].item()

def with_numpy(df, code=0):
    return df['code2'].to_numpy()[df['code1'].to_numpy() == code].item()

def with_numpy_values(code1, code2, code=0):
    desired = code1 == code
    desired_code2 = code2[desired][0]
    return desired_code2

def DataFrameIndex(df, code=0):
    return df.loc[code].code2

def with_numpy_argmax(code1, code2, code=0):
    return code2[np.argmax(code1==code)]

def numpy_search_sorted(code1, code2, sorter, code=0):
    return code2[sorter[np.searchsorted(code1, code, sorter=sorter)]]

def python_dict(code1_dict, code2, code=0):
    return code2[code1_dict[code]]

def shuffled_array(N):
    a = np.arange(0, N)
    np.random.shuffle(a)
    return a

def setup(N):
    print(f'setup {N}')
    df = pd.DataFrame({'code1': shuffled_array(N),
                                  'code2': shuffled_array(N),
                                  'code3': shuffled_array(N)})
    
    code = df.iloc[min(len(df)//2, len(df)//2 + 20)].code1
    
    sorted_index = df['code1'].values.argsort()
    
    code1 = df['code1'].values
    code2 = df['code2'].values
    
    code1_dict = {code1[i]: i for i in range(len(code1))}
    
    return df, df.set_index('code1'), code1, code2, sorted_index, code1_dict, code


for relative_to in [5, 6, 7]:
    perfplot.show(
        setup=setup,
        kernels=[
            lambda df, _, __, ___, sorted_index, ____, code: DataFrame_Slice(df, code),
            lambda df, _, __, ___, sorted_index, ____, code: DataFrame_Slice_Item(df, code),
            lambda df, _, __, ___, sorted_index, ____, code: Series_Slice_Item(df, code),
            lambda df, _, __, ___, sorted_index, ____, code: with_numpy(df, code),
            lambda _, __, code1, code2, sorted_index, ____,  code: with_numpy_values(code1, code2, code),
            lambda _, __, code1, code2, sorted_index, ____, code: with_numpy_argmax(code1, code2, code),
            lambda _, df, __, ___, sorted_index, ____, code: DataFrameIndex(df, code),
            lambda _, __, code1, code2, search_index, ____, code: numpy_search_sorted(code1, code2, search_index, code),
            lambda _, __, ___, code2, _____, code1_dict, code: python_dict(code1_dict, code2, code)
        ],
        logy=True,
        labels=['DataFrame_Slice', 'DataFrame_Slice_Item', 'Series_Slice_Item', 'with_numpy', 'with_numpy_values', 'with_numpy_argmax', 'DataFrameIndex', 'numpy_search_sorted', 'python_dict'],
        n_range=[2 ** k for k in range(1, 25)],
        equality_check=np.allclose,  
        relative_to=relative_to,
        xlabel='len(df)')

结论:Numpy 搜索排序是性能最好的技术,除了低于 100 标记的非常小的数据集。使用底层 numpy 数组的顺序搜索是数据集大约低于 100,000 的下一个最佳选择,之后最好的选择是使用 DataFrame 索引。但是,当达到 multi-million 记录标记时,DataFrame 索引不再执行良好,可能是由于散列冲突。

[编辑 2022 年 3 月 24 日] 使用 Python 字典比所有其他技术至少高出一个数量级。

注意:我们假设在 DataFrame 中重复搜索,以抵消获取底层 numpy 数组、索引 DataFrame 或对 numpy 数组排序的成本。