DataFrame和Series赋值的性能优化

结论

DataFrame最好直接进行重构赋值新变量,而不做修改删除等操作。因为两者量级一旦起来存在极大时间差异。

背景

工作场景中,生产环境的linux系统 与 本地windows对比,发现有时间方面差异。本身0.3s能在windows匹配出来的数据,在linux中却1s匹配。

那么,在生产环境的服务器性能优于自己电脑,却产生这样子情况,故进行问题查找。

时间装饰器

首先排查问题是需要找到每一个函数所使用的时间,但是每次都写



import



会十分浪费空间大小,所以可以用装饰器解决。



# 装饰器
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、分析哪一步耗时慢




如何将series转化成图表 怎么把series变成dataframe_如何将series转化成图表


函数内部使用的就是一个个表达式,无法使用装饰器,那么只能够


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或dict 构建新的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)