本文主要涉及python基于pandas的大数据项目的一次调优,调优的过程很难受,调优的结果很开心,运行时间从22分钟降低到了7分钟左右。涉及多线程,代码重构,以及语法调优。时间紧张的话,结论在最后。
问题的发现以及初步调优
由于项目的技术架构是基于aws的lambda服务进行无限并发处理,而每个lambda进程要求程序整体运行不能超过15分钟。但是通过测试后发现,竟然跑完要22分钟左右,肯定不行,所以就开始了如下调优的过程。
先看下项目的优化前运行时间
计算完成,用时:1278.003386735916138
########################################################
再看下项目优化后的运行时间
计算完成,用时:423.5311672687530518
发现问题
- 首先统计各模块执行时间,确定耗时占比比较大的模块
- 通过梳理几个耗时较大模块的逻辑,发现了有几处逻辑处理复杂化的地方,重构优化,节省了大概3分钟的样子
- 因为数据量比较大,千万级别的数据,而项目中没有用到大数据框架,都是基于pandas直接做读取操作,这部分耗时也比较大,于是决定更改为采用后台启用多线程读取大数据文件,在需要该数据集时检测该线程的结果(线程同步),这一步大概节约了6分钟的样子
- 这样一算,差不多程序13分钟,满足了需求,但是逼近时间点,会担心万一超时的情况发生,况且我们的数据量可能会有波动,在某些时间点可能成倍的增加,那么就意味着有可能数据量的增加仍然会带来时间上的不满足
- 找来找去感觉没地方可以优化了,但是必须还得研究怎么优化!最终找到了如下代码段,程序运行缓慢的罪魁祸首,耗时竟然达到了6-8分钟,占据了我程序差不多一半的时间
import pandas as pd
# 这里只是给一个上下文语境
ord_df = pd.read_csv("./ord.csv")
slfrt_df = pd.read_csv("./slfrt.csv")
# 数据拼接
df = pd.merge(ord_df, slfrt_df, on=["name", "code"])
# 将字符串时间格式转换为date类型
df["start_date"] = df["start_date"].apply(lambda x: pd.to_datetime(x).date())
df["end_date"] = df.apply(lambda x: x.orddt + datetime.timedelta(days=x.seddays) - datetime.timedelta(days=1), axis=1)
# 筛选我需要的数据
df = df[(df.start_date >= df.orddt) & (df.start_date <= df.end_date)].copy()
分析问题
开始分析这段代码的耗时原因
首先我以为是pd.merge耗费了我大量的时间,因为数据比较大,千万级别,但是后来做测试发现时间耗时根本不长
然后就是时间格式转换了,因为我的项目是整体以datetime.date为格式进行时间的比较计算,所以我这里要将字符串时间格式转换为date格式(pandas不支持datetime和date格式进行比较),由于pandas只有这一种方式支持将时间转为date格式(不是datetime64格式),测试500W数据的时间格式转化,如下所示
# 这里仅仅构造500万的数据,进行测试
import random
import datetime
# 构造字符串格式的日期格式
date_period = [str(_date)[:10] for _date in pd.date_range('2020-07-01', '2020-08-08', freq='D')]
df = pd.DataFrame({"name": [str(i) for i in range(5000000)], "date_period": [random.choice(date_period) for i in range(5000000)]})
t_start = time.time()
df["date_period"] = df["date_period"].apply(lambda x: pd.to_datetime(x).date())
t = time.time() - t_start
print(t) # 耗时: 437.8680636882782秒
上面的500万数据转换仅仅转换为日期格式竟然花费了437秒,也就是差不多7分钟的样子,这怎么能忍!!!于是尝试更改为pandas内置方法(如下),测试结果
# 这里仅仅构造500万的数据,进行测试
import random
import datetime
date_period = [str(_date)[:10] for _date in pd.date_range('2020-01-05', '2020-05-01', freq='D')]
df = pd.DataFrame({"name": [str(i) for i in range(5000000)], "date_period": [random.choice(date_period) for i in range(5000000)]})
t_start = time.time()
# 主要测试这行代码的变化带来的事件影响
df["date_period"] = pd.to_datetime(df["date_period"])
t = time.time() - t_start
print(t) # 耗时: 0.5311672687530518秒
天了噜,竟然相差了几千倍的速度,怪不得项目耗损了大量的时间
解决问题
改吧,更改如下
import pandas as pd
# 这里只是给一个上下文语境
ord_df = pd.read_csv("./ord.csv")
slfrt_df = pd.read_csv("./slfrt.csv")
# 基础拼接
df = pd.merge(ord_df, slfrt_df, on=["name", "code"])
################# 前面这些都是一样的,从这里开始优化处理数据 ###############
# 进行计算的时候更改为用datetime.datetime格式
df["orddt"] = pd.to_datetime(df["orddt"])
df["start_date"] = pd.to_datetime(df["start_date"])
# 将时间间隔生成pandas内置格式方便做加减运算
df["temp_seddays"] = pd.to_timedelta(df["seddays"], unit='d')
# 构建临时辅助时间间隔列,用来计算时间格式的加减法
df["tmp_dela"] = pd.to_timedelta(1, unit='d')
# 计算需要筛选的时间
df["end_date"] = df["orddt"] + df["temp_seddays"] - df["tmp_dela"]
# 通过时间范围筛选数据
df = df[(df.start_date >= df.orddt) & (df.start_date <= df.end_date)].copy()
# 恢复原表的结构,删除辅助字段
df.drop(columns=["tmp_dela", "temp_seddays"], inplace=True)
总结
- 如果需要对大数据文件读取处理,可以适当在后台开启多线程进行IO操作,开启多进程进行计算操作
- 涉及到df.iterrows()迭代计算的,可以尝试下转换为np.array()去解决问题,兴许就会有新的发现
- 在pandas中,因为没有办法在读取文件时指定时间格式,所以建议有需要转换时间格式的话,最好使用pandas内置的pd.to_datetime()去处理,效率非常之高,千万别用lambda函数,血汗史都在上面了