Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Chunked N-D arrays with compression and cloud storage. NumPy-style indexing. Backends: local, S3, GCS, ZIP, memory. Dask/Xarray integration for parallel and labeled computation. For lineage use lamindb; for labeled arrays use xarray.
.claude/skills/jaechang-hits-zarr-python/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 391% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 160% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 445% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 178% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 438% | 0% |
Zarr is a Python library for storing large N-dimensional arrays with chunking, compression, and parallel I/O. It provides NumPy-compatible indexing with pluggable storage backends (local, cloud, in-memory), making it the standard format for cloud-native scientific data pipelines.
bashpip install zarr # Cloud storage support pip install s3fs # Amazon S3 pip install gcsfs # Google Cloud Storage
Requires Python 3.11+.
pythonimport zarr import numpy as np # Create a chunked, compressed 2D array z = zarr.create_array( store="data/my_array.zarr", shape=(10000, 10000), chunks=(1000, 1000), dtype="f4" ) # Write with NumPy-style indexing z[:, :] = np.random.random((10000, 10000)).astype("f4") # Read a slice (only reads needed chunks) subset = z[0:100, 0:100] print(f"Shape: {subset.shape}, dtype: {subset.dtype}") # Shape: (100, 100), dtype: float32
pythonimport zarr import numpy as np # Empty arrays z = zarr.zeros(shape=(10000, 10000), chunks=(1000, 1000), dtype="f4", store="data.zarr") z = zarr.ones((5000, 5000), chunks=(500, 500), dtype="f4") z = zarr.full((1000, 1000), fill_value=42, chunks=(100, 100), dtype="i4") # From existing NumPy data data = np.arange(10000, dtype="f4").reshape(100, 100) z = zarr.array(data, chunks=(10, 10), store="from_numpy.zarr") print(f"Created: shape={z.shape}, chunks={z.chunks}, dtype={z.dtype}") # Create like another array (matches shape, chunks, dtype) z2 = zarr.zeros_like(z)
python# Open existing array z = zarr.open_array("data.zarr", mode="r+") # Read-write z = zarr.open_array("data.zarr", mode="r") # Read-only z = zarr.open("data.zarr") # Auto-detect array vs group
pythonimport zarr import numpy as np z = zarr.zeros((10000, 10000), chunks=(1000, 1000), dtype="f4") # Write slices z[0, :] = np.arange(10000, dtype="f4") z[10:20, 50:60] = np.random.random((10, 10)).astype("f4") z[:] = 42 # Fill entire array # Read slices (returns NumPy array) row = z[5, :] block = z[0:100, 0:100] print(f"Row shape: {row.shape}, block shape: {block.shape}") # Advanced indexing z.vindex[[0, 5, 10], [2, 8, 15]] # Coordinate (fancy) indexing z.oindex[0:10, [5, 10, 15]] # Orthogonal indexing z.blocks[0, 0] # Block/chunk indexing # Resize and append z.resize(15000, 15000) z.append(np.random.random((1000, 10000)).astype("f4"), axis=0)
Chunk shape is the most important performance parameter.
pythonimport zarr from zarr.codecs import ShardingCodec # Chunk aligned with access pattern # Row-wise access → chunk spans columns z_row = zarr.zeros((10000, 10000), chunks=(10, 10000), dtype="f4") # Column-wise access → chunk spans rows z_col = zarr.zeros((10000, 10000), chunks=(10000, 10), dtype="f4") # Mixed access → balanced square chunks (~1MB each for float32) z_bal = zarr.zeros((10000, 10000), chunks=(512, 512), dtype="f4") # 512*512*4 bytes = ~1MB per chunk # Sharding: group small chunks into larger storage objects # Useful when millions of small chunks cause filesystem overhead z_sharded = zarr.create_array( store="sharded.zarr", shape=(100000, 100000), chunks=(100, 100), # Small chunks for fine-grained access shards=(1000, 1000), # Groups 100 chunks per shard dtype="f4" ) print(f"Chunks: {z_sharded.chunks}, shards reduce file count")
Chunk size guidelines:
pythonfrom zarr.codecs.blosc import BloscCodec from zarr.codecs import GzipCodec, ZstdCodec, BytesCodec import zarr # Default: Blosc with Zstandard (good balance) z = zarr.zeros((1000, 1000), chunks=(100, 100), dtype="f4") # Explicit Blosc configuration z = zarr.create_array( store="compressed.zarr", shape=(1000, 1000), chunks=(100, 100), dtype="f4", codecs=[BloscCodec(cname="zstd", clevel=5, shuffle="shuffle")] ) # Speed-optimized (LZ4) z_fast = zarr.create_array( store="fast.zarr", shape=(1000, 1000), chunks=(100, 100), dtype="f4", codecs=[BloscCodec(cname="lz4", clevel=1)] ) # Maximum compression (Gzip level 9) z_small = zarr.create_array( store="small.zarr", shape=(1000, 1000), chunks=(100, 100), dtype="f4", codecs=[GzipCodec(level=9)] ) # No compression z_raw = zarr.create_array( store="raw.zarr", shape=(1000, 1000), chunks=(100, 100), dtype="f4", codecs=[BytesCodec()] )
Codec selection: Blosc/Zstd (default, balanced) → LZ4 (fastest) → Gzip (smallest). Enable shuffle="shuffle" for numeric data — it reorders bytes for better compression ratios.
pythonimport zarr import numpy as np from zarr.storage import LocalStore, MemoryStore, ZipStore # Local filesystem (default — string paths create LocalStore automatically) z = zarr.open_array("data/array.zarr", mode="w", shape=(1000, 1000), chunks=(100, 100), dtype="f4") # In-memory (not persisted) store = MemoryStore() z_mem = zarr.open_array(store=store, mode="w", shape=(1000, 1000), chunks=(100, 100), dtype="f4") # ZIP file storage store = ZipStore("data.zip", mode="w") z_zip = zarr.open_array(store=store, mode="w", shape=(1000, 1000), chunks=(100, 100), dtype="f4") z_zip[:] = np.random.random((1000, 1000)).astype("f4") store.close() # IMPORTANT: must close ZipStore
python# Cloud storage: Amazon S3 import s3fs s3 = s3fs.S3FileSystem(anon=False) store = s3fs.S3Map(root="my-bucket/path/array.zarr", s3=s3) z = zarr.open_array(store=store, mode="w", shape=(1000, 1000), chunks=(500, 500), dtype="f4") z[:] = np.random.random((1000, 1000)).astype("f4") # Consolidate metadata for faster subsequent reads zarr.consolidate_metadata(store) # Google Cloud Storage import gcsfs gcs = gcsfs.GCSFileSystem(project="my-project") store = gcsfs.GCSMap(root="my-bucket/path/array.zarr", gcs=gcs)
Cloud best practices: consolidate metadata, use 5–100 MB chunks, enable sharding to reduce object count, use Dask for parallel I/O.
pythonimport zarr # Create hierarchical structure (like HDF5 groups) root = zarr.group(store="hierarchy.zarr") # Create sub-groups temperature = root.create_group("temperature") precipitation = root.create_group("precipitation") # Create arrays within groups temp_arr = temperature.create_array( name="t2m", shape=(365, 720, 1440), chunks=(1, 720, 1440), dtype="f4" ) precip_arr = precipitation.create_array( name="prcp", shape=(365, 720, 1440), chunks=(1, 720, 1440), dtype="f4" ) # Access by path arr = root["temperature/t2m"] print(root.tree()) # / # ├── temperature # │ └── t2m (365, 720, 1440) f4 # └── precipitation # └── prcp (365, 720, 1440) f4
pythonimport zarr z = zarr.zeros((1000, 1000), chunks=(100, 100), dtype="f4") # Attach metadata (must be JSON-serializable) z.attrs["description"] = "Temperature data in Kelvin" z.attrs["units"] = "K" z.attrs["processing_version"] = 2.1 print(z.attrs["units"]) # K # Group-level attributes root = zarr.group("data.zarr") root.attrs["project"] = "Climate Analysis" root.attrs["institution"] = "Research Institute"
| Data Shape | Access Pattern | Recommended Chunks | Rationale | |-----------|---------------|-------------------|-----------| | (N, M) 2D | Row-wise | (small, M) | Each chunk spans full row | | (N, M) 2D | Column-wise | (N, small) | Each chunk spans full column | | (N, M) 2D | Random/mixed | (√(1MB/dtype), √(1MB/dtype)) | Balanced ~1MB per chunk | | (T, H, W) time series | Time slice | (1, H, W) | One timestep per chunk | | (T, H, W) time series | Spatial region | (T, small, small) | Full time for region |
1 MB rule: For float32 (4 bytes), 1 MB = 262,144 elements. For float64 (8 bytes), 1 MB = 131,072 elements.
For stores with many arrays (10+), consolidate metadata into a single read:
pythonzarr.consolidate_metadata("data.zarr") root = zarr.open_consolidated("data.zarr") # Single metadata read
Critical for cloud storage (reduces N metadata requests to 1). Caveat: becomes stale if arrays update without re-consolidation.
pythonimport zarr import numpy as np import s3fs # Step 1: Write to S3 with cloud-optimized chunks s3 = s3fs.S3FileSystem() store = s3fs.S3Map(root="s3://my-bucket/experiment.zarr", s3=s3) root = zarr.group(store=store) data_arr = root.create_array( name="measurements", shape=(10000, 10000), chunks=(500, 500), # ~1MB chunks, good for cloud dtype="f4" ) data_arr[:] = np.random.random((10000, 10000)).astype("f4") data_arr.attrs["experiment"] = "batch_42" # Step 2: Consolidate metadata zarr.consolidate_metadata(store) # Step 3: Read from anywhere store_read = s3fs.S3Map(root="s3://my-bucket/experiment.zarr", s3=s3) root_read = zarr.open_consolidated(store_read) subset = root_read["measurements"][0:100, 0:100] print(f"Read subset: {subset.shape}")
pythonimport dask.array as da import zarr import numpy as np # Step 1: Create large Zarr array z = zarr.open("large_data.zarr", mode="w", shape=(100000, 100000), chunks=(1000, 1000), dtype="f4") # (populate with data...) # Step 2: Load as Dask array (lazy — no data loaded yet) dask_arr = da.from_zarr("large_data.zarr") print(f"Dask array: {dask_arr.shape}, {dask_arr.npartitions} partitions") # Step 3: Compute in parallel (out-of-core) col_means = dask_arr.mean(axis=0).compute() print(f"Column means: {col_means.shape}") # Step 4: Write Dask result back to Zarr large_random = da.random.random((100000, 100000), chunks=(1000, 1000)) da.to_zarr(large_random, "output.zarr")
pythonimport xarray as xr import numpy as np import pandas as pd # Step 1: Create labeled dataset ds = xr.Dataset( { "temperature": (["time", "lat", "lon"], np.random.random((365, 180, 360)).astype("f4")), "precipitation": (["time", "lat", "lon"], np.random.random((365, 180, 360)).astype("f4")), }, coords={ "time": pd.date_range("2024-01-01", periods=365), "lat": np.arange(-90, 90, 1.0), "lon": np.arange(-180, 180, 1.0), } ) # Step 2: Save to Zarr ds.to_zarr("climate.zarr") # Step 3: Open with lazy loading ds_loaded = xr.open_zarr("climate.zarr") print(ds_loaded) # Step 4: Label-based selection (only reads needed chunks) subset = ds_loaded.sel(time="2024-06", lat=slice(30, 60)) print(f"June subset: {subset['temperature'].shape}")
pythonimport zarr import numpy as np # HDF5 → Zarr import h5py with h5py.File("data.h5", "r") as h5: z = zarr.array(h5["dataset_name"][:], chunks=(1000, 1000), store="from_hdf5.zarr") # NumPy → Zarr data = np.load("data.npy") z = zarr.array(data, chunks="auto", store="from_numpy.zarr") # Zarr → NetCDF (via Xarray) import xarray as xr ds = xr.open_zarr("data.zarr") ds.to_netcdf("data.nc")
| Parameter | Module | Default | Options | Effect | |-----------|--------|---------|---------|--------| | shape | create_array | Required | Tuple of ints | Array dimensions | | chunks | create_array | Auto | Tuple of ints, "auto" | Chunk shape per dimension | | shards | create_array | None | Tuple of ints | Shard shape (groups chunks) | | dtype | create_array | "f8" | NumPy dtype | Data type | | codecs | create_array | Blosc/Zstd | List of codec objects | Compression pipeline | | mode | open_array | "r" | "r", "r+", "w", "a" | File access mode | | store | All | LocalStore | Store object or path | Storage backend | | cname | BloscCodec | "zstd" | "lz4", "zstd", "gzip", etc. | Compressor algorithm | | clevel | BloscCodec | 5 | 0–9 | Compression level | | shuffle | BloscCodec | "noshuffle" | "shuffle", "bitshuffle" | Byte reordering for compression |
z[:]: For large arrays, use Dask (da.from_zarr) or process in explicit chunks. z[:] loads everything into memory.zarr.consolidate_metadata(store) after creating all arrays. Then open with zarr.open_consolidated(). This reduces N metadata reads to 1 — critical for S3/GCS latency.ZipStore requires store.close() after writing. Forgetting this corrupts the ZIP file.pythonimport zarr z = zarr.open("data.zarr") print(z.info) # Shows: type, shape, chunks, dtype, compressor, storage size print(f"Compressed: {z.nbytes_stored / 1e6:.2f} MB") print(f"Uncompressed: {z.nbytes / 1e6:.2f} MB") print(f"Ratio: {z.nbytes / z.nbytes_stored:.1f}x")
pythonimport zarr import numpy as np # Create extensible array (start with 0 timesteps) z = zarr.open("timeseries.zarr", mode="a", shape=(0, 720, 1440), chunks=(1, 720, 1440), dtype="f4") # Append new timesteps incrementally for day in range(365): new_step = np.random.random((1, 720, 1440)).astype("f4") z.append(new_step, axis=0) print(f"Final shape: {z.shape}") # (365, 720, 1440)
pythonimport dask.array as da # Generate large dataset in parallel data = da.random.random((100000, 100000), chunks=(1000, 1000)) # Write to Zarr (parallel across chunks) da.to_zarr(data, "parallel_output.zarr") # Verify z = da.from_zarr("parallel_output.zarr") print(f"Written: {z.shape}, {z.npartitions} partitions")
| Problem | Cause | Solution | |---------|-------|----------| | Slow read performance | Chunk shape misaligned with access pattern | Profile access pattern; realign chunks (row-access → wide chunks) | | MemoryError on read | Loading entire array or chunk too large | Use Dask da.from_zarr() for out-of-core; reduce chunk size | | High cloud latency | Many small metadata reads | Call zarr.consolidate_metadata(store) then open_consolidated() | | Corrupted ZIP store | Forgot to call store.close() | Always close ZipStore after write; use context manager | | Concurrent write conflicts | Multiple processes writing overlapping chunks | Use ProcessSynchronizer or ensure non-overlapping chunk writes | | Poor compression ratio | No shuffle on numeric data | Add shuffle="shuffle" to BloscCodec | | Stale consolidated metadata | Arrays modified after consolidation | Re-run zarr.consolidate_metadata() after updates | | ModuleNotFoundError: s3fs | Missing cloud storage dependency | pip install s3fs (S3) or pip install gcsfs (GCS) |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | pass→pass | 15,228 | 8,536 | -44% | 1 | 1 | 0% | 2,809 | 7,801 | +178% | 0 | 0 | — |
case-01 | fail→fail | 17,339 | 12,590 | -27% | 1 | 1 | 0% | 3,304 | 8,705 | +163% | 0 | 0 | — |
case-03 | pass→pass | 8,494 | 4,042 | -52% | 1 | 1 | 0% | 1,265 | 6,803 | +438% | 0 | 0 | — |
case-04 | fail→pass | 7,256 | 5,232 | -28% | 1 | 1 | 0% | 1,411 | 6,928 | +391% | 0 | 0 | — |
case-05 | pass→pass | 10,150 | 5,829 | -43% | 1 | 1 | 0% | 1,812 | 6,924 | +282% | 0 | 0 | — |
case-06 | pass→pass | 2,928 | 3,878 | +32% | 1 | 1 | 0% | 574 | 6,666 | +1061% | 0 | 0 | — |
case-07 | pass→pass | 4,218 | 17,312 | +310% | 1 | 1 | 0% | 811 | 6,760 | +734% | 0 | 0 | — |
case-08 | fail→pass | 16,208 | 12,386 | -24% | 1 | 1 | 0% | 3,184 | 8,273 | +160% | 0 | 0 | — |
case-09 | pass→pass | 7,757 | 6,425 | -17% | 1 | 1 | 0% | 1,501 | 7,289 | +386% | 0 | 0 | — |
case-10 | pass→pass | 8,301 | 7,908 | -5% | 1 | 1 | 0% | 1,815 | 7,652 | +322% | 0 | 0 | — |
case-11 | pass→pass | 12,525 | 7,496 | -40% | 1 | 1 | 0% | 2,356 | 7,610 | +223% | 0 | 0 | — |
case-12 | pass→pass | 7,366 | 4,815 | -35% | 1 | 1 | 0% | 1,224 | 6,808 | +456% | 0 | 0 | — |
case-13 | fail→pass | 27,114 | 3,898 | -86% | 1 | 1 | 0% | 1,222 | 6,664 | +445% | 0 | 0 | — |
case-14 | pass→pass | 3,723 | 3,493 | -6% | 1 | 1 | 0% | 749 | 6,700 | +795% | 0 | 0 | — |
case-15 | pass→pass | 3,250 | 3,011 | -7% | 1 | 1 | 0% | 567 | 6,559 | +1057% | 0 | 0 | — |
case-16 | pass→pass | 9,649 | 7,192 | -25% | 1 | 1 | 0% | 2,042 | 7,596 | +272% | 0 | 0 | — |
case-17 | pass→pass | 9,993 | 3,722 | -63% | 1 | 1 | 0% | 1,370 | 6,690 | +388% | 0 | 0 | — |
case-18 | pass→pass | 4,333 | 2,506 | -42% | 1 | 1 | 0% | 849 | 6,502 | +666% | 0 | 0 | — |
case-19 | pass→pass | 12,672 | 9,132 | -28% | 1 | 1 | 0% | 2,459 | 7,857 | +220% | 0 | 0 | — |
case-20 | pass→pass | 15,074 | 6,703 | -56% | 1 | 1 | 0% | 2,581 | 7,293 | +183% | 0 | 0 | — |
case-21 | pass→pass | 14,587 | 13,195 | -10% | 1 | 1 | 0% | 2,498 | 8,351 | +234% | 0 | 0 | — |
case-22 | pass→pass | 3,540 | 2,278 | -36% | 1 | 1 | 0% | 660 | 6,452 | +878% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +14 percentage points is the difference between those two pass rates over the 21 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.