A.3 The numpy package and arrays#

NumPy#


     The core of Python includes the basic features of the language. All other features are included in separate packages (called libraries or toolboxes in other programming languages). The package for creating and manipulating arrays is called numpy. The numpy package also includes a set of basic mathematical functions (cos, exp, log, sum, etc.). Extensive resources on numpy’s capabilities are available on the Internet, including the numpy web site. This section focuses on how to use numpy arrays of numbers.
     Before a package can be used, it needs to be imported. The following line imports the numpy package and renames it to np (the default short name used in the scientific Python community).

import numpy as np

All functions of the numpy package can now be called using np.function_name(), for example

np.sqrt(10)
3.1622776601683795

      Mathematical numpy functions work on both real and complex variables. Note, however, that the square root of a negative number returns a nan (not a number) and a warning is thrown. The following command
np.sqrt(-1)
returns
nan plus a nasty looking RuntimeWarning invalid value encountered in sqrt (error message not shown here).

A complex variable is returned when a complex variable is passed to the function (even when the imaginary part equals zero)

print(np.sqrt(-1 + 0j))

And for those users that want a complex number returned when taking the square root of a negative number, use the special np.lib.scimath.sqrt function

3.0 3j

Creating arrays#

     There are multiple ways to create an array. For example, a list can be converted to a numpy array with the np.array() command

a = np.array([1, 2, 3, 4])
print(a, 'has type', type(a))
[1 2 3 4] has type <class 'numpy.ndarray'>

The type function shows that a is an ndarray, which stands for n-dimensional array. All elements within such an array have the same variable type (contrary to a list, which can contain items of different types).
      Functions that work for individual numbers can also be used on arrays, for example

print('sqrt(a) gives:', np.sqrt(a))
sqrt(a) gives: [1.         1.41421356 1.73205081 2.        ]

Arithmetic operators can also be used with arrays. The following code defines an array b and multiplies the elements of the arrays a and b element by element

b = np.array([2, 2, 3, 3])
print('a * b gives:', a * b)
a * b gives: [ 2  4  9 12]

      Readers working with matrices may want to compute the dot product of two vectors (arrays), rather than multiplying two vectors term by term. The dot product of arrays a and b is obtained by using the @ operator

print(a @ b) # Or, alternatively: np.dot(a,b)
27

      In addition to converting a list to an array using the np.array() function, arrays can be generated with the np.arange function (similar to the range function used earlier, but now resulting in an array). The arange(start, end_before, step) function creates an array starting at start, taking steps equal to step, and stopping before it reaches the specified end_before value. If only a single value is passed to the function, it is interpreted to be the end_before value (i.e., the default values for start and step are 0 and 1, respectively). Using a negative step is possible as long as the start argument is larger than the end_before argument. Note that the default type of np.arange is integers unless one of the arguments is a float.

print(np.arange(3)) # so start=0 and step=1
print(np.arange(2, 6)) # so step=1
print(np.arange(2, 10, 2))
print(np.arange(10, 0, -2)) # steps can be negative
print(np.arange(2, 10, 2.0)) # now returns array of floats
[0 1 2]
[2 3 4 5]
[2 4 6 8]
[10  8  6  4  2]
[2. 4. 6. 8.]

      Instead of specifying a step, it is also possible to create an array by specifying the number of elements using the np.linspace(begin, end, num) function. The following function call returns seven points with values of equal increment from 1 to 4. Note that, somewhat confusingly, and unlike range and np.arange, the end value in linspace is included as the last element of the array. The default return value of np.linspace is an array of floats.

np.linspace(1, 4, 7)
array([1. , 1.5, 2. , 2.5, 3. , 3.5, 4. ])

Two-dimensional arrays#

     Arrays may have multiple dimensions. A two-dimensional array can be thought of as a matrix with rows and columns. It can be created by entering a list of rows, where each row is again a list that contains the values for that row. Care must be taken that each of the lists has the same number of elements (equal to the number of columns). When the array is printed to the screen, each row is on a separate line and the columns are separated by a space.

A = np.array([[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]])
print(A)
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]

      Arrays filled with zeros or ones may be created with the functions np.zeros and np.ones, respectively. Each of these functions requires the specification of a shape (e.g., the length of the array, or a tuple with the number of rows and columns for a two-dimensional array). The default return type is float.

print(np.zeros(7))
print(np.ones((2, 3))) # 2 rows, 3 columns
[0. 0. 0. 0. 0. 0. 0.]
[[1. 1. 1.]
 [1. 1. 1.]]

Slicing arrays#

     Just like for a list, the individual elements of an array can be accessed by indexing using square brackets (remember that the first index has number 0). The syntax for a onedimensional array x is x[start:end_before] or x[start:end_before:step]. If the start isn’t specified, 0 will be used. If the step isn’t specified, 1 will be used.
      When programming, the index of the end is generally not known as the array size can vary. To find out how long the array is and access the last value of array x, one can type x[len(x) - 1], but that is a bit inconvenient. Luckily, there is a shortcut: x[-1] is the same as x[len(x) - 1] and represents the last value in the array. Typing x[-2] returns the second to last value in the array, etc. The following code examples demonstrate various slicing operations.

x = np.arange(20, 30)
print(x)
print(x[0])
print(x[0:5]) # same as x[:5]
print(x[-5:]) # returns the last 5 values
print(x[3:7])
print(x[2:9:2]) # step is 2
print(x[-1:4:-2]) # starts at back, stops before reaching index 4 with step -2
print(x[[0, 3, 7]]) # passes indices as a list
[20 21 22 23 24 25 26 27 28 29]
20
[20 21 22 23 24]
[25 26 27 28 29]
[23 24 25 26]
[22 24 26 28]
[29 27 25]
[20 23 27]

      The contents of an array can be modified by assigning values to an array slice. In the following example, the first 5 elements of array x are replaced by 40, after which the last value is changed to 40 in a second command.

x = 20 * np.ones(10)
print(x)
x[0:5] = 40
print(x)
x[-1] = 40 # change last value
print(x)
[20. 20. 20. 20. 20. 20. 20. 20. 20. 20.]
[40. 40. 40. 40. 40. 20. 20. 20. 20. 20.]
[40. 40. 40. 40. 40. 20. 20. 20. 20. 40.]

Slicing arrays with conditional operators#

     Parts or values of an array may be selected based on conditions. This is called advanced slicing. For example,

a = np.arange(5)
print(a)
print(a < 3) # a < 3 returns a Boolean array
[0 1 2 3 4]
[ True  True  True False False]

Because < is a conditional operator, the statement a < 3 returns a Boolean array. Instead of printing the outcome to the screen, as in the above example, a more useful application is to store it as a variable. The variable can then be used to slice the array.

a_less_than_3 = a < 3
print('values in array a less than 3:', a[a_less_than_3])
values in array a less than 3: [0 1 2]

The ~ operator swaps the True and False values

print('values in array a not less than 3:', a[~a_less_than_3])
values in array a not less than 3: [3 4]

For more compact and readable code, the conditional statement can be specified inside the square brackets. It can even be used to assign a different value to selected elements of an array

a[a < 3] = 10 # Replace all values less than 3 by 10
print('new a:', a)
new a: [10 10 10  3  4]

Conditions for indices can be compounded with either & (and) or | (or), when the conditions are put between parentheses.

a = np.arange(10)
print('array:', a)
print('less than 3 or larger than 7:', a[(a < 3) | (a > 7)])
print('larger than 3 and less than 7:', a[(a > 3) & (a < 7)])
array: [0 1 2 3 4 5 6 7 8 9]
less than 3 or larger than 7: [0 1 2 8 9]
larger than 3 and less than 7: [4 5 6]

Manipulating arrays#

     Arrays can be stacked next to each other (horizontally) using the np.hstack command and above each other (vertically) using the np.vstack function

a = np.arange(3)
print(np.hstack((a, a)))
print(np.vstack((a, a)))
[0 1 2 0 1 2]
[[0 1 2]
 [0 1 2]]

Arrays can be combined to form larger arrays in this way, but it is not very efficient computationally, so programs may run slowly when large arrays are stacked or arrays are stacked multiple times, for example in a loop. It is better to create an array of the correct size first and then fill it with a for loop (this may be difficult when the size of the array is not known upfront). Note the use of enumerate in the code cell below.

x = np.linspace(0, np.pi, 4)
y = np.empty(len(x))
for i, xx in enumerate(x):
    y[i] = np.sin(xx)
print(y)
[0.00000000e+00 8.66025404e-01 8.66025404e-01 1.22464680e-16]

The code cell above is a demonstration of creating an empty array and filling it with a for loop. For this specific example, y = sin(x) is, of course, much easier and faster.