原文链接: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

最新文章

  1. Spring MVC学习笔记——返回JSON对象
  2. Linux 内核中的 Device Mapper 机制
  3. swagger:The World's Most Popular Framework for APIs.
  4. PHPstudy和ecshop的安装和使用
  5. javase基础笔记2——数据类型和面向对象
  6. 如何利用花生壳和VisualSVN Server建立远程代码仓库
  7. zw版【转发·台湾nvp系列Delphi例程】HALCON FillUpShape1
  8. 创建link server链接服务器碰到的问题及解决办法
  9. JS、C# 去除html标签
  10. 安装完oracle重新启动后报ORA-12514: TNS: 监听程序当前无法识别连接描述符中请求的服务(重启前正常)
  11. 【剑指offer】面试题40:数组中只出现一次的数字
  12. #include 和 #pragma comment 的相对路径起点
  13. 用fiddler2调试localhost
  14. SQLServer数据库维护(一)碎片检查整理
  15. java 11 增加了一系列的字符串处理方法,Optional 加强 ,改进的文件API
  16. 基于快速排序思想partition查找第K大的数或者第K小的数。
  17. JVM 堆参数调优 (四)
  18. .net Core 部署到 Linux
  19. Modelsim添加Lattice库
  20. VRRP概述

热门文章

  1. 织梦Dedecms容易被挂马文件以及可疑文件汇总
  2. c# 通过.net自带的chart控件绘制饼图pie chart
  3. MyBatis 原码解析(version:3.2.7)
  4. eclipse中设置文件的编码格式为utf-8
  5. Linux od命令(以指定进制显示文件)
  6. MyBatis入门程序之Mapper代理方式
  7. Java使用dom4j读取xml时报错:org.dom4j.DocumentException: Error on line 2 of document : Invalid byte 2 of 2-byte UTF-8 sequence. Nested exception: Invalid byte 2 of 2-byte UTF-8 sequence
  8. 两台Linux主机互传文件可以使用SCP命令来实现
  9. javascript/css压缩工具---yuicompressor使用方法
  10. 剑指offer面试题5:逆序打印单链表(Java)