This notebook has been automatically translated to make it accessible to more people, please let me know if you see any typos.

We are going to see a small introduction to the matrix calculation library

. This library is designed for all types of matrix calculus, so we are going to stay only with the part that will be useful to understand the calculations inside the neural networks, but we will leave out interesting things like the use of the library for linear algebra.**Numpy**

Numpy is a Python library designed to perform matrix computation. Matrix computation is something that is used a lot in science in general and data science in particular, so it is necessary to have a library that does this very well.

Its name stands for numerical python

Its main object is the

, which encapsulates **ndarray**

dimension arrays of homogeneous data types, unlike Python lists that can have data of different types.**n**

Numpy aims to perform matrix computation much faster than with Python lists, but how is this possible?

- Numpy uses compiled code, while Python uses interpreted code. The difference is that Python at runtime has to interpret, compile and execute the code, while in Numpy it is already compiled, so it runs faster.
- The

s have a fixed size, unlike the Python lists that are dynamic. If in Numpy you want to modify the size of an array, a new one will be created and the old one will be deleted.**ndarray** - All elements of the

s are of the same data type, unlike Python lists that can have elements of different types.**ndarray** - Part of the Numpy code is written in C/C++ (much faster than Python).
- Array data is stored in memory continuously, unlike Python lists, which makes it much faster to manipulate it.

Numpy offers the facility of using code that is simple to write and read, but is written and precompiled in C, which makes it much faster.

Suppose we want to multiply two vectors, this would be done in C in the following way

`for (i = 0; i < rows; i++): {`

for (j = 0; j < columns; j++): {

c[i][j] = a[i][j]*b[i][j];

}

}

Numpy offers the possibility of executing this code underneath, but much easier to write and understand by means of

“python

c = a * b

**`**

Numpy offers vectorized code, which means not having to write loops, but nevertheless if they are being executed underneath in optimized and precompiled C code. This has the following advantages:

- The code is easier to write and read.
- Fewer lines of code mean fewer errors are likely to be introduced.
- The code looks more like mathematical notation.

Generally when importing Numpy it is usually imported with the alias

.**np**

import numpy as np

print(np.__version__)

As explained Numpy performs the calculation much faster than Python lists, let’s see an example in which the scalar product of two matrices is performed, using Python lists and using

s**ndarray**

from time import time

# Dimensión de las matrices

dim = 1000

shape = (dim, dim)

# Se crean dos ndarrays de Numpy de dimensión dim x dim

ndarray_a = np.ones(shape=shape)

ndarray_b = np.ones(shape=shape)

# Se crean dos listas de Python de dimensión dim x dim a partir de los ndarrays

list_a = list(ndarray_a)

list_b = list(ndarray_b)

# Se crean el ndarray y la lista de Python donde se guardarán los resultados

ndarray_c = np.empty(shape=shape)

list_c = list(ndarray_c)

# Producto escalar de dos listas de python

t0 = time()

for fila in range(dim):

for columna in range(dim):

list_c[fila][columna] = list_a[fila][columna] * list_b[fila][columna]

t = time()

t_listas = t-t0

print(f"Tiempo para realizar el producto escalar de dos listas de Python de dimensiones {dim}x{dim}: {t_listas:.4f} ms")

# Producto escalar de dos ndarrays de Numpy

t0 = time()

ndarray_c = ndarray_a * ndarray_b

t = time()

t_ndarrays = t-t0

print(f"Tiempo para realizar el producto escalar de dos ndarrays de Numpy de dimensiones {dim}x{dim}: {t_ndarrays:.4f} ms")

# Comparación de tiempos

print(f"\nHacer el cálculo con listas de Python tarda {t_listas/t_ndarrays:.2f} veces más rápido que con ndarrays de Numpy")

In Numpy an array is an

object.**ndarray**

arr = np.array([1, 2, 3, 4, 5])

print(arr)

print(type(arr))

With the

method you can create **array()**

s by entering Python lists (as the example above), or tuples**ndarray**

arr = np.array((1, 2, 3, 4, 5))

print(arr)

print(type(arr))

With the

method you can create arrays filled with zeros.**zeros()**

arr = np.zeros((3, 4))

print(arr)

The

method returns an array with the same shape as the array A, but filled with zeros.**zeros_like(A)**

A = np.array((1, 2, 3, 4, 5))

arr = np.zeros_like(A)

print(arr)

With the

method it is possible to create arrays filled with ones**ones()**

arr = np.ones((4, 3))

print(arr)

The

method returns an array with the same shape as the array A, but filled with zeros.**ones_like(A)**

A = np.array((1, 2, 3, 4, 5))

arr = np.ones_like(A)

print(arr)

With the

method you can create arrays with the dimensions you want, but randomly initialized.**empty()**

arr = np.empty((6, 3))

print(arr)

The

method returns an array with the same shape as the array A, but randomly initialized.**empty_like(A)**

A = np.array((1, 2, 3, 4, 5))

arr = np.empty_like(A)

print(arr)

With the

method you can create arrays in a given range. This method is similar to Python’s **arange(start, stop, step)**

method.**range()**

arr = np.arange(10, 30, 5)

print(arr)

When

is used with floating point arguments, it is generally not possible to predict the number of elements obtained, because floating point precision is finite.**arange**

For this reason, it is usually better to use the

function that receives as argument the number of elements we want, instead of the **linspace(start, stop, n)**

step**linspace(start, stop, n)**

arr = np.linspace(0, 2, 9)

print(arr)

Finally, if we want to create arrays with random numbers we can use the

function with a tuple with the dimensions as a parameter**random.rand**

arr = np.random.rand(2, 3)

print(arr)

In Numpy we can create arrays of any dimension. To get the dimension of an array we use the

method**ndim**

Matrix of dimension 0, which would be equivalent to one number

arr = np.array(42)

print(arr)

print(arr.ndim)

Matrix of dimension 1, which would be equivalent to a vector

arr = np.array([1, 2, 3, 4, 5])

print(arr)

print(arr.ndim)

Matrix of dimension 2, which is equivalent to a matrix of dimension 2, which is equivalent to a matrix of dimension 2, which is equivalent to a matrix of dimension 3.

arr = np.array([[1, 2, 3, 4, 5],

[6, 7, 8, 9, 10]])

print(arr)

print(arr.ndim)

Dimension 3 matrix

arr = np.array([

[[1, 2, 3, 4, 5],

[6, 7, 8, 9, 10]],

[[11, 12, 13, 14, 15],

[16, 17, 18, 19, 20]]

])

print(arr)

print(arr.ndim)

Array of dimension N. When creating

s, the number of dimensions can be set by means of the **ndarray**

parameter**ndim**

arr = np.array([1, 2, 3, 4, 5], ndmin=6)

print(arr)

print(arr.ndim)

If instead of the dimension of the guideline, we want to see the size of the guideline, we can use the

method**shape**

arr = np.array([

[[1, 2, 3, 4, 5],

[6, 7, 8, 9, 10]],

[[11, 12, 13, 14, 15],

[16, 17, 18, 19, 20]]

])

print(arr.shape)

The data that Numpy arrays can store are as follows:

– Entero**i**- ‘b’ – Booleano

– Unsigned integer**u**

– Floating**f**

– Floating complex**c**

– Timedelta**m**

– Datetime**M**

– Object**O**

– String**S**

– Unicode string**U**

– Fixed memory fragment for another type (void)**V**

We can check the data type of an array using

.**dtype**

arr = np.array([1, 2, 3, 4])

print(arr.dtype)

arr = np.array(['apple', 'banana', 'cherry'])

print(arr.dtype)

We can also create arrays by indicating the type of data we want it to have through

.**dtype**

arr = np.array([1, 2, 3, 4], dtype='i')

print("Enteros:")

print(arr)

print(arr.dtype)

arr = np.array([1, 2, 3, 4], dtype='f')

print("\nFloat:")

print(arr)

print(arr.dtype)

arr = np.array([1, 2, 3, 4], dtype='f')

print("\nComplejos:")

print(arr)

print(arr.dtype)

arr = np.array([1, 2, 3, 4], dtype='S')

print("\nString:")

print(arr)

print(arr.dtype)

arr = np.array([1, 2, 3, 4], dtype='U')

print("\nUnicode string:")

print(arr)

print(arr.dtype)

arr = np.array([1, 2, 3, 4], dtype='O')

print("\nObjeto:")

print(arr)

print(arr.dtype)

Matrix operations are performed by elements, for example, if we add two matrices, the elements of each matrix of the same position will be added, as is done in the mathematical addition of two matrices.

A = np.array([1, 2, 3])

B = np.array([1, 2, 3])

print(f"Matriz A: tamaño {A.shape}\n{A}\n")

print(f"Matriz B: tamaño {B.shape}\n{B}\n")

C = A + B

print(f"Matriz C: tamaño {C.shape}\n{C}\n")

D = A - B

print(f"Matriz D: tamaño {D.shape}\n{D}")

However, if we multiply two matrices, we also multiply each element of the matrices (scalar product).

A = np.array([[3, 5], [4, 1]])

B = np.array([[1, 2], [-3, 0]])

print(f"Matriz A: tamaño {A.shape}\n{A}\n")

print(f"Matriz B: tamaño {B.shape}\n{B}\n")

C = A * B

print(f"Matriz C: tamaño {C.shape}\n{C}\n")

To make the matrix product that has been taught in mathematics all your life you have to use the operator

or the **@**

method.**dot**

A = np.array([[3, 5], [4, 1], [6, -1]])

B = np.array([[1, 2, 3], [-3, 0, 4]])

print(f"Matriz A: tamaño {A.shape}\n{A}\n")

print(f"Matriz B: tamaño {B.shape}\n{B}\n")

C = A @ B

print(f"Matriz C: tamaño {C.shape}\n{C}\n")

D = A.dot(B)

print(f"Matriz D: tamaño {D.shape}\n{D}")

If instead of creating a new matrix, you want to modify an existing one, you can use the

, **+=**

or **-=**

switches.***=**

A = np.array([[3, 5], [4, 1]])

B = np.array([[1, 2], [-3, 0]])

print(f"Matriz A: tamaño {A.shape}\n{A}\n")

print(f"Matriz B: tamaño {B.shape}\n{B}\n")

A += B

print(f"Matriz A tras suma: tamaño {A.shape}\n{A}\n")

A -= B

print(f"Matriz A tras resta: tamaño {A.shape}\n{A}\n")

A *= B

print(f"Matriz A tras multiplicación: tamaño {A.shape}\n{A}\n")

It is possible to perform operations on all the elements of an array, this is thanks to a property called

that we will see later in more depth**brodcasting**

A = np.array([[3, 5], [4, 1]])

print(f"Matriz A: tamaño {A.shape}\n{A}\n")

B = A * 2

print(f"Matriz B: tamaño {B.shape}\n{B}\n")

C = A ** 2

print(f"Matriz C: tamaño {C.shape}\n{C}\n")

D = 2*np.sin(A)

print(f"Matriz D: tamaño {D.shape}\n{D}")

As you can see in the last calculation, Numpy offers function operators on matrices, there are a lot of functions that can be performed on matrices, mathematical, logical, linear algebra, etc. Here are some of them

A = np.array([[3, 5], [4, 1]])

print(f"A\n{A}\n")

print(f"exp(A)\n{np.exp(A)}\n")

print(f"sqrt(A)\n{np.sqrt(A)}\n")

print(f"cos(A)\n{np.cos(A)}\n")

There are some functions that return information from the matrices, such as the average

A = np.array([[3, 5], [4, 1]])

print(f"A\n{A}\n")

print(f"A.mean()\n{A.mean()}\n")

However, we can obtain this information for each axis by means of the

attribute, if this is 0 it is done on each column, while if it is 1 it is done on each row.**axis**

A = np.array([[3, 5], [4, 1]])

print(f"A\n{A}\n")

print(f"A.mean() columnas\n{A.mean(axis=0)}\n")

print(f"A.mean() filas\n{A.mean(axis=1)}\n")

Matrix operations can be performed with matrices of different dimensions. In this case Numpy will detect this and make a projection of the smaller matrix until it equals the larger one.

This is a great quality of Numpy, which makes it possible to perform calculations on matrices without having to worry about matching their dimensions.

A = np.array([1, 2, 3])

print(f"A\n{A}\n")

B = A + 5

print(f"B\n{B}\n")

A = np.array([1, 2, 3])

B = np.ones((3,3))

print(f"A\n{A}\n")

print(f"B\n{B}\n")

C = A + B

print(f"C\n{C}\n")

A = np.array([1, 2, 3])

B = np.array([[1], [2], [3]])

print(f"A\n{A}\n")

print(f"B\n{B}\n")

C = A + B

print(f"C\n{C}\n")

The indexing of arrays is done in the same way as with Python lists.

arr = np.array([1, 2, 3, 4, 5])

arr[3]

In the case of having more than one dimension, the index must be indicated in each one of them.

arr = np.array([[1, 2, 3, 4, 5],

[6, 7, 8, 9, 10]])

arr[1, 2]

Negative indexing can be used

arr[-1, -2]

If you do not indicate one of the axes, it is considered that you want an integer.

arr = np.array([[1, 2, 3, 4, 5],

[6, 7, 8, 9, 10]])

arr[1]

When indexing we can keep parts of arrays just as we used to do with Python lists.

Remember that it was done as follows:

**start:stop:step**

Where the range goes from

(included) to **start**

(not included) with a step of **stop**

.**step**

If

is not specified default is 1**step**

For example, if we want items from the second row and from the second to the fourth column:

- We select the second row with a 1 (since we start counting from 0).
- We select from the second to the fourth row using 1:4, the 1 to indicate the second column and the 4 to indicate the fifth (since the second number indicates the column in which it ends without including this column). The two numbers taking into account that we start counting from 0

print(arr)

print(arr[1, 1:4])

We can take from one position to the end

arr[1, 2:]

From the beginning to a position

arr[1, :3]

Set the range with negative numbers

arr[1, -3:-1]

Choose the step

arr[1, 1:4:2]

The iteration over multidimensional matrices is performed with respect to the first axis

M = np.array( [[[ 0, 1, 2],

[ 10, 12, 13]],

[[100,101,102],

[110,112,113]]])

print(f'Matriz de dimensión: {M.shape}\n')

i = 0

for fila in M:

print(f'Fila {i}: {fila}')

i += 1

However, if we want to iterate for each item we can use the ‘flat’ method

i = 0

for fila in M.flat:

print(f'Elemento {i}: {fila}')

i += 1

In Numpy we have two ways to copy arrays, by

, which makes a new copy of the array, and by **copy**

which makes a view of the original array.**view**

The copy owns the data and any changes made to the copy will not affect the original matrix, and any changes made to the original matrix will not affect the copy.

The view does not own the data and any changes made to the copy will affect the original matrix, and any changes made to the original matrix will affect the copy.

arr = np.array([1, 2, 3, 4, 5])

copy_arr = arr.copy()

arr[0] = 42

copy_arr[1] = 43

print(f'Original: {arr}')

print(f'Copia: {copy_arr}')

arr = np.array([1, 2, 3, 4, 5])

view_arr = arr.view()

arr[0] = 42

view_arr[1] = 43

print(f'Original: {arr}')

print(f'Vista: {view_arr}')

When in doubt whether we have a copy or a view we can use

.**base**

arr = np.array([1, 2, 3, 4, 5])

copy_arr = arr.copy()

view_arr = arr.view()

print(copy_arr.base)

print(view_arr.base)

We can know the shape of the array using the

method. This will return a tuple, the size of the tuple represents the dimensions of the array, in each element of the tuple the number of items in each of the dimensions of the array is indicated.**shape**

arr = np.array([

[[1, 2, 3, 4, 5],

[6, 7, 8, 9, 10]],

[[11, 12, 13, 14, 15],

[16, 17, 18, 19, 20]]

])

print(arr)

print(arr.shape)

We can change the shape of the matrices to the one we want using the

method.**reshape**

For example, the matrix above, which has a form of

. We can pass it to **(2, 2, 4)**

.**(5, 4)**

arr_reshape = arr.reshape(5, 4)

print(arr_reshape)

print(arr_reshape.shape)

Note that to resize the matrices, the number of items in the new form must have the same number of items as in the first form.

That is, in the previous example, the first matrix had 20 items (2x2x4), and the new matrix has 20 items (5×4). What we cannot do is to resize it to a matrix of size (3, 4), since there would be a total of 12 items.

arr_reshape = arr.reshape(3, 4)

In case we want to change the shape of a matrix and one of the dimensions is unknown to us, we can have Numpy calculate it for us by entering a

as a parameter**-1**

arr = np.array([

[[1, 2, 3, 4, 5],

[6, 7, 8, 9, 10]],

[[11, 12, 13, 14, 15],

[16, 17, 18, 19, 20]]

])

arr_reshape = arr.reshape(2, -1)

print(arr_reshape)

print(arr_reshape.shape)

Note that you cannot put just any number in the known dimensions. The number of items in the original matrix must be a multiple of the known dimensions.

In the above example, the matrix has 20 items, which is a multiple of 2, the known dimension entered. It would not have been possible to put a 3 as the known dimension, since 20 is not a multiple of 3, and there would be no number that could be put in the unknown dimension that would make a total of 20 items.

We can flatten the matrices, that is, pass them to a single dimension by

. In this way, whatever the dimensions of the original matrix, the new one will always have only one dimension.**reshape(-1)**

arr = np.array([

[[1, 2, 3, 4, 5],

[6, 7, 8, 9, 10]],

[[11, 12, 13, 14, 15],

[16, 17, 18, 19, 20]]

])

arr_flatten = arr.reshape(-1)

print(arr_flatten)

print(arr_flatten.shape)

Another way to flatten a matrix is by using the

method.**ravel()**

arr = np.array([

[[1, 2, 3, 4, 5],

[6, 7, 8, 9, 10]],

[[11, 12, 13, 14, 15],

[16, 17, 18, 19, 20]]

])

arr_flatten = arr.ravel()

print(arr_flatten)

print(arr_flatten.shape)

The transpose of a matrix can be obtained using the

method. To do the transpose of a matrix is to interchange the rows and columns of the matrix, in the following image you can see an example that clarifies it more clearly**T**

arr = np.array([[1, 0, 4],

[0, 5, 0],

[6, 0, -9]])

arr_t = arr.T

print(arr_t)

print(arr_t.shape)

Matricecs can be stacked vertically (joining rows) using the

method.**vstack()**

a = np.array([[1, 1, 1],

[2, 2, 2],

[3, 3, 3]])

b = np.array([[4, 4, 4],

[5, 5, 5],

[6, 6, 6]])

c = np.vstack((a,b))

c

If you have arrays of more than 2 dimensions

will stack along the first dimension.**vsatck()**

a = np.array([

[[1, 1],

[2, 2]],

[[3, 3],

[4, 4]]

])

b = np.array([

[[5, 5],

[6, 6]],

[[7, 7],

[8, 8]]

])

c = np.vstack((a,b))

c

You can stack matrices horizontally (joining columns) using the

method.**hstack()**

a = np.array([[1, 2, 3],

[1, 2, 3],

[1, 2, 3]])

b = np.array([[4, 5, 6],

[4, 5, 6],

[4, 5, 6]])

c = np.hstack((a,b))

c

If you have arrays of more than 2 dimensions

will stack along the second dimension.**hsatck()**

a = np.array([

[[1, 1],

[2, 2]],

[[3, 3],

[4, 4]]

])

b = np.array([

[[5, 5],

[6, 6]],

[[7, 7],

[8, 8]]

])

c = np.hstack((a,b))

c

Another way to add columns to a matrix is by using the

method.**column_stack()**

a = np.array([[1, 2, 3],

[1, 2, 3],

[1, 2, 3]])

b = np.array([4, 4, 4])

c = np.column_stack((a,b))

c

Arrays can be stacked in depth (third dimension) using the

method.**dstack()**

a = np.array([

[[1, 1],

[2, 2]],

[[3, 3],

[4, 4]]

])

b = np.array([

[[1, 1],

[2, 2]],

[[3, 3],

[4, 4]]

])

c = np.dstack((a,b))

print(f"c: {c}\n")

print(f"a.shape: {a.shape}, b.shape: {b.shape}, c.shape: {c.shape}")

If you have arrays of more than 4 dimensions

will stack along the third dimension.**dsatck()**

a = np.array([1, 2, 3, 4, 5], ndmin=4)

b = np.array([1, 2, 3, 4, 5], ndmin=4)

c = np.dstack((a,b))

print(f"a.shape: {a.shape}, b.shape: {b.shape}, c.shape: {c.shape}")

Using the

method you can choose the axis on which you want to stack the arrays**concatenate()**

a = np.array([

[[1, 1],

[2, 2]],

[[3, 3],

[4, 4]]

])

b = np.array([

[[5, 5],

[6, 6]],

[[7, 7],

[8, 8]]

])

conc0 = np.concatenate((a,b), axis=0) # concatenamiento en el primer eje

conc1 = np.concatenate((a,b), axis=1) # concatenamiento en el segundo eje

conc2 = np.concatenate((a,b), axis=2) # concatenamiento en el tercer eje

print(f"conc0: {conc0}\n")

print(f"conc1: {conc1}\n")

print(f"conc2: {conc2}")

Matricecs can be split vertically (by separating rows) using the

method.**vsplit()**

a = np.array([[1.1, 1.2, 1.3, 1.4],

[2.1, 2.2, 2.3, 2.4],

[3.1, 3.2, 3.3, 3.4],

[4.1, 4.2, 4.3, 4.4]])

[a1, a2] = np.vsplit(a, 2)

print(f"a1: {a1}\n")

print(f"a2: {a2}")

If you have matrices of more than 2 dimensions

will divide along the first dimension.**vsplit()**

a = np.array([

[[1, 1],

[2, 2]],

[[3, 3],

[4, 4]]

])

[a1, a2] = np.vsplit(a, 2)

print(f"a1: {a1}\n")

print(f"a2: {a2}")

You can split matrices horizontally (separating columns) using the

method.**hsplit()**

a = np.array([[1.1, 1.2, 1.3, 1.4],

[2.1, 2.2, 2.3, 2.4],

[3.1, 3.2, 3.3, 3.4],

[4.1, 4.2, 4.3, 4.4]])

[a1, a2] = np.hsplit(a, 2)

print(f"a1: {a1}\n")

print(f"a2: {a2}")

If you have matrices of more than 2 dimensions

will divide along the second dimension.**hsplit()**

a = np.array([

[[1, 1],

[2, 2]],

[[3, 3],

[4, 4]]

])

[a1, a2] = np.hsplit(a, 2)

print(f"a1: {a1}\n")

print(f"a2: {a2}")

By means of the

method you can choose the axis on which you want to split the arrays**array_split()**

a = np.array([

[[1, 1],

[2, 2]],

[[3, 3],

[4, 4]]

])

[a1_eje0, a2_eje0] = np.array_split(a, 2, axis=0)

[a1_eje1, a2_eje1] = np.array_split(a, 2, axis=1)

[a1_eje2, a2_eje2] = np.array_split(a, 2, axis=2)

print(f"a1_eje0: {a1_eje0}\n")

print(f"a2_eje0: {a2_eje0}\n\n")

print(f"a1_eje1: {a1_eje1}\n")

print(f"a2_eje1: {a2_eje1}\n\n")

print(f"a1_eje2: {a1_eje2}\n")

print(f"a2_eje2: {a2_eje2}")

If you want to search for a value within an array you can use the

method which returns the positions where the array is worth the value you are looking for.**where()**

arr = np.array([1, 2, 3, 4, 5, 4, 4])

ids = np.where(arr == 4)

ids

You can use functions to search, for example, if you want to search in which positions the values are pairs

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

ids = np.where(arr%2)

ids

By means of the

method we can sort arrays**sort()**

arr = np.array([3, 2, 0, 1])

arr_ordenado = np.sort(arr)

arr_ordenado

If what we have are strings, it sorts them alphabetically

arr = np.array(['banana', 'apple', 'cherry'])

arr_ordenado = np.sort(arr)

arr_ordenado

And the Boolean arrays are also sorted by

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

arr_ordenado = np.sort(arr)

arr_ordenado

If you have matrices of more than one dimension, it orders them by dimensions, that is, if you have a 2-dimensional matrix, it orders the numbers of the first row among them and those of the second row among them.

arr = np.array([[3, 2, 4], [5, 0, 1]])

arr_ordenado = np.sort(arr)

arr_ordenado

By default it always sorts with respect to rows, but if you want it to sort with respect to another dimension, you have to specify it with the variable

.**axis**

arr = np.array([[3, 2, 4], [5, 0, 1]])

arr_ordenado0 = np.sort(arr, axis=0) # Se ordena con respecto a la primera dimensión

arr_ordenado1 = np.sort(arr, axis=1) # Se ordena con respecto a la segunda dimensión

print(f"arr_ordenado0: {arr_ordenado0}\n")

print(f"arr_ordenado1: {arr_ordenado1}\n")

Numpy offers the ability to search for certain elements of an array and create a new one.

This is done by creating an array of Boolean indexes, i.e., it creates a new array that indicates which positions in the array we keep and which we do not keep.

Let’s see an example of a Boolean index array

arr = np.array([37, 85, 12, 45, 69, 22])

indices_booleanos = [False, False, True, False, False, True]

arr_filter = arr[indices_booleanos]

print(f"Array original: {arr}")

print(f"indices booleanos: {indices_booleanos}")

print(f"Array filtrado: {arr_filter}")

As you can see, the filtered array (

), has only been left from the original array (**arr_filetr**

) with the elements that match those where the array **arr**

is **indices_booleans**

.**True**

Another thing we can see is that it has only kept the even elements, so now we will see how to keep the even elements of an array, without having to do it by hand as we have done in the previous example

arr = np.array([[1, 2, 3, 4, 5],

[6, 7, 8, 9, 10]])

indices_booleanos = arr % 2 == 0

arr_filter = arr[indices_booleanos]

print(f"Array original: {arr}\n")

print(f"indices booleanos: {indices_booleanos}\n")

print(f"Array filtrado: {arr_filter}")