NumPy elementwise chaining comparison (a <= b <= c)

Viewed 73

Python support chaining comparison:

1<2<3<4

How to do that for NumPy?

If only 3 array, we can do this:

a = np.array([1,2,4,5,6])
b = np.array([2,6,1,5,6])
c = np.array([7,4,6,6,8])
np.logical_and((a <= b),(b <= c))

With 4 array, it becomes too burdensome since np.logical_and only accept 2 inputs.

a = np.array([1,2,4,5,6])
b = np.array([2,6,1,5,6])
c = np.array([7,4,6,6,8])
d = np.array([8,9,9,9,2])
np.logical_and((a <= b),(b <= c))  # work
np.logical_and((b <= c),(c <= d))  # work
np.logical_and((a <= b),(b <= c),(c <= d))  # not work

Edit: Slightly add complexity, on 2D:

a = np.array([[1,2,3],
              [4,5,6]])
b = np.array([[2,6,3],
              [1,5,6]])
c = np.array([[7,4,3],
              [6,6,8]])
d = np.array([[8,9,3],
              [9,9,2]])
4 Answers

You can use the reduce method from functools.

import functools
import numpy as np

a = np.array([1,2,4,5,6])
b = np.array([2,6,1,5,6])
c = np.array([7,4,6,6,8])
d = np.array([8,9,9,9,2])

arr = [a <= b, b <= c, c <= d]
result = functools.reduce(np.logical_and, arr)

EDIT

There's a cleaner way, proposed in the comments. Reduce method is already part of ufunc in numpy. Using it removes the need for additional imports. We can change the last line of the example above the following way.

result = np.logical_and.reduce(arr)

TLDR:

Use Kelly Bundy answer. No need for more import. Even work without importing numpy by yourself if you got the array for other package.

If only 3 array, use OP answer.

Explanation:

After running the test again and again, all three (OP, AndrejH, and Kelly Bundy) results is fast and inconsistent which one is the best. Someone with better profiler can do better benchmark.


Benchmark:

import functools
import numpy as np

a = np.random.randint(1,10,(1000,100))
b = np.random.randint(1,10,(1000,100))
c = np.random.randint(1,10,(1000,100))
d = np.random.randint(1,10,(1000,100))
# arbitrary number of chains
%timeit (a <= b) & (b <= c) & (c <= d)
%timeit functools.reduce(np.logical_and, [a <= b, b <= c, c <= d])
%timeit np.logical_and.reduce([a <= b, b <= c, c <= d])
%timeit np.all([a<=b, b<=c, c<=d], axis=0)
%timeit np.apply_along_axis(lambda arr: all(arr[1:] >= arr[:-1]), 0, np.stack([a, b, c, d]))
1.02 ms ± 166 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
930 µs ± 93.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
1.09 ms ± 11.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
1.52 ms ± 360 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
1.1 s ± 276 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# only up to three array
%timeit (a <= b) & (b <= c)
%timeit functools.reduce(np.logical_and, [a <= b, b <= c])
%timeit np.logical_and.reduce([a <= b, b <= c])
%timeit np.all([a <= b, b <= c], axis=0)
%timeit np.logical_and((a <= b), (b <= c))
%timeit np.apply_along_axis(lambda arr: all(arr[1:] >= arr[:-1]), 0, np.stack([a, b, c]))
927 µs ± 457 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
559 µs ± 25.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
733 µs ± 39.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
819 µs ± 42.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
585 µs ± 40.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
1.01 s ± 69.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

OLD BENCHMARK

Chaining (best: AndrejH)

%timeit functools.reduce(np.logical_and, [a <= b, b <= c, c <= d])  # AndrejH
6.27 µs ± 244 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit np.all([a<=b, b<=c, c<=d], axis=0)  # Quang Hoang
59.7 µs ± 8.37 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Chaining up to three array (best: OP)

%timeit np.logical_and((a <= b), (b <= c))  # OP
3.8 µs ± 339 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit functools.reduce(np.logical_and, [a <= b, b <= c])  # AndrejH
4.2 µs ± 75.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit np.all([a <= b, b <= c], axis=0)  # Quang Hoang
45.6 µs ± 6.13 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

For this particular case, you can stack the arrays and check if each column is sorted.

np.apply_along_axis(lambda arr: all(arr[1:] >= arr[:-1]), 0, np.stack([a, b, c, d]))
# array([ True, False, False,  True, False])

Simply use &:

(a <= b) & (b <= c) & (c <= d)

As the doc says:

The & operator can be used as a shorthand for np.logical_and on boolean ndarrays.

Related