pandas numpy處理缺失值,none與nan比較


原文鏈接:https://junjiecai.github.io/posts/2016/Oct/20/none_vs_nan/

建議從這里下載這篇文章對應的.ipynb文件和相關資源。這樣你就能在Jupyter中邊閱讀,邊測試文中的代碼。

python原生的None和pandas, numpy中的numpy.NaN盡管在功能上都是用來標示空缺數據。但它們的行為在很多場景下確有一些相當大的差異。由於不熟悉這些差異,曾經給我的工作帶來過不少麻煩。 特此整理了一份詳細的實驗,比較None和NaN在不同場景下的差異。

實驗的結果有些在意料之內,有些則讓我大跌眼鏡。希望讀者看過此文后會None和NaN這對“小妖精”有更深的理解。

為了理解本文的內容,希望本文的讀者需要對pandas的Series使用有一定的經驗。

首先,導入所需的庫

 
from numpy import NaN from pandas import Series, DataFrame import numpy as np 

數據類型?

None是一個python特殊的數據類型, 但是NaN卻是用一個特殊的float

 
type(None) 
 
NoneType
 
type(NaN) 
 
float

能作為dict的key?

 
{None:1} 
 
{None: 1}
 
{NaN:1} 
 
{nan: 1}
 
{None:1, NaN:2} 
 
{nan: 2, None: 1}

都可以,而且會被認為是不同的key

Series函數中的表現

Series.map

 
s = Series([None, NaN, 'a']) s 
 
0    None
1     NaN
2       a
dtype: object
 
s.map({None:1,'a':'a'}) 
 
0    1
1    1
2    a
dtype: object

可以看到None和NaN都會替換成了1

 
s.map({NaN:1,'a':'a'}) 
 
0    1
1    1
2    a
dtype: object

同樣None和NaN都會替換成了1

 
s.map({NaN:2,'None':1,'a':'a'}) 
 
0    2
1    2
2    a
dtype: object

將None替換成1的要求被忽略了

 
s.map({'None':1,NaN:2,'a':'a'}) 
 
0    2
1    2
2    a
dtype: object

將NaN替換成1的要求被忽略了

總結: 用Series.map對None進行替換時,會“順便”把NaN也一起替換掉;NaN也會順便把None替換掉。

如果None和NaN分別定義了不同的映射數值,那么只有一個會生效。

Series.replace中的表現

 
s = Series([None, NaN, 'a']) s 
 
0    None
1     NaN
2       a
dtype: object
 
s.replace([NaN],9) 
 
0    9
1    9
2    a
dtype: object
 
s.replace([None],9) 
 
0    9
1    9
2    a
dtype: object

和Series.map的情況類似,指定了None的替換值后,NaN會被替換掉;反之亦然。

對函數的支持

numpy有不少函數可以自動處理NaN。

 
np.nansum([1,2,NaN]) 
 
3.0

但是None不能享受這些函數的便利,如果數據包含的None的話會報錯

 
try: np.nansum([1,2,None]) except Exception as e: print(type(e),e) 
 
 unsupported operand type(s) for +: 'int' and 'NoneType'

pandas中也有不少函數支持NaN卻不支持None。(畢竟pandas的底層是numpy)

 
import pandas as pd pd.cut(Series([NaN]),[1,2]) 
 
0    NaN
dtype: category
Categories (1, object): [(1, 2]]
 
import pandas as pd try: pd.cut(Series([None]),[1,2]) except Exception as e: print(type(e),e) 
 
 unorderable types: int() > NoneType()

對容器數據類型的影響

混入numpy.array的影響

如果數據中含有None,會導致整個array的類型變成object。

 
np.array([1, None]).dtype 
 
dtype('O')

而np.NaN盡管會將原本用int類型就能保存的數據轉型成float,但不會帶來上面這個問題。

 
np.array([1, NaN]).dtype 
 
dtype('float64')

混入Series的影響

下面的結果估計大家能猜到

 
Series([1, NaN]) 
 
0    1.0
1    NaN
dtype: float64

下面的這個就很意外的吧

 
Series([1, None]) 
 
0    1.0
1    NaN
dtype: float64

pandas將None自動替換成了NaN!

 
Series([1.0, None]) 
 
0    1.0
1    NaN
dtype: float64

卻是Object類型的None被替換成了float類型的NaN。 這么設計可能是因為None無法參與numpy的大多數計算, 而pandas的底層又依賴於numpy,因此做了這樣的自動轉化。

不過如果本來Series就只能用object類型容納的話, Series不會做這樣的轉化工作。

 
Series(['a', None]) 
 
0       a
1    None
dtype: object

如果Series里面都是None的話也不會做這樣的轉化

 
Series([None,None]) 
 
0    None
1    None
dtype: object

其它的數據類型是bool時,也不會做這樣的轉化。

 
Series([True, False, None]) 
 
0     True
1    False
2     None
dtype: object

等值性判斷

單值的等值性比較

下面的實驗中None和NaN的表現會作為后面的等值性判斷的基准(后文稱為基准)

 
None == None 
 
True
 
NaN == NaN 
 
False
 
None == NaN 
 
False

在tuple中的情況

這個不奇怪

 
(1, None) == (1, None) 
 
True

這個也不意外

 
(1, None) == (1, NaN) 
 
False

但是下面這個實驗NaN的表現和基准不一致

 
(1, NaN) == (1, NaN) 
 
True

在numpy.array中的情況

 
np.array([1,None]) == np.array([1,None]) 
 
array([ True,  True], dtype=bool)
 
np.array([1,NaN]) == np.array([1,NaN]) 
 
array([ True, False], dtype=bool)
 
np.array([1,NaN]) == np.array([1,None]) 
 
array([ True, False], dtype=bool)

和基准的表現一致。 

但是大部分情況我們希望上面例子中, 我們希望左右兩邊的array被判定成一致。這時可以用numpy.testing.assert_equal函數來處理。 注意這個函數的表現同assert, 不會返回True, False, 而是無反應或者raise Exception

 
np.testing.assert_equal(np.array([1,NaN]), np.array([1,NaN])) 

它也可以處理兩邊都是None的情況

 
np.testing.assert_equal(np.array([1,None]), np.array([1,None])) 

但是一邊是None,一邊是NaN時會被認為兩邊不一致, 導致AssertionError

 
try: np.testing.assert_equal(np.array([1,NaN]), np.array([1,None])) except Exception as e: print(type(e),e) 
 
 
Arrays are not equal

(mismatch 50.0%)
 x: array([  1.,  nan])
 y: array([1, None], dtype=object)

在Series中的情況

下面兩個實驗中的表現和基准一致

 
Series([NaN,'a']) == Series([NaN,'a']) 
 
0    False
1     True
dtype: bool
 
Series([None,'a']) == Series([NaN,'a']) 
 
0    False
1     True
dtype: bool

但是None和基准的表現不一致。

 
Series([None,'a']) == Series([None,'a']) 
 
0    False
1     True
dtype: bool

和array類似,Series也有專門的函數equals用於判斷兩邊的Series是否整體看相等

 
Series([None,'a']).equals(Series([NaN,'a'])) 
 
True
 
Series([None,'a']).equals(Series([None,'a'])) 
 
True
 
Series([NaN,'a']).equals(Series([NaN,'a'])) 
 
True

比numpy.testing.assert_equals更智能些, 三種情況下都能恰當的處理

在DataFrame merge中的表現

兩邊的None會被判為相同

 
a = DataFrame({'A':[None,'a']}) b = DataFrame({'A':[None,'a']}) a.merge(b,on='A', how = 'outer') 
 
  A
0 None
1 a

兩邊的NaN會被判為相同

 
a = DataFrame({'A':[NaN,'a']}) b = DataFrame({'A':[NaN,'a']}) a.merge(b,on='A', how = 'outer') 
 
  A
0 NaN
1 a

無論兩邊都是None,都是NaN,還是都有,相關的列都會被正確的匹配。 注意一邊是None,一邊是NaN的時候。會以左側的結果為准。

 
a = DataFrame({'A':[None,'a']}) b = DataFrame({'A':[NaN,'a']}) a.merge(b,on='A', how = 'outer') 
 
  A
0 None
1 a
 
a = DataFrame({'A':[NaN,'a']}) b = DataFrame({'A':[None,'a']}) a.merge(b,on='A', how = 'outer') 
 
  A
0 NaN
1 a

注意

這和空值在postgresql等sql數據庫中的表現不一樣, 在數據庫中, join時兩邊的空值會被判定為不同的數值

在groupby中的表現

 
d = DataFrame({'A':[1,1,1,1,2],'B':[None,None,'a','a','b']}) d.groupby(['A','B']).apply(len) 
 
A  B
1  a    2
2  b    1
dtype: int64

可以看到(1, NaN)對應的組直接被忽略了

 
d = DataFrame({'A':[1,1,1,1,2],'B':[None,None,'a','a','b']}) d.groupby(['A','B']).apply(len) 
 
A  B
1  a    2
2  b    1
dtype: int64

(1,None)的組也被直接忽略了

 
d = DataFrame({'A':[1,1,1,1,2],'B':[None,NaN,'a','a','b']}) d.groupby(['A','B']).apply(len) 
 
A  B
1  a    2
2  b    1
dtype: int64

那么上面這個結果應該沒啥意外的

總結

DataFrame.groupby會忽略分組列中含有None或者NaN的記錄

支持寫入數據庫?

往數據庫中寫入時NaN不可處理,需轉換成None,否則會報錯。這個這里就不演示了。

相信作為pandas老司機, 至少能想出兩種替換方法。

 
s = Series([None,NaN,'a']) s 
 
0    None
1     NaN
2       a
dtype: object

方案1

 
s.replace([NaN],None) 
 
0    None
1    None
2       a
dtype: object

方案2

 
s[s.isnull()]=None s 
 
0    None
1    None
2       a
dtype: object

然而這么就覺得完事大吉的話就圖樣圖森破了, 看下面的例子

 
s = Series([NaN,1]) s 
 
0    NaN
1    1.0
dtype: float64
 
s.replace([NaN], None) 
 
0    NaN
1    1.0
dtype: float64
 
s[s.isnull()] = None s 
 
0    NaN
1    1.0
dtype: float64

當其他數據是int或float時,Series又一聲不吭的自動把None替換成了NaN。

這時候可以使用第三種方法處理

 
s.where(s.notnull(), None) 
 
0    None
1       1
dtype: object

where語句會遍歷s中所有的元素,逐一檢查條件表達式, 如果成立, 從原來的s取元素; 否則用None填充。 這回沒有自動替換成NaN

None vs NaN要點總結

  1. 在pandas中, 如果其他的數據都是數值類型, pandas會把None自動替換成NaN, 甚至能將s[s.isnull()]= None,和s.replace(NaN, None)操作的效果無效化。 這時需要用where函數才能進行替換。

  2. None能夠直接被導入數據庫作為空值處理, 包含NaN的數據導入時會報錯。

  3. numpy和pandas的很多函數能處理NaN,但是如果遇到None就會報錯。

  4. None和NaN都不能被pandas的groupby函數處理,包含None或者NaN的組都會被忽略。

等值性比較的總結:(True表示被判定為相等)

  None對None NaN對NaN None對NaN
單值 True False False
tuple(整體) True True False
np.array(逐個) True False False
Series(逐個) False False False
assert_equals True True False
Series.equals True True True
merge True True True

由於等值性比較方面,None和NaN在各場景下表現不太一致,相對來說None表現的更穩定。

為了不給自己惹不必要的麻煩和額外的記憶負擔。 實踐中,建議遵循以下三個原則即可

  • 在用pandas和numpy處理數據階段將None,NaN統一處理成NaN,以便支持更多的函數。
  • 如果要判斷Series,numpy.array整體的等值性,用專門的Series.equals,numpy.array函數去處理,不要自己用==判斷 * 如果要將數據導入數據庫,將NaN替換成None


免責聲明!

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



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