python使用pandas處理大數(shù)據(jù)節(jié)省內(nèi)存技巧(推薦)
一般來(lái)說(shuō),用pandas處理小于100兆的數(shù)據(jù),性能不是問(wèn)題。當(dāng)用pandas來(lái)處理100兆至幾個(gè)G的數(shù)據(jù)時(shí),將會(huì)比較耗時(shí),同時(shí)會(huì)導(dǎo)致程序因內(nèi)存不足而運(yùn)行失敗。
當(dāng)然,像Spark這類(lèi)的工具能夠勝任處理100G至幾個(gè)T的大數(shù)據(jù)集,但要想充分發(fā)揮這些工具的優(yōu)勢(shì),通常需要比較貴的硬件設(shè)備。而且,這些工具不像pandas那樣具有豐富的進(jìn)行高質(zhì)量數(shù)據(jù)清洗、探索和分析的特性。對(duì)于中等規(guī)模的數(shù)據(jù),我們的愿望是盡量讓pandas繼續(xù)發(fā)揮其優(yōu)勢(shì),而不是換用其他工具。
本文我們討論pandas的內(nèi)存使用,展示怎樣簡(jiǎn)單地為數(shù)據(jù)列選擇合適的數(shù)據(jù)類(lèi)型,就能夠減少dataframe近90%的內(nèi)存占用。
處理棒球比賽記錄數(shù)據(jù)
我們將處理130年的棒球甲級(jí)聯(lián)賽的數(shù)據(jù),數(shù)據(jù)源于
Retrosheet(http://www.retrosheet.org/gamelogs/index.html)
原始數(shù)據(jù)放在127個(gè)csv文件中,我們已經(jīng)用csvkit
(https://csvkit.readthedocs.io/en/1.0.2/)
(https://data.world/dataquest/mlb-game-logs)
我們從導(dǎo)入數(shù)據(jù),并輸出前5行開(kāi)始:
我們將一些重要的字段列在下面:
date- 比賽日期
v_name- 客隊(duì)名
v_league- 客隊(duì)聯(lián)賽
h_name- 主隊(duì)名
h_league- 主隊(duì)聯(lián)賽
v_score- 客隊(duì)得分
h_score- 主隊(duì)得分
v_line_score- 客隊(duì)線(xiàn)得分, 如010000(10)00.
h_line_score- 主隊(duì)線(xiàn)得分, 如010000(10)0X.
park_id- 主辦場(chǎng)地的ID
attendance- 比賽出席人數(shù)
我們可以用Dataframe.info()方法來(lái)獲得我們dataframe的一些高level信息,譬如數(shù)據(jù)量、數(shù)據(jù)類(lèi)型和內(nèi)存使用量。
這個(gè)方法默認(rèn)情況下返回一個(gè)近似的內(nèi)存使用量,現(xiàn)在我們?cè)O(shè)置參數(shù)memory_usage為‘deep'來(lái)獲得準(zhǔn)確的內(nèi)存使用量:
我們可以看到它有171907行和161列。pandas已經(jīng)為我們自動(dòng)檢測(cè)了數(shù)據(jù)類(lèi)型,其中包括83列數(shù)值型數(shù)據(jù)和78列對(duì)象型數(shù)據(jù)。對(duì)象型數(shù)據(jù)列用于字符串或包含混合數(shù)據(jù)類(lèi)型的列。
由此我們可以進(jìn)一步了解我們應(yīng)該如何減少內(nèi)存占用,下面我們來(lái)看一看pandas如何在內(nèi)存中存儲(chǔ)數(shù)據(jù)。
Dataframe對(duì)象的內(nèi)部表示
在底層,pandas會(huì)按照數(shù)據(jù)類(lèi)型將列分組形成數(shù)據(jù)塊(blocks)。下圖所示為pandas如何存儲(chǔ)我們數(shù)據(jù)表的前十二列:
可以注意到,這些數(shù)據(jù)塊沒(méi)有保持對(duì)列名的引用,這是由于為了存儲(chǔ)dataframe中的真實(shí)數(shù)據(jù),這些數(shù)據(jù)塊都經(jīng)過(guò)了優(yōu)化。有個(gè)BlockManager類(lèi)
會(huì)用于保持行列索引與真實(shí)數(shù)據(jù)塊的映射關(guān)系。他扮演一個(gè)API,提供對(duì)底層數(shù)據(jù)的訪(fǎng)問(wèn)。每當(dāng)我們查詢(xún)、編輯或刪除數(shù)據(jù)時(shí),dataframe類(lèi)會(huì)利用BlockManager類(lèi)接口將我們的請(qǐng)求轉(zhuǎn)換為函數(shù)和方法的調(diào)用。
每種數(shù)據(jù)類(lèi)型在pandas.core.internals模塊中都有一個(gè)特定的類(lèi)。pandas使用ObjectBlock類(lèi)來(lái)表示包含字符串列的數(shù)據(jù)塊,用FloatBlock類(lèi)來(lái)表示包含浮點(diǎn)型列的數(shù)據(jù)塊。對(duì)于包含數(shù)值型數(shù)據(jù)(比如整型和浮點(diǎn)型)的數(shù)據(jù)塊,pandas會(huì)合并這些列,并把它們存儲(chǔ)為一個(gè)Numpy數(shù)組(ndarray)。Numpy數(shù)組是在C數(shù)組的基礎(chǔ)上創(chuàng)建的,其值在內(nèi)存中是連續(xù)存儲(chǔ)的。基于這種存儲(chǔ)機(jī)制,對(duì)其切片的訪(fǎng)問(wèn)是相當(dāng)快的。
由于不同類(lèi)型的數(shù)據(jù)是分開(kāi)存放的,我們將檢查不同數(shù)據(jù)類(lèi)型的內(nèi)存使用情況,我們先看看各數(shù)據(jù)類(lèi)型的平均內(nèi)存使用量:
由于不同類(lèi)型的數(shù)據(jù)是分開(kāi)存放的,我們將檢查不同數(shù)據(jù)類(lèi)型的內(nèi)存使用情況,我們先看看各數(shù)據(jù)類(lèi)型的平均內(nèi)存使用量:
我們可以看到內(nèi)存使用最多的是78個(gè)object列,我們待會(huì)再來(lái)看它們,我們先來(lái)看看我們能否提高數(shù)值型列的內(nèi)存使用效率。
選理解子類(lèi)(Subtypes)
剛才我們提到,pandas在底層將數(shù)值型數(shù)據(jù)表示成Numpy數(shù)組,并在內(nèi)存中連續(xù)存儲(chǔ)。這種存儲(chǔ)方式消耗較少的空間,并允許我們較快速地訪(fǎng)問(wèn)數(shù)據(jù)。由于pandas使用相同數(shù)量的字節(jié)來(lái)表示同一類(lèi)型的每一個(gè)值,并且numpy數(shù)組存儲(chǔ)了這些值的數(shù)量,所以pandas能夠快速準(zhǔn)確地返回?cái)?shù)值型列所消耗的字節(jié)量。
pandas中的許多數(shù)據(jù)類(lèi)型具有多個(gè)子類(lèi)型,它們可以使用較少的字節(jié)去表示不同數(shù)據(jù),比如,float型就有float16、float32和float64這些子類(lèi)型。這些類(lèi)型名稱(chēng)的數(shù)字部分表明了這種類(lèi)型使用了多少比特來(lái)表示數(shù)據(jù),比如剛才列出的子類(lèi)型分別使用了2、4、8個(gè)字節(jié)。下面這張表列出了pandas中常用類(lèi)型的子類(lèi)型:
一個(gè)int8類(lèi)型的數(shù)據(jù)使用1個(gè)字節(jié)(8位比特)存儲(chǔ)一個(gè)值,可以表示256(2^8)個(gè)二進(jìn)制數(shù)值。這意味著我們可以用這種子類(lèi)型去表示從-128到127(包括0)的數(shù)值。
我們可以用numpy.iinfo類(lèi)來(lái)確認(rèn)每一個(gè)整型子類(lèi)型的最小和最大值,如下:
這里我們還可以看到uint(無(wú)符號(hào)整型)和int(有符號(hào)整型)的區(qū)別。兩者都占用相同的內(nèi)存存儲(chǔ)量,但無(wú)符號(hào)整型由于只存正數(shù),所以可以更高效的存儲(chǔ)只含正數(shù)的列。
用子類(lèi)型優(yōu)化數(shù)值型列
我們可以用函數(shù)pd.to_numeric()來(lái)對(duì)數(shù)值型進(jìn)行向下類(lèi)型轉(zhuǎn)換。我們用DataFrame.select_dtypes來(lái)只選擇整型列,然后我們優(yōu)化這種類(lèi)型,并比較內(nèi)存使用量。
我們看到內(nèi)存用量從7.9兆下降到1.5兆,降幅達(dá)80%。這對(duì)我們?cè)糳ataframe的影響有限,這是由于它只包含很少的整型列。
同理,我們?cè)賹?duì)浮點(diǎn)型列進(jìn)行相應(yīng)處理:
我們可以看到所有的浮點(diǎn)型列都從float64轉(zhuǎn)換為float32,內(nèi)存用量減少50%。
我們?cè)賱?chuàng)建一個(gè)原始dataframe的副本,將其數(shù)值列賦值為優(yōu)化后的類(lèi)型,再看看內(nèi)存用量的整體優(yōu)化效果。
可以看到通過(guò)我們顯著縮減數(shù)值型列的內(nèi)存用量,我們的dataframe的整體內(nèi)存用量減少了7%。余下的大部分優(yōu)化將針對(duì)object類(lèi)型進(jìn)行。
在這之前,我們先來(lái)研究下與數(shù)值型相比,pandas如何存儲(chǔ)字符串。
選對(duì)比數(shù)值與字符的儲(chǔ)存
object類(lèi)型用來(lái)表示用到了Python字符串對(duì)象的值,有一部分原因是Numpy缺少對(duì)缺失字符串值的支持。因?yàn)镻ython是一種高層、解析型語(yǔ)言,它沒(méi)有提供很好的對(duì)內(nèi)存中數(shù)據(jù)如何存儲(chǔ)的細(xì)粒度控制。
這一限制導(dǎo)致了字符串以一種碎片化方式進(jìn)行存儲(chǔ),消耗更多的內(nèi)存,并且訪(fǎng)問(wèn)速度低下。在object列中的每一個(gè)元素實(shí)際上都是存放內(nèi)存中真實(shí)數(shù)據(jù)位置的指針。
下圖對(duì)比展示了數(shù)值型數(shù)據(jù)怎樣以Numpy數(shù)據(jù)類(lèi)型存儲(chǔ),和字符串怎樣以Python內(nèi)置類(lèi)型進(jìn)行存儲(chǔ)的。
圖示來(lái)源并改編自Why Python Is Slow
你可能注意到上文表中提到object類(lèi)型數(shù)據(jù)使用可變(variable)大小的內(nèi)存。由于一個(gè)指針占用1字節(jié),因此每一個(gè)字符串占用的內(nèi)存量與它在Python中單獨(dú)存儲(chǔ)所占用的內(nèi)存量相等。我們用sys.getsizeof()來(lái)證明這一點(diǎn),先來(lái)看看在Python單獨(dú)存儲(chǔ)字符串,再來(lái)看看使用pandas的series的情況。
你可以看到這些字符串的大小在pandas的series中與在Python的單獨(dú)字符串中是一樣的。
選用類(lèi)別(categoricalas)類(lèi)型優(yōu)化object類(lèi)型
Pandas在0.15版本中引入類(lèi)別類(lèi)型。category類(lèi)型在底層使用整型數(shù)值來(lái)表示該列的值,而不是用原值。Pandas用一個(gè)字典來(lái)構(gòu)建這些整型數(shù)據(jù)到原數(shù)據(jù)的映射關(guān)系。當(dāng)一列只包含有限種值時(shí),這種設(shè)計(jì)是很不錯(cuò)的。當(dāng)我們把一列轉(zhuǎn)換成category類(lèi)型時(shí),pandas會(huì)用一種最省空間的int子類(lèi)型去表示這一列中所有的唯一值。
為了介紹我們何處會(huì)用到這種類(lèi)型去減少內(nèi)存消耗,讓我們來(lái)看看我們數(shù)據(jù)中每一個(gè)object類(lèi)型列中的唯一值個(gè)數(shù)。
可以看到在我們包含了近172000場(chǎng)比賽的數(shù)據(jù)集中,很多列只包含了少數(shù)幾個(gè)唯一值。
我們先選擇其中一個(gè)object列,開(kāi)看看將其轉(zhuǎn)換成類(lèi)別類(lèi)型會(huì)發(fā)生什么。這里我們選用第二列:day_of_week。
我們從上表中可以看到,它只包含了7個(gè)唯一值。我們用.astype()方法將其轉(zhuǎn)換為類(lèi)別類(lèi)型。
可以看到,雖然列的類(lèi)型改變了,但數(shù)據(jù)看上去好像沒(méi)什么變化。我們來(lái)看看底層發(fā)生了什么。
下面的代碼中,我們用Series.cat.codes屬性來(lái)返回category類(lèi)型用以表示每個(gè)值的整型數(shù)字。
可以看到,每一個(gè)值都被賦值為一個(gè)整數(shù),而且這一列在底層是int8類(lèi)型。這一列沒(méi)有任何缺失數(shù)據(jù),但是如果有,category子類(lèi)型會(huì)將缺失數(shù)據(jù)設(shè)為-1。
最后,我們來(lái)看看這一列在轉(zhuǎn)換為category類(lèi)型前后的內(nèi)存使用量。
存用量從9.8兆降到0.16兆,近乎98%的降幅!注意這一特殊列可能代表了我們一個(gè)極好的例子——一個(gè)包含近172000個(gè)數(shù)據(jù)的列只有7個(gè)唯一值。
這樣的話(huà),我們把所有這種類(lèi)型的列都轉(zhuǎn)換成類(lèi)別類(lèi)型應(yīng)該會(huì)很不錯(cuò),但這里面也要權(quán)衡利弊。首要問(wèn)題是轉(zhuǎn)變?yōu)轭?lèi)別類(lèi)型會(huì)喪失數(shù)值計(jì)算能力,在將類(lèi)別類(lèi)型轉(zhuǎn)換成真實(shí)的數(shù)值類(lèi)型前,我們不能對(duì)category列做算術(shù)運(yùn)算,也不能使用諸如Series.min()和Series.max()等方法。
對(duì)于唯一值數(shù)量少于50%的object列,我們應(yīng)該堅(jiān)持首先使用category類(lèi)型。如果某一列全都是唯一值,category類(lèi)型將會(huì)占用更多內(nèi)存。這是因?yàn)檫@樣做不僅要存儲(chǔ)全部的原始字符串?dāng)?shù)據(jù),還要存儲(chǔ)整型類(lèi)別標(biāo)識(shí)。有關(guān)category類(lèi)型的更多限制,參看pandas文檔。
下面我們寫(xiě)一個(gè)循環(huán),對(duì)每一個(gè)object列進(jìn)行迭代,檢查其唯一值是否少于50%,如果是,則轉(zhuǎn)換成類(lèi)別類(lèi)型。
更之前一樣進(jìn)行比較:
這本例中,所有的object列都被轉(zhuǎn)換成了category類(lèi)型,但其他數(shù)據(jù)集就不一定了,所以你最好還是得使用剛才的檢查過(guò)程。
本例的亮點(diǎn)是內(nèi)存用量從752.72兆降為51.667兆,降幅達(dá)93%。我們將其與我們dataframe的剩下部分合并,看看初始的861兆數(shù)據(jù)降到了多少。
耶,看來(lái)我們的進(jìn)展還不錯(cuò)!我們還有一招可以做優(yōu)化,如果你記得我們剛才那張類(lèi)型表,會(huì)發(fā)現(xiàn)我們數(shù)據(jù)集第一列還可以用datetime類(lèi)型來(lái)表示。
你可能還記得這一列之前是作為整型讀入的,并優(yōu)化成了uint32。因此,將其轉(zhuǎn)換成datetime會(huì)占用原來(lái)兩倍的內(nèi)存,因?yàn)閐atetime類(lèi)型是64位比特的。將其轉(zhuǎn)換為datetime的意義在于它可以便于我們進(jìn)行時(shí)間序列分析。
轉(zhuǎn)換使用pandas.to_datetime()函數(shù),并使用format參數(shù)告之日期數(shù)據(jù)存儲(chǔ)為YYYY-MM-DD格式。
在數(shù)據(jù)讀入的時(shí)候設(shè)定數(shù)據(jù)類(lèi)型
目前為止,我們探索了一些方法,用來(lái)減少現(xiàn)有dataframe的內(nèi)存占用。通過(guò)首先讀入dataframe,再對(duì)其一步步進(jìn)行內(nèi)存優(yōu)化,我們可以更好地了解這些優(yōu)化方法能節(jié)省多少內(nèi)存。然而,正如我們之前談到,我們通常沒(méi)有足夠的內(nèi)存去表達(dá)數(shù)據(jù)集中的所有數(shù)據(jù)。如果不能在一開(kāi)始就創(chuàng)建dataframe,我們?cè)鯓硬拍軕?yīng)用內(nèi)存節(jié)省技術(shù)呢?
幸運(yùn)的是,我們可以在讀入數(shù)據(jù)集的時(shí)候指定列的最優(yōu)數(shù)據(jù)類(lèi)型。pandas.read_csv()函數(shù)有一些參數(shù)可以做到這一點(diǎn)。dtype參數(shù)接受一個(gè)以列名(string型)為鍵字典、以Numpy類(lèi)型對(duì)象為值的字典。
首先,我們將每一列的目標(biāo)類(lèi)型存儲(chǔ)在以列名為鍵的字典中,開(kāi)始前先刪除日期列,因?yàn)樗枰珠_(kāi)單獨(dú)處理。
現(xiàn)在我們使用這個(gè)字典,同時(shí)傳入一些處理日期的參數(shù),讓日期以正確的格式讀入。
通過(guò)對(duì)列的優(yōu)化,我們是pandas的內(nèi)存用量從861.6兆降到104.28兆,有效降低88%。
分析棒球比賽
現(xiàn)在我們有了優(yōu)化后的數(shù)據(jù),可以進(jìn)行一些分析。我們先看看比賽日的分布情況。
我們可以看到,1920年代之前,周日棒球賽很少是在周日的,隨后半個(gè)世紀(jì)才逐漸增多。
我們也看到最后50年的比賽日分布變化相對(duì)比較平穩(wěn)。
我們來(lái)看看比賽時(shí)長(zhǎng)的逐年變化。
看來(lái)棒球比賽時(shí)長(zhǎng)從1940年代之后逐漸變長(zhǎng)。
總結(jié)
我們學(xué)習(xí)了pandas如何存儲(chǔ)不同的數(shù)據(jù)類(lèi)型,并利用學(xué)到的知識(shí)將我們的pandas dataframe的內(nèi)存用量降低了近90%,僅僅只用了一點(diǎn)簡(jiǎn)單的技巧:
將數(shù)值型列降級(jí)到更高效的類(lèi)型
將字符串列轉(zhuǎn)換為類(lèi)別類(lèi)型
通過(guò)對(duì)列的優(yōu)化,我們是pandas的內(nèi)存用量從861.6兆降到104.28兆,有效降低88%。
以上所述是小編給大家介紹的python使用pandas處理大數(shù)據(jù)節(jié)省內(nèi)存技巧詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
python字符串驗(yàn)證的幾種實(shí)現(xiàn)方法
字符串的驗(yàn)證是確保數(shù)據(jù)符合特定要求的關(guān)鍵步驟之一,本文主要介紹了python字符串驗(yàn)證的幾種實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07Python 50行爬蟲(chóng)抓取并處理圖靈書(shū)目過(guò)程詳解
這篇文章主要介紹了Python 50行爬蟲(chóng)抓取并處理圖靈書(shū)目過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09Python中的response.text與content區(qū)別詳解
這篇文章主要介紹了Python中的response.text與content區(qū)別詳解,?從網(wǎng)絡(luò)請(qǐng)求下來(lái)的數(shù)據(jù),他們都是字節(jié)類(lèi)型的,如果服務(wù)器不指定的話(huà),默認(rèn)編碼是"ISO-8859-1",我們使用text直接拿到的是字符串類(lèi)型,沒(méi)有進(jìn)行解碼操作,則會(huì)出現(xiàn)亂碼問(wèn)題,需要的朋友可以參考下2023-12-12對(duì)Python subprocess.Popen子進(jìn)程管道阻塞詳解
今天小編就為大家分享一篇對(duì)Python subprocess.Popen子進(jìn)程管道阻塞詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-10-10python實(shí)現(xiàn)與redis交互操作詳解
這篇文章主要介紹了python實(shí)現(xiàn)與redis交互操作,結(jié)合實(shí)例形式分析了Python Redis模塊的安裝、導(dǎo)入、連接與簡(jiǎn)單操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2020-04-04