美文网首页
python - 精进 - DataFrame和Series赋值

python - 精进 - DataFrame和Series赋值

作者: Gaafung峰 | 来源:发表于2020-07-24 14:48 被阅读0次

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

    image-20200724141548533

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

    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)

    相关文章

      网友评论

          本文标题:python - 精进 - DataFrame和Series赋值

          本文链接:https://www.haomeiwen.com/subject/srijlktx.html