許多教學(xué)中的數(shù)據(jù)和現(xiàn)實(shí)世界中的數(shù)據(jù)的區(qū)別在于,現(xiàn)實(shí)世界的數(shù)據(jù)很少干凈且均勻分布的。特別是在許多有趣的數(shù)據(jù)集中,有著大量的缺失數(shù)據(jù)。讓事情變得更加復(fù)雜的是,不同的數(shù)據(jù)來(lái)源可能會(huì)出現(xiàn)不同的形式的缺失數(shù)據(jù)。
在這一章中,我們將會(huì)討論處理缺失數(shù)據(jù)的一些常見(jiàn)思路。討論P(yáng)andas是如何表達(dá)缺失數(shù)據(jù)的,并展示Python中內(nèi)置的一些用來(lái)處理缺失數(shù)據(jù)的Pandas工具。在本中,我們通常將“null”, “NaN”, or “NA” 這些值認(rèn)為是缺失數(shù)據(jù)。
處理缺失數(shù)據(jù)常用方法的利弊權(quán)衡
人們開(kāi)發(fā)了許多方案用來(lái)在一組數(shù)據(jù)中表示缺失的數(shù)據(jù)的存在。一般來(lái)說(shuō),它們圍繞以下這兩種策略:用一個(gè)掩碼來(lái)全局的表示缺失的值,或者選擇一個(gè)哨兵值來(lái)表示缺失記錄。
在掩碼方法中,掩碼可以是一個(gè)完全獨(dú)立的布爾數(shù)組,或者在數(shù)據(jù)表示中以一個(gè)獨(dú)立的位來(lái)表明該值為空的狀態(tài)。
在哨兵值的方法中,哨兵值可以使一些特定數(shù)據(jù)類(lèi)型慣用的值,比如用-9999或者罕見(jiàn)的位模式表明缺失的整數(shù)值。哨兵值也可以是更加普遍的慣用值,比如說(shuō)用NaN(Not a Number——不是一個(gè)數(shù)字,IEEE浮點(diǎn)數(shù)規(guī)約中的一個(gè)特殊的值)來(lái)表明一個(gè)缺失的浮點(diǎn)數(shù)值。
這些方法中沒(méi)有一個(gè)是不進(jìn)行折衷權(quán)衡的:使用一個(gè)單獨(dú)的掩碼數(shù)組就需要分配一個(gè)額外的布爾數(shù)組,這將增加存儲(chǔ)和計(jì)算的開(kāi)銷(xiāo)。而哨兵值降低了可表示的合法值的范圍,并且可能需要CPU和GPU額外的(通常是非優(yōu)化的)算法邏輯。像NaN這樣的通用特殊值并不是對(duì)于所有的數(shù)據(jù)類(lèi)型的可用的。
和許多沒(méi)有全局最優(yōu)解的情況類(lèi)似,不同的開(kāi)發(fā)語(yǔ)言和系統(tǒng)使用不同的傳統(tǒng)。舉例來(lái)說(shuō),R語(yǔ)言使用每個(gè)數(shù)據(jù)類(lèi)型中保留的位模式作為哨兵值來(lái)表示缺失的數(shù)據(jù),而SciDB系統(tǒng)則為每一個(gè)數(shù)據(jù)點(diǎn)附上額外的一個(gè)字節(jié)表示數(shù)據(jù)缺失的(NA)狀態(tài)。
Pandas中缺失數(shù)據(jù)的處理
Pandas對(duì)于如何處理缺失值的選擇受限于它對(duì)于NumPy包的依賴(lài)。NumPy中沒(méi)有對(duì)于非浮點(diǎn)數(shù)數(shù)據(jù)類(lèi)型缺失值的內(nèi)置表示。
Pandas本可以像R語(yǔ)言一樣為每一個(gè)單獨(dú)的數(shù)據(jù)類(lèi)型使用特定的位模式來(lái)表示空值,但是這一方法被證明在Pandas中是相當(dāng)笨重且難以實(shí)現(xiàn)。R語(yǔ)言?xún)H包含四中基本數(shù)據(jù)類(lèi)型,但NumPy支持的遠(yuǎn)多于這些:比如說(shuō),R語(yǔ)言只有一個(gè)整數(shù)類(lèi)型,但是如果考慮到可用精度、符號(hào)和編碼,NumPy支持十四種基本整數(shù)類(lèi)型。如果為所有的NumPy中的數(shù)據(jù)類(lèi)型都保留一個(gè)特定的位模式,這將會(huì)導(dǎo)致對(duì)各種數(shù)據(jù)類(lèi)型的各種特定場(chǎng)景的操作的開(kāi)銷(xiāo)大大增加,并且其實(shí)現(xiàn)很有可能需要新建一個(gè)NumPy包。
NumPy是支持掩碼數(shù)組的,比如說(shuō)數(shù)組中附有一個(gè)單獨(dú)的布爾掩碼數(shù)組用來(lái)將數(shù)據(jù)標(biāo)記為“好”和“壞”。Pandas本來(lái)可以沿用這一特性,但是存儲(chǔ)、計(jì)算和代碼維護(hù)上的開(kāi)銷(xiāo)使得這一選擇根本沒(méi)有吸引力。
考慮到這些限制,Pandas的實(shí)現(xiàn)選擇為缺失數(shù)據(jù)賦予哨兵值,并且進(jìn)一步選擇使用兩個(gè)在Python中已經(jīng)存在的空值:特殊的浮點(diǎn)數(shù)空值NaN和Python的None對(duì)象。這一選擇也有一些副作用,我們?cè)谙挛闹袝?huì)提道,但是在實(shí)際應(yīng)用中的大部分場(chǎng)景下都是一個(gè)不錯(cuò)的折衷之選。
None:Python式的缺失數(shù)據(jù)處理
Pandas首先使用的哨兵值是None。None是一個(gè)Python中的單例對(duì)象,在Python的代碼中經(jīng)常被用于表示缺失的數(shù)據(jù)。由于這是一個(gè)Python對(duì)象,它不是能被NumPy/Pandas中隨意一個(gè)數(shù)組使用,它只能在數(shù)據(jù)類(lèi)型為“Object”的數(shù)據(jù)中使用。(比如說(shuō)類(lèi)型為Python對(duì)象的數(shù)組)
import numpy as np import pandas as pd
vals1 = np.array([1, None, 3, 4]) vals1
這里的dtype=object表示NumPy根據(jù)數(shù)組內(nèi)容推斷出的最匹配的數(shù)組元素類(lèi)型就是它們是Python對(duì)象。盡管這類(lèi)對(duì)象數(shù)組在某些情況下是比較適用的,但是任何對(duì)于數(shù)據(jù)的運(yùn)算將在Python層面上完成,這將會(huì)比基于原生類(lèi)型數(shù)組的傳統(tǒng)的快速運(yùn)算有更多的開(kāi)銷(xiāo)。
for dtype in ['object', 'int']:
print("dtype =", dtype)
%timeit np.arange(1E6, dtype=dtype).sum()
print()
在一個(gè)數(shù)組中使用Python對(duì)象也意味著如果你在一個(gè)有None值的數(shù)組上執(zhí)行諸如sum()和min()的聚合運(yùn)算,通常情況下會(huì)報(bào)錯(cuò)。
vals1.sum()
這是因?yàn)镻ython中將一個(gè)整數(shù)和None相加的結(jié)果是undefined。
NaN: 缺失的數(shù)值型數(shù)據(jù)
另一個(gè)缺失數(shù)據(jù)的表示NaN(“Not a Number”,不是一個(gè)數(shù)字的縮寫(xiě))與前面提到的None有所不同,這是一個(gè)特殊浮點(diǎn)數(shù)值。它可以被所有使用標(biāo)準(zhǔn)IEEE浮點(diǎn)數(shù)值表示方法的系統(tǒng)所識(shí)別。
vals2 = np.array([1, np.nan, 3, 4]) vals2.dtype
這里可以注意到,NumPy為這個(gè)數(shù)組選擇的是原生的浮點(diǎn)數(shù)類(lèi)型。這意味著不像上面的對(duì)象數(shù)組,這個(gè)數(shù)組支持在已編譯代碼中的那些快速運(yùn)算。你應(yīng)該要意識(shí)到NaN像一個(gè)數(shù)據(jù)病毒,它會(huì)影響到任何其他它接觸到的對(duì)象。不管是什么運(yùn)算,任何有NaN參與的運(yùn)算結(jié)果將會(huì)是另外一個(gè)NaN。
1 + np.nan
0 *? np.nan
請(qǐng)注意,這意味著對(duì)包含NaN值的求和或求最大值等操作是明確定義的(不會(huì)導(dǎo)致一個(gè)錯(cuò)誤),但也不是很有用的。
vals2.sum(), vals2.min(), vals2.max()
請(qǐng)記住,NaN是一個(gè)特別的浮點(diǎn)數(shù)值,在整型、字符串或其他類(lèi)型中并不存在等效的值。
一些例子
以上每一個(gè)哨兵值表示方法都有其用武之地,Pandas中對(duì)于這兩者的處理幾乎是可以交替互換的,會(huì)根據(jù)合適的場(chǎng)景在兩個(gè)哨兵值之間互相轉(zhuǎn)換。
data = pd.Series([1, np.nan, 2, None]) data
記住,盡管None是一個(gè)Python對(duì)象類(lèi)型的數(shù)據(jù)缺失表示、NaN是浮點(diǎn)型的數(shù)據(jù)缺失表示,但在Pandas中字符串、布爾值或整數(shù)值并沒(méi)有對(duì)應(yīng)類(lèi)型的缺失值的表示。當(dāng)這些類(lèi)型需要表示數(shù)據(jù)缺失時(shí),Pandas通過(guò)類(lèi)型轉(zhuǎn)換來(lái)解決這個(gè)問(wèn)題。例如,如果我們將一個(gè)整數(shù)數(shù)組中的值設(shè)為np.nan,該數(shù)組將會(huì)自動(dòng)被轉(zhuǎn)換為浮點(diǎn)型,從而使得NaN可以適用于數(shù)組。
x = pd.Series(range(2), dtype=int) x[0] = None x
需要注意的是,除了將整數(shù)數(shù)組轉(zhuǎn)化為浮點(diǎn)類(lèi)型,Pandas還會(huì)自動(dòng)將None轉(zhuǎn)化為NaN。盡管與像R語(yǔ)言這樣的特定領(lǐng)域語(yǔ)言中所使用的更加統(tǒng)一的方法相比,Pandas的這種處理會(huì)令人感到有點(diǎn)隨性,但是在實(shí)際應(yīng)用中,Pandas的哨兵(類(lèi)型轉(zhuǎn)換)方法很好用,并且以我個(gè)人使用經(jīng)驗(yàn)來(lái)說(shuō),很少出問(wèn)題。
| 類(lèi)型 | 當(dāng)存儲(chǔ)缺失數(shù)據(jù)時(shí)的類(lèi)型轉(zhuǎn)換 | 缺失數(shù)據(jù)對(duì)應(yīng)的哨兵值 |
| floating浮點(diǎn)值 | 無(wú)改變 | np.nan |
| Objcet
對(duì)象 |
無(wú)改變 | None?或者?np.nan |
| Integer整型 | 轉(zhuǎn)化為?64位浮點(diǎn)數(shù)float64 | np.nan |
| Boolean布爾值 | 轉(zhuǎn)化為?對(duì)象Objcet | None?或者?np.nan |
請(qǐng)記住,在Pandas中,字符串?dāng)?shù)據(jù)總是以Python對(duì)象類(lèi)型來(lái)存儲(chǔ)的。
空值運(yùn)算
我們已經(jīng)看到,Pandas將None和NaN按需交替使用來(lái)表示缺失或者空值。為了充分發(fā)揮這一習(xí)慣用法的作用,在Pandas中有幾個(gè)有用的方法來(lái)檢測(cè)、去除和替代Pandas數(shù)據(jù)結(jié)構(gòu)中的空值,它們是:
? isnull(): 生成一個(gè)布爾掩碼數(shù)組來(lái)標(biāo)示出缺失的值
? notnull(): 與isnull()的作用相反
? dropna(): 返回一個(gè)過(guò)濾缺失值/空值后的數(shù)據(jù)
? fillna(): 返回一個(gè)數(shù)據(jù)拷貝,其中的缺失值都已被填充或者估算
檢測(cè)空值
Pandas數(shù)據(jù)結(jié)構(gòu)中有兩種檢測(cè)空數(shù)據(jù)的有用的方法:isnull() 以及 notnull()。
這兩個(gè)都會(huì)返回一個(gè)基于數(shù)據(jù)的布爾類(lèi)型的標(biāo)記數(shù)組,例如:
data = pd.Series([1, np.nan, 'hello', None])
data.isnull()
正如在第X章提到的,布爾掩碼數(shù)組可以直接作為一個(gè)Series或者DataFrame的索引值
data[data.notnull()]
isnull() 和 notnull() 對(duì)于DataFrame會(huì)產(chǎn)生相似的布爾結(jié)果。
處理空值
除了上面用到的掩碼方法之外,還有其他簡(jiǎn)便的方法,dropna() 和 fillna()。這兩個(gè)方法分別用于去除缺失數(shù)據(jù)和填充缺失數(shù)據(jù)。對(duì)于一個(gè)Series來(lái)說(shuō),其結(jié)果是顯而易見(jiàn)的:
data.dropna()
而對(duì)于一個(gè)DataFrame來(lái)說(shuō),還可以有更多的選擇。如下面這個(gè)DataFrame:
df = pd.DataFrame([[1, np.nan, 2], [2, 3, 5], [np.nan, 4, 6]]) df
我們不能從DataFrame中把每一個(gè)單獨(dú)的值去除掉,我們只能去除整行或者整列。根據(jù)不用的應(yīng)用,你可能會(huì)想選擇兩者之一。所以說(shuō)dropna()方法為DataFrame提供了不少選擇。
默認(rèn)情況下,dropna()方法將會(huì)去除所有數(shù)據(jù)中包含空值的行。
df.dropna()
或者,你可以基于不同的軸(axis)來(lái)去除缺失值:axis=1 將會(huì)去除所有包含空值的列。
df.dropna(axis=1)
但是這樣也會(huì)把好的數(shù)據(jù)也除去了。你可能對(duì)于去除行或列值均為空(或大部分為空)的那些數(shù)據(jù)感興趣。這可以通過(guò)how和thresh參數(shù)來(lái)進(jìn)行指定,它們?cè)试S你對(duì)可以通過(guò)的空值的數(shù)量進(jìn)行精確的控制。
how參數(shù)的默認(rèn)值是‘a(chǎn)ny’,使得任何包含空值的行或者列(取決于軸關(guān)鍵字)將會(huì)被剔除。你也可以將how指定為‘a(chǎn)ll’,這樣的話(huà)只有所有值都為空的行和列才會(huì)被去除。
df[3] = np.nan df
df.dropna(axis=1, how='all')
請(qǐng)記住,為了使得代碼更為清晰易懂,你可以使用axis=’rows’ 而不是 axis=0 , 以及axis=’columns’ 而不是 axis=1.
為了更加細(xì)粒度的控制,thresh參數(shù)讓你可以指定保留的行或者列的最小非空值數(shù)。
df.dropna(thresh=3)
這里第一行和最后一行被去除了,因?yàn)樗鼈儍H包含2個(gè)非空值。
填充空值
有的時(shí)候與其去掉空值,你更愿意用一個(gè)合法的值來(lái)進(jìn)行替代。這個(gè)值可能是一個(gè)數(shù),比如零?;蛘咚赡苁悄撤N形式的基于合法值的插值或插補(bǔ)。你可以直接用isnull()方法作為掩碼來(lái)做到這一點(diǎn),但因?yàn)檫@種操作太常見(jiàn)了,Pandas提供了fillna()方法,它會(huì)返回一個(gè)數(shù)組的拷貝,其中的空值已被替換。
Consider the following Series:參考下面這個(gè)Series:
data = pd.Series([1, np.nan, 2, None, 3], index=list('abcde'))</p>
data
我們可以用一個(gè)單獨(dú)的值來(lái)填充缺失值,比如說(shuō)零。
data.fillna(0)
我們可以指定一個(gè)前置值填充,將前一個(gè)值傳到空值處填充。
# forward-fill data.fillna(method='fill')
或者我們可以指定一個(gè)后置填充,將下一個(gè)值傳到空值處填充。
# back-fill data.fillna(method='bill')
對(duì)于DataFrame來(lái)說(shuō),其選擇是相似的,但是我們還可以指定一整個(gè)軸的值來(lái)進(jìn)行填充。
df
df.fillna(method='ffill', axis=1)
請(qǐng)注意,如果前一個(gè)值在前置填充的過(guò)程中不可用,那么缺失值將仍然保留。
總結(jié)
本文中,我們看到了Pandas是如何處理空值/缺失值的,以及一些專(zhuān)門(mén)為了一致地處理DataFrame和Series中的缺失數(shù)據(jù)而設(shè)計(jì)的方法。在現(xiàn)實(shí)世界的數(shù)據(jù)集中,缺失數(shù)據(jù)是生活的真實(shí)寫(xiě)照,我們將在接下來(lái)的章節(jié)中經(jīng)??吹竭@些(處理缺失數(shù)據(jù)的)工具。
杰克?范德普拉斯(Jake VanderPlas)
杰克?范德普拉斯是Python科學(xué)計(jì)算組件的長(zhǎng)期用戶(hù)和開(kāi)發(fā)者。他現(xiàn)在是華盛頓大學(xué)跨學(xué)科研究主管,他主要進(jìn)行他自己的天文學(xué)研究,并在各個(gè)領(lǐng)域?yàn)榈目茖W(xué)家提供建議和咨詢(xún)。

