优化Pandas DataFrame中循环赛式比较的嵌套循环性能
你的问题核心是O(n²)复杂度的嵌套循环在大样本下的性能瓶颈,13000条数据的循环次数超过1.6亿次,再加上复杂的check()和funcleft(),慢是必然的。以下是几个可落地的优化方向,按性价比排序:
1. 先换掉iterrows(基础优化,立竿见影)
iterrows()是Pandas里效率极低的遍历方式,每次返回的Series对象会带来额外开销。直接提取content列的numpy数组遍历,能快2-3倍:
import tqdm # 直接获取numpy数组,避免Pandas对象的开销 contents = data['content'].values temp = [] for a in tqdm.tqdm(contents): total = 0 for b in contents: results = check(a, b) total += funcleft(a, results) average = total / len(contents) temp.append(average)
2. 并行化外层循环(性价比最高的优化)
每个外层循环的a是独立计算的,完全可以用多进程把任务拆分到多个CPU核心,直接把时间砍到原来的1/N(N是核心数)。用joblib实现最简单:
from joblib import Parallel, delayed import tqdm contents = data['content'].values # 把单个a的计算逻辑封装成函数 def calculate_single_avg(a): total = 0 for b in contents: results = check(a, b) total += funcleft(a, results) return total / len(contents) # n_jobs=-1表示用所有可用CPU核心 temp = Parallel(n_jobs=-1)( delayed(calculate_single_avg)(a) for a in tqdm.tqdm(contents) )
如果你的函数涉及GIL锁(比如纯Python代码),用多进程;如果是调用C扩展的函数(比如numpy/pandas内部操作),可以用多线程(backend="threading"),开销更小。
3. 向量化改造(如果函数支持批量输入)
如果能把check()和funcleft()改造成支持批量数组输入,可以利用numpy的广播特性一次性计算所有组合,彻底摆脱Python循环:
import numpy as np contents = data['content'].values # 广播成二维矩阵:a的每个元素对应一行,b的每个元素对应一列 a_batch = np.expand_dims(contents, axis=1) # shape (13000, 1) b_batch = np.expand_dims(contents, axis=0) # shape (1, 13000) # 假设check支持二维数组输入,一次性计算所有a-b组合的结果 all_results = check(a_batch, b_batch) temp = [] for idx, a in enumerate(tqdm.tqdm(contents)): # 取当前a对应的所有b的结果,计算总和 row_results = all_results[idx] total = np.sum(funcleft(a, row_results)) temp.append(total / len(contents))
注意:这要求check()能处理批量输入,如果原来的函数只支持单个字符串,需要修改为接受数组参数,或者用np.vectorize()包装(但vectorize本质还是循环,只是底层用numpy优化,速度提升有限)。
4. 减少重复计算(如果存在对称性)
如果check(a,b)和check(b,a)存在某种对称关系,或者funcleft(a, check(a,b))的结果可以通过funcleft(b, check(b,a))推导,那可以只计算上三角矩阵的结果,再复用数据,把计算量减半:
contents = data['content'].values n = len(contents) # 初始化一个数组存储所有结果 all_totals = np.zeros(n) for i in tqdm.tqdm(range(n)): a = contents[i] for j in range(n): if i <= j: # 只计算上三角 res = check(a, contents[j]) val_i = funcleft(a, res) all_totals[i] += val_i # 如果对称,直接把val_j加到all_totals[j] if i != j: val_j = funcleft(contents[j], check(contents[j], a)) all_totals[j] += val_j temp = [total / n for total in all_totals]
这个方案的效果完全取决于你的函数是否存在对称性,需要根据实际逻辑判断。
5. Numba编译加速(适合数值型计算)
如果check()和funcleft()是数值为主的计算,用Numba把函数编译成机器码,能大幅提升单线程循环速度:
from numba import jit import tqdm contents = data['content'].values # 用numba编译计算函数,nopython=True表示完全脱离Python解释器 @jit(nopython=True) def compute_avg(a, contents): total = 0.0 for b in contents: res = check(a, b) total += funcleft(a, res) return total / len(contents) temp = [compute_avg(a, contents) for a in tqdm.tqdm(contents)]
注意:Numba对字符串处理的支持有限,如果你的函数是NLP相关的复杂字符串操作,这个方案可能不适用。
内容的提问来源于stack exchange,提问作者Sampath Gudibettumane

