1

I am trying to implement a function that will shift a particular element in a numpy 2d array a desired number of spaces in a given direction. Empty spaces should be filled with 0's. This function will take as input a numpy array, the x and y coordinates, a desired direction, and the number of spaces to shift.

For example, where shift is the hypothetical function:

arr = np.array([1, 1, 1], [1, 1, 1], [1, 1, 1])
arr out: [[1, 1, 1],
          [1, 1, 1],
          [1, 1, 0]]

shift(arr, 0, 0, 'right', 2)
arr out: [[0, 0, 1],
          [1, 1, 1],
          [1, 1, 0]]

shift(arr, 0, 2, 'down', 1)
arr out: [[0, 0, 0],
          [1, 1, 1],
          [1, 1, 1]]

I have found that I can achieve the desired shifting of all elements of either a row or column along that row or column with numpy's roll function. However, this approach simply cycles elements back to the beginning of the same row or column and does not fill empty spaces with 0's. For example:

arr[:, 0] = np.roll(arr[:, 0], 1)
arr out: [[1, 0, 0],
          [0, 1, 1],
          [1, 1, 1]]

Any assistance is very much appreciated.

edit: The x and y coordinates are the coordinates of the element to be shifted within the 2d array. The the rest of the elements within the same row or column are then shifted with respect to that element in the desired direction. For example shift(arr, 2, 2, 'down', 1) shifts the elements in the column with respect to the element at (2, 2) down by 1. All input values may be assumed to be valid at all times.

edit: This problem differs from the one linked in that elements are shifted with respect to the element at the coordinates provided, and this shifting occurs in a nested array. Furthermore, this solution does not allow for shifting of elements either up or down within the same column.

martinsarif
  • 105
  • 3
  • 11
  • 1
    Possible duplicate of [Shift elements in a numpy array](https://stackoverflow.com/questions/30399534/shift-elements-in-a-numpy-array) – Arya McCarthy Mar 11 '18 at 17:51
  • What do you mean, x,y coordinates? – J...S Mar 11 '18 at 18:04
  • What if number of shift is larger than number of dimensions? – Mazdak Mar 11 '18 at 18:04
  • @J...S I updated the question with more information. – martinsarif Mar 11 '18 at 18:39
  • @AryaMcCarthy My problem differs from this one in that elements are shifted with respect to the element at the coordinates provided, and this shifting occurs in a nested array. – martinsarif Mar 11 '18 at 18:42
  • Can you just use `.roll` and then zero out everything that got looped back around? – scnerd Mar 12 '18 at 17:14
  • In your hypothetical example, initial array is all ones, yet, when printed, arr[2,2] = 0. Why? Also, from you shift "down" example, it follows that all rows are shifted and yet in your shift "right" example only first row is shifted. This is so weird. – AGN Gazer Mar 12 '18 at 17:23
  • @AGNGazer Thank you for pointing this out! I accidentally made a typo in the hypothetical function call. This should be shift(arr, 0, 2, 'down', 1) rather than shift(arr, 2, 2, 'down', 1). – martinsarif Mar 13 '18 at 16:31

2 Answers2

1

Just roll and then zero out data that got rotated around:

# Let direction = "down" and col = 0, n = 2
In [1]: i
Out[1]:
array([[1., 0., 0.],
       [1., 1., 1.],
       [0., 0., 1.]])

In [2]: i[:, col] = np.roll(i[:, col], n)

In [3]: i[:n, col] = 0

In [4]: i
Out[4]:
array([[0., 0., 0.],
       [0., 1., 1.],
       [1., 0., 1.]])

You'd need to implement the equivalent versions for the other three directions, but it'd just be variations of those two lines.

scnerd
  • 5,836
  • 2
  • 21
  • 36
1

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]]
jdehesa
  • 58,456
  • 7
  • 77
  • 121
  • How might I modify such an implementation to allow shifting of elements into empty positions in the array? – martinsarif Mar 13 '18 at 16:28
  • @martinsarif Not sure what you mean by that, maybe you could give an example. – jdehesa Mar 13 '18 at 16:30
  • For the sake of simplicity I will use a single dimensional array to demonstrated the elements of the row or column in which the shift operation is being performed. Consider [1, 0, 1, 0, 0]. Suppose that I were to shift the first element in this row or column 2 spaces towards the end. This would result in the following new row or column [0, 0, 1, 1, 0]. Notice how rather than shifting the 0 elements, the algorithm now treats these elements as if they were empty positions in the array. Furthermore, each element acts as a sort of sliding tile. Does this clarify what I was asking? – martinsarif Mar 13 '18 at 16:38
  • @martinsarif Not completely sure... So if I have a row `[1, 1, 0, 1, 0]` and decide I want to shift the first element two positions to the right, the result should be `[0, 0, 1, 1, 1]`? If that's the case, it seems like a rather different (and harder) problem... Also, are you arrays always made of `1` and `0` only? – jdehesa Mar 13 '18 at 16:52
  • You are correct on what the solution should be in that scenario. I only used 1's and 0's for the sake of simplicity, they could be any integer value. However, with 0's being treated as empty positions in the array. – martinsarif Mar 13 '18 at 16:58
  • I decided to create an entirely different question for this problem. Upon reflection I agree that this is sufficiently different to warrant its own post. – martinsarif Mar 13 '18 at 23:22
  • @martinsarif Since [the other question](https://stackoverflow.com/q/49267379/1782792) has been closed, I have added the "sliding tiles" function here. I have voted to reopen it, if that happens I will post the answer there. – jdehesa Mar 14 '18 at 11:19
  • This implementation seems to not work when attempting to move elements into 0 spaces without shifting any tiles. In these circumstances errors are generated such as ValueError: could not broadcast input array from shape (0) into shape (2) – martinsarif Mar 16 '18 at 02:19
  • @martinsarif Yeah, lots of corner cases here... I've edited it, I hope it's right this time, the case `shift(m, 2, 2, 'up', 1)` which didn't work before seems fine now. – jdehesa Mar 16 '18 at 10:24
  • Testing it this morning gives me errors such as "ValueError: shape mismatch: value array of shape (0,) could not be broadcast to indexing result of shape (1,)" when sliding elements down or right into empty spaces. – martinsarif Mar 16 '18 at 16:56
  • @martinsarif It would be easier to find out the issues if you gave a failing example. – jdehesa Mar 16 '18 at 17:56
  • An example of such an array in my test program is shift(m, 1, 1, 'down', 1). Also there seems to be an error in the code you submitted above, it never goes beyond the return statement. – martinsarif Mar 16 '18 at 20:30
  • @martinsarif Oh right looks like one line got accidentally deleted in my previous edition, I put it back. – jdehesa Mar 16 '18 at 21:36
  • I will test the could again and let you know if I see any issues. – martinsarif Mar 17 '18 at 16:53
  • I get 'ValueError: shape mismatch: value array of shape (0,) could not be broadcast to indexing result of shape (1,) ' when I execute shift(m, 0, 0, 'right', 2), but not shift(m, 0, 0, 'right', 1) or with shift(m, 0, 0, 'right', 3). These errors will also occur when moving in other directions. The exact circumstance that this error occurs seems to be when moving a tile into an empty cell directly adjacent to the last element in a row or column, where all other positions are empty and the last position contains an element. – martinsarif Mar 17 '18 at 21:11
  • It seems that it happens when moving into the second last element in a row or column regardless of whether or not that element is a 0 or not. This is the code I am using to brute force all possible values to this function: `for i in range(0, m.shape[0]): for j in range(0, m.shape[1]): for n in range(1, m.shape[0]): print(shift(m, i, j, 'up', n)) print(shift(m, i, j, 'down', n)) print(shift(m, i, j, 'left', n)) print(shift(m, i, j, 'right', n))` – martinsarif Mar 17 '18 at 21:55
  • @martinsarif There we go again. I got to run your full loop with this one, so that's something. – jdehesa Mar 19 '18 at 10:17