Учебник: Маскированные массивы
Предварительные требования
Прежде чем читать этот учебник, вам следует немного знать Python. Если вы хотите освежить свои знания, ознакомьтесь с учебником по Python.
Если вы хотите выполнить примеры в этом учебнике, вам также необходимо установить matplotlib на вашем компьютере.
Профиль обучающегося
Этот учебник предназначен для людей, которые имеют базовое понимание NumPy и хотят понять, как на практике используются маскированные массивы и модуль numpy.ma.
Цели обучения
После этого учебника вы сможете:
- Понять, что такое маскированные массивы и как они могут быть созданы
- Понять, как получить доступ к данным маскированных массивов и изменить их
- Определить, когда использование маскированных массивов уместно в некоторых ваших приложениях
Что такое маскированные массивы?
Рассмотрим следующую проблему. У вас есть набор данных с отсутствующими или недопустимыми значениями. Если вы выполняете какие-либо обработку этих данных и хотите отметить или пометить эти нежелательные значения, не удаляя их, вам, возможно, придется использовать условные операторы или как-то отфильтровать ваши данные. Модуль numpy.ma предоставляет функциональность, аналогичную NumPy ndarrays, с добавленной структурой, которая гарантирует, что недопустимые значения не используются в вычислениях.
Из документации Reference Guide:
Маскированный массив — это комбинация стандартного numpy.ndarray и маски. Маска является либо nomask, указывая, что ни одно значение связанного массива не является недопустимым, либо массивом булевых значений, определяющим для каждого элемента связанного массива, является ли значение допустимым или нет. Когда элемент маски False, соответствующий элемент связанного массива допустим и считается не замаскированным. Когда элемент маски True, соответствующий элемент связанного массива считается замаскированным (недопустимым).
Мы можем рассматривать MaskedArray как комбинацию:
- Данных, как обычного
numpy.ndarrayлюбой формы или типа данных; - Булевой маски с той же формой, что и данные;
- Значения
fill_value, которое может использоваться для замены недопустимых значений, чтобы получить стандартныйnumpy.ndarray.
Когда они могут быть полезны?
Существует несколько ситуаций, когда маскированные массивы могут быть более полезны, чем просто исключение недопустимых элементов массива:
- Когда вы хотите сохранить замаскированные значения для дальнейшей обработки, не копируя массив;
- Когда вам нужно обрабатывать множество массивов, каждый со своей маской. Если маска является частью массива, вы избегаете ошибок, и код, возможно, более компактен;
- Когда у вас есть разные флаги для отсутствующих или недопустимых значений, и вы хотите сохранить эти флаги без их замены в исходном наборе данных, но исключить их из вычислений;
- Если вы не можете избежать или устранить отсутствующие значения, но не хотите иметь дело с значениями
NaN(Not A Number) в своих операциях.
Маскированные массивы также являются хорошей идеей, поскольку модуль 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, label='Mainland China');
In [45]: plt.plot(t[china_total.mask], cubic_fit[china_total.mask], '--',
....: color='orange', label='Cubic estimate');
....:
In [46]: plt.plot(7, np.polyval(params, 7), 'r*', label='7 days after start');
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();
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–2022 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.21/user/tutorial-ma.html