При работе с данными часто возникает необходимость найти элементы по определенному условию. С точки зрения поиска ошибок в данных критерии поиска могут быть разными, например:
- Отрицательные значения в графе «Количество продаж»
- Буквы вместо цифр в графе «Номер телефона»
- Нереалистично большие числа в графе «Сумма сделки»
В стандартном Python искать и фильтровать значения можно с помощью функции filter()
. В Numpy есть схожая функциональность, которую мы рассмотрим в этом уроке. Вы узнаете, как получить элементы по заданному условию при работе с массивами numpy.ndarray
.
Как создать булеву маску
Для фильтрации значений массива numpy.ndarray
по определенному условию используют булевы маски — массивы значений True
и False
. Каждый элемент проходит фильтрацию через булеву маску и распределяется в зависимости от значения маски:
- Если на той же позиции в маске стоит значение
True
, элемент добавляется в итоговый массив - Если на позиции стоит значение
False
, то элемент не будет включен в итоговый массив
Существует три способа работы с булевой маской:
- Создать массив значений
True
иFalse
вручную - Использовать операторы сравнения над элементами исходного массива
- Применить логическое отрицание к текущей маске — поменять
True
наFalse
и наоборот
Так все три способа выглядят в коде:
import numpy as np
# Исходный массив
base_array = np.array([0, 1, 2, 3, 4, 5, 6, 7,])
print(base_array)
# => [0 1 2 3 4 5 6 7]
# Ручное создание маски
handmade_mask = [True, True, True, False, False, False, False, False,]
print(handmade_mask)
# => [True, True, True, False, False, False, False, False]
# Создание маски по условию
compare_mask = base_array < 3
print(compare_mask)
# => [ True True True False False False False False]
# Создание маски по логическому отрицанию условия
opposite_compare_mask = ~(base_array >= 3)
print(opposite_compare_mask)
# => [ True True True False False False False False]
Первый способ на практике встречается редко, потому что на ручное создание маски уходит слишком много времени, особенно при большом размере массива:
Как применять маску
Чтобы применить булеву маску к исходному массиву, достаточно подставить ее в качестве индекса:
print(base_array[handmade_mask])
# => [0 1 2]
print(base_array[compare_mask])
# => [0 1 2]
print(base_array[opposite_compare_mask])
# => [0 1 2]
Как мы говорили выше, в Python реализована функция filter()
, которая применяется для итеративной фильтрации значений списка по условию:
# Фильтрация значений с использованием filter
filtered_list = list(
filter(
lambda x: x < 3,
[0, 1, 2, 3, 4, 5, 6, 7,]
)
)
print(filtered_list)
# => [0 1 2]
Для итеративной фильтрации элементов массива numpy.ndarray
используется другой способ — метод numpy.fromiter()
:
# Итеративное создание нового массива с использованием fromiter, тип массива задается аргументом dtype
filtered_array = np.fromiter(
(base_array_element for base_array_element in base_array if base_array_element < 3),
dtype = base_array.dtype
)
print(filtered_array)
# => [0 1 2]
На практике часто требуется не только убирать значения из исходного массива, но и заменять их. Для этого используется метод numpy.where()
:
# Заменяем отфильтрованные элементы на 0
print(np.where(base_array < 3, base_array, 0))
# => [0 1 2 0 0 0 0 0]
Если для фильтрации мы используем составное условие, лучше инициализировать маску отдельно. Это упрощает поддержку и делает код более читабельным:
# Заменяем отфильтрованные элементы на 0 с использованием маски
compare_masks = (base_array > 5) | (base_array < 3)
print(np.where(compare_masks, base_array, 0))
# => [0 1 2 0 0 0 6 7]
В реальных данных регулярно возникают пропущенные значения. Это может происходить из-за человеческого фактора, сбоя в работе сервисов или ошибки при записи в базу данных. Для таких случаев в Numpy существует отдельный тип данных numpy.nan
(not a number):
# Массив с пропущенными значениями
raw_array = np.array([0, 1, None, 3, 4, None, 6, 7,], dtype=np.float64)
print(raw_array)
# => [ 0. 1. nan 3. 4. nan 6. 7.]
Для корректной работы программ необходимо обнаруживать пропуски, чистить данные от них или заменять на значение по умолчанию.
Рассмотрим на примере. Сначала нужно обнаружить пропуски:
# Маска для поиска пропусков
nan_mask = np.isnan(raw_array)
print(nan_mask)
# => [False False True False False True False False]
Затем чистим данные от пропусков:
# Маска для фильтрации пропущенных значений
not_nan_mask = ~nan_mask
print(raw_array[not_nan_mask])
# => [0. 1. 3. 4. 6. 7.]
Теперь заменяем пропуски на некоторое значение:
# Заменяем пропуски на 0
print(np.where(nan_mask, 0, raw_array))
# => [0. 1. 0. 3. 4. 0. 6. 7.]
Как применять маску с двумерными массивами
Выше мы рассмотрели фильтрацию значений на примере одномерного массива. Те же принципы применимы и в работе с двумерными массивами. Рассмотрим пример задачи — подготовим сырые данные продаж магазина ноутбуков по следующим шагам:
- Сначала обнаружим выбросы двух типов — значения выше 200 и отрицательные значения. Число 200 выбрано потому, что именно столько ноутбуков хранится на складе. Менеджер магазина знает, что в день не бывает более 200 продаж
- Затем заменим выбросы и пропуски на среднее значение продаж
Мы проводим эти операции, чтобы подготовить сырые данные к более глубокому анализу с применением методов статистики и машинного обучения:
# Создаем список списков продаж четырех магазинов
orders_values = [
[7, 1, -7, None],
[1000, 2, 4, None],
[3, None, 503, 3],
[8, 12, 8, 7],
[15, 11, None, 9],
[None, 18, 17, -21],
[252, 16, 25, 17]
]
# Конвертируем в Numpy-массив
orders = np.array(orders_values, dtype=np.float64)
print(orders)
# => [[ 7. 1. -7. nan]
# [1000. 2. 4. nan]
# [ 3. nan 503. 3.]
# [ 8. 12. 8. 7.]
# [ 15. 11. nan 9.]
# [ nan 18. 17. -21.]
# [ 252. 16. 25. 17.]]
Чтобы отфильтровать значения, нужно создать маски:
# Маска для отрицательных значений
negative_values = orders < 0
print(orders[negative_values])
# => [ -7. -21.]
# Маска для больших значений
big_values = orders > 200
print(orders[big_values])
# => [1000. 503. 252.]
# Маска для пропущенных значений и подсчета их количества
nan_values = np.isnan(orders)
print(sum(sum(nan_values)))
# => 5
Чтобы найти средние значения, нужно оставить только стандартные значения продаж. Сделать это можно маской, объединяющей логические отрицания сформированных масок:
# Маска стандартных значений
normal_mask = ~negative_values & ~big_values & ~nan_values
# Стандартные значения заказов
normal_values = orders[normal_mask]
# Среднее значение для стандартных заказов
normal_mean = normal_values.mean()
normal_mean = int(normal_mean)
print(normal_mean)
# => 10
Далее остается только заменить нестандартные значения на величину среднего:
# Массив, в котором нестандартные значения заменены на среднее
prepared_orders = np.where(normal_mask, orders, normal_mean)
print(prepared_orders)
# => [[ 7. 1. 10. 10.]
# [10. 2. 4. 10.]
# [ 3. 10. 10. 3.]
# [ 8. 12. 8. 7.]
# [15. 11. 10. 9.]
# [10. 18. 17. 10.]
# [10. 16. 25. 17.]]
Выводы
В этом уроке мы разобрали методы поиска и отбора значений по определенному условию, которые часто применяются при работе с данными. Мы узнали, как искать элементы, неудовлетворяющие некоторому условию, а также отсутствующие элементы и пропуски. Это самые частые случаи при работе с реальными данными, поэтому важно знать, как с ним работать.
Все рассмотренные методы применимы к массивам различной размерности. На примере матрицы заказов мы продемонстрировали подходы по поиску и исправлению некорректных значений в данных.
Самостоятельная работа
Нажмите, чтобы увидеть тестовые данные
clicks_values = [
[319, -265, 319, 328],
[292, 274, 292, 301],
[283, 301, 274, 283],
[328, 364, 328, None],
[391, 355, 373, 337],
[445, 418, 409, 445],
[481, 400, 481, 409],
[None, 267, 333, 344],
[300, 278, 300, 311],
[289, 311, -278, 289],
[344, 388, 344, 333],
[421, 377, 399, 355],
[487, 454, -443, 487],
[531, 432, 531, 443],
[312, 264, 312, 320],
[288, None, 288, 296],
[280, 296, 272, -280],
[320, 352, 320, 312],
[376, 344, 360, 328],
[-424, 400, 392, 424],
[456, 384, 456, 392],
[347, 269, 347, 360],
[308, 282, 308, 321],
[295, 321, 282, None],
[360, 412, 360, 347],
[None, 399, 425, 373],
[529, 490, 477, 529],
[581, 464, 581, -477]
]
clicks_values = np.array(clicks_values, dtype=np.float64)
clicks_values
Важная часть работы с данными — это поиск ошибок, которые могут в них содержаться. При больших объемах данных проверку невозможно проводить вручную, поэтому важно научиться работать с автоматизацией. Сегодня мы создадим утилиту для анализа и корректировки данных о продажах ноутбуков. Предварительно обнаружены два типа ошибок: отрицательные значения и пропуски.
Пройдем по всему процессу пошагово:
Шаг 1. Напишите функцию get_negative_values_number(clicks_values: np.ndarray)
, которая возвращает количество отрицательных значений в данных.
Нажмите, чтобы увидеть ответ
def test(negative_values_number):
expected_value = 6
# shape
assert negative_values_number == expected_value
def get_negative_values_number(clicks_values):
return len(clicks_values[clicks_values < 0])
negative_values_number = get_negative_values_number(clicks_values)
test(negative_values_number)
Шаг 2. Напишите функцию get_nan_values_number(clicks_values: np.ndarray)
, которая возвращает количество пропусков (np.nan
) значений в данных.
Нажмите, чтобы увидеть ответ
def test(negative_values_number):
expected_value = 5
# shape
assert negative_values_number == expected_value
def get_nan_values_number(clicks_values):
return len(clicks_values[np.isnan(clicks_values)])
nan_values_number = get_nan_values_number(clicks_values)
test(nan_values_number)
Шаг 3. Анализ отрицательных значений выявил баг — он возникает, когда оператор зажимает клавишу Shift. При этом вводимое значение по модулю (то есть без знака) является правильным. Напишите функцию fillneg_values_number(clicks_values: np.ndarray)
, которая возвращает массив с исправленными отрицательными значениями на абсолютные.
Нажмите, чтобы увидеть ответ
def test(corr_neg_clicks_values):
# num negatives
assert len(corr_neg_clicks_values[corr_neg_clicks_values < 0]) == 0
# abs values
assert (corr_neg_clicks_values + clicks_values == 0).sum() == len(clicks_values[clicks_values < 0])
def fillneg_values_number(clicks_values):
return np.where(clicks_values < 0, abs(clicks_values), clicks_values)
corr_neg_clicks_values = fillneg_values_number(clicks_values)
test(corr_neg_clicks_values)
Шаг 4. С пропущенными значениями баг оказался серьезнее. Команда аналитиков решила, что лучше заполнить пропуски нулевыми значениями. Напишите функцию fillna_values_number(clicks_values: np.ndarray)
, которая будет возвращать массив, где пропуски заполнены нулями.
Нажмите, чтобы увидеть ответ
def test(corr_nan_clicks_values):
# num nan
assert np.isnan(corr_nan_clicks_values).sum() == 0
# fils values
assert len(corr_nan_clicks_values[corr_nan_clicks_values == 0]) == len(clicks_values[np.isnan(clicks_values)])
def fillna_values_number(clicks_values):
return np.where(np.isnan(clicks_values), 0, clicks_values)
corr_nan_clicks_values = fillna_values_number(clicks_values)
test(corr_nan_clicks_values)
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.