Skip to content

缺失数据清洗和整理

在数据处理中难免会出现一些缺失值,在正式进行数据统计分析时就需要对他们进行处理。而对缺失值的处理有两种思路一种就是清洗也就是所谓的删除缺失值,一种就是填充就通过各种方法来填充数据(主要通过周围的数据来拟合)。

Pandas 中的缺失值

首先要知道如何在 Pandas 中表示缺失值。在 Pandas 中有四个表示缺失值的形式:

  1. NaN(Not a Number): 这是引自 numpy 的一种表示方式,他最大的问题就是本身属于浮点数,这就导致了如果非浮点数由他来表示缺失值就导致类型转换(其中整数转换为浮点数,其他转换为 object)
  2. None: Python 原生表示缺失值的对象,他通常作为标量的缺失值表示在 Pandas 中体现的并不多(注意他不与其他类型兼容,如果混用同样需要是 object)
  3. NaT(Not a Time): 同样是引入 numpy 的一种表示方式,他主要用于时间序列
  4. pd.NA: 这是 Pandas 1.0+ 引入的旨在统一不同数据类型的缺失值表示,同时他引入了 nullable 数据引擎来表示 nullable Integernullable Bolleannullable String

由于历史遗留原因,默认情况构造的 DataFrame 依然沿用了 NaN 和 NaT 这个 numpy 的类型系统。尽管在 Pandas1.0 中引入了 pd.NA 他也是在 numpy 上引入了 nullable-numpy 类型系统。依然会混用他们。直到 pandas 2.0 引入了 pyarrow 类型系统才真正的实现统一使用 pd.NA 来表示缺失值:

Python
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() 方法来检测缺失值,其中 NoneNaNNaT 以及 NA 都被认为是缺失值。

Python
In [13]: df.isna()
Out[13]:
       a      b      c      d
0  False  False  False  False
1   True   True   True   True

缺失值运算

一般来说缺失值会在涉及的运算中传播,及当一个操作数未知时涉及到的结果也通常是未知的。

例如涉及 NA 的算术运算中:

Python
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 也不影响结果时也会返回结果:

Python
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:

Python
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,那一大堆的聚合运算就别玩了,所以不同的聚合算法中缺失值的处理方式不同:

  1. sum(): 求和算法中缺失值被认为是 0
  2. prod(): 求积运算中缺失值被认为是 1
  3. 累计运算中会默认忽略缺失值,也会提供参数来决定是否让缺失值参与

不同类型系统转换

由于历史原因目前默认依然混用 NaNNaTNone 来作为缺失值,他也是 numpy 中表示缺失值的方式。

Pandas 直接使用 numpy 的最大的问题就是 Integer 会转换为 Float,Boolean、String 会转换为 Object 来兼容数据类型因此 Pandas 1.0 中引入了 NA 并引入了 Int64DtypeStringDtypeBooleanDtype 数据类型他们也被称为numpy_nullable,我们可以使用 astype 来转换:

Python
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 上:

Python
# 也可以是 "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() 方法来统一转换:

Python
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 时首先执行下该函数:

Python
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 来在读取时直接转换为对应的数据类型系统

清洗缺失值

对缺失值的清洗无非三种形式:

  1. dropna(): 删除缺失值
  2. fillna(): 用其他值替换缺失值(需要注意类型兼容)
  3. interpolate(): 用各种插值方法填充缺失值

dropna

dropna()用于删除缺失值:

Python
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 之间的配合来决定要删除那些缺失值数据:

Python
>>> 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 类型填充空字符串,除非你先转换为字符串。

interpolate

参考