The numpy package¶

What is the numpy package ?¶

numpy is a python package designed for numerical computations. It provdes the main object called ndarray, which stands for $n$-dimensional arrays. This ndarrays play the role of matrices in linear algebra.

Importing the package¶

To use numpy, we must import it into the kernel. It is common to give it a nicknake np.

In [1]:
# Import numpy into the current kernel and nickname it as np.
import numpy as np

Creating an array¶

In python, if you want to use a function func from a package pack, we call it by pack.func. Similarly, to call the function array from numpy (now nicknamed np), we call it with np.array. This function np.array changes a list with appropriate structure into an ndarray.

In [2]:
l = [1, 2, 3, 4]
print(l)
print(type(l))
[1, 2, 3, 4]
<class 'list'>
In [3]:
a = np.array(l)
print(a)
print(type(a))
[1 2 3 4]
<class 'numpy.ndarray'>
In [4]:
A = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
print(A)
print(type(A))
[[1 2 3 4]
 [5 6 7 8]]
<class 'numpy.ndarray'>

Array shape¶

The shape of a ndarray is a tuple in which each element is the length of the corresponding dimension. To check the shape of an array a, we can use either np.shape(a) or an array's method a.shape.

In the next blocks, we check the dimension of the arrays a and A.

The first np.shape(a) should return (4,), which means that a is a one-dimensional array (only one number output) and there are 4 items in this dimension.

The second np.shape(A) should return (2,4). This means that A is a two-dimensional array (two numbers are returned), where there are 2 rows and 4 columns.

In [5]:
np.shape(a)
Out[5]:
(4,)
In [6]:
A.shape
Out[6]:
(2, 4)

One may also create a higher-dimensional array, but this would be out of our scopes.

A caution with the shapes (n,), (1,n) and (n,1)¶

In mathematics, we do not distinguish between vectors in $\mathbb{R}^{n}$ and a matrices in $\mathbb{R}^{n \times 1}$. However, in python's context, an array of shapes (n,), (1,n) and (n,1) are considered different due to the memory usage.

In particular, a (n,) array is the cheapest in terms of memory. A (1,n) array is considered as a matrix of 1 row and n columns, and a (n,1) array is considered as a matrix of n rows and 1 column.

The differences of these arrays will become clearer when we later do computation with them.

In [7]:
a = np.array([1, 2, 3, 4])
a_row = np.array([[1, 2, 3, 4]])
a_col = np.array([[1], [2], [3], [4]])
In [8]:
print(a)
np.shape(a)
[1 2 3 4]
Out[8]:
(4,)
In [9]:
print(a_row)
np.shape(a_row)
[[1 2 3 4]]
Out[9]:
(1, 4)
In [10]:
print(a_col)
np.shape(a_col)
[[1]
 [2]
 [3]
 [4]]
Out[10]:
(4, 1)

Accessing entries¶

We access entries of a ndarray in a similar way we would do with a list.

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

We can use all the slicing methods we learned with a list with a ndarray.

In [13]:
A[0:2]  # This calls A[0] and A[1].
Out[13]:
array([[1, 2, 3, 4],
       [5, 6, 7, 8]])

We access entries of a higher-dimensional array using a tuple of indices. Slicing in each dimension is also possible.

In [14]:
A[0, 3]
Out[14]:
np.int64(4)
In [15]:
# Take rows 2--3 and columns 1--2.
A[1:3, :2]
Out[15]:
array([[ 5,  6],
       [ 9, 10]])
In [16]:
# Take all rows of columns 2--3.
A[:, 1:3]
Out[16]:
array([[ 2,  3],
       [ 6,  7],
       [10, 11]])

One selective method that works with a ndarray but not a list is to explicitly pick the items along a list of indices.

In [17]:
# Take every columns of rows 1 and 3.
A[[0, 2], :]
Out[17]:
array([[ 1,  2,  3,  4],
       [ 9, 10, 11, 12]])

Transposition¶

We may use the method T of an array A to transpose itself, that is A.T.

If one prefers a clearer procedure, np.transpose can also be used.

In [18]:
A = np.array([[1, 2, 3], [4, 5, 6]])
A_t = A.T
print(f"A =\n{A}")
print(f"A_t =\n{A_t}")
A =
[[1 2 3]
 [4 5 6]]
A_t =
[[1 4]
 [2 5]
 [3 6]]
In [19]:
B = np.array([[1, 1], [2, 2], [3, 3]])
B_t = np.transpose(B)
print(f"B =\n{B}")
print(f"B_t =\n{B_t}")
B =
[[1 1]
 [2 2]
 [3 3]]
B_t =
[[1 2 3]
 [1 2 3]]

Addition and subtraction of arrays¶

Unlike a list, the objects of type ndarray allow us to do the real computation. The operators + and - are used for adding and subtracting two arrays.

Arrays with the same shapes¶

As one have always do in mathematics, addition and subtraction requires both matrices to have the same dimension.

In [20]:
A = np.array([[1, 2, 1], [3, 1, 4]])
B = np.array([[0, 1, 2], [-1, 3, 1]])
In [21]:
A + B
Out[21]:
array([[1, 3, 3],
       [2, 4, 5]])
In [22]:
A - B
Out[22]:
array([[ 1,  1, -1],
       [ 4, -2,  3]])

Broadcasting of arrays with different shapes¶

Broadcasting is a coding mechanism that allows operations to be performed on arrays with different shapes. The broadcasting is usually not mathematically correct, but it is a programming trick designed to simplify the code and is actually a key to achieve a clean and fast code.

Requirements¶

To add or subtract two arrays of different shapes, a + A or a - A, the two arrays must satisfy one of the following cases:

  • The shapes of a is (m,) and of A is (n,m): a+A adds a to every rows of A.
  • The shapes of a is (1,m) and of A is (n,m): a+A adds a to every rows of A.
  • The shapes of a is (n,1) and of A is (n,m): a+A adds a to every columns of A.
In [23]:
A = np.array([[1, 2, 1], [3, 1, 4]])
a = np.array([2, 1, 1])
a_row = np.array([[2, 1, 1]])
a_col = np.array([[3], [1]])
In [24]:
a + A
Out[24]:
array([[3, 3, 2],
       [5, 2, 5]])
In [25]:
a_row + A
Out[25]:
array([[3, 3, 2],
       [5, 2, 5]])
In [26]:
a_col + A
Out[26]:
array([[4, 5, 4],
       [4, 2, 5]])

Componentwise multiplication¶

The operator * is used when one wants to multiply a scalar (a number) to a ndarray. Note that this is not matrix multiplication.

In [27]:
c = 3
A = np.array([[1, 0, -2], [1, 4, 3]])
In [28]:
c * A
Out[28]:
array([[ 3,  0, -6],
       [ 3, 12,  9]])

Broadcasting with *¶

The operator * could also be used on arrays of different shapes in the same way with + and -. Let us demonstrate below.

In [29]:
a * A
Out[29]:
array([[ 2,  0, -2],
       [ 2,  4,  3]])
In [30]:
a_row * A
Out[30]:
array([[ 2,  0, -2],
       [ 2,  4,  3]])
In [31]:
a_col * A
Out[31]:
array([[ 3,  0, -6],
       [ 1,  4,  3]])

Matrix multiplication¶

Here, we are talking about the conventional matrix multiplication. The operator to use is @.

In [32]:
A = np.array([[2, 2, 3, 0], [1, 0, 1, 2]])
B = np.array([[1, 1], [2, 2], [3, 0], [-2, 1]])
In [33]:
A @ B
Out[33]:
array([[15,  6],
       [ 0,  3]])

There are also functions like np.matmul and np.dot that also do the job of matrix multiplication. Nevertheless, it is not recommended to use np.dot to do matrix multiplication. On the other hand, np.matmul implements the semantic of @ and also does complicate broadcasting. These aspects are, however, outside of the scopes of this introductory course.