掩码数组#
你将做什么#
使用NumPy的掩码数组模块分析COVID-19数据并处理缺失值。
你将学到什么#
你将理解什么是掩码数组以及如何创建它们
你将看到如何访问和修改掩码数组的数据
你将能够决定何时在某些应用中适合使用掩码数组
你将需要什么#
对Python有基本了解。如果你想回顾一下,请查看Python教程。
对NumPy有基本了解
要在你的计算机上运行绘图,你需要matplotlib。
什么是掩码数组?#
考虑以下问题。你有一个包含缺失或无效条目的数据集。如果你要对这些数据进行任何类型的处理,并且希望*跳过*或标记这些不需要的条目而不只是删除它们,你可能需要使用条件语句或以某种方式过滤你的数据。numpy.ma模块提供了与NumPy ndarray相同的一些功能,并增加了结构以确保无效条目不被用于计算。
摘自参考指南
掩码数组是标准numpy.ndarray和**掩码**的组合。掩码要么是
nomask
,表示关联数组中没有值是无效的,要么是一个布尔数组,它决定了关联数组的每个元素是否有效。当掩码的元素是False
时,关联数组的相应元素是有效的,被称为未掩码。当掩码的元素是True
时,关联数组的相应元素被称为已掩码(无效)。
我们可以将MaskedArray视为以下各项的组合:
数据,作为任何形状或数据类型的常规
numpy.ndarray
;一个与数据形状相同的布尔掩码;
一个
fill_value
,一个可用于替换无效条目以返回标准numpy.ndarray
的值。
何时它们会很有用?#
在以下几种情况下,掩码数组比简单地消除数组中的无效条目更有用:
当你希望保留你掩码的值以便后续处理,而无需复制数组时;
当你必须处理许多数组,每个数组都有自己的掩码时。如果掩码是数组的一部分,你可以避免错误,并且代码可能更紧凑;
当你对缺失或无效值有不同的标记,并希望在原始数据集中保留这些标记而不替换它们,但将它们从计算中排除时;
如果你无法避免或消除缺失值,但又不想在操作中处理NaN(非数字)值。
掩码数组也是一个好主意,因为numpy.ma
模块还附带了大多数NumPy通用函数(ufuncs)的特定实现,这意味着你仍然可以在掩码数据上应用快速矢量化函数和操作。输出将是掩码数组。我们将在下面看到一些关于这在实践中如何工作的示例。
使用掩码数组查看COVID-19数据#
从Kaggle可以下载一个包含2020年初COVID-19疫情初始数据的数据集。我们将查看此数据的一个小子集,包含在文件who_covid_19_sit_rep_time_series.csv
中。(*请注意,此文件已在2020年末某个时候被替换为不含缺失数据的版本。*)
import numpy as np
import os
# The os.getcwd() function returns the current folder; you can change
# the filepath variable to point to the folder where you saved the .csv file
filepath = os.getcwd()
filename = os.path.join(filepath, "who_covid_19_sit_rep_time_series.csv")
数据文件包含不同类型的数据,其组织如下:
第一行是标题行,它(大部分)描述了后续行中每列的数据,从第四列开始,标题是观察日期。
第二行到第七行包含摘要数据,其类型与我们要检查的数据不同,因此我们需要将其从我们将使用的数据中排除。
我们要使用的数值数据从第4列、第8行开始,并从那里延伸到最右边的列和最下面的行。
让我们探索这个文件中前14天的记录数据。为了从.csv
文件中获取数据,我们将使用numpy.genfromtxt函数,确保我们只选择包含实际数字的列,而不是前四列包含位置数据的列。我们还跳过此文件的前6行,因为它们包含我们不感兴趣的其他数据。另外,我们将提取此数据的日期和位置信息。
# Note we are using skip_header and usecols to read only portions of the
# data file into each variable.
# Read just the dates for columns 4-18 from the first row
dates = np.genfromtxt(
filename,
dtype=np.str_,
delimiter=",",
max_rows=1,
usecols=range(4, 18),
encoding="utf-8-sig",
)
# Read the names of the geographic locations from the first two
# columns, skipping the first six rows
locations = np.genfromtxt(
filename,
dtype=np.str_,
delimiter=",",
skip_header=6,
usecols=(0, 1),
encoding="utf-8-sig",
)
# Read the numeric data from just the first 14 days
nbcases = np.genfromtxt(
filename,
dtype=np.int_,
delimiter=",",
skip_header=6,
usecols=range(4, 18),
encoding="utf-8-sig",
)
在numpy.genfromtxt
函数调用中,我们为每个数据子集(整数 - numpy.int_
- 或字符串 - numpy.str_
)选择了numpy.dtype。我们还使用了encoding
参数将utf-8-sig
选为文件的编码(有关编码的更多信息,请参阅官方Python文档)。你可以从参考文档或基本IO教程中阅读有关numpy.genfromtxt
函数的更多信息。
探索数据#
首先,我们可以绘制我们拥有的所有数据,看看它长什么样。为了获得可读的图表,我们只选择少量日期在x轴刻度中显示。另请注意,在我们的绘图命令中,我们使用nbcases.T
(nbcases
数组的转置),因为这意味着我们将文件的每一行绘制为单独的线。我们选择绘制虚线(使用'--'
线型)。有关更多信息,请参阅matplotlib文档。
import matplotlib.pyplot as plt
selected_dates = [0, 3, 11, 13]
plt.plot(dates, nbcases.T, "--")
plt.xticks(selected_dates, dates[selected_dates])
plt.title("COVID-19 cumulative cases from Jan 21 to Feb 3 2020")
Text(0.5, 1.0, 'COVID-19 cumulative cases from Jan 21 to Feb 3 2020')

该图表从1月24日到2月1日呈现出奇怪的形状。了解这些数据来源会很有趣。如果我们查看从.csv
文件中提取的locations
数组,我们可以看到它有两列,第一列包含区域,第二列包含国家名称。然而,只有前几行包含第一列的数据(中国各省的名称)。之后,我们只有国家名称。因此,将所有来自中国的数据分组到一行中是有意义的。为此,我们将从nbcases
数组中选择locations
数组的第二个条目对应于中国的行。接下来,我们将使用numpy.sum函数对所有选定的行求和(axis=0
)。另请注意,第35行对应于每个日期的全国总计数。由于我们想从省份数据中自行计算总和,所以我们必须首先从locations
和nbcases
中删除该行。
totals_row = 35
locations = np.delete(locations, (totals_row), axis=0)
nbcases = np.delete(nbcases, (totals_row), axis=0)
china_total = nbcases[locations[:, 1] == "China"].sum(axis=0)
china_total
array([ 247, 288, 556, 817, -22, -22, -15, -10, -9,
-7, -4, 11820, 14410, 17237])
这些数据有问题——累计数据集中不应该有负值。发生了什么?
缺失数据#
查看数据,我们发现:存在一段**缺失数据**的时期
nbcases
array([[ 258, 270, 375, ..., 7153, 9074, 11177],
[ 14, 17, 26, ..., 520, 604, 683],
[ -1, 1, 1, ..., 422, 493, 566],
...,
[ -1, -1, -1, ..., -1, -1, -1],
[ -1, -1, -1, ..., -1, -1, -1],
[ -1, -1, -1, ..., -1, -1, -1]], shape=(263, 14))
我们看到的所有-1
值都来自numpy.genfromtxt
试图从原始.csv
文件中读取缺失数据。显然,我们不想将缺失数据计算为-1
——我们只想跳过此值,以免它干扰我们的分析。导入numpy.ma
模块后,我们将创建一个新数组,这次对无效值进行掩码处理
from numpy import ma
nbcases_ma = ma.masked_values(nbcases, -1)
如果我们查看nbcases_ma
掩码数组,这就是我们拥有的
nbcases_ma
masked_array(
data=[[258, 270, 375, ..., 7153, 9074, 11177],
[14, 17, 26, ..., 520, 604, 683],
[--, 1, 1, ..., 422, 493, 566],
...,
[--, --, --, ..., --, --, --],
[--, --, --, ..., --, --, --],
[--, --, --, ..., --, --, --]],
mask=[[False, False, False, ..., False, False, False],
[False, False, False, ..., False, False, False],
[ True, False, False, ..., False, False, False],
...,
[ True, True, True, ..., True, True, True],
[ True, True, True, ..., True, True, True],
[ True, True, True, ..., True, True, True]],
fill_value=-1)
我们可以看到这是一种不同类型的数组。正如引言中提到的,它有三个属性(data
、mask
和fill_value
)。请记住,mask
属性对于对应于**无效**数据(在data
属性中用两个破折号表示)的元素具有True
值。
让我们尝试看看排除第一行(中国湖北省的数据)后的数据是什么样子,这样我们就可以更仔细地查看缺失数据
plt.plot(dates, nbcases_ma[1:].T, "--")
plt.xticks(selected_dates, dates[selected_dates])
plt.title("COVID-19 cumulative cases from Jan 21 to Feb 3 2020")
Text(0.5, 1.0, 'COVID-19 cumulative cases from Jan 21 to Feb 3 2020')

现在我们的数据已经掩码了,让我们尝试计算中国所有病例的总和
china_masked = nbcases_ma[locations[:, 1] == "China"].sum(axis=0)
china_masked
masked_array(data=[278, 309, 574, 835, 10, 10, 17, 22, 23, 25, 28, 11821,
14411, 17238],
mask=[False, False, False, False, False, False, False, False,
False, False, False, False, False, False],
fill_value=999999)
请注意,china_masked
是一个掩码数组,因此它与常规NumPy数组具有不同的数据结构。现在,我们可以使用.data
属性直接访问其数据
china_total = china_masked.data
china_total
array([ 278, 309, 574, 835, 10, 10, 17, 22, 23,
25, 28, 11821, 14411, 17238])
这样好多了:没有负值了。然而,我们仍然可以看到,在某些日子里,累计病例数似乎下降了(例如,从835降到10),这与“累计数据”的定义不符。如果仔细查看数据,我们可以看到,在中国大陆数据缺失期间,香港、台湾、澳门和“未指定”的中国地区都有有效数据。也许我们可以将这些地区从中国的总病例数中移除,以便更好地理解数据。
首先,我们将识别中国大陆地区的位置索引
china_mask = (
(locations[:, 1] == "China")
& (locations[:, 0] != "Hong Kong")
& (locations[:, 0] != "Taiwan")
& (locations[:, 0] != "Macau")
& (locations[:, 0] != "Unspecified*")
)
现在,china_mask
是一个布尔值数组(True
或False
);我们可以使用掩码数组的ma.nonzero方法检查索引是否符合我们的预期
china_mask.nonzero()
(array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28, 29, 31, 33]),)
现在我们可以正确地对中国大陆的条目求和了
china_total = nbcases_ma[china_mask].sum(axis=0)
china_total
masked_array(data=[278, 308, 440, 446, --, --, --, --, --, --, --, 11791,
14380, 17205],
mask=[False, False, False, False, True, True, True, True,
True, True, True, False, False, False],
fill_value=999999)
我们可以用这些信息替换数据并绘制新图,重点关注中国大陆
plt.plot(dates, china_total.T, "--")
plt.xticks(selected_dates, dates[selected_dates])
plt.title("COVID-19 cumulative cases from Jan 21 to Feb 3 2020 - Mainland China")
Text(0.5, 1.0, 'COVID-19 cumulative cases from Jan 21 to Feb 3 2020 - Mainland China')

很明显,掩码数组是这里的正确解决方案。我们无法在不误导曲线演变的情况下表示缺失数据。
数据拟合#
我们能想到的一种可能性是插值缺失数据以估计1月下旬的病例数。请注意,我们可以使用.mask
属性选择被掩码的元素
china_total.mask
invalid = china_total[china_total.mask]
invalid
masked_array(data=[--, --, --, --, --, --, --],
mask=[ True, True, True, True, True, True, True],
fill_value=999999,
dtype=int64)
我们也可以通过对这个掩码进行逻辑非运算来访问有效条目
valid = china_total[~china_total.mask]
valid
masked_array(data=[278, 308, 440, 446, 11791, 14380, 17205],
mask=[False, False, False, False, False, False, False],
fill_value=999999)
现在,如果我们要为这些数据创建一个非常简单的近似,我们应该考虑无效条目周围的有效条目。所以首先让我们选择数据有效的日期。请注意,我们可以使用china_total
掩码数组中的掩码来索引日期数组
dates[~china_total.mask]
array(['1/21/20', '1/22/20', '1/23/20', '1/24/20', '2/1/20', '2/2/20',
'2/3/20'], dtype='<U7')
最后,我们可以使用numpy.polynomial包的拟合功能来创建一个三次多项式模型,使其尽可能地拟合数据。
t = np.arange(len(china_total))
model = np.polynomial.Polynomial.fit(t[~china_total.mask], valid, deg=3)
plt.plot(t, china_total)
plt.plot(t, model(t), "--")
[<matplotlib.lines.Line2D at 0x7f6048898bb0>]

这张图表可读性不高,因为线条似乎相互重叠,所以让我们用更精细的图表来概括。我们将绘制可用的真实数据,并显示不可用数据的三次拟合,使用此拟合来计算2020年1月28日(记录开始7天后)观察到的病例数的估计值。
plt.plot(t, china_total)
plt.plot(t[china_total.mask], model(t)[china_total.mask], "--", color="orange")
plt.plot(7, model(7), "r*")
plt.xticks([0, 7, 13], dates[[0, 7, 13]])
plt.yticks([0, model(7), 10000, 17500])
plt.legend(["Mainland China", "Cubic estimate", "7 days after start"])
plt.title(
"COVID-19 cumulative cases from Jan 21 to Feb 3 2020 - Mainland China\n"
"Cubic estimate for 7 days after start"
)
Text(0.5, 1.0, 'COVID-19 cumulative cases from Jan 21 to Feb 3 2020 - Mainland China\nCubic estimate for 7 days after start')

实践中#
将
-1
添加到缺失数据对numpy.genfromtxt
来说不是问题;在这个特定案例中,用0
替换缺失值可能没问题,但我们稍后会看到这远非一个通用解决方案。此外,可以使用usemask
参数调用numpy.genfromtxt
函数。如果usemask=True
,numpy.genfromtxt
会自动返回一个掩码数组。
进一步阅读#
本教程未涵盖的主题可以在文档中找到
参考#
Ensheng Dong, Hongru Du, Lauren Gardner, 实时跟踪COVID-19的交互式网络仪表板, The Lancet Infectious Diseases, 第20卷, 第5期, 2020年, 第533-534页, ISSN 1473-3099, https://doi.org/10.1016/S1473-3099(20)30120-1。