python使用pandas處理大資料節省記憶體技巧
一般來說,用pandas處理小於100兆的資料,效能不是問題。當用pandas來處理100兆至幾個G的資料時,將會比較耗時,同時會導致程式因記憶體不足而執行失敗。
當然,像Spark這類的工具能夠勝任處理100G至幾個T的大資料集,但要想充分發揮這些工具的優勢,通常需要比較貴的硬體裝置。而且,這些工具不像pandas那樣具有豐富的進行高質量資料清洗、探索和分析的特性。對於中等規模的資料,我們的願望是儘量讓pandas繼續發揮其優勢,而不是換用其他工具。
本文我們討論pandas的記憶體使用,展示怎樣簡單地為資料列選擇合適的資料型別,就能夠減少dataframe近90%的記憶體佔用。
處理棒球比賽記錄資料
我們將處理130年的棒球甲級聯賽的資料,資料來源於
Retrosheet(http://www.retrosheet.org/gamelogs/index.html)
原始資料放在127個csv檔案中,我們已經用csvkit
(https://csvkit.readthedocs.io/en/1.0.2/)
(https://data.world/dataquest/mlb-game-logs)
我們從匯入資料,並輸出前5行開始:
我們將一些重要的欄位列在下面:
date- 比賽日期
v_name- 客隊名
v_league- 客隊聯賽
h_name
h_league- 主隊聯賽
v_score- 客隊得分
h_score- 主隊得分
v_line_score- 客隊線得分, 如010000(10)00.
h_line_score- 主隊線得分, 如010000(10)0X.
park_id- 主辦場地的ID
attendance- 比賽出席人數
我們可以用Dataframe.info()方法來獲得我們dataframe的一些高level資訊,譬如資料量、資料型別和記憶體使用量。
這個方法預設情況下返回一個近似的記憶體使用量,現在我們設定引數memory_usage為‘deep’來獲得準確的記憶體使用量:
我們可以看到它有171907行和161列。pandas已經為我們自動檢測了資料型別,其中包括83列數值型資料和78列物件型資料。物件型資料列用於字串或包含混合資料型別的列。
由此我們可以進一步瞭解我們應該如何減少記憶體佔用,下面我們來看一看pandas如何在記憶體中儲存資料。
Dataframe物件的內部表示
在底層,pandas會按照資料型別將列分組形成資料塊(blocks)。下圖所示為pandas如何儲存我們資料表的前十二列:
可以注意到,這些資料塊沒有保持對列名的引用,這是由於為了儲存dataframe中的真實資料,這些資料塊都經過了優化。有個BlockManager類
會用於保持行列索引與真實資料塊的對映關係。他扮演一個API,提供對底層資料的訪問。每當我們查詢、編輯或刪除資料時,dataframe類會利用BlockManager類介面將我們的請求轉換為函式和方法的呼叫。
每種資料型別在pandas.core.internals模組中都有一個特定的類。pandas使用ObjectBlock類來表示包含字串列的資料塊,用FloatBlock類來表示包含浮點型列的資料塊。對於包含數值型資料(比如整型和浮點型)的資料塊,pandas會合並這些列,並把它們儲存為一個Numpy陣列(ndarray)。Numpy陣列是在C陣列的基礎上建立的,其值在記憶體中是連續儲存的。基於這種儲存機制,對其切片的訪問是相當快的。
由於不同型別的資料是分開存放的,我們將檢查不同資料型別的記憶體使用情況,我們先看看各資料型別的平均記憶體使用量:
由於不同型別的資料是分開存放的,我們將檢查不同資料型別的記憶體使用情況,我們先看看各資料型別的平均記憶體使用量:
我們可以看到記憶體使用最多的是78個object列,我們待會再來看它們,我們先來看看我們能否提高數值型列的記憶體使用效率。
選理解子類(Subtypes)
剛才我們提到,pandas在底層將數值型資料表示成Numpy陣列,並在記憶體中連續儲存。這種儲存方式消耗較少的空間,並允許我們較快速地訪問資料。由於pandas使用相同數量的位元組來表示同一型別的每一個值,並且numpy陣列儲存了這些值的數量,所以pandas能夠快速準確地返回數值型列所消耗的位元組量。
pandas中的許多資料型別具有多個子型別,它們可以使用較少的位元組去表示不同資料,比如,float型就有float16、float32和float64這些子型別。這些型別名稱的數字部分表明了這種型別使用了多少位元來表示資料,比如剛才列出的子型別分別使用了2、4、8個位元組。下面這張表列出了pandas中常用型別的子型別:
一個int8型別的資料使用1個位元組(8位位元)儲存一個值,可以表示256(2^8)個二進位制數值。這意味著我們可以用這種子型別去表示從-128到127(包括0)的數值。
我們可以用numpy.iinfo類來確認每一個整型子型別的最小和最大值,如下:
這裡我們還可以看到uint(無符號整型)和int(有符號整型)的區別。兩者都佔用相同的記憶體儲存量,但無符號整型由於只存正數,所以可以更高效的儲存只含正數的列。
用子型別優化數值型列
我們可以用函式pd.to_numeric()來對數值型進行向下型別轉換。我們用DataFrame.select_dtypes來只選擇整型列,然後我們優化這種型別,並比較記憶體使用量。
我們看到記憶體用量從7.9兆下降到1.5兆,降幅達80%。這對我們原始dataframe的影響有限,這是由於它只包含很少的整型列。
同理,我們再對浮點型列進行相應處理:
我們可以看到所有的浮點型列都從float64轉換為float32,記憶體用量減少50%。
我們再建立一個原始dataframe的副本,將其數值列賦值為優化後的型別,再看看記憶體用量的整體優化效果。
可以看到通過我們顯著縮減數值型列的記憶體用量,我們的dataframe的整體記憶體用量減少了7%。餘下的大部分優化將針對object型別進行。
在這之前,我們先來研究下與數值型相比,pandas如何儲存字串。
選對比數值與字元的儲存
object型別用來表示用到了Python字串物件的值,有一部分原因是Numpy缺少對缺失字串值的支援。因為Python是一種高層、解析型語言,它沒有提供很好的對記憶體中資料如何儲存的細粒度控制。
這一限制導致了字串以一種碎片化方式進行儲存,消耗更多的記憶體,並且訪問速度低下。在object列中的每一個元素實際上都是存放記憶體中真實資料位置的指標。
下圖對比展示了數值型資料怎樣以Numpy資料型別儲存,和字串怎樣以Python內建型別進行儲存的。
圖示來源並改編自Why Python Is Slow
你可能注意到上文表中提到object型別資料使用可變(variable)大小的記憶體。由於一個指標佔用1位元組,因此每一個字串佔用的記憶體量與它在Python中單獨儲存所佔用的記憶體量相等。我們用sys.getsizeof()來證明這一點,先來看看在Python單獨儲存字串,再來看看使用pandas的series的情況。
你可以看到這些字串的大小在pandas的series中與在Python的單獨字串中是一樣的。
選用類別(categoricalas)型別優化object型別
Pandas在0.15版本中引入類別型別。category型別在底層使用整型數值來表示該列的值,而不是用原值。Pandas用一個字典來構建這些整型資料到原資料的對映關係。當一列只包含有限種值時,這種設計是很不錯的。當我們把一列轉換成category型別時,pandas會用一種最省空間的int子型別去表示這一列中所有的唯一值。
為了介紹我們何處會用到這種型別去減少記憶體消耗,讓我們來看看我們資料中每一個object型別列中的唯一值個數。
可以看到在我們包含了近172000場比賽的資料集中,很多列只包含了少數幾個唯一值。
我們先選擇其中一個object列,開看看將其轉換成類別型別會發生什麼。這裡我們選用第二列:day_of_week。
我們從上表中可以看到,它只包含了7個唯一值。我們用.astype()方法將其轉換為類別型別。
可以看到,雖然列的型別改變了,但資料看上去好像沒什麼變化。我們來看看底層發生了什麼。
下面的程式碼中,我們用Series.cat.codes屬性來返回category型別用以表示每個值的整型數字。
可以看到,每一個值都被賦值為一個整數,而且這一列在底層是int8型別。這一列沒有任何缺失資料,但是如果有,category子型別會將缺失資料設為-1。
最後,我們來看看這一列在轉換為category型別前後的記憶體使用量。
存用量從9.8兆降到0.16兆,近乎98%的降幅!注意這一特殊列可能代表了我們一個極好的例子——一個包含近172000個數據的列只有7個唯一值。
這樣的話,我們把所有這種型別的列都轉換成類別型別應該會很不錯,但這裡面也要權衡利弊。首要問題是轉變為類別型別會喪失數值計算能力,在將類別型別轉換成真實的數值型別前,我們不能對category列做算術運算,也不能使用諸如Series.min()和Series.max()等方法。
對於唯一值數量少於50%的object列,我們應該堅持首先使用category型別。如果某一列全都是唯一值,category型別將會佔用更多記憶體。這是因為這樣做不僅要儲存全部的原始字串資料,還要儲存整型類別標識。有關category型別的更多限制,參看pandas文件。
下面我們寫一個迴圈,對每一個object列進行迭代,檢查其唯一值是否少於50%,如果是,則轉換成類別型別。
更之前一樣進行比較:
這本例中,所有的object列都被轉換成了category型別,但其他資料集就不一定了,所以你最好還是得使用剛才的檢查過程。
本例的亮點是記憶體用量從752.72兆降為51.667兆,降幅達93%。我們將其與我們dataframe的剩下部分合並,看看初始的861兆資料降到了多少。
耶,看來我們的進展還不錯!我們還有一招可以做優化,如果你記得我們剛才那張型別表,會發現我們資料集第一列還可以用datetime型別來表示。
你可能還記得這一列之前是作為整型讀入的,並優化成了uint32。因此,將其轉換成datetime會佔用原來兩倍的記憶體,因為datetime型別是64位位元的。將其轉換為datetime的意義在於它可以便於我們進行時間序列分析。
轉換使用pandas.to_datetime()函式,並使用format引數告之日期資料儲存為YYYY-MM-DD格式。
在資料讀入的時候設定資料型別
目前為止,我們探索了一些方法,用來減少現有dataframe的記憶體佔用。通過首先讀入dataframe,再對其一步步進行記憶體優化,我們可以更好地瞭解這些優化方法能節省多少記憶體。然而,正如我們之前談到,我們通常沒有足夠的記憶體去表達資料集中的所有資料。如果不能在一開始就建立dataframe,我們怎樣才能應用記憶體節省技術呢?
幸運的是,我們可以在讀入資料集的時候指定列的最優資料型別。pandas.read_csv()函式有一些引數可以做到這一點。dtype引數接受一個以列名(string型)為鍵字典、以Numpy型別物件為值的字典。
首先,我們將每一列的目標型別儲存在以列名為鍵的字典中,開始前先刪除日期列,因為它需要分開單獨處理。
現在我們使用這個字典,同時傳入一些處理日期的引數,讓日期以正確的格式讀入。
通過對列的優化,我們是pandas的記憶體用量從861.6兆降到104.28兆,有效降低88%。
分析棒球比賽
現在我們有了優化後的資料,可以進行一些分析。我們先看看比賽日的分佈情況。
我們可以看到,1920年代之前,週日棒球賽很少是在週日的,隨後半個世紀才逐漸增多。
我們也看到最後50年的比賽日分佈變化相對比較平穩。
我們來看看比賽時長的逐年變化。
看來棒球比賽時長從1940年代之後逐漸變長。
總結
我們學習了pandas如何儲存不同的資料型別,並利用學到的知識將我們的pandas dataframe的記憶體用量降低了近90%,僅僅只用了一點簡單的技巧:
將數值型列降級到更高效的型別
將字串列轉換為類別型別
通過對列的優化,我們是pandas的記憶體用量從861.6兆降到104.28兆,有效降低88%。
原文連結:https://www.dataquest.io/blog/pandas-big-data/