Manipulating, Operating On, and Mapping Over Arrays
In this section, we learn to manipulate, operate on, and map over NumPy arrays.
Array Manipulation Functions and Methods
NumPy has many powerful functions and methods for manipulating arrays. We cover only those most frequently useful to us, here; for a full list and documentation, see (NumPy Developers [2022] 2024).
Sorting
To sort an array, the np.sort(a)
function returns a sorted
copy of a
and the a.sort()
method will sort (mutate)
a
itself. For instance,
= np.array([6, -3, 0, 9, -6])
a # => [-6, -3, 0, 6, 9] (copy)
np.sort(a) # a: [-6, -3, 0, 6, 9] a.sort()
The function and the method have the same optional arguments, the
most useful of which is axis: int
, the
axis along which to sort. The default is -1
(i.e., the last dimension).
Transposing
The mathematical matrix transpose (i.e., swapping dimensions by flipping the matrix along its diagonal) can be obtained for a Python matrix via a few different techniques. The following three techniques neither mutate the original matrix nor return transposed copies; rather, they return a transposed view of the original matrix:
= np.array([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]) # 3x4
A # Transpose attribute (view)
A.T # Transpose method (view)
A.transpose() # Transpose function (view) np.transpose(A)
All three transpose statements return a \(4\times 3\) array view that prints as follows:
[[ 0, 4, 8],
[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11]]
The original array A
remains
the same and is linked to the transposed view objects. To get a
transposed copy, append the copy()
method to any of these
statements.
Unlike for matrices, a vector transpose view is no different than the
original vector. However, a row vector
(i.e., 2D array with first axis of length 1
) or a column vector (i.e., 2D array with second axis of
length 1
)
can be created by adding an axis to a vector. For instance,
= np.array([0, 1, 2, 3]) # A vector
a # => [0, 1, 2, 3] (same vector view)
a.T # => [[0, 1, 2, 3]] (1x4 row vector view)
a[np.newaxis, :] # => [[0], [1], [2], [3]] (4x1 column vector view) a[:, np.newaxis]
The following are the shapes of these objects:
a.shape
returns(4,)
(i.e., a 1D array of size \(4\))a[np.newaxis, :].shape
returns(1, 4)
(i.e., a 2D view of shape \(1\times 4\))a[:, np.newaxis].shape
returns(4, 1)
(i.e., a 2D view of shape \(4\times 1\))
Constructing row and column vectors will be important for computing mathematical matrix-vector multiplication. They can also be properly transposed back-and-forth between row and column vectors.
Reshaping
Transposing, as we have seen, is one way to reshape an array. Another
way is to use the np.reshape(a: np.ndarray, newshape: tuple)
function or the equivalent method for array a
, a.reshape(newshape: tuple)
.
Both return a view of the original array with its elements filling the
newly shaped array. The argument newshape
may be an int
, in which
case the array is flattened 1D array, or a tuple
following
the usual pattern of an array shape. The number of elements in the new
view must equal that of the original array. For instance,
= np.array([[0, 1, 2], [3, 4, 5]]) # A 2x3 matrix
A = A.reshape((3,2)) # => [[0, 1], [2, 3], [4, 5]] (3x2 view) Ar
This also provides a second way of forming a row or column vector view from a 1D array; for example,
= np.array([0, 1, 2])
a = np.reshape((1, len(a))) # 1x3 row vector view
a_row = np.reshape((len(a), 1)) # 3x1 column vector view a_col
Operations on Arrays and Broadcasting
The basic arithmetic operators \(+\), \(-\), \(\times\), and \(/\) can be applied to NumPy arrays with the
operators +
, -
, *
, and /
,
respectively. These operations are applied element-wise as the following example
demonstrates:
= np.array([0, 1, 2])
a = np.array([3, 4, 5])
b + b # => [3, 4, 7]
a - b # => [-3, -3, -3]
a * b # => [0, 4, 10]
a / b # => [0, 0.25, 0.4] a
Broadcasting
In the cases above, the array shapes matched exactly. However, it is convenient to be able to perform these types of operations on arrays of different size such that the smaller array dimensions are broadcast (i.e., stretched or copied) to fill in the portions of the array it is missing. The simplest case is for an operation between a 0D array (i.e., a scalar) and another array, as in the following cases:
= np.array([0, 1, 2])
a + 4 # = a + [4, 4, 4] => [4, 5, 6]
a - 4 # = a - [4, 4, 4] => [-4, -3, -2]
a * 4 # = a * [4, 4, 4] => [0, 4, 8]
a / 4 # = a / [4, 4, 4] => [0, 0.25, 0.5] a
Here the scalar 4
was broadcast
to match the (larger) a
array
with shape (3,)
and added
element-wise.
Broadcasting is quite general and works for operations between arrays of many dimensions. Dimensions of two arrays are compatible if they are of equal size or if one has size \(1\), in which case it can be broadcast. The dimensions are compared from last to first. If one array runs out of dimensions, the rest are treated as \(1\). Here are some examples of compatible array dimensions in each column:
Operations on arrays with compatible dimensions will be broadcast automatically. This is not only convenient, in most cases it is also much more efficient (in terms of memory usage and computation time) than constructing the arrays or executing loops.1 Therefore, we usually prefer broadcasting.
Matrix Multiplication
Matrix multiplication can be performed with the @
operator. For
instance, consider the matrices and column vector \[
A = \begin{bmatrix}
0 & 1 & 2 \\
3 & 4 & 5 \\
6 & 7 & 8
\end{bmatrix}\,,
\quad
B = \begin{bmatrix}
0 & 1 \\
2 & 3 \\
4 & 5
\end{bmatrix}\,,\text{ and}
\quad
\bm{x} = \begin{bmatrix}
0 \\ 1 \\ 2
\end{bmatrix}.
\] Further consider the following matrix products: \[
A B, \quad A \bm{x},\text{ and} \quad B^\top A \bm{x}.
\] The following code computes these products:
= np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) # 3x3
A = np.array([[0, 1], [2, 3], [4, 5]]) # 3x2
B = np.array([[0], [1], [2]]) # 3x1
x @ B # => [[10, 13], [28, 40], [46, 67]] (3x2 matrix)
A @ x # => [[5], [14], [23]] (3x1 column vector)
A @ A @ x # => [[120], [162]] (2x1 column vector) B.T
The @
operator is equivalent to the use of the np.matmul()
function. A related
function is np.dot(a, b)
, which
takes the dot product of a
and
b
. If a
and b
are matrices, this is equivalent to
a @ b
.
However, in this case np.matmul()
and a @ b
are preferred.
Other Matrix Operations
We have considered matrix transposes and multiplication. Other common
mathematical matrix operations include addition, subtraction, and scalar
multiplication. These are element-wise operations, so we can simply use
NumPy’s usual +
, -
, and *
operators,
respectively.
The multiplicative inverse \(A^{-1}\) of a matrix \(A\) can be computed with the np.linalg.inv()
function from the linalg
module. For example,
= np.array([[1, 0, 0], [0, 2, 0], [0, 0, 4]]) # 3x3
A # => [[1., 0., 0.], [0., 0.5, 0.], [0., 0., 0.25]] np.linalg.inv(A)
If the matrix is not invertible, the exception LinAlgError: Singular matrix
is
raised.
Element-Wise Mathematical Functions
NumPy has mathematical functions that automatically operate
element-wise on arrays. Trigonometric functions include np.sin()
, np.cos()
, and np.tan()
; exponential and logarithmic
functions include np.exp()
, np.log()
, and np.log10()
; hyperbolic functions
include np.sinh()
, np.cosh()
, and np.tanh()
; complex-number functions
include np.real()
, np.imag()
, and np.angle()
; rounding functions include
np.round()
,
np.ceil()
, and np.floor()
. All these functions operate
element-wise, as shown in the following example:
= np.linspace(0, 2*np.pi, 5)
x round(np.sin(x), 10) # => [0., 1., 0., -1., -0.] (round to 10 dec.)
np.round(np.cos(x), 10) # => [1., 0., -1., -0., 1.] (round to 10 dec.) np.
This element-wise operation is not only convenient, it is highly optimized. NumPy takes advantage of precompiled C functions for performing these operations, so they execute much faster than would a Python loop through each element. The element-wise operation is called vectorization (n.b., sometimes this is the term given to the sometimes-attendant optimization), and NumPy takes great advantage of this, which is one of its key features.
Mapping Over Arrays and Lambda Functions
As we have seen, NumPy includes many built-in functions that are vectorized (i.e., applied element-wise). Our own custom function and method definitions can (and often should) also be vectorized. Usually, nothing special is required because we can take advantage of NumPy’s built-in functions and broadcasting. For instance, consider the following example, which defines a Python function corresponding to \(x \mapsto \sqrt{x} - 1\):
def sqrt_m1(x: np.ndarray) -> np.ndaray:
return np.sqrt(x) - 1
Here np.sqrt(x)
is already
vectorized and the subtraction is automatically broadcast, so our sqrt_m1
is vectorized. Note that this
will be much faster than a for
loop
through the elements of x
.
In general, the application of a function to each element of an array
(or iterable) object is called mapping. In
plain Python, we can apply a function f
to each element of a list l
with the built-in function map(f, l)
. This
is effectively just a for
loop, which
is not particularly performant. Vectorization in NumPy allows us to
usually avoid for
loops or
equivalent calls to map()
.
Lambda Functions
At times, it is convenient to write an anonymous function, often called a lambda function, which is a function that need not be given a name (although it can be). Mathematically, a lambda function can be expressed as, for instance, \[ x \mapsto (x + 2)^3. \] The Python syntax for a corresponding lambda function is
lambda x: (x + 2) ** 3
A lambda function can be applied directy to an argument. For instance,
lambda x: (x + 2) ** 3)(1) ## => 27 (
It can also be given a name, as in
= lambda x: (x + 2) ** 3
f 1) # => 27 f(
In some ways, this defeats the purpose of the lambda function. The PEP 8 style guide discourages this use.
So when is a lambda function actually useful? One case is for applying a non-vectorized function to a list. For instance,
= [1, 2, 3]
l list(map(lambda x: x ** 2, l)) # => [1, 4, 9]
However, in this and most cases where numerical computation, it is better to use the vectorization of NumPy. In the case of a non-numerical function mapping over a list of strings, the lambda function is a good choice, as in the following case:
= ["foo", "bar", "baz"]
l list(map(lambda s: s.capitalize(), l)) # => ["Foo", "Bar", "Baz"]
Conditional Functions
There are some more complex custom functions that are difficult to vectorize. An example is a function with conditions. Consider the following function:
def square_positive(x):
if x > 0:
return x ** 2
else:
return x
This function can be applied to a single number x
, but it cannot take an array
argument. One solution would be to write a for
loop over
the elements of x
, but this would
be inefficient.
The function np.where(condition, a_true, a_false)
returns an array chosen from a_true
and a_false
based on the condition
. Consider the following
version of square_positive()
:
def square_positive(x: np.ndarray) -> np.ndarray:
return np.where(x > 0, x ** 2, x)
This is vectorized so it can be applied to arrays and it is much
more performant than a for
-loop
solution.
numpy-user-guide, numpy-reference
Loops are executed in broadcasting, but these are loops in the more-efficient C programming language (in which Python is written), not in Python.↩︎
Online Resources for Section 3.2
No online resources.