Python数据分析-pandas04:索引与缺失值

0

上一节介绍了 pandas 中的索引,本节补充关于索引对齐的更多内容。

numpy 中对两个数组做运算,如果它们形状既不完全一致,也不满足广播规则,那么会产生错误:

a01 = np.arange(1, 5)
a02 = np.linspace(1, 5, 5)
try:
    a01 + a02
except ValueError as e:
    print(e)
operands could not be broadcast together with shapes (4,) (5,)

但是对两个类似的 Series 对象做运算时,就不会产生这种错误:

s01 = pd.Series(np.arange(1, 5))
s02 = pd.Series(np.linspace(1, 5, 5))
s01 + s02
0 2.0 1 4.0 2 6.0 3 8.0 4 NaN dtype: float64

虽然不会产生错误,但是注意到得到的 Series 包含 5 个元素,并且最后一个元素似乎不是数值数据。同时结果的类型由整数变为了浮点数。

这两个现象就包含了本节的内容。接下来逐一介绍。

索引对齐

当在两个 SeriesDataFrame 对象上做计算时,pandas 会按照索引值配对计算元素,而不是按位置配对:

np.divide(pd.Series(range(1, 5), index=list('ABCD')),
          pd.Series(range(1, 5), index=list('ACBD')))
A 1.000000 B 0.666667 C 1.500000 D 1.000000 dtype: float64

这实际上是由于 pandas 会在计算过程中对齐两个对象的索引。索引对齐确保计算可以得到合理的结果,并且当处理不完整的数据时也更方便。

例如,以下根据现有的地区面积和人口数据,计算人口密度:

territory = pd.Series(
    {'D0': 1708, 'D1': 9403, 'D2': 3640, 'D3': 3360},
    name='territory')
population = pd.Series(
    {'D0': 14300, 'D2': 13900, 'D3': 33280},
    name='population')
population / territory
D0 8.372365 D1 NaN D2 3.818681 D3 9.904762 dtype: float64

结果数组的索引是两个输入数组索引的并集,并用索引相同的元素做运算。这样一来不需要使两个 Series 都是完整且顺序一致的,也能根据索引完成配对元素的计算。

在计算两个 DataFrame 时,类似的索引对齐规则也同样会出现在列索引中:

(pd.DataFrame(np.arange(6).reshape(3, 2), columns=list('AB')) +
 pd.DataFrame(np.arange(9).reshape(3, 3), columns=list('BAC')))
ABC
011NaN
166NaN
21111NaN

因此,索引对齐就是在计算时根据配对的索引完成元素的运算。如果有一个运算对象缺少该索引,该位置的数据会用 NaN 填充。这是 pandas 表示缺失值的方法,接下来会介绍缺失值的处理方法。

处理缺失值

认识缺失值

在 Python 中,空值一般用 None 对象表示。它是一个特殊的 Python object 对象 ,由 Python 解释器提供并处理。

None 作为一个 Python 对象,并不能兼容任何 numpy 的原生类型。如果在创建数组时包含 None ,那么数组的类型会被强制提升为 object

a1 = np.array([0, 1, 3, None])
a1
array([0, 1, 3, None], dtype=object)

然而,使用 object 作为数组类型会严重拖慢计算速度,因为它在底层不但占用更多空间,并且无法通过向量化加速运算:

a2 = np.array(range(10000), dtype=np.int32)
a3 = np.array(range(10000), dtype=object)
%timeit a2 / 2
%timeit a3 / 2
9.99 µs ± 51.7 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each) 199 µs ± 1.35 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

可以看到即便对于简单的除法运算,两者都有 20 倍的速度差异。除此之外,数组中一旦包含 None 值,那么它就无法参与各种运算,因为 Python 并没有实现 None 和其它类型的运算方法。

不过好在 numpy 提供了另一种缺失值:NaN 。它全称 Not a Number ,即非数字,是一种按照 IEEE 754 标准设计的特殊浮点数:(有关 NaN 与浮点数的更多底层设计可以参见维基百科

np.array([1, np.nan, 2, 3])
array([ 1., nan, 2., 3.])

NaN 作为一种浮点数,在大多数编程语言中都可以被处理。浮点类型使得含 NaN 的数组可以使用向量化计算,获得很快的运算速度。

NaN 的一个特性是:它与任何数字的运算结果都是它本身。也就是说无论 NaN 参与何种运算,最终结果都是 NaN

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

这种特性使得在对含有 NaN 的数组做聚合处理时,虽然不会引起异常,但结果不一定有效:

a5 = np.array([1, np.nan, 2, 3])
a5.sum(), a5.max()
(nan, nan)

为此,numpy 提供了一些以 nan 开头的特殊累计函数,它们可以忽略数组中的缺失值:

np.nansum(a5), np.nanmax(a5)
(6.0, 3.0)

处理缺失值

NaN 虽然不像 None 的问题那么明显,但也容易出现奇怪的问题。接下来看看 pandas 中对 NaN 的处理方式。

考虑到空值 None 的副作用太过明显,pandas 会将空值自动转换为 NaN

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

SeriesDataFrame 均可以使用 .isnull().notnull() 方法来发现缺失值,它们像通用函数一样返回布尔数组,例如:

s03.isnull()
0 False 1 True 2 True 3 False dtype: bool

这种布尔数组可以配合数组索引直接修改缺失值:

s03[s03.isnull()] = 2
s03
0 0.0 1 2.0 2 2.0 3 1.0 dtype: float64

pandas 还提供了两种很好用的缺失值处理方式,分别是 .dropna().fillna() 方法,分别用于剔除缺失值和填充缺失值。在 Series 上使用这些方法比较易懂:

s03.dropna()
0 0.0 3 1.0 dtype: float64

更复杂的情况涉及对 DataFrame 的缺失值处理,因为 DataFrame 增删的最小单元是一行或一列。默认情况下,DataFrame.dropna() 会剔除任何包含缺失值的整行数据。可以设置按不同的坐标轴剔除缺失值,比如 axis=1(或 axis='columns' )会剔除任何包含缺失值的整列数据:

df01 = pd.DataFrame([[1, np.nan, 2], [2, 3, 5], [np.nan, 4, 6]])
df01.dropna()
012
12.03.05

这种做法会把非缺失值一并剔除。有时候可能只需要剔除缺失值较多的行或列,这种需求可以通过以下两个参数来满足:

  • how 参数的默认值是 'any' ,表示只要有缺失值就剔除整行或整列。还可以传入 'all' ,从而剔除全部是缺失值的行或列
  • thresh 参数用于设置需要保留的行或列中非缺失值的最小数量

剔除缺失值时只关注特定的列也是一种常见的需求,因为有时 DataFrame 只有部分列会参与运算,而其它列无论是否包含缺失值都想保留下了。这时可以通过向 subset 参数传入包含列名的列表来指定剔除缺失值时只关注表的哪些部分。

除此之外,inplace 当然也是 .dropna() 方法具有的参数,说明该方法默认情况下也不修改原有表,而是得到一个剔除缺失值后的副本。

.fillna() 方法用于填充缺失值。对于 Series 而言,该方法就是一个简单的替换:

s03.fillna(3)
0 0.0 1 3.0 2 3.0 3 1.0 dtype: float64

对于 DataFrame 而言,除了用单个值填充所有的缺失位置外,还可以使用字典为不同列指定不同的填充值:

df01.fillna({0: 10, 1: 20})
012
01.020.02
12.03.05
210.04.06

.fillna() 方法还有以下两个常用参数:

  • method :参数指定填充的方法,例如 "pad""ffill" 用缺失值前或上面的有效值填充;"bfill""backfill" 用缺失值后或下面的有效值填充。默认用自定义行为填充
  • limit :填充的最大数量

可空类型

最后要说明的是,尽管 pandas 为处理 NaN 提供了很多便利的工具,但是尽可能不要向表中引入 NaN 。因为一旦表中出现一个 NaN ,会使得一列的数据类型都变成浮点数:

df02 = pd.DataFrame(np.arange(12).reshape(3, 4), dtype='int32')
df02.iloc[2, 1] = np.nan
df02
0123
001.023
145.067
28NaN1011

浮点数可能使得表在处理时出现问题。例如,如果一个整型或布尔数组出现了一个浮点数,那么它便无法用于索引。

为此,pandas 提供了一类特别的可空类型,向可空类型中引入空值并不会使 Series 变为浮点数。目前 pandas 具有的可空类型包括各种长度的 IntFloat 类型(注意首字母大写)、string 类型和 boolean 类型。

如果在创建 Series 时使用 dtype 参数指定类型为以上这些可空类型,那么其中的缺失值便会使用特别的缺失值指示器 pd.NA 代替:

s04 = pd.Series([0, None, np.nan, pd.NA, 1],
                dtype='Int32')
s04
0 0 1 <NA> 2 <NA> 3 <NA> 4 1 dtype: Int32

pd.NA 不代表任何实际的值,因此可以用在任何数组中而不会改变其原有类型。例如,如果创建一个类型为 boolean 的可空布尔数组,那么它便可用于索引,并且其中的 pd.NA 会被当做 False 用于索引:

s05 = pd.Series([True, None, np.nan, False], dtype='boolean')
s01[s05]
0 1 dtype: int32

但截至 pandas 1.5 ,该功能似乎仍然处于实验阶段。

京ICP备2021034974号
contact me by hello@frozencandles.fun