1 簡介
pandas
雖然是個非常流行的資料分析利器,但很多朋友在使用pandas
處理較大規模的資料集的時候經常會反映pandas
運算“慢”,且記憶體開銷“大”。
特別是很多學生黨在使用自己效能一般的筆記本嘗試處理大型資料集時,往往會被捉襟見肘的算力所勸退。但其實只要掌握一定的pandas
使用技巧,配置一般的機器也有能力hold住大型資料集的分析。
本文就將以真實資料集和運存16G的普通膝上型電腦為例,演示如何運用一系列策略實現多快好省地用pandas
分析大型資料集。
2 pandas多快好省策略
我們使用到的資料集來自kaggle
上的TalkingData AdTracking Fraud Detection Challenge競賽( https://www.kaggle.com/c/talkingdata-adtracking-fraud-detection ),使用到其對應的訓練集,這是一個大小有7.01G的csv
檔案。
下面我們將循序漸進地探索在記憶體開銷和計算時間成本之間尋求平衡,首先我們不做任何優化,直接使用pandas
的read_csv()
來讀取train.csv
檔案:
import pandas as pd
raw = pd.read_csv('train.csv')
# 檢視資料框記憶體使用情況
raw.memory_usage(deep=True)
可以看到首先我們讀入整個資料集所花費的時間達到了將近三分鐘,且整個過程中因為中間各種臨時變數的建立,一度快要撐爆我們16G的執行記憶體空間。
這樣一來我們後續想要開展進一步的分析可是說是不可能的,因為隨便一個小操作就有可能會因為中間過程大量的臨時變數而撐爆記憶體,導致當機藍屏,所以我們第一步要做的是降低資料框所佔的記憶體:
- 指定資料型別以節省記憶體
因為pandas
預設情況下讀取資料集時各個欄位確定資料型別時不會替你優化記憶體開銷,比如我們下面利用引數nrows
先讀入資料集的前1000行試探著看看每個欄位都是什麼型別:
raw = pd.read_csv('train.csv', nrows=1000)
raw.info()
怪不得我們的資料集讀進來會那麼的大,原來所有的整數列都轉換為了int64
來儲存,事實上我們原資料集中各個整數字段的取值範圍根本不需要這麼高的精度來儲存,因此我們利用dtype
引數來降低一些欄位的數值精度:
raw = pd.read_csv('train.csv', nrows=1000,
dtype={
'ip': 'int32',
'app': 'int16',
'device': 'int16',
'os': 'int16',
'channel': 'int16',
'is_attributed': 'int8'
})
raw.info()
可以看到,在修改資料精度之後,前1000行資料集的記憶體大小被壓縮了將近54.6%,這是個很大的進步,按照這個方法我們嘗試著讀入全量資料並檢視其info()
資訊:
可以看到隨著我們對資料精度的優化,資料集所佔記憶體有了非常可觀的降低,使得我們開展進一步的資料分析更加順暢,比如分組計數:
(
raw
# 按照app和os分組計數
.groupby(['app', 'os'])
.agg({'ip': 'count'})
)
那如果資料集的資料型別沒辦法優化,那還有什麼辦法在不撐爆記憶體的情況下完成計算分析任務呢?
- 只讀取需要的列
如果我們的分析過程並不需要用到原資料集中的所有列,那麼就沒必要全讀進來,利用usecols
引數來指定需要讀入的欄位名稱:
raw = pd.read_csv('train.csv', usecols=['ip', 'app', 'os'])
raw.info()
可以看到,即使我們沒有對資料精度進行優化,讀進來的資料框大小也只有4.1個G,如果配合上資料精度優化效果會更好:
如果有的情況下我們即使優化了資料精度又篩選了要讀入的列,資料量依然很大的話,我們還可以以分塊讀入的方式來處理資料:
- 分塊讀取分析資料
利用chunksize
引數,我們可以為指定的資料集建立分塊讀取IO流,每次最多讀取設定的chunksize
行資料,這樣我們就可以把針對整個資料集的任務拆分為一個一個小任務最後再彙總結果:
from tqdm.notebook import tqdm
# 在降低資料精度及篩選指定列的情況下,以1千萬行為塊大小
raw = pd.read_csv('train.csv',
dtype={
'ip': 'int32',
'app': 'int16',
'os': 'int16'
},
usecols=['ip', 'app', 'os'],
chunksize=10000000)
# 從raw中迴圈提取每個塊並進行分組聚合,最後再彙總結果
result = \
(
pd
.concat([chunk
.groupby(['app', 'os'], as_index=False)
.agg({'ip': 'count'}) for chunk in tqdm(raw)])
.groupby(['app', 'os'])
.agg({'ip': 'sum'})
)
result
可以看到,利用分塊讀取處理的策略,從始至終我們都可以保持較低的記憶體負載壓力,並且一樣完成了所需的分析任務,同樣的思想,如果你覺得上面分塊處理的方式有些費事,那下面我們就來上大招:
- 利用dask替代pandas進行資料分析
dask
相信很多朋友都有聽說過,它的思想與上述的分塊處理其實很接近,只不過更加簡潔,且對系統資源的排程更加智慧,從單機到叢集,都可以輕鬆擴充套件伸縮。
推薦使用conda install dask
來安裝dask
相關元件,安裝完成後,我們僅僅需要需要將import pandas as pd
替換為import dask.dataframe as dd
,其他的pandas
主流API使用方式則完全相容,幫助我們無縫地轉換程式碼:
可以看到整個讀取過程只花費了313毫秒,這當然不是真的讀進了記憶體,而是dask
的延時載入技術,這樣才有能力處理超過記憶體範圍的資料集。
接下來我們只需要像操縱pandas
的資料物件一樣正常書寫程式碼,最後加上.compute()
,dask
便會基於前面搭建好的計算圖進行正式的結果運算:
(
raw
# 按照app和os分組計數
.groupby(['app', 'os'])
.agg({'ip': 'count'})
.compute() # 啟用計算圖
)
並且dask
會非常智慧地排程系統資源,使得我們可以輕鬆跑滿所有CPU:
關於dask
的更多知識可以移步官網自行學習( https://docs.dask.org/en/latest/ )。
以上就是本文的全部內容,歡迎在評論區與我進行討論~