
本文旨在探讨在pandas dataframe中,如何高效地查找满足特定特定条件的历史最新索引。针对传统apply方法在处理此类依赖于过去状态的问题时性能瓶颈,我们将介绍并详细分析基于python内置bisect模块的优化方案,该方案通过结合二分查找和哈希表,显著提升了处理大规模数据集的效率,并提供了详细的代码实现与性能对比。
在数据分析中,我们经常需要根据当前行的数据,回溯查找历史上满足特定条件的最新记录。例如,给定一个DataFrame,其中包含lower和upper两列以及一个时间索引DATE,我们的目标是为每一行查找其之前所有行中,lower值大于或等于当前行upper值的最新DATE索引。
以下是一个典型的示例DataFrame及其初始的低效实现:
import pandas as pd
import numpy as np
# 示例DataFrame
data = {'lower': [7, 1, 6, 1, 1, 1, 1, 11, 1, 1],
'upper': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}
df = pd.DataFrame(data=data)
df['DATE'] = pd.date_range('2020-01-01', periods=len(data['lower']))
df.set_index('DATE', inplace=True)
print("原始DataFrame:")
print(df)
# 低效方案:使用 df.apply
def get_most_recent_index_baseline(row, dataframe):
# 查找当前行之前的所有行
# 注意:row.name - pd.Timedelta(minutes=1) 确保只考虑严格早于当前行的记录
previous_indices = dataframe.loc[:row.name - pd.Timedelta(minutes=1)]
# 筛选满足条件的记录,并返回最新的索引
recent_index = previous_indices[previous_indices['lower'] >= row['upper']].index.max()
return recent_index
# 应用函数到每一行
# df['prev_baseline'] = df.apply(lambda row: get_most_recent_index_baseline(row, df), axis=1)
# print("\n低效方案结果:")
# print(df)低效原因分析:
上述df.apply结合自定义函数的方案虽然直观,但效率极低,主要原因如下:
在实际性能测试中,对于包含10万行数据的DataFrame,这种基线方案可能需要数分钟甚至更长时间才能完成。
为了解决上述性能问题,我们可以利用Python内置的bisect模块进行二分查找,结合哈希表来优化查找过程。bisect模块提供了一组函数,用于在有序序列中插入元素或查找元素的位置,其时间复杂度为O(log N)。
以下是基于bisect模块的优化方案实现:
from bisect import bisect_left
def get_prev_with_bisect(lower_series, upper_series, date_index):
"""
使用bisect模块高效查找满足条件的历史最新索引。
参数:
lower_series (pd.Series): DataFrame的'lower'列。
upper_series (pd.Series): DataFrame的'upper'列。
date_index (pd.DatetimeIndex): DataFrame的时间索引。
返回:
list: 包含每行对应的历史最新索引的列表。
"""
# 获取所有不重复的lower值并排序,用于二分查找
uniq_lower = sorted(set(lower_series))
# last_seen 字典用于存储每个lower值最近出现的日期
# 键为lower值,值为对应的最新日期
last_seen = {}
results = []
# 遍历每一行数据
for l, u, d in zip(lower_series, upper_series, date_index):
max_date = None
# 使用bisect_left查找在uniq_lower中,第一个大于或等于当前u的元素的索引
# 这意味着从idx开始的所有uniq_lower元素都满足 lower >= u 的条件
idx = bisect_left(uniq_lower, u)
# 遍历所有满足条件的lower值
for lv in uniq_lower[idx:]:
if lv in last_seen:
# 如果该lower值在之前出现过
if max_date is None:
max_date = last_seen[lv]
elif last_seen[lv] > max_date:
# 更新为更近的日期
max_date = last_seen[lv]
results.append(max_date)
# 更新last_seen字典:记录当前l值对应的最新日期d
last_seen[l] = d
return results
# 应用优化后的函数
df['prev_bisect'] = get_prev_with_bisect(df["lower"], df["upper"], df.index)
print("\nBisect方案结果:")
print(df)原理分析:
时间复杂度分析:
为了更直观地展示不同方法的性能差异,我们使用一个包含10万行数据的DataFrame进行测试。
import pandas as pd
import numpy as np
from bisect import bisect_left
import time
def get_sample_df(rows=100_000):
# Sample DataFrame
data = {'lower': np.random.default_rng(seed=1).uniform(1,100,rows),
'upper': np.random.default_rng(seed=2).uniform(1,100,rows)}
df = pd.DataFrame(data=data)
df = df.astype(int)
df['DATE'] = pd.date_range('2020-01-01', periods=len(data['lower']), freq="min")
df.set_index('DATE', inplace=True)
return df
# 基线方法 (get_baseline) - 与 get_most_recent_index_baseline 相同
def get_baseline():
df = get_sample_df()
def get_most_recent_index(row):
previous_indices = df.loc[:row.name - pd.Timedelta(minutes=1)]
recent_index = previous_indices[previous_indices['lower'] >= row['upper']].index.max()
return recent_index
df['prev'] = df.apply(get_most_recent_index, axis=1)
return df
# Bisect 方法 (get_bisect) - 与 get_prev_with_bisect 相同
def get_bisect():
df = get_sample_df()
df["prev"] = get_prev_with_bisect(df["lower"], df["upper"], df.index)
return df
# 朴素的enumerate循环方法 (get_enumerate)
def get_enumerate():
df = get_sample_df()
df.reset_index(inplace=True) # 重置索引方便列表操作
date_list=df["DATE"].values.tolist()
lower_list=df["lower"].values.tolist()
upper_list=df["upper"].values.tolist()
new_list=[]
for i,(x,y) in enumerate(zip(lower_list,upper_list)):
if i==0:
new_list.append(None)
else:
found_date = None
# 从后向前遍历,找到第一个满足条件的日期
for ll,dl in zip(reversed(lower_list[0:i]),reversed(date_list[0:i])):
if ll>=y:
found_date = dl
break
new_list.append(found_date)
df['prev']=new_list
df['prev']=pd.to_datetime(df['prev'])
return df
print("--- 性能测试 (100,000 行) ---")
start_time = time.time()
get_baseline()
print(f"Baseline (df.apply): {time.time() - start_time:.2f} seconds")
start_time = time.time()
get_bisect()
print(f"Bisect: {time.time() - start_time:.2f} seconds")
start_time = time.time()
get_enumerate()
print(f"Enumerate (Python loop): {time.time() - start_time:.2f} seconds")预期性能结果(基于原始问题中的数据):
从结果可以看出,bisect方法在处理大规模数据时,性能远超df.apply和直接的Python循环(enumerate)。df.apply由于其内部开销和重复操作,效率最低。enumerate虽然是纯Python循环,但仍然需要进行O(N)的线性扫描,导致其时间复杂度依然是O(N^2)。
关于pyjanitor的说明:
原始问题中提到了pyjanitor库的一个尝试方案,但该方案在处理大规模数据时遇到了内存分配错误("Unable to allocate 37.2 GiB for an array...")。这表明虽然pyjanitor提供了强大的条件连接功能,但对于某些特定场景,尤其是在需要创建大量中间数据结构时,可能会面临内存限制,不适合所有情况。
总结
在Pandas中处理依赖于历史状态的条件查找问题时,直接使用df.apply是效率最低的选择。通过巧妙地结合Python内置的bisect模块进行二分查找和哈希表(字典)来存储历史状态,我们可以构建出性能卓越的解决方案。这种方法将时间复杂度从O(N^2)显著降低,使其能够有效地处理大规模数据集。在实际开发中,理解问题的本质并选择合适的算法和数据结构是优化性能的关键。
以上就是Pandas高效查找历史条件匹配的最新索引:Bisect方法详解的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号