在 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 的 单个 值,因此您可以采取一些措施来提高性能。
- 使用
.item()
而不是 [0]
,它有一个小但不错的改进,尤其是对于较小的数据帧。
- 屏蔽 整个 DataFrame 只是为了 select 一个已知系列是一种浪费。而是仅屏蔽系列和 select 值。尽管您可能会认为“哦,这是被链接的——被禁止的
][
”,但它只是被链接的 assignment,这令人担忧,而不是被链接的 selection.
- 使用 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 数组排序的成本。
我正在尝试将我的一些 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 的 单个 值,因此您可以采取一些措施来提高性能。
- 使用
.item()
而不是[0]
,它有一个小但不错的改进,尤其是对于较小的数据帧。 - 屏蔽 整个 DataFrame 只是为了 select 一个已知系列是一种浪费。而是仅屏蔽系列和 select 值。尽管您可能会认为“哦,这是被链接的——被禁止的
][
”,但它只是被链接的 assignment,这令人担忧,而不是被链接的 selection. - 使用 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 数组排序的成本。