Indexing#

NumPy arrays can be indexed and sliced over, much like lists and other Python sequences. Specifically, there are three types of indexing available in NumPy: single indexing, slicing, and advanced indexing. The first two are quite similar to what we have seen in Python lists, but the third is a bit more advanced.

indexing

import numpy as np

Single indexing#

Single-element indexing for a vector (1d array) works exactly like that for other standard Python sequences.

  • A single element is extracted by giving its position in brackets.

  • The index must be between 0 and size-1.

  • Negative integers between -size and -1 can be used for indexing from the end of the array.

vector = np.array([2.1, -5.7, 2, 9.4, 8])

vector[0]
vector[2]
vector[-1]

print(vector)
print()
print("vector[ 0]:", vector[0])
print("vector[ 2]:", vector[2])
print("vector[-1]:", vector[-1])
[ 2.1 -5.7  2.   9.4  8. ]

vector[ 0]: 2.1
vector[ 2]: 2.0
vector[-1]: 8.0

NumPy also supports indexing for multidimensional arrays.

  • A single element is accessed by giving a tuple of integers, one for each axis.

  • For each axis, the integer must be between (0, shape[n]-1) or (-shape[n], -1).

matrix = np.array([[1,3,4],[2,5,8],[3,1,4],[7,1,9]])

matrix[1, 2]

print(matrix)
print()
print("matrix[1,2]:", matrix[1,2])
[[1 3 4]
 [2 5 8]
 [3 1 4]
 [7 1 9]]

matrix[1,2]: 8
volume = np.array([[[1,3,1],[2,5,1]], [[5,9,0],[5,7,3]]])

volume[1,0,-2]

print(volume)
print()
print("volume[1,0,-2]:", volume[1,0,-2])
[[[1 3 1]
  [2 5 1]]

 [[5 9 0]
  [5 7 3]]]

volume[1,0,-2]: 9

Note

For omitted axes, the ellipsis ... is a useful shorthand: tensor[..., 0] selects the first item on the last axis. Avoid chained indexing such as a[rows][cols] when you mean paired multidimensional indexing: the first operation creates an intermediate array and changes the semantics. Prefer a single expression such as a[rows, cols].

Slicing#

It is possible to extract a contiguous subarray using the slice notation:

  • vector[start:stop] - items from start to stop-1

  • vector[start:    ] - items from start to the end

  • vector[     :stop] - items from the beginning to stop-1

  • vector[     :    ] - all items (from the beginning to the end)

vector[1:3]
vector[ :4]
vector[2: ]

print(vector)
print()
print('vector[1:3] ->', vector[1:3])
print()
print('vector[ :4] ->', vector[:4])
print()
print('vector[2: ] ->', vector[2:])
[ 2.1 -5.7  2.   9.4  8. ]

vector[1:3] -> [-5.7  2. ]

vector[ :4] -> [ 2.1 -5.7  2.   9.4]

vector[2: ] -> [2.  9.4 8. ]

Slicing can be applied to any axis of a multidimensional array, and mixed with other types of indexing.

matrix[2,:]
matrix[1:3,:2]

print(matrix)
print()
print("matrix[2,:] or matrix[2]\n", matrix[2,:])
print()
print("matrix[1:3,:2]\n", matrix[1:3, :2])
[[1 3 4]
 [2 5 8]
 [3 1 4]
 [7 1 9]]

matrix[2,:] or matrix[2]
 [3 1 4]

matrix[1:3,:2]
 [[2 5]
 [3 1]]

Copying vs Sharing#

Slicing returns a view whenever possible. A view behaves like a regular array, except that its elements are shared with the array from which it was created. Use np.shares_memory(a, b) to test whether two arrays may affect the same data.

a = np.array([4, 10, 7, 2, 5])

b = a[1:3] # Shared data!

print("a:", a)
print("b:", b)
print("shares memory:", np.shares_memory(a, b))
a: [ 4 10  7  2  5]
b: [10  7]
shares memory: True

Pay attention to this important detail.

Any modification to a “sliced array” is actually performed on the array from which it was created.

b[0] = -1 # this also modifies the "original" array
a: [ 4 -1  7  2  5]
b: [-1  7]

To explicitly make an independent copy of sliced elements, call copy().

a = np.array([4, 10, 7, 2, 5])

b = a[1:3].copy() # Copied data!

b[0] = -1 # this does NOT modify the "original" array

print("a:", a)
print("b:", b)
print("shares memory:", np.shares_memory(a, b))
a: [ 4 10  7  2  5]
b: [-1  7]
shares memory: False

Remark. A simple assignment makes neither a copy, nor a view of the original array. It just creates a new alias (i.e., a reference) to the assigned array.

a = np.array([4, 10, 7, 2, 5])

b = a # Same array!

print("Same array:", a is b)
Same array: True

Assigning values to sliced arrays#

Due to how slicing works, it is possible to assign values to a slice of an array. The only constraint is that the values being assigned to the sliced array must be shape consistent (the same shape, or broadcastable to the shape produced by slicing).

x = np.arange(10)

print("x:", x)
x: [0 1 2 3 4 5 6 7 8 9]

For example, it is permitted to assign a constant to a slice.

x[2:7] = -10

print("x:", x)
x: [  0   1 -10 -10 -10 -10 -10   7   8   9]

Alternatively, it is permitted to assign an array of the right size.

x[2:7] = np.array([10, 20, 30, 40, 50])

print("x:", x)
x: [ 0  1 10 20 30 40 50  7  8  9]

In both cases, the original array x is modified in the positions specified by the slice.

Advanced indexing#

Vectors can be indexed by one array of integers.

  • The integer array holds the positions of the elements to be extracted.

  • This array can have an arbitrary size.

  • The selected values are copied into a new array with the same shape as the indexing array.

         +---+---+---+---+---+
   idx = | 1 | 1 | 3 | 8 | 5 |
         +---+---+---+---+---+

         +------+------+------+------+------+
x[idx] = | x[1] | x[1] | x[3] | x[8] | x[5] |
         +------+------+------+------+------+

Here is a concrete example of integer indexing.

np.random.seed(1)

# original vector
x = np.random.randint(0, 10, size=12)  

# integer vector of indices
idx = np.array([1,1,3,8,5])

# new vector with the selected values
y = x[idx]  

Hide code cell source

print("     x:", x)
print("   idx:", idx)
print("x[idx]:", y)
     x: [5 8 9 5 0 0 1 7 6 9 2 4]
   idx: [1 1 3 8 5]
x[idx]: [8 8 5 6 0]

Matrices can be indexed by two arrays of integers.

  • The indexing arrays hold the row/column positions of the elements to be extracted.

  • The indexing arrays can be any (broadcastable) shape.

  • The selected values are copied into an array with the broadcast shape of the indexing arrays.

         +---+---+---+---+---+
     i = | 1 | 1 | 3 | 8 | 5 |
         +---+---+---+---+---+ 
         +---+---+---+---+---+
     j = | 2 | 4 | 5 | 3 | 7 |
         +---+---+---+---+---+

         +--------+--------+--------+--------+--------+
X[i,j] = | X[1,2] | x[1,4] | x[3,5] | x[8,3] | x[5,7] |
         +--------+--------+--------+--------+--------+

In general, multiple-axis arrays can be indexed by an array of integers for each axis.

a = np.arange(12).reshape(3,4)

i = np.array( [[0,1], [1,2]] )  # indices (2d array) for axis 0
j = np.array( [[2,1], [3,3]] )  # indices (2d array) for axis 1

b = a[i,j]   # i and j must have compatible shape

Hide code cell source

print("----- a -----")
print(a)
print("\n----- i -----")
print(i)
print("\n----- j -----")
print(j)
print("\n----- a[i,j] -----")
print(b)
----- a -----
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]

----- i -----
[[0 1]
 [1 2]]

----- j -----
[[2 1]
 [3 3]]

----- a[i,j] -----
[[ 2  5]
 [ 7 11]]

Arrays can be also indexed by one boolean array.

  • The boolean array indicates both the items to select (True) and to discard (False).

  • The boolean array must be the same shape as the original array.

  • The selected elements are copied into a return vector (1d array) whose length is the number of True entries.

          +----+----+----+----+----+----+----+----+
      x = | x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 |
          +----+----+----+----+----+----+----+----+
          
              0       1       2       3       4       5       6       7
          +-------+-------+-------+-------+-------+-------+-------+-------+
   mask = | False | True  | False | True  | False | False | True  | True  |
          +-------+-------+-------+-------+-------+-------+-------+-------+
  
  
          +----+----+----+----+
x[mask] = | x1 | x3 | x6 | x7 |
          +----+----+----+----+

The following are concrete examples of boolean indexing.

np.random.seed(1)

# original vector
x = np.random.randint(0, 10, size=8)  

# boolean vector of indices
mask = np.array([False, True, False, True, False, False, True, True])

# new vector with the selected values
y = x[mask]

Hide code cell source

print("      x:", x)
print("   mask:", mask)
print("x[mask]:", y)
      x: [5 8 9 5 0 0 1 7]
   mask: [False  True False  True False False  True  True]
x[mask]: [8 5 1 7]
matrix = np.arange(12).reshape(3,4)

mask = matrix > 5  # matrix of booleans (same shape)

vector = matrix[mask] # the result is always a vector

Hide code cell source

print("----- matrix -----")
print(matrix)
print("\n----- mask -----")
print(mask)
print("\n----- vector -----")
print(vector)
----- matrix -----
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]

----- mask -----
[[False False False False]
 [False False  True  True]
 [ True  True  True  True]]

----- vector -----
[ 6  7  8  9 10 11]

NumPy also supports masks matching leading dimensions. If a boolean mask has shape (d1, ..., dk) matching the first k dimensions of an array, it selects those leading positions and preserves the remaining dimensions. In the example below, a (2, 3) mask selects three positions from a (2, 3, 4) tensor, producing shape (3, 4).

tensor = np.arange(24).reshape(2,3,4)

mask = np.array([[True, False, True], 
                 [False, True, False]])

result = tensor[mask]

Hide code cell source

print("----- tensor -----")
print(tensor)
print("\n----- mask -----")
print(mask)
print("\n----- result -----")
print(result)
----- tensor -----
[[[ 0  1  2  3]
  [ 4  5  6  7]
  [ 8  9 10 11]]

 [[12 13 14 15]
  [16 17 18 19]
  [20 21 22 23]]]

----- mask -----
[[ True False  True]
 [False  True False]]

----- result -----
[[ 0  1  2  3]
 [ 8  9 10 11]
 [16 17 18 19]]

Exercises#

Solve the following exercises on array indexing and slicing. Then, proceed to the next section.

Quiz 8#

Create a vector of size 10 filled with zero, and set the first and last elements to 1.

Remark: The solution can take more than one line of code.

# YOUR CODE HERE
head_tail = None

Quiz 9#

Swap (in-place) the 2nd and 4th element of a vector.

Remark: Single indexing extracts one element from an array. In Python, single values are always copied when assigned to a variable.

# This is the vector to work with
vector = np.array([5, -1, 3, 12, 8, 0])

print("--- before swap ---")
print(vector)

# YOUR CODE HERE
None

print("\n--- after swap ---")
print(vector)
--- before swap ---
[ 5 -1  3 12  8  0]

--- after swap ---
[ 5 -1  3 12  8  0]

Quiz 10#

Given a matrix, extract the elements indicated in the figure below.

Hint: Recall that you can extract a contiguous part of a matrix via the slicing notation:

matrix[start_row : stop_row : step_row , start_col : stop_col : step_col]

slicing

# This is the matrix to work with
matrix = np.array([[ 1,  2,  3], 
                   [ 4,  5,  6], 
                   [ 7,  8,  9], 
                   [10, 11, 12]])

A = None # YOUR CODE HERE
B = None # YOUR CODE HERE
C = None # YOUR CODE HERE
D = None # YOUR CODE HERE
E = None # YOUR CODE HERE
F = None # YOUR CODE HERE
G = None # YOUR CODE HERE
H = None # YOUR CODE HERE