python原生的None和pandas, numpy中的numpy.NaN儘管在功能上都是用來標示空缺資料。但它們的行為在很多場景下確有一些相當大的差異。由於不熟悉這些差異,曾經給我的工作帶來過不少麻煩。 特此整理了一份詳細的實驗,比較None和NaN在不同場景下的差異。
實驗的結果有些在意料之內,有些則讓我大跌眼鏡。希望讀者看過此文後會None和NaN這對“小妖精”有更深的理解。
為了理解本文的內容,希望本文的讀者需要對pandas的Series使用有一定的經驗。
首先,匯入所需的庫
In[2]:
1 2 3 |
from numpy import NaN from pandas import Series, DataFrame import numpy as np |
資料型別?
None是一個python特殊的資料型別, 但是NaN卻是用一個特殊的float
In[3]:
1 |
type(None) |
Out[3]:
1 |
NoneType |
In[4]:
1 |
type(NaN) |
Out[4]:
1 |
float |
能作為dict的key?
In[5]:
1 |
{None:1} |
Out[5]:
1 |
{None: 1} |
In[6]:
1 |
{NaN:1} |
Out[6]:
1 |
{nan: 1} |
In[7]:
1 |
{None:1, NaN:2} |
Out[7]:
1 |
{nan: 2, None: 1} |
都可以,而且會被認為是不同的key
Series函式中的表現
Series.map
In[8]:
1 2 |
s = Series([None, NaN, 'a']) s |
Out[8]:
1 2 3 4 |
0 None 1 NaN 2 a dtype: object |
In[9]:
1 |
s.map({None:1,'a':'a'}) |
Out[9]:
1 2 3 4 |
0 1 1 1 2 a dtype: object |
可以看到None和NaN都會替換成了1
In[10]:
1 |
s.map({NaN:1,'a':'a'}) |
Out[10]:
1 2 3 4 |
0 1 1 1 2 a dtype: object |
同樣None和NaN都會替換成了1
In[11]:
1 |
s.map({NaN:2,'None':1,'a':'a'}) |
Out[11]:
1 2 3 4 |
0 2 1 2 2 a dtype: object |
將None替換成1的要求被忽略了
In[12]:
1 |
s.map({'None':1,NaN:2,'a':'a'}) |
Out[12]:
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]:
1 2 |
s = Series([None, NaN, 'a']) s |
Out[13]:
1 2 3 4 |
0 None 1 NaN 2 a dtype: object |
In[14]:
1 |
s.replace([NaN],9) |
Out[14]:
1 2 3 4 |
0 9 1 9 2 a dtype: object |
In[15]:
1 |
s.replace([None],9) |
Out[15]:
1 2 3 4 |
0 9 1 9 2 a dtype: object |
和Series.map的情況類似,指定了None的替換值後,NaN會被替換掉;反之亦然。
對函式的支援
numpy有不少函式可以自動處理NaN。
In[16]:
1 |
np.nansum([1,2,NaN]) |
Out[16]:
1 |
3.0 |
但是None不能享受這些函式的便利,如果資料包含的None的話會報錯
In[17]:
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]:
1 2 |
import pandas as pd pd.cut(Series([NaN]),[1,2]) |
Out[18]:
1 2 3 |
0 NaN dtype: category Categories (1, object): [(1, 2]] |
In[19]:
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]:
1 |
np.array([1, None]).dtype |
Out[20]:
1 |
dtype('O') |
而np.NaN儘管會將原本用int型別就能儲存的資料轉型成float,但不會帶來上面這個問題。
In[21]:
1 |
np.array([1, NaN]).dtype |
Out[21]:
1 |
dtype('float64') |
混入Series的影響
下面的結果估計大家能猜到
In[22]:
1 |
Series([1, NaN]) |
Out[22]:
1 2 3 |
0 1.0 1 NaN dtype: float64 |
下面的這個就很意外的吧
In[23]:
1 |
Series([1, None]) |
Out[23]:
1 2 3 |
0 1.0 1 NaN dtype: float64 |
pandas將None自動替換成了NaN!
In[24]:
1 |
Series([1.0, None]) |
Out[24]:
1 2 3 |
0 1.0 1 NaN dtype: float64 |
卻是Object型別的None被替換成了float型別的NaN。 這麼設計可能是因為None無法參與numpy的大多數計算, 而pandas的底層又依賴於numpy,因此做了這樣的自動轉化。
不過如果本來Series就只能用object型別容納的話, Series不會做這樣的轉化工作。
In[25]:
1 |
Series(['a', None]) |
Out[25]:
1 2 3 |
0 a 1 None dtype: object |
如果Series裡面都是None的話也不會做這樣的轉化
In[26]:
1 |
Series([None,None]) |
Out[26]:
1 2 3 |
0 None 1 None dtype: object |
其它的資料型別是bool時,也不會做這樣的轉化。
In[27]:
1 |
Series([True, False, None]) |
Out[27]:
1 2 3 4 |
0 True 1 False 2 None dtype: object |
等值性判斷
單值的等值性比較
下面的實驗中None和NaN的表現會作為後面的等值性判斷的基準(後文稱為基準)
In[28]:
1 |
None == None |
Out[28]:
1 |
True |
In[29]:
1 |
NaN == NaN |
Out[29]:
1 |
False |
In[30]:
1 |
None == NaN |
Out[30]:
1 |
False |
在tuple中的情況
這個不奇怪
In[31]:
1 |
(1, None) == (1, None) |
Out[31]:
1 |
True |
這個也不意外
In[32]:
1 |
(1, None) == (1, NaN) |
Out[32]:
1 |
False |
但是下面這個實驗NaN的表現和基準不一致
In[33]:
1 |
(1, NaN) == (1, NaN) |
Out[33]:
1 |
True |
在numpy.array中的情況
In[34]:
1 |
np.array([1,None]) == np.array([1,None]) |
Out[34]:
1 |
array([ True, True], dtype=bool) |
In[35]:
1 |
np.array([1,NaN]) == np.array([1,NaN]) |
Out[35]:
1 |
array([ True, False], dtype=bool) |
In[36]:
1 |
np.array([1,NaN]) == np.array([1,None]) |
Out[36]:
1 |
array([ True, False], dtype=bool) |
和基準的表現一致。
但是大部分情況我們希望上面例子中, 我們希望左右兩邊的array被判定成一致。這時可以用numpy.testing.assert_equal函式來處理。 注意這個函式的表現同assert, 不會返回True, False, 而是無反應或者raise Exception
In[37]:
1 |
np.testing.assert_equal(np.array([1,NaN]), np.array([1,NaN])) |
它也可以處理兩邊都是None的情況
In[38]:
1 |
np.testing.assert_equal(np.array([1,None]), np.array([1,None])) |
但是一邊是None,一邊是NaN時會被認為兩邊不一致, 導致AssertionError
In[39]:
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) |
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]:
1 |
Series([NaN,'a']) == Series([NaN,'a']) |
Out[40]:
1 2 3 |
0 False 1 True dtype: bool |
In[41]:
1 |
Series([None,'a']) == Series([NaN,'a']) |
Out[41]:
1 2 3 |
0 False 1 True dtype: bool |
但是None和基準的表現不一致。
In[42]:
1 |
Series([None,'a']) == Series([None,'a']) |
Out[42]:
1 2 3 |
0 False 1 True dtype: bool |
和array類似,Series也有專門的函式equals用於判斷兩邊的Series是否整體看相等
In[43]:
1 |
Series([None,'a']).equals(Series([NaN,'a'])) |
Out[43]:
1 |
True |
In[44]:
1 |
Series([None,'a']).equals(Series([None,'a'])) |
Out[44]:
1 |
True |
In[45]:
1 |
Series([NaN,'a']).equals(Series([NaN,'a'])) |
Out[45]:
1 |
True |
比numpy.testing.assert_equals更智慧些, 三種情況下都能恰當的處理
在DataFrame merge中的表現
兩邊的None會被判為相同
In[46]:
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]:
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]:
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]:
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]:
1 2 |
d = DataFrame({'A':[1,1,1,1,2],'B':[None,None,'a','a','b']}) d.groupby(['A','B']).apply(len) |
Out[50]:
1 2 3 4 |
A B 1 a 2 2 b 1 dtype: int64 |
可以看到(1, NaN)對應的組直接被忽略了
In[51]:
1 2 |
d = DataFrame({'A':[1,1,1,1,2],'B':[None,None,'a','a','b']}) d.groupby(['A','B']).apply(len) |
Out[51]:
1 2 3 4 |
A B 1 a 2 2 b 1 dtype: int64 |
(1,None)的組也被直接忽略了
In[52]:
1 2 |
d = DataFrame({'A':[1,1,1,1,2],'B':[None,NaN,'a','a','b']}) d.groupby(['A','B']).apply(len) |
Out[52]:
1 2 3 4 |
A B 1 a 2 2 b 1 dtype: int64 |
那麼上面這個結果應該沒啥意外的
總結
DataFrame.groupby會忽略分組列中含有None或者NaN的記錄
支援寫入資料庫?
往資料庫中寫入時NaN不可處理,需轉換成None,否則會報錯。這個這裡就不演示了。
相信作為pandas老司機, 至少能想出兩種替換方法。
In[53]:
1 2 |
s = Series([None,NaN,'a']) s |
Out[53]:
1 2 3 4 |
0 None 1 NaN 2 a dtype: object |
方案1
In[54]:
1 |
s.replace([NaN],None) |
Out[54]:
1 2 3 4 |
0 None 1 None 2 a dtype: object |
方案2
In[55]:
1 2 |
s[s.isnull()]=None s |
Out[55]:
1 2 3 4 |
0 None 1 None 2 a dtype: object |
然而這麼就覺得完事大吉的話就圖樣圖森破了, 看下面的例子
In[56]:
1 2 |
s = Series([NaN,1]) s |
Out[56]:
1 2 3 |
0 NaN 1 1.0 dtype: float64 |
In[57]:
1 |
s.replace([NaN], None) |
Out[57]:
1 2 3 |
0 NaN 1 1.0 dtype: float64 |
In[58]:
1 2 |
s[s.isnull()] = None s |
Out[58]:
1 2 3 |
0 NaN 1 1.0 dtype: float64 |
當其他資料是int或float時,Series又一聲不吭的自動把None替換成了NaN。
這時候可以使用第三種方法處理
In[59]:
1 |
s.where(s.notnull(), None) |
Out[59]:
1 2 3 |
0 None 1 1 dtype: object |
where語句會遍歷s中所有的元素,逐一檢查條件表示式, 如果成立, 從原來的s取元素; 否則用None填充。 這回沒有自動替換成NaN
None vs NaN要點總結
- 在pandas中, 如果其他的資料都是數值型別, pandas會把None自動替換成NaN, 甚至能將
s[s.isnull()]= None
,和s.replace(NaN, None)
操作的效果無效化。 這時需要用where函式才能進行替換。 - None能夠直接被匯入資料庫作為空值處理, 包含NaN的資料匯入時會報錯。
- numpy和pandas的很多函式能處理NaN,但是如果遇到None就會報錯。
- 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