DataFrame和Series赋值的性能优化
结论
DataFrame最好直接进行重构赋值新变量,而不做修改删除等操作。因为两者量级一旦起来存在极大时间差异。
背景
工作场景中,生产环境的linux系统 与 本地windows对比,发现有时间方面差异。本身0.3s能在windows匹配出来的数据,在linux中却1s匹配。
那么,在生产环境的服务器性能优于自己电脑,却产生这样子情况,故进行问题查找。
时间装饰器
首先排查问题是需要找到每一个函数所使用的时间,但是每次都写
import time
start = time.time()
df(xxx)
print("耗费时间是:{}".format(time.time()-start))
会十分浪费空间大小,所以可以用装饰器解决。
# 装饰器
def ctime(func):
def warpper(*arsg, **kwargs):
start_time = time.time()
res = func(*arsg, **kwargs)
end_time = time.time()
print("%s cost %ss" % (func.__name__, end_time - start_time))
return res
return warpper
用法是
import time
@ctime
df(xxx)
查找时间分布
1、找到耗时函数
利用装饰器找到一个函数的耗时,两者差异较大,如果在linux耗时0.4s,在windows只需0.03s-0.10s(pycharm有编译器,用pycharm 0.03s,但是python xxx.py时却0.10s)
2、分析哪一步耗时慢
函数内部使用的就是一个个表达式,无法使用装饰器,那么只能够
start = time.time()
df_tmp['num_get'] = df_tmp.apply(lambda x: self.interval_treat(str(x['info_match'])), axis=1)
print('1-------{}'.format(time.time()-start))
df_tmp['num_one'], df_tmp['num_two'] = df_tmp['num_get'].str.split('plus').str
print('2-------{}'.format(time.time()-start))
df_tmp.loc[:, 'num_get'] = df_tmp.apply(lambda x: x['num_one'] if int(x['num_two']) == 0 else x['num_get'], axis=1)
print('3-------{}'.format(time.time()-start))
df_tmp['search_num'] = df_tmp.apply(
lambda x: int(x['num_one']) % 2 if int(x['num_one']) != 0 else 'common', axis=1)
print('4-------{}'.format(time.time()-start))
逐步输出。
发现这些耗时的函数,有一个共同特点,就是在已有的DataFrame中进行添加列的操作。
查找解决办法
假设试验
一开始以为是warning问题
df_tmp['num_get'] = df_tmp.apply(lambda x: self.interval_treat(str(x['info_match'])), axis=1)
改为
df_tmp.loc[:, 'num_get'] = df_tmp.apply(lambda x: self.interval_treat(str(x['info_match'])), axis=1)
并不是这个问题
经过不断假设,发现
df_tmp['num_get'] = df_tmp.apply(lambda x: self.interval_treat(str(x['info_match'])), axis=1)
改为
df_tmp.apply(lambda x: self.interval_treat(str(x['info_match'])), axis=1)
两者差异 从 0.11s 变为 0.02s,推测可能原因是 dataframe的赋值问题。
验证想法
结合搜索找到一个对比试验
import pandas as pd
import random
import timeit
def func1():
aa = []
for x in range(200):
aa.append([random.randint(0, 1000) for r in range(5)])
pdaa = pd.DataFrame(aa)
def func2():
pdbb = pd.DataFrame()
for y in range(200):
pdbb[y] = pd.Series([random.randint(0, 1000) for r in range(5)])
def func3():
aa = {}
for x in range(200):
aa[str(x)] = random.randint(0, 1000)
psaa = pd.Series(aa)
def func4():
psbb = pd.Series()
for y in range(200):
psbb[str(y)] = random.randint(0, 1000)
t1 = timeit.timeit(stmt =func1, number=100)
t2 = timeit.timeit(stmt =func2, number=100)
print(t1, t2)
t3 = timeit.timeit(stmt =func3, number=100)
t4 = timeit.timeit(stmt =func4, number=100)
print(t3, t4)
这个函数比较出来的结果是
print(t1,t2)
0.7337615000014921 30.031491499999902
===========================
print(t3, t4)
18.894987499999843 47.094585599999846
可以发现,直接重新从list构建新的DataFrame输出,速度会提高。
按照这个思路,我将所有赋值的一些判断,全部丢到同一函数,传入的参数从 某个值, 变成直接 dataframe的每一行,让其返回的数据,从一个值变成一个列表。
def interval_treat(df_tmp_info):
addr = str(df_tmp_info['info_match'])
#addr 输出 num_1 num_2
num = str(num_1)+ 'plus' + str(num_2)
result = []
result.extend(df_tmp_info.tolist())
result.extend([num, int(num_1), int(num_2)])
return result
df_tmp['num_get'] = df_tmp.apply(lambda x: interval_treat(str(x['info_match'])), axis=1)
改为
df_tmp = df_tmp.apply(lambda x: pd.Series(interval_treat(x),index = [list(df_tmp.index)+[需要的新增字段]]), axis=1)
调用的逻辑就从赋新列的值变为直接重组成一个新DataFrame
最后实践效果:linux该函数从0.4s变为0.07s。
将pandas DataFrame列扩展为多行
推演继续
代码中很多函数需要用到一列转多行的操作,本来是使用
def split_vartical_shape(database_deal, _name):
database_deal = database_deal.drop(_name, axis=1).join(
database_deal[_name].str.split('|', expand=True).stack().reset_index(level=1, drop=True).rename(_name))
return database_deal
进行列转多行操作,可以发现它使用了join方法操作
后面更改成
def using_repeat(df, col_name_lst, repeat):
col_name_lst.remove(repeat) if repeat in col_name_lst else col_name_lst
lens = [len(item) for item in df[repeat]]
dataframe_dict = {}
for col_name in col_name_lst:
dataframe_dict[col_name] = np.repeat(df[col_name].values, lens)
dataframe_dict[repeat] = np.concatenate(df[repeat].values)
return pd.DataFrame(dataframe_dict)
50万的数据,90s执行时间优化为35s
总结
1、不管做什么,都要有对比思维,换产品经理就叫AB测试、数学就叫控制变量、生活就叫分类对比。
2、对专业方面的事情,需要有足够敏感性,发现 条件足够好,表现却不理想,需要寻找原因。
3、最基本、最蠢的方法就是最有效的手段,不要“认为”、“感觉”,要“比较”、“测试”。
4、学会一法通万法,不断复用,及时总结。比如:找到是赋值问题,那么所有代码中赋值操作是否可以优化,是否值得优化。一个数据提高0.01s速度,一百万数据就提高 1万秒(2.77h)