【python】python multiprocessing多进程处理dataframe,快得飞起~
建模过程中的特征工程工作往往是最耗时的大工程,很多场景下要使用pandas对数据进行加工处理,但pandas对数据的处理不能像Lightgbm训练数据一样自动设置了满线程运算(通过num_threads参数调控),导致其对数据的处理效率非常低下,尤其是在一个多核服务器上处理数据时,如果不做特殊处理,pandas对数据的运算只能使用一个核,是对时间和资源的极大浪费,本篇博客就来分享一下如何使用multiprocessing库充分利用计算资源,提高运行效率。
首先读入数据,指定变量。
import pandas as pd
import numpy as np
df = pd.read_csv('file.csv')
var_list = [x for x in df if x!='overdue_flag']
这里以var_cross函数为例,该函数的功能是对所有变量分箱进行两两组合(比如性别男且年龄30-35岁),找到坏账率明显区别于整体客群(明显高于或低于整体客群坏账率)的组合方式,记录下来,并将这样的组合以独热编码的方式(命中这样一个组合的打标为1,否则打标为0)生成一个新的变量。该函数返回两个dataframe,df_res是所有新生成的独热变量,res记录了哪些变量的哪些分箱进行组合,以及组合后的坏账率。(只为演示如何多线程处理,可以不对这个函数功能进行深究。)
def var_cross(res_col, target = 'overdue_flag', bad_thresh=0.12, good_thresh=0.04, num_rate=0.02, lift_rate=1.2):
res = pd.DataFrame()
df_res = pd.DataFrame()
for group_col in res_col:
group_df = df[list(group_col)+[target]].copy()
for num_col in group_df.select_dtypes(include=[np.number]):
if group_df[num_col].value_counts().shape[0]>=10:
group_df[num_col] = pd.qcut(group_df[num_col],10,duplicates='drop').astype(str)
tmp = group_df.groupby(list(group_col) if isinstance(group_col,tuple) else group_col)[target].agg({'count','mean'})
for item in tmp.iterrows():
if (item[1]['count']>df.shape[0]*num_rate)&((item[1]['mean']>bad_thresh)|(item[1]['mean']<good_thresh)):
br1 = df.loc[df[group_col[0]]==item[0][0],target].mean()
br2 = df.loc[df[group_col[1]]==item[0][1],target].mean()
if ( (item[1]['mean']>=br1*lift_rate)&(item[1]['mean']>=br2*lift_rate) )|( (item[1]['mean']*lift_rate<=br1)&(item[1]['mean']*lift_rate<=br2) ):
col_name = 'CROSS_'+str(group_col[0])+str(item[0][0])+str(group_col[1])+str(item[0][1])
col_name = col_name.replace(', ','_').replace('(','').replace(']','')
group_df[col_name]=0
group_df.loc[(group_df[group_col[0]]==item[0][0])&(group_df[group_col[1]]==item[0][1]),col_name]=1
df_tmp = pd.DataFrame({'var':str(group_col),'bin':str(item[0]),'count':item[1]['count'],
'bad_rate':item[1]['mean'],'var_bad_rate':str([br1,br2])},index=[0])
res = pd.concat([res,df_tmp])
df_res = pd.concat([df_res,group_df[[x for x in group_df if x.startswith('CROSS')]]],axis=1)
return df_res, res
首先我们生成一个非重复的所有变量两两组合的列表,作为函数var_cross的入参。
from itertools import product
col=[var_list,var_list]
res_col = [list(x) for x in product(*col) if len(list(x))==len(set(x))]
res_col = set([tuple(set(sorted(x))) for x in res_col])
print(list(res_col)[:5])
##输出结果如下:
[('Var_56', 'Var_21'),
('Var_42', 'Var_8'),
('Var_55', 'Var_61'),
('Var_43', 'Var_21'),
('Var_8', 'Var_37')]
然后记录一下函数运行所需时间。
import time
start_time = time.time()
df_result,info_result = var_cross(res_col)
end_time = time.time()
print('耗时{}分钟'.format((end_time-start_time)/60))
##输出结果如下:
耗时6.315534996986389分钟
耗时6分半,再来通过linux下的htop命令,看一下函数运行过程中的CPU状态:
64个核,只有第42个核在满负载运行,剩下的63个都闲得蛋疼,极大浪费计算资源,效率提升空间巨大。
我们以多线程的方式进行处理,思路为把所有的变量组合拆分成多份,每一份都同时调用var_cross函数,计算完成后再把各个线程的结果拼接起来。按照这样的思路,我们新写一个多线程的函数,即var_cross_multi:
import multiprocessing
from tqdm import tqdm
def var_cross_multi(res_col, target = 'overdue_flag', bad_thresh=0.12, good_thresh=0.04, num_rate=0.02, lift_rate=1.2):
jobn = 60
###该部分即把入参res_col拆分成60分,
###每一份放在一个list里面,把所有list结果统统放在data_list中
row_s = pd.Series(range(0, len(res_col)), index=res_col)
jobn = min(jobn, len(row_s.index))
row_cut = pd.qcut(row_s, jobn, labels=range(0, jobn))
data_list = []
for i in range(0, jobn):
data_list.append(list(row_cut[row_cut == i].index))
###开启多线程,每一份同时调用var_cross函数
mp = multiprocessing.Pool(jobn)
mplist = []
for i in range(0, jobn):
mplist.append(
mp.apply_async(
func=var_cross,
kwds={'res_col':data_list[i],'target':target, 'bad_thresh':bad_thresh, 'good_thresh':good_thresh, 'num_rate':num_rate, 'lift_rate':lift_rate}))
mp.close()
mp.join()
###把每个线程的结果拼接起来
res = pd.DataFrame()
df_res = pd.DataFrame()
for result in tqdm(mplist):
part_res = result.get()
if len(part_res)>1:
res = pd.concat([res,part_res[1]])
df_res = pd.concat([df_res,part_res[0]],axis=1)
print('FINISH!!')
return df_res,res
运行var_cross函数,再看一下消耗时间。
start_time = time.time()
df_result2,info_result2 = var_cross_multi(res_col)
end_time = time.time()
print('耗时{}分钟'.format((end_time-start_time)/60))
##输出结果如下:
100%|██████████| 60/60 [00:53<00:00, 1.12it/s]
FINISH!!
耗时1.1838727752367655分钟
耗时1分多钟,效率极大提高。再看一下函数运行时CPU的状态:
每一个核都动员起来了,都在高负载运行。但整体耗时只缩短了6倍,比预期的要慢很多啊。原因在哪里?我们通过tqdm进度条记录了一下,发现最后dtaframe拼接就耗时53秒,虽然多线程处理计算快了很多,但dataframe结果拼接却降低了效率,能不能再优化一下呢?我们对最后拼接的部分做了如下优化,避免多次调用concat函数,而是先把各个线程返回的要拼接的dataframe放到一个list里面,最后统一调用concat函数进行拼接:
res = pd.DataFrame()
df_res_list = []
# df_res = pd.DataFrame()
for result in tqdm(mplist):
part_res = result.get()
if len(part_res)>1:
res = pd.concat([res,part_res[1]])
df_res_list.append(part_res[0])
# df_res = pd.concat([df_res,part_res[0]],axis=1)
df_res = pd.concat(df_res_list,axis=1)
再来调用看一下时间:
start_time = time.time()
df_result3,info_result3 = var_cross_multi(res_col)
end_time = time.time()
print('耗时{}分钟'.format((end_time-start_time)/60))
##输出结果如下:
100%|██████████| 60/60 [00:00<00:00, 932.42it/s]
FINISH!!
耗时0.3334477424621582分钟
耗时半分钟,效率再提高一倍。最后我们再检验一下多线程处理的结果和单线程结果是否一致:
(df_result==df_result3).all()
结果全是True,没啥问题。
最后总结一下,提高效率的两点:
一、需处理的变量分多份,多线程调用,最后再拼接;
二、拼接的时候避免多次调用concat函数,先把结果放list里,最后统一concat。