Сравнения и логика: маски, any, all и логические функции
Урок про логику в NumPy: как сравнения порождают булевы массивы и как их агрегировать и комбинировать.
Булев массив — массив значений True/False, который возвращают поэлементные сравнения; он лежит в основе фильтрации, проверок и подсчётов.
Сравнения возвращают массив, а не одно значение
Операторы сравнения (== != < > <= >=) применяются поэлементно и дают булев массив той же формы. Это краеугольный камень всей логики NumPy: вместо «истинно ли условие» вы получаете «где именно оно истинно».
import numpy as np
a = np.array([1, 5, 3, 8, 2])
b = np.array([2, 5, 1, 7, 9])
print(a > 3) # [False True False True False]
print(a == b) # поэлементное равенство
print(a >= b) # [False True True True False]
Вывод:
[False True False True False] [False True False False False] [False True True True False]
any и all: свести массив к одному ответу
Часто нужно ответить на вопрос «есть ли хоть один элемент, удовлетворяющий условию?» или «все ли удовлетворяют?». Это делают any (хотя бы один True) и all (все True). Они тоже принимают axis, чтобы проверять вдоль оси.
import numpy as np
a = np.array([2, 4, 6, 8])
print((a > 0).all()) # True — все положительные
print((a > 5).any()) # True — есть элемент больше 5
print((a % 2 == 0).all()) # True — все чётные
m = np.array([[1, 2], [3, 4], [5, 6]])
print((m > 2).any(axis=1)) # в каждой строке есть >2?
Вывод:
True True True [False True True]
Идея any/all — это встроенные функции Python, поднятые на массивы:
a = [2, 4, 6, 8]
print(all(x > 0 for x in a)) # все положительные?
print(any(x > 5 for x in a)) # есть больше 5?
print(sum(1 for x in a if x > 5)) # сколько больше 5
Вывод:
True True 2
Почему сравнение даёт массив: модель мышления
Чтобы уверенно работать с логикой в NumPy, важно перестроить интуицию. В обычном Python x > 3 отвечает на вопрос «истинно ли это» одним True или False. В NumPy a > 3 отвечает на вопрос «для каких именно элементов это истинно» — и возвращает карту ответов, булев массив той же формы. Это сдвиг от «одного решения» к «решению для каждого элемента». Именно поэтому к массиву нельзя применить if a > 3 напрямую: интерпретатор не знает, как превратить целый массив ответов в одно True/False, и выдаёт ошибку «истинность массива неоднозначна». Чтобы свести карту к одному ответу, вы осознанно выбираете: «истинно ли хоть для одного» (any) или «истинно ли для всех» (all). Эта явность — не неудобство, а защита: она заставляет вас сформулировать, что именно вы спрашиваете о массиве, и предотвращает скрытые логические ошибки. Усвоив этот сдвиг — «сравнение порождает карту, а не вердикт», — вы перестанете спотыкаться на логике массивов.
Подсчёт совпадений: True как 1
В NumPy True при сложении ведёт себя как 1, а False как 0. Поэтому (условие).sum() — это число элементов, удовлетворяющих условию. А mean() булева массива — это доля True. Это идиоматичный способ считать совпадения без циклов.
import numpy as np
scores = np.array([55, 80, 42, 91, 67, 30, 88])
passed = scores >= 60
print(passed.sum()) # сколько сдали
print(passed.mean()) # доля сдавших
print((scores < 50).sum()) # сколько ниже 50
Вывод:
4 0.5714285714285714 2
any/all по оси: построчные проверки
С параметром axis функции any и all превращаются в инструмент построчных и постолбцовых проверок. (m > 0).all(axis=1) отвечает для каждой строки: «все ли её элементы положительны?» — и возвращает булев вектор длиной в число строк. Это мощный приём отбора: например, оставить только те строки таблицы, где все признаки заполнены (нет пропусков), — m[~np.isnan(m).any(axis=1)]. Или найти столбцы, где хоть один элемент превышает порог. Связка «проверка any/all по оси, затем отбор маской» — стандартный шаблон очистки данных. Важно не путать оси: как и в агрегациях, axis указывает ось, которая сворачивается. any(axis=1) сворачивает столбцы внутри каждой строки, давая по одному ответу на строку. Проговаривайте смысл так же, как с суммами, — это убережёт от перепутанных осей.
Комбинирование условий: операторы и функции
Как мы видели в разделе про индексацию, объединять условия нужно поэлементными операторами &, |, ~ (со скобками), а не and/or/not. У этих операторов есть функциональные аналоги: np.logical_and, np.logical_or, np.logical_not, np.logical_xor. Они делают то же самое, но иногда читаются яснее и удобны для передачи как функция.
import numpy as np
a = np.arange(10)
print(a[(a > 2) & (a < 7)]) # операторы
print(a[np.logical_and(a > 2, a < 7)]) # эквивалентная функция
print(np.logical_or(a < 2, a > 7)) # маска: меньше 2 или больше 7
print(np.logical_not(a > 5)) # инверсия условия
Вывод:
[3 4 5 6] [3 4 5 6] [ True True False False False False False False True True] [ True True True True True True False False False False]
Важное различие: &/| — это побитовые операторы. Для булевых массивов они работают как логические и-или, но для целочисленных массивов они выполнят побитовую операцию над числами, что почти наверняка не то, что вы хотели. np.logical_and же всегда трактует входы как условия (приводит к bool). Поэтому при малейшем сомнении в типе данных безопаснее logical_*.
Короткое замыкание против полного вычисления
Есть тонкое отличие any/all NumPy от встроенных. Питоновские any/all ленивы: они прекращают перебор, как только результат ясен (встретили первый True для any). NumPy-методы arr.any()/arr.all() в общем случае сначала вычисляют весь булев массив, а потом сворачивают его. Для уже готовой маски это нормально, но если вы пишете (expensive(arr) > 0).any(), то дорогое вычисление выполнится для всех элементов, даже если ответ определился бы на первом. На практике это редко важно — векторные операции и так быстры, — но в горячем коде с очень дорогим поэлементным условием иногда выгоднее иная организация. Знать об этом полезно, чтобы не удивляться, почему «раннего выхода» не происходит.
np.isnan, np.isinf и проверка особых значений
Сравнивать с nan через == бессмысленно: по стандарту IEEE 754 nan != nan. Поэтому для поиска пропусков есть специальные функции np.isnan, а для бесконечностей — np.isinf, np.isfinite. Это единственно правильный способ ловить «дыры» в данных.
import numpy as np
a = np.array([1.0, np.nan, 3.0, np.inf, 5.0])
print(np.isnan(a)) # [False True False False False]
print(np.isfinite(a)) # [ True False True False True]
print(a[np.isfinite(a)]) # оставить только нормальные числа
print(np.isnan(a).sum()) # сколько пропусков
Вывод:
[False True False False False] [ True False True False True] [1. 3. 5.] 1
Тонкость приоритетов: почему нужны скобки
Требование скобок вокруг условий — не каприз, а следствие приоритетов операторов в Python. Побитовые &, | имеют более высокий приоритет, чем операторы сравнения <, >, ==. Поэтому выражение a > 5 & a < 12 Python разбирает не как «(a>5) и (a<12)», а как a > (5 & a) < 12 — сначала вычислится 5 & a (побитовое И числа 5 с массивом), что почти наверняка не то, что вы хотели, и часто приводит к ошибке или бессмысленному результату. Скобки (a > 5) & (a < 12) навязывают правильный порядок: сначала сравнения, потом логическое И булевых масок. Это одна из самых частых ошибок новичков в NumPy, и она коварна тем, что иногда не падает, а молча даёт неверную маску. Возьмите за железное правило: каждое условие в составной маске — в свои скобки. Привычка избавит от целого класса труднонаходимых багов.
Подводные камни
- and/or с массивами. Дают «истинность массива неоднозначна». Используйте
&/|илиlogical_*. - &/| на целых массивах. Это побитовые операции над числами, а не логика. Для условий приводите к bool или берите
logical_*. - Сравнение с nan.
x == np.nanвсегда False. Ловите пропуски черезnp.isnan. - Скобки вокруг условий.
a>2 & a<7без скобок интерпретируется неверно из-за приоритета.
Логика как мост к фильтрации и where
Булевы массивы — не самоцель, а связующее звено всего NumPy. Они рождаются из сравнений, сворачиваются через any/all и подсчёты, комбинируются логическими операторами — и затем питают два мощнейших инструмента: продвинутую индексацию (a[mask] для фильтрации, разобранную в прошлом разделе) и условный выбор np.where(mask, x, y) (его подробно разберём в финальном разделе). По сути, маска — это «программа отбора», которую вы один раз формулируете, а потом применяете для чтения, записи, подсчёта и выбора. Поэтому уверенное владение логикой — это фундамент для всей условной обработки данных. Когда вы научитесь бегло строить и комбинировать маски, задачи вроде «обнулить выбросы», «посчитать долю прошедших порог», «заменить отрицательные нулём», «отобрать строки по нескольким условиям» будут решаться в одну-две строки без единого цикла. Это и есть характерный стиль зрелого кода на NumPy, который вы встретите в реальных проектах и в pandas, где та же логика масок работает поверх таблиц.
Лучшие практики
- Считайте совпадения через
(условие).sum()и доли через.mean()— без циклов. - Проверяйте «есть ли / все ли» через
any/all, при необходимости сaxis. - Для надёжной логики над неоднозначными типами берите
np.logical_and/or/not. - Особые значения (
nan,inf) проверяйте только функциямиisnan/isfinite.
Логика в NumPy — это не отдельная тема, а сквозной язык работы с данными. Сравнения порождают маски, маски сворачиваются в ответы и подсчёты, комбинируются в сложные условия и применяются для фильтрации и выбора. Освоив этот язык, вы решаете большинство задач отбора и условной обработки декларативно — описывая, что отобрать, а не как перебрать.
Итог
- Сравнения дают булев массив той же формы — основу фильтрации и подсчётов.
any/allсводят массив к ответу «хоть один / все»; работают и по оси.(условие).sum()считает совпадения,.mean()— их долю (True=1).- Комбинируйте через
&/|/~илиlogical_*;nanловите черезisnan, не через ==.