實操 | 內存占用減少高達90%,還不用升級硬件?沒錯,這篇文章教你妙用Pandas輕松處理大規模數據


注:Pandas(Python Data Analysis Library) 是基於 NumPy 的一種工具,該工具是為了解決數據分析任務而創建的。此外,Pandas 納入了大量庫和一些標准的數據模型,提供了高效地操作大型數據集所需的工具。

 

相比較於 Numpy,Pandas 使用一個二維的數據結構 DataFrame 來表示表格式的數據, 可以存儲混合的數據結構,同時使用 NaN 來表示缺失的數據,而不用像 Numpy 一樣要手工處理缺失的數據,並且 Pandas 使用軸標簽來表示行和列。

 

Pandas 通常用於處理小數據(小於 100Mb),而且對計算機的性能要求不高,但是當我們需要處理更大的數據時(100Mb到幾千Gb),計算機性能就成了問題,如果配置過低就會導致更長的運行時間,甚至因為內存不足導致運行失敗。

 

在處理大型數據集時(100Gb到幾TB),我們通常會使用像 Spark 這樣的工具,但是想要充分發揮 Spark 的功能,通常需要很高的硬件配置,導致成本過高。而且與 Pandas 不同,這些工具缺少可用於高質量數據清洗、勘測和分析的特征集。

 

因此對於中等規模的數據,我們最好挖掘 Pandas 的潛能,而不是轉而使用其他工具。那么在不升級計算機配置的前提下,我們要怎么解決內存不足的問題呢?

 

在這篇文章中,我們將介紹 Pandas 的內存使用情況,以及如何通過為數據框(dataframe)中的列(column)選擇適當的數據類型,將數據框的內存占用量減少近 90%。

 

棒球比賽日志

 

我們將要處理的是 130 年來的大型棒球聯盟比賽數據,原始數據來源於 retrosheet。

 

最原始的數據是 127 個獨立的 CSV 文件,不過我們已經使用 csvkit 合並了這些文件,並且在第一行中為每一列添加了名字。 如果讀者想親自動手操作,可下載網站上的數據實踐下:https://data.world/dataquest/mlb-game-logs

 

首先讓我們導入數據,看看前五行:

import pandas as pdgl = pd.read_csv('game_logs.csv')gl.head()

 

 

我們總結了一些重要的列,但是如果你想查看所有的列的指南,我們也為整個數據集創建了一個數據字典:

 

 

 

 

我們可以使用 DataFrame.info() 的方法為我們提供數據框架的更多高層次的信息,包括數據大小、類型、內存使用情況的信息。默認情況下,Pandas 會占用和數據框大小差不多的內存來節省時間。因為我們對准確度感興趣,所以我們將 memory_usage 的參數設置為 ‘deep’,以此來獲取更准確的數字。

 

 

 

我們可以看到,這個數據集共有 171,907 行、161 列。Pandas 已經自動檢測了數據的類型:83 列數字(numeric),78 列對象(object)。對象列(object columns)主要用於存儲字符串,包含混合數據類型。為了更好地了解怎樣減少內存的使用量,讓我們看看 Pandas 是如何將數據存儲在內存中的。

 

數據框的內部表示

 

在底層,Pandas 按照數據類型將列分成不同的塊(blocks)。這是 Pandas 如何存儲數據框前十二列的預覽。

 

 

你會注意到這些數據塊不會保留對列名的引用。這是因為數據塊對存儲數據框中的實際值進行了優化,BlockManager class 負責維護行、列索引與實際數據塊之間的映射。它像一個 API 來提供訪問底層數據的接口。每當我們選擇、編輯、或刪除某個值時,dataframe class 會和 BlockManager class 進行交互,將我們的請求轉換為函數和方法調用。

 

每個類型在 pandas.core.internals 模塊中都有一個專門的類, Pandas 使用 ObjectBlock class 來代表包含字符串列的塊,FloatBlock class 表示包含浮點型數據(float)列的塊。對於表示數值(如整數和浮點數)的塊,Pandas 將這些列組合在一起,並存儲為 NumPy ndarry 數組。NumPy ndarry 是圍繞 C array 構建的,而且它們的值被存儲在連續的內存塊中。由於采用這種存儲方案,訪問這些值的地址片段(slice)是非常快的。

 

因為不同的數據都是單獨存儲的,所以我們將檢查不同類型的數據的內存使用情況。我們先來看看所有數據類型的平均內存使用情況。

 

 

可以看到,大部分的內存都被 78 個對象列占用了。我們稍后再來分析,首先看看我們是否可以提高數字列(numeric columns)的內存使用率。

 

了解子類型

 

正如前面介紹的那樣,在底層,Pandas 將數值表示為 NumPy ndarrays,並將它存儲在連續的內存塊中。該存儲模型消耗的空間較小,並允許我們快速訪問這些值。因為 Pandas 中,相同類型的值會分配到相同的字節數,而 NumPy ndarray 里存儲了值的數量,所以 Pandas 可以快速並准確地返回一個數值列占用的字節數。

 

Pandas 中的許多類型包含了多個子類型,因此可以使用較少的字節數來表示每個值。例如,float 類型就包含 float16、float32、float64 等子類型。類型名稱的數字部分代表 了用於表示值類型的位數。例如,我們剛剛列出的子類型就分別使用了 2、4、8、16 個字節。下表顯示了最常見的 Pandas 的子類型:

 

 

int8 使用 1 個字節(或者 8 位)來存儲一個值,並且可以以二進制表示 256 個值。這意味着,我們可以使用這種子類型來表示從 -128 到 127 (包括0)的值。我們可以使用 numpy.iinfo class 來驗證每個整數子類型的最小值和最大值,我們來看一個例子:

 

 

我們可以在這里看到 uint(無符號整數)和 int(有符號整數)之間的區別。這兩種類型具有相同的存儲容量,但如果只存儲正數,無符號整數顯然能夠讓我們更高效地存儲只包含正值的列。

 

使用子類型優化數字列

 

我們可以使用函數 pd.to_numeric() 來 downcast(向下轉型)我們的數值類型。我們將使用 DataFrame.select_dtypes 來選擇整數列,然后優化這些列包含的類型,並比較優化前后內存的使用情況。

 


我們可以看到,內存的使用量從 7.9Mb 降到了 1.5 Mb,減少了 80% 以上。但這對原始數據框的影響並不大,因為本身整數列就非常少。

 

現在,讓我們來對浮點型數列做同樣的事情。

 

 

可以看到,我們所有的浮點型數列都從 float64 轉換成 float32,使得內存的使用量減少了 50%。

 

讓我們創建一個原始數據框的副本,然后分配這些優化后的數字列代替原始數據,並查看現在的內存使用情況。

 

 

雖然我們大大減少了數字列的內存使用量,但是從整體來看,我們只是將數據框的內存使用量降低了 7%。內存使用量降低的主要原因是我們對對象類型(object types)進行了優化。

 

在動手之前,讓我們仔細看一下,與數字類型相比,字符串是怎樣存在 Pandas 中的。

 

比較數字和字符串的存儲方式

 

對象類型代表了 Python 字符串對象的值,部分原因是 NumPy 缺少對字符串值的支持。因為 Python 是一種高級的解釋語言,它不能對數值的存儲方式進行細粒度控制。

 

這種限制使得字符串以分散的方式存儲在內存里,不僅占用了更多的內存,而且訪問速度較慢。對象列表中的每一個元素都是一個指針(pointer),它包含了實際值在內存中位置的“地址”。

 

下面的圖標展示了數字值是如何存儲在 NumPy 數據類型中,以及字符串如何使用 Python 內置的類型存儲。

 

你可能已經注意到,我們的圖表之前將對象類型描述成使用可變內存量。當每個指針占用一字節的內存時,每個字符的字符串值占用的內存量與 Python 中單獨存儲時相同。讓我們使用 sys.getsizeof() 來自證明這一點:先查看單個字符串,然后查看 Pandas 系列中的項目(items)。

 

 

 

你可以看到,存儲在 Pandas 中的字符串的大小與作為 Python 中單獨字符串的大小相同。

 

使用分類來優化對象類型板面的做法和配料

 

Pandas 在 0.15版引入了  Categoricals (分類)。category 類型在底層使用整數類型來表示該列的值,而不是原始值。Pandas 用一個單獨的字典來映射整數值和相應的原始值之間的關系。當某一列包含的數值集有限時,這種設計是很有用的。當我們將列轉換為 category dtype 時,Pandas 使用了最省空間的 int 子類型,來表示一列中所有的唯一值。

 

 

想要知道我們可以怎樣使用這種類型來減少內存使用量。首先 ,讓我們看看每一種對象類型的唯一值的數量。

 

 

可以看到,我們的數據集中一共有 17.2 萬場比賽, 而唯一值的數量是非常少的。

 

在我們深入分析之前,我們首先選擇一個對象列,當我們將其轉換為 categorical type時,觀察下會發生什么。我們選擇了數據集中的第二列 day_of_week 來進行試驗。

 

在上面的表格中,我們可以看到它只包含了七個唯一的值。我們將使用 .astype() 的方法將其轉換為 categorical。

 

 

如你所見,除了列的類型已經改變,這些數據看起來完全一樣。我們來看看發生了什么。

 

在下面的代碼中,我們使用 Series.cat.codes 屬性來返回 category 類型用來表示每個值的整數值。

 

 

你可以看到,每個唯一值都被分配了一個整數,並且該列的底層數據類型現在是 int8。該列沒有任何缺失值,如果有的話,這個 category 子類型會將缺省值設置為 -1。最后,我們來看看這個列在轉換到 category 類型之前和之后的內存使用情況。

 

 

 

可以看到,內存使用量從原來的 9.8MB 降到了 0.16MB,相當於減少了 98%!請注意,這一列可能代表我們最好的情況之一:一個具有 172,000 個項目的列,只有 7 個唯一的值。

 

將所有的列都進行同樣的操作,這聽起來很吸引人,但使我們要注意權衡。可能出現的最大問題是無法進行數值計算。我們不能在將其轉換成真正的數字類型的前提下,對這些 category 列進行計算,或者使用類似 Series.min() 和 Series.max() 的方法。

 

當對象列中少於 50% 的值時唯一對象時,我們應該堅持使用 category 類型。但是如果這一列中所有的值都是唯一的,那么 category 類型最終將占用更多的內存。這是因為列不僅要存儲整數 category 代碼,還要存儲所有的原始字符串的值。你可以閱讀 Pandas 文檔,了解 category 類型的更多限制。

 

我們將編寫一個循環程序,遍歷每個對象列,檢查其唯一值的數量是否小於 50%。如果是,那么我們就將這一列轉換為 category 類型。

 

 

和之前的相比

 

 

 

在這種情況下,我們將所有對象列都轉換為 category 類型,但是這種情況並不符合所有的數據集,因此務必確保事先進行過檢查。

 

此外,對象列的內存使用量已經從 752MB 將至 52MB,減少了 93%。現在,我們將其與數據框的其余部分結合起來,再與我們最開始的 861MB 的內存使用量進行對比。

 

 

 

可以看到,我們已經取得了一些進展,但是我們還有一個地方可以優化。回到我們的類型表,里面有一個日期(datetime)類型可以用來表示數據集的第一列。

 

 

 

你可能記得這一列之前是作為整數型讀取的,而且已經被優化為 uint32。因此,將其轉換為 datetime 時,內存的占用量會增加一倍,因為 datetime 的類型是 64 位。無論如何,將其轉換成 datetime 是有價值的,因為它將讓時間序列分析更加容易。

 

我們將使用 pandas.to_datetime()  函數進行轉換,並使用 format 參數讓日期數據按照  YYYY-MM-DD 的格式存儲。

 

 

在讀取數據時選擇類型

 

到目前為止,我們已  探索了減少現有數 據框內存占用的方法。首先,讀入閱讀數據框,然后再反復迭代節省內存的方法,這讓我們可以更好地了解每次優化可以節省的內存空間。然而,正如我們前面提到那樣,我們經常沒有足夠的內存來表示數據集中所有的值。如果一開始就不能創建數據框,那么我們該怎樣使用內存節省技術呢?

 

幸運的是,當我們讀取數據集時,我們可以制定列的最優類型。pandas.read_csv() 函數有幾個不同的參數可以讓我們做到這一點。dtype 參數可以是一個以(字符串)列名稱作為 keys、以 NumPy 類型對象作為值的字典。

 

首先,我們將每列的最終類型、以及列的名字的 keys 存在一個字典中。因為日期列需要單獨對待,因此我們先要刪除這一列。

 

 

現在,我們可以使用字典、以及幾個日期的參數,通過幾行代碼,以正確的類型讀取日期數據。

 

 

通過優化這些列,我們設法將 pandas 中的內存使用量,從 861.6MB 降到了 104.28MB,減少了 88%。

 

分析棒球比賽

 

我們已經優化了數據,現在我們可以開始對數據進行分析了。我們來看看比賽的時間分布。

 

 

 

可以看到,在二十世紀二十年代之前,棒球比賽很少在周日舉行,一直到下半世紀才逐漸流行起來。此外,我們也可以清楚地看到,在過去的五十年里,比賽時間的分是相對靜態的。我們來看看比賽時長多年來的變化。

 

 

 

 

看起來,棒球比賽的時長自 1940 年以來就一直處於增長狀態。

 

總結和后續步驟 

 

我們已經了解到 Pandas 是如何存儲不同類型的數據的,然后我們使用這些知識將 Pandas 里的數據框的內存使用量降低了近 90%,而這一切只需要幾個簡單的技巧:

 

  • 將數字列 downcast 到更節省空間的類型;

    • 將字符串轉換為分類類型(categorical type)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM