NumPy es una librería para realizar cálculo en el campo del algebra lineal. Originalmente fue derivado de un paquete de rutinas hecho para Fortran, conocido como BLAS.
NumPy es un paquete fundamental para utilizar python en computación de cálculos científicos. Esta es una librería que provee de objetos equivalentes a arreglos multidimensionales y todo un conjunto de rutinas para una operación rápida en los campos de la matemáticas, lógica, manipulación de formas, ordenamientos, entre/salida de datos, Trasformada discreta de Fourier, álgebra lineal básica, operaciones estadísticas básicas, simulaciones con números al azar y otros muchos temas.
Es una librería más importante en el uso científico de python.
Se puede conseguir muchas mas información en www.numpy.org
Como toda librería externa se debe cargar para que pueda ser usada por el notebook. El Python de por si no tiene esta librería en memoria, por lo cual, debe ser el usuario el que decida utilizarla y para ello debe "importarla". Al hacerlo cargará no sólo los algoritmos que lleva programados, sino además las Clases con las cuales se definene las variables propias de la librería. Los objetos que se usan en NumPy son básicametne arreglos que mantienen cierta similitud con los vectores y matrices del álgebra.
Primero tenemos que cargar la librería, es bastante popular llamar "np" a la libreria numPy. Aunque no es obligatorio llamarla como "np" ya es un estandard reconocido.
import numpy as np
Este comando carga la librería NumPy, pero con un cambio de nombre, la traducción literal del comando sería "Carga NumPy como np". desde esta manera Numpy de ahora en adelante se llamará np en mi programa. Es normal renombrarla como "np", aunque nada evita que se pudiera haber elegido cualquier otro nombre.
NumPy usa sus propias variables, es decir como Python es un lenguaje orientado a objetos se definió una clase para generar objetos con las propiedades necesarios para hacer álgebra vectorial. Es decir uso de vectores, matrices, cubos, etc, con mucha eficiencia y respetando propiedades del álgebra. Básicamente NumPy utiliza arreglos que son propios de su librería
Puedo crear un arreglo numpy tomándolo de una lista o tupla, o creándolos nuevos.
Veamos el ejemplo de usar una lista y con ella crear un arreglo NUmpy.
Para ello uso la función array de NumPy, y como a NumPy lo llamé "np", a la función la tengo que llamar np.array( )
El arreglo NumPy creado ya no es una lista (aunque se le parece mucho), tiene otras propiedades.
a_lista = [1,2,3,4,5,6]
a = np.array(a_lista)
print(a)
Notar que ahora no tenemos las "," típicas de una lista.
O podría hacer directamente
a = np.array([1,2,3,4,5,6])
Si pregunto el tipo de variable que tengo ahora
print("Es de la clase:", type(a))
# funciona también con tuplas:
b = np.array((1,2,3))
print(b)
print("Es de la clase:",type(b)) # Ya NO es una Tupla.... y además
# desaparecen las comas de la lista o tupla
# Creo una lista
L = [1, 2, 3, 4]
# Convierto mi lista a un arreglo NumPy
a = np.array(L)
print("La lista L es del tipo: ",type(L))
print("a es de tipo:",type(a))
print("El arreglo a es del tipo:",a.dtype)
print("")
print(L)
print(a)
El dtype es un comando de Numpy. Me indica que el arreglo "a" no es una lista de Python. dtype me indica el tipo de arreglo dentro de las posibilidades de arreglos Numpy. En este caso es tipo Integer*8 (64 bits).
A diferencia de las listas los arreglos Numpy engloban a variables que son del mismo tipo. Es decir que todo el arreglo Numpy es del mismo tipo de variable (Real*8, Int*8, complejo, etc)
Una vez definido, todos los agregados se modifican para que sean válidos en el tipo de arreglo final.
Lista2=[1,2,22.] # <-- 1 y 2 enteros, 22. es real
Lista_numpy=np.array(Lista2)
print(Lista_numpy)
Todo el arreglo terminó siendo real.
Los arreglos NumPy son estructuras donde los cálculos son rápidos y eficientes.
Para ver la ventaja real de esta librería usaremos el comando del notebook %timeit (que es una función del notebook, no es una orden de Python) que me permite tomar tiempos.
range es una función de python.
arange es una función que se agrega porque viene con numPy. Esta última crea los mismos valores de la lista, pero ahora en array NumPy
L = range(1000) # Crea una lista
%timeit for L in range(1000): A=L**2
print('Pero usando NumPy')
A = np.arange(1000) # Crea un arreglo Numpy
%timeit A2 = A**2
La idea que hay que tener presente cuando se usa NumPy, es que se va a trabajar con arreglos como variables. No hay que pensar en escribir código que apunte a algoritmos donde se trabaja con los elementos de a uno en un arreglo. El código se escribe pensando las operaciones matemáticas que se realizan sobre vectores y matrices. Veremos esto con detalle durante el curso.
Los arreglos NumPy llevan consigo al ser objetos, atributos y funciones. Estas funciones en particular son mucho más eficientes que las originales de Python. Probemos creando un arreglo usar primero la función que busca el mínimo valor de Python (min(a)) y luego lo que hace el mismo trabajo pero con la función que reside en el objeto NumPy (a.min()). Noten que tiene el mismo nombre pero que se las llama para su uso de forma diferente.
# construyo un vector de 10.000.000 números con primer valor 1000 y último 0, con paso -0.00001
a = np.arange(1000,0,-0.0001)
# veo que tengo en a
print("Largo de a: ",len(a))
%timeit min(a)
%timeit a.min()
Hay que usar las funciones que vienen en las librerías que son especifícas para un tema particular, en el caso de álgebra vectorial (como en este último ejemplo) es prácticamente obligatorio usar las funciones de los objetos NumPy.
a = np.array([1,2,3,4,5,6])
b = np.array([[1,2],[1,4]])
c=np.array([[[1,2], [2,3]], [[3,4], [4,5]]])
print("Imprimo el atributo shape")
print( a.shape, b.shape, c.shape)
print("")
print("Pero si me fijo el len()")
print(len(a),len(b), len(c))
El atributo shape me cuenta de las dimensiones del arreglo, en cambio la función len( ) me cuenta de su largo.
El comando shape me devuelve una tupla con la información, en cambio len( ) me devuelve un número.
Si tengo varias dimensiones y quiero saber la cantidad total de elementos es conveniente imprimir el atributo size.
Veamos como imprimimos el array c y sus componentes ya que este arreglo es trimensional.
print(c[0])
print(c[0][0])
print(c[0][0][0]) #<-- recién aquí me devuelve un número
# size me da la cantidad de valores en el array
b.size
Pero estrictamente la dimensión me la da el atributo ndim
print (a.ndim, b.ndim, c.ndim )
La forma como NumPy numera los ejes, se conoce como el estilo "C" (por el lenguaje C que lo hace de esa manera) y es diferente al estilo Fortran. Aunque siempre es más cómodo visualizar las distintas dimensiones como arreglos de arreglos de otros arreglos...
Crédito del dibujo:https://www.tomasbeuzen.com/python-programming-for-data-science/chapters/chapter5-numpy.html
La librería numPy trae funciones asociadas.
Las puedo buscar en el manual (www.numpy.org), veamos algunos ejemeplos:
a = np.array([1,2,3,4,5,6,7])
Por ejemplo, probemos con dos, la que calcula promedios de un array a.mean( ) y la que me encuentra el valor máximo a.max( )
print(a.mean(), a.max())
mean( ) y max( ) son métodos (funciones) de la clase del arreglo, necesitan los paréntesis ( ).
print(a.mean) # Esto así imprime de que trata la funcion no su resultado
print ("Imprimo b: ",b)
print ("Imprimo el promedio de b: ",b.mean()) # Promedio sobre todo el arreglo
print ("Imprimo el promedio de las columnas de b: ",b.mean(axis=0)) # Promedio sobre el primer eje (columnas)
print ("Imprimo el promedio de las filas de b: ", b.mean(1)) # Promedio sobre las filas, se sobreentiende que es "axis=1"
La idea es que el arreglo reciba una cantidad de elementos creados en una forma automática y que cumplen reglas que se especifican como bucles. Es decir con un inicio, un final y un paso. Donde este último no tiene que ser obligtoriamente lineal. Por ejemplo, podríamos tener un paso geométrico, logarítmico, exponencial, etc.
También veremos que existen órdenes que copian la forma de otro arreglo, pero no sus elementos, sólo su forma, es decir su tamaño y dimensión.
print(np.arange(10))
print(np.linspace(0, 1, 10)) # Comienzo, final, y número de puntos
print(np.linspace(0, 1, 10, endpoint=False)) # No incluimos el punto final, o lo hacemos hasta 11.
print(np.zeros(2)) # Lleno con ceros
print(np.zeros((2,2))) # es un arreglo de dos dimensiones lleno con ceros
print(np.ones_like(a)) # Esto es muy bueno, creo un arreglo (o tupla)
# usando las propiedades de otro que ya tengo
print(np.zeros_like(a)+3) # Me sirve para llenar con otro valor que no son ceros o 1's
print(np.ones_like([1,2,3]))
a22=np.empty([2, 2])
a11=np.empty([10])
print(np.logspace(0, 2, 10)) # va de 10**comienzo hasta 10**final y calculo 10 valores
Una manera muy típica de trabajar en python es primero crea el array como unidimensional y luego darle forma con el comando reshape( ), que lo vimos en Fortran 90.
a = np.array([1,2,3,4,5,6])
b = a.reshape((3,2)) # Este comando no cambia la "forma" de a
print(a)
print('')
print(b)
Mientras que la función ravel( ) lo vuelve unidimensional.
print(b.ravel())
b = a.reshape((3,2))
c = a.reshape((3,2))
print(a.shape, b.shape)
print(b)
print("")
b[1,1] = 100 # modifico un valor del arreglo
print(b)
print(a) # !!! a y b son el mismo objeto comparten el mismo espacio de memoria,
# es decir apuntan a los mismos valores
print(b[1,1],a[3]) # el mismo valor
c = a.reshape((2,3)).copy() # Esta es la solución
print(a)
print('')
print(c)
c[0,0] = 8888
print(a)
print(c)
Tengo funciones específicas ya programadas en NumPy para realizar la tarea.
ran_uniform = np.random.rand(5) # Entre 0 y 1
ran_normal = np.random.randn(5) # Gausiana promedio 0 varianza 1
print("Números con distribución Uniforme: ",ran_uniform)
print ('')
print ("Números con distribución Gaussiana: ",ran_normal)
print ('')
ran_normal_2D = np.random.randn(5,5) # Gausiana promedio 0 varianza 1
print("Números con distribución Gaussiana, pero ahora en una matriz: \n\n",ran_normal_2D)
Si en la celda siguiente elimino el # en la línea y corro la celda me da como resultado un "help" del comando. Estas ayudas que se agregan en las funciones son los comentarios que se agregaron en el código de la función como docstrings.
#np.random.randn?
Es tal como hemos visto con los comandos originales de Python. El rabanado de un objetos de Numpy se hace con los mismos comandos.
a = np.arange(10)
print(a)
print(a[1:8:3])
Puedo en NumPy cambiar varios números en una sólo línea con un lista de estos elementos. Veamos un ejemplo:
print(a)
a[[2,4,6]] = -999 # Puse como ínidices en el arreglo a, una lista de elementos 2,4,6
print(a)
Veamos como construir un arreglo NumPy donde sus elementos son variables lógicas
a = np.random.randint(0, 100, 20) # min, max, N
print(a)
a < 50
La respuesta al operador de relación fue un arreglo con valores booleanos, es decir es la respueta si es verdadera o falsa la pregunta que se realizó en cada elemento.
Veamos esto con más detalle y como usarlo en un caso real:
Utilizo el arreglo a del ejemplo anterior:
a
Y lo uso para realizar operacion matemáticas simples:
a + 1
Hay que recordar que ahora la variable para los cálculos es todo el arreglo. Y podemos hacer operaciones bastante más complejas, como la que sigue:
a**2 + 3*a**3
Creo un arreglo más pequeño:
a = np.arange(10)
print("a es:",a)
Y realizo una operación más compleja y asigno el resultado a otro arreglo que llamo "b"
b = a**2 + (a+1)**2
print("b es:",b)
Hago otra operación a la que asigno el resultado al arreglo "c"
c = (a+2)**2
print("b es:",b)
print("c es:",c)
Podría entonces preguntar que valores están repetidos en el mismo lugar en los arreglos b y c
print(b == c)
Guardo el resultado en el arreglo "máscara"
mascara = b==c
print("mascara es:", mascara)
¿Y si la máscara la uso para filtrar el vector? ¿Cómo funciona eso?
Es decir pido que imprima b[mascara], pero mascara ahora funciona como un arreglo de los índices de b al poner de esa manera el comando. Sólo pasarán el filtro los elementos verdaderos.
print("b es:",b)
print("b filtrado por la mascara es: ",b[mascara])
También podría haber usado un expresión más compleja
mascara= b < 15
print(mascara)
Podría entonces usar el arreglo máscara para filtrar los valores que cumplen con la condición solicitada.
d=b[mascara]
print(d)
Incluso podría haber hecho todo esta operación en un sólo comando:
print(b>c)
print(a[b>c])
Cuidado que esto último lo puedo hacer porque a,b y c tienen el mismo tamaño y los elementos se corresponden en la numeración
a = np.arange(18)
print("a es:",a)
print("")
print("El log10 de a es:",np.log10(a))
Notar que hubo un error al sacar el log10 de 0. Pero el programa siguió corriendo.
Ese error fue notificado en el cartel que sale con el "RuntimeWarning".
Podemos encontrarnos elementos que dieron errores y estos pueden ser: -inf, +inf, y NaN.
a = [[1, 0], [0, 1]]
b = [[4, 1], [2, 2]]
c = np.matmul(a, b)
print("c es:",c)
a = [[1, 0], [0, 1]]
b = [1, 2]
c= np.matmul(a, b)
print(c)
c= np.matmul(b, a)
print(c)
a = np.arange(1000000).reshape(1000,1000)
b = np.ones_like(a)
print(a)
print("")
print(b)
c=np.matmul(a,b)
print(c)
A = floor(random.rand(4000,4000)*20-10) <-- genera una matriz de 4000 x 4000 valores llena con números al azar
A = np.random.rand(4000,4000)
print("Imprimo A")
print (A)
b = np.floor(np.random.rand(4000,1)*20-10)
print("")
print("Imprimo b")
print(b)
# Resolvamos Ax = b usando el comando NumPy para la tarea:
x = np.linalg.solve(A,b)
print("")
print("Imprimo el resultado")
print(x)
El broadcasting es la manera que tiene Python de compensar operaciones elemento a elemento entre arreglos de diferentes tamaños.
La situación más simple es cuando se realiza una operación entre un arreglo por un escalar. El escalar en virtualmente repetido todas las veces necesarias para repetir el tamaño del escalar.
Se suele considerar que el broadcasting es un fuerte consumidor de memoria de la computadora.
a = np.array([1.0, 2.0, 3.0])
b = 2.0
a * b
En este caso hubiese sido similar a que conviertiera b en b=np.array([2.0, 2.0, 2.0])
a = np.array([1.0, 2.0, 3.0])
b = b=np.array([2.0, 2.0, 2.0])
a * b
a = np.array([[0.0], [10.0], [20.0], [30.0]])
b = np.array([1.0, 2.0, 3.0])
a+b
Moraleja: Los arreglos sirven para hacer álgebra vectorial, pero NO son vectores, ni matrices...
No tienen las mismas propiedades...
El broadcast sólo se puede realizar si:
- Los arreglos son iguales o
- uno de ellos tiene dimensión 1 arreglos son iguales
Veamos como hay que razonar para usar NumPy y de paso calculemos PI (una vez más...)
Esta vez usando series de Taylor. En este caso arctan(1) como hizo en su momento Leibniz
$$\frac{\pi}{4} = \sum_{k=0}^{\infty} \frac{(-1)^k}{2k+1}$$Veamos un la forma de plantearlo en Python, y usando NumPy:
Por lo que calculo la serie de dos maneras, primero estilo Fortran: término por término de la serie mientras los sumo.
La segunda manera en modo NumPy, todos los términos al mismo tiempo, creando un vector de términos y que luego sumo.
La función pow(x, y) is igual a hacer el cálculo $x^y$, y funciona mejor que $x**y$, porque permite valores fracionarios y negativos de $y$.
# Estilo Fortran
n = 10000000
total = 0
for k in range(n):
total = total + pow(-1,k)/(2*k+1.0)
print("Cálculo un elemento y lo voy sumando :",total*4)
#Estilo NumPy
k=np.arange(n)
term=pow(-1,k)/(2*k+1.0)
total=term.sum()
print("Todos los términos al mismo tiempo y sumo al final:", total*4)
Noten que la forma NumPy es mucho más veloz.