MÓDULO NumPy

Los módulos NumPy (Numerical Python) y SciPy proporcionan funciones y rutinas matemáticas para la manipulación de arrays y matrices de datos numéricos de una forma eficiente.

  • El módulo SciPy extiende la funcionalidad de NumPy con una colección de algoritmos matemáticos (minimización, transformada de Fourier, regresión, ...).

NumPy proporciona:

  • El objeto ndarray: Un array multidimensional que permite realizar operaciones aritméticas sobre vectores muy eficientes. Colección de funciones matemáticas muy eficientes que operan sobre vectores (ndarrays) sin necesidad de escribir bucles (for o while).
  • Son más eficientes y rápidas que las operaciones sobre listas.


LOS ARRAYS DE NUMPY: NDARRAY

¿Qué es un array?

En programación se denomina matriz, vector (de una sola dimensión) o formación (en inglés array) a una zona de almacenamiento contiguo que contiene una serie de elementos del mismo tipo. Desde el punto de vista lógico, una matriz se puede ver como un conjunto de elementos ordenados en fila (o filas y columnas si tuviera dos dimensiones).

*``Matriz unidimensional de 10 elementos``*


En principio, se puede considerar que todas las matrices son de una dimensión, la dimensión principal, pero los elementos de dicha fila pueden ser a su vez matrices (un proceso que puede ser recursivo), lo que nos permite hablar de la existencia de matrices multidimensionales, aunque las más fáciles de imaginar son los de una, dos y tres dimensiones.

Estas estructuras de datos son adecuadas para situaciones en las que el acceso a los datos se realice de forma aleatoria e impredecible. Por el contrario, si los elementos pueden estar ordenados y se va a utilizar acceso secuencial sería más adecuado utilizar una lista, ya que esta estructura puede cambiar de tamaño fácilmente durante la ejecución de un programa.

  • En NumPy el tipo fundamental es el array multidimensional: el objeto ndarray.
  • Los ndarrays son similares a las listas en Python, con la diferencia de que todos los elementos de un ndarray son del mismo tipo.

Para usar NumPy lo primero que debemos hacer es importarlo. Para importar el módulo NumPy debemos la siguiente instruccion:


In [104]:
import numpy as np

La instrucción anterior permite utilizar las funciones del módulo NumPy con el nombre abreviado np en lugar de numpy.


In [105]:
# Para crear un array escribimos lo siguiente:
a = np.array([[1,2,3],[4,5,6], [7,8,9]])
a


Out[105]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

Permite realizar operaciones sobre los arrays multidimensionales como si se tratara de operaciones sobre escalares:


In [106]:
# Se puede, por ejemplo, multiplcar facilmente todos los elementos de un array por un número 
a * 10


Out[106]:
array([[10, 20, 30],
       [40, 50, 60],
       [70, 80, 90]])

Propiedades de los objetos ndarray:

  • La propiedad shape que indica las dimensiones del array.
  • La propiedad dtype indica el tipo de los elementos almacenados en el array.

    Tipos para dtype:
    • int8: Byte (-128 to 127)
    • int16: Integer (-32768 to 32767)
    • int32: Integer (-2147483648 to 2147483647)
    • int64: Integer (-9223372036854775808 to 9223372036854775807)
    • uint8: Unsigned integer (0 to 255)
    • uint16: Unsigned integer (0 to 65535)
    • uint32: Unsigned integer (0 to 4294967295)
    • uint64: Unsigned integer (0 to 18446744073709551615)
    • float16: Half precision float: sign bit, 5 bits exponent, 10 bits mantissa
    • float32: Single precision float: sign bit, 8 bits exponent, 23 bits mantissa
    • float64: Double precision float: sign bit, 11 bits exponent, 52 bits mantissa

In [107]:
# 'shape' --> Filas, Columnas
a.shape, a.dtype


Out[107]:
((3, 3), dtype('int32'))


CREACIÓN DE ARRAYS

Existen varias formas para crear un array.

  • La forma más sencilla de crear un array es utilizando la función array y una lista de objetos, que pueden ser otros arrays:

In [108]:
# Definimos el array 'a'
a = np.array( [2,3,4] )
# Definimos el array 'b'
b = np.array( [1.2, 3.5, 5] )
# Nos devuelve el tipo de datos almacenados en cada array
a.dtype,b.dtype


Out[108]:
(dtype('int32'), dtype('float64'))

In [109]:
# Recuerda que con la función type() puedes saber el tipo de dato de la variable por la que preguntes
type(a)


Out[109]:
numpy.ndarray

El módulo NumPy nos permite crear arrays de un cierto tipo, lo que garantizará la eficiencia de las operaciones que se vayan a realizar con los `mismos.


In [110]:
# Creación de un array de 2f, 2r (2 files, 2 rows) con formato float16
a = np.array([[1,2],[3,4]], dtype='float16')
print(a)
a.shape , a.dtype


[[ 1.  2.]
 [ 3.  4.]]
Out[110]:
((2, 2), dtype('float16'))

Acabamos de crear un array de 2 filas y 2 columnas de reales. El argumento dtype sirve para especificar la precisión. Los elementos de un array suelen ser desconocidos, pero lo normal es que el tamaño de los datos sea conocido.

Las funciones zeros y ones permiten crear un array cuyo contenido son todo ceros y unos, respectivamente. Hay que indicar el tamaño de las dimensiones del array mediante una tupla o una lista.


In [111]:
# Array de todo ceros, 1 dimensión y 10 elementos
a1 = np.zeros(10)
print(a1)
a1.dtype


[ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
Out[111]:
dtype('float64')

In [112]:
# Array de 3 filas, 4 columnas
a2 = np.zeros((3,4))
print(a2)
a2.dtype


[[ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]]
Out[112]:
dtype('float64')

In [113]:
# Array de 3 filas, 4 columnas
# Se puede especificar el tipo de datos al crear el array
a3 = np.zeros((3,4), dtype='int32')
print(a3)
a3.dtype


[[0 0 0 0]
 [0 0 0 0]
 [0 0 0 0]]
Out[113]:
dtype('int32')

Las funciones zeros_like y ones_like son análogas a zeros y ones pero no es necesario indicar las dimensiones, solo hay que indicar el array del cual queremos copiar las dimensiones.


In [114]:
# Creamos el array 'a4' a partir del array 'a'
print(a)
a4 = np.ones_like(a)
a4


[[ 1.  2.]
 [ 3.  4.]]
Out[114]:
array([[ 1.,  1.],
       [ 1.,  1.]], dtype=float16)

Crear un array mediante una secuencia de números con cierto criterio. Utilizamos las funciones arange y linspace.


In [115]:
# Creamos el array 'a6' con números del 0 al 10(excluído) con pasos 0.5
# La función 'range' crea un objeto iterable de enteros. La función 'arange' crea un objeto de tipo ndarray.
# arange(incio, fin, salto)
a6 = np.arange(0, 10, .5)
print(a6)
a6.dtype, a.shape


[ 0.   0.5  1.   1.5  2.   2.5  3.   3.5  4.   4.5  5.   5.5  6.   6.5  7.
  7.5  8.   8.5  9.   9.5]
Out[115]:
(dtype('float64'), (2, 2))

In [116]:
c = np.arange(0 , 2 , .3)
print(c)


[ 0.   0.3  0.6  0.9  1.2  1.5  1.8]

Como la función arange utiliza argumentos de tipo float, no es posible predecir el número de elementos del array. En ese caso es mejor utilizar la función linspace que genera un array con un número determinado de elementos sin indicar el paso.


In [117]:
# Creamos un array de 10 números del 0 al 2
# linspace(inicio, fin, nº de elementos)
e = np.linspace( 0, 2, 10 ) 
print(e)


[ 0.          0.22222222  0.44444444  0.66666667  0.88888889  1.11111111
  1.33333333  1.55555556  1.77777778  2.        ]

In [118]:
"""generación de números que se usa habitualmente en la evalución de funciones"""
# Importamos las librerias
import math as mt
import matplotlib.pyplot as plt
import numpy as np

%pylab inline

# Representación gráfica de la funcion seno(2*pi)
# Creamos un array de 100 elementos entre 0 y 2*pi
x = np.linspace( 0, 2*mt.pi, 100 )
y = np.sin(x)
plt.plot(x, y);


Populating the interactive namespace from numpy and matplotlib
C:\ProgramData\Anaconda3\lib\site-packages\IPython\core\magics\pylab.py:161: UserWarning: pylab import has clobbered these variables: ['e']
`%matplotlib` prevents importing * from pylab and numpy
  "\n`%matplotlib` prevents importing * from pylab and numpy"

Creación con datos aleatorios mediante la función rand del módulo Random. La función rand devuelve un número aleatorio procedente de una distribución uniforme en el intervalo [0,1).


In [119]:
# Genera 10 números aleatorios entre el 0 y el 1
a1 = np.random.rand(10)
print (a1)
a1.dtype


[ 0.15488194  0.28268175  0.85292209  0.26504539  0.61551371  0.93789024
  0.1196295   0.23473603  0.55394442  0.66060065]
Out[119]:
dtype('float64')

In [120]:
# Representación gráfica de la función rand para 100 elementos
y = np.random.rand(100)
plt.plot(y);



In [121]:
# Genera un array de 3 filas y 4 columnas, con valores aleatorios [0,1)
a2 = np.random.rand(3, 4)
print(a2)
a2.dtype


[[ 0.82129265  0.78095593  0.87298614  0.48199395]
 [ 0.3051632   0.59703883  0.49777872  0.6211044 ]
 [ 0.28901523  0.76866413  0.66169332  0.6960465 ]]
Out[121]:
dtype('float64')


OPERACIONES ENTRE ARRAYS Y ESCALARES

Los operadores aritméticos aplicados a arrays, se aplican elemento a elemento.

  • Para que la operación tenga éxito, los arrays implicados han de tener la misma dimensión.
  • El resultado es un nuevo array cuyos datos depende de la operación realizada.

In [122]:
# Creamos dos arrays 'a' y 'b' de tipo entero(int) y una dimensión
a = np.array([1,2,3], int)
b = np.array([4,5,6], int)

# Operaciones entre arrays
rs = a + b
rr = a - b
rp = a * b
rd = b / a
rm = a % b
re = b ** a

# Operaciones entre arrays y escalares
rp_esc = a * 10
re_esc = b ** 2

print('Operaciones vectoriales')
print('-----------------------')
print("suma:     ", rs)
print("producto: ", rp)
print("potencia: ", re)
print()
print('Operaciones escalares')
print('---------------------')
print(rp_esc)


Operaciones vectoriales
-----------------------
suma:      [5 7 9]
producto:  [ 4 10 18]
potencia:  [  4  25 216]

Operaciones escalares
---------------------
[10 20 30]

En el caso de arrays multidimensionales, se sigue manteniendo que las operaciones se realizan elemento a elemento. Por ejemplo en el caso de dos dimensiones, el producto de dos arrays no se corresponde con la multiplicación de matrices según la conocemos.


In [123]:
# Creamos dos arrays 'a' y 'b' de tipo real(float) y 2x2 dimensiones
a = np.array([[1,2], [3,4]], float)
b = np.array([[2,0], [1,3]], float)
print(a)
print(b)


[[ 1.  2.]
 [ 3.  4.]]
[[ 2.  0.]
 [ 1.  3.]]

In [124]:
# Las operaciones vectoriales en los arrays multidimensionales se ejecutan como en los unidimensionales
p = a * b
print(p)


[[  2.   0.]
 [  3.  12.]]

El producto matricial se obtiene mediante el uso de la función dot o creando objetos de tipo matrix en lugar de array.

Como recordatorio tenemos esta imagen que nos dice como se calcula el producto de dos matrices:


Para calcular el producto, de una manera mas visual, sería como sigue:
1x3 + 7x5 1x3 + 7x2 = 38 17
2x3 + 4x5 2x3 + 4x2 = 26 14


In [125]:
# Creamos dos arrays de 2x2
A = np.array( [[1,7],
               [2,4]] )
B = np.array( [[3,3],
               [5,2]] )

# Para multiplicar dos matrices (producto matricial), tal y como se hace en matemáticas se usa la función 'dot'
C = np.dot(A,B)
print(C)


[[38 17]
 [26 14]]


FUNCIONES UNIVERSALES

Se trata de funciones que actúan sobre cada uno de los elementos de un array.


In [126]:
# Creamos un array de 2x2 y de tipo float
a = np.array([[82.,-25], [12,-4]], float)
a


Out[126]:
array([[ 82., -25.],
       [ 12.,  -4.]])

In [127]:
# Uso de la función 'abs()' que nos devuelve el valor absoluto (sin números negativos)
abs(a)


Out[127]:
array([[ 82.,  25.],
       [ 12.,   4.]])

In [128]:
# Función que muestra los valores máximos al comparar los elementos de un array, columa por columna
np.maximum([2, 3, 4],
           [1, 5, 2])


Out[128]:
array([2, 5, 4])

In [129]:
# Función que nos crea una matriz unitaria (donde todos los elementos de su diagonal son 1)
np.eye(4)


Out[129]:
array([[ 1.,  0.,  0.,  0.],
       [ 0.,  1.,  0.,  0.],
       [ 0.,  0.,  1.,  0.],
       [ 0.,  0.,  0.,  1.]])

In [130]:
# Se pueden concatenar funciones de la siguiente manera:
a = np.random.rand(4)
np.maximum(np.eye(4), a)


Out[130]:
array([[ 1.        ,  0.45099469,  0.45498682,  0.51245408],
       [ 0.48896964,  1.        ,  0.45498682,  0.51245408],
       [ 0.48896964,  0.45099469,  1.        ,  0.51245408],
       [ 0.48896964,  0.45099469,  0.45498682,  1.        ]])

In [131]:
# Compara ambas matrices y muestra true cuando se cumpla la condición >=
np.greater_equal(np.eye(4), a)


Out[131]:
array([[ True, False, False, False],
       [False,  True, False, False],
       [False, False,  True, False],
       [False, False, False,  True]], dtype=bool)


UPCASTING

Cuando se opera con arrays de distinto tipo, el tipo del array resultante es el tipo con más precisión. Este comportamiento se conoce como upcasting.


In [132]:
# Creamos dos arrays, uno con todo ceros de tipo entero (int32)
a = np.ones(3, dtype=int32)
# Creamos un array de 3 elementos entre 0 y pi, tipo float64
b = np.linspace(0, np.pi, 3)
print( "a : ", a)
print( "b : ", b)
print( "Tipo de a: " , a.dtype)
print( "Tipo de b: " , b.dtype)


a :  [1 1 1]
b :  [ 0.          1.57079633  3.14159265]
Tipo de a:  int32
Tipo de b:  float64

In [133]:
# El resultado será un array de tipo float64, pues es capaz de representar números con mayor exactitud que int32
# (sin tener en cuenta que además int32 NO es capaz de representar números decimales)
c = a + b
print("c :", c)
print("Tipo de c:", c.dtype)


c : [ 1.          2.57079633  4.14159265]
Tipo de c: float64


ELEMENTOS DE UN ARRAY: ACCESO Y RECORRIDO

Cuando trabajamos con arrays de una dimensión, el acceso a los elementos se realiza de forma similar a como se hace en el caso de listas o tuplas de elementos.


In [134]:
# Creamos un array de 10 elementos [0, 10)
a = np.arange(10)**3
a


Out[134]:
array([  0,   1,   8,  27,  64, 125, 216, 343, 512, 729], dtype=int32)

In [135]:
# Muestra la posición '2' dentro del array
a[2]


Out[135]:
8

In [136]:
# Acceso al rango de elementos [2,5) (excluye la posición 5)
a[2:5]


Out[136]:
array([ 8, 27, 64], dtype=int32)

Una diferencia importante con las listas, es que las particiones de un ndarray mediante la notación [inicio:fin:paso] son vistas del array original. Todos los cambios realizados en las vistas, se reflejan en el array original:


In [137]:
# No se define un nuevo array, solo una vista de a
# 'b' son los 5 primeros elementos de 'a'
b = a[0:5]
# Para todos los elementos de 'b' su valor será ahora 0
b[::] = 0
# Al presentar el valor de 'a' comprobamos que se han modificado los 5 primeros elementos
a


Out[137]:
array([  0,   0,   0,   0,   0, 125, 216, 343, 512, 729], dtype=int32)

Este comportamiento evita problemas de memoria. Hay que recordar que NumPy ha sido diseñado para manejar grandes cantidades de datos.

El acceso a los elementos de un array bidimensional, se realiza indicando los índices separados por una coma.


In [138]:
m = np.array([[2,4,6],[1,2,3]], dtype = 'int')
m


Out[138]:
array([[2, 4, 6],
       [1, 2, 3]])

In [139]:
# Acceso al elemento de la fila 0, columna 2
m[0, 2]


Out[139]:
6

In [140]:
# Acceso a los elementos de arrays multidimensionales
# array[elementos_fila, elementos_columna]
b = np.array([[ 0,  1,  2,  3],
              [10, 11, 12, 13],
              [20, 21, 22, 23],
              [30, 31, 32, 33],
              [40, 41, 42, 43]])

In [141]:
#Acceso a todos los elementos de la segunda columna
b[:, 1]


Out[141]:
array([ 1, 11, 21, 31, 41])

In [142]:
# Acceso a los todos los elementos de las filas 2 y 3 de todas las columnas
b[1:3, :]


Out[142]:
array([[10, 11, 12, 13],
       [20, 21, 22, 23]])

Para recorrer los elementos de un array podemos utilizar un bucle del tipo for. El siguiente ejemplo recorre las filas de la matriz b:


In [143]:
for row in b:
    print("fila: " , row)


fila:  [0 1 2 3]
fila:  [10 11 12 13]
fila:  [20 21 22 23]
fila:  [30 31 32 33]
fila:  [40 41 42 43]

Para acceder a los elementos uno por uno, podemos usar el atributo flat de los arrays:


In [144]:
for elem in b.flat:
    print(elem)


0
1
2
3
10
11
12
13
20
21
22
23
30
31
32
33
40
41
42
43


USO DE MÁSCARAS EN ARRAYS

Otra forma de acceso a partes de un ndarray es mediante un array de bool que actúa como máscara. Supongamos que queremos seleccionar la primera fila y la cuarta fila:


In [145]:
b


Out[145]:
array([[ 0,  1,  2,  3],
       [10, 11, 12, 13],
       [20, 21, 22, 23],
       [30, 31, 32, 33],
       [40, 41, 42, 43]])

In [146]:
# Creamos un array/máscara para aplicar al array 'b', el array/máscara es de tipo booleano
# En este caso cuando aparece 'True' muestra los valores de dicha linea del array, cuando es 'False' no los muestra
# Debería mostrar las líneas 0 y 3
mascara = np.array([True, False, False, True, False])
b[mascara]


Out[146]:
array([[ 0,  1,  2,  3],
       [30, 31, 32, 33]])

In [147]:
# Se puede definir la máscara como algo más complejo
# Elementos de una matriz que cumplen una cierta propiedad comparativa
# Creamos un array 'A' de 2x2
A = np.array([[22,0], [1,10]], np.float)
print(A)
# Creamos un array 'B' copia del anterior en el que se mostrará 'True' o 'False' si cumple la condición
B = A < 7.
B
# El resultado es una matriz booleana


[[ 22.   0.]
 [  1.  10.]]
Out[147]:
array([[False,  True],
       [ True, False]], dtype=bool)

In [148]:
# Ahora podemos poner a 0 todos los valores menores que 7.
B[ B < 7 ] = 0
B


Out[148]:
array([[False, False],
       [False, False]], dtype=bool)


CAMBIAR LA FORMA DE UN ARRAY

Métodos reshape, ravel y transpose(T).

A parte de la función reshape que permite redimensionar un array, nos puede interesar aplanar un array mediante la función ravel o transponer un array mediante la función transpose(T).


In [149]:
# Creamos un array de 2x2, de tipo entero
m = np.array([[2,4,6],[1,2,3]], dtype = 'int')
m


Out[149]:
array([[2, 4, 6],
       [1, 2, 3]])

In [150]:
# Uso de la función ravel() para aplanar el array
a = m.ravel()
a


Out[150]:
array([2, 4, 6, 1, 2, 3])

In [151]:
# Uso de la función transpose, para transponer un array
t = m.T
t


Out[151]:
array([[2, 1],
       [4, 2],
       [6, 3]])


Funciones vstack y hstack

A partir de 2 arrays, es posible concatenarlos por alguna de las dimensiones (por filas o por columnas) mediante las funciones vstack y hstack:


In [152]:
# Creamos dos arrays random de 2x2 mediante la función floor()
# Esta función devuelve el entero más grande no mayor que el parámetro de entrada. 
# El suelo de la escalar x es el mayor integer i, tal que i <= x
# Siendo en el ejemplo de abajo el escalar x = 10, todo valor generado por random deberá ser menor o igual a 10
a = np.floor(10*np.random.random((2,2)))
b = np.floor(10*np.random.random((2,2)))
print(a)
print(b)


[[ 9.  8.]
 [ 3.  6.]]
[[ 0.  2.]
 [ 6.  3.]]

In [153]:
# Uso de vstack para unir dos arrays por columnas
c = np.vstack((a,b))
c


Out[153]:
array([[ 9.,  8.],
       [ 3.,  6.],
       [ 0.,  2.],
       [ 6.,  3.]])

In [154]:
# Uso de vstack para unir dos arrays por filas
d = np.hstack((a,b))
d


Out[154]:
array([[ 9.,  8.,  0.,  2.],
       [ 3.,  6.,  6.,  3.]])


Copias y vistas de arrays

Cuando se manipula arrays, los datos pueden ser copiados en otro array (y se duplican los datos) o por el contrario, los arrays comparten datos aunque pueden ser accedidos mediante nombres diferentes. Veamos algunos ejemplos:


In [155]:
# Creamos un array de 12 elementos, [0,11)
a = np.arange(12)
a


Out[155]:
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

In [156]:
# No se crea un nuevo array. Ambos nombres apuntan al mismo objeto
b = a
a, b


Out[156]:
(array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11]),
 array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11]))

In [157]:
# Si modificamos el array 'b' modificaremos el array 'a'
b.shape = (4,3)
print(b)
print(a)


[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]
[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]

La función view permite crear un array cuyos datos son compartidos con otro, pero cuya forma y acceso a los datos puede ser diferente:


In [158]:
# Creamos un array 'a' de 12 elementos entre [0,11)
a = np.arange(12)
# Creamos una vista del array 'a' en 'c'
c = a.view()
# Al modificar las propiedades de 'c' no se modifica 'a'
c.shape = (4,3)
print(a)
print(c)


[ 0  1  2  3  4  5  6  7  8  9 10 11]
[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]

In [159]:
# Sin embargo al modificar el contenido del array 'c', si se modifica el array origen 'a'
c[0:3] = 0
print(c)
print(a)


[[ 0  0  0]
 [ 0  0  0]
 [ 0  0  0]
 [ 9 10 11]]
[ 0  0  0  0  0  0  0  0  0  9 10 11]

Si lo que queremos es hacer una copia completa de un array (copia de todos los datos), utilizaremos la función copy:


In [160]:
# Creamos un array de 6 elementos
a = np.arange(6)
print('a =',a)
# Creamos una copia de 'a' en 'd'
d = a.copy()
print('d =',d)
# Modificiamos los valores de 'd' y estos no modifican los de 'a' al ser arrays diferentes
d[0] = 9999
print('a =',a)
print('d =',d)


a = [0 1 2 3 4 5]
d = [0 1 2 3 4 5]
a = [0 1 2 3 4 5]
d = [9999    1    2    3    4    5]

In [ ]: