DataFrame和Series賦值的性能優(yōu)化
結(jié)論
DataFrame最好直接進(jìn)行重構(gòu)賦值新變量,而不做修改刪除等操作。因?yàn)閮烧吡考?jí)一旦起來(lái)存在極大時(shí)間差異。
背景
工作場(chǎng)景中,生產(chǎn)環(huán)境的linux系統(tǒng) 與 本地windows對(duì)比,發(fā)現(xiàn)有時(shí)間方面差異。本身0.3s能在windows匹配出來(lái)的數(shù)據(jù),在linux中卻1s匹配。
那么,在生產(chǎn)環(huán)境的服務(wù)器性能優(yōu)于自己電腦,卻產(chǎn)生這樣子情況,故進(jìn)行問(wèn)題查找。
時(shí)間裝飾器
首先排查問(wèn)題是需要找到每一個(gè)函數(shù)所使用的時(shí)間,但是每次都寫
import time
start = time.time()
df(xxx)
print("耗費(fèi)時(shí)間是:{}".format(time.time()-start))
會(huì)十分浪費(fèi)空間大小,所以可以用裝飾器解決。
# 裝飾器
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)
查找時(shí)間分布
1、找到耗時(shí)函數(shù)
利用裝飾器找到一個(gè)函數(shù)的耗時(shí),兩者差異較大,如果在linux耗時(shí)0.4s,在windows只需0.03s-0.10s(pycharm有編譯器,用pycharm 0.03s,但是python xxx.py時(shí)卻0.10s)
2、分析哪一步耗時(shí)慢

函數(shù)內(nèi)部使用的就是一個(gè)個(gè)表達(dá)式,無(wú)法使用裝飾器,那么只能夠
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))
逐步輸出。
發(fā)現(xiàn)這些耗時(shí)的函數(shù),有一個(gè)共同特點(diǎn),就是在已有的DataFrame中進(jìn)行添加列的操作。
查找解決辦法
假設(shè)試驗(yàn)
一開始以為是warning問(wèn)題
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)
并不是這個(gè)問(wèn)題
經(jīng)過(guò)不斷假設(shè),發(fā)現(xiàn)
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 變?yōu)?0.02s,推測(cè)可能原因是 dataframe的賦值問(wèn)題。
驗(yàn)證想法
結(jié)合搜索找到一個(gè)對(duì)比試驗(yàn)
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)
這個(gè)函數(shù)比較出來(lái)的結(jié)果是
print(t1,t2)
0.7337615000014921 30.031491499999902
===========================
print(t3, t4)
18.894987499999843 47.094585599999846
可以發(fā)現(xiàn),直接重新從list構(gòu)建新的DataFrame輸出,速度會(huì)提高。
按照這個(gè)思路,我將所有賦值的一些判斷,全部丟到同一函數(shù),傳入的參數(shù)從 某個(gè)值, 變成直接 dataframe的每一行,讓其返回的數(shù)據(jù),從一個(gè)值變成一個(gè)列表。
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)
調(diào)用的邏輯就從賦新列的值變?yōu)橹苯又亟M成一個(gè)新DataFrame
最后實(shí)踐效果:linux該函數(shù)從0.4s變?yōu)?.07s。
將pandas DataFrame列擴(kuò)展為多行
推演繼續(xù)
代碼中很多函數(shù)需要用到一列轉(zhuǎn)多行的操作,本來(lái)是使用
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
進(jìn)行列轉(zhuǎn)多行操作,可以發(fā)現(xiàn)它使用了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萬(wàn)的數(shù)據(jù),90s執(zhí)行時(shí)間優(yōu)化為35s
總結(jié)
1、不管做什么,都要有對(duì)比思維,換產(chǎn)品經(jīng)理就叫AB測(cè)試、數(shù)學(xué)就叫控制變量、生活就叫分類對(duì)比。
2、對(duì)專業(yè)方面的事情,需要有足夠敏感性,發(fā)現(xiàn) 條件足夠好,表現(xiàn)卻不理想,需要尋找原因。
3、最基本、最蠢的方法就是最有效的手段,不要“認(rèn)為”、“感覺(jué)”,要“比較”、“測(cè)試”。
4、學(xué)會(huì)一法通萬(wàn)法,不斷復(fù)用,及時(shí)總結(jié)。比如:找到是賦值問(wèn)題,那么所有代碼中賦值操作是否可以優(yōu)化,是否值得優(yōu)化。一個(gè)數(shù)據(jù)提高0.01s速度,一百萬(wàn)數(shù)據(jù)就提高 1萬(wàn)秒(2.77h)