Учебник: Маскированные массивы
Предварительные требования
Перед чтением этого учебника вам следует знать немного Python. Если вы хотите освежить свои знания, взгляните на учебник по Python.
Если вы хотите выполнить примеры в этом учебнике, вам также необходимо установить matplotlib на вашем компьютере.
Профиль обучающегося
Этот учебник предназначен для людей, которые имеют базовое понимание NumPy и хотят понять, как на практике использовать маскированные массивы и модуль numpy.ma.
Цели обучения
После этого учебника вы должны уметь:
- Понять, что такое маскированные массивы и как они могут быть созданы
- Понять, как получить доступ к данным и изменять их для маскированных массивов
- Определить, когда использование маскированных массивов целесообразно в некоторых ваших приложениях
Что такое маскированные массивы?
Рассмотрим следующую задачу. У вас есть набор данных с пропущенными или недопустимыми записями. Если вы выполняете какие-либо операции с этими данными и хотите отметить эти нежелательные записи без простого их удаления, вам, возможно, придется использовать условные операторы или фильтровать данные каким-либо образом. Модуль numpy.ma предоставляет некоторую функциональность NumPy ndarrays с добавленной структурой для обеспечения того, чтобы недопустимые записи не использовались в вычислениях.
Из Reference Guide:
Маскированный массив — это комбинация стандартного numpy.ndarray и маски. Маска либо nomask, что указывает на то, что ни одно значение связанного массива не является недопустимым, либо массив булевых значений, определяющий для каждого элемента связанного массива, является ли значение допустимым или нет. Когда элемент маски False, соответствующий элемент связанного массива допустим и считается не замаскированным. Когда элемент маски True, соответствующий элемент связанного массива считается замаскированным (недопустимым).
Мы можем рассматривать MaskedArray как комбинацию:
- Данных, как обычного
numpy.ndarrayлюбой формы или типа данных; - Булевой маски с той же формой, что и данные;
- Замещающего значения, значения, которое может быть использовано для замены недопустимых записей, чтобы вернуть стандартный
numpy.ndarray.
Когда они могут быть полезны?
Существует несколько ситуаций, когда маскированные массивы могут быть более полезны, чем просто удаление недопустимых записей из массива:
- Когда вы хотите сохранить замаскированные значения для дальнейшей обработки без копирования массива;
- Когда вам нужно обрабатывать много массивов, каждый со своей маской. Если маска является частью массива, вы избегаете ошибок, и код, возможно, более компактен;
- Когда у вас есть разные флаги для пропущенных или недопустимых значений, и вы хотите сохранить эти флаги без их замены в исходном наборе данных, но исключить их из вычислений;
- Если вы не можете избежать или устранить пропущенные значения, но не хотите иметь дело со значениями
NaN(Не Число) в своих операциях.
Маскированные массивы также хороши, поскольку модуль numpy.ma также поставляется со специфической реализацией большинства универсальных функций NumPy (ufuncs), что означает, что вы по-прежнему можете применять быстрые векторные функции и операции к маскированным данным. Результатом затем будет маскированный массив. Ниже мы рассмотрим несколько примеров, как это работает на практике.
Использование маскированных массивов для просмотра данных о COVID-19
Из Kaggle можно загрузить набор данных с начальными данными о вспышке COVID-19 в начале 2020 года. Мы собираемся взглянуть на небольшой подмножество этих данных, содержащихся в файле who_covid_19_sit_rep_time_series.csv.
In [1]: import numpy as np In [2]: 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 In [3]: filepath = os.getcwd() In [4]: filename = os.path.join(filepath, "who_covid_19_sit_rep_time_series.csv")
Файл данных содержит данные разных типов и организован следующим образом:
- Первая строка — это строка заголовка, которая (в основном) описывает данные в каждом столбце, которые следуют в строках ниже, а начиная с четвертого столбца, заголовок — это дата наблюдения.
- Вторая по седьмую строку содержат сводные данные, тип которых отличается от того, что мы собираемся изучать, поэтому нам нужно исключить их из данных, с которыми мы будем работать.
- Числовые данные, с которыми мы хотим работать, начинаются в столбце 4, строке 8 и простираются до правого столбца и нижней строки.
Давайте изучим данные в этом файле для первых 14 дней записей. Чтобы собрать данные из файла .csv, мы будем использовать функцию numpy.genfromtxt, убедившись, что мы выбираем только столбцы с фактическими числами вместо первых трех столбцов, которые содержат данные о местоположении. Мы также пропустим первые 7 строк этого файла, так как они содержат другие данные, которые нас не интересуют. Отдельно мы извлечем информацию о датах и местоположении для этих данных.
# 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 3-7 from the first row In [5]: dates = np.genfromtxt(filename, dtype=np.unicode_, delimiter=",", ...: max_rows=1, usecols=range(3, 17), ...: encoding="utf-8-sig") ...: # Read the names of the geographic locations from the first two # columns, skipping the first seven rows In [6]: locations = np.genfromtxt(filename, dtype=np.unicode_, delimiter=",", ...: skip_header=7, usecols=(0, 1), ...: encoding="utf-8-sig") ...: # Read the numeric data from just the first 14 days In [7]: nbcases = np.genfromtxt(filename, dtype=np.int_, delimiter=",", ...: skip_header=7, usecols=range(3, 17), ...: encoding="utf-8-sig") ...:
В вызове функции numpy.genfromtxt мы выбрали тип данных numpy.dtype для каждого подмножества данных (целое число - numpy.int_ - или строка символов - numpy.unicode_). Мы также использовали аргумент encoding для выбора utf-8-sig в качестве кодировки файла (подробнее о кодировке см. в официальной документации Python). Подробнее о функции numpy.genfromtxt вы можете узнать из документации по Reference Documentation или из учебника по базовому вводу-выводу.
Изучение данных
Прежде всего, мы можем построить весь набор данных, который у нас есть, и посмотреть, как он выглядит. Для получения удобочитаемого графика мы выбираем только несколько дат для отображения в нашей команде x-axis ticks. Обратите также внимание, что в нашей команде построения графиков мы используем nbcases.T (транспонированный массив nbcases), так как это означает, что мы построим каждую строку файла в виде отдельной линии. Мы выбираем построение пунктирной линии (используя стиль линии '--'). Подробнее об этом см. в документации matplotlib.
In [8]: import matplotlib.pyplot as plt
In [9]: selected_dates = [0, 3, 11, 13]
In [10]: plt.plot(dates, nbcases.T, '--');
In [11]: plt.xticks(selected_dates, dates[selected_dates]);
In [12]: plt.title("COVID-19 cumulative cases from Jan 21 to Feb 3 2020");
Примечание
Если вы выполняете вышеуказанные команды в оболочке IPython, может потребоваться использовать команду plt.show() для отображения окна изображения. Обратите также внимание, что мы используем точку с запятой в конце строки, чтобы подавить ее вывод, но это необязательно.
График имеет странную форму с 24 января по 1 февраля. Было бы интересно узнать, откуда эти данные. Если мы посмотрим на массив locations, который мы извлекли из файла .csv, мы увидим, что у нас есть два столбца, где первый будет содержать регионы, а второй — название страны. Однако только в первых строках содержатся данные для первого столбца (названия провинций в Китае). В дальнейшем у нас есть только названия стран. Поэтому имеет смысл сгруппировать все данные из Китая в одну строку. Для этого мы выберем из массива nbcases только те строки, для которых второй элемент массива locations соответствует Китаю. Далее мы воспользуемся функцией numpy.sum для суммирования всех выбранных строк (axis=0):
In [13]: china_total = nbcases[locations[:, 1] == 'China'].sum(axis=0)
In [14]: china_total
Out[14]:
array([ 247, 288, 556, 817, -22, -22, -15, -10, -9,
-7, -4, 11820, 14410, 17237])
Что-то не так с этими данными — у нас не должно быть отрицательных значений в кумулятивном наборе данных. Что происходит?
Пропущенные данные
Глядя на данные, вот что мы обнаружили: есть период с пропущенными данными:
In [15]: nbcases
Out[15]:
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]])
Все значения -1 , которые мы видим, поступают от numpy.genfromtxt, пытающегося прочитать пропущенные данные из исходного файла .csv. Очевидно, мы не хотим считать пропущенные данные как -1 — мы просто хотим пропустить это значение, чтобы оно не мешало нашему анализу. После импорта модуля numpy.ma мы создадим новый массив, на этот раз замаскировав недопустимые значения:
In [16]: from numpy import ma In [17]: nbcases_ma = ma.masked_values(nbcases, -1)
Если мы посмотрим на маскированный массив nbcases_ma, то вот что у нас есть:
In [18]: nbcases_ma
Out[18]:
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 имеет значение True для элементов, соответствующих некорректным данным (представленным двумя дефисами в атрибуте data).
Примечание
Добавление -1 к отсутствующим данным не является проблемой для numpy.genfromtxt; в данном конкретном случае, подстановка отсутствующего значения с 0 могла бы подойти, но позже мы увидим, что это далеко не общее решение. Также можно вызвать функцию numpy.genfromtxt с параметром usemask. Если usemask=True, numpy.genfromtxt автоматически возвращает массив с пропусками.
Давайте попробуем посмотреть, как выглядят данные, исключая первую строку (данные из провинции Хубэй в Китае), чтобы более подробно рассмотреть пропущенные данные:
In [19]: plt.plot(dates, nbcases_ma[1:].T, '--');
In [20]: plt.xticks(selected_dates, dates[selected_dates]);
In [21]: plt.title("COVID-19 cumulative cases from Jan 21 to Feb 3 2020");
Теперь, когда наши данные были обработаны с пропусками, давайте попробуем просуммировать все случаи в Китае:
In [22]: china_masked = nbcases_ma[locations[:, 1] == 'China'].sum(axis=0)
In [23]: china_masked
Out[23]:
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:
In [24]: china_total = china_masked.data
In [25]: china_total
Out[25]:
array([ 278, 309, 574, 835, 10, 10, 17, 22, 23,
25, 28, 11821, 14411, 17238])
Это лучше: больше нет отрицательных значений. Однако мы все еще можем видеть, что в некоторые дни кумулятивное количество случаев, кажется, уменьшается (например, с 835 до 10), что не соответствует определению «кумулятивных данных». Если мы более подробно рассмотрим данные, мы увидим, что в период, когда были пропущенные данные на материковом Китае, были корректные данные для Гонконга, Тайваня, Макао и «неопределенных» регионов Китая. Возможно, мы можем исключить их из общей суммы случаев в Китае, чтобы лучше понять данные.
Сначала мы определим индексы местоположений в материковом Китае:
In [26]: china_mask = ((locations[:, 1] == 'China') & ....: (locations[:, 0] != 'Hong Kong') & ....: (locations[:, 0] != 'Taiwan') & ....: (locations[:, 0] != 'Macau') & ....: (locations[:, 0] != 'Unspecified*')) ....:
Теперь china_mask - это массив логических значений (True или False); мы можем проверить, что индексы соответствуют нашим требованиям, используя метод ma.nonzero для массивов с пропусками:
In [27]: china_mask.nonzero()
Out[27]:
(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]),)
Теперь мы можем корректно просуммировать значения для материкового Китая:
In [28]: china_total = nbcases_ma[china_mask].sum(axis=0)
In [29]: china_total
Out[29]:
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)
Мы можем заменить данные этой информацией и построить новую диаграмму, фокусируясь на материковом Китае:
In [30]: plt.plot(dates, china_total.T, '--');
In [31]: plt.xticks(selected_dates, dates[selected_dates]);
In [32]: plt.title("COVID-19 cumulative cases from Jan 21 to Feb 3 2020 - Mainland China");
Ясно, что массивы с пропусками являются правильным решением в этом случае. Мы не можем представить пропущенные данные без искажения эволюции кривой.
Подгонка данных
Один из возможных вариантов - интерполировать пропущенные данные, чтобы оценить количество случаев в конце января. Обратите внимание, что мы можем выбрать элементы с пропусками, используя атрибут .mask:
In [33]: china_total.mask
Out[33]:
array([False, False, False, False, True, True, True, True, True,
True, True, False, False, False])
In [34]: invalid = china_total[china_total.mask]
In [35]: invalid
Out[35]:
masked_array(data=[--, --, --, --, --, --, --],
mask=[ True, True, True, True, True, True, True],
fill_value=999999,
dtype=int64)
Мы также можем получить доступ к корректным значениям, используя логическое отрицание для этой маски:
In [36]: valid = china_total[~china_total.mask]
In [37]: valid
Out[37]:
masked_array(data=[278, 308, 440, 446, 11791, 14380, 17205],
mask=[False, False, False, False, False, False, False],
fill_value=999999)
Теперь, если мы хотим создать очень простое приближение для этих данных, мы должны учитывать корректные значения вокруг пропущенных. Итак, сначала давайте выберем даты, для которых данные корректны. Обратите внимание, что мы можем использовать маску из массива с пропусками china_total для индексации массива дат:
In [38]: dates[~china_total.mask]
Out[38]:
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.polyfit и numpy.polyval для создания кубического многочлена, который наилучшим образом аппроксимирует данные:
In [39]: t = np.arange(len(china_total)) In [40]: params = np.polyfit(t[~china_total.mask], valid, 3) In [41]: cubic_fit = np.polyval(params, t) In [42]: plt.plot(t, china_total); In [43]: plt.plot(t, cubic_fit, '--');
Эта диаграмма не очень читаема, так как линии перекрываются, поэтому давайте сгруппируем их в более подробную диаграмму. Мы отобразим реальные данные, когда они доступны, и покажем кубическую аппроксимацию для недоступных данных, используя эту аппроксимацию для вычисления оценки наблюдаемого числа случаев 28 января 2020 года, через 7 дней после начала регистрации:
In [44]: plt.plot(t, china_total);
In [45]: plt.plot(t[china_total.mask], cubic_fit[china_total.mask], '--', color='orange');
In [46]: plt.plot(7, np.polyval(params, 7), 'r*');
In [47]: plt.xticks([0, 7, 13], dates[[0, 7, 13]]);
In [48]: plt.yticks([0, np.polyval(params, 7), 10000, 17500]);
In [49]: plt.legend(['Mainland China', 'Cubic estimate', '7 days after start']);
In [50]: plt.title("COVID-19 cumulative cases from Jan 21 to Feb 3 2020 - Mainland China\n"
....: "Cubic estimate for 7 days after start");
....:
Дополнительные материалы для чтения
Темы, не освещенные в этом руководстве, можно найти в документации:
-
Hardmasksпротивsoftmasks - Модуль numpy.ma
© 2005–2021 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.20/user/tutorial-ma.html