缺失数据清洗和整理
在数据处理中难免会出现一些缺失值,在正式进行数据统计分析时就需要对他们进行处理。而对缺失值的处理有两种思路一种就是清洗也就是所谓的删除缺失值,一种就是填充就通过各种方法来填充数据(主要通过周围的数据来拟合)。
Pandas 中的缺失值
首先要知道如何在 Pandas 中表示缺失值。在 Pandas 中有四个表示缺失值的形式:
NaN(Not a Number)
: 这是引自 numpy 的一种表示方式,他最大的问题就是本身属于浮点数,这就导致了如果非浮点数由他来表示缺失值就导致类型转换(其中整数转换为浮点数,其他转换为 object)None
: Python 原生表示缺失值的对象,他通常作为标量的缺失值表示在 Pandas 中体现的并不多(注意他不与其他类型兼容,如果混用同样需要是 object)NaT(Not a Time)
: 同样是引入 numpy 的一种表示方式,他主要用于时间序列pd.NA
: 这是 Pandas 1.0+ 引入的旨在统一不同数据类型的缺失值表示,同时他引入了 nullable 数据引擎来表示nullable Integer
、nullable Bollean
和nullable String
由于历史遗留原因,默认情况构造的 DataFrame 依然沿用了 NaN 和 NaT 这个 numpy 的类型系统。尽管在 Pandas1.0 中引入了 pd.NA
他也是在 numpy 上引入了 nullable-numpy 类型系统。依然会混用他们。直到 pandas 2.0 引入了 pyarrow 类型系统才真正的实现统一使用 pd.NA
来表示缺失值:
In [7]: df = pd.DataFrame(
...: [{"a": 1, "b": True, "c": 1.2, "d": datetime(2000,1,1)},
...: {"a": None, "b": None, "c": None, "d": None}]
...: )
# a 整数被转换为浮点数
# b 布尔值,被转换为 object 因为 True 和 None 分属不同类型
# c 没问题, NaN 本质上就是一个浮点数
# d 时间类型,用 NaT 来表示
In [8]: df
Out[8]:
a b c d
0 1.0 True 1.2 2000-01-01
1 NaN None NaN NaT
In [9]: df['b']
Out[9]:
0 True
1 None
Name: b, dtype: object
# Pandas 1.0 引入了 numpy_nullable 类型系统,提供了 pd.NA 来表示缺失值
# 他提供了 Nullable-Integer Nullable-Boolean 但是时间类型依然不统一,这还是比较混乱
In [10]: df.convert_dtypes(dtype_backend='numpy_nullable')
Out[10]:
a b c d
0 1 True 1.2 2000-01-01
1 <NA> <NA> <NA> NaT
# Pandas 2.0 引入了 pyarrow 类型系统
# 整个 Pandas 的世界都是用 pd.NA 来表示缺失值
In [11]: df.convert_dtypes(dtype_backend='pyarrow')
Out[11]:
a b c d
0 1 True 1.2 2000-01-01 00:00:00
1 <NA> <NA> <NA> <NA>
Tips
更多的可以参考Pandas 数据类型
缺失值检测
使用 DataFrame|Series.isna()
和 DataFrame|Series.notna()
方法来检测缺失值,其中 None
、NaN
、NaT
以及 NA
都被认为是缺失值。
缺失值运算
一般来说缺失值会在涉及的运算中传播,及当一个操作数未知时涉及到的结果也通常是未知的。
例如涉及 NA 的算术运算中:
In [25]: pd.NA + 1
Out[25]: <NA>
# 因为 NA 兼容任何数据类型这样处理没有问题
In [26]: "a" * pd.NA
Out[26]: <NA>
# np.nan 实际上是 float 类型
In [27]: np.nan + 1
Out[27]: nan
# 报错类型不兼容
In [28]: "a" * np.nan
------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[28], line 1
----> 1 'a'*np.nan
TypeError: can't multiply sequence by non-int of type 'float'
# None 与任何数据类型都不兼容所以所有操作都会报错
Tips
这里可以看出引入 NA 的重要性。他让任何计算都不会因为缺失值的不兼容而报错
当然也有一些特殊情况,即使其中一个操作数是 NA 也不影响结果时也会返回结果:
In [29]: pd.NA ** 0
Out[29]: 1
In [30]: 1 ** pd.NA
Out[30]: 1
In [31]: True | pd.NA
Out[31]: True
In [32]: False & pd.NA
Out[32]: False
Tips
NA 的语义和 nan 是完全不同的,nan 表示不是一个数值也就决定了他和任何数值执行操作都是没有意义的。而 NA 的语义是他是一个兼容的数据类型,可能是该类型取值范围的任意数只是我们不知道他的具体值,这样 NA 执行的一些运算就存在意义了。比较典型的就是上面的逻辑运算
缺失值比较
这其中 None 比较特殊,他等于自身。其他缺失值都不等于自身还有 NA 的先等比较依然返回 NA:
In [14]: None == None # noqa: E711
Out[14]: True
In [15]: np.nan == np.nan
Out[15]: False
In [16]: pd.NaT == pd.NaT
Out[16]: False
In [17]: pd.NA == pd.NA
Out[17]: <NA>
聚合算法中缺失值参与运算
如果涉及缺失值的计算都是 NA,那一大堆的聚合运算就别玩了,所以不同的聚合算法中缺失值的处理方式不同:
sum()
: 求和算法中缺失值被认为是 0prod()
: 求积运算中缺失值被认为是 1- 累计运算中会默认忽略缺失值,也会提供参数来决定是否让缺失值参与
不同类型系统转换
由于历史原因目前默认依然混用 NaN
、NaT
和 None
来作为缺失值,他也是 numpy 中表示缺失值的方式。
Pandas 直接使用 numpy 的最大的问题就是 Integer 会转换为 Float,Boolean、String 会转换为 Object 来兼容数据类型因此 Pandas 1.0 中引入了 NA
并引入了 Int64Dtype
、StringDtype
和 BooleanDtype
数据类型他们也被称为numpy_nullable,我们可以使用 astype 来转换:
In [30]: df
Out[30]:
a b c d
0 1.0 True 1.2 2000-01-01
1 NaN None NaN NaT
# 或者使用 astype("boolean") ! 注意一定不能是 "bool"
In [32]: df['b'].astype(pd.BooleanDtype())
Out[32]:
0 True
1 <NA>
Name: b, dtype: boolean
# 也可以是 pd.Int32Dtype()
In [35]: df['a'].astype('Int32')
Out[35]:
0 1
1 <NA>
Name: a, dtype: Int32
numpy_nullable
同样也有问题,他只是引入了 NA 来为字符串、整数和布尔值引入了自己的缺失值类型,而浮点数依然是 NaN、时间类型依然是 NaT,这样的类型系统还是非常混乱。直到 Pandas 2.0 引入了 pyarrow 类型系统,他将所有的类型的缺失值都统一到了 NA 上:
# 也可以是 "float32[pyarrow]"
In [42]: df['c'].astype(pd.ArrowDtype(pa.float32()))
Out[42]:
0 1.2
1 <NA>
Name: c, dtype: float[pyarrow]
In [59]: df['d'].astype("timestamp[ns][pyarrow]")
Out[59]:
0 2000-01-01 00:00:00
1 <NA>
Name: d, dtype: timestamp[ns][pyarrow]
Tips
目前 Pandas 有三套类型系统,具体他们之间的转换关系查看Pandas 中类型系统对比
整体转换
一个个转换比较麻烦,Pandas 提供了 DataFrame|Series.convert_dtypes()
方法来统一转换:
def convert_dtypes(
infer_objects=True, # 是否自动转换为最佳数据类型
convert_string=True,
convert_integer=True,
convert_boolean=True,
convert_floating=True,
dtype_backend:{"numpy_nullable", "pyarrow"}='numpy_nullable'
):
"""
该函数最初就是将数据类型转换为 numpy_nullable 的,所以并没有 dtype_backend 参数,pyarrow 类型系统的引入在 Pandas 2.0 中引入了该参数来转换类系统到 pyarrow
"""
pass
建议在使用 DataFrame 时首先执行下该函数:
In [60]: df.convert_dtypes(dtype_backend='pyarrow')
Out[60]:
a b c d
0 1 True 1.2 2000-01-01 00:00:00
1 <NA> <NA> <NA> <NA>
In [61]: df.convert_dtypes(dtype_backend='pyarrow').dtypes
Out[61]:
a int64[pyarrow]
b bool[pyarrow]
c double[pyarrow]
d timestamp[ns][pyarrow]
dtype: object
Tips
所有的IO函数都支持 dtype_backend
来在读取时直接转换为对应的数据类型系统
清洗缺失值
对缺失值的清洗无非三种形式:
dropna()
: 删除缺失值fillna()
: 用其他值替换缺失值(需要注意类型兼容)interpolate()
: 用各种插值方法填充缺失值
dropna
dropna()用于删除缺失值:
def DataFrame.dropna(
axis:{0, 1}=1, # 删除缺失值方向 0 -> 'index' 1 -> 'columns'
how:{"any", "all"}="any", # "any": 如果存在即删除,如果所有都是就删除
thresh:int=None, # 要求多少为非 NA 值才删除(指定后 how 不再起作用)
subset:list[column|index]=None, # 和 axis 匹配,例如要删除行,这里是列标签列表,他和 how 决定了那些列为 NA 才删除,防止 axis=1 这里这是行标签列表
inplace:bool=False, # 是否就地修改
ignore_index:bool=False, # 如果为 True 则结果索引会 reindex(range)
):
pass
对于 Series.dropna()
来说很简单他只有 ignore_index
参数有意义,而对于 DataFrame 来说最重要的就是 axis how subset
之间的配合来决定要删除那些缺失值数据:
>>> df = pd.DataFrame({"name": ['Alfred', 'Batman', 'Catwoman'],
... "toy": [np.nan, 'Batmobile', 'Bullwhip'],
... "born": [pd.NaT, pd.Timestamp("1940-04-25"),
... pd.NaT]})
>>> df
name toy born
0 Alfred NaN NaT
1 Batman Batmobile 1940-04-25
2 Catwoman Bullwhip NaT
# 默认 axis = 0 删除行
# how = 'any' 即删除行中只要包含任意缺失值就会删除
# subset 没有设置表示全部
>>> df.dropna()
name toy born
1 Batman Batmobile 1940-04-25
# 指定了 subset 即在 name toy 列查找缺失值
>>> df.dropna(subset=['name', 'toy'])
name toy born
1 Batman Batmobile 1940-04-25
2 Catwoman Bullwhip NaT
fillna
fillna()比较简单唯一个要求就是填充缺失值是需要保证数据兼容。例如你不能为 datetime 类型填充空字符串,除非你先转换为字符串。