【python】python multiprocessing多进程处理dataframe,快得飞起~

【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。

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
Pythonpandas包中有一种数据结构叫做DataFrame,它可以将数据以表格的方式展示出来,类似于Excel的表格。但是,当数据量比较大时,单线程的数据处理速度就会非常慢,这时使用多进程来并行处理数据是一种不错的方法。 使用Pythonmultiprocessing包可以比较方便地实现多进程并行处理DataFrame。首先,需要将DataFrame按照要处理的数据量进行拆分,然后将每个子DataFrame分配给不同的进程进行处理,最后将处理结果合并起来即可。 具体的步骤如下: 1. 将原DataFrame按照需要拆分成多个子DataFrame; 2. 构建一个进程池,将每个子DataFrame提交给进程池中的进程进行处理; 3. 等待所有进程处理完成并返回结果; 4. 合并所有进程的结果,得到最终的数据处理结果。 需要注意的是,在多进程并行处理DataFrame时,需要避免使用共享内存(比如Pythonmultiprocessing.Manager),因为共享内存会带来进程之间的同步和锁竞争问题,容易引发程序的复杂和不稳定性。推荐使用进程间通信IPC(Inter-Process Communication)方式,比如Pythonmultiprocessing.Pipe或multiprocessing.Queue等。 总的来说,使用多进程处理DataFrame可以有效提高数据处理的效率,对于大数据量的数据处理任务尤为重要。但是,多进程并行处理也需要注意进程之间的通信和同步问题,以及合理地利用多核CPU资源。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值