1. 缺失记号及其缺陷
在 python
中的缺失值用 None
表示,该元素除了等于自己本身之外,与其他任何元素不相等:
In [41]: None == None
Out[41]: True
In [42]: None == False
Out[42]: False
In [43]: None == []
Out[43]: False
In [44]: None == ''
Out[44]: False
在 numpy
中利用 np.nan
来表示缺失值,该元素除了不和其他任何元素相等之外,和自身的比较结果也返回 False
:
In [45]: np.nan == np.nan
Out[45]: False
In [46]: np.nan == None
Out[46]: False
In [47]: np.nan == False
Out[47]: False
值得注意的是,虽然在对缺失序列或表格的元素进行比较操作的时候, np.nan
的对应位置会返回 False
,但是在使用 equals
函数进行两张表或两个序列的相同性检验时,会自动跳过两侧表都是缺失值的位置,直接返回 True
:
In [48]: s1 = pd.Series([1, np.nan])
In [49]: s2 = pd.Series([1, 2])
In [50]: s3 = pd.Series([1, np.nan])
In [51]: s1 == 1
Out[51]:
0 True
1 False
dtype: bool
In [52]: s1.equals(s2)
Out[52]: False
In [53]: s1.equals(s3)
Out[53]: True
在时间序列的对象中, pandas
利用 pd.NaT
来指代缺失值,它的作用和 np.nan
是一致的(时间序列的对象和构造将在第十章讨论):
In [54]: pd.to_timedelta(['30s', np.nan]) # Timedelta中的NaT
Out[54]: TimedeltaIndex(['0 days 00:00:30', NaT], dtype='timedelta64[ns]', freq=None)
In [55]: pd.to_datetime(['20200101', np.nan]) # Datetime中的NaT
Out[55]: DatetimeIndex(['2020-01-01', 'NaT'], dtype='datetime64[ns]', freq=None)
那么为什么要引入 pd.NaT
来表示时间对象中的缺失呢?仍然以 np.nan
的形式存放会有什么问题?在 pandas
中可以看到 object
类型的对象,而 object
是一种混杂对象类型,如果出现了多个类型的元素同时存储在 Series
中,它的类型就会变成 object
。例如,同时存放整数和字符串的列表:
In [56]: pd.Series([1, 'two'])
Out[56]:
0 1
1 two
dtype: object
NaT
问题的根源来自于 np.nan
的本身是一种浮点类型,而如果浮点和时间类型混合存储,如果不设计新的内置缺失类型来处理,就会变成含糊不清的 object
类型,这显然是不希望看到的。
In [57]: type(np.nan)
Out[57]: float
同时,由于 np.nan
的浮点性质,如果在一个整数的 Series
中出现缺失,那么其类型会转变为 float64
;而如果在一个布尔类型的序列中出现缺失,那么其类型就会转为 object
而不是 bool
:
In [58]: pd.Series([1, np.nan]).dtype
Out[58]: dtype('float64')
In [59]: pd.Series([True, False, np.nan]).dtype
Out[59]: dtype('O')
因此,在进入 1.0.0
版本后, pandas
尝试设计了一种新的缺失类型 pd.NA
以及三种 Nullable
序列类型来应对这些缺陷,它们分别是 Int, boolean
和 string
。
2. Nullable类型的性质
从字面意义上看 Nullable
就是可空的,言下之意就是序列类型不受缺失值的影响。例如,在上述三个 Nullable
类型中存储缺失值,都会转为 pandas
内置的 pd.NA
:
In [60]: pd.Series([np.nan, 1], dtype = 'Int64') # "i"是大写的
Out[60]:
0 <NA>
1 1
dtype: Int64
In [61]: pd.Series([np.nan, True], dtype = 'boolean')
Out[61]:
0 <NA>
1 True
dtype: boolean
In [62]: pd.Series([np.nan, 'my_str'], dtype = 'string')
Out[62]:
0 <NA>
1 my_str
dtype: string
在 Int
的序列中,返回的结果会尽可能地成为 Nullable
的类型:
In [63]: pd.Series([np.nan, 0], dtype = 'Int64') + 1
Out[63]:
0 <NA>
1 1
dtype: Int64
In [64]: pd.Series([np.nan, 0], dtype = 'Int64') == 0
Out[64]:
0 <NA>
1 True
dtype: boolean
In [65]: pd.Series([np.nan, 0], dtype = 'Int64') * 0.5 # 只能是浮点
Out[65]:
0 <NA>
1 0.0
dtype: Float64
对于 boolean
类型的序列而言,其和 bool
序列的行为主要有两点区别:
第一点是带有缺失的布尔列表无法进行索引器中的选择,而 boolean
会把缺失值看作 False
:
In [66]: s = pd.Series(['a', 'b'])
In [67]: s_bool = pd.Series([True, np.nan])
In [68]: s_boolean = pd.Series([True, np.nan]).astype('boolean')
# s[s_bool] # 报错
In [69]: s[s_boolean]
Out[69]:
0 a
dtype: object
第二点是在进行逻辑运算时, bool
类型在缺失处返回的永远是 False
,而 boolean
会根据逻辑运算是否能确定唯一结果来返回相应的值。那什么叫能否确定唯一结果呢?举个简单例子: True | pd.NA
中无论缺失值为什么值,必然返回 True
; False | pd.NA
中的结果会根据缺失值取值的不同而变化,此时返回 pd.NA
; False & pd.NA
中无论缺失值为什么值,必然返回 False
。
In [70]: s_boolean & True
Out[70]:
0 True
1 <NA>
dtype: boolean
In [71]: s_boolean | True
Out[71]:
0 True
1 True
dtype: boolean
In [72]: ~s_boolean # 取反操作同样是无法唯一地判断缺失结果
Out[72]:
0 False
1 <NA>
dtype: boolean
关于 string
类型的具体性质将在下一章文本数据中进行讨论。
一般在实际数据处理时,可以在数据集读入后,先通过 convert_dtypes
转为 Nullable
类型:
In [73]: df = pd.read_csv('data/learn_pandas.csv')
In [74]: df = df.convert_dtypes()
In [75]: df.dtypes
Out[75]:
School string
Grade string
Name string
Gender string
Height Float64
Weight Int64
Transfer string
Test_Number Int64
Test_Date string
Time_Record string
dtype: object
3. 缺失数据的计算和分组
当调用函数 sum, prod
使用加法和乘法的时候,缺失数据等价于被分别视作0和1,即不改变原来的计算结果:
In [76]: s = pd.Series([2,3,np.nan,4,5])
In [77]: s.sum()
Out[77]: 14.0
In [78]: s.prod()
Out[78]: 120.0
当使用累计函数时,会自动跳过缺失值所处的位置:
In [79]: s.cumsum()
Out[79]:
0 2.0
1 5.0
2 NaN
3 9.0
4 14.0
dtype: float64
当进行单个标量运算的时候,除了 np.nan ** 0
和 1 ** np.nan
这两种情况为确定的值之外,所有运算结果全为缺失( pd.NA
的行为与此一致 ),并且 np.nan
在比较操作时一定返回 False
,而 pd.NA
返回 pd.NA
:
In [80]: np.nan == 0
Out[80]: False
In [81]: pd.NA == 0
Out[81]: <NA>
In [82]: np.nan > 0
Out[82]: False
In [83]: pd.NA > 0
Out[83]: <NA>
In [84]: np.nan + 1
Out[84]: nan
In [85]: np.log(np.nan)
Out[85]: nan
In [86]: np.add(np.nan, 1)
Out[86]: nan
In [87]: np.nan ** 0
Out[87]: 1.0
In [88]: pd.NA ** 0
Out[88]: 1
In [89]: 1 ** np.nan
Out[89]: 1.0
In [90]: 1 ** pd.NA
Out[90]: 1
另外需要注意的是, diff, pct_change
这两个函数虽然功能相似,但是对于缺失的处理不同,前者凡是参与缺失计算的部分全部设为了缺失值,而后者缺失值位置会被设为 0% 的变化率:
In [91]: s.diff()
Out[91]:
0 NaN
1 1.0
2 NaN
3 NaN
4 1.0
dtype: float64
In [92]: s.pct_change()
Out[92]:
0 NaN
1 0.500000
2 0.000000
3 0.333333
4 0.250000
dtype: float64
对于一些函数而言,缺失可以作为一个类别处理,例如在 groupby, get_dummies
中可以设置相应的参数来进行增加缺失类别:
In [93]: df_nan = pd.DataFrame({'category':['a','a','b',np.nan,np.nan],
....: 'value':[1,3,5,7,9]})
....:
In [94]: df_nan
Out[94]:
category value
0 a 1
1 a 3
2 b 5
3 NaN 7
4 NaN 9
In [95]: df_nan.groupby('category',
....: dropna=False)['value'].mean() # pandas版本大于1.1.0
....:
Out[95]:
category
a 2
b 5
NaN 8
Name: value, dtype: int64
In [96]: pd.get_dummies(df_nan.category, dummy_na=True)
Out[96]:
a b NaN
0 1 0 0
1 1 0 0
2 0 1 0
3 0 0 1
4 0 0 1