I am learning about methods like argwhere and nonzero in NumPy. It appears that thenumpy.nonzero(x) the function returns a tuple of one-dimensional ndarray objects so that the output of this function could be used for indexing.
I have not ready the C source code of nonzero because I don't know how to find it. However, I imagine that the nonzero function will construct an m by ndim ndarray object (for some m that depends on the input) that will hold the indices of nonzero elements of a. To verify that this guess is correct, I tried:
import numpy as np
from numpy.random import Generator, PCG64
rg = Generator(PCG64())
x = rg.integers(0,2,(10000,10000))
y = np.nonzero(x)
print(y[0].base is y[1].base)
z = y[0].base
print(type(z),z.shape)
print(np.array_equal(z[:,0].reshape(-1),y[0]))
print(np.array_equal(z[:,1].reshape(-1),y[1]))
which outputs:
True
<class 'numpy.ndarray'> (50005149, 2)
True
True
My interpretation of the above is that the nonzero function does construct a single m by ndim sized array.
There is also a np.argwhere(x) function. Unlike np.nonzero, it will return an m by ndim size array, not a tuple. Reason suggests that argwhere and nonzero would be practically the same, except that nonzero returns the output in a slightly different format. To my surprise, argwhere seems to be implemented as follows (according to: https://numpy.org/doc/stable/reference/generated/numpy.argwhere.html):
# nonzero does not behave well on 0d, so promote to 1d
if np.ndim(a) == 0:
a = shape_base.atleast_1d(a)
# then remove the added dimension
return argwhere(a)[:,:0]
return transpose(nonzero(a))
Because the nonzero function returns a tuple, it seems like the transpose operation in argwhere is going to require an extra copy for no reason. A quick experiment with a timer suggested that this copy does happen.
Question
Can someone explain why the argwhere function is implemented this way? For example, the alternative
def faster_argwhere(a):
# nonzero does not behave well on 0d, so promote to 1d
if np.ndim(a) == 0:
a = shape_base.atleast_1d(a)
# then remove the added dimension
a=a[:,:0]
return np.nonzero(a)[0].base
already seems better, since:
x = rg.integers(0,2,(10000,10000))
t_0 = time.perf_counter()
y = np.argwhere(x)
t_1 = time.perf_counter()
z = faster_argwhere(x)
t_2 = time.perf_counter()
print('elapsed time for argwhere:' + str(t_1-t_0) + ", and for other method:" + str(t_2-t_1))
print(np.array_equal(y,z))
yields:
elapsed time for argwhere:2.175326200000086, and for other method:1.7338391999999203
True