In order to be able to use numpy we need to import the library using the special word import. Also, to avoid typing numpy every time we want to use one if its functions we can provide an alias using the special word as:

import numpy as np

Now, we have access to all the functions available in numpy by typing np.name_of_function. For example, the equivalent of 1 + 1 in Python can be done in numpy:

np.add(1,1)

Although this might not seem very useful, however, even simple operations like this one, can be much quicker in numpy than in standard Python when using lots of numbers.

Tip: To access the documentation explaining how a function is used, its input parameters and output format we can press Shift+Tab after the function name"
np.add

By default the result of a function or operation is shown underneath the cell containing the code. If we want to reuse this result for a later operation we can assign it to a variable:

a = np.add(2,3)

We have just declared a variable a that holds the result of the function. We cannow use of display this variable, at any point of this notebook. For example we can show its contents by typing the variable name in a new cell:

a

Exercise 1.1: Can you use the previous numpy function to add the values 34 and 29

from check_answer import check_answer

# Substitute the ? symbols by the correct expressions and values
answ = ?

check_answer("1.1", answ)

One of numpy's core concepts is the array, which is equivalent to numpy lists, but can be multidimensional and with much more functionality. To declare a numpy array explicity we do:

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

Most of the functions and operations defined in numpy can be applied to arrays. For example, with the previous add operation:

arr1 = np.array([1,2,3,4])
arr2 = np.array([3,4,5,6])

np.add(arr1, arr2)

But a more simple and convenient notation can also be used:

Note: This operation detects that numpy arrays are being added and calls the previous function for efficient execution. Think that these arrays can contain large amounts of values.
arr1 + arr2

Arrays can be sliced and diced. We can get subsets of the arrays using the indexing notation which is [start:end:stride]. Let's see what this means:

arr = np.array([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15])

print(arr[5])
print(arr[5:])
print(arr[:5])
print(arr[::2])

Experiment playing with the indexes to understand the meaning of start, end and stride. What happend if you don't specify a start? What value numpy uses instead? Note that numpy indexes start on 0, the same convention used in Python lists.

Exercise 1.2: Can you declare a new array with contents [5,4,3,2,1] and slice it to select the last 3 items?

# Substitute the ? symbols by the correct expressions and values
arr = ?
answ = arr[?:?]

check_answer("1.2", answ)

Indexes can also be negative, meaning that you start counting by the end. For example, to select the last 2 elements in an array we can do:

arr = np.array([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15])

arr[-2:]

Exercise 1.3: Can you figure out how to select all the elements in the previous array excluding the last one, [15]?

# Substitute the ? symbols by the correct expressions and values
answ = arr[?]

check_answer("1.3", answ)

Exercise 1.4: What about doing excluding the last element from the list but selecting every 3rd element this time? Remember the third index indicates strides if used

Tip: Result should be ?[0,3,6,9,12]
answ = arr[?:?:?]

check_answer("1.4", answ)

Numpy arrays can have multiple dimensions. For example, we define a 2-dimensional (1,9) array using nested square brackets [ ]. The convention in numpy is that the outer [ ] represent the first dimension and the inner [ ] contains the last dimension.

drawing

For example the following cell declares a 2-dimensional array with shape (1, 9)

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

To visualise the shape (dimensions) of a numpy array we can add the suffix .shape to an array expression or variable containing a numpy array.

arr1 = np.array([1,2,3,4,5,6,7,8,9])
arr2 = np.array([[1,2,3,4,5,6,7,8,9]])
arr3 = np.array([[1],[2],[3],[4],[5],[6],[7],[8],[9]])

arr1.shape, arr2.shape, arr3.shape, np.array([1,2,3]).shape

Arrays can be reshaped into different shapes using the function reshape:

Note: The total number of elements has to be the same before and after the reshape operation, otherwise numpy will throw and error.
np.array([1,2,3,4,5,6,7,8]).reshape((2,4))

See the following example how a 9-element array can be reshaped into two dimensional arrays with different shapes.

Note: We are declaring the array and reshaping all in one line. This is called ’chaining’ and allows us to conveniently perform multiple operations. The expressions are evaluated from left to right, so in this case first we create and array and then apply the reshape operation to the resulting array.
arr1 = np.array([1,2,3,4,5,6,7,8,9]).reshape(1,9)
arr2 = np.array([1,2,3,4,5,6,7,8,9]).reshape(9,1)
arr3 = np.array([1,2,3,4,5,6,7,8,9]).reshape(3,3)

arr1.shape, arr2.shape, arr3.shape

Exercise 1.5: Can you declare a 1-dimensional array with 6 elements and then reshape it into a 2-dimensional array with shape (2,3)? You can try to do this in one or two lines.

# variable answ should contain your 2-dimensional array with shape (2,3)
answ = ?

check_answer("1.5", answ.shape)

There are convenient functions in numpy for declaring common arrays without having to type all their elements:

arr1 = np.arange(9)
arr2 = np.ones((3,3))
arr3 = np.zeros((2,2,2))
                
print(arr1)
print("--------")
print(arr2)
print("--------")
print(arr3)

Exercise 1.6: Can you declare a 3-dimensional array with shape (5,3,3)? The contents of the array don't matter for this exercise so you can use any of the previously introduced functions.

# variable answ should contain your 3-dimensional array with shape (5,3,3)
answ = ?

check_answer("1.6", answ.shape)

Exercise 1.7: Can you create another array with the same shape and use the numpy function to add both arrays:

arr1 = ?
arr2 = ?

answ = np.?

check_answer("1.7", answ.shape)

Numpy has useful functions for calculating the mean, standard deviation and sum of the elements of an array.

arr = np.arange(9).reshape((3,3))

print(arr)
print("--------")
print("Mean:", np.mean(arr))
print("Std Dev:", np.std(arr))
print("Sum:",np.sum(arr))

These operation can be performed along specific axis.

arr = np.arange(9).reshape((3,3))

print(arr)
print("--------")
print("Sum along the vertical axis:", np.sum(arr, axis=0))
print("--------")
print("Sum along the horizontal axis:", np.sum(arr, axis=1))

Exercise 1.8: Declare a 2-dimensional array with shape (20,10) all filled with ones. Then calculate the sum of its values along the first dimension (axis=0). The result has to be a 1-dimensional array with shape (10,)

# variable answ should contain your 3-dimensional array with shape (5,3,3)
answ = ?

check_answer("1.8", answ)

Numpy arrays can contain numerical values of different types. These types can be divided in these groups:

  • Integers
    • Unsigned
      • 8 bits: uint8
      • 16 bits: uint16
      • 32 bits: uint32
      • 64 bits: uint64
    • Signed
      • 8 bits: int8
      • 16 bits: int16
      • 32 bits: int32
      • 64 bits: int64
  • Floats
    • 32 bits: float32
    • 64 bits: float64

We can look up the type of an array by using the .dtype suffix.

arr = np.ones((10,10,10))

arr.dtype

To specify the type of an array, we can add the dtype parameter to the declaration expression.

arr = np.ones((10,10,10), dtype=np.uint8)

arr.dtype

We can also change the type of an existing array using the .astype function.

arr = np.ones((10,10,10))
arr = arr.astype(np.float32)

# Or all in one line
arr = np.ones((10,10,10)).astype(np.float32)

arr.dtype

Exercise 1.9: Change the type of the following array to int16

answ = np.arange(10)

answ = answ#Your code goes here

check_answer("1.9", answ.dtype)

Broadcasting: numpy is set up internally in a way that allows performing array operations efficiently. Sometimes it is not entirely obvious what is going on. For example:

a = np.zeros((10,10))

a = a + 1

a

The previous operation declares a 10x10 array, assigns that to a variable a and then we add 1 to this variable. However, 1 is a single value and is not even an array so it is not entirely clear what is going on. Broadcasting is the ability in numpy to arrays to replicate or promoted arrays involved in operations to match their shapes.

a = np.arange(9).reshape((3,3))

b = np.arange(3)

a + b

Exercise 1.10: Can you declare a new 1-dimensional array with shape (10,) all filled with 2 values?

Tip: We have just seen an example of broadcasting by adding a single value to an array. Broadcasting also works with other operations, such as multiplication or division, so you can complete this exercise declaring an initial array containing all zeros or ones and then using one operation to modify all its values.
answ = ?

check_answer("1.10", answ)

Boolean values: Numpy arrays normally store numeric values but they can also contain boolean values. Booleans is a data type that can have two possible values: [True, False]. For example:

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

arr, arr.shape, arr.dtype

We can operate with boolean arrays using the numpy functions for performing logical operations such as and, or and others.

arr1 = np.array([True, True, False, False])
arr2 = np.array([True, False, True, False])

print(np.logical_and(arr1, arr2))
print(np.logical_or(arr1, arr2))

These operations are conveniently offered by numpy with the * and +.

Note: Here the * and + symbols are not performing multiplication and addition as with numerical arrays. Numpy detects the type of the arrays involved in the operation and changes the behaviour of these operators. This ability to change the behaviour of operators depending on the situation is called ’operator overloading’ in programming languages.
print(arr1 * arr2)
print(arr1 + arr2)

Boolean arrays are often the result of comparing a numerical arrays with certain values. This is sometimes useful to detect values that are equal, below or above a number in a numpy array. For example, is we want to know which values in an array are equal to 1 and the values that are greater than 2 we can do:

arr = np.array([1, 3, 5, 1, 6, 3, 1, 5, 7, 1])

print(arr == 1)
print(arr > 2)

Exercise 1.11: For this exercise you'll need to combine array comparisons and logical operators to find out the values in the following array that are greater than 3 and less than 7.

arr = np.array([1, 3, 5, 1, 6, 3, 1, 5, 7, 1])

answ = ?

check_answer("1.11", answ)

Boolean types are quite handy for indexing and selecting parts of images as we will see later. Many numpy functions also work with Boolean types.

arr = np.array([1,2,3,4,5,6,7,8,9])
mask = np.array([True,False,True,False,True,False,True,False,True])

arr[mask]

Exercise 1.12: Based on the previous example how would you select the values in the array that are greater than 3 and less than 7. In this case you want a new smaller array containing just the values that are within that range.

arr = np.array([1, 3, 5, 1, 6, 3, 1, 5, 7, 1])

answ = ?

check_answer("1.12", answ)

Depending of the language that you have used before this behaviour in Python might strike you:

a = np.array([0,0,0])

# We make a copy of array a with name b
b = a

# We modify the first element of b
b[0] = 1

print(a)
print(b)

Both arrays have been modified. This is in fact because a and b are references to the same underlying array. If you want to have variables with independent arrays you'll have to use the b = np.copy(a) function to explicitly make a copy of the array.

a = np.array([0,0,0])

# We explicitly make a copy of array a with name b
b = np.copy(a)

# We modify the first element of b
b[0] = 1

print(a)
print(b)