7.7 處理缺失數(shù)據(jù)
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
本節(jié)是《Python 數(shù)據(jù)科學(xué)手冊》(Python Data Science Handbook)的摘錄析孽。
許多教程中的數(shù)據(jù)與現(xiàn)實世界中的數(shù)據(jù)之間的差異在于埠褪,真實世界的數(shù)據(jù)很少是干凈和同構(gòu)的睛约。特別是怔锌,許多有趣的數(shù)據(jù)集缺少一些數(shù)據(jù)氯材。為了使事情變得更復(fù)雜兆沙,不同的數(shù)據(jù)源可能以不同的方式標(biāo)記缺失數(shù)據(jù)荣挨。
在本節(jié)中男韧,我們將討論缺失數(shù)據(jù)的一些一般注意事項,討論 Pandas 如何選擇來表示它默垄,并演示一些處理 Python 中的缺失數(shù)據(jù)的 Pandas 內(nèi)置工具此虑。在整本書中,我們將缺失數(shù)據(jù)稱為空值或NaN
值口锭。
缺失數(shù)據(jù)慣例中的權(quán)衡
許多方案已經(jīng)開發(fā)出來朦前,來指示表格或DataFrame
中是否存在缺失數(shù)據(jù)。通常鹃操,它們圍繞兩種策略中的一種:使用在全局表示缺失值的掩碼韭寸,或選擇表示缺失條目的標(biāo)記值。
在掩碼方法中荆隘,掩碼可以是完全獨立的布爾數(shù)組恩伺,或者它可以在數(shù)據(jù)表示中占用一個比特,在本地表示值的空狀態(tài)椰拒。
在標(biāo)記方法中晶渠,標(biāo)記值可能是某些特定于數(shù)據(jù)的慣例凰荚,例如例如使用-9999
或某些少見的位組合來表示缺失整數(shù)值,或者它可能是更全局的慣例褒脯,例如使用NaN
(非數(shù)字)表示缺失浮點值便瑟,這是一個特殊值,它是 IEEE 浮點規(guī)范的一部分憨颠。
這些方法都沒有權(quán)衡:使用單獨的掩碼數(shù)組需要分配額外的布爾數(shù)組胳徽,這會增加存儲和計算的開銷。標(biāo)記值減少了可以表示的有效值的范圍爽彤,并且可能需要 CPU 和 GPU 算法中的額外(通常是非最優(yōu)的)邏輯养盗。 像NaN
這樣的常見特殊值不適用于所有數(shù)據(jù)類型。
在大多數(shù)情況下适篙,不存在普遍最佳選擇往核,不同的語言和系統(tǒng)使用不同的慣例。例如嚷节,R 語言使用每種數(shù)據(jù)類型中的保留位組合聂儒,作為表示缺失數(shù)據(jù)的標(biāo)記值,而 SciDB 系統(tǒng)使用表示 NA 狀態(tài)的額外字節(jié)硫痰,附加到每個單元衩婚。
Pandas 中的缺失數(shù)據(jù)
Pandas 處理缺失值的方式受到其對 NumPy 包的依賴性的限制,NumPy 包沒有非浮點數(shù)據(jù)類型的 NA 值的內(nèi)置概念效斑。
Pandas 可以遵循 R 的指導(dǎo)非春,為每個單獨的數(shù)據(jù)類型指定位組合來表示缺失值,但這種方法結(jié)果相當(dāng)笨拙缓屠。雖然 R 包含四種基本數(shù)據(jù)類型奇昙,但 NumPy 支持更多:例如,R 具有單個整數(shù)類型敌完,但是一旦考慮到編碼的可用精度储耐,簽名和字節(jié)順序,NumPy 支持十四個基本整數(shù)類型滨溉。
在所有可用的 NumPy 類型中保留特定的位組合什湘,將產(chǎn)生各種類型的各種操作的大量開銷,甚至可能需要 NumPy 包的新分支业踏。 此外禽炬,對于較小的數(shù)據(jù)類型(例如 8 位整數(shù)),犧牲一個位用作掩碼勤家,將顯著減小它可以表示的值的范圍腹尖。
NumPy 確實支持掩碼數(shù)組嗎?也就是說,附加了一個獨立的布爾掩碼數(shù)組的數(shù)組热幔,用于將數(shù)據(jù)標(biāo)記為“好”或“壞”乐设。Pandas 可能源于此,但是存儲绎巨,計算和代碼維護(hù)的開銷近尚,使得這個選擇變得沒有吸引力。
考慮到這些約束场勤,Pandas 選擇使用標(biāo)記來丟失數(shù)據(jù)戈锻,并進(jìn)一步選擇使用兩個已經(jīng)存在的 Python 空值:特殊浮點值NaN
和 Python None
對象。我們將要看到和媳,這種選擇有一些副作用格遭,但實際上在大多數(shù)相關(guān)情況下,最終都是很好的妥協(xié)留瞳。
None
:Python 風(fēng)格的缺失數(shù)據(jù)
Pandas 使用的第一個標(biāo)記值是None
拒迅,這是一個 Python 單例對象,通常用于 Python 代碼中的缺失數(shù)據(jù)她倘。因為它是一個 Python 對象璧微,所以None
不能用于任何 NumPy/Pandas 數(shù)組,只能用于數(shù)據(jù)類型為'object'
的數(shù)組(即 Python 對象數(shù)組):
import numpy as np
import pandas as pd
vals1 = np.array([1, None, 3, 4])
vals1
# array([1, None, 3, 4], dtype=object)
這個dtype = object
意味著硬梁,它是最好的公共類型表示前硫。NumPy 可以推斷出,數(shù)組的內(nèi)容是 Python 對象荧止。雖然這種對象數(shù)組對于某些目的很有用开瞭,但是對數(shù)據(jù)的任何操作都將在 Python 層面完成,與具有原生類型的數(shù)組的常見快速操作相比罩息,其開銷要大得多:
for dtype in ['object', 'int']:
print("dtype =", dtype)
%timeit np.arange(1E6, dtype=dtype).sum()
print()
'''
dtype = object
10 loops, best of 3: 78.2 ms per loop
dtype = int
100 loops, best of 3: 3.06 ms per loop
'''
在數(shù)組中使用 Python 對象也意味著,如果你在一個帶有None
值的數(shù)組中執(zhí)行sum()
或min()
之類的聚合个扰,你通常會得到錯誤:
vals1.sum()
'''
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-4-749fd8ae6030> in <module>()
----> 1 vals1.sum()
/Users/jakevdp/anaconda/lib/python3.5/site-packages/numpy/core/_methods.py in _sum(a, axis, dtype, out, keepdims)
30
31 def _sum(a, axis=None, dtype=None, out=None, keepdims=False):
---> 32 return umr_sum(a, axis, dtype, out, keepdims)
33
34 def _prod(a, axis=None, dtype=None, out=None, keepdims=False):
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
'''
這反映了一個事實瓷炮,即整數(shù)和None
之間的加法是未定義的。
NaN
:缺失的數(shù)值數(shù)據(jù)
另一個缺失的數(shù)據(jù)表示递宅,NaN
(“非數(shù)字”的首字母縮寫)是不同的娘香;它是所有系統(tǒng)都識別的特殊浮點值,使用標(biāo)準(zhǔn) IEEE 浮點表示:
vals2 = np.array([1, np.nan, 3, 4])
vals2.dtype
# dtype('float64')
請注意办龄,NumPy 為此數(shù)組選擇了一個原生浮點類型:這意味著與之前的對象數(shù)組不同烘绽,此數(shù)組支持推送到編譯代碼中的快速操作。你應(yīng)該知道NaN
有點像數(shù)據(jù)病毒 - 它會感染它觸及的任何其他對象俐填。無論操作如何安接,NaN
的算術(shù)結(jié)果都是另一個NaN
:
1 + np.nan
# nan
0 * np.nan
# nan
請注意,這意味著值的聚合是定義良好的(即英融,它們不會導(dǎo)致錯誤)盏檐,但并不總是有用:
vals2.sum(), vals2.min(), vals2.max()
# (nan, nan, nan)
NumPy 確實提供了一些忽略這些缺失值的特殊聚合:
np.nansum(vals2), np.nanmin(vals2), np.nanmax(vals2)
# (8.0, 1.0, 4.0)
請記住歇式,NaN
是一個特殊浮點值;整數(shù)胡野,字符串或其他類型沒有等效的NaN
值材失。
Pandas 中的NaN
和None
NaN
和None
都有它們的位置,并且 Pandas 的構(gòu)建是為了幾乎可以互換地處理這兩個值硫豆,在適當(dāng)?shù)臅r候在它們之間進(jìn)行轉(zhuǎn)換:
pd.Series([1, np.nan, 2, None])
'''
0 1.0
1 NaN
2 2.0
3 NaN
dtype: float64
'''
對于沒有可用標(biāo)記值的類型龙巨,當(dāng)存在 NA 值時,Pandas 會自動進(jìn)行類型轉(zhuǎn)換熊响。例如旨别,如果我們將整數(shù)數(shù)組中的值設(shè)置為np.nan
,它將自動向上轉(zhuǎn)換為浮點類型來兼容 NA:
x = pd.Series(range(2), dtype=int)
x
'''
0 0
1 1
dtype: int64
'''
x[0] = None
x
'''
0 NaN
1 1.0
dtype: float64
'''
請注意耘眨,除了將整數(shù)數(shù)組轉(zhuǎn)換為浮點數(shù)外昼榛,Pandas 還會自動將None
轉(zhuǎn)換為NaN
值。(請注意剔难,有人建議未來向 Pandas 添加原生整數(shù) NA胆屿;截至本文撰寫時,尚未包含此內(nèi)容偶宫。)
雖然與 R 等領(lǐng)域特定語言中非迹,更為統(tǒng)一的 NA 值方法相比,這種黑魔法可能會有些笨拙纯趋,但 Pandas 標(biāo)記值方法在實踐中運作良好憎兽,根據(jù)我的經(jīng)驗,很少會產(chǎn)生問題吵冒。
下表列出了引入 NA 值時 Pandas 中的向上轉(zhuǎn)換慣例:
類型 | 儲存 NA 時的慣例 | NA 標(biāo)記值 |
---|---|---|
floating |
不變 | np.nan |
object |
不變 |
None 或np.nan
|
integer |
轉(zhuǎn)換為float64
|
np.nan |
boolean |
轉(zhuǎn)換為object
|
None 或np.nan
|
請記住纯命,在 Pandas 中,字符串?dāng)?shù)據(jù)始終與object dtype
一起存儲痹栖。
空值上的操作
正如我們所看到的亿汞,Pandas 將None
和NaN
視為基本可互換的,用于指示缺失值或空值揪阿。為了促進(jìn)這個慣例疗我,有幾種有用的方法可用于檢測,刪除和替換 Pandas 數(shù)據(jù)結(jié)構(gòu)中的空值南捂。他們是:
-
isnull()
: 生成表示缺失值的布爾掩碼 -
notnull()
:isnull()
的反轉(zhuǎn) -
dropna()
: 返回數(shù)據(jù)的過濾后版本 -
fillna()
: 返回數(shù)據(jù)的副本吴裤,填充了缺失值
我們將結(jié)束本節(jié),簡要探討和演示這些例程溺健。
檢測控制
Pandas 數(shù)據(jù)結(jié)構(gòu)有兩種有用的方法來檢測空數(shù)據(jù):isnull()
和notnull()
麦牺。任何一個都返回數(shù)據(jù)上的布爾掩碼。例如:
data = pd.Series([1, np.nan, 'hello', None])
data.isnull()
'''
0 False
1 True
2 False
3 True
dtype: bool
'''
如“數(shù)據(jù)索引和選擇”中所述,布爾掩碼可以直接用作Series
或DataFrame
的索引:
data[data.notnull()]
'''
0 1
2 hello
dtype: object
'''
isnull()
和notnull()
方法為DataFrame
生成類似的布爾結(jié)果枕面。
刪除空值
除了之前使用的掩碼之外愿卒,還有一些方便的方法,dropna()
(刪除 NA 值)和fillna()
(填充 NA 值)潮秘。 對于Series
琼开,結(jié)果很簡單:
data.dropna()
'''
0 1
2 hello
dtype: object
'''
對于DataFrame
,還有更多選項枕荞」窈颍考慮以下DataFrame
:
df = pd.DataFrame([[1, np.nan, 2],
[2, 3, 5],
[np.nan, 4, 6]])
df
0 | 1 | 2 | |
---|---|---|---|
0 | 1.0 | NaN | 2 |
1 | 2.0 | 3.0 | 5 |
2 | NaN | 4.0 | 6 |
我們不能從DataFrame
中刪除單個值;我們只能刪除完整行或完整列躏精。取決于應(yīng)用渣刷,你可能需要其中一個,因此dropna()
為DataFrame
提供了許多選項矗烛。
默認(rèn)情況下辅柴,dropna()
將刪除包含空值的所有行:
df.dropna()
0 | 1 | 2 | |
---|---|---|---|
1 | 2.0 | 3.0 | 5 |
或者,你可以沿不同的軸刪除 NA 值; axis = 1
刪除包含空值的所有列:
df.dropna(axis='columns')
2 | |
---|---|
0 | 2 |
1 | 5 |
2 | 6 |
但這也會丟掉一些好的數(shù)據(jù); 你可能更愿意刪除全部為 NA 值或大多數(shù)為 NA 值的行或列瞭吃。這可以通過how
或thresh
參數(shù)來指定碌嘀,這些參數(shù)能夠精確控制允許通過的空值數(shù)量。
默認(rèn)值是how ='any'
歪架,這樣任何包含空值的行或列(取決于axis
關(guān)鍵字)都將被刪除股冗。你也可以指定how ='all'
,它只會丟棄全部為空值的行/列:
df[3] = np.nan
df
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 1.0 | NaN | 2 | NaN |
1 | 2.0 | 3.0 | 5 | NaN |
2 | NaN | 4.0 | 6 | NaN |
df.dropna(axis='columns', how='all')
0 | 1 | 2 | |
---|---|---|---|
0 | 1.0 | NaN | 2 |
1 | 2.0 | 3.0 | 5 |
2 | NaN | 4.0 | 6 |
對于更細(xì)粒度的控制和蚪,thresh
參數(shù)允許你為要保留的行/列指定最小數(shù)量的非空值:
df.dropna(axis='rows', thresh=3)
0 | 1 | 2 | 3 | |
---|---|---|---|---|
1 | 2.0 | 3.0 | 5 | NaN |
這里刪除了第一行和最后一行止状,因為它們只包含兩個非空值。
填充空值
有時比起刪除 NA 值攒霹,你寧愿用有效值替換它們怯疤。這個值可能是單個數(shù)字,如零催束,或者可能是某種良好的替換或插值旅薄。你可以將isnull()
方法用作掩碼,原地執(zhí)行此操作泣崩,但因為它是如此常見的操作,Pandas 提供fillna()
方法洛口,該方法返回數(shù)組的副本矫付,其中空值已替換。
考慮下面的Series
:
data = pd.Series([1, np.nan, 2, None, 3], index=list('abcde'))
data
'''
a 1.0
b NaN
c 2.0
d NaN
e 3.0
dtype: float64
'''
我們可以使用單個值填充 NA 條目第焰,例如零:
data.fillna(0)
'''
a 1.0
b 0.0
c 2.0
d 0.0
e 3.0
dtype: float64
'''
我們可以指定前向填充來傳播前一個值:
# 向前填充
data.fillna(method='ffill')
'''
a 1.0
b 1.0
c 2.0
d 2.0
e 3.0
dtype: float64
'''
或者我們可以指定反向填充买优,來向后傳播下一個值:
# 向后填充
data.fillna(method='bfill')
'''
a 1.0
b 2.0
c 2.0
d 3.0
e 3.0
dtype: float64
'''
對于DataFrame
,選項也類似,但我們也可以指定axis
杀赢,沿著該軸進(jìn)行填充:
df
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 1.0 | NaN | 2 | NaN |
1 | 2.0 | 3.0 | 5 | NaN |
2 | NaN | 4.0 | 6 | NaN |
df.fillna(method='ffill', axis=1)
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 1.0 | 1.0 | 2.0 | 2.0 |
1 | 2.0 | 3.0 | 5.0 | 5.0 |
2 | NaN | 4.0 | 6.0 | 6.0 |
請注意烘跺,如果在前向填充期間前一個值不可用,則 NA 值仍然存在脂崔。