Логические операции
Contents
Логические операции#
Сравнение массивов 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
. Такие массивы булевых значений часто называют масками.
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
Получили значение, очень близкое к ожидаемому.