En esta lección revisamos uno de lso formatos más usados para el almacenamiento de datos de tipo científicos. HDF5 es recomendado para registrar informaciones secuenciales, por ejemplo proveniente de medidas de equipos científicos, salud, etc.
Los datos que se almacenan en archivos de tipo HDF5 se llaman datasets y básicamente corresponden a arrays multidimensionales.
Cuando los datos son de tipo tabular (tipos tabla de datos o dataframes) otrso formatos son recomendados, como por ejemplo el formato parquet.
#! conda install h5py
import numpy as np
import h5py
import os
### define carpeta de trabajo
# reproducibilidad
np.random.seed(100)
# Temperatura
temperatura_15 = np.random.random(1024) # temperaturas estación. 15
temperatura_10 = np.random.random(1024)
# Viento
viento_15 = np.random.random(2048)
viento_10 = np.random.random(2048)
estacion_15 = 15 # estación 15
estacion_10 = 10
dt_temp = 10 # delta-T temperatura
dt_viento = 20
start_time_temp_15 = 1375204299 # en Unix time
start_time_temp_10 = 1375204500 # en Unix time
start_time_viento_15 = 137550000
start_time_viento_10 = 137550000
print(temperatura_15)
[0.54340494 0.27836939 0.42451759 ... 0.24908888 0.27119505 0.05880634]
folder = '../Datos/'
file_name = "clima.hdf5" # archivo
file = os.path.join(folder, file_name)
f = h5py.File(file, mode='w')
print(file)
print(f)
../Datos/clima.hdf5 <HDF5 file "clima.hdf5" (mode r+)>
f['/15/temperatura'] = temperatura_15
f['/15/temperatura'].attrs["dt"] = dt_temp
f['/15/temperatura'].attrs['star_time'] = start_time_temp_15
f['/15/viento'] = viento_15
f['/15/viento'].attrs["dt"] = dt_viento
f['/15/viento'].attrs['star_time'] = start_time_viento_15
f['/10/temperatura'] = temperatura_10
f['/10/temperatura'].attrs["dt"] = dt_temp
f['/10/temperatura'].attrs['star_time'] = start_time_temp_10
f['/10/viento'] = viento_10
f['/10/viento'].attrs["dt"] = dt_viento
f['/10/viento'].attrs['star_time'] = start_time_viento_10
f.close()
file = os.path.join(folder, file_name)
f = h5py.File(file, mode='r')
f.keys()
<KeysViewHDF5 ['10', '15']>
group_15 = f['/15']
group_15
<HDF5 group "/15" (2 members)>
group_15.keys()
<KeysViewHDF5 ['temperatura', 'viento']>
dataset = f['/15/temperatura']
for key, value in dataset.attrs.items():
print('%s: %s' % (key, value))
dt: 10 star_time: 1375204299
dataset[0:10]
array([0.54340494, 0.27836939, 0.42451759, 0.84477613, 0.00471886, 0.12156912, 0.67074908, 0.82585276, 0.13670659, 0.57509333])
type(dataset)
h5py._hl.dataset.Dataset
np.mean(dataset)
0.4948712717532915
dataset.shape
(1024,)
HDF5 utiliza un sistema de tipos muy similar a Numpy. Cada array
(dataset) o conjunto de datos en un archivo HDF5 tiene un tipo fijo representado por un objeto de tipo. El paquete h5py
mapea automáticamente el HDF5
type system en NumPy dtypes, lo que, entre otras cosas, facilita el intercambio
datos con NumPy.
Por ejemplo, HDF5, toma prestada esta sintaxis de "corte" para permitir cargar solo porciones de un conjunto de datos
import h5py
f = h5py.File("../Datos/nombre.hdf5","a")
f.close()
El ejemplo típico de manejo de contexto con archivos es
with open("../data/garbage.txt","w") as f:
f.write("Hello!")
Cuando se sale del contexto, el archvo se cierra automáticamente.
Podemos hacer lo mismo con archivos hdf5
with h5py.File("../data/nombre.hdf5","w") as f:
print(f["dataset_perdido"]) # error dataset no existe en el archivo
list(f.keys()) # error. Archivo cerrado
print(f)
type(f)
Los controladores de archivos se encuentran entre el sistema de archivos y el mundo de alto nivel de los grupos HDF5, los datasets y atributos. Los controladores se ocupan de la mecánica del manejo de espacio en memoria de los archivos HDF5 disponibles en el disco.
Por lo general, no tendrá que preocuparse por cúal controlador está en uso, ya que el controlador predeterminado funciona bien para la mayoría de las aplicaciones.
Lo mejor de los controladores es que una vez que se abre el archivo, son totalmente transparentes. Simplemente use la biblioteca HDF5 como de costumbre, y el controlador se encarga de la mecánica de almacenamiento.
El controlador core
almacena su archivo completamente en la memoria. Obviamente, hay un límite en cuanto a cómo
gran cantidad de datos que puede almacenar, pero la compensación es lecturas y escrituras increíblemente rápidas. Es una gran elección cuando desea la velocidad de acceso a la memoria, pero también desea utilizar el HDF5
estructuras.
f = h5py.File('../Datos/nombre.hdf5', driver='core')
Para decirle a HDF5 que cree un archivo y que guarde la imagen actual del archivo cuando se cierre, puede usar backing store
:
f = h5py.File('../data/nombre.hdf5', driver='core', backing_store=True)
Permite separar un archivo en múltiple imágenes, cada de la cuales comparte cierto tamaño máximo.
# divide el archivo en trozos de 1Gb de memoria
f = h5py.File('../data/family.hdf5', memb_size=1024**3)
El tamaño por defecto es memb_size = $2^{31}-1$.
Este controlador es el corazón de Parallel HDF5. Le permite acceder al mismo archivo desde múltiples procesos al mismo tiempo. Puede tener docenas o incluso cientos de procesos de computación paralela, todos los cuales comparten una vista coherente de un solo archivo en el disco.
Una característica interesante de HDF5 es que los archivos pueden estar precedidos por datos arbitrarios de usuario.
Cuando se abre un archivo, la biblioteca busca el encabezado HDF5 al comienzo del archivo, luego 512 bytes en, luego 1024, y así sucesivamente en potencias de 2.
Dicho espacio al principio del archivo se denomina bloque de usuario
y allí el usuario puede almacenar los datos que desee.
Las únicas restricciones están en el tamaño del bloque (potencias de 2 y al menos 512), y que
no debería tener el archivo abierto en HDF5 al escribir en el bloque de usuario. Aquí hay un
ejemplo:
f = h5py.File('../Datos/userblock_example.hdf5', 'w', userblock_size=512)
print(f.userblock_size)
f.close()
512
palabra = 'a'*512
with open('../Datos/userblock_example.hdf5', 'r+') as f:
f.write(palabra)
with open('../Datos/userblock_example.hdf5', 'rb+') as f: # abre como binario porque así está almacenado
pal = f.read(512)
pal
b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
Los conjuntos de datos (datasets
) son la característica central de HDF5. Puede pensar en ellos como arreglos (arrays) NumPy que viven en disco. Cada conjunto de datos en HDF5 tiene un nombre, un tipo y una forma, y admite datos aleatorios.
acceso.
A diferencia de np.save y friends integrados en Numpy, no es necesario leer ni escribir el arreglo completo como un bloque; puede usar la sintaxis estándar de NumPy para cortar, leer o escribir solo las partes que desee del array.
Primero, creemos un archivo para que tengamos un lugar donde almacenar nuestros conjuntos de datos:
f = h5py.File('../Datos/testfile.hdf5', 'w')
Cada conjunto de datos de un archivo HDF5 tiene un nombre. Veamos qué sucede si asignamos un nuevo array NumPy a un nombre en el archivo:
import numpy as np
arr = np.ones((5,2))
f['my_dataset'] = arr
dset = f['my_dataset']
dset
<HDF5 dataset "my_dataset": shape (5, 2), type "<f8">
f.keys()
<KeysViewHDF5 ['my_dataset']>
Observe que dset es una instancia de la clase h5py-Dataset. Este objeto se accesa como cualquier archivo Numpy.
print(dset[:])
print(dset[...])
[[1. 1.] [1. 1.] [1. 1.] [1. 1.] [1. 1.]] [[1. 1.] [1. 1.] [1. 1.] [1. 1.] [1. 1.]]
print(dset.dtype)
print(dset.shape)
print(dset[:, -1])
out = dset[...]
print(type(out))
print(out.shape)
float64 (5, 2) [1. 1. 1. 1. 1.] <class 'numpy.ndarray'> (5, 2)
No es necesario tener una matriz NumPy lista para crear un conjunto de datos.
dset = f.create_dataset('test1', shape=(10,10))
f.keys()
<KeysViewHDF5 ['my_dataset', 'test1']>
dset[...]
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., 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.], [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., 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.]], dtype=float32)
HDF5 es lo suficientemente inteligente como para asignar solo la cantidad de espacio en el disco que realmente necesita almacenar los datos que escribe. A continuación, se muestra un ejemplo: suponga que desea crear un conjunto de datos 1D que puede contener muestras de datos de 4 gigabytes de un experimento de larga duración:
dset = f.create_dataset('big_dataset', (1024**3), dtype= np.float32)
dset[0:1024] = np.arange(1024)
f.flush() # envia los datos al archivo
#f.close()
# Por favor revise el tamaño del archivo en el sistema operativo
# ls -lh testfile.hdf5
import numpy as np
import h5py
bigdata = np.ones((100,1000)) # np.float64
print(bigdata.dtype)
print(bigdata.shape)
float64 (100, 1000)
Guarda con el tipo de dato original: np.float64
with h5py.File('../Datos/big1.hdf5','w') as f1:
f1['big'] = bigdata
Cambia el tipo de dato al enviar al archivo
with h5py.File('../Datos/big2.hdf5', 'w') as f2:
f2.create_dataset('big', data=bigdata, dtype=np.float32)
Recupera los datos
f1 = h5py.File('../Datos/big1.hdf5')
f2 = h5py.File('../Datos/big2.hdf5')
print(f1['big'].dtype)
print(f2['big'].dtype)
float64 float32
f1 =
Conversión automática de tipos
La propia biblioteca HDF5 maneja la conversión de tipos y lo hace sobre la marcha (on the fly) cuando se guarda en o se lee de un archivo.
Vemos como sucede esto.
dset = f2['big']
print(dset.dtype)
print(dset.shape)
En este momento dset apunta a los datos en el archivo, pero no han sido cargados en memoria. Para cargarlos con 'float64' lo que debe hacer asignar primero un arreglo vacío de doble precisión en la memoria:
big_out = np.empty((100, 1000), dtype= np.float64)
Ahora cargamos los datos a la memoria en ese espacio asignao en memoria. La API de HDF5 hace la conversón por el camino (on the fly):
dset.read_direct(big_out)
print(dset.dtype)
print(big_out.dtype)
dset sigue apuntando al archivo, mientras que big_out apunta a los datos (convertidos) en memoria.
En realidad con read_direct no necesariamente tiene que leer todos los datos.
with dset.astype('float64'):
out = dset[0,:]
print(out.dtype)
print(out.shape)
En esta seccion estudiamos inportantes asuntos de implementación que mejoran el desempeño de los programas con hdf5. Primero estudiamos algunas diferencias entre los arreglos de Numpy y los datasets de hdf5.
Empezamos leyendo de un dataset existente
import numpy as np
import h5py
f2 = h5py.File('../Datos/big1.hdf5','r+')
print(f2.keys())
dset = f2['big']
dset
out = dset[0:10, 20:70]
out.shape
Tomar rebabadas de tamaño razonable. Revise los dos siguientes snippets de código. ¿Cuál es más eficiente?
# Chequear valiores negativos y reemplazarlos por cero (0).
from time import time
%time
f2 = h5py.File('../Datos/big1.hdf5','r+')
for ix in range(100):
for iy in range(1000):
val = dset[ix,iy] # lee elemento
if val < 0:
dset[ix,iy] = 0 # recorta a 0 si es negativo
# Chequear valiores negativos y reemplazarlos por cero (0).
%time
for ix in range(100):
val = dset[ix,:] # lee una fila
val[val<0] = 0 # recorta si es negativo
dset[ix,:] = val # escribe de regreso en el dataset hdf5
from dask.distributed import Client
client = Client(n_workers=4)
import dask.array as da
x = da.from_array(dset, chunks=(100))
x
%time
x[x<0] = 0
dset[:] = x
f2.close()
import numpy as np
import h5py
f = h5py.File('../Datos/example1.hdf5','w')
dset = f.create_dataset('range', data=range(10))
print(dset[4]) # un elemento
print(dset[4:8]) # rebanado
print(dset[...]) # notación puntos suspensivos (ellipsis) todo
print(dset[:]) # todo en una dimensión
print(dset[4:-1]) # notación del último elemento
print(dset[::-1]) # Fallo. Recorrer en modo reverse
dset = f.create_dataset('4d', shape=(100,80,50,20))
print(dset.shape)
print(dset[...].shape)
dset = f.create_dataset('1d', shape=(1,), data = 42)
print(dset[0])
print(dset.shape)
print(dset[...].shape)
print(dset[:])
Como en Numpy. se pueden usar máscara con valores boolenos para extraer elementos de un dataset.
data = np.random.random(10)*2 -1
data
dset = f.create_dataset('random', data=data)
dset[data<0] = 0
dset[...]
dset[data<0] = -1*data[data<0]
dset[...]
dset[[1,2,7]]
Al leer directamente en un array existente se llena el arrelgo y se hace el recast (conversión de tipo) de manera automática
dset = f.create_dataset('100_1000_array', shape=(100, 1000), dtype=np.float32)
dset
out = np.empty((100,1000), dtype=np.float64)
dset.read_direct(out)
out
Supongamos que queremos leer la primera fila, en dset [0 ,:], y depositarlo en el array de salida en fila 50: out[50 ,:]. Podemos utilizar las palabras clave source_sel y dest_sel, para la selección de la fuente y la selección del destino respectivamente:
dset.read_direct(out, source_sel = np.s_[0,:], dest_sel= np.s_[50,:])
Observe los dos siguientes snippets de código. ¿Cuál es más eficiente?
out = dset[:, 0:50]
print(out.shape)
means = out.mean(axis=1)
print(means.shape)
out = np.empty((100,50),dtype = np.float32)
dset.read_direct(out, np.s_[:,0:50])
mean = out.mean(axis=1)
Este puede parecer un caso trivial, pero hay una diferencia importante entre los dos enfoques.
No hay dierencia en el desempeño, pero ahora evaluamos un arreglo grande
dset = f.create_dataset('test', shape=(10000, 10000), dtype=np.float32)
dset[:] = np.random.random(10000) # uso de broadcasting para completar
from time import time
from dask.distributed import Client
client = Client(n_workers=4)
import dask.array as da
%time
dset[:,0:500].mean(axis=1)
#numpy
out = np.empty((10000, 500), dtype=np.float32)
%time
dset.read_direct(out, np.s_[:,0:500])
out.mean(axis=1)
# dask
%time
x = da.from_array(dset[:,0:500], chunks=(4))
y= x.mean(axis=1)
y.compute()
def time_dataset():
dset[:,0:500].mean(axis=1)
def time_numpy():
dset.read_direct(out, np.s_[:,0:500])
out.mean(axis=1)
#def time_dask():
# x = da.from_array(dset[:,0:500], chunks=(4))
# y = x.mean(axis=1).compute()
from timeit import timeit
timeit(time_dataset, number=100)
timeit(time_numpy, number=100)
#timeit(time_dask, number=100)
Así como en Numpy se puede cambiar el tamaño de los arreglos, mientras se mantenga la coherencia en el tamaño global, dejando al usuario la responsabilidad de la interpretabilidad de los datos, con los datasets de hdf5 podemos hacer lo mismo, pero es posible perder información. Veámos.
dset = f.create_dataset('fixed', (2,2))
print(dset.shape)
print(dset.maxshape)
La propiedad maxshape sugiere que el tamaño podría variar hasta esos extremos. En efecto, asi es. Veámos.
dset = f.create_dataset('resizable', (2,2), maxshape=(2,2))
print(dset.shape)
print(dset.maxshape)
dset.resize((1,1))
print(dset.shape)
print(dset.maxshape)
dset.resize((1,3)) # falla porque 3>2
print(dset.shape)
print(dset.maxshape)
No esposible cambiar las dimensiones, es decir, el número total de ejes, de una dataset. Pero si se pueden dejar dimensiones sin un tamaño definido para que crezcan idefinidamente. Basta colocar en la posición respectiva None cuando crea el dataset. Veámos
dset = f.create_dataset('unlimited', (2,2), maxshape =(2,None))
print(dset.shape)
print(dset.maxshape)
dset.resize((2,2*30))
print(dset.shape)
Las reglas de Numpy no aplican exactamento a los dataset. Observe el siguiente ejemplo Numpy
a = np.array([[1,2],[3,4]])
print(a.shape)
print(a)
a.resize((1,4))
print(a)
a.resize((1,10))
print(a)
Ahora veámos el mismo código con dataset
dset = f.create_dataset('sizetest', (2,2), dtype=np.int32,
maxshape=(None, None))
dset[...] = [[1,2],[3,4]]
print(dset)
dset[...]
dset.resize((1,4))
dset[...] # se perdió la fila 1, hdf5 no pudo hacer el reordenamiento
dset.resize((1,10))
dset[...]
Mostramos dos vías. La segunda es más recomendada.
# Agregado al paso
dset1= f.create_dataset('time_traces', (1,1000), maxshape=(None,1000))
def add_trace(arr):
dset.resize(dset1.shape[0]+1, 1000)
dset[-1,:] = arr
# creando dataset grande y al final podando
dset2 = f.create_dataset('time_traces_2', (5000,1000), maxshape=(None,1000))
ntraces= 0
def add_trace_2(arr):
global ntraces
dset2[ntraces,:] = arr
ntraces += 1
def done():
dset.resize((ntraces,1000))
Consideremos el siguiente arreglo 2-dimensional de Numpy
import numpy as np
a = np.array([['A','B'],['C','D']])
print(a)
En realidad el arreglo esta organizado en la memoria linealmente, en el siguiente orden
Lo mismo ocurre con los datasets de hdf5. Esto implica que algunas forma de recuperacion de información son mpás eficientes que otras.
Suponga que va a almacenar 10 imágenes en b/n (escala de grises) de tamaño 480*640. Entonces haría algo como lo siguiente
import numpy as np
import h5py
f = h5py.File('imagetest.hdf5','w')
dset = f.create_dataset('Imagenes', (100,480,640))
La primera imagen se recupera como
imagen = dset[0,:,:]
image.shape # (480, 640)
Esta imagen muesra como es almacenado el dataset
Pero, ¿qué pasa si, en lugar de procesar imágenes completas una tras otra, nuestra aplicación trata con mosaicos de imagen?
Supongamos que queremos leer y procesar los datos en un segmento de 64 × 64 píxeles en la esquina de la primera imagen; por ejemplo, digamos que queremos agregar un logotipo. Nuestra selección de rebanado sería:
tile = dset[0,0:64, 0.64]
tile.shape # (64,64)
¿Y si hubiera alguna forma de expresar esto de antemano? ¿No hay forma de preservar la forma del conjunto de datos, que es semánticamente importante, pero dígale a HDF5 que optimice la conjunto de datos para el acceso en bloques de 64 × 64 píxeles?
Eso es lo que hace la fragmentación en HDF5. Le permite especificar la "forma" N-dimensional que se adapta mejor a su patrón de acceso. Cuando llega el momento de escribir datos en el disco, HDF5 se divide los datos en "trozos" de la forma especificada, los aplana y los escribe en el disco. Los fragmentos se almacenan en varios lugares del archivo y sus coordenadas están indexadas por un Árbol B (binario).
Aquí tenemos un ejemplo. Tomemos el conjunto de datos de forma (100, 480, 640) que se acaba de mostrar y digamos a HDF5 que lo almacene en formato fragmentado.
Hacemos esto proporcionando una nueva palabra clave, fragmentos, al método create_dataset:
dset = f.create_dataset('fragmentado', (100,480,640), dtype='i1',
chunks=(1,64,64))
Así son almacenados los datos en este caso.
Así es como ocurre la compresión en hdf5.
Si no esta seguro sobre como gestionará el dataset, puede dejar que hdf5 decida como fragmentarlo
dset = f.create_dataset('imagenes2', (100,480,640), chunks=True)
dset.chunks # (13, 60,80)
A continuación, se incluyen algunas cosas que debe tener en cuenta al trabajar con fragmentos. El proceso de elegir la forma de los fragmentos es una compensación entre las siguientes tres restricciones:
de 1 MB ni siquiera compartiran el caché.
Entonces, estos son los puntos principales a tener en cuenta:
se accederá de una manera que probablemente sea ineficaz con el almacenamiento contiguo o una forma de trozo adivinada automáticamente. Y como todas las optimizaciones, ¡debería compararlas!
su aplicación leerá "mosaicos" particulares de 64 × 64, podría usar N × 64 × 64 trozos (o N × 128 × 128) a lo largo de los ejes de la imagen.
tomado por metadatos. Una buena regla general para la mayoría de los conjuntos de datos es mantener fragmentos por encima de 10 KB más o menos.
se lee el fragmento. Si solo usa un subconjunto de los datos, el tiempo extra dedicado a la lectura de el disco está desperdiciado. Tenga en cuenta que los trozos de más de 1 MiB de forma predeterminada no participar en el rápido "caché de fragmentos" en memoria y, en su lugar, se leerá desde el disco cada vez.
Con la fragmentación, es posible realizar la compresión de forma transparente en un conjunto de datos.
Se conoce el tamaño inicial de cada fragmento y, dado que están indexados por un árbol B, pueden almacenarse en cualquier lugar del archivo, no solo uno tras otro. En otras palabras, cada trozo es libre para crecer o encogerse sin golpear a los demás
HDF5 tiene el concepto de una tubería de filtro, que es solo una serie de operaciones realizadas en cada fragmento cuando está escrito. Cada filtro es libre de hacer lo que quiera con los datos en el fragmento: comprimirlo, sumarlo, agregar metadatos, cualquier cosa. Cuando se lee el archivo, el filtro se ejecuta en modo "inverso" para reconstruir los datos originales.
La imagen muestra como sucede la acción
Hay varios filtros de compresión disponibles en HDF5. Con mucho, el más utilizado es el filtro GZIP. (También escuchará que se hace referencia a esto como el filtro "DEFLATED"; en el mundo HDF5, ambos nombres se utilizan para el mismo filtro).
A continuación, se muestra un ejemplo de compresión GZIP utilizada en un conjunto de datos de punto flotante:
dset = f.create_dataset('BigDataset', (1000,1000),dtype='f',
compression='gzip' )
dset.compression # 'gzip
Los grupos son el objeto contenedor HDF5, análogo a las carpetas de un sistema de archivos. Ellos pueden contener conjuntos de datos y otros grupos, lo que le permite construir una estructura jerárquica con objetos perfectamente organizados en grupos y subgrupos
El objeto de grupo más general es h5py.Group, del cual h5py.File es una subclase. Otro Los grupos se crean fácilmente con el método create_group:
#import h5py
f = h5py.File('../Datos/Groups.hdf5','w')
subgroup = f.create_group('SubGroup')
subgroup
<HDF5 group "/SubGroup" (0 members)>
subgroup.name
'/SubGroup'
Por supuesto, los grupos también se pueden anidar. El método create_group existe en todos los grupos objetos, no solo File:
subsubgroup = subgroup.create_group('AnotherGroup')
subsubgroup.name
'/SubGroup/AnotherGroup'
No es necesario crear manualmente todos los subgrupos anidados.
out = f.create_group('/some/big/path')
out
<HDF5 group "/some/big/path" (0 members)>
f.keys()
<KeysViewHDF5 ['SubGroup', 'some']>
Si no recuerda nada más de esta sección, recuerde esto: los grupos funcionan principalmente como diccionarios.
Hay un par de agujeros en esta abstracción, pero en general funciona sorprendentemente bien. Los grupos son iterables y tienen un subconjunto del diccionario Python normal API.
Agregemos unos pocos objetos al archivo de este ejemplo para lo que sigue.
f['Dataset1'] = 1.0
f['Dataset2'] = 2.0
f['Dataset3'] = 3.0
subgroup['Dataset4'] = 4.0
# accesa el dataset asociado a la 'clave' dataset1
dset1 = f['Dataset1']
dset4 = f['SubGroup/Dataset4']
print(len(f))
print(len(f['SubGroup']))
print(f.keys())
5 2 <KeysViewHDF5 ['Dataset1', 'Dataset2', 'Dataset3', 'SubGroup', 'some']>
import h5py
f = h5py.File('../Datos/propdemo.hdf5', 'w')
grp = f.create_group('hola')
print(grp.file == f)
print(grp.parent)
True <HDF5 group "/" (1 members)>
Existe una capa entre el objeto Group y sus objetos miembros. Los dos están relacionados por el concepto de enlaces.
Los enlaces en HDF5 se manejan de la misma manera que en los sistemas de archivos modernos. Objetos como los datasets y los grupos no tienen un nombre intrínseco; más bien, tienen una dirección (byte offset) en el archivo que HDF5 tiene que buscar. Cuando asigna un objeto a un nombre en un grupo esa dirección se registra en el grupo y se asocia con el nombre que proporcionó para formar un enlace.
Entre otras cosas, esto significa que los objetos de un archivo HDF5 pueden tener más de un nombre; de hecho, tienen tantos nombres como enlaces existan apuntando a ellos. El número de enlaces que apuntan a un objeto se registra, y cuando no existen más enlaces, el espacio utilizado es liberado porque el objeto es liberado.
Este tipo de vínculo, el predeterminado en HDF5, se denomina vínculo duro para diferenciarlo de otros tipos de enlaces.
Veámos un ejemplo.
f = h5py.File('linksdemo.hdf5', 'w')
grpx = f.create_group('x')
print(grpx.name)
f['y'] = grpx
grpy = f['y']
print(grpy == grpx)
print(grpy.name)
/x True /y
Para remover enlaces se usa la sintaxis se diccionarios del
print(f.keys())
del f['y'] # elimina el enlace 'y'
print(f.keys())
del f['x'] # elimina el enlace 'x'
# como no hay mas enlces el grupe es eliminado
print(f.keys())
<KeysViewHDF5 ['x', 'y']> <KeysViewHDF5 ['x']> <KeysViewHDF5 []>
Cuando se elimina un objeto (por ejemplo, un conjunto de datos grande), el espacio que ocupaba en el disco se reutiliza para nuevos objetos como grupos y conjuntos de datos. Sin embargo, en el momento de escribir, HDF5 no realiza un seguimiento de ese "espacio libre" en los ciclos de apertura/cierre de archivos.
Entonces, si no terminas reutilizando el espacio en el momento en que cierre el archivo, puede terminar con un "agujero" de inutilizable espacio en el archivo que no se puede recuperar.
Este tema es una alta prioridad de desarrollo para el Grupo HDF. Mientras tanto, si los archivos parecen inusualmente grandes, puede "volver a empaquetarlos" con la herramienta h5repack
, de HDF5. En el sistema operativo vaya a la carpeta en donde está el archivo y ejecute por ejemplo
h5repack bigfile.hdf5 out.hdf5
A diferencia de los enlaces duros, que asocian un nombre de enlace con un objeto particular en el archivo, los enlaces blandos en su lugar, almacena la ruta a un objeto.
Si tuviéramos que crear un enlace duro en el grupo raíz al conjunto de datos, siempre apuntaría a ese objeto en particular, incluso si el conjunto de datos es movido o desvinculado del grupo. Aquí hay un ejemplo:
import h5py
f = h5py.File('../Datos/test.hdf5', 'w')
grp = f.create_group('mygroup')
dset = grp.create_dataset('dataset', (100,))
f['hardlink'] = dset
print(f['hardlink'] == grp['dataset'])
True
Movamos hacia atrás el conjunto de datos y luego creemos un vínculo blando que apunte a la ruta /mygroup/dataset. Para decirle a HDF5 que queremos crear un enlace suave, asigne un instancia de la clase h5py.SoftLink a un nombre en el archivo:
grp.move('dataset', 'new_dataset_name')
print(f['hardlink'] == grp['new_dataset_name'])
True
grp.move('new_dataset_name', 'dataset')
f['softlink'] = h5py.SoftLink('/mygroup/dataset')
print(f['softlink'] == grp['dataset'])
True
f.close()
Los objetos SoftLink son muy simples; solo tienen una propiedad, .path, que contiene la ruta siempre que se creen:
softlink = h5py.SoftLink('../Datos/path')
print(softlink)
print(softlink.path)
<SoftLink to "../Datos/path"> ../Datos/path
Esta versión de get es un poco más capaz que la de Python.
Hay dos palabras clave adicionales además del estilo de diccionario predeterminado: getclass
y getlink`.
La palabra clave getclass
le permite recuperar el tipo de un objeto sin
realmente tener que abrirlo.
En el nivel HDF5, esto solo requiere leer algunos metadatos y por lo tanto es muy rápido. Aquí hay un ejemplo: primero crearemos un archivo que contenga un solo grupo y un solo conjunto de datos:
import h5py
f = h5py.File('../Datos/get_demo.hdf5', 'w')
f.create_group('subgroup')
f.create_dataset('dataset', (100,))
<HDF5 dataset "dataset": shape (100,), type "<f4">
for name in f:
print(name, f.get(name, getclass=True))
dataset <class 'h5py._hl.dataset.Dataset'> subgroup <class 'h5py._hl.group.Group'>
A diferencia de los diccionarios de Python, no puede sobrescribir directamente a los miembros de un grupo:
f = h5py.File('../Datos/require_demo.hdf5','w')
f.create_group('x')
f.create_group('y')
f.create_group('y')
f['y'] = f['x']
Esta es una función intencional diseñada para evitar la pérdida de datos. Dado que los objetos son inmediatamente eliminado cuando los desvincula de un grupo, debe eliminar explícitamente el enlace en lugar de que tener HDF5 lo haga por usted:
del f['y']
f['y'] = f['x']
Esto conduce a algunos dolores de cabeza en el código del mundo real. Por ejemplo, un fragmento de análisis El código puede crear un archivo y escribir los resultados en un conjunto de datos:
data = do_large_calculation()
with h5py.File('output.hdf5') as f:
...
f.create_dataset('results', data=data)
Si hay muchos conjuntos de datos y grupos en el archivo, es posible que no sea apropiado sobrescribir el archivo completo cada vez que se ejecuta el código. Pero si no abrimos en modo w, nuestro programa sólo funcionará la primera vez, a menos que eliminemos manualmente el archivo de salida cada vez que corre el programa
Para lidiar con esto, create_group y create_dataset tienen métodos complementarios llamados require_group y require_dataset. Hacen exactamente lo mismo, solo que primero busque un grupo o conjunto de datos existente y lo devuleven en su lugar.
Ambas versiones toman exactamente los mismos argumentos y palabras clave. En el caso de require_dataset, h5py también verifica la forma solicitada y el tipo dtype con cualquier conjunto de datos existente y falla si no coinciden:
f.create_dataset('dataset', (100,), dtype='i')
f.require_dataset('dataset', (100,), dtype='f')
Hay un detalle menor aquí, en el sentido de que un conflicto solo se considera que ocurre si las formas no coincidencia, o la precisión solicitada del tipo de datos es mayor que la precisión existente. Entonces, si hay un conjunto de datos int64 preexistente, require_dataset tendrá éxito si int32 es solicitado:
f.create_dataset('int_dataset', (100,), dtype='int64')
f.require_dataset('int_dataset', (100,), dtype='int32')
Las reglas de conversión de NumPy se utilizan para comprobar si hay conflictos; puedes probar los tipos tú mismo utilizando np.can_cast.
En el archivo HDF5, los miembros del grupo se indexan mediante una estructura denominada "árbol B".
Los "árboles B" son estructuras de datos excelentes para realizar un seguimiento de una gran cantidad de elementos, al mismo tiempo que agiliza la recuperación (y la adición) de elementos. Funcionan tomando una colección de elementos, cada uno de los cuales se puede ordenar de acuerdo con algún esquema como un nombre de cadena o identificador numérico y la creación de un "índice" en forma de árbol para recuperar rápidamente un elemento.
f = h5py.File('iterationdemo.hdf5', 'w')
f.create_group('1')
f.create_group('2')
f.create_group('10')
f.create_dataset('data', (100,))
f.keys()
<KeysViewHDF5 ['1', '10', '2', 'data']>
[x for x in f]
['1', '10', '2', 'data']
[(x,y) for (x,y) in f.items()]
[('1', <HDF5 group "/1" (0 members)>), ('10', <HDF5 group "/10" (0 members)>), ('2', <HDF5 group "/2" (0 members)>), ('data', <HDF5 dataset "data": shape (100,), type "<f4">)]
[y for y in f.values()]
[<HDF5 group "/1" (0 members)>, <HDF5 group "/10" (0 members)>, <HDF5 group "/2" (0 members)>, <HDF5 dataset "data": shape (100,), type "<f4">]
'1' in f
True
grp = f['1']
path = '/1'
path in grp
True
f.close()