4.处理缺失数据

教学中的数据和实际数据的区别在于,实际数据很少是干净整齐的。许多有趣的数据集都有某种程度上的数据缺失。更糟糕的是,不同的数据源数据丢失的方式也不同。
本章,我们将会探讨对缺失数据的一般性考虑,讨论Pandas是怎样选择表达的,我们也会演示几种用来处理缺失数据的Pandas内置工具。在整本书中,我们一般将缺失数据称为NULL、NA或NA值。

对缺失数据约定的权衡

对于在表格和DataFrame中缺失数据的表示有好几种方案。通常情况下,它们都是基于两种策略:使用一个用于全局指示缺失值的掩码,或者选择一个表示缺失条目的哨兵值。
使用掩码方法,掩码可能是一个独立完整的布尔数组,或者在数据表达中引入一个适当的bit位来局部的指示空值状态。
使用哨兵方法,哨兵值可以是一些约定好的特殊数值,比如为表示一个缺失的整型数值,可以使用-9999或少见的bit模式,也可以是更全局性的约定,例如使用NaN来表示一缺失的浮点型数值,NaN是IEEE浮点规范中的一个特殊值。
所有的方法都需要一些权衡:使用独立的掩码数组需要分配额外的布尔数组空间,这将对储存和计算都增加负担。哨兵值减少了可以有效表达的数据范围,也可能在CPU和GPU的计算时需要另外的逻辑。像NaN这样普通的特殊值并不是对所有数据类型都适用。
在大多数情况下,并没有一个普遍的最优选择存在,不同的语言和系统使用不同的约定。例如R语言在每种数据类型里面使用保留的bit模式来表明缺失的数据,而SciDB系统对每个单位附加额外的字节来指示缺失状态。

Pandas上的数据缺失

Pandas处理缺失数据的方法受到它所依赖的NumPy包的约束,NumPy里面对非浮点型数据并没有内置的缺失值表达。
Pandas可以采用R语言的bit模式来标记各种数据类型的空值,但这种方法被证明是相当的不方便。R语言只包含四种基本数据类型,而NumPy支持的数据类型要多得多:例如,R有一个但整型类型,但考虑到可用精度,符号性和字节的编码顺序,NumPy支持基本整型类型多达14种。为所有可用的NumPy类型保留一个bit模式,将会导致大量的负担来处理不同类型的不同操作,很有可能需要NumPy包引出一个新的分支来支持。另外,对于小规模的数据类型(如8位的整数),牺牲其中一个bit作为掩码,将会显著的减少它所能表示的数据范围。
NumPy也不支持掩码数组,就是那种用来表示是好数据还是坏数据的独立布尔数据。Pandas可以继承这些,但是存储空间,计算和代码维护的额外代价使这种方法不使一个吸引人的选项。
考虑到这些限制,Pandas选择使用哨兵值来表示缺失数据,并且选用了两种Python里面已经存在的空值:特殊的浮点值NaN和Python的None对象。如我们将看到的,这种选择有一些副作用,但实际上它是在考虑到各种利益情况下的一个较好的折中。

None:Python式的缺失数据

Pandas使用的第一哨兵值是None,一个通常用在Python代码种表示缺失的单态对象。因为它是一个Python对象,None不能用在任何专属的NumPy/Pandas数组中,而只能用在对象类型位‘object’的数组中:

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数组中可以推断出最通用的数据类型是Python对象。尽管这种对象数组在某些情况下有用,但任何数据操作都是在Python层面的,这种操作比对原生类型的快速操作要慢很多。

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

在数组种使用Python 对象意味着如果要在带有None值的数组上执行聚合操作如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'

NaN:缺失数值数据

另一个缺失数据表示,NaN,就有些不同;它是一个能够被所有使用IEEE浮点表达式系统识别的特殊的浮点值。

vals2 = np.array([1, np.nan, 3, 4]) 
vals2.dtype
dtype('float64')

注意NumPy为这个数组选择一个原生的浮点类型:这意味着与之前的的对象数组不同,这个浮点数组支持在编译代码中的快速操作。你可能意识到NaN有点像数据病毒--它能感染任何它所接触的对象。无论是什么操作,与NaN计算的结果都是NaN:

1 + np.nan
nan
0 *  np.nan
nan

注意,这意味着对那些数据的聚合操作都是有效的(它们不会产生错误),只是没什么用处:

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是一个特殊的浮点值;对于整型,字符串和其他类型并没有对应的NaN值:

Pandas中的NaN和None

NaN和None在Pandas中都有使用,但对它们的处理被设计成几乎可以互换,在合适的情况下对它们进行转换:

pd.Series([1, np.nan, 2, None])
0    1.0
1    NaN
2    2.0
3    NaN
dtype: float64

对于那些没有哨兵值的类型,但空值出现时,Pandas会自动的进行类型转换。例如,我们给一个整型数组中的值设置为np.nan,整个数组将会自动的向上转型为浮点类型来适应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

注意除了把整型数组转换为浮点型,Pandas也自动的把None转换为NaN值
这种转换与特定语言(如R语言)的统一的方法比起来看上去不那么优雅,但在实际的使用过程中,Pandas的哨兵/转换方法工作的相当好,很少导致问题。
下表列出了Pandas中当出现NA值时的转型约定:

Typeclass Conversion When Storing NAs NA Sentinel Value
floating No change np.nan
object No change None or np.nan
integer Cast to float64 np.nan
boolean Cast to object None or np.nan

记住,在Pandas上,字符串数据总时被存储为对象类型。

空值操作

如我们看到的,Pandas把None和NaN在表示缺失或空值时当成时基本上可互换的。为了简化这种约定,有几个有用的方法用于检测,移除以及替换Pandas数据结构中的空值。它们是:

  • isnull(): 生成布尔掩码用来指示缺失的数据
  • notnull(): isnull()的反操作
  • dropna(): 返回数据过滤后的版本
  • fillna(): 返回缺失数据被填充后的数据拷贝

我们将对这些函数进行简要探索和示范,然后结束这一部分。

空值检测

Pandas数据结构有两个有用的方法用于检测空值:isnull()和notnull()。两者都在数据上返回布尔掩码。例如:

data = pd.Series([1, np.nan, 'hello', None])
data.isnull()
0    False
1     True
2    False
3     True
dtype: bool

我们在 Data Indexing and Selection 提到过,布尔掩码也可以直接被用作Series或FataFrame的索引:

data[data.notnull()]
0        1
2    hello
dtype: object

isnull()和notnull()方法对于DataFrame产生相似的布尔结果。

去掉空值

除了之前用过的掩码手段,还有很方便的函数,dropna(用于移除NA值)和fillna(填充NA值)。对于Series,结果非常直观:

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中的单个值;我们只能去掉整行或整列。在不同的应用环境,你可能想用不同的方式,所以dropna()为DataFrame提供了许多选项。
默认情况下,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

但是这种方法也去掉了一些好的数据;你可能想只去掉只含有NA值的行或列,或者大部分是NA值的行或列。可以通过指定参数how或者thresh来精确的控制允许的空值数目。
how的默认值是‘any’,因此任何包含空值的行或列都会被去掉。你可以指定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

为了细粒度的控制,thresh参数允许你规定可以保留行/列所需要的最少非空数据数目:

df.dropna(axis='rows', thresh=3)
0 1 2 3
1 2.0 3.0 5 NaN

第一行和最后一行被去掉了,因为它们只包含两个非空数据。

填充空值

有时候与其去掉NA值,我们宁愿把它们换成有效的值。这个值可能是像0那样的单个数字,或者有效值的插值。可以通过使用isnull()方法作为过滤条件来原地替换,但是因为这个操作很常用,Pandas提供了fillna()方法,它可以返回空值被替换后的数组拷贝。
考虑如下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

我们可以用单个数值如0来替换空值:

data.fillna(0)
a    1.0
b    0.0
c    2.0
d    0.0
e    3.0
dtype: float64

我们可以指定前值填充方法来使用空值前面的数据作为替换:

# forward-fill
data.fillna(method='ffill')
a    1.0
b    1.0
c    2.0
d    2.0
e    3.0
dtype: float64

或者我们可以指定后值填充方法使用空值后面的值作为替换值:

# back-fill
data.fillna(method='bfill')
a    1.0
b    2.0
c    2.0
d    3.0
e    3.0
dtype: float64

对于DataFrames,这些选项是类似的,但我们也可以指定发生填充的轴向:

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值将会保留。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容