Логические операции#

Сравнение массивов NumPy#

Операторы сравнения “<”, “<=”, “>”, “>=”, “==” и “!=” перегружены для массивов NumPy и работают аналогичные правила, как и для арифметических операторов. Результатом операции сравнения является NumPy массив булевых значений True и False.

import numpy as np

a = np.array([
    [1, 1],
    [-1, -1],
    [1, -1],
    [-1, 1]
])
print(a)
[[ 1  1]
 [-1 -1]
 [ 1 -1]
 [-1  1]]

Например, можно в следующей ячейке демонстрируется, как можно одной строкой кода узнать, какие элемента массива неотрицательны.

print(a >= 0)
[[ True  True]
 [False False]
 [ True False]
 [False  True]]

В результирующем булевом массиве на месте неотрицательных элементов стоит значение True, а в остальных позициях стоит False,

Как и в случае арифметических операторов, логические операции тоже осуществляются поэлементно. В предыдущем примере применялся broadcasting, т.к. двумерный массив сравнивался со скаляром 0.

В примере ниже тот же самый массив a сравнивается с массивом строкой [-1, 1]. В итоге все элементы первого столбца массива a сравниваются с числом -1, а все элементы второго — с числом 1.

b = np.array([1, -1])

print(a == b)
[[ True False]
 [False  True]
 [ True  True]
 [False False]]

В отличие от встроенных в python булевых значений, логические операции “И”, “ИЛИ” и “НЕ” (логическое отрицание) осуществляются операторами “|”, “&” и “~” соответственно, а не ключевыми словами and, or и not.

Note

В самом python операторы “|”, “&” и “~” определенны для целых чисел и представляют собой побитовые операции.

Логическое “И” — оператор “&”. В качестве примера найдем числа в диапазоне от 3 до 6.

x = np.arange(0, 10)

print(x)
between_3_and_6 = (x >= 3) & (x <= 6)
print(between_3_and_6)
[0 1 2 3 4 5 6 7 8 9]
[False False False  True  True  True  True False False False]

Логическое “ИЛИ” — оператор “|”. В качестве примера найдем числа снаружи предыдущего диапазона.

outside_of_3_and_6 = (x < 3) | (x > 6)
print(outside_of_3_and_6)
[ True  True  True False False False False  True  True  True]

Логическое “НЕ” — оператор “~”. В качестве примера найдем числа снаружи диапазона \([3, 6]\) как отрицание чисел внутри диапазона.

print(~between_3_and_6)
[ True  True  True False False False False  True  True  True]

Индексация булевымы массивами#

Массивы булевых значений могут быть использованы для индексации: если между парой квадратных скобок “[]” массива python указать массив булевых значений, то в качестве ответа возвращаются те значения массива, напротив которых стояло значение True. Такие массивы булевых значений часто называют масками.

../../_images/masks.svg
array = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

mask = np.array([
    [True, False, False],
    [False, False, True],
    [False, True, True]
])

array[mask]
array([1, 6, 8, 9])

Так как результаты логических операций над массивами NumPy приводят к как раз булевым маскам, то можно очень легко фильтровать значения массива.

rng = np.random.default_rng()
x = rng.normal(size=8)
print(x)
[-0.53883649  0.04717316 -0.88935288 -0.94240822  0.60245659 -0.30592793
  1.24714734  0.7061065 ]

Например, в ячейке выше генерируется массив из 20 случайных чисел из нормального распределения. Извлечь только положительные из него можно инструкцией.

print(x[x > 0])
[0.04717316 0.60245659 1.24714734 0.7061065 ]

Обратите внимание, что внутри пары квадратных скобок “[]” располагается выражение x > 0, которое при вычислении даёт массив булевых значений. Этот массив далее используется в качестве маски, что и приводит к тому, что в результате мы видим только положительные значения.

Сгенерируем выборку побольше и эмпирически проверим правило трех сигм, согласно которому с вероятностью \(\approx99.73\%\) нормально распределенная случайная величина располагается в интервале \((\mu - 3\sigma, \mu + 3\sigma)\), где \(\mu\) — математическое ожидание, \(\sigma\) — среднеквадратичное отклонение. Это значит, что при \(\mu=0\) и \(\sigma=1\) ожидается, что чуть меньше 3 чисел из выборки размером 10000 окажутся за пределами интервала \((-3, 3)\).

x = rng.normal(size=1000)
x[(x > 3) | (x < -3)]
array([3.07690946, 3.04144012, 3.1831983 ])

В результате численного эксперимента получилось ровно три числа, но стохастическая природа этого эксперимента не обещает, что при повторном проведении такого результата вы получите такие же результаты.

Можно индексировать булевыми масками вдоль осей.

array = rng.integers(0, 10, size=(4, 4))
print(array)
[[7 8 8 4]
 [0 3 0 3]
 [4 2 5 5]
 [4 8 7 6]]

Например, оставить только строки, напротив которых стоит True.

mask = np.array([True, False, True, True])
print(array[mask])
[[7 8 8 4]
 [4 2 5 5]
 [4 8 7 6]]

Или аналогично со столбцами.

array[:, mask]
array([[7, 8, 4],
       [0, 0, 3],
       [4, 5, 5],
       [4, 7, 6]], dtype=int64)

Агрегация булевых значений#

Функции np.any и np.all принимают на вход булевый массив и агрегируют значения в нем:

  • np.any возвращает True, если хотя бы один из элементов массива True или приводится к True;

  • np.all возвращает True, если все элементы массива True или приводятся к True.

По поведению эти функции похожи на функции на функции np.sum и np.prod.

print(mask)
print(f"any: {np.any(mask)}")
print(f"all: {np.all(mask)}")
[ True False  True  True]
any: True
all: False

Они также принимают опциональный аргумент axis.

np.all(mask, axis=1)
array([False, False,  True,  True])

Функция np.sum может просуммировать и булевый массив. Значение True интерпретируется как 1, значение False — как 0. Если mask — булевая маска, то np.sum(mask) может быть использовано для того, что подсчитать количество значений True в этой маске. То же самое верное и про np.mean — эта функция может использована для того, чтобы найти долю значений True в маске булевых значений.

В целях демонстрации вернемся к примеру с правилом трех сигм. Сгенерируем массив из миллиона распределенных по нормальному закону случайных чисел, составим маску тех значений, которые находятся внутри интервала \((-3\sigma, 3\sigma)\), и подсчитаем их долю во всём массиве.

x = rng.normal(size=1_000_000)
mask = (x > 3) | (x < -3)
print(mask.mean())
0.002716

Получили значение, очень близкое к ожидаемому.