Numpy III

Terminamos la serie de entradas de la librería Numpy. En la presente entrada, Numpy III, presento unos ejemplos finales para el uso de la librería. Además de la librería Numpy, utilizaré la librería matplotlib para la visualización de datos.

Para el lector interesado, puede acceder a las entradas previas en los siguientes enlaces:

Los ejemplos de utilización de la librería Numpy a presentar en esta entrada son los siguientes:

  1. Generación aleatoria de números.
  2. Transformación de arrays de una dimensión a otra.
  3. Álgebra linear.
  4. Operaciones sobre conjuntos.
  5. Serialización de arrays.

1.- Generación aleatoria de números.

Para la generación de números aleatorios emplearemos la función randn; para la definición de la semilla de generación, emplearemos la función seed; y, para definir un objeto de estado para la generación de números, emplearemos el objeto RandomState.

Para definir una distribución, empleados la función scatter del módulo matplotlib.pyplot; y, para representar desde un punto de vista gráfico, se emplea la función show. El snippet de código de ejemplo es el siguiente:

seed = np.random.seed(123)
randn = np.random.randn(3)
print(f'randn=\n{randn}\n')

rang1 = np.random.RandomState(seed=123)
array_rang1 = rang1.randn(3)
print(f'array_rang1=\n{array_rang1}\n')

rang2 = np.random.RandomState(seed=123)
array_rang2 = rang2.randn(100, 2)  # Retorna una distribución normal
print(f'array_rang2=\n{array_rang2}\n')

plt.scatter(array_rang2[:, 0], array_rang2[:, 1])

rang3 = np.random.RandomState(seed=123)
normal_distribution = 2. * rang3.randn(100, 2) + 5.
plt.scatter(normal_distribution[:, 0], normal_distribution[:, 1])
plt.show()

La salida por consola son los siguientes:

randn=
[-1.0856306   0.99734545  0.2829785 ]

array_rang1=
[-1.0856306   0.99734545  0.2829785 ]

array_rang2=
[[-1.08563060e+00  9.97345447e-01]
   ...
 [-3.41261716e-01 -2.17946262e-01]]

La gráfica generada en el ejemplo anterior es la que se muestra en el siguiente gráfico:

2.- Transformación de arrays de una dimensión a otra.

La creación de array n-dimensionales los podemos crear a partir de un array de una dimensión. Para realizar el cambio de dimensión, se realiza con la función reshape; con esta función, se puede especificar las dimensiones de forma explícita o implícito. Para realizar el proceso inverso, empleamos la función flatten; y, por último, para concatenar arrays, se emplea la función concatenate. En el siguiente snippet se muestra ejemplos de uso:

array = np.array([1, 2, 3, 4, 5, 6])
print(f'array=\n{array}\n')
array_23 = array.reshape(2, 3)  
print(f'array_23=\n{array_23}\n')
print(f'may_share_memory=\n{np.may_share_memory(array_23, array)}\n')

array_2_1 = array.reshape(2, -1)  
print(f'array_2_1=\n{array_2_1}\n')

array_1_2 = array.reshape(-1, 2)  
print(f'array_1_2=\n{array_1_2}\n')

array2 = np.array([[1, 2, 3],
                      [4, 5, 6]])
print(f'array2=\n{array2}\n')
array2_1 = array2.reshape(-1)  
print(f'array2_1=\n{array2_1}\n')
print(f'array2.ravel()=\n{array2.ravel()}\n')  # ravel es flatten

print(f'np.may_share_memory(array2.flatten(), array2)={np.may_share_memory(array2.flatten(), array2)}')  # False
print(f'np.may_share_memory(array2.ravel(), array2)  ={np.may_share_memory(array2.ravel(), array2)}')  # True

ary = np.array([1, 2, 3])
ary_concatenate = np.concatenate((ary, ary))
print(f'ary_concatenate=\n{ary_concatenate}\n')

La salida por consola es la siguiente:

array=
 [1 2 3 4 5 6]

array_23=
  [[1 2 3]
   [4 5 6]]

may_share_memory=
    True

array_2_1=
  [[1 2 3]
  [4 5 6]]

array_1_2=
  [[1 2]
   [3 4]
   [5 6]]

array2=
  [[1 2 3]
   [4 5 6]]

array2_1=
 [1 2 3 4 5 6]

array2.ravel()=
 [1 2 3 4 5 6]

np.may_share_memory(array2.flatten(), array2)=False
np.may_share_memory(array2.ravel(), array2)  =True
ary_concatenate=
   [1 2 3 1 2 3]

3.- Algebra linear.

La multiplicación de matrices así como la operación con la matriz transpuesta es una operación típica, en el presente apartado, presentamos dos funciones para realizar el producto de dos matrices: función matmul, multiplica dos matrices pasadas por parámetro; y, la función dot, realiza la misma funcionalidad pero más eficiente. Para calcular la función transpuesta, empleamos la función T de un array. En el siguiente ejemplo, se muestra un snippet de código con ejemplos de productos de matrices.

matrix = np.array([[1, 2, 3],
                    [4, 5, 6]])
column_vector = np.array([[1, 2, 3]]).reshape(-1, 1)
print(f'matrix=\n{matrix}\n')
print(f'column_vector=\n{column_vector}\n')

result = np.matmul(matrix, column_vector)
print(f'matrix X column_vector=\n{result}\n')

# Más eficiente con dot.
print(f'np.dot(row_vector, row_vector)=\n{np.dot(row_vector, row_vector)}\n')
print(f'np.dot(matrix, row_vector)=\n{np.dot(matrix, row_vector)}\n')
print(f'np.dot(matrix, column_vector)=\n{np.dot(matrix, column_vector)}\n')

print(f'matrix.transpose()=\n{matrix.transpose()}\n')
print(f'matrix.T=\n{matrix.T}\n')
print(f'np.dot(matrix, matrix.T)=\n{np.dot(matrix, matrix.T)}\n')
print(f'np.matmul(matrix, matrix.T)=\n{np.matmul(matrix, matrix.T)}\n')

La salida por consola es la siguiente:

matrix=
   [[1 2 3]
    [4 5 6]]

column_vector=
   [[1]
    [2]
    [3]]

matrix X column_vector=
  [[14]
   [32]]

np.dot(row_vector, row_vector)=
  14

np.dot(matrix, row_vector)=
    [14 32]

np.dot(matrix, column_vector)=
    [[14]
     [32]]

matrix.transpose()=
    [[1 4]
     [2 5]
     [3 6]]

matrix.T=
    [[1 4]
     [2 5]
     [3 6]]

np.dot(matrix, matrix.T)=
    [[14 32]
     [32 77]]

np.matmul(matrix, matrix.T)=
    [[14 32]
     [32 77]]

4.- Operaciones sobre conjuntos.

Las operaciones de intersección, diferencia, unión o conjunto único sin repeticiones, se realizan respectivamente con las siguientes funciones: intersect1d, setdiff1d, union1d y unique. En el siguiente snippet de código se muestra ejemplos de uso:

array = np.array([1, 1, 2, 3, 1, 5])
array_set = np.unique(array)
print(f'array_set=\n{array_set}\n')

array1 = np.array([1, 2, 3])
array2 = np.array([3, 4, 5, 6])
print(f'array1=\n{array1}\n')
print(f'array2=\n{array2}\n')

array_intersec = np.intersect1d(array1, array2, assume_unique=True)
print(f'array_intersec=\n{array_intersec}\n')

array_diff = np.setdiff1d(array1, array2, assume_unique=True)  # aaray1 - array2
print(f'array_diff=\n{array_diff}\n')

array_union = np.union1d(array1, array2)La librería Numpy es aquella librería pensada y preparada para realizar operaciones matemáticas orientadas a distintos ámbitos de la ciencia la cual, en mi caso, me permita profundizar en casos prácticos de Machine Learning.
print(f'array_union=\n{array_union}\n')

La salida por consola es la siguiente:

array_set=
[1 2 3 5]

array1=
[1 2 3]

array2=
[3 4 5 6]

array_intersec=
[3]

array_diff=
[1 2]

array_union=
[1 2 3 4 5 6]

5.- Serialización de arrays.

Para almacenar los valores de un array en un fichero, empleamos la función save; para almacenar un array con los índices, se emplea la función savez; y, para realizar la carga de un fichero en memoria, se emplea la función load. Los ficheros con los que se operan tienen extensión .npz.

En el siguiente snippet de código se muestra unos ejemplos de uso de estas funciones:

array = np.array([1, 2, 3])
np.save('ary-data.npy', array)

data_file = np.load('ary-data.npy')
print(f'data_file=\n{data_file}\n')

array2 = np.array([1, 2, 3, 4, 5, 6])
np.savez('ary2-data.npz', array, array2)  

ary2_data = np.load('ary2-data.npz')
print(f'ary2_data=\n{ary2_data}\n')

array2_key = ary2_data.keys()
print(f'array2_key=\n{array2_key}\n')
print(f'ary2_data["arr_0"]=\n{ary2_data["arr_0"]}\n')  
print(f'ary2_data["arr_1"]=\n{ary2_data["arr_1"]}\n')

kwarg = {'ary1': array, 'ary2': array2}
np.savez('ary3-data.npz', **kwarg)

ary3_data = np.load('ary3-data.npz')
print(f'ary3_data=\n{ary3_data}\n')
print(f'ary3_data["ary1"]=\n{ary3_data["ary1"]}\n')  
print(f'ary3_data["ary2"]=\n{ary3_data["ary2"]}\n')

La salida por consola es la siguiente:

data_file=
[1 2 3]

ary2_data=
<numpy.lib.npyio.NpzFile object at 0x7f580fa5a128>

array2_key=
KeysView(<numpy.lib.npyio.NpzFile object at 0x7f580fa5a128>)

ary2_data["arr_0"]=
 [1 2 3]

ary2_data["arr_1"]=
  [1 2 3 4 5 6]

ary3_data=
  <numpy.lib.npyio.NpzFile object at 0x7f57e8b7def0>

ary3_data["ary1"]=
  [1 2 3]

ary3_data["ary2"]=
  [1 2 3 4 5 6]

La librería Numpy es aquella librería pensada y preparada para realizar operaciones matemáticas orientadas a distintos ámbitos de la ciencia la cual, en mi caso, me permita profundizar en casos prácticos de Machine Learning.

Numpy II

En la entrada anterior, Numpy I, realicé una presentación de la librería Numpy y realicé la descripción de unos ejemplos básicos. En la presenta entrada, Numpy II, continuaré presentando ejemplos de operaciones con la Numpy.

Los ejemplos de utilización de la librería Numpy a presentar en esta entrada son los siguientes:

  1. Operaciones de arrays con sus dimensiones.
  2. Indexación avanzada.
  3. Operaciones de comparación.

1.- Operaciones de arrays con sus dimensiones.

Los arrays son estructuras n-dimensionales con los cuáles podemos realizar sumas a pesar de tener diferente número de dimensión. A continuación, muestro una serie de ejemplos.

Sean dos arrays con la misma dimensión y número de elementos por dimensión, la operación suma se realiza con el operador +. El snippet del código es el siguiente:

array1 = np.array([1, 2, 3])
array2 = np.array([4, 5, 6])
print(f'array1 + array2={array1 + array2}')

La salida por consola es la siguiente:

array1 + array2=[5 7 9]

Sea un array de dos dimenciones con tres elementos por dimensión y un array de una dimensión e igual número de elementos que el primero. El snippet del código es el siguiente:

array3 = np.array([[7, 8, 9], [3, 2, 1]])
print(f'array3 + array1={array3 + array1}')

La salida por consola es la siguiente:

array3 + array1=[[ 8 10 12]
 [ 4  4  4]]

Sea un array de dos dimensiones con tres elementos y un array de dos dimensiones de un elemento por dimensión. El resultado es una array de dos dimensiones de tres elementos en el que se ha incrementado el valor del segundo array. El snippet del código es el siguiente:

array_21 = np.array([[1], [2]])
print(f'array3 + array_21={array3 + array_21}')  

La salida por consola es la siguiente:

array3 + array_21=[[ 8  9 10]
 [ 5  4  3]]

2.- Indexación avanzada.

Para realizar operaciones con parte de los elmentos de un array, se deben de crear referencias a la estructura con la que se desea operar. Si se desea incrementar en 100 la segunda posición de las dimensiones que forman parte de un array, se crea una referencia a las posiciones de todas las dimensiones y se incrementa en 100 con el operador +=, en nuestro ejemplo, center_array; una vez operado, el array inicial contiene el resultado. Hay que destacar que las operaciones no son inmutables y se trabaja con referencias. El snippet del código es el siguiente:

array = np.array([[1, 2, 3], [4, 5, 6]])
center_array = array[:, 1]
center_array += 100
print(f'center_array=\n{center_array}')
print(f'array=\n{array}')

La salida por consola es la siguiente:

center_array=
[102 105]
array=
[[  1 102   3]
 [  4 105   6]]

Si se desea realizar una copia de una dimensión se utiliza la función copy. En el siguiente ejemplo, se realiza una copia de una dimensión de una array inicial. El snippet de ejemplo es el siguiente:

array2 = np.array([[1, 2, 3], [4, 5, 6]])
second_row = array2[1].copy()  
second_row += 100
print(f'second_row=\n{second_row}')
print(f'array2=\n{array2}')

La salida por consola es la siguiente:

second_row=
[104 105 106]
array2=
[[1 2 3]
 [4 5 6]]

Para determinar si una referencia es una copia o forma parte de una estructura se emplea la función may_share_memory. A continuación, se muestran dos ejemplos de ejemplos previos. El snippet de código es el siguiente:

  first_row = array2[:1]
  np.may_share_memory(first_row, array2)
  print(f'np.may_share_memory(first_row, array2)=\n{np.may_share_memory(first_row, array2)}')
  print(f'np.may_share_memory(second_row, array2)=\n{np.may_share_memory(second_row, array2)}')

La salida por consola es la siguiente:

  np.may_share_memory(first_row, array2)=
  True
  np.may_share_memory(second_row, array2)=
  False

Para obtener arrays que sean subconjuntos de un array, podemos realizar seleccion de posiciones de una determinada dimensión. En el siguiente ejemplo, se imprimen todos los elementos de la primera dimensión y el primer y último elemento de la segundo; y, por último, el caso contrario, último y primero. El snippet del código es el siguiente:

array3 = np.array([[1, 2, 3], [4, 5, 6]])
print(f'array3=\n{array3}')  
print(f'array3[:, [0,2]]=\n{array3[:, [0,2]]}')  
print(f'array3[:, [2,0]]=\n{array3[:, [2,0]]}')  

La salida por consola es la siguiente:

array3=
[[1 2 3]
 [4 5 6]]
array3[:, [0,2]]=
[[1 3]
 [4 6]]
array3[:, [2,0]]=
[[3 1]
 [6 4]]

Por último, se pueden realizar operaciones de comparación de los elementos y obtener array lógicos con el resultado. En el siguiente ejemplo, se imprimen los elementos mayores a 3; impresión de los elementos que son mayores a 3; y, por último, se compone un predicado lógico. El snippet del código es el siiguiente:

array3 = np.array([[1, 2, 3], [4, 5, 6]])
print(f'array3 > 3=\n{array3 > 3}')  
print(f'array3[array3 > 3]=\n{array3[array3 > 3]}')  
print(f'array3[(array3 > 2) & (array3 <5)]=\n{array3[(array3 > 2) & (array3 <5)]}')

La salida por consola es la siguiente:

array3 > 3=
[[False False False]
 [ True  True  True]]
array3[array3 > 3]=
[4 5 6]
array3[(array3 > 2) & (array3 <5)]=
[3 4]

3.- Operaciones de comparación.

En el apartado anterior, se presentó la operación de comparación y, en el presente apartado, profundizaremos en las operaciones de comparación. En este primer ejemplo, se muestra una selección de elementos de un array mayores a 2. El snippet del código es el siguiente:

array = np.array([1, 2, 3, 4])
array_mayor_2 = array > 2
print(f'array_mayor_2=\n{array_mayor_2}\n')

La salida por consola es la siguiente:

array_mayor_2=
[False False  True  True]

Con los resultados lógicos podemos realizar operaciones para poder operar con ellos. Para ello, podemos emplear las siguientes funciones:

  • Función sum.- función que cuantifica el número de elementos que cumplen la función.
  • Función nonzero.- función que retorna el índice de la posición en la dimensión.

En el siguiente snippet se muestran ejemplos a partir del array inicial:

print(f'array[array_mayor_2]=\n{array[array_mayor_2]}\n')
print(f'array_mayor_2.sum()=\n{array_mayor_2.sum()}\n')  
print(f'array_mayor_2.nonzero()=\n{array_mayor_2.nonzero()}\n')  
print(f'(array > 2).nonzero()=\n{(array > 2).nonzero()}\n')  

La salida por consola es la siguiente:

array[array_mayor_2]=
[3 4]

array_mayor_2.sum()=
 2

array_mayor_2.nonzero()=
(array([2, 3]),)

(array > 2).nonzero()=
(array([2, 3]),)

Para poder realizar un tratamiento más específico, podemos utilizar la función where en la cuál podemos declarar qué valor asignar al resultado si cumple una condición, o bien, cuando no la cumple. En el siguiente snippet se muestra un ejemplo de uso de la función where:

array_where = np.where(array > 2)
print(f'np.where(array > 2)=\n{array_where}\n')
array_where_2 = np.where(array > 2, 1, 0)  
print(f'np.where(array > 2, 1, 0)=\n{array_where_2}\n')

La salida por consola es la siguiente:

 np.where(array > 2)=
  (array([2, 3]),)

  np.where(array > 2, 1, 0)=
  [0 0 1 1]

Otra forma de trabajar con predicados sin la función where es declarando un predicado con una condición. Una vez creado el predicado, lo aplicamos en el array como una indexación asignando el valor para el caso de éxito, o bien, utilizando el carácter ~ para el caso de no cumplirse. En el siguiente snippet se muestra un ejemplo de uso de ejemplo:

array2 = np.array([1, 2, 3, 4])
array2_mayor_2 = array2 > 2
array2[array2_mayor_2] = 1  
array2[~array2_mayor_2] = 0  
print(f'array2={array2}')

La salida por consola es la siguiente:

array2=[0 0 1 1]

En la siguiente entrada con título Numpy III presentaré los últimos ejemplos y finalizaré la serie de entradas relacionadas con la librería Numpy.

Numpy I

Numpy es una librería Python open-source para computación científica que permite tener el poder de computación de lenguajes como C o Fortran en lenguaje Python. En la presente entrada, Numpy I, realizaré una breve presentación y realizaré unos ejemplos básicos.

Las características generales de Numpy son las siguientes:

  1. Permite trabajar con matrices N dimensionales.
  2. Proporciona herramientas de computación numérica ofreciendo un conjunto de funciones matemáticas complejas.
  3. Es interoperable, con lo cual, permite trabajar con amplias plataformas, funciona con bibliotecas distribuidas y de GPU.
  4. El núcleo de la librería es código C bien optimizado.
  5. Fácil de usar.
  6. Es una librería de código abierto.

Los ejemplos practicos que mostraré en los siguientes apartados están desarrollados con Python 3.6. Las dependencias de las librerías utilizadas son las siguientes: numpy y matplotlib; y, para la utilización de las funciones en cada módulo, es necesario importar la librería de la siguiente forma:

import numpy as np

Los ejemplos de utilización de la librería Numpy a presentar en esta entrada son los siguientes:

  1. Creación de arrays n-dimensionales.
  2. Generación e inicialización de arrys n-dimensionales.
  3. Indexación de elementos en arrays n-dimensionales.
  4. Funciones básicas en arrays n-dimensionales.

1.- Creación de arrays N dimensaionales.

La creación de un array la realizaremos empleando la función array y, para determinar el tipo de los elementos del array, emplearemos la función dtype. En el siguiente ejemplo se define una array de dos dimensiones a partir de una lista.

lst = [[1, 2, 3],
       [4, 5, 6]]
arrayld = np.array(lst)
print(f'Array integer:\n{arrayld}')
print(f'Type={arrayld.dtype}')

La salida por consola es la siguiente:

Array integer:
[[1 2 3]
[4 5 6]]
Type=int64

Para crear un array de elementos de tipos reales a partir del array anterior, utilizamos la función astype con el tipo float32 definidos en Numpy, el snippet ejemplo es el siguiente:

array_float_32 = arrayld.astype(np.float32)
print(f'Array float32:\n{array_float_32}')
print(f'Type={array_float_32.dtype}\n')

La salida por consola es la siguiente:

Array float32:
[[1. 2. 3.]
[4. 5. 6.]]
Type=float32

Para crear un array de dos dimensiones de elementos enteros a partir de una lista, se utiliza la función array y se especifica el tipos int64. Una vez creado el array, podemos conocer sus características con las siguientes funciones:

  • itemsize.- para determinar el tamaño en bit que ocupan en el array.
  • size.- para determinar el número de elementos del array.
  • ndim.- para determinar el número de dimensiones.
  • shap.- para determinar el número de elementos por dimensión.

El snippet ejemplo es el siguiente:

array_2_dimesion = np.array([[1, 2, 3], [4, 5, 6]], dtype='int64')
print(f'array_2_dimesion=\n{array_2_dimesion}')
print(f'array_2_dimesion.itemsize={array_2_dimesion.itemsize}') 
print(f'array_2_dimesion.size={array_2_dimesion.size}') 
print(f'array_2_dimesion.ndim={array_2_dimesion.ndim}') 
print(f'array_2_dimesion.shape={array_2_dimesion.shape}\n') 

La salida por consola es la siguiente:

array_2_dimesion=
[[1 2 3]
 [4 5 6]]
array_2_dimesion.itemsize=8
array_2_dimesion.size=6
array_2_dimesion.ndim=2
array_2_dimesion.shape=(2, 3)

En el caso de un array de una dimensión, el resultado de la función shape sería el siguiente:

array_ahape =  np.array([1, 2, 3]).shape
print(f'array_ahape={array_ahape}')  

La salida por consola es la siguiente:

array_ahape=(3,)

2.- Generación e inicialización de arrays n-dimensionales.

La creación de un array n-dimensional de forma dinámica con un generador se realiza con la función fromiter. El generador debe de ser pasado como parámetro ya sea una función explícita, o bien, mediante una sentencia que defina un generador. El snippet con un ejemplo es el siguiente:

 def generator():
     for i in range(10):
         if not (i % 2):
             yield i
 gen = generator()
 array_generator = np.fromiter(gen, dtype=int)
 print(f'array_generator={array_generator}')
 generator_expression = (i for i in range(10) if i % 2)
 array_generator_expression = np.fromiter(generator_expression, dtype=int)
 print(f'array_generator_expression={array_generator_expression}')

La salida por consola es la siguiente:

array_generator=[0 2 4 6 8]
array_generator_expression=[1 3 5 7 9]
array_3_3_1=
  [[1. 1. 1.]
   [1. 1. 1.]
   [1. 1. 1.]]

Otra forma de crear arrays con inicializaciones determinadas se pueden realizar con las siguientes funciones:

  • ones.- Creación de un array n-dimensional de números reales con valor 1.
  • zeros.- Creación de un array n-dimensional de números reales con valor 0.
  • eye.- Creación de un array n-dimensional cuya diagonal tiene valor 1.
  • diag.- Creación de un array n-dimensional cuya diagonal tiene un valor pasado por parámetro.
  • arrange.- Creación de un array n-dimensional cuya diagonal tiene el rango de valores pasados por parámetro.
  • linspace.- Creación de un array n-dimensional cuyos de x elementos pasados por paámetros y valores comprendidos entre un valor mínimo y máximo.

El snippet de código con ejemplos es el siguiente:

 # Inicializadores de arrays.
 array_3_3_1 = np.ones((3, 3))
 print(f'array_3_3_1=\n{array_3_3_1}\n')
 array_3_3_0 = np.zeros((3, 3))
 print(f'array_3_3_0=\n{array_3_3_0}\n')
 array_eye_diagonal = np.eye(3)
 print(f'array_eye_diagonal=\n{array_eye_diagonal}\n')
 array_diagonal = np.diag((3, 3, 3))
 print(f'array_diagonal=\n{array_diagonal}\n')
 array_arange_float = np.arange(4., 10.)  
 print(f'array_arange_float=\n{array_arange_float}\n')
 array_arange_int = np.arange(5)  
 print(f'array_arange_int=\n{array_arange_int}\n')
 array_arange_interval = np.arange(1., 11., 2)  
 print(f'array_arange_interval=\n{array_arange_interval}\n')
 array_insterval_space_1 = np.linspace(0., 1., num=5)  
 print(f'array_insterval_space_1=\n{array_insterval_space_1}\n')
 array_insterval_space_2 = np.linspace(0., 1., num=6)  
 print(f'array_insterval_space_2=\n{array_insterval_space_2}\n')

La salida por consola es la siguiente:

  array_3_3_0=
  [[0. 0. 0.]
   [0. 0. 0.]
   [0. 0. 0.]]
  array_eye_diagonal=
  [[1. 0. 0.]
   [0. 1. 0.]
   [0. 0. 1.]]
  array_diagonal=
  [[3 0 0]
   [0 3 0]
   [0 0 3]]
  array_arange_float=
  [4. 5. 6. 7. 8. 9.]
  array_arange_int=
  [0 1 2 3 4]
  array_arange_interval=
  [1. 3. 5. 7. 9.]
  array_insterval_space_1=
  [0.   0.25 0.5  0.75 1.  ]
  array_insterval_space_2=
  [0.  0.2 0.4 0.6 0.8 1. ]

3.- Indexación de elementos en arrays n-dimensionales.

El acceso a los elementos de los array se realiza indicando la posición entre corchetes. Unos ejemplos de acceso a elementos de un array de una y dos dimensiones son los que se muestran en el siguiente snippet de código:

array = np.array([1, 2, 3])
print(f'array=\n{array}\n')
print(f'array[0]=\n{array[0]}')
print(f'array[1]=\n{array[1]}\n')
print(f'array[:2]=\n{array[:2]}\n')
print(f'array[1:]=\n{array[1:]}\n')
array_22 = np.array([[1, 2, 3], [4, 5, 6]])
print(f'array=\n{array_22}\n')
print(f'array_22[0,0]=\n{array_22[0, 0]}')
print(f'array_22[-1,-1]=\n{array_22[-1, -1]}\n')
print(f'array_22[0]=\n{array_22[0]}\n') 
print(f'array_22[:, 0]=\n{array_22[:, 0]}\n')
print(f'array_22[:, :2]=\n{array_22[:, :2]}\n')

La salida por consola es la siguiente:

array=
[1 2 3]
array[0]=
1
array[1]=
2
array[:2]=
[1 2]
array[1:]=
[2 3]
array=
[[1 2 3]
 [4 5 6]]
array_22[0,0]=
1
array_22[-1,-1]=
6
array_22[0]=
[1 2 3]
array_22[:, 0]=
[1 4]
array_22[:, :2]=
[[1 2]
 [4 5]]

4.- Funciones básicas en arrays n-dimensionales.

La manipulación de los elementos de los array n-dimensionales se puede realizar accediendo de forma directo, o bien, utilizando operadores matemáticos. En el siguiente snippet, se muestra unos ejemplos de manipulación de elementos:

 # Forma1: Suma 1 a los elementos de la lista
 list = [[1, 2, 3], [4, 5, 6]]
 list_mas_1 = [[cell + 1 for cell in row] for row in list]  # Incrementamos en 1
 print(f'list_mas_1=\n{list_mas_1}\n')
 # Forma2: Suma 1
 array_nd_list_mas_1 = np.add(list, 1)
 print(f'array_nd_list_mas_1=\n{array_nd_list_mas_1}\n')
 # Forma3: Suma 1
 ndarray_3 = np.array(list)
 array_nd_list_mas_12 = ndarray_3 + 1
 print(f'array_nd_list_mas_12=\n{array_nd_list_mas_12}\n')
 ndarray_cuadrada = np.array(list)
 array_nd_list_cuadrado = ndarray_cuadrada**2
 print(f'array_nd_list_cuadrado=\n{array_nd_list_cuadrado}\n')

La salida por consola es la siguiente:

list_mas_1=
[[2, 3, 4], [5, 6, 7]]
array_nd_list_mas_1=
[[2 3 4]
 [5 6 7]]
array_nd_list_mas_12=
[[2 3 4]
 [5 6 7]]
array_nd_list_cuadrado=
[[ 1  4  9]
 [16 25 36]]

Otras funciones básicas pueden ser las siguientes:

  • reduce.- función de suma de columnas de un array n-dimensional.
  • sum.- función de suma de elemntos de una terminada dimensión.
  • mean.- función de cálculo de la media.
  • std.- función de cálculo de la desviación típica.
  • var.- función de la varianza.
  • max.- función de cálculo del valor máximo.
  • min.- función de cálculo del vfalor mínimo

El snippet con ejemplos de utilización de uso de dichas funciones es el siguiente:

  ndarray = np.array(list)
  print(f'np.add.reduce(ndarray)={np.add.reduce(ndarray)}')  
  print(f'np.sum(ndarray, axis=0)={np.sum(ndarray, axis=0)}')  
  print(f'np.sum(ndarray, axis=1)={np.sum(ndarray, axis=1)}')  
  print(f'ndarray.sum()={ndarray.sum()}')  
  print(f'ndarray.mean()={ndarray.mean()}')
  print(f'ndarray.std()={ndarray.std()}')
  print(f'ndarray.var()={ndarray.var()}')
  print(f'ndarray.max()={ndarray.max()}')
  print(f'ndarray.min()={ndarray.min()}')
  print(f'ndarray.argmax()={ndarray.argmax()}')
  print(f'ndarray.argmin()={ndarray.argmin()}')

La salida por consola es la siguiente:

np.add.reduce(ndarray)=[5 7 9]
np.sum(ndarray, axis=0)=[5 7 9]
np.sum(ndarray, axis=1)=[ 6 15]
ndarray.sum()=21
ndarray.mean()=3.5
ndarray.std()=1.707825127659933
ndarray.var()=2.9166666666666665
ndarray.max()=6
ndarray.min()=1
ndarray.argmax()=5
ndarray.argmin()=0

En la siguiente entrada, Numpy II, continuaré describiendo operaciones con arrays en Numpy.

Microservicios en Python: plantilla básica.

En la presente entrada, Microservicios en Python: plantilla básica, realizaré una descripción de una plantilla base de un microservicio en Python utilizando la librería Flask.

La arquitectura de microservicios es aquel enfoque que permite definir aplicaciones software mediante un conjunto de servicios desplegables de forma independiente, es decir, una aplicación es un conjunto de pequeñas aplicaciones poco acopladas. La definición de microservicio que realiza Martin Fowler es la siguiente:

“El término ‘Arquitectura de microservicio’ ha surgido en los últimos años para describir una forma particular de diseñar aplicaciones de software como conjuntos de servicios desplegables de forma independiente. Si bien no existe una definición precisa de este estilo arquitectónico, existen ciertas características comunes en torno a la organización en torno a la capacidad empresarial, la implementación automatizada, la inteligencia en los puntos finales y el control descentralizado de idiomas y datos.”

El acoplamiento entre los microservicios, se puede realizar utilizando colas, brokers de mensajería o mediante peticiones HTTP. Un ejemplo de un productor y consumidor de mensajes para el broker de mensjaes que contiene Redis pueden ser los que describo en los siguientes enlaces:

La funcionalidad de la plantilla del microservicio es muy simple, se definirá un punto de entrada de tipo POST al cual se le pasarán los campos nombre, operación y operador y, como resultado, retornará un JSON con el resultado. Se empleará la técnica DDD Domain Driven Design para definir una entidad de dominio la cual será almacenda en un supuesto contenedor de datos, en nuestro caso, en memoria.

Las dependencias de las librerías del proyecto se definen en el fichero requirements.txt y contiene las siguientes referencias: flask, dataclasses y pytest.

La arquitectura está compuesta por tres capas horizontales: capa de presentación, representada por el paquete entrypoints; cada de servicios, representada por el paquete services; y, capa de datos, representada por el paquete repository. Desde un punto de vista vertical, tenemos las siguientes capas: capa de dominio, representada por el paquete domain en donde se define las entidades de dominio y DTO; y, por último,capa de excepciones, representado con el paquete exception en cual contiene las excepciones del
aplicativo.

Descripción arquitectónica por capas

Capa de dominio

La capa de dominio está compuesto por el módulo entity_model.py el cual contiene la entidad de dominio UseCaseEntity y los DTO UseCaseRequest y UseCaseResponse. El snippet de la entidad de dominio es el siguiente:

class UseCaseEntity:
    def __init__(self,
                 uuid: str,
                 name: str,
                 operation: str,
                 operator: int,
                 date_data: Optional[date] = None):
        self.uuid = uuid
        self.name = name
        self.operation = operation
        self.operator = operator
        self.date = date_data
    @property
    def calculate(self) -> int:
        result = 0
        if self.operation == "+":
             result = self.operator + self.operator
        elif self.operation == "*":
             result = self.operator * self.operator
        else:
             result = -1
        return result

Capa de presentación

La capa de presentación está compuesta por el módulo app.py. Los puntos de entrada son: métodos liveness y rediness para conocer el estado del microservicio (en el ejemplo no realizan ninguna operación) y el método para la operación de negocio use_case_example; este método, realiza la obtención de los parámetros de la petición HTTP, creación del DTO de la petición e invocación al método de servicio; para finalizar, retorna el resultado. El snippet de la función es la siguiente:

@app.route("/use_case_example", methods=['POST'])
def do_use_case_example():
    """
    use case example
    curl --header "Content-Type: application/json" --request POST \
         --data '{"name":"xyz1", "operation":"+", "operator":"20"}' \
         http://localhost:5000/use_case_example
    :return: str
    """
    p_name = request.json['name']
    p_operation = request.json['operation']
    p_operator = int(request.json['operator'])
    current_app.logger.info(f"[*] /use_case_example")
    current_app.logger.info(f"[*] Request: Name={p_name} operation={p_operation} operator={p_operator}")
    current_app.logger.info(f"Name={p_name} operation={p_operation} operator={p_operator}")
    data_request = entity_model.UseCaseRequest(uuid=uuid.UUID,
                                               name=p_name,
                                               operation=p_operation,
                                               operator=p_operator)
    repository = use_case_repository.UseCaseRepository()
    response_use_case = use_case_service.do_something(data_request, repository)
    data = jsonify({'result': response_use_case.resul})
    return data, 200

Capa de servicio

La capa de servicio está compuesta por el módulo use_case_service.py el cual contiene la función que realiza la operación de negocio: creación de la entidad de dominio, inserción en el repositorio de datos y retorno del resultado. El snippet de la función es el siguiente:

def do_something(request: entity_model.UseCaseRequest,
                 repository: use_case_repository.AbstractUseCaseRepository) -> entity_model.UseCaseResponse:
    """
    Business operation.
    :param request: entity_model.UseCaseRequest
    :param repository: use_case_repository.AbstractUseCaseRepository

    :return: entity_model.UseCaseResponse
    """
    if request is None:
        raise use_case_exception.UseCaseRequestException()
    logging.info(f"[**] /use_case_service.do_something")
    entity = entity_model.UseCaseEntity(uuid=request.uuid,
                                        name=request.name,
                                        operation=request.operation,
                                        operator=request.operator,
                                        date_data=date.today())
    repository.add(entity)
    return entity_model.UseCaseResponse(str(entity.calculate))

Capa de repositorios

La capa de repositorio define el respositorio en donde se almacenan los datos la cual está compuesta por el módulo use_case_repository.py. El módulo contiene la definición del repositorio UseCaseRepository para la entidad UseCaseEntity y la clase de abstracta con las operaciones de los repositorios. El snippet del repositorio es el siguiente:

class UseCaseRepository(AbstractUseCaseRepository):
    """
    Definition of the operations that connect to database.
    """
    def __init__(self) -> None:
        self.database: [entity_model.UseCaseEntity] = []
    def add(self, entity: entity_model.UseCaseEntity) -> bool:
        result: bool = False
        logging.info(f"[***] /use_case_repository.add")
        if entity is not None:
            result = True
            self.database.append(entity)
        return result
    def get(self, p_uuid: str) -> entity_model.UseCaseEntity:
        index = 0
        enc = False
        result: entity_model.UseCaseEntity = None
        logging.info(f"[***] /use_case_repository.get")
        while (index < len(self.database)) and not enc:
            aux: entity_model.UseCaseEntity = self.database[index]
            if aux.uuid == uuid.UUID(p_uuid):
                result = aux
                enc = True
            index += 1
       return result

Pruebas

La plantilla contiene el directorio tests el cual contiene los test de la plantilla del microservicio. Para ejecutar los test se ejecuta el siguiente comando desde la carpeta raíz del proyecto:

>pytest --setup-show

Docker

Todo microservicio debe de tener la definición de la imagen para que sea ejecutado en un contenedor. Así, existe el fichero Dockerfile para definir dicha imagen. El contenido de la imagen contiene las operaciones de instalación de las herramientas para Python, instalación de las librerías, copiado de código fuente y variables de entorno y ejecutación. El snippet con el contenido del DOckerfile es el siguiente:

FROM python:3.8-alpine
RUN apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev
RUN apk add libpq
COPY requirements.txt /tmp
RUN pip install -r /tmp/requirements.txt
RUN apk del --no-cache .build-deps
RUN mkdir -p /app
COPY . /app/
WORKDIR /app
ENV FLASK_APP=entrypoints/app.py FLASK_DEBUG=1 PYTHONUNBUFFERED=1
CMD flask run --host=0.0.0.0 --port=80

Makefile

Para facilitar las operaciones de Docker, se ha definido un fichero de tipo Makefile en el cual se definen las operaciones necesarias para operar con Docker. Las operaciones son las siguientes:

  • Operación build.- Creación de la imagen. Para ejecutar la operación se ejecuta el comando make build desde la raíz del proyecto. El snippet de la definición de la operación build es la siguiente:
build:
     docker image build -t alvaroms/template-microservice:v1.0 .
  • Operación run.- Arranque de un contenedor con la imagen del proyecto. Para ejecutar la operación se ejecuta el comando make run desde la raíz del proyecto. El snippet de la definición de la operación run es la siguiente:
run:
   docker container run -d --name template-microservice -p 6060:80 alvaroms/template-microservice:v1.0
  • Operación exec.- Acceso a la consola del contenedor. Para ejecutar la operación se ejecuta el comando make exec desde la raíz del proyecto. El snippet de la definición de la operación exec es la siguiente:
exec:
    docker container exec -it template-microservice /bin/sh
  • Operación logs.- Visualización de los logs del contenedor. Para ejecutar la operación se ejecuta el comando make logs desde la raíz del proyecto. El snippet de la definición de la operación logs es la siguiente:
logs:
    docker container logs template-microservice
  • Operación test.- Ejecución de los test. Para ejecutar la operación se ejecuta el comando make test desde la raíz del proyecto. El snippet de la definición de la operación test es la siguiente:
test:
    pytest --setup-show
  • Operación all.- Ejecución de los test, construcción de la imagen y arranque del contenedor. Para ejecutar la operación se ejecuta el comando make all desde la raíz del proyecto. El snippet de la definición de la operación test es la siguiente:
all: test build run

Pruebas del API

Los comando curl para realizar las pruebas sobre el microservicio desplegado en el contenedor son los siguientes:

  • Root del microsercicio.
    curl http://localhost:6060/
  • Función rediness
    curl http://localhost:6060/readiness
  • Función liveness
    curl http://localhost:6060/liveness
  • Función de negocio.
    curl --header "Content-Type: application/json" --request POST \
    
    --data '{"name": "xyz1", "operation": "+", "operator": "20"}' \
    
    http://localhost:6060/use_case_example

Integración Contínua

Para finalizar, se define un pipeline de integración contínua definida en el fichero .travis.yml. El snippet con el contenido es el siguiente:

dist: xenial
language: python
python: 3.6
install:
- pip3 install -r requirements.txt

script:
- make test

branches:

 

Para el lector interesado puede acceder al código a través del siguiente enlace.

Redis: consumidor de mensajes

En la entrada anterior, Redis: productor de mensajes, describo cómo definir un productor para la publicación de mensajes en el broker Redis; en la presente entrada, Redis: consumidor de mensajes, describiré cómo consumir mensajes del broker.

El primer paso es crear el broker al cual publicar mensajes; para ello, trabajaré con una imagen Docker con Redis. Para descarga la imagen y arrancar el contenedor es necesario ejecutar los siguientes comandos:

docker pull redis
docker run --name some-redis -d redis

Tras su ejecución, tendremos Redis en una contenedor cuyo puerto de acceso es el 6379.

El segundo paso, es escribir el código del consumidor. Seguiremos los mismos criterios que en la entrada Redis: productor de mensajes.

Para crear la conexión con Redis, creamos un objeto de tipo Redis con los datos de la conexión a Redis. El snippet del código es el siguiente:

import redis
publish_redis = redis.Redis(host=config.HOST_REDIS, port=config.PORT_REDIS, db=0)

Una vez que tenemos la referencia a Redis, necesitamos suscribirnos al topic donde leer los mensajes; una vez suscritos, nos mantenemos a la espera de la recepción del mesaje; al recepcionar el mensaje, obtenemos un mensaje con una estructura de diccionario del cual deberemos de obtener el campo data. El snippet con el código es el siguiente:

consume_client_topic = publish_redis.pubsub()
consume_client_topic.subscribe(config.TOPIC_REDIS)
for message in consume_client_topic.listen():
  if message['data'] != 1:
    data = json.loads(message['data'].decode())
    logging.info(f"mesagge={data['message']} result={data['result']}")

Para el lector interesado, el código del enlace está en el siguiente enlace.

Redis: productor de mensajes

Redis es una herramienta Open source la cual puede ser utilizada como un almacén de estructura de datos en memoria, como una cache, como base de datos y como un broker
de mensajes. En la presente entrada, Redis: productor de mensajes en el broker, me centraré en describir cómo crear un productor de mensajes en el broker de mensajes de Redis. El ejemplo estará definido en lenguaje Python.

El primer paso es crear el broker al cual publicar mensajes; para ello, trabajaré con una imagen Docker con Redis. Para descargar la imagen y arrancar el contenedor es necesario ejecutar los siguientes comandos:

docker pull redis
docker run --name some-redis -d redis

Tras su ejecución, tendremos Redis en una contenedor cuyo puerto de acceso es el 6379.

El segundo paso es crear un proyecto Python en donde definiremos la dependencia del paquete redis y un fichero de tipo Python con el código del productor.

Para crear la conexión con Redis, creamos un objeto de tipo Redis con los datos de la conexión a Redis. El snippet del código es el siguiente:

import redis
publish_redis = redis.Redis(host=config.HOST_REDIS, port=config.PORT_REDIS, db=0)

Una vez creado la referecia a Redis, utilizaremos la función publish para publicar un mensaje en un topic de Redis. El snippet ejemplo es el siguiente:

msg = '{"message": "Test message-%d", "result": "OK"}' % index
publish_redis.publish(config.TOPIC_REDIS, msg)

El valor config.TOPIC_REDIS corresponde con un valor alfanumérico.

Para el lector interesado, el código del enlace está en el siguiente enlace.

En la siguiente entrada, Redis: consumidor de mensajes, realizaré la descripción de un consumidor de mensajes.

Test unitarios y cobertura de código en Python

En la presente entrada, Test unitarios y cobertura de código en Python, realizaré la descripción de cómo se realizan test unitarios en Python con unittest y, además, cómo se realizan el análisis del código para generar el índice de cobertura de código con la herramienta coverage.

Los ejemplos estarán realizados con la versión 3.6 de Python.

logo-python

Test unitarios

Los test unitarios los definimos utilizando el framework unittest el cual está incorporado en la distribución de la versión del lenguaje.

Para realizar un test de un código, iniciaremos la definición de un código al cual se definirán el conjunto de test a definir. Este proceso inicial lo realizaré para comprender el proceso.

Definiré una clase de utilidad Util con un método statusToCode cuya funcionalidad consistirá en parsear un parámetro alfanumérico de entrada y, como salida, retornará un valor entero.

El snippet de la clase es la siguiente:

class Util:
  """Class utils."""
  @staticmethod
  def statusToCode(code="") -> int:
    """Return exit code"""
    assert len(code) > 0, "Argument not valid"
    result = {
      'UP': 0,
      'WARNING': 1,
      'CRITICAL': 2,
      'UNKOWN': 3,
    }.get(code, 3)
    return result

La clase Util está definida en el módulo util.py dentro de la carpeta lib. La función statusToCode tiene un decorador definido con nombre @staticmethod el cual permite definir el método en la clase con referencia estática para poderlo utilizar sin la necesidad de instanciar la clase.

Los test los definiremos en la carpeta lib_test la cual está definida al mismo nivel que la clase lib. Definiré las clases de test conforme a los módulos definidos. Así, tendremos la clase UtilTest en el módulo test_utils.py de la carpeta lib_test.

La clase UtilTest deberá de importar el módulo unittest y definir la clase heredando de la clase unittest.TestCase para poder realizar los test. Además, deberá de importar la clase con el código que se desea probar. Así, la clase queda definida de la siguiente forma:

import unittest
from lib.utils import Util

class UtilTests(unittest.TestCase):
  def setUp(self):
    pass

  def test_statusToCode_EMPTY(self):
    try:
      print(sys.executable)
      Util.statusToCode("")
    except AssertionError as exception:
      self.assertTrue(exception != None)

  def test_statusToCode_UP(self):
    self.assertEqual(Util.statusToCode("UP"), 0)

  def test_statusToCode_WARNING(self):
    self.assertEqual(Util.statusToCode("WARNING"), 1)

  def test_statusToCode_CRITICAL(self):
    self.assertEqual(Util.statusToCode("CRITICAL"), 2)

  def test_statusToCode_UNKNOWN(self):
    self.assertEqual(Util.statusToCode("UNKNOWN"), 3)

La clases UtilTests presenta seis métodos: el método setUp, el cual realiza la definición de las operaciones previas a la ejecución de los test, en nuestro caso no es necesario realizar ninguna; y, el resto de métodos, que definen los test al tener como prefijo la cadena “test_”.

La verificación de los resultados se realiza empleando la referencia self la cual define las funciones de comprobación.

Cobertura

La cobertura de código la realizaremos con la herramienta coverage cuya referencia la pondremos en el fichero requirements.txt para que sea cargado en el entorno virtual del proyecto.

La herramienta coverage tiene la capacidad de realizar la generación de los informes por línea de comando, o bien, la generación de los informes en formato html; dichos informes, se generarán en la carpeta htmlcov del propio proyecto.

Los comandos que ejecutaremos son los siguientes:

  • coverage erase.- Eliminación de los datos previos de cobertura. Un ejemplo de ejecución en la línea de comandos es el siguiente: coverage erase
  • coverage run.- Arranque del programa Python que recolecta los datos. Un ejemplo de ejecución en la línea de comandos es el siguiente: coverage run –omit=’.tox/*,.venv/*’ -m unittest
  • coverage report.- Generación resultados. Un ejemplo de ejecución en la línea de comandos es el siguiente:  coverage report –omit=’.tox/*,venv/*’ -m
  • coverage html.- Generación de los informer en formato HTML. Un ejemplo de ejecución en la línea de comandos es el siguiente: coverage html –omit=’.tox/*,venv/*’

Un ejemplo de informe de cobertura tiene el siguiente aspecto:

python coverage html

Automatización del proceso de Cobertura

En el apartado anterior, he definido la forma de ejecutar los test y la Generación de los informes de cobertura y, en el presente apartado, realizaré la descripción de cómo lo podemos automatizar.

La automatización la realizamos empleando la herramienta tox(https://tox.readthedocs.io/en/latest/) Para poder utilizar tox, primeramente, es necerios definir en el fichero requirement.txt la herramienta tox; una vez instalado en el entorno virtual, deberemos definir el plan de ejecución de tox el cual se define en el fichero tox.ini ubicado en la carpeta raíz del proyecto.

El aspecto del fichero tox es el siguiente:

[tox]
envlist = py36, coverage-report
skipsdist = True

[testenv]
commands = python -m pytest {posargs}
deps =
-r{toxinidir}/requirements.txt
freezegun==0.3.9
pytest==3.5.0
passenv=*

[testenv:coverage-report]
skip_install = true
commands =
coverage erase
coverage run --omit='.tox/*,.venv/*' -m unittest
coverage report --omit='.tox/*,venv/*' -m
coverage html --omit='.tox/*,venv/*'

El fichero tox está compuesto de tres elementos de configuración los cuáles tienen la siguiente descripción:

  • Elemento tox: Definición del entorno virtual de ejecución, elemento a ejecutar y el flag de generación del artefacto para la distribución
  • Elemento testenv: Definición de la configuración necesaria por tox.
  • Elemento coverage-report: Definición de la secuencia de comandos de la herramienta coverage para el cálculo y generación de informes de cobertura.

Para ejecutar el proceso automático tecleamos en la línea de comando y posicionados en la carpeta de proyecto el comando tox.

Conclusión

El proceso de generación de test unitartios en Python es un proceso parecido a otros lenguajes como Java o Scala. Además, al estár el framework incorporado en la distribución no requiere de ninguna operación de carga, facilitando su uso.