I have a numpy matrix like so:
array([[2, 1, 23, 32],
[34, 3, 3, 0],
[3, 33, 0, 0],
[32, 0, 0, 0]], dtype=int32)
Now
Here's a vectorized approach with masking -
valid_mask = a!=0
flipped_mask = valid_mask.sum(1,keepdims=1) > np.arange(a.shape[1]-1,-1,-1)
a[flipped_mask] = a[valid_mask]
a[~flipped_mask] = 0
Sample run -
In [90]: a
Out[90]:
array([[ 2, 1, 23, 32],
[34, 0, 3, 0], # <== Added a zero in between for variety
[ 3, 33, 0, 0],
[32, 0, 0, 0]])
# After code run -
In [92]: a
Out[92]:
array([[ 2, 1, 23, 32],
[ 0, 0, 34, 3],
[ 0, 0, 3, 33],
[ 0, 0, 0, 32]])
One more generic sample run -
In [94]: a
Out[94]:
array([[1, 1, 2, 3, 1, 0, 3, 0, 2, 1],
[2, 1, 0, 1, 2, 0, 1, 3, 1, 1],
[1, 2, 0, 3, 0, 3, 2, 0, 2, 2]])
# After code run -
In [96]: a
Out[96]:
array([[0, 0, 1, 1, 2, 3, 1, 3, 2, 1],
[0, 0, 2, 1, 1, 2, 1, 3, 1, 1],
[0, 0, 0, 1, 2, 3, 3, 2, 2, 2]])
Runtime test
Approaches that work on generic cases -
# Proposed in this post
def masking_based(a):
valid_mask = a!=0
flipped_mask = valid_mask.sum(1,keepdims=1) > np.arange(a.shape[1]-1,-1,-1)
a[flipped_mask] = a[valid_mask]
a[~flipped_mask] = 0
return a
# @Psidom's soln
def sort_based(a):
return a[np.arange(a.shape[0])[:, None], (a != 0).argsort(1, kind="mergesort")]
Timings -
In [205]: a = np.random.randint(0,4,(1000,1000))
In [206]: %timeit sort_based(a)
10 loops, best of 3: 30.8 ms per loop
In [207]: %timeit masking_based(a)
100 loops, best of 3: 6.46 ms per loop
In [208]: a = np.random.randint(0,4,(5000,5000))
In [209]: %timeit sort_based(a)
1 loops, best of 3: 961 ms per loop
In [210]: %timeit masking_based(a)
1 loops, best of 3: 151 ms per loop
Trivial attempt in non-numpy based python -
>>> arr = [[2, 1, 23, 32],
... [34, 3, 3, 0],
... [3, 33, 0, 0],
... [32, 0, 0, 0]]
...
>>> t_arr = [[0 for _ in range(cur_list.count(0))]\
+ [i for i in cur_list if i!=0]\
for cur_list in arr]
>>> t_arr
[[2, 1, 23, 32], [0, 34, 3, 3], [0, 0, 3, 33], [0, 0, 0, 32]]
You can also perform sorting on the masked array with the help of numpy.ma.sort() that sorts the array in-place along the last axis, axis=-1
as shown:
np.ma.array(a, mask=a!=0).sort()
Now a
becomes:
array([[ 2, 1, 23, 32],
[ 0, 34, 3, 3],
[ 0, 0, 3, 33],
[ 0, 0, 0, 32]])
The only downside is that it is not as fast as some of the approaches mentioned above but nevertheless a short one-liner to have.
A row roll based solution, in the spirit of @EDChum's
pandas version:
def rowroll(arr):
for row in arr:
row[:] = np.roll(row,-np.count_nonzero(row))
return arr
In [221]: rowroll(arr.copy())
Out[221]:
array([[ 2, 1, 23, 32],
[ 0, 34, 3, 3],
[ 0, 0, 3, 33],
[ 0, 0, 0, 32]])
np.count_nonzero
is a fast compiled way of finding the number of nonzeros. It is used by np.where
to find its return size.
But looking at the np.roll
code, I think it's overly complicated for the task, since it can work with several axes.
This looks messier, but I suspect it's as fast, if not faster than roll
:
def rowroll(arr):
for row in arr:
n = np.count_nonzero(row)
temp = np.zeros_like(row)
temp[-n:] = row[:n]
row[:] = temp
return arr
The roll
solutions require trailing 0s in the original, not scattered 0s.
pandas method:
In [181]:
# construct df from array
df = pd.DataFrame(a)
# call apply and call np.roll rowise and roll by the number of zeroes
df.apply(lambda x: np.roll(x, (x == 0).sum()), axis=1).values
Out[181]:
array([[ 2, 1, 23, 32],
[ 0, 34, 3, 3],
[ 0, 0, 3, 33],
[ 0, 0, 0, 32]])
This uses apply
so we can call np.roll
on each row by the number of zeroes in each row
You can also use numpy.argsort
with advanced indexing:
arr[np.arange(arr.shape[0])[:, None], (arr != 0).argsort(1, kind="mergesort")]
#array([[ 2, 1, 23, 32],
# [ 0, 34, 3, 3],
# [ 0, 0, 3, 33],
# [ 0, 0, 0, 32]], dtype=int32)