Практическое руководство по Zarr: чанки, сжатие, индексация и визуализация больших массивов
Начало работы с Zarr
Этот практический туториал рассматривает Zarr — библиотеку для эффективного хранения и обработки больших многомерных массивов на диске. Мы пройдём по базовым операциям, протестируем стратегии чанкинга, сравним кодеки сжатия, организуем иерархические наборы данных с метаданными и продемонстрируем расширенную индексацию и визуализацию. Примеры реализованы на Python и моделируют реальные рабочие сценарии для временных рядов и объёмных данных.
!pip install zarr numcodecs -q
import zarr
import numpy as np
import matplotlib.pyplot as plt
from numcodecs import Blosc, Delta, FixedScaleOffset
import tempfile
import shutil
import os
from pathlib import Path
print(f"Zarr version: {zarr.__version__}")
print(f"NumPy version: {np.__version__}")
print("=== BASIC ZARR OPERATIONS ===")
Базовые операции и создание массивов
Создадим рабочую директорию и инициализируем Zarr-массивы на диске: большой 2D массив нулей и 3D массив единиц. Затем заполним части массивов случайными и последовательными значениями и проверим формы, размеры чанков и приблизительное использование диска.
tutorial_dir = Path(tempfile.mkdtemp(prefix="zarr_tutorial_"))
print(f"Working directory: {tutorial_dir}")
z1 = zarr.zeros((1000, 1000), chunks=(100, 100), dtype='f4',
store=str(tutorial_dir / 'basic_array.zarr'), zarr_format=2)
z2 = zarr.ones((500, 500, 10), chunks=(100, 100, 5), dtype='i4',
store=str(tutorial_dir / 'multi_dim.zarr'), zarr_format=2)
print(f"2D Array shape: {z1.shape}, chunks: {z1.chunks}, dtype: {z1.dtype}")
print(f"3D Array shape: {z2.shape}, chunks: {z2.chunks}, dtype: {z2.dtype}")
z1[100:200, 100:200] = np.random.random((100, 100)).astype('f4')
z2[:, :, 0] = np.arange(500*500).reshape(500, 500)
print(f"Memory usage estimate: {z1.nbytes_stored() / 1024**2:.2f} MB")
Продвинутый чанкинг для временных рядов и пространственных запросов
Расположение чанков сильно влияет на производительность. Здесь мы моделируем годовой временной ряд с пространственными размерами и выбираем чанки, которые балансируют запросы по времени и по пространству. Добавляем сезонный сигнал и шум, после чего измеряем время доступа к временной и пространственной выборкам.
print("\n=== ADVANCED CHUNKING ===")
time_steps, height, width = 365, 1000, 2000
time_series = zarr.zeros(
(time_steps, height, width),
chunks=(30, 250, 500),
dtype='f4',
store=str(tutorial_dir / 'time_series.zarr'),
zarr_format=2
)
for t in range(0, time_steps, 30):
end_t = min(t + 30, time_steps)
seasonal = np.sin(2 * np.pi * np.arange(t, end_t) / 365)[:, None, None]
spatial = np.random.normal(20, 5, (end_t - t, height, width))
time_series[t:end_t] = (spatial + 10 * seasonal).astype('f4')
print(f"Time series created: {time_series.shape}")
print(f"Approximate chunks created")
import time
start = time.time()
temporal_slice = time_series[:, 500, 1000]
temporal_time = time.time() - start
start = time.time()
spatial_slice = time_series[100, :200, :200]
spatial_time = time.time() - start
print(f"Temporal access time: {temporal_time:.4f}s")
print(f"Spatial access time: {spatial_time:.4f}s")
Сжатие и сравнение кодеков
Разные кодеки и уровни сжатия меняют занимаемое на диске место и скорость доступа. Мы сравниваем отсутствие сжатия, LZ4, ZSTD и подход для последовательных данных. Сравнение размеров на диске помогает выбрать оптимальный кодек для ваших целей.
print("\n=== COMPRESSION AND CODECS ===")
data = np.random.randint(0, 1000, (1000, 1000), dtype='i4')
from zarr.codecs import BloscCodec, BytesCodec
z_none = zarr.array(data, chunks=(100, 100),
codecs=[BytesCodec()],
store=str(tutorial_dir / 'no_compress.zarr'))
z_lz4 = zarr.array(data, chunks=(100, 100),
codecs=[BytesCodec(), BloscCodec(cname='lz4', clevel=5)],
store=str(tutorial_dir / 'lz4_compress.zarr'))
z_zstd = zarr.array(data, chunks=(100, 100),
codecs=[BytesCodec(), BloscCodec(cname='zstd', clevel=9)],
store=str(tutorial_dir / 'zstd_compress.zarr'))
sequential_data = np.cumsum(np.random.randint(-5, 6, (1000, 1000)), axis=1)
z_delta = zarr.array(sequential_data, chunks=(100, 100),
codecs=[BytesCodec(), BloscCodec(cname='zstd', clevel=5)],
store=str(tutorial_dir / 'sequential_compress.zarr'))
sizes = {
'No compression': z_none.nbytes_stored(),
'LZ4': z_lz4.nbytes_stored(),
'ZSTD': z_zstd.nbytes_stored(),
'Sequential+ZSTD': z_delta.nbytes_stored()
}
print("Compression comparison:")
original_size = data.nbytes
for name, size in sizes.items():
ratio = size / original_size
print(f"{name}: {size/1024**2:.2f} MB (ratio: {ratio:.3f})")
Иерархическая организация данных и метаданные
Zarr поддерживает иерархические группы и атрибуты для документирования экспериментов. Пример ниже организует сырые изображения, временные метки и обработанные данные, а также добавляет атрибуты корневой группы с описанием эксперимента.
print("\n=== HIERARCHICAL DATA ORGANIZATION ===")
root = zarr.open_group(str(tutorial_dir / 'experiment.zarr'), mode='w')
raw_data = root.create_group('raw_data')
processed = root.create_group('processed')
metadata = root.create_group('metadata')
raw_data.create_dataset('images', shape=(100, 512, 512), chunks=(10, 128, 128), dtype='u2')
raw_data.create_dataset('timestamps', shape=(100,), dtype='datetime64[ns]')
processed.create_dataset('normalized', shape=(100, 512, 512), chunks=(10, 128, 128), dtype='f4')
processed.create_dataset('features', shape=(100, 50), chunks=(20, 50), dtype='f4')
root.attrs['experiment_id'] = 'EXP_2024_001'
root.attrs['description'] = 'Advanced Zarr tutorial demonstration'
root.attrs['created'] = str(np.datetime64('2024-01-01'))
raw_data.attrs['instrument'] = 'Synthetic Camera'
raw_data.attrs['resolution'] = [512, 512]
processed.attrs['normalization'] = 'z-score'
timestamps = np.datetime64('2024-01-01') + np.arange(100) * np.timedelta64(1, 'h')
raw_data['timestamps'][:] = timestamps
for i in range(100):
frame = np.random.poisson(100 + 50 * np.sin(2 * np.pi * i / 100), (512, 512)).astype('u2')
raw_data['images'][i] = frame
print(f"Created hierarchical structure with {len(list(root.group_keys()))} groups")
print(f"Data arrays and groups created successfully")
Продвинутая индексация в объёмных данных
Генерируем синтетический 4D-объём и демонстрируем быстрые операции срезов: максимальные проекции, z-стеки и пороговую фильтрацию. Это показывает, как Zarr позволяет извлекать подмножества данных без загрузки всего объёма в память.
print("\n=== ADVANCED INDEXING ===")
volume_data = zarr.zeros((50, 20, 256, 256), chunks=(5, 5, 64, 64), dtype='f4',
store=str(tutorial_dir / 'volume.zarr'), zarr_format=2)
for t in range(50):
for z in range(20):
y, x = np.ogrid[:256, :256]
center_y, center_x = 128 + 20*np.sin(t*0.1), 128 + 20*np.cos(t*0.1)
focus_quality = 1 - abs(z - 10) / 10
signal = focus_quality * np.exp(-((y-center_y)**2 + (x-center_x)**2) / (50**2))
noise = 0.1 * np.random.random((256, 256))
volume_data[t, z] = (signal + noise).astype('f4')
print("Various slicing operations:")
max_projection = np.max(volume_data[:, 10], axis=0)
print(f"Max projection shape: {max_projection.shape}")
z_stack = volume_data[25, :, 100:156, 100:156]
print(f"Z-stack subset: {z_stack.shape}")
bright_pixels = volume_data[volume_data > 0.5]
print(f"Pixels above threshold: {len(bright_pixels)}")
Оптимизация производительности
Обработка пакетами размеров чанков снижает требования к памяти и позволяет эффективно применять фильтры и агрегаты на больших наборах данных. Пример ниже демонстрирует такую стратегию обработки.
print("\n=== PERFORMANCE OPTIMIZATION ===")
def process_chunk_serial(data, func):
results = []
for i in range(0, len(dt), 100):
chunk = data[i:i+100]
results.append(func(chunk))
return np.concatenate(results)
def gaussian_filter_1d(x, sigma=1.0):
kernel_size = int(4 * sigma)
if kernel_size % 2 == 0:
kernel_size += 1
kernel = np.exp(-0.5 * ((np.arange(kernel_size) - kernel_size//2) / sigma)**2)
kernel = kernel / kernel.sum()
return np.convolve(x.astype(float), kernel, mode='same')
large_array = zarr.random.random((10000,), chunks=(1000,),
store=str(tutorial_dir / 'large.zarr'), zarr_format=2)
start_time = time.time()
chunk_size = 1000
filtered_data = []
for i in range(0, len(large_array), chunk_size):
end_idx = min(i + chunk_size, len(large_array))
chunk_data = large_array[i:end_idx]
smoothed = np.convolve(chunk_data, np.ones(5)/5, mode='same')
filtered_data.append(smoothed)
result = np.concatenate(filtered_data)
processing_time = time.time() - start_time
print(f"Chunk-aware processing time: {processing_time:.4f}s")
print(f"Processed {len(large_array):,} elements")
Визуализация результатов
Графики помогают проверить стратегии чанкинга и сжатия: временные ряды, пространственные паттерны, соотношения сжатия и профили по Z дают быстрый обзор поведения данных.
print("\n=== VISUALIZATION ===")
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
fig.suptitle('Advanced Zarr Tutorial - Data Visualization', fontsize=16)
axes[0,0].plot(temporal_slice)
axes[0,0].set_title('Temporal Evolution (Single Pixel)')
axes[0,0].set_xlabel('Day of Year')
axes[0,0].set_ylabel('Temperature')
im1 = axes[0,1].imshow(spatial_slice, cmap='viridis')
axes[0,1].set_title('Spatial Pattern (Day 100)')
plt.colorbar(im1, ax=axes[0,1])
methods = list(sizes.keys())
ratios = [sizes[m]/original_size for m in methods]
axes[0,2].bar(range(len(methods)), ratios)
axes[0,2].set_xticks(range(len(methods)))
axes[0,2].set_xticklabels(methods, rotation=45)
axes[0,2].set_title('Compression Ratios')
axes[0,2].set_ylabel('Size Ratio')
axes[1,0].imshow(max_projection, cmap='hot')
axes[1,0].set_title('Max Intensity Projection')
z_profile = np.mean(volume_data[25, :, 120:136, 120:136], axis=(1,2))
axes[1,1].plot(z_profile, 'o-')
axes[1,1].set_title('Z-Profile (Center Region)')
axes[1,1].set_xlabel('Z-slice')
axes[1,1].set_ylabel('Mean Intensity')
axes[1,2].plot(result[:1000])
axes[1,2].set_title('Processed Signal (First 1000 points)')
axes[1,2].set_xlabel('Sample')
axes[1,2].set_ylabel('Amplitude')
plt.tight_layout()
plt.show()
Выводы
Zarr предоставляет гибкие инструменты для управления чанками, кодеками и структурой набора данных, что позволяет адаптировать хранение и доступ под конкретные нагрузки. Примеры и бенчмарки в этом руководстве показывают практические решения для временных рядов, изображений и объёмных данных, а также демонстрируют интеграцию чанко-ориентированной обработки и визуализации в масштабируемые пайплайны.