Pandas字符串操作的各種方法速度測試
由于LLM的發展, 很多的數據集都是以DF的形式發布的,所以通過Pandas操作字符串的要求變得越來越高了,所以本文將對字符串操作方法進行基準測試,看看它們是如何影響pandas的性能的。因為一旦Pandas在處理數據時超過一定限制,它們的行為就會很奇怪。
我們用Faker創建了一個100,000行的測試數據。
測試方法
安裝:
!pip install faker
生成測試數據的方法很簡答:
import pandas as pd
import numpy as np
def gen_data(x):
from faker import Faker
fake = Faker()
outdata = {}
for i in range(0,x):
outdata[i] = fake.profile()
return pd.DataFrame(outdata).T
n= 100000
basedata = gen_data(n)
然后把Google Colab將輸出存儲在Google drive中
from google.colab import drive
drive.mount('/content/drive')
創建了非常簡單的函數來測試連接兩個字符串的各種方法。
def process(a,b):
return ''.join([a,b])
def process(a,b):
return a+b
def process(a,b):
return f"{a}{b}"
def process(a,b):
return f"{a}{b}"*100
創建一個空DF,編寫一個函數將輸出%%timeit作為一行添加到數據框中
# add a row to the dataframe using %%timeit output
def add_to_df(n, m, x, outputdf):
outputdf.loc[len(outputdf.index)] = [m, n, x]
# output frame
outputdf = pd.DataFrame(columns=['method', 'n', 'timing'])
outputdf
然后就是運行上面的每個函數并將數據導出到pandas的代碼。
# get a sample of data
n = 10000
suffix = 'fstring_100x'
data = basedata.copy().sample(n).reset_index()
記錄運行時間
%%timeit -r 7 -n 1 -o
data['newcol'] = ''
for row in range(len(data)):
data.at[row ,'newcol'] = process(data.at[row, 'job'], data.at[row, 'company'])
# 451 ms ± 34 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# <TimeitResult : 451 ms ± 34 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)>
完整的函數調用
m = "Iterating over the rows"
add_to_df(n = n, m = m, x = vars(_), outputdf = outputdf)
試驗
上面是代碼,下面開始用上面的代碼進行試驗:
Iterrows (pandas原生函數)每行相加
%%timeit -r 7 -n 1 -o
data['newcol'] = ''
for row, item in data.iterrows():
data.at[row ,'newcol'] = process(item['job'], item['company'])
Itertuples(由于不可變而更安全)每行相加
%%timeit -r 7 -n 1 -o
data['newcol'] = ''
for row, job, company in data[['job','company']].itertuples():
data.at[row ,'newcol'] = process(job, company)
使用pandas原生函數作為字符串相加
%%timeit -r 7 -n 1 -o
data['newcol'] = data.job + data.company
使用原生函數pandas. series .add
%%timeit -r 7 -n 1 -o
data['newcol'] = data.job.add(data.company)
使用dataframe.apply
%%timeit -r 7 -n 1 -o
data['newcol'] = data.apply(lambda row: process(row['job'],row['company']), axis=1)
使用List Map
%%timeit -r 7 -n 1 -o
data['newcol'] = list(map(process, data.job, data.company))
Pandas矢量化
%%timeit -r 7 -n 1 -o
data['newcol'] = process(data.job, data.company)
numpy數組矢量化
%%timeit -r 7 -n 1 -o
data['newcol'] = process(data.job.to_numpy(), data.company.to_numpy())
顯式在numpy數組上使用numpy向量化
%%timeit -r 7 -n 1 -o
data['newcol'] = np.vectorize(process)(data.job.to_numpy(), data.company.to_numpy())
優化后的列表推導式
%%timeit -r 7 -n 1 -o
data['newcol'] = ''
data['newcol'] =[process(i,j) for i,j in list(zip(data.job, data.company)) ]
最后是結果的輸出:
outputdf.to_csv(f"./drive/MyDrive/{n}_{suffix}.csv")
結果
結果如下所示。我用了上面3種不同函數測試了結果。
原生的字符串加法C = a+b
從1000行擴展到100,000行所需的時間;
可視化對比:
所有矢量化方法都非常快,而且pandas標準的str.add對numpy數組也進行了矢量化。能夠看到Pandas的原生方法一般都是線性的。List-map似乎以N的平方根的速度增長
使用fstring: c = f " {a}{b} "
使用fstring,結果很有趣,有的結果無法解釋。
時間
可視化
從時間上看,長度超過10,000的DF時,向量化是正確執行的
下圖是第三個函數,就是*100,這更能說明問題,向量化操作的基本上時間沒有變化
總結
通過上面的測試,我們可以總結一下結果:
1、還是老生常談的問題,不要使用iterrows(), itertuples(),盡量不要使用DataFrame.apply(),因為幾個函數還是循環遍歷的。
2、矢量化操作在字符串操作中也是可以使用的,但是為了安全起見,使用Numpy數組。
3、列表推導式就像它的名字一樣,它還是一個list
4、還有一些奇怪的無法解釋的問題,但是大部分的情況都是可以解釋的