二、缺失值的填充和插值

1. 利用fillna进行填充

fillna 中有三个参数是常用的: value, method, limit 。其中, value 为填充值,可以是标量,也可以是索引到元素的字典映射; method 为填充方法,有用前面的元素填充 ffill 和用后面的元素填充 bfill 两种类型, limit 参数表示连续缺失值的最大填充次数。

下面构造一个简单的 Series 来说明用法:

In [19]: s = pd.Series([np.nan, 1, np.nan, np.nan, 2, np.nan],
   ....:                list('aaabcd'))
   ....: 
In [20]: s
Out[20]: 
a    NaN
a    1.0
a    NaN
b    NaN
c    2.0
d    NaN
dtype: float64
In [21]: s.fillna(method='ffill') # 用前面的值向后填充
Out[21]: 
a    NaN
a    1.0
a    1.0
b    1.0
c    2.0
d    2.0
dtype: float64
In [22]: s.fillna(method='ffill', limit=1) # 连续出现的缺失,最多填充一次
Out[22]: 
a    NaN
a    1.0
a    1.0
b    NaN
c    2.0
d    2.0
dtype: float64
In [23]: s.fillna(s.mean()) # value为标量
Out[23]: 
a    1.5
a    1.0
a    1.5
b    1.5
c    2.0
d    1.5
dtype: float64
In [24]: s.fillna({'a': 100, 'd': 200}) # 通过索引映射填充的值
Out[24]: 
a    100.0
a      1.0
a    100.0
b      NaN
c      2.0
d    200.0
dtype: float64

有时为了更加合理地填充,需要先进行分组后再操作。例如,根据年级进行身高的均值填充:

In [25]: df.groupby('Grade')['Height'].transform(
   ....:                      lambda x: x.fillna(x.mean())).head()
   ....: 
Out[25]: 
0    158.900000
1    166.500000
2    188.900000
3    163.075862
4    174.000000
Name: Height, dtype: float64

练一练

对一个序列以如下规则填充缺失值:如果单独出现的缺失值,就用前后均值填充,如果连续出现的缺失值就不填充,即序列[1, NaN, 3, NaN, NaN]填充后为[1, 2, 3, NaN, NaN],请利用 fillna 函数实现。(提示:利用 limit 参数)

2. 插值函数

在关于 interpolate 函数的 文档 描述中,列举了许多插值法,包括了大量 Scipy 中的方法。由于很多插值方法涉及到比较复杂的数学知识,因此这里只讨论比较常用且简单的三类情况,即线性插值、最近邻插值和索引插值。

对于 interpolate 而言,除了插值方法(默认为 linear 线性插值)之外,有与 fillna 类似的两个常用参数,一个是控制方向的 limit_direction ,另一个是控制最大连续缺失值插值个数的 limit 。其中,限制插值的方向默认为 forward ,这与 fillnamethod 中的 ffill 是类似的,若想要后向限制插值或者双向限制插值可以指定为 backwardboth

In [26]: s = pd.Series([np.nan, np.nan, 1,
   ....:                np.nan, np.nan, np.nan,
   ....:                2, np.nan, np.nan])
   ....: 
In [27]: s.values
Out[27]: array([nan, nan,  1., nan, nan, nan,  2., nan, nan])

例如,在默认线性插值法下分别进行 backward 和双向限制插值,同时限制最大连续条数为1:

In [28]: res = s.interpolate(limit_direction='backward', limit=1)
In [29]: res.values
Out[29]: array([ nan, 1.  , 1.  ,  nan,  nan, 1.75, 2.  ,  nan,  nan])
In [30]: res = s.interpolate(limit_direction='both', limit=1)
In [31]: res.values
Out[31]: array([ nan, 1.  , 1.  , 1.25,  nan, 1.75, 2.  , 2.  ,  nan])

第二种常见的插值是最近邻插补,即缺失值的元素和离它最近的非缺失值元素一样:

In [32]: s.interpolate('nearest').values
Out[32]: array([nan, nan,  1.,  1.,  1.,  2.,  2., nan, nan])

最后来介绍索引插值,即根据索引大小进行线性插值。例如,构造不等间距的索引进行演示:

In [33]: s = pd.Series([0,np.nan,10],index=[0,1,10])
In [34]: s
Out[34]: 
0      0.0
1      NaN
10    10.0
dtype: float64
In [35]: s.interpolate() # 默认的线性插值,等价于计算中点的值
Out[35]: 
0      0.0
1      5.0
10    10.0
dtype: float64
In [36]: s.interpolate(method='index') # 和索引有关的线性插值,计算相应索引大小对应的值
Out[36]: 
0      0.0
1      1.0
10    10.0
dtype: float64

同时,这种方法对于时间戳索引也是可以使用的,有关时间序列的其他话题会在第十章进行讨论,这里举一个简单的例子:

In [37]: s = pd.Series([0,np.nan,10],
   ....:               index=pd.to_datetime(['20200101',
   ....:                                     '20200102',
   ....:                                     '20200111']))
   ....: 
In [38]: s
Out[38]: 
2020-01-01     0.0
2020-01-02     NaN
2020-01-11    10.0
dtype: float64
In [39]: s.interpolate()
Out[39]: 
2020-01-01     0.0
2020-01-02     5.0
2020-01-11    10.0
dtype: float64
In [40]: s.interpolate(method='index')
Out[40]: 
2020-01-01     0.0
2020-01-02     1.0
2020-01-11    10.0
dtype: float64

关于polynomial和spline插值的注意事项

interpolate 中如果选用 polynomial 的插值方法,它内部调用的是 scipy.interpolate.interp1d(*,*,kind=order) ,这个函数内部调用的是 make_interp_spline 方法,因此其实是样条插值而不是类似于 numpy 中的 polyfit 多项式拟合插值;而当选用 spline 方法时, pandas 调用的是 scipy.interpolate.UnivariateSpline 而不是普通的样条插值。这一部分的文档描述比较混乱,而且这种参数的设计也是不合理的,当使用这两类插值方法时,用户一定要小心谨慎地根据自己的实际需求选取恰当的插值方法。