NumPy: Get and set values in an array using various indexing

Posted: | Tags: Python, NumPy

This article explains how to get and set values, such as individual elements or subarrays (e.g., rows or columns), in a NumPy array (ndarray) using various indexing.

See the following articles for information on deleting, concatenating, and adding to ndarray.

The NumPy version used in this article is as follows. Note that functionality may vary between versions.

import numpy as np

print(np.__version__)
# 1.26.1

Basics of selecting values in an ndarray

Individual elements or subarrays (such as rows or columns) in an ndarray can be selected by specifying their positions or ranges in each dimension with commas, as in [○, ○, ○, ...]. The trailing : can be omitted, making [○, ○, :, :] equivalent to [○, ○].

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_2d[1, 2])
# 6

print(a_2d[1])
# [4 5 6 7]

print(a_2d[:, 1])
# [1 5 9]

For a 2D array, [i] selects the ith row, and [:, i] selects the ith column (indexes start from 0). More details will be provided later.

Positions in each dimension can be specified not only as integers but also in other formats such as lists or slices, allowing for the selection of any subarray.

a_3d = np.arange(24).reshape(2, 3, 4)
print(a_3d)
# [[[ 0  1  2  3]
#   [ 4  5  6  7]
#   [ 8  9 10 11]]
# 
#  [[12 13 14 15]
#   [16 17 18 19]
#   [20 21 22 23]]]

print(a_3d[1, [True, False, True], ::2])
# [[12 14]
#  [20 22]]

Specify with integers

Positions (indexes) are specified as integers (int).

Indexes start from 0, and negative values can be used to specify positions from the end (-1 represents the last). Specifying a non-existent position results in an error.

a_1d = np.arange(10)
print(a_1d)
# [0 1 2 3 4 5 6 7 8 9]

print(a_1d[0])
# 0

print(a_1d[4])
# 4

print(a_1d[-1])
# 9

print(a_1d[-4])
# 6

# print(a_1d[100])
# IndexError: index 100 is out of bounds for axis 0 with size 10

# print(a_1d[-100])
# IndexError: index -100 is out of bounds for axis 0 with size 10

The same applies to multi-dimensional arrays. Positions are specified for each dimension.

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_2d[1, -1])
# 7

You can omit the specification of later dimensions.

In a 2D array, [i], equivalent to [i, :], selects the ith row as a 1D array, and [:, i] selects the ith column.

print(a_2d[1])
# [4 5 6 7]

print(a_2d[1, :])
# [4 5 6 7]

print(a_2d[:, 1])
# [1 5 9]

Specify with slices

Ranges can be selected with slices (start:end:step).

Example with a 1D array:

a_1d = np.arange(10)
print(a_1d)
# [0 1 2 3 4 5 6 7 8 9]

print(a_1d[2:7])
# [2 3 4 5 6]

print(a_1d[:7])
# [0 1 2 3 4 5 6]

print(a_1d[2:])
# [2 3 4 5 6 7 8 9]

print(a_1d[2:7:2])
# [2 4 6]

Example with a 2D array:

The trailing : can be omitted.

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_2d[:2, 1:3])
# [[1 2]
#  [5 6]]

print(a_2d[:2, :])
# [[0 1 2 3]
#  [4 5 6 7]]

print(a_2d[:2])
# [[0 1 2 3]
#  [4 5 6 7]]

Using a slice i:i+1 selects a single row or column, preserving the array's dimensions, unlike selection with an integer (int), which reduces the dimensions.

print(a_2d[1:2])
# [[4 5 6 7]]

print(a_2d[1:2].shape)
# (1, 4)

print(a_2d[1])
# [4 5 6 7]

print(a_2d[1].shape)
# (4,)
print(a_2d[:, 2:3])
# [[ 2]
#  [ 6]
#  [10]]

print(a_2d[:, 2:3].shape)
# (3, 1)

print(a_2d[:, 2])
# [ 2  6 10]

print(a_2d[:, 2].shape)
# (3,)
print(a_2d[1:2, 2:3])
# [[6]]

print(a_2d[1:2, 2:3].shape)
# (1, 1)

print(a_2d[1, 2])
# 6

print(a_2d[1, 2].shape)
# ()

print(type(a_2d[1, 2]))
# <class 'numpy.int64'>

Slices preserve the original array's dimensions, while integers reduce them. This difference can affect outcomes or cause errors in operations like concatenation, even with the same range selected.

Specify with a list of Boolean values: Boolean indexing

Specifying a list or ndarray of Boolean values (True or False) matching the dimensions' sizes selects True positions, similar to masking.

Example with a 1D array:

An error occurs if the sizes do not match.

a_1d = np.arange(4)
print(a_1d)
# [0 1 2 3]

print(a_1d[[True, False, True, False]])
# [0 2]

print(a_1d[np.array([True, False, True, False])])
# [0 2]

# print(a_1d[[True, False]])
# IndexError: boolean index did not match indexed array along dimension 0; dimension is 4 but corresponding boolean dimension is 2

Example with a 2D array:

Rows or columns can be extracted using a slice :.

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_2d[[True, False, True]])
# [[ 0  1  2  3]
#  [ 8  9 10 11]]

print(a_2d[:, [True, False, True, False]])
# [[ 0  2]
#  [ 4  6]
#  [ 8 10]]

Note that specifying a list of Boolean values for multiple dimensions simultaneously does not yield the expected result. Using np.ix_() is necessary.

print(a_2d[[True, False, True], [True, False, True, False]])
# [ 0 10]

print(a_2d[np.ix_([True, False, True], [True, False, True, False])])
# [[ 0  2]
#  [ 8 10]]

As with slices, selecting a range of width 1 (a single row or column) preserves the original array's number of dimensions.

print(a_2d[:, [True, False, False, False]])
# [[0]
#  [4]
#  [8]]

A comparison on an ndarray yields a Boolean ndarray. Using this for indexing with [] selects True values, producing a flattened 1D array.

print(a_2d > 5)
# [[False False False False]
#  [False False  True  True]
#  [ True  True  True  True]]

print(a_2d[a_2d > 5])
# [ 6  7  8  9 10 11]

Specify multiple conditions using & (AND), | (OR), and ~ (NOT) with parentheses (). Using and, or, not, or omitting parentheses results in an error.

print((a_2d > 5) & (a_2d < 10))
# [[False False False False]
#  [False False  True  True]
#  [ True  True False False]]

print(a_2d[(a_2d > 5) & (a_2d < 10)])
# [6 7 8 9]

For methods of extracting rows or columns that meet certain conditions using Boolean indexing, refer to the following article.

Specify with a list of integers: fancy indexing

It is also possible to select ranges with a list or ndarray of integers.

Example with a 1D array:

Order can be inverted or repeated, and using negative values is allowed. Essentially, it involves creating a new array by selecting specific positions from the original array.

a_1d = np.arange(4)
print(a_1d)
# [0 1 2 3]

print(a_1d[[0, 2]])
# [0 2]

print(a_1d[[3, 3, 3, -3, 0]])
# [3 3 3 1 0]

print(a_1d[np.array([3, 3, 3, -3, 0])])
# [3 3 3 1 0]

Example with a 2D array:

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_2d[[0, 2]])
# [[ 0  1  2  3]
#  [ 8  9 10 11]]

print(a_2d[:, [0, 3]])
# [[ 0  3]
#  [ 4  7]
#  [ 8 11]]

As with Boolean indexing, specifying lists for multiple dimensions simultaneously does not yield the expected result. Using np.ix_() is necessary.

print(a_2d[[0, 2], [0, 3]])
# [ 0 11]

print(a_2d[np.ix_([0, 2], [0, 3])])
# [[ 0  3]
#  [ 8 11]]

As in the 1D example, the order can be inverted or repeated, and negative values are also permissible.

print(a_2d[np.ix_([2, 1, 1, -1, -1], [0, 2, 1, 3])])
# [[ 8 10  9 11]
#  [ 4  6  5  7]
#  [ 4  6  5  7]
#  [ 8 10  9 11]
#  [ 8 10  9 11]]

When selecting with a list of one element, the original array's number of dimensions is preserved, in contrast to specifying with an integer.

print(a_2d[:, [1]])
# [[1]
#  [5]
#  [9]]

print(a_2d[:, [1]].shape)
# (3, 1)

print(a_2d[:, 1])
# [1 5 9]

print(a_2d[:, 1].shape)
# (3,)

Combine different specification formats

Different formats can be used to specify each dimension.

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_2d[1, 1:3])
# [5 6]

print(a_2d[1, [False, True, True, False]])
# [5 6]

print(a_2d[1, [1, 2]])
# [5 6]

print(a_2d[1:, 1:3])
# [[ 5  6]
#  [ 9 10]]

print(a_2d[1:, [False, True, True, False]])
# [[ 5  6]
#  [ 9 10]]

print(a_2d[1:, [1, 2]])
# [[ 5  6]
#  [ 9 10]]

A combination of a list of Boolean values and a list of integers requires the use of np.ix_().

print(a_2d[[False, True, True], [1, 2]])
# [ 5 10]

print(a_2d[np.ix_([False, True, True], [1, 2])])
# [[ 5  6]
#  [ 9 10]]

Note that np.ix_() can only accept 1D lists or arrays.

For example, when specifying multiple lists for arrays of three dimensions or more, np.ix_() is required. However, it cannot be combined with integers or slices in the same selection operation.

a_3d = np.arange(24).reshape(2, 3, 4)
print(a_3d)
# [[[ 0  1  2  3]
#   [ 4  5  6  7]
#   [ 8  9 10 11]]
# 
#  [[12 13 14 15]
#   [16 17 18 19]
#   [20 21 22 23]]]

# print(a_3d[np.ix_(0, [True, True, False], [0, 2])])
# ValueError: Cross index must be 1 dimensional

# print(a_3d[np.ix_(:1, [True, True, False], [0, 2])])
# SyntaxError: invalid syntax

Integers can be specified as lists containing a single element. In this case, the resulting array retains the same number of dimensions as the original ndarray.

print(a_3d[np.ix_([0], [True, True, False], [0, 2])])
# [[[0 2]
#   [4 6]]]

You can use range() to achieve similar functionality as slices. For example, to simulate slicing, retrieve the size of the target dimension using the shape attribute and pass it to range() as in range(a.shape[n])[::].

print(a_3d[np.ix_(range(a_3d.shape[0])[:1], [True, True, False], [0, 2])])
# [[[0 2]
#   [4 6]]]

print(list(range(5)[::2]))
# [0, 2, 4]

print(list(range(5)[1:3]))
# [1, 2]

Assign new values to selected ranges

You can assign new values to selected ranges in an ndarray.

Specifying a scalar value on the right side assigns that value to all elements in the selected range on the left side.

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

a_2d[0, 0] = 10
print(a_2d)
# [[10  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

a_2d[1] = 100
print(a_2d)
# [[ 10   1   2   3]
#  [100 100 100 100]
#  [  8   9  10  11]]

a_2d[np.ix_([False, True, True], [1, 3])] = 1000
print(a_2d)
# [[  10    1    2    3]
#  [ 100 1000  100 1000]
#  [   8 1000   10 1000]]

Arrays can also be specified on the right side.

If the shape of the selected range on the left side matches that of the array on the right side, it is directly assigned. Non-contiguous locations pose no problem.

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_2d[::2, 1:])
# [[ 1  2  3]
#  [ 9 10 11]]

print(np.arange(6).reshape(2, 3) * 100)
# [[  0 100 200]
#  [300 400 500]]

a_2d[::2, 1:] = np.arange(6).reshape(2, 3) * 100
print(a_2d)
# [[  0   0 100 200]
#  [  4   5   6   7]
#  [  8 300 400 500]]

If the shape of the selected range on the left side does not match that of the array on the right side, it is assigned through broadcasting.

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_2d[::2, 1:])
# [[ 1  2  3]
#  [ 9 10 11]]

print(np.arange(3) * 100)
# [  0 100 200]

a_2d[::2, 1:] = np.arange(3) * 100
print(a_2d)
# [[  0   0 100 200]
#  [  4   5   6   7]
#  [  8   0 100 200]]

An error occurs if the shapes cannot be broadcast.

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_2d[::2, 1:])
# [[ 1  2  3]
#  [ 9 10 11]]

print(np.arange(2) * 100)
# [  0 100]

# a_2d[::2, 1:] = np.arange(2) * 100
# ValueError: could not broadcast input array from shape (2,) into shape (2,3)

For more information on broadcasting, refer to the following article.

Views and copies

The specification format used for each dimension when selecting subarrays determines whether a view or a copy of the original array is returned.

For example, using slices returns a view.

Whether two arrays refer to the same memory can be checked using np.shares_memory().

a = np.arange(6).reshape(2, 3)
print(a)
# [[0 1 2]
#  [3 4 5]]

a_slice = a[:, :2]
print(a_slice)
# [[0 1]
#  [3 4]]

print(np.shares_memory(a, a_slice))
# True

In the case of a view, changing the value in the selected subarray also changes the value in the original array, and vice versa.

a_slice[0, 0] = 100
print(a_slice)
# [[100   1]
#  [  3   4]]

print(a)
# [[100   1   2]
#  [  3   4   5]]

a[0, 0] = 0
print(a)
# [[0 1 2]
#  [3 4 5]]

print(a_slice)
# [[0 1]
#  [3 4]]

Boolean indexing or fancy indexing returns a copy.

a_boolean_index = a[:, [True, False, True]]
print(a_boolean_index)
# [[0 2]
#  [3 5]]

print(np.shares_memory(a, a_boolean_index))
# False

In this case, changing the value in the selected subarray does not affect the original array, and vice versa.

a_boolean_index[0, 0] = 100
print(a_boolean_index)
# [[100   2]
#  [  3   5]]

print(a)
# [[0 1 2]
#  [3 4 5]]

To create a copy of a subarray selected with a slice and process it separately from the original ndarray, use the copy() method.

a_slice_copy = a[:, :2].copy()
print(a_slice_copy)
# [[0 1]
#  [3 4]]

print(np.shares_memory(a, a_slice_copy))
# False

a_slice_copy[0, 0] = 100
print(a_slice_copy)
# [[100   1]
#  [  3   4]]

print(a)
# [[0 1 2]
#  [3 4 5]]

When combining different specification formats, using Boolean or fancy indexing returns a copy.

a_fancy_index_slice = a[:, [0, 2]]
print(a_fancy_index_slice)
# [[0 2]
#  [3 5]]

print(np.shares_memory(a, a_fancy_index_slice))
# False

Using integers and slices returns a view.

a_scalar_slice = a[1, :2]
print(a_scalar_slice)
# [3 4]

print(np.shares_memory(a, a_scalar_slice))
# True

Related Categories

Related Articles