Сравнения и логика: маски, 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, не через ==.
Проверьте себя
1. Что вернёт выражение (scores >= 60).sum() для массива оценок scores?
AСумму всех оценок не ниже 60
BКоличество элементов, удовлетворяющих условию (True считается как 1)
CTrue или False
DСреднее значение оценок
2. Почему для проверки пропусков нельзя писать a == np.nan?
AПотому что nan нельзя хранить в массиве
BПотому что по стандарту IEEE 754 nan не равен ничему, даже самому себе; нужна функция np.isnan
CПотому что == работает только с целыми числами
DПотому что nan автоматически превращается в ноль
3. В чём риск использования & для объединения условий над ЦЕЛОЧИСЛЕННЫМИ массивами?
AНикакого риска, & всегда работает как логическое И
B& для целых массивов выполнит побитовую операцию над числами, а не логическое И над условиями
C& вызовет ошибку для любых массивов
D& автоматически приведёт массивы к строкам
Поддержать проект