NumPy 掩码数组与缺失值

在真实数据集中,经常会出现缺失或无效的数据。NumPy的掩码数组(MaskedArray)通过一个独立的布尔掩码来标记哪些数据是无效的,从而在计算时自动忽略这些值。这使得处理缺失值变得简单而高效。

1. 掩码数组概述

掩码数组是 numpy.ma 模块提供的 MaskedArray 类,它包含两个核心部分:数据数组掩码数组。掩码是一个布尔数组,True 表示对应位置的数据无效(被掩码),False 表示有效。当对掩码数组执行运算时,被掩码的值会被自动忽略。

2. 创建掩码数组

2.1 从现有数组创建

使用 np.ma.masked_array 可以直接从数据和掩码创建。

import numpy as np
import numpy.ma as ma

data = np.array([1, 2, 3, 4, 5])
mask = np.array([False, False, True, False, True])  # 标记索引2和4为无效
masked_arr = ma.masked_array(data, mask=mask)
print("掩码数组:", masked_arr)

输出:

掩码数组: [1 2 -- 4 --]

其中 -- 表示被掩码的值。

2.2 使用条件创建掩码

np.ma.masked_where(condition, arr) 会将满足条件的位置掩码。

arr = np.array([1, 2, 3, 4, 5])
masked = ma.masked_where(arr > 3, arr)
print("掩码大于3的值:", masked)   # [1 2 3 -- --]

2.3 掩码无效值(如 NaN 或 inf)

使用 np.ma.masked_invalid 可以自动掩码 NaN 和 inf。

arr_with_nan = np.array([1, 2, np.nan, 4, np.inf])
masked_invalid = ma.masked_invalid(arr_with_nan)
print("掩码无效值:", masked_invalid)   # [1.0 2.0 -- 4.0 --]

2.4 使用 masked_values

如果需要掩码特定的数值(如 -9999 表示缺失值),可以使用 masked_values

arr = np.array([10, -9999, 30, -9999, 50])
masked_val = ma.masked_values(arr, -9999)
print("掩码特定值:", masked_val)   # [10 -- 30 -- 50]

3. 访问掩码和数据

通过 .data.mask 属性可以分别访问原始数据和掩码。

print("数据:", masked_arr.data)
print("掩码:", masked_arr.mask)

输出:

数据: [1 2 3 4 5]
掩码: [False False  True False  True]

4. 操作掩码数组

4.1 算术运算自动忽略掩码

a = ma.masked_where([False, True, False], [1, 2, 3])
b = ma.masked_where([False, False, True], [10, 20, 30])
print("a:", a)   # [1 -- 3]
print("b:", b)   # [10 20 --]
c = a + b
print("a + b:", c)   # [11 -- --]  (被掩码的位置结果也被掩码)

4.2 聚合函数(自动忽略掩码)

arr = ma.masked_where([False, True, False, False], [1, 2, 3, 4])
print("数组:", arr)          # [1 -- 3 4]
print("总和:", arr.sum())    # 1+3+4 = 8
print("均值:", arr.mean())   # 8/3 ≈ 2.6667

4.3 填充缺失值:filled()

使用 filled(fill_value) 可以将掩码值替换为指定值,返回普通数组。

filled_arr = arr.filled(0)
print("填充0:", filled_arr)   # [1 0 3 4]

4.4 压缩掩码值:compressed()

compressed() 返回只包含有效值的一维数组。

compressed = arr.compressed()
print("压缩后:", compressed)   # [1 3 4]

5. 修改掩码

可以直接修改 .mask 属性,或者使用 masked_where 重新掩码。

arr.mask[2] = True   # 将索引2也掩码
print("修改后:", arr)   # [1 -- -- 4]

6. 与普通数组的相互转换

  • 普通数组转掩码数组:使用 ma.masked_arrayma.masked_where 等。
  • 掩码数组转普通数组:使用 filled() 填充缺失值,或 compressed() 丢弃缺失值。
  • 直接使用 np.array(masked_arr) 会丢失掩码信息,返回原始数据(包含无效值),通常不推荐。
# 不推荐:直接转回普通数组会包含原始值(包括原本掩码的值)
plain = np.array(masked_arr)
print("直接转回:", plain)   # [1 2 3 4 5]

7. 常用掩码数组函数

numpy.ma 模块提供了与 numpy 对应的函数,它们自动处理掩码。例如:

  • ma.sum, ma.mean, ma.std, ma.var
  • ma.max, ma.min
  • ma.concatenate
print("最大值:", ma.max(arr))
print("最小值:", ma.min(arr))   # 忽略掩码

8. 实际应用示例:处理气象数据

假设有一组温度数据,其中 -999 表示缺失值。

temps = np.array([25, 28, -999, 22, 23, -999, 30])
# 将 -999 掩码
masked_temps = ma.masked_values(temps, -999)
print("掩码后温度:", masked_temps)
print("有效温度均值:", masked_temps.mean())
print("有效温度标准差:", masked_temps.std())

输出:

掩码后温度: [25 28 -- 22 23 -- 30]
有效温度均值: 25.6
有效温度标准差: 3.114482

9. 性能与注意事项

  • 掩码数组在每次运算时都会检查掩码,因此比普通数组稍慢。但对于适度大小的数组,这种开销可以接受。
  • 对于非常大的数据,如果缺失值比例很高,可以考虑使用稀疏格式或专门处理缺失值的库(如 pandas)。
  • 掩码数组与 pandas 的 Series/DataFrame 可以相互转换,pandas 的 isnull 也提供了类似功能。
提示: 如果只是简单忽略 NaN 值,也可以使用 np.nanmean 等函数,但掩码数组提供了更灵活的控制(例如标记任意值为无效)。

总结

掩码数组是 NumPy 处理缺失值和无效数据的强大工具。通过 numpy.ma 模块,可以轻松标记无效数据,并在后续计算中自动忽略它们,从而避免手动过滤的麻烦。掌握掩码数组的创建、修改和应用,能让你更高效地进行数据清洗和分析。下一章我们将讨论 NumPy 的性能优化技巧。