pandas性能百倍提升之用字典索引或ndarray替換DataFrame索引以及內存佔用分析

       在利用pandas進行數據分析時,DataFrame是其基本的數據結構,當數據量較小時還好,一旦數據量較大,比如幾十萬上百萬時,這時DataFrame就會變得笨重,笨重主要體現在對其索引的操作上,而對DataFrame的索引操作又是基本的操作,所以這時,在性能上就會有很大的損失;對pandas的使用可以讓我們可以直觀簡單的進行數據分析,但是往往會在性能上有較大的損失。當然,對於性能的損失不能一概而論,pandas具有很多的內置函數,這些函數的底層很多是c語言的python封裝,如果我們可以靈活的使用這些內置函數,那麼性能上的損失其實是很小的,甚至會提升性能,只是有時候在一些比較複雜的操作中,並沒有對應的內置函數供我們直接調用,這時,就需要從其他的角度去考慮提升性能。

性能對比和分析

       在數據分析中,往往最消耗時間的是在loop上,一旦數據量較大,比如一個50萬行的DataFrame,如果我們需要進行逐行的loop,那麼時間的消耗就相當於是一個loop的50萬倍,這種量級的放大是很恐怖的,所以,當我們要不可避免的使用loop的時候,就需要非常的謹慎,儘量的減少一個loop需要消耗的時間。

       在pandas中,一個loop中比較消耗時間的地方往往有兩方面:一是對很少的數據量用pandas的內置函數,起步時間消耗過多,對此可以轉而用python原生的方式實現,具體可以看筆者的這篇文章;二是對DataFrame的索引操作上。本文就是解決第二個問題帶來的時間消耗。

       pandas中對於DataFrame的索引操作是相對低效的,所以我們不應該在一個幾十萬的循環中使用DataFrame的索引操作,否則會造成程序效率極其低下。爲了保持DataFrame的這種數據的相對結構,我們可以有兩種方式去替換DataFrame的頻繁的索引操作:一,替換成numpy的ndarray;二,替換成python的字典數據結構。下面我們通過一個簡單的例子來對比下這三種方式的效率,如下所示。

import time
import pandas as pd
import numpy as np

df=pd.DataFrame(np.arange(800000).reshape(200000,4),columns=list('abcd'))
t1=time.time()
s1=df.apply(lambda x:x['a']+x['d'],axis=1)
t2=time.time()
print(t2-t1)

arr=df.values
t3=time.time()
s=np.apply_along_axis(lambda x:x[0]+x[3],axis=1,arr=arr)
t4=time.time()
print(t4-t3)

dic=df.to_dict()
l=[]
t5=time.time()
for i in range(len(df)):
    l.append(dic['a'][i]+dic['d'][i])
t6=time.time()
print(t6-t5)

# output: 10.034282922744751
#         1.6413774490356445
#         0.11075925827026367

       上述例子僅僅只是一個例子,只是爲了傳達本文的思想而已,因爲實際上可以完全可以通過df[['a','d']].sum(axis=1)函數來快速實現。從例子中我們可以看到,三種方式的運行效率相差很大。首先第一種方式,我們是直接利用DataFrame的apply方法實現逐行的loop,對於每一個loop,通過直接對Series的索引來實現兩列的相加;第二種方式,我們是先將DatFrame轉爲ndarray,然後在numpy中採取類似的做法;第三種方式,我們先將DataFrame轉爲python的字典對象,然後通過for loop實現,內一個loop中直接通過字典索引實現兩列的相加。

       通過對比,第一種和第二種方式之間,後者相當於把DataFrame轉爲numpy的ndarray再進行處理,可知numpy的ndarray的效率更高,雖然pandas也是基於numpy的,但是由於pandas進行了進一步的封裝,所以效率自然更低,因此,我們總是可以用numpy來處理比較耗時的pandas任務,特別是在數據量很大的時候,且無法通過內置函數直接辦到的任務,那麼numpy提升的效率可以很顯著。我們再看第一種和第三種方式的對比,後者是先把DataFrame先轉爲字典,然後通過for loop實現,這裏兩者的區別還在於前者是apply,後者是for loop,但其實apply和for loop在非內置函數的簡單操作上的效率是差不多的,甚至apply會更慢些,但是在pandas內置函數的操作上apply會快很多,具體可看筆者的這篇文章;所以這裏兩者的差距我們可以認爲是由索引造成的,明顯的,字典索引相對於DataFrame的索引,前者的效率會高很多,在python中,字典索引幾乎是最爲高效的索引方式了,因此,我們把DataFrame轉爲字典並對字典進行操作,這種方式提升的效率效果是最好的,性能幾乎提升近百倍!

內存佔用分析

        上面只是單純的從性能上進行分析對比,下面還要對比一下這三種方式在內存佔用上的區別。首先,對於DataFrame和ndarray,由於前者是基於後者的,因此兩者的內存佔用其實是產不多的,而且由於前者對後者進行了封裝,所以嚴格來講,DataFrame的內存佔用會大一些,ndarray的內存佔用會小一些,但是差別不大。由於ndarray是對內存結構進行了優化的,所以相比於python的字典對象,儲存相同的信息,字典對象的內存佔用會大很多,當然,這並不是說就是字典對象的缺點,因爲字典對象可以存儲不同類型的對象,而ndarray只可以存儲同一類型的元素,因此,不同的設計方式使得兩者在內存佔用上不太一樣,各有優劣。但是當我們通過把DataFrame轉爲dict時,相比於DataFrame,dict可儲存多類型對象的優勢不再,這時dict對象顯著變大了,這就是其一個缺點了。具體的可看如下結果。

import sys

size=total_size(dic)
print(sys.getsizeof(df))
print(arr.nbytes)
print(size)

# output: 3200104
#         3200000
#         86715124

       可以看到,df佔據了3200104字節,arr佔據了3200000字節,而dic則佔據了86715124字節。這裏對於arr和dic不能直接用sys.getsizeof獲取其實際內存,因爲sys.getsizeof只能獲取對象的本身的內存,如果對象是個容器,容器內部的內容內存佔用是無法獲取到的;字典實際上是個容器,而ndarray又有自己的內存設計,可以通過其nbytes屬性獲取,而df則兼容了sys.getsizeof這個接口。所以這裏對於dic,筆者用的是total_size這個函數,關於該函數的定義,可以查看筆者的這篇文章

       Anyway,最後我們看到的是,字典佔據的內存暴漲,相當於是df和ndarray的二十多倍。雖然字典在訪問速度上極快,但是內存佔用也是極高的,典型的用空間換時間。所以,當我們考慮性能的時候,也要兼考慮內存佔用,特別是在大數據量的情況下,如果我們盲目的將df轉爲字典,那麼可能內存就承受不住了。所以如果內存充分的足夠,那麼可以轉爲字典,但是如果內存並不是那麼充裕,那麼我們可以採用轉爲ndarray的方式去提高性能,因爲ndarray的內存佔用是三者裏最佳的,而且在性能上也是很不錯的,因此,在大數據量的情形下,考慮到內存佔用,ndarray往往是一個更佳的選擇!

發佈了143 篇原創文章 · 獲贊 83 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章