Uh oh!
There was an error while loading. Please reload this page.
Metadata compression - #33
Merged
Merged
Conversation
`numeric_only_compression` exists because Blosc2 crashes on variable-length-string HDF5 datasets, so every string column in obs/var is written uncompressed. That left the package with no size strategy for metadata at all. Investigating what to do about it turned up a plain bug rather than a codec question. `anndata.AnnData.write_h5ad` converts string columns to categoricals before writing (its own `convert_strings_to_categoricals=True`). `VCSCAnnData` writes field-by-field rather than delegating -- deliberately, for the reasons in `_write_group`'s comment -- and so never did. A low-cardinality annotation (cell type, sample ID, batch) therefore landed as one variable-length string per row, uncompressed and unrecoverable. On 200k cells x 2k genes with three such columns: before (plain strings) file 41.72 MB after (categorical) file 13.48 MB Adds the same parameter with the same name and default to `write_h5ad` and `write_zarr`. anndata only converts columns with fewer categories than rows, so a per-row-unique column (barcodes, gene symbols) is left alone and this can never make a column larger. The codec question was answered too, by writing a 20k-element vlen string dataset under each filter, one subprocess each, against h5py 3.16.0 / HDF5 2.0.0 / hdf5plugin 7.0.0: none, gzip and lzf all fine; blosc2 still dies with SIGFPE. So the workaround stays, and it's specifically Blosc2 rather than HDF5 filters in general. gzip/lzf would be safe but aren't worth adopting -- vlen payloads live in HDF5's global heap where per-dataset compression doesn't reach them well, and the categorical encoding above is worth far more than any string codec could be. Both findings are recorded in `_compression`'s module docstring. No upstream issue filed yet: reproducing this outside the h5py/hdf5plugin combination in use here needs a check against a current hdf5plugin build first. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
aarmey
approved these changes
Sep 4, 2026
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Categorical-encode obs/var strings on write, as anndata's writers do
Background
numeric_only_compression(src/vsparse/_compression.py) exists because Blosc2 crashes on variable-length-string HDF5 datasets, so every string column inobs/varis written uncompressed. That left the package with no size strategy for metadata at all, and metadata is a large fraction of a written file.This started as an investigation into whether a different codec was safe for strings, but it turned up a plain bug instead.
The bug
anndata.AnnData.write_h5adconverts string columns to categoricals before writing — that's its ownconvert_strings_to_categoricals=Trueparameter.VCSCAnnDatawrites field-by-field rather than delegating to anndata's writer and so never did this.A low-cardinality annotation — cell type, sample ID, batch — therefore landed as one variable-length string per row uncompressed, and unrecoverable by any later step. Measured on 200k cells × 2k genes with three such columns:
3.1× off the whole file, from a parameter the parent class has had all along.
What this changes
write_h5adandwrite_zarrgainconvert_strings_to_categoricals: bool = True. Note it mutatesobs/varin place, exactly as anndata's writers do. PassFalseto opt out.The codec question, answered
A 20k-element vlen string dataset written under each available filter, one subprocess per filter, against h5py 3.16.0 / HDF5 2.0.0 / hdf5plugin 7.0.0:
nonegziplzfblosc2SIGFPEbefore returning