import numpy as np
= np.array([1, 2, 3])
arr = arr + 10.0
arr2 print("The result of adding a scalar to an array is:", arr2)
The result of adding a scalar to an array is: [11. 12. 13.]
Last lecture we learned about Numpy and how it solves the problem of slow for-loops: It basically provides hundreds of pre-written functions to do the common things for which we’d normally need to write a for-loop.
These functions are both much faster than what we could write in Python, but they also save us time since we don’t have to write code for a bunch of common operations. So even if Numpy was not built on highly efficient C-based code, we’d still use it (or something like it) to perform all the routine functions for numerical arrays.
In this chapter we’ll learn about a few more techniques that Numpy offers to help us avoid writing complex code ourselves. Among other things, we’ll start learning about using Numpy for linear algebra and other mathematical operations.
Last lecture we learned about “element-wise” operations, where we could do an operation between a scalar and an array:
import numpy as np
= np.array([1, 2, 3])
arr = arr + 10.0
arr2 print("The result of adding a scalar to an array is:", arr2)
The result of adding a scalar to an array is: [11. 12. 13.]
Or operate two arrays of the same size:
= np.array([1, 2, 3])
arr1 = np.array([11, 12, 13])
arr2 = arr1 + arr2
arr3 print("The result of adding two arrays (of matching shape) is:", arr3)
The result of adding two arrays (of matching shape) is: [12 14 16]
But Numpy also let’s us do operations on arrays with different shape…as long as the shapes are compatible. This is called broadcasting and it works as follows:
1= np.ones((2, 3))*100
arr1 print(arr1)
2= np.arange(3)
arr2 print(arr2)
3= arr1 + arr2
arr3 print(arr3)
arr1
gets added to each row of arr2
[[100. 100. 100.]
[100. 100. 100.]]
[0 1 2]
[[100. 101. 102.]
[100. 101. 102.]]
There are actually three scenarios where broadcasting is invoked:
ndarray
. This one is not an official rule, but is worth including this list to remind us that operations using scalars are a form of broadcasting.Figure 9.2 shows a few examples of broadcasting arrays of different shapes:
Let’s see each of these in action.
We have already seen this one, but let’s look again. If we have a 2D array and a 1D array, we can multiply them together, and the result will be a 2D array:
= np.ones((3, 4))*10
arr2d = np.arange(4)
arr1d = arr1d*arr2d
arr print(arr)
[[ 0. 10. 20. 30.]
[ 0. 10. 20. 30.]
[ 0. 10. 20. 30.]]
In this case Numpy takes arr1d
and adds a new dimension at the beginning of the list of dimensions, then duplicates the values from the original dimension. This is like using vstack
on arr1d
3 times. In fact we can recreate this behavior ourselves:
= np.ones((3, 4))*10
arr2d = np.arange(4)
arr1d 1= np.vstack((arr1d, arr1d, arr1d))
arr1d_stacked = arr1d_stacked*arr2d
arr print(arr)
arr1d
and “hard-coded” this value when we called vstack()
.
[[ 0. 10. 20. 30.]
[ 0. 10. 20. 30.]
[ 0. 10. 20. 30.]]
“Hard-coding” values like this is bad practice, since it will obviously only work for arrays of that specific shape. We can do this is a more abstract way using the tile
function as follows:
= np.ones((3, 4))*10
arr2d = np.arange(4)
arr1d 1= np.tile(arr1d, [arr2d.shape[0], 1])
arr1d_tiled = arr1d_tiled*arr2d
arr print(arr)
tile
the input array (arr1d
) N
times, where N
is take from the shape of arr2d
. This way the code it totally general for any shape of arr2d
.
[[ 0. 10. 20. 30.]
[ 0. 10. 20. 30.]
[ 0. 10. 20. 30.]]
For the sake of completeness, we could also use some of the built-in Python features to broadcast the lower-dimensional array ourselves:
= np.ones((3, 4))*10
arr2d = np.arange(4)
arr1d = np.array([arr1d]*arr2d.shape[0]).reshape(arr2d.shape)
arr1d_pythonic = arr1d_pythonic*arr2d
arr print(arr)
[[ 0. 10. 20. 30.]
[ 0. 10. 20. 30.]
[ 0. 10. 20. 30.]]
If both arrays are 2D but not the same shape, Numpy will duplicate the “lower dimensional” array along its “singleton” axis, assuming it has one.
When an array has a singleton axis it means that the shape in that dimension/axis is 1. Sometimes singleton axes are necessary so that arrays can be compatible with each other. Other times the singleton axis is redundant and can be removed.
= np.ones((1, 3, 3))
arr print(arr)
[[[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]]]
Note that this array is nested inside three sets of [ ]
, indicating a 3D array.
We can use the np.squeeze()
function to removes singleton dimension(s):
= np.ones((1, 3, 3))
arr = np.squeeze(arr)
arr print(arr)
[[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]]
Note that missing set of [ ]
around this array, indicating it’s been reduced to 2D.
= np.ones((3, 3))*10
arr2d = np.arange(3).reshape((3, 1))
arr2d_singleton = arr2d*arr2d_singleton
arr print(arr)
[[ 0. 0. 0.]
[10. 10. 10.]
[20. 20. 20.]]
When operating on a pair of arrays with the same dimensionality, Numpy doesn’t care which dimension it needs to tile along, so broadcasting will also work in this case:
= np.ones((3, 3))*10
arr2d = np.arange(3).reshape((1, 3))
arr2d_singleton = arr2d*arr2d_singleton
arr print(arr)
[[ 0. 10. 20.]
[ 0. 10. 20.]
[ 0. 10. 20.]]
The final broadcasting rule basically states that if Rules 1 or 2 don’t work, then an error occurs.
The obvious case is when the dimensions are fundamentally incompatible in both directions:
= np.ones((3, 3))*10
arr1 = np.arange(8).reshape((2, 4))
arr2 try:
= arr1*arr2
arr3 1except ValueError as e:
print(e)
try/except
blocks, here is a useful case. You can catch the error then print it, instead of having it halt your code. Of course this is only a good idea for things like notes and tutorials. In reality you want your code to halt if there is a problem.
operands could not be broadcast together with shapes (3,3) (2,4)
There is just no way to that arr2
could be altered, rotated, or tiled to match the shape of arr1
.
Rule 3 also applies even when one of the arrays does have a singleton dimension:
= np.ones((3, 3))*10
arr1 1= np.arange(4).reshape((1, 4))
arr2 try:
= arr1*arr2
arr3 except ValueError as e:
print(e)
arr2
has a size of 4 in the 2nd dimension, while arr1
has shape of 3 in both, so no broadcasting is possible.
operands could not be broadcast together with shapes (3,3) (1,4)
Consider the more subtle case where we have a common size in one dimension, but neither array has a singleton axis:
= np.ones((3, 3))*10
arr2d = np.arange(6).reshape((2, 3))
arr2d_no_singleton try:
= arr2d*arr2d_no_singleton
arr3 except ValueError as e:
print(e)
operands could not be broadcast together with shapes (3,3) (2,3)
It is important to understand why this case fails: It is because Numpy does not know what data to put in the new row. In the case of a singleton axis it is obvious that the values in that dimension should be copied, but in the case of a (2, 3)
array, there is no clear choice for which data to put in row 3.
So far we have seen that the Numpy library offers several functions at the “top-level” (i.e. np.<func>
). We have focused on the “utility” functions, such as:
np.array()
converts a list of values into an ndarray
, which is Numpy’s special data-type.np.vstack()
, np.split()
, and np.reshape()
np.ones()
, np.zeros()
, and np.arange()
.In addition to these utility-type functions, Numpy also offers a large variety of mathematical and numerical functions. These are officially called “ufuncs” for Universal Functions.
The list of available functions is too long to reproduce here, so the links below take you to the Numpy documentation:
Some of the functions offered by Numpy perform an operation on a single array, such as the trigonometric functions like np.cos()
, or the mathematical functions like np.log10()
.
These work on 1D arrays:
= np.arange(4)
arr = np.cos(arr)
res print(res)
[ 1. 0.54030231 -0.41614684 -0.9899925 ]
And ND arrays:
= np.arange(4).reshape((2, 2))
arr = np.cos(arr)
res print(res)
[[ 1. 0.54030231]
[-0.41614684 -0.9899925 ]]
Even though the math
library in Python’s standard library has a cos
function, it does not work on ndarrays
. If you attempt to do the following, you will get an error message:
from math import cos
= np.arange(4)
arr cos(arr)
Only the Numpy functions are able to operate on the Numpy arrays in the element-wise fashion that we want.
Other functions operate on 2 arrays, such as add
and multiply
. These functions work exactly like the corresponding mathematical operator. For instance, addition using np.add()
and using +
are identical:
= np.ones((1, 3))*10
arr1 = np.ones((3, 3))
arr2 = arr1 + arr2
res print(res)
[[11. 11. 11.]
[11. 11. 11.]
[11. 11. 11.]]
and
= np.ones((1, 3))*10
arr1 = np.ones((3, 3))
arr2 = np.add(arr1, arr2)
res print(res)
[[11. 11. 11.]
[11. 11. 11.]
[11. 11. 11.]]
Some of the functions do multiple calculations in a single call. These are offered for convenience. For instance, np.logaddexp()
is the same thing as calling np.log(np.exp(x1) + np.exp(x2))
.
Numpy includes several modules containing “application specific” functions that are common enough that they are included in Numpy, rather than a separate package. These including the following “numerical” features:
And the following “utilities”:
Let’s take a closer look as some of these.
numpy.random
ModuleOne of the main uses of this module is to generate random values from a specified distribution. These distributions will be more relevant once you’ve taken a course in statistics, but consider the following:
= np.random.rand(10000)
arr1 = np.random.normal(0.5, 0.1, 10000)
arr2
= plt.subplots(figsize=[5, 5])
fig, ax ='k', label='uniform', alpha=0.75)
ax.hist(arr1, edgecolor='k', label='normal', alpha=0.75)
ax.hist(arr2, edgecolor
ax.legend()'Value')
ax.set_xlabel('Number of occurrences in stated range'); ax.set_ylabel(
An partial list of functions for generating data from different distributions is given below:
beta
binomial
exponential
gamma
logistic
lognormal
normal
pareto
poisson
power
rand
randint
rayleigh
vonmises
weibull
Each of these distributions is relevant to some different physical system. The normal
distribution is good for describing things like height or weight of a group of people. The weibull
distribution is useful for describing manufacturing and production rates. The functions in numpy.random
are quite basic only useful for generating values. To do more detailed analysis of statistical data you can use scipy.stats
.
numpy.linalg
ModuleThe linalg
module contains all the functions you would expect for performing “matrix” operations, such as dot-products, cross-products, matrix multiplication, and finding eigenvalues.
matmul
Some of the functions in the linalg
module are so commonly used that they are also included in the top-level namespace of the Numpy package for convenience. For instance, np.matmul
just calls np.linalg.matmul
, but it saves the programmer some typing.
Matrix multiplication is the most important computer operation in all of engineering and science. This is because the mathematical description of many physical systems yields a “system of coupled linear equations”. We must solve this system of equations to find the values of the physical process.
Fully appreciating this fact might require a few more years of coursework. But let’s take a quick look at a “resistor network” problem, which occurs in electric circuits, but is also used as an analogy for many other systems such as fluid flow in pipes.
Let’s explore a few more features of “matrix multiplication”. Firstly, we can use the @
symbol to indicate matrix multiplication:
= A @ V
I print(I.reshape(I.size, 1))
[[-9.69696960e-01]
[-6.36363640e-01]
[-4.00000006e-08]
[ 3.99999998e-08]
[ 5.15151520e-01]
[ 1.09090908e+00]]
Alternatively, we can convert A
to a matrix
instead of an ndarray
. This will tell Numpy that all operations with A
should be of the matrix-type. As shown below the *
symbol will result in matrix multiplication if applied to 2 matrix’s.
= np.matrix(A)
Amat = np.matrix(V).T
Vmat = Amat*Vmat
I print(I)
[[-9.69696960e-01]
[-6.36363640e-01]
[-4.00000006e-08]
[ 3.99999998e-08]
[ 5.15151520e-01]
[ 1.09090908e+00]]
Remember that the above only works if the ndarrays
have been converted to matrix
data types. If for some reason we wanted to do normal element-wise multiplication we’d need to use np.multiply()
:
= np.multiply(Amat, Vmat)
res print(res)
[[-10. 6. 4. 0. 0.
0. ]
[ 6. -8. 0. 2. 0.
0. ]
[ 3.03030304 0. -9.09090912 4.54545456 1.51515152
0. ]
[ 0. 1.36363636 4.09090908 -9.54545452 0.
4.09090908]
[ 0. 0. 1. 0. -3.
2. ]
[ 0. 0. 0. 3. 2.
-5. ]]
Which has the same result as multiplying the ndarrays
.
= A * V
res print(res)
[[-10. 6. 3.03030304 0. 0.
0. ]
[ 6. -8. 0. 1.36363636 0.
0. ]
[ 4. 0. -9.09090912 4.09090908 1.
0. ]
[ 0. 2. 4.54545456 -9.54545452 0.
3. ]
[ 0. 0. 1.51515152 0. -3.
2. ]
[ 0. 0. 0. 4.09090908 2.
-5. ]]
Given an array (or matrix) we can use Numpy to find a wide variety of properties.
Consider the coefficient matrix from the previous section:
= [[-5., 3., 2., 0., 0., 0.],
A 3., -4., 0., 1., 0., 0.],
[ 2., 0., -6., 3., 1., 0.],
[ 0., 1., 3., -7., 0., 3.],
[ 0., 0., 1., 0., -3., 2.],
[ 0., 0., 0., 3., 2., -5.]] [
We can find the determinant:
= np.linalg.det(A)
det print(det)
-1.2352024165401014e-12
The eigenvalues:
= np.linalg.eigvals(A)
eigvals print(eigvals)
[-1.12458298e+01 -4.22673747e-15 -1.53348494e+00 -7.69023393e+00
-4.20608062e+00 -5.32437066e+00]
It’s also possible to find the “inverse” of a matrix, which will be discussed in the following example.
Scipy is the sibling package to Numpy. Scipy’s purpose is to hold a huge selection of additional mathematical functions which cover nearly every corner of science, physics and engineering. All functions in Scipy are designed to operate on Numpy arrays. Both packages are developed in parallel by many of the same people.
The following table shows the modules that are included with Scipy. More details can be found on the Scipy Website.
Module Name | Full Name |
---|---|
scipy.cluster |
Clustering package |
scipy.constants |
Constants |
scipy.datasets |
Datasets |
scipy.fft |
Discrete Fourier transforms |
scipy.integrate |
Integration and ODEs |
scipy.interpolate |
Interpolation |
scipy.io |
Input and output |
scipy.linalg |
Linear algebra |
scipy.misc |
Miscellaneous routines |
scipy.ndimage |
Multidimensional image processing |
scipy.odr |
Orthogonal distance regression |
scipy.optimize |
Optimization and root finding |
scipy.signal |
Signal processing |
scipy.sparse |
Sparse matrices |
scipy.spatial |
Spatial algorithms and data structures |
scipy.special |
Special functions |
scipy.stats |
Statistical functions |
Each of the modules included with Scipy are useful for a different subset of applications. It is not really feasible or helpful to cover all of these here. You can learn about them as you need them. Instead we’ll do an example of image analysis using ndimage
since we introduced images in the last chapter.
Now that we are thoroughly comfortable with Numpy arrays it is likely that we’ll be using them for everything. This means we might want/need to save our data to disk for use later.
Numpy offers 2 convenient ways to do this.
= np.random.rand(100, 100)
arr 1'random_values', arr) np.save(
.npy
for us.
And of course we can load it back from the disk into active memory just as easily:
1= np.load('random_values.npy')
arr2 print(np.all(arr == arr2))
random_values
and random_values.npy
are both valid file names.
True
The np.save()
and np.load()
functions save the numerical data in binary format, meaning these files are not human readable. Numpy does offer savetxt
and loadtxt
to write/read normal text files, but this not recommended for saving large amounts of data since conversion to and from string
slows everything down.
Often we are working on a project that has many arrays, such as processing a batch of images. We can save all these arrays to a single archive file. This helps with keeping related items together in one place.
= np.random.rand(100, 100)
arr1 = np.random.rand(100, 100)
arr2 1'multiple_random_arrays', foo=arr1, bar=arr2) np.savez(
.npz
. The z
reminds us that it’s an archive containing multiple files.
Now to read them back in we can use the same np.load()
function, which will adapt to handle the archive instead of just a single array.
= np.load('multiple_random_arrays.npz') dict_containing_both_arrays
The function returns a “dict-like” object and we can access the arrays using the keywords that we used when we wrote the file:
1= dict_containing_both_arrays['foo']
arr3 print(np.all(arr1 == arr3))
True
Lastly, Numpy offers us a way to save an archive of files in a compressed format to save space. This is acomplished with np.savez_compressed()
. The files are still reloaded using np.load()
, and the reuslt is a dictionary like discussed above.