Shift Elements in a Numpy Array

Shift elements in a numpy array

For those who want to just copy and paste the fastest implementation of shift, there is a benchmark and conclusion(see the end). In addition, I introduce fill_value parameter and fix some bugs.

Benchmark

import numpy as np
import timeit

# enhanced from IronManMark20 version
def shift1(arr, num, fill_value=np.nan):
arr = np.roll(arr,num)
if num < 0:
arr[num:] = fill_value
elif num > 0:
arr[:num] = fill_value
return arr

# use np.roll and np.put by IronManMark20
def shift2(arr,num):
arr=np.roll(arr,num)
if num<0:
np.put(arr,range(len(arr)+num,len(arr)),np.nan)
elif num > 0:
np.put(arr,range(num),np.nan)
return arr

# use np.pad and slice by me.
def shift3(arr, num, fill_value=np.nan):
l = len(arr)
if num < 0:
arr = np.pad(arr, (0, abs(num)), mode='constant', constant_values=(fill_value,))[:-num]
elif num > 0:
arr = np.pad(arr, (num, 0), mode='constant', constant_values=(fill_value,))[:-num]

return arr

# use np.concatenate and np.full by chrisaycock
def shift4(arr, num, fill_value=np.nan):
if num >= 0:
return np.concatenate((np.full(num, fill_value), arr[:-num]))
else:
return np.concatenate((arr[-num:], np.full(-num, fill_value)))

# preallocate empty array and assign slice by chrisaycock
def shift5(arr, num, fill_value=np.nan):
result = np.empty_like(arr)
if num > 0:
result[:num] = fill_value
result[num:] = arr[:-num]
elif num < 0:
result[num:] = fill_value
result[:num] = arr[-num:]
else:
result[:] = arr
return result

arr = np.arange(2000).astype(float)

def benchmark_shift1():
shift1(arr, 3)

def benchmark_shift2():
shift2(arr, 3)

def benchmark_shift3():
shift3(arr, 3)

def benchmark_shift4():
shift4(arr, 3)

def benchmark_shift5():
shift5(arr, 3)

benchmark_set = ['benchmark_shift1', 'benchmark_shift2', 'benchmark_shift3', 'benchmark_shift4', 'benchmark_shift5']

for x in benchmark_set:
number = 10000
t = timeit.timeit('%s()' % x, 'from __main__ import %s' % x, number=number)
print '%s time: %f' % (x, t)

benchmark result:

benchmark_shift1 time: 0.265238
benchmark_shift2 time: 0.285175
benchmark_shift3 time: 0.473890
benchmark_shift4 time: 0.099049
benchmark_shift5 time: 0.052836

Conclusion

shift5 is winner! It's OP's third solution.

Shifting Elements in a Numpy Array

Here is a more or less comprehensive function to solve it:

def shift(a, i, j, dir, n, fill=0, inplace=False):
out = a
if not inplace:
out = a.copy()
if dir == 'down':
if n < 0:
return shift(out, i, j, 'up', -n, fill=fill, inplace=True)
n = min(n, a.shape[0] - i)
out[i+n:, j] = a[i:a.shape[0] - n, j]
out[i:i+n, j] = fill
elif dir == 'up':
if n < 0:
return shift(out, i, j, 'down', -n, fill=fill, inplace=True)
n = min(n, i+1)
out[:i+1-n, j] = a[n:i+1, j]
out[i+1-n:i+1, j] = fill
elif dir == 'right':
if n < 0:
return shift(out, i, j, 'left', -n, fill=fill, inplace=True)
n = min(n, a.shape[1] - j)
out[i, j+n:] = a[i, j:a.shape[1] - n]
out[i, j:j+n] = fill
elif dir == 'left':
if n < 0:
return shift(out, i, j, 'right', -n, fill=fill, inplace=True)
n = min(n, j+1)
out[i, :j+1-n] = a[i, n:j+1]
out[i, j+1-n:j+1] = fill
else:
raise ValueError('Unknown direction "{}".'.format(dir))
return out

Some tests:

import numpy as np

arr = np.arange(25).reshape((5, 5))
print(arr)
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 11 12 13 14]
# [15 16 17 18 19]
# [20 21 22 23 24]]
print(shift(arr, 2, 1, 'up', 2))
# [[ 0 11 2 3 4]
# [ 5 0 7 8 9]
# [10 0 12 13 14]
# [15 16 17 18 19]
# [20 21 22 23 24]]
print(shift(arr, 2, 1, 'left', -2))
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 0 0 11 12]
# [15 16 17 18 19]
# [20 21 22 23 24]]
print(shift(arr, 2, 1, 'down', 1, fill=100))
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [ 10 100 12 13 14]
# [ 15 11 17 18 19]
# [ 20 16 22 23 24]]
shift(arr, 2, 1, 'right', 3, inplace=True)
print(arr)
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 0 0 0 11]
# [15 16 17 18 19]
# [20 21 22 23 24]]

EDIT

Following discussion in comments, I add another function(s) to solve the problem of shifting "sliding tiles":

import numpy as np

def shift_vector(v, i, n, empty=0):
if n < 0:
return shift_vector(v[::-1], len(v) - i - 1, -n)[::-1]
if n < len(v) - i:
# Find n empty places after i
idx = np.where(np.cumsum(v[i + 1:] == empty) == n)[0]
last_zero_idx = idx[0] if len(idx) > 0 else len(v) - i - 1
# Get non-empty values
v_slice = v[i + 1:i + last_zero_idx + 1]
values = v_slice[np.where(v_slice != empty)[0]]
# Copy to vector
v[i + n] = v[i]
r = range(i + n + 1, min(i + last_zero_idx + 2, len(v)))
v[r] = values[:len(r)]
v[i:i + n] = empty
return v

def shift(a, i, j, dir, n, empty=0, inplace=False):
out = a
if not inplace:
out = a.copy()
if dir == 'down':
out[:, j] = shift_vector(out[:, j], i, n, empty=empty)
elif dir == 'up':
out[:, j] = shift_vector(out[:, j], i, -n, empty=empty)
elif dir == 'right':
out[i, :] = shift_vector(out[i, :], j, n, empty=empty)
elif dir == 'left':
out[i, :] = shift_vector(out[i, :], j, -n, empty=empty)
else:
raise ValueError('Unknown direction "{}".'.format(dir))
return out

m = np.array([[1, 0, 0, 2],
[3, 4, 0, 0],
[5, 0, 6, 7],
[0, 8, 9, 0]])
print("m")
print(m)
print("shift(m, 1, 0, 'right', 2)")
print(shift(m, 1, 0, 'right', 2))
print("shift(m, 3, 1, 'down', -2)")
print(shift(m, 3, 1, 'down', -2))
print("shift(m, 0, 3, 'left', 3)")
print(shift(m, 0, 3, 'left', 3))
print("shift(m, 2, 2, 'up', 1)")
print(shift(m, 2, 2, 'up', 1))

Output:

m
[[1 0 0 2]
[3 4 0 0]
[5 0 6 7]
[0 8 9 0]]
shift(m, 1, 0, 'right', 2)
[[1 0 0 2]
[0 0 3 4]
[5 0 6 7]
[0 8 9 0]]
shift(m, 3, 1, 'down', -2)
[[1 4 0 2]
[3 8 0 0]
[5 0 6 7]
[0 0 9 0]]
shift(m, 0, 3, 'left', 3)
[[2 0 0 0]
[3 4 0 0]
[5 0 6 7]
[0 8 9 0]]
shift(m, 2, 2, 'up', 1)
[[1 0 0 2]
[3 4 6 0]
[5 0 0 7]
[0 8 9 0]]

Shift values in numpy array by differing amounts

If you want to stick with NumPy, you can achieve this using np.unique by returning the counts per unique elements with the return_counts option.

Then, simply roll the values and construct a new array with np.repeat:

>>> s, i, c = np.unique(a, return_index=True, return_counts=True)
(array([ 2, 3, 7, 9, 15]), array([0, 3, 6, 8, 5]), array([3, 2, 2, 1, 1]))

The three outputs are respectively: unique sorted elements, indices of first encounter unique element, and the count per unique element.

np.unique sorts the value, so we need to unsort the values as well as the counts first. We can then shift the values with np.roll:

>>> idx = np.argsort(i)
>>> v = np.roll(s[idx], 1)
>>> v[0] = 0
array([ 0, 2, 3, 15, 7])

Alternatively with np.append, this requires a whole copy though:

>>> v = np.append([0], s[idx][:-1])
array([ 0, 2, 3, 15, 7])

Finally reassemble:

>>> np.repeat(v, c[idx])
array([ 0, 0, 0, 2, 2, 3, 15, 15, 7])

Another - more general - solution that will work when there are recurring values in a. This requires the use of np.diff.

You can get the indices of the elements with:

>>> i = np.diff(np.append(a, [0])).nonzero()[0] + 1
array([3, 5, 6, 8, 9])

>>> idx = np.append([0], i)
array([0, 3, 5, 6, 8, 9])

The values are then given using a[idx]:

>>> v = np.append([0], a)[idx]
array([ 0, 2, 3, 15, 7, 9])

And the counts per element with:

>>> c = np.append(np.diff(i, prepend=0), [0])
array([3, 2, 1, 2, 1, 0])

Finally, reassemble:

>>> np.repeat(v, c)
array([ 0, 0, 0, 2, 2, 3, 15, 15, 7])

Fastest way to shift a Numpy array

You could have the shifting and differentiating both done in a function call like so -

def syf1(A, E=True):
out = np.empty_like(A)
out[:-1] = A[1:] - A[:-1] # Or np.diff(my_array,axis=0)
if E == True:
out[-1] = 0
else:
out[-1] = -A[-1]
return out

Thus, the equivalent modified version of syf for runtime comparison purposes would be -

def syf(A, E=True):
if E == True:
return np.concatenate((A[1:], A[-1].reshape(Yshape)), axis=0) - A
else:
return np.concatenate((A[1:], Yzeros), axis=0) - A

Runtime tests

Let's compare the equivalent version of syf with the proposed approach on runtime performance for the inputs listed in the question code -

In [113]: %timeit syf(my_array)
1000 loops, best of 3: 518 µs per loop

In [114]: %timeit syf1(my_array)
1000 loops, best of 3: 494 µs per loop

So, there is some improvement there!

Shift a numpy array by an increasing value with each row

Assuming an array with arbitrary values, you could use:

# add enough "0" columns for the shift
arr2 = np.c_[arr, np.zeros((arr.shape[0], arr.shape[0]-1), dtype=arr.dtype)]
# get the indices as ogrid
r, c = np.ogrid[:arr2.shape[0], :arr2.shape[1]]
# roll the values
arr2 = arr2[r, c-r]

used input:

arr = np.arange(1,10).reshape(3,3)
# array([[1, 2, 3],
# [4, 5, 6],
# [7, 8, 9]])

output:

array([[1, 2, 3, 0, 0],
[0, 4, 5, 6, 0],
[0, 0, 7, 8, 9]])

How to shift the entire numpy array, with wrapping

Use numpy.roll.
For instance, for the first output you can roll 1 index to the right, meaning along axis 1:

import numpy as np
x = np.array([[0,1,2], [3,4,5], [6,7,8]])
x_shifted = np.roll(x, shift=1, axis=1)

Due to commutativity you can roll twice (once along each dimension) for the two-directional cyclic permutation effect:

x_double_shifted = np.roll(np.roll(x, shift=2, axis=1), shift=2, axis=0)

Obviously can be done more "pretty" ;-)

Good luck!

Shift rows of a numpy array independently

Inspired by Roll rows of a matrix independently's solution, here's a vectorized one based on np.lib.stride_tricks.as_strided -

from skimage.util.shape import view_as_windows as viewW

def strided_indexing_roll(a, r):
# Concatenate with sliced to cover all rolls
p = np.full((a.shape[0],a.shape[1]-1),np.nan)
a_ext = np.concatenate((p,a,p),axis=1)

# Get sliding windows; use advanced-indexing to select appropriate ones
n = a.shape[1]
return viewW(a_ext,(1,n))[np.arange(len(r)), -r + (n-1),0]

Sample run -

In [76]: a
Out[76]:
array([[4, 0, 0],
[1, 2, 3],
[0, 0, 5]])

In [77]: r
Out[77]: array([ 2, 0, -1])

In [78]: strided_indexing_roll(a, r)
Out[78]:
array([[nan, nan, 4.],
[ 1., 2., 3.],
[ 0., 5., nan]])

How to shift array symmetrically in numpy

You could add +1 to all elements that appear before zero or are equal to zero, and add -1 to all elements that follow zero. Here is one approach:

values = np.array([1, 0.34, 0.59, 0.40, 0.94])
a = np.array([0,1,2,3,4])
for i in range(a.size):
print(values[a])
a += 2 * (np.arange(a.size) <= a.argmin()) - 1

# [1. 0.34 0.59 0.4 0.94]
# [0.34 1. 0.34 0.59 0.4 ]
# [0.59 0.34 1. 0.34 0.59]
# [0.4 0.59 0.34 1. 0.34]
# [0.94 0.4 0.59 0.34 1. ]

Alternatively, you could use np.arange with negative range and take absolute values:

for i in range(0, -5, -1):
print(np.abs(np.arange(i,i+5)))

You could also generate a matrix of all shifted rows in one go:

mat = np.eye(5, dtype=int).cumsum(0).cumsum(0)
mat += np.rot90(mat, k=2) - np.eye(5, dtype=int) - 1

[[0 1 2 3 4]
[1 0 1 2 3]
[2 1 0 1 2]
[3 2 1 0 1]
[4 3 2 1 0]]

Move all array elements one space up in python NumPy

You could use numpy.roll() with a subset assignment:

arr = numpy.array([2,4,5,6])

arr[:3] = numpy.roll(arr[:3],1)

print(arr)
[5 2 4 6]


Related Topics



Leave a reply



Submit