1.1. Arrays#

A NumPy array stores multiple elements of the same type (usually numbers). It is an object of the class ndarray (alias: array), which provides the following attributes.

  • .ndim The number of axes (i.e., dimensions) of the array.

  • .shape A tuple of integers indicating the size of the array in each dimension.

  • .size The total number of elements of the array.

There are multiple ways to create and manipulate a NumPy array. Let’s review the basic ones.

import numpy as np

1.1.1. Vectors#

The simplest way to create an array is by passing a list of numbers to the function np.array(). This produces an array with one axis, which is referred to as vector.

list1 = [0,1,2,3,4]

vector = np.array(list1)
+--------+-------------+
| vector | [0 1 2 3 4] |
+--------+-------------+

Let’s inspect its attributes.

+--------------+------+
|  vector.ndim | 1    |
+--------------+------+
| vector.shape | (5,) |
+--------------+------+
|  vector.size | 5    |
+--------------+------+

From the above one can deduce that a vector of size \(N\) presents the following attributes:

  • .ndim = 1

  • .shape = (N,)

  • .size = N

In the mathematical language, the notation \(\mathbf{x}\in \mathbb{R}^N\) indicates a vector of size \(N\):

\[\begin{split} \mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \\ \vdots\\ x_{N} \end{bmatrix}. \end{split}\]

Note that vectors are conventionally listed out vertically. However, this contrasts with the convention adopted by NumPy, where vectors are instead listed out horizontally:

\[ \mathbf{x}^\top = \begin{bmatrix}x_1 & x_2 & \dots & x_{N} \end{bmatrix}. \]

Keep that in mind when you implement mathematical expressions in NumPy!

1.1.2. Matrices#

It is also possible to create an array by passing a sequence of lists to np.array(). This produces an array with two axes, which is referred to as matrix.

list2 = [[0,1,2], [3,4,5], [6,7,8]]

matrix = np.array(list2)
+--------+-----------+
| matrix | [[0 1 2]  |
|        |  [3 4 5]  |
|        |  [6 7 8]] |
+--------+-----------+

Let’s inspect its attributes.

+--------------+--------+
|  matrix.ndim | 2      |
+--------------+--------+
| matrix.shape | (3, 3) |
+--------------+--------+
|  matrix.size | 9      |
+--------------+--------+

A matrix with \(N\) rows and \(M\) columns presents the following attributes:

  • .ndim = 2

  • .shape = (N, M)

  • .size = N*M

In the mathematical language, the notation \(X\in \mathbb{R}^{N\times M}\) indicates a matrix of shape \(N\times M\):

\[\begin{split}X = \begin{bmatrix} x_{1,1} & x_{1,2} & \dots & x_{1,M}\\ x_{2,1} & x_{2,2} & \dots & x_{2,M}\\ \vdots & \vdots & \ddots & \vdots \\ x_{N,1} & x_{N,2} & \dots & x_{N,M} \end{bmatrix}.\end{split}\]

Either axis of a matrix can have length one, thus yielding 2 special cases.

  • A matrix with one row and multiple columns:

\[X_{\rm row} \in \mathbb{R}^{1\times M}.\]
  • A matrix with one column and multiple rows:

\[X_{\rm col} \in \mathbb{R}^{N\times 1}.\]

To avoid errors and unexpected behaviours, it is crucial to understant the difference between vectors (1d arrays) and rows/columns (2d arrays).

vec = np.array([2,5,9])       # vector (1d array)
row = np.array([[2,5,9]])     # matrix (2d array) with one row
col = np.array([[2],[5],[9]]) # matrix (2d array) with one column
+------------------+-----------+
|           vector | [2 5 9]   |
+------------------+-----------+
| matrix (one row) | [[2 5 9]] |
+------------------+-----------+
| matrix (one col) | [[2]      |
|                  |  [5]      |
|                  |  [9]]     |
+------------------+-----------+

Pay attention to this important detail.

  • The vector has shape (3,).

  • The single-row matrix has shape (1,3).

  • The single-column matrix has shape (3,1).

+--------+---------+   +--------+-----------+   +----------+--------+
| vector | [2 5 9] |   | matrix | [[2 5 9]] |   |   matrix | [[2]   |
|        |         |   |  (row) |           |   | (column) |  [5]   |
|        |         |   |        |           |   |          |  [9]]  |
+--------+---------+   +--------+-----------+   +----------+--------+
|  shape | (3,)    |   |  shape | (1, 3)    |   |    shape | (3, 1) |
+--------+---------+   +--------+-----------+   +----------+--------+
|   ndim | 1       |   |   ndim | 2         |   |     ndim | 2      |
+--------+---------+   +--------+-----------+   +----------+--------+

1.1.3. ND arrays#

There is no limit to the nesting level of lists passed to np.array(). The following creates an array with three axes, which is referred to as volume.

list3 = [[[0,1,2], [3,4,5], [6,7,8]],  [[4,1,3], [6,1,3], [9,4,5]]]

volume = np.array(list3)
+--------+-------------+
| volume | [[[0 1 2]   |
|        |   [3 4 5]   |
|        |   [6 7 8]]  |
|        |             |
|        |  [[4 1 3]   |
|        |   [6 1 3]   |
|        |   [9 4 5]]] |
+--------+-------------+

Let’s inspect its attributes.

+--------------+-----------+
|  volume.ndim | 3         |
+--------------+-----------+
| volume.shape | (2, 3, 3) |
+--------------+-----------+
|  volume.size | 18        |
+--------------+-----------+

1.1.4. Reshaping#

The shape of an array can be changed with various commands:

  • .reshape() reconfigures the array into a new shape,

  • .ravel() stretches the array into a vector.

The following are examples of reshaping.

x = np.random.randint(10, size=(3,4))

y = x.reshape(6,2)

z = x.ravel()
+-------+-------------+  +-------+---------+  +-------+---------------------------+
|     x | [[6 4 2 8]  |  |     y | [[6 4]  |  |     z | [6 4 2 8 0 3 1 8 6 3 5 8] |
|       |  [0 3 1 8]  |  |       |  [2 8]  |  |       |                           |
|       |  [6 3 5 8]] |  |       |  [0 3]  |  |       |                           |
|       |             |  |       |  [1 8]  |  |       |                           |
|       |             |  |       |  [6 3]  |  |       |                           |
|       |             |  |       |  [5 8]] |  |       |                           |
+-------+-------------+  +-------+---------+  +-------+---------------------------+
| shape | (3, 4)      |  | shape | (6, 2)  |  | shape | (12,)                     |
+-------+-------------+  +-------+---------+  +-------+---------------------------+

The elements of a reshaped array keep the same order in memory as the original array. The order of these elements is “C-style” by default, that is, the rightmost index changes the fastest, so the element after x[0,0] is x[0,1], then x[0,2], and so on.

+--------+---------------------------+
| x.flat | [6 4 2 8 0 3 1 8 6 3 5 8] |
+--------+---------------------------+
| y.flat | [6 4 2 8 0 3 1 8 6 3 5 8] |
+--------+---------------------------+

Reshaping does not change the elements in the original array. If the new shape is not compatible with the original shape, an error is thrown.

z = x.reshape(2,7)   # This throws an error
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-19-dc58052866c2> in <module>()
----> 1 z = x.reshape(2,7)
      2 
      3 # ValueError: cannot reshape array of size 12 into shape (2,7)

ValueError: cannot reshape array of size 12 into shape (2,7)

Reshaping does not make a copy of the data; rather, it produces a new view of the original array (whenever possible). A view behaves like a regular array, except that its elements are shared with the array from which it was created.

a = np.array([4, 10, 7, 2, 5, 1])

b = a.reshape(2,3) # Shared data!
+--------+---------------------+
|      a | [ 4 10  7  2  5  1] |
+--------+---------------------+
|      b | [[ 4 10  7]         |
|        |  [ 2  5  1]]        |
+--------+---------------------+
| shared | True                |
+--------+---------------------+

Pay attention to this important detail. The reshaped array shares the same elements of the original array, without making an actual copy of the data (whenever possible). Hence, any modification on the reshaped array is performed on the original array.

b[1,0] = -10 # This also modifies the "source" array
+---+---------------------------+
| a | [  4  10   7 -10   5   1] |
+---+---------------------------+
| b | [[  4  10   7]            |
|   |  [-10   5   1]]           |
+---+---------------------------+

To summarize, the functions reshape() and ravel() reconfigure the elements of an array into a new shape, without changing their values. They work according to the following rules.

  • Shallow copy: The reshaped array shares the same elements with the original array.

  • Same order: The shared elements keep the same order in memory.

  • Same size: The total number of elements does not change.