Skip to content

Metadata compression - #33

Merged
aarmey merged 6 commits into
mainfrom
investigate/metadata-compression
Sep 4, 2026
Merged

Metadata compression#33
aarmey merged 6 commits into
mainfrom
investigate/metadata-compression

Conversation

@fishidaho

Copy link
Copy Markdown
Contributor

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 in obs/var is 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_h5ad converts string columns to categoricals before writing — that's its own convert_strings_to_categoricals=True parameter. VCSCAnnData writes 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:

before (plain strings)   file  41.72 MB
after  (categorical)     file  13.48 MB

3.1× off the whole file, from a parameter the parent class has had all along.

What this changes

write_h5ad and write_zarr gain convert_strings_to_categoricals: bool = True. Note it mutates obs/var in place, exactly as anndata's writers do. Pass False to 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:

filter result
none fine
gzip fine
lzf fine
blosc2 dies with SIGFPE before returning

fishidaho and others added 6 commits September 2, 2026 17:36
`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>
Clean merge. #23 touched _anndata_class only in _subset_2d's comment;
this branch touches _write_group/write_h5ad/write_zarr, so the two don't
overlap. Nothing in #22/#23 addresses obs/var metadata encoding, and the
categorical conversion is unaffected by the indexing changes.
@fishidaho
fishidaho requested a review from aarmey September 4, 2026 00:27
@fishidaho fishidaho self-assigned this Sep 4, 2026
@fishidaho
fishidaho marked this pull request as ready for review September 4, 2026 00:27
@aarmey
aarmey merged commit 6354c55 into main Sep 4, 2026
3 checks passed
@aarmey
aarmey deleted the investigate/metadata-compression branch September 4, 2026 01:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants