Arrays estructurados#
Una de las características que hemos visto de los arrays de numpy es que su tipo interno de datos es homogéneo. Sin embargo, los datos tabulares a veces requieren datos con tipos heterogéneos.
Para este tipo de datos, numpy ofrece arrays con un tipo de datos complejo: arrays estructurados y recarray
.
Este tipo de datos funciona bien en escenarios de uso sencillos. Cuando las operaciones sobre los
datos se vuelven complejas, resulta más conveniente utilizar Pandas y su DataFrame
.
Supongamos que tenemos datos sobre los planetas del sistema solar en diferentes categorías. Se podrían almacenar estos datos en diferentes arrays o listas
import numpy as np
nombre = ['Mercurio', 'Venus', 'Tierra', 'Marte', 'Júpiter', 'Saturno', 'Urano', 'Neptuno']
semieje = [0.39, 0.72, 1.0, 1.52, 5.2, 9.54, 19.19, 30.06]
nlunas = [0, 0, 1, 2, 79, 82, 27, 14]
Pero este estilo no es muy conveniente. No hay nada en los datos que indique que pertencen a los mismos objetos. Sería más natural si pudiéramos encuadrar los datos dentro de un única estrutura.
Numpy puede manejar este tipo de datos mediante arrays estructurados, que son que aqellos que tienen
un tipo de datos dtype
compuesto.
Igual que para generar un array de enteros hacemos:
x = np.zeros(8, dtype=int)
Podemos generar un array compuesto pasando un dicionario en dtype
(hay otras posibilidades):
data = np.zeros(8, dtype={'names': ('nombre', 'semieje', 'nlunas'),
'formats': ('U10', 'f4', 'u2')})
print(data.dtype)
[('nombre', '<U10'), ('semieje', '<f4'), ('nlunas', '<u2')]
Los diferentes formatos son
U10
: cadena de unicode longitud máxima 10f4
: número de coma flotante de 4 bytes o 32 bitsu2
: número entero sin signo de 2 bytes o 16 bits
Ahora que tenemos el contenedor creado, podemos rellenarlo con los valores anteriores. Podemos acceder a cada columna por el nombre:
data['nombre'] = nombre
data['semieje'] = semieje
data['nlunas'] = nlunas
print(data)
[('Mercurio', 0.39, 0) ('Venus', 0.72, 0) ('Tierra', 1. , 1)
('Marte', 1.52, 2) ('Júpiter', 5.2 , 79) ('Saturno', 9.54, 82)
('Urano', 19.19, 27) ('Neptuno', 30.06, 14)]
De esta manera, todos los datos están almacenados en un solo objeto en memoria. Además, podemos acceder a los registros por un índice o a las diferentes columnas por nombre:
# Nombres de los planetas
data['nombre']
array(['Mercurio', 'Venus', 'Tierra', 'Marte', 'Júpiter', 'Saturno',
'Urano', 'Neptuno'], dtype='<U10')
# Tercer planeta desde el Sol
data[2]
np.void(('Tierra', 1.0, 1), dtype=[('nombre', '<U10'), ('semieje', '<f4'), ('nlunas', '<u2')])
# Lunas del penúltimo planeta
data[-2]['nlunas']
np.uint16(27)
Con las herramientas de las secciones anteriores podemos realizar filtrado con máscara booleanas.
# Lunas de los planetas exteriores
# i.e, distancia mayor que Marte (1.5)
data[ data['semieje'] > 2 ]['nlunas']
array([79, 82, 27, 14], dtype=uint16)
Aunque este tipo de manipulaciones son factibles, en cuanto se hacen un poco complicadas es más práctico utilizar un paquete especializado de datos tabulares, como pandas o xarray.
Los tipos estructurados de numpy están diseñados para reflejar los tipos de dato
struct
de C y para acceder a buffers de datos de bajo nivel, como por
ejemplo para interpretar datos opacos (blobs) binarios. Por eso los dtype
estructurados tienen soporte para uniones (union
), datos anidados, etc.
Puede darse el caso de que los tipos estructurados de numpy tengan peor desempeño
que los tipos de pandas.
Métodos de creación de arrays estructurados#
Los arrays estructurados pueden crearse de diferentes maneras. Existen unas cuantas que solo estan permitidas para mantener compatibilidad hacia atrás con versiones antiguas de numpy (y que no deben utilizarse en código nuevo).
dtype
puede especificarse como diccionario:
np.dtype({'names': ('nombre', 'semieje', 'nlunas'),
'formats': ('U10', 'f4', 'u2')})
dtype([('nombre', '<U10'), ('semieje', '<f4'), ('nlunas', '<u2')])
también con tipos de datos en vez de cadenas
np.dtype({'names': ('nombre', 'semieje', 'nlunas'),
'formats': ((np.str_, 10), np.float32, np.uint16)})
dtype([('nombre', '<U10'), ('semieje', '<f4'), ('nlunas', '<u2')])
o como lista de tuplas
np.dtype([('nombre', (np.str_, 10)), ('semieje', np.float32), ('nlunas', np.uint16)])
dtype([('nombre', '<U10'), ('semieje', '<f4'), ('nlunas', '<u2')])
si no se da nombre a las columnas, el nombre por defecto es f#
np.dtype('U10,f4,u2')
dtype([('f0', '<U10'), ('f1', '<f4'), ('f2', '<u2')])
El tipo recarray
#
Por último, mencionar que el tipo recarray
es equivalente a los tipos estructurados
con la capacidad adicional de que se puede acceder a las columnas como atributo.
Podemos convertir la tabla anterior a recarray con
data_rec =data.view(np.recarray)
data_rec.nlunas
array([ 0, 0, 1, 2, 79, 82, 27, 14], dtype=uint16)
La pega es que el acceso a los datos en un recarray es más lento
%timeit data['nlunas']
%timeit data_rec['nlunas']
%timeit data_rec.nlunas
70 ns ± 0.794 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
1.34 μs ± 2.74 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
2.54 μs ± 26.1 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
Incluso usando el operador[], el acceso en recarray es más lento por un orden de magnitud