[pythonskill]Python中NaN和None的詳細比較

陳楚桐發表於2018-08-06

原文出自:http://junjiecai.github.io/posts/2016/Oct/20/null_value_comparison/

感謝~

 

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

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

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

首先,匯入所需的庫

In[2]:

Python

 

1

2

3

from numpy import NaN

from pandas import Series, DataFrame

import numpy as np

 

資料型別?

None是一個python特殊的資料型別, 但是NaN卻是用一個特殊的float

In[3]:

Python

 

1

type(None)

Out[3]:

Python

 

1

NoneType

In[4]:

Python

 

1

type(NaN)

Out[4]:

Python

 

1

float

 

能作為dict的key?

In[5]:

Python

 

1

{None:1}

Out[5]:

Python

 

1

{None: 1}

In[6]:

Python

 

1

{NaN:1}

Out[6]:

Python

 

1

{nan: 1}

In[7]:

Python

 

1

{None:1, NaN:2}

Out[7]:

Python

 

1

{nan: 2, None: 1}

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

Series函式中的表現

Series.map

In[8]:

Python

 

1

2

s = Series([None, NaN, `a`])

s

Out[8]:

Python

 

1

2

3

4

0    None

1     NaN

2       a

dtype: object

In[9]:

Python

 

1

s.map({None:1,`a`:`a`})

Out[9]:

Python

 

1

2

3

4

0    1

1    1

2    a

dtype: object

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

In[10]:

Python

 

1

s.map({NaN:1,`a`:`a`})

Out[10]:

Python

 

1

2

3

4

0    1

1    1

2    a

dtype: object

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

In[11]:

Python

 

1

s.map({NaN:2,`None`:1,`a`:`a`})

Out[11]:

Python

 

1

2

3

4

0    2

1    2

2    a

dtype: object

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

In[12]:

Python

 

1

s.map({`None`:1,NaN:2,`a`:`a`})

Out[12]:

Python

 

1

2

3

4

0    2

1    2

2    a

dtype: object

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

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

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

Series.replace中的表現

In[13]:

Python

 

1

2

s = Series([None, NaN, `a`])

s

Out[13]:

Python

 

1

2

3

4

0    None

1     NaN

2       a

dtype: object

In[14]:

Python

 

1

s.replace([NaN],9)

Out[14]:

Python

 

1

2

3

4

0    9

1    9

2    a

dtype: object

In[15]:

Python

 

1

s.replace([None],9)

Out[15]:

Python

 

1

2

3

4

0    9

1    9

2    a

dtype: object

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

對函式的支援

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

In[16]:

Python

 

1

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

Out[16]:

Python

 

1

3.0

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

In[17]:

Python

 

1

2

3

4

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)

In[18]:

Python

 

1

2

import pandas as pd

pd.cut(Series([NaN]),[1,2])

Out[18]:

Python

 

1

2

3

0    NaN

dtype: category

Categories (1, object): [(1, 2]]

In[19]:

Python

 

1

2

3

4

5

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。

In[20]:

Python

 

1

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

Out[20]:

Python

 

1

dtype(`O`)

而np.NaN儘管會將原本用int型別就能儲存的資料轉型成float,但不會帶來上面這個問題。

In[21]:

Python

 

1

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

Out[21]:

Python

 

1

dtype(`float64`)

 

混入Series的影響

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

In[22]:

Python

 

1

Series([1, NaN])

Out[22]:

Python

 

1

2

3

0    1.0

1    NaN

dtype: float64

下面的這個就很意外的吧

In[23]:

Python

 

1

Series([1, None])

Out[23]:

Python

 

1

2

3

0    1.0

1    NaN

dtype: float64

pandas將None自動替換成了NaN!

In[24]:

Python

 

1

Series([1.0, None])

Out[24]:

Python

 

1

2

3

0    1.0

1    NaN

dtype: float64

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

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

In[25]:

Python

 

1

Series([`a`, None])

Out[25]:

Python

 

1

2

3

0       a

1    None

dtype: object

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

In[26]:

Python

 

1

Series([None,None])

Out[26]:

Python

 

1

2

3

0    None

1    None

dtype: object

其它的資料型別是bool時,也不會做這樣的轉化。

In[27]:

Python

 

1

Series([True, False, None])

Out[27]:

Python

 

1

2

3

4

0     True

1    False

2     None

dtype: object

 

等值性判斷

單值的等值性比較

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

In[28]:

Python

 

1

None == None

Out[28]:

Python

 

1

True

In[29]:

Python

 

1

NaN == NaN

Out[29]:

Python

 

1

False

In[30]:

Python

 

1

None == NaN

Out[30]:

Python

 

1

False

 

在tuple中的情況

這個不奇怪

In[31]:

Python

 

1

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

Out[31]:

Python

 

1

True

這個也不意外

In[32]:

Python

 

1

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

Out[32]:

Python

 

1

False

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

In[33]:

Python

 

1

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

Out[33]:

Python

 

1

True

 

在numpy.array中的情況

In[34]:

Python

 

1

np.array([1,None]) == np.array([1,None])

Out[34]:

Python

 

1

array([ True,  True], dtype=bool)

In[35]:

Python

 

1

np.array([1,NaN]) == np.array([1,NaN])

Out[35]:

Python

 

1

array([ True, False], dtype=bool)

In[36]:

Python

 

1

np.array([1,NaN]) == np.array([1,None])

Out[36]:

Python

 

1

array([ True, False], dtype=bool)

和基準的表現一致。

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

In[37]:

Python

 

1

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

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

In[38]:

Python

 

1

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

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

In[39]:

Python

 

1

2

3

4

try:

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

except Exception as e:

    print(type(e),e)

 

Python

 

1

2

3

4

5

6

<class `assertionerror`=””>

Arrays are not equal

 

(mismatch 50.0%)

x: array([  1.,  nan])

y: array([1, None], dtype=object)

 

在Series中的情況

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

In[40]:

Python

 

1

Series([NaN,`a`]) == Series([NaN,`a`])

Out[40]:

Python

 

1

2

3

0    False

1     True

dtype: bool

In[41]:

Python

 

1

Series([None,`a`]) == Series([NaN,`a`])

Out[41]:

Python

 

1

2

3

0    False

1     True

dtype: bool

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

In[42]:

Python

 

1

Series([None,`a`]) == Series([None,`a`])

Out[42]:

Python

 

1

2

3

0    False

1     True

dtype: bool

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

In[43]:

Python

 

1

Series([None,`a`]).equals(Series([NaN,`a`]))

Out[43]:

Python

 

1

True

In[44]:

Python

 

1

Series([None,`a`]).equals(Series([None,`a`]))

Out[44]:

Python

 

1

True

In[45]:

Python

 

1

Series([NaN,`a`]).equals(Series([NaN,`a`]))

Out[45]:

Python

 

1

True

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

在DataFrame merge中的表現

兩邊的None會被判為相同

In[46]:

Python

 

1

2

3

a = DataFrame({`A`:[None,`a`]})

b = DataFrame({`A`:[None,`a`]})

a.merge(b,on=`A`, how = `outer`)

Out[46]:

  A
0 None
1 a

兩邊的NaN會被判為相同

In[47]:

Python

 

1

2

3

a = DataFrame({`A`:[NaN,`a`]})

b = DataFrame({`A`:[NaN,`a`]})

a.merge(b,on=`A`, how = `outer`)

Out[47]:

  A
0 NaN
1 a

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

In[48]:

Python

 

1

2

3

a = DataFrame({`A`:[None,`a`]})

b = DataFrame({`A`:[NaN,`a`]})

a.merge(b,on=`A`, how = `outer`)

Out[48]:

  A
0 None
1 a

In[49]:

Python

 

1

2

3

a = DataFrame({`A`:[NaN,`a`]})

b = DataFrame({`A`:[None,`a`]})

a.merge(b,on=`A`, how = `outer`)

Out[49]:

  A
0 NaN
1 a

注意

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

在groupby中的表現

In[50]:

Python

 

1

2

d = DataFrame({`A`:[1,1,1,1,2],`B`:[None,None,`a`,`a`,`b`]})

d.groupby([`A`,`B`]).apply(len)

Out[50]:

Python

 

1

2

3

4

A  B

1  a    2

2  b    1

dtype: int64

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

In[51]:

Python

 

1

2

d = DataFrame({`A`:[1,1,1,1,2],`B`:[None,None,`a`,`a`,`b`]})

d.groupby([`A`,`B`]).apply(len)

Out[51]:

Python

 

1

2

3

4

A  B

1  a    2

2  b    1

dtype: int64

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

In[52]:

Python

 

1

2

d = DataFrame({`A`:[1,1,1,1,2],`B`:[None,NaN,`a`,`a`,`b`]})

d.groupby([`A`,`B`]).apply(len)

Out[52]:

Python

 

1

2

3

4

A  B

1  a    2

2  b    1

dtype: int64

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

總結

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

支援寫入資料庫?

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

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

In[53]:

Python

 

1

2

s = Series([None,NaN,`a`])

s

Out[53]:

Python

 

1

2

3

4

0    None

1     NaN

2       a

dtype: object

方案1

In[54]:

Python

 

1

s.replace([NaN],None)

Out[54]:

Python

 

1

2

3

4

0    None

1    None

2       a

dtype: object

方案2

In[55]:

Python

 

1

2

s[s.isnull()]=None

s

Out[55]:

Python

 

1

2

3

4

0    None

1    None

2       a

dtype: object

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

In[56]:

Python

 

1

2

s = Series([NaN,1])

s

Out[56]:

Python

 

1

2

3

0    NaN

1    1.0

dtype: float64

In[57]:

Python

 

1

s.replace([NaN], None)

Out[57]:

Python

 

1

2

3

0    NaN

1    1.0

dtype: float64

In[58]:

Python

 

1

2

s[s.isnull()] = None

s

Out[58]:

Python

 

1

2

3

0    NaN

1    1.0

dtype: float64

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

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

In[59]:

Python

 

1

s.where(s.notnull(), None)

Out[59]:

Python

 

1

2

3

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


相關文章