Check if a large matrix is diagonal matrix in python
We can actually do quite a bit better than what Daniel F suggested:
import numpy as np
import time
a = np.diag(np.random.random(19999))
t1 = time.time()
np.all(a == np.diag(np.diagonal(a)))
print(time.time()-t1)
t1 = time.time()
b = np.zeros(a.shape)
np.fill_diagonal(b, a.diagonal())
np.all(a == b)
print(time.time()-t1)
results in
2.5737204551696777
0.6501829624176025
One tricks is that np.diagonal(a)
actually uses a.diagonal()
, so we use that one directly. But what takes the cake the the fast build of b
, combined with the in-place operation on b
.
Not sure how fast this is compared to the others, but:
def isDiag(M):
i, j = np.nonzero(M)
return np.all(i == j)
EDIT Let's time things:
M = np.random.randint(0, 10, 1000) * np.eye(1000)
def a(M): #donkopotamus solution
return np.count_nonzero(M - np.diag(np.diagonal(M)))
%timeit a(M)
100 loops, best of 3: 11.5 ms per loop
%timeit is_diagonal(M)
100 loops, best of 3: 10.4 ms per loop
%timeit isDiag(M)
100 loops, best of 3: 12.5 ms per loop
Hmm, that's slower, probably from constructing i
and j
Let's try to improve the @donkopotamus solution by removing the subtraction step:
def b(M):
return np.all(M == np.diag(np.diagonal(M)))
%timeit b(M)
100 loops, best of 3: 4.48 ms per loop
That's a bit better.
EDIT2 I came up with an even faster method:
def isDiag2(M):
i, j = M.shape
assert i == j
test = M.reshape(-1)[:-1].reshape(i-1, j+1)
return ~np.any(test[:, 1:])
This isn't doing any calculations, just reshaping. Turns out reshaping to +1 rows on a diagonal matrix puts all the data in the first column. You can then check a contiguous block for any nonzeros which is much fatser for numpy
Let's check times:
def Make42(m):
b = np.zeros(m.shape)
np.fill_diagonal(b, m.diagonal())
return np.all(m == b)
%timeit b(M)
%timeit Make42(M)
%timeit isDiag2(M)
100 loops, best of 3: 4.88 ms per loop
100 loops, best of 3: 5.73 ms per loop
1000 loops, best of 3: 1.84 ms per loop
Seems my original is faster than @Make42 for smaller sets
M = np.diag(np.random.randint(0,10,10000))
%timeit b(M)
%timeit Make42(M)
%timeit isDiag2(M)
The slowest run took 35.58 times longer than the fastest. This could mean that an intermediate result is being cached.
1 loop, best of 3: 335 ms per loop
<MemoryError trace removed>
10 loops, best of 3: 76.5 ms per loop
And @Make42 gives memory error on the larger set. But then I don't seem to have as much RAM as they do.
Remove the diagonal and count the non zero elements:
np.count_nonzero(x - np.diag(np.diagonal(x)))
Appproach #1 : Using NumPy strides
/np.lib.stride_tricks.as_strided
We can leverage NumPy strides
to give us the off-diag elements as a view. So, no memory overhead there and virtually free runtime! This idea has been explored before in this post
.
Thus, we have -
# https://stackoverflow.com/a/43761941/ @Divakar
def nodiag_view(a):
m = a.shape[0]
p,q = a.strides
return np.lib.stride_tricks.as_strided(a[:,1:], (m-1,m), (p+q,q))
Sample run to showcase its usage -
In [175]: a
Out[175]:
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
In [176]: nodiag_view(a)
Out[176]:
array([[ 1, 2, 3, 4],
[ 6, 7, 8, 9],
[11, 12, 13, 14]])
Let's verify the free runtime and no memory overhead claims, by using it on a large array -
In [182]: a = np.zeros((10000,10000), dtype=int)
...: np.fill_diagonal(a,np.arange(len(a)))
In [183]: %timeit nodiag_view(a)
6.42 µs ± 48.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [184]: np.shares_memory(a, nodiag_view(a))
Out[184]: True
Now, how do we use it here? Simply check if all nodiag_view
elements are 0s, signalling a diagonal matrix!
Hence, to solve our case here, for an input array a
, it would be -
isdiag = (nodiag_view(a)==0).all()
Appproach #2 : Hacky way
For completeness, one hacky way would be to temporarily save diag elements, assign 0s
there, check for all elements to be 0s. If so, signalling a diagonal matrix. Finally assign back the diag elements.
The implementation would be -
def hacky_way(a):
diag_elem = np.diag(a).copy()
np.fill_diagonal(a,0)
out = (a==0).all()
np.fill_diagonal(a,diag_elem)
return out
Benchmarking
Let's time on a large array and see how these compare on performance -
In [3]: a = np.zeros((10000,10000), dtype=int)
...: np.fill_diagonal(a,np.arange(len(a)))
In [4]: %timeit (nodiag_view(a)==0).all()
52.3 ms ± 393 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [5]: %timeit hacky_way(a)
51.8 ms ± 250 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Other approaches from @Daniel F's post that captured other approaches -
# @donkopotamus solution improved by @Daniel F
def b(M):
return np.all(M == np.diag(np.diagonal(M)))
# @Daniel F's soln without assert check
def isDiag2(M):
i, j = M.shape
test = M.reshape(-1)[:-1].reshape(i-1, j+1)
return ~np.any(test[:, 1:])
# @Make42's soln
def Make42(m):
b = np.zeros(m.shape)
np.fill_diagonal(b, m.diagonal())
return np.all(m == b)
Timings with same setup as earlier -
In [6]: %timeit b(a)
...: %timeit Make42(a)
...: %timeit isDiag2(a)
218 ms ± 1.68 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
302 ms ± 1.25 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
67.1 ms ± 1.35 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)