Mostrando entradas con la etiqueta slicing. Mostrar todas las entradas
Mostrando entradas con la etiqueta slicing. Mostrar todas las entradas

sábado, 9 de enero de 2021

4) Operaciones con vectores en arrays o vectorización.



Operaciones con vectores en arrays o vectorización.


El trabajar con bucles "for" en python resulta muy lento. Si tienes que repetir una operación matemática en muchos elementos consecutivos de una matriz, siempre resulta mejor utilizar una operación vectorizada si es posible, es hasta un 80% más rápido. 

En la práctica una operación vectorizada, significa rehacer nuestro código para evitar el uso de bucles y utilizar en su lugar slicing, rebanadas o vectores de arrays de Numpy para aplicar la operación a todo o parte del array.

Vamos a ver la diferencia que existe con un ejemplo. Supongamos que tienes que calcular la diferencia existente entre los elementos consecutivos de una matriz. 

Como en este ejemplo solo queremos ver la diferencia de tiempos de ejecución que hay con ambos sistemas vamos a crear una matriz "a" que va a contener todos los números desde el cero al 999. 

La matriz "a" seria algo así:

a = [0,1,2,3,4,5,6,..........,998,999], tiene 1000 elementos

Lo que queremos hacer es obtener otra matriz de resultados, a la que llamaremos "b", con la diferencia entre los elementos consecutivos de la matriz. Es decir, la matriz "b" seria algo como esto:

b = [(1-0),(2-1),(3-2),(4-3).....................(998-997),(999-998)] que tiene 999 elementos.

Como ves en esta matriz, todos sus elementos van a ser unos. Pero esto no nos interesa, no nos interesa el resultado, sino el proceso que hay que seguir en python para hacerlo.


Para ello vamos a crear un archivo en python, yo lo llamaré consecutivos.py, con el siguiente código:


# importamos la libreria numpy para crear los array
import numpy as np

# Creamos la matriz a con 1000 números consecutivos del 0 al 999
a = np.arange(1000)

# Creamos la matriz b rellena con ceros para luego almacenar los resultados.
# Al estar formada con la diferencia entre 2 elementos consecutivos, tendrá
# 1000 - 1 elementos, es decir 999
b = np.zeros(999, int)

# Bucle for que calcula la diferencia entre un elemento (i) y el anterior
# (i-1), recorre haciendo el calculo con un bucle for la matriz a
# y asigna el resultado en la matriz b.
# e.j b[0]= a[1] que es 1 menos a[0] que es el cero 
for i in range(1,len(a)):
    b[i-1]=a[i]-a[i-1]


Pues bien, esto mismo se puede conseguir usando una operación vectorizada, con el siguiente código:

c = a[1:] - a[:-1]

Con esto estaríamos restando, elemento a elemento, la siguiente rebanada de la matriz a:

a[1:] = [1,2,3,4,...,998,999] desde el elemento segundo, cuyo valor es 1, hasta el final. (se empieza a contar en el cero)

a[: -1] = [0,1,2,3,....,997,998] desde el primer elemento que es el 0 hasta el penúltimo, el 998.


Si no lo acabas de ver bien, imagínatelo en pequeñito, con este ejemplo.

[1,2,3,4,5]

arr[1:]   → [2,3,4,5]

arr[:-1] →  [1,2,3,4]

dif =           [1,1,1,1]


Vamos ahora a juntarlo todo y ver lo que se tarda en ejecutar de las dos formas. Pero para que se note un poco más y se vea claramente la diferencia vamos a realizarlo, no con una matriz no de 1.000 elementos sino una de 1.000.000 de números.


# importamos la libreria numpy para crear los array
import numpy as np
# importamos time para medir el tiempo de ejecución
from time import time

# Creamos la matriz a con 1.000.000 de números consecutivos del 0 al 999.999
# en python el guion bajo _ nos permite separar los miles para verlo mejor.
a = np.arange(1_000_000)

# Creamos la matriz b rellena con ceros para luego almacenar los resultados.
# Al estar formada con la diferencia entre 2 elementos consecutivos, tendrá
# 1.000.000 - 1 elementos, es decir 999.999
b = np.zeros(len(a-1), int)

# Bucle for que calcula la diferencia entre un elemento (i) y el anterior
# (i-1), recorre haciendo el calculo con un bucle for la matriz a
# y asigna el resultado en la matriz b.
# e.j b[0]= a[1] que es 1 menos a[0] que es el cero 
t0 = time()
for i in range(1,len(a)):
    b[i-1]=a[i]-a[i-1]
t1 = time()

# Utilizando vectores de la matriz o slicing en el array.
t2 = time()
c = a[1:]-a[:-1]
t3 = time()

print("tiempo del bucle for ", (t1-t0))
print("tiempo con vectorización", (t3-t2))
print((t1-t0)>(t3-t2))

Yo, en mi equipo obtengo los siguientes resultados:

tiempo del bucle for  0.6465959548950195
tiempo con vectorización 0.0030362606048583984
True

Como puedes ver, la diferencia de tiempo es MUY significativa.


Seleccionar elementos que cumplan una determinada condición en un vector.


Una tarea que se realiza con bastante frecuencia es seleccionar elementos de un vector. Esta selección se pude llevar a cabo en base a índices o en base a un determinado criterio (por ejemplo que los elementos a seleccionar sean mayores que un determinado valor o que se encuentren en un determinado rango)

Vamos a realizar un ejemplo. Creamos una matriz de ejemplo en Numpy:

>>> import numpy as np

>>> matriz = np.arange(10)

>>> matriz
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Ahora vamos a seleccionar todos los elementos del array que sean mayores que 5. Al utilizar operadores de igualdad o comparación vamos a obtener un nuevo array con valores booleanos.

>>> matriz > 5
array([False, False, False, False, False, False,  True,  True,  True,
        True])
Como ves obtenemos un nuevo vector en el que los 6 primeros elementos son falsos y los 4 últimos verdaderos. Este resultado se puede guardar en una variable y utilizarlo para seleccionar los elementos que cumplen la condición.

Si a un array de Numpy se le pasa un vector de valores booleanos el resultado es un nuevo vector en el que solo estarán los elementos cuyas condiciones sean ciertas. Vamos a verlo en nuestro ejemplo:

>>> mayor_5 = matriz > 5

# seleccionamos los elementos de matriz en base a matriz > 5
>>> matriz[mayor_5]
array([6, 7, 8, 9])
Y como puedes ver hemos seleccionado los elementos de nuestra matriz original que cumplen la condición de ser mayores de 5.


Seleccionar elementos en base a múltiples condiciones en Numpy


Una vez que hemos visto como seleccionar elementos de un vector en base a una condición, podemos hacer las selecciones complejas que queramos utilizando operadores lógicos igual que en Python. Por ejemplo vamos como obtendríamos todos los números mayores que 5 pero a la vez que sean pares. Recordemos que para que un número sea para el resto de dividir ese número entre dos tiene que ser cero. Pues bien vamos a utilizar la función np.mod(matriz, divisor) que nos va a proporcionar el resto de dividir cada elemento de la matriz entre el divisor que elijamos.

Con todo lo anterior para elegir cual de los elementos son pares y mayores que 5 el código sería el siguiente:

>>> matriz[np.mod(matriz,2) & matriz > 5]
array([6, 8])

Le estamos diciendo Numpy que selecciones los elementos de la matriz que cumplan 2 criterios:
- Que el resto de su división entre 2 sea cero o lo que es lo  mismo que el número es par.
- Que sea mayor que 5.

Como hemos visto, con está técnica podemos filtrar grandes cantidades de datos en python de forma muy eficiente ya podemos implementar fácilmente filtros complejos. 



EJERCICIOS.

Calculo Diferencial

Con este ejercicio vamos a practicar también la vectorización, lo cual es crucial para obtener buenos resultados en términos de velocidad de cálculo y rendimiento con Numpy.

Para el que lo haya visto recordar que las derivadas, en matemáticas, se pueden calcular numéricamente con el método de las diferencias finitas como:

f′(xi)=(f(xi+Δx)−f(xi−Δx))/2Δx

Vamos a construir una matriz unidimensional que contenga los valores de xi en el intervalo 0 y π/2 con incrementos de 0.10.

Luego calcularemos numéricamente la derivada de la función seno y coseno en ese intervalo (excluyendo los puntos que constituyen los extremos) y para terminar las representaremos gráficamente.

Primeramente importamos las librerías necesarias:

>>> import numpy as np
>>> import matplnotlib.pyplot as plt
Con un array de Numpy realizamos los cálculos y construimos las funciones seno y coseno entre 0 y pi/2.

>>> dx=0.10
>>> x=np.arange(0,np.pi/2,dx) # Intervalo entre 0 y pi/2 con incrementos de 0.10
>>> f=np.sin(x) # función seno en ese intervalo
>>> g=np.cos(x) # función coseno en ese intervalo.

>>> print(x)
[0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.  1.1 1.2 1.3 1.4 1.5]

>>> print(f) # Seno
[0.         0.09983342 0.19866933 0.29552021 0.38941834 0.47942554
 0.56464247 0.64421769 0.71735609 0.78332691 0.84147098 0.89120736
 0.93203909 0.96355819 0.98544973 0.99749499]

>>> print(g) # Coseno
[1.         0.99500417 0.98006658 0.95533649 0.92106099 0.87758256
 0.82533561 0.76484219 0.69670671 0.62160997 0.54030231 0.45359612
 0.36235775 0.26749883 0.16996714 0.0707372 ]


Ahora calcularemos las derivadas de ambas funciones por el método de las diferencias finitas.

>>> df_sen=(f[2:]-f[:-2])/2*dx
>>> print(df_sen)
[0.00993347 0.00978434 0.00953745 0.00919527 0.00876121 0.00823961
 0.00763568 0.00695546 0.00620574 0.00539402 0.00452841 0.00361754
 0.00267053 0.00169684]

>>> df_cos=(g[2:]-g[:-2])/2*dx
>>> print(df_cos)
[-0.00099667 -0.00198338 -0.00295028 -0.0038877  -0.00478627 -0.00563702
 -0.00643145 -0.00716161 -0.00782022 -0.00840069 -0.00889723 -0.00930486
 -0.00961953 -0.00983808]
Para entenderlo mejor vamos a desarrollar los arreglos de la función seno para que veas como funcionan:

# función f(seno)
>>> print(f)
[0.         0.09983342 0.19866933 0.29552021 0.38941834 0.47942554
 0.56464247 0.64421769 0.71735609 0.78332691 0.84147098 0.89120736
 0.93203909 0.96355819 0.98544973 0.99749499]

>>> print(f[2:])
[0.19866933 0.29552021 0.38941834 0.47942554 0.56464247 0.64421769
 0.71735609 0.78332691 0.84147098 0.89120736 0.93203909 0.96355819
 0.98544973 0.99749499]

>>> print(f[:-2])
[0.         0.09983342 0.19866933 0.29552021 0.38941834 0.47942554
 0.56464247 0.64421769 0.71735609 0.78332691 0.84147098 0.89120736
 0.93203909 0.96355819]

# Como ves ambos arreglos tienen el mismo número de elementos para poder
# luego restarlos y acabar de aplicar la fórmula.
Para dibujar las funciones y su relación con las derivadas:

plt.plot(x[1:-1], df_sen)
plt.plot(x[1:-1], f[1:-1])
plt.plot(x[1:-1], df_cos)
plt.plot(x[1:-1], g[1:-1])
plt.show()






Próximo Post. Manipulación de Arrays y Transmisión en Numpy.

domingo, 3 de enero de 2021

2) Accediendo a los datos de las matrices con Numpy. Copia de Matrices. Añadir elementos a un array con np.append()




Accediendo a los datos de las matrices con Numpy. Copia de Matrices. Añadir elementos a un array con numpy.append()


La forma en la que se accede a los datos de las matrices en Numpy es similar a como se hace con las listas en Python. La diferencia está en que mientras que las listas son unidimensionales las matrices son ciertamente multidimensionales. Esto significa que mientras que en una lista solo hay un índice para cada elemento, con Numpy al tratarse de matrices puede haber varios valores índices (uno para cada dimensión)

Vamos a explicar esto mejor, con un ejemplo.

Para acceder a un elemento concreto de una matriz de 3 dimensiones, tenemos que usar el valor del índice de cada dimensión separada por comas:

>>> import numpy as np
# Importamos la librería Numpy

>>> a = np.array([[1,2,3],[4,5,6],[7,8,9]])
>>> print(a)
[[1 2 3]
 [4 5 6]
 [7 8 9]]


Ahora imaginemos que queremos seleccionar el número 3 de la primera fila, el 5 de la segunda y el 7 de la tercera fila. Lo primero que tendremos que hacer es, indicar en que fila está el elemento y posteriormente, después de poner la coma, indicar la posición del elemento en la fila. Ten en cuenta que en Python los elementos de una lista si se  empiezan a contar por la izquierda se comienza a contar desde el 0.

>>> print(a[0,2])
3
>>> print(a[1,1])
5
# Seleccionamos el elemento contando desde la izquierda.
>>> print(a[2,-3])
7

Otra forma de acceder a un elemento de estas matrices anteriores es usando una forma muy parecida a la anterior, usando [][]. Por ejemplo el elemento 8 de la matriz a anterior que está en la 3ª fila y 2ª columna se puede buscar también así:

>>> print(a[3,-1][2,-1])
8
# realmente se pondría print(a[2][1]) pero he preferido poner la resta
# ya que los elementos se empiezan a contar en el cero, con lo que el 
# elemento 8, estaría en la fila 2 y columna 1 en python.

También podemos hacer "slicing" o arreglos con las matrices, podemos seleccionar una parte o "rebanada" de los datos de una matriz incluso con múltiples dimensiones.

Pongamos un ejemplo. Creemos un array unidimensional con 10 elementos y seleccionemos los elementos que están desde la posición 1 al 2, funciona como las listas de python. La selección va desde el primer número indicado hasta el último sin incluir este.

>>> m = np.arange(10)
>>> m
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> m[1:3]
array([1, 2])

o una selección que vaya desde la posición 4ª hasta el final

>>> m[3:]
array([3, 4, 5, 6, 7, 8, 9])

o que incluya todos los elementos menos el último

>>> m[:-1]
array([0, 1, 2, 3, 4, 5, 6, 7, 8])

o cambiar los elementos que van desde el 4 al final con un 0

>>> m[4:] = 0
array([0, 1, 2, 3, 0, 0, 0, 0, 0, 0])

o seleccionar los elementos del 1 al 7 (sin incluir) con un salto de dos elementos:

m[1:7:2]
array([1, 3, 0])

Veamos también el caso de una matriz multidimensional con varias filas. Imaginemos que creamos una matriz de 5x5 y la rellenamos con ceros. Después queremos insertar el número 3 en los elementos que ocupan la primera y segunda fila y también  la primera y segunda columna (recordando que tanto filas como columnas se empiezan a contar en el cero)

>>> n = np.zeros((5,5))
>>> n
array([[0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0.]])
>>> n[1:3,1:3]=3
>>> n
array([[0., 0., 0., 0., 0.],
       [0., 3., 3., 0., 0.],
       [0., 3., 3., 0., 0.],
       [0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0.]])

Matrices y copia de matrices.


Si queremos copiar una matriz no basta solamente con elegir un nuevo nombre y asignárselo. Si modificamos la matriz usando la nueva referencia, los cambios son visibles en cualquiera de las asignaciones que hagan referencia a la misma matriz. Veámoslo con un ejemplo:

>>> matriz_a = np.array([1,3,5,7,9])
>>> matriz_b = matriz_a
>>> matriz_b[2:]=-7
>>> matriz_a
array([ 1,  3, -7, -7, -7])


Si realmente queremos copiar una matriz y que sea realmente independiente del original hay que utilizar el método copy()

>>> matriz_a = np.array([1,3,5,7,9])
>>> matriz_b = matriz_a.copy()
# Esto copia la matriz_a en matriz_b y hace a esta independiente.

Hay que tener en cuenta que si hacemos slicing a una matriz, cualquier modificación que hagamos quedará reflejada en la matriz. Por tanto si queremos hacer una copia de una parte de una matriz debemos utilizar el método copy()

>>> original = np.arange(10)
>>> una_parte = original[1:4] # Esto es una vista, si cambiamos algún elemento
de una_parte los cambios se reflejaran en los elementos [1:4] de la original.
>>> una_parte = original[1:4].copy() # Esto es una verdadera copia de una parte
de la matriz original.


Añadir elementos a un array con np.append()


Con numpy.append() podemos añadir nuevos elementos al final de los arrays de numpy. Este método tiene la siguiente forma:

np.append(arr, values, axis=None)

En donde:

arr es un array o matriz de Numpy.

values son los elementos que se agregaran al final del array que se pasa en el argumento anterior. Este elemento no tiene porque ser solo unos números ya que también puede ser un array o matriz.

axis (opcional) es el eje en el que se agregarán los valores.

IMPORTANTE:  ¡Este método no MODIFICA el array original, sino que crea una copia a la que se agregan los nuevos valores.!

Por otro lado si no especificamos un eje en el que añadir los elementos, la matriz que le hayamos pasado se aplanará antes de agregar los nuevos elementos, para así poder añadirlos al final.

Recordamos que si:

axis = 0 estamos refiriéndonos al eje vertical por lo que si es posible se añadirán nuevas filas.
axis = 1 estamos refiriéndonos al eje horizontal por lo que si es posible se añadirán nuevas columnas.

Veamos unos cuantos ejemplos de todo esto.


Añadir un número al final de un array unidimensional.


>>> import numpy as np
>>> arr = np.array([1, 2, 3, 4, 5])

# Queremos añadir el número 10 al final de este array
>>> np.append(arr, 10)
array([ 1,  2,  3,  4,  5, 10])

#O también podríamos agregar otro arrray.
>>> np.append(arr, [11, 12])
array([ 1,  2,  3,  4,  5, 11, 12])

# Como puedes ver en este último caso el array original no se modifica,
# ya que como ves no aparece el número 10 del caso anterior.

Agregar elementos a matrices o arrays que no sean unidimensionales.

Cuando agregamos elementos a una matriz  es necesario que indiquemos mediante la propiedad axis si los nuevos elementos se agregan por filas o por columnas. En el caso de que no lo hagamos, la matriz se aplanará antes de añadir los elementos, obteniéndose un array unidimensional.

Por ejemplo:

>>> mat = np.array([[1, 2, 3],[4, 5, 6]])
>>> np.append(mat, [11, 12])
array([ 1,  2,  3,  4,  5,  6, 11, 12])

Como veremos más adelante, para poder añadir una nueva fila a la matriz esta tiene que ser compatible, es decir tiene que tener el mismo número de columnas que la original. Sino es así Numpy te dará un error. Si en el ejemplo anterior intentamos añadir estos dos elementos a la matriz original, nos dará un error ya que nuestra matriz tiene 3 columnas y le estamos tratando de añadir solo 2 elementos en vez de 3.

>>> np.append(mat, [[11, 12]], axis=0)
ValueError: all the input array dimensions for the concatenation axis 
must match exactly, but along dimension 1, the array at index 0 has 
size 3 and the array at index 1 has size 2

Por tanto si queremos añadirle una nueva fila a nuestra matriz esta tiene que tener 3 elementos para poder hacerlo, además de establecer el parámetro axis=0. Vamos a añadir entonces [11, 12, 13]  a nuestra matriz.

>>> np.append(mat, [[11, 12, 13]], axis=0)
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [11, 12, 13]])

Si lo que queremos añadir es una nueva columna el método funciona exactamente igual. Los elementos a agregar deberán estar en una matriz con el mismo número de filas que la matriz original.

Como nuestra matriz original tiene 2 filas si queremos añadir una nueva columna deberemos añadir una matriz con  2 elementos y que tenga la misma dimensión.

>>> np.append(mat, [[11], [12]], axis=1)
array([[ 1,  2,  3, 11],
       [ 4,  5,  6, 12]])


Y con esto hemos visto lo básico del método np.append() de Numpy, sigamos repasando lo visto en el capítulo con un nuevo ejercicio.


EJERCICIO


Creemos una matriz con una dimensión de 4*4 con valores aleatorios. 

Luego extraigamos todos los valores de la segunda fila.

A continuación extraeremos todos los valores de la tercera columna.

Asignaremos el valor 0.31 al subarreglo  2x2 superior izquierdo.

Para finalizar crearemos una matriz de 8x8 con un patrón de tablero de damas, es decir, alternando ceros y unos :

1 0 1

0 1 0

1 0 1

...


SOLUCIÓN.


>>> import numpy as np


"Creamos una matriz de 4x4 con valores arbitrarios enteros del 0,1"
>>> a=np.random.rand(4,4) # o números enteros con np.random.randint(0,11,(4,4))
>>> print(a)
[[0.86244938 0.10086289 0.49685669 0.89849421]
 [0.63274446 0.90953088 0.87053284 0.32822434]
 [0.94201367 0.60204052 0.72596334 0.36410397]
 [0.68208797 0.46102898 0.59214942 0.89901397]]


"Extraer cada elemento de la segunda fila."
>>> print(a[1])
[0.63274446 0.90953088 0.87053284 0.32822434]

"Extraer cada elemento de la tercera columna"
print(a[:,2])
[0.49685669 0.87053284 0.72596334 0.59214942]

"Asignar un valor de .31 a la matriz de 2*2 de arriba a la izquierda"
a[:2,:2]=0.31
print(a)
[[0.31       0.31       0.49685669 0.89849421]
 [0.31       0.31       0.87053284 0.32822434]
 [0.94201367 0.60204052 0.72596334 0.36410397]
 [0.68208797 0.46102898 0.59214942 0.89901397]]

"Crear una matriz de 8x8 alternando 0 y 1"
#b = np.array([[(i+j+1) % 2 for i in range(8)] for j in range(8)]) solución alternativa
b=np.zeros((8,8),dtype="int")
b[::2,::2]=1
b[1::2,1::2]=1
print(b)
[[1 0 1 0 1 0 1 0]
 [0 1 0 1 0 1 0 1]
 [1 0 1 0 1 0 1 0]
 [0 1 0 1 0 1 0 1]
 [1 0 1 0 1 0 1 0]
 [0 1 0 1 0 1 0 1]
 [1 0 1 0 1 0 1 0]
 [0 1 0 1 0 1 0 1]]



Próximo Post. Matemáticas con Numpy.