Repository navigation
0.10.0
Adds Parse Biosciences split-pipe output to adata convert, read in two
streaming passes like the MTX folders of 0.8.0. Features are now named by
gene id by default, for every format: ids are unique where symbols repeat,
so a gene-level matrix no longer needs --make-unique (a probe-level Flex
matrix still does).
Added
-
adata convertreads Parse Biosciences split-pipe output
(#30). A run folder, a
sample folder or aDGE_filtered/DGE_unfilteredfolder is recognised
whatever its name:count_matrix.mtx(cells x genes) becomes X through the streaming MTX
reader; integer counts stay integers;cell_metadata.csvbecomes obs, indexed bybc_wells, each column
typed as pandas reads it and stored as anndata writes it;all_genes.csvbecomes var, withgene_symbolsandgenome;--sample NAMEreads one sample,--all-sampleswrites one output per
sample, and--unfilteredreads every barcode;--genome Gkeeps G's genes and the cells split-pipe called as G.
Checked against Parse's own scanpy recipe in CI, and on a real split-pipe
v1.6.1 run: X, obs and var all match, for 29,588 cells and for the
431,061 barcodes of the unfiltered folder, at a peak of about 200 MB. -
A streaming CSV table reader (#55),
formats/table.py: two passes over the file, columns typed as
pd.read_csvtypes them, theobsdataframe written a block of rows at
a time. It reads Parse's cell metadata, and is there for #25's delimited
tables.
Changed
- Features are named by gene id by default
(#54). This changes what
adata convertwrites from 10x HDF5, MTX folders, CellBender and Parse:
var names are the gene ids, and the symbols are invar["gene_symbols"],
the layoutscanpy.read_10x_mtx(var_names="gene_ids")gives. Symbols
repeat (Cell Ranger's GRCh38-2020-A reference repeats 10), so naming by
them needed--make-unique.--var-names gene_symbolsgives the old
names, as scanpy and CellBender's loaders give them. A probe-level (Flex)
matrix still repeats gene ids, and still warns.
Fixed
- Categories are ordered as anndata orders them
(#55). anndata sorts the
categories it makes from strings naturally (A2beforeA10);convert
sorted them plainly, so a categorical var column holding numbers in its
values came out in a different order from scanpy's.
Benchmark vs anndata/scanpy
Results — 0.10.0
| Tier | ci, 50,000 obs x 20,000 var, 5% dense CSR |
| Var-heavy shape | 2,000 obs x 36,601 var |
| Page cache | dropped between runs |
| Address-space ceiling | 12.9 GB per process |
| Repeats | 3 (fastest shown) |
| Platform | Linux x86_64, Python 3.12.3 |
| Versions | adata-cli 0.10.0, anndata 0.13.4, scanpy 1.12.4 |
| Generated | 2026-10-19T10:48:49Z |
startup
What does each contender cost before it does any work?
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 0.59 s | 69.9 MB | - | no change |
| anndata | 1.55 s | 133.5 MB | - | no change |
| scanpy | 3.26 s | 260.0 MB | - | no change |
concat-inner
Concatenate two stores on the obs axis, inner join.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 12.13 s | 172.9 MB | 583.3 MB | no change |
| anndata (concat_on_disk) | 6.07 s | 448.3 MB | 831.9 MB | no change |
| anndata (in memory) | 12.14 s | 1.8 GB | 580.3 MB | no change |
| scanpy | n/a | n/a | n/a | - |
| scanpy has no concat of its own; it re-exports anndata's |
concat-merge-same
Concatenate carrying var columns forward -- the REQ-71798 case.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 1.75 s | 196.4 MB | 45.7 MB | no change |
| anndata (concat_on_disk) | 2.27 s | 247.3 MB | 63.2 MB | no change |
| anndata (in memory) | 2.66 s | 289.0 MB | 44.6 MB | no change |
subset-query
Keep the obs rows matching a predicate on an existing column.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 4.12 s | 227.8 MB | 147.1 MB | no change |
| anndata (backed) | 4.93 s | 653.9 MB | 146.6 MB | no change |
| anndata (in memory) | 5.20 s | 984.0 MB | 146.6 MB | no change |
| scanpy (filter_cells) | 8.82 s | 1.6 GB | 291.2 MB | no change |
h5ad-to-zarr
Convert a store to Zarr.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 6.53 s | 125.2 MB | 151.6 MB in 4717 files | no change |
| anndata (in memory) | 5.91 s | 815.4 MB | 136.8 MB in 55 files | no change |
| scanpy | n/a | n/a | n/a | - |
| no streaming converter; scanpy defers to anndata |
inspect
Report what is in the store, without reading the matrix.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 0.59 s | 72.4 MB | - | no change |
| anndata (read_elem on obs) | 1.71 s | 160.5 MB | - | no change |
| anndata (full load) | 3.22 s | 584.3 MB | - | no change |
export-obs
Write the obs table out as CSV.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 0.76 s | 85.7 MB | 1.8 MB | no change |
| anndata (read_elem on obs) | 1.72 s | 160.8 MB | 1.8 MB | no change |
| anndata (full load) | 3.20 s | 584.4 MB | 1.8 MB | no change |
split-by-sample
Write one store per distinct value of an obs column.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 6.57 s | 199.7 MB | 302.6 MB in 9 files | no change |
| anndata (hand-written loop) | 7.74 s | 684.2 MB | 301.7 MB in 8 files | no change |
ls
List everything in the store.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 0.60 s | 71.6 MB | - | no change |
| h5py (visit) | 1.62 s | 136.4 MB | - | no change |
| h5ls -r | n/a | n/a | n/a | - |
| h5ls is not installed |
create
Write an empty store with a given obs/var shape.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 0.65 s | 85.0 MB | 3.4 MB | no change |
| anndata | 8.63 s | 228.7 MB | 49.2 MB | no change |
export-array
Write a dense element out as .npy.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 0.63 s | 83.6 MB | 10.0 MB | no change |
| anndata (read_elem) | 1.64 s | 154.6 MB | 10.0 MB | no change |
| anndata (full load) | 3.13 s | 584.3 MB | 10.0 MB | no change |
export-sparse
Write X out as Matrix Market text.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 10.88 s | 257.1 MB | 661.3 MB | no change |
| anndata (sparse_dataset) | 7.05 s | 968.2 MB | 661.6 MB | no change |
| anndata (full load) | 7.12 s | 773.7 MB | 661.6 MB | no change |
import-dataframe
Replace obs from a CSV.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 1.22 s | 114.9 MB | 296.0 MB | no change |
| anndata | 6.92 s | 600.5 MB | 291.2 MB | no change |
import-10x-mtx
Read a gzipped Cell Ranger 3 MTX folder of the same counts into an h5ad. adata-cli streams it in two passes and keeps integer counts; scanpy loads it and converts them to float32.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 15.74 s | 345.3 MB | 279.2 MB | no change |
| scanpy | 13.65 s | 3.3 GB | 284.6 MB | no change |
concat-outer
Concatenate two stores keeping the union of variables.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 12.12 s | 189.1 MB | 583.3 MB | no change |
| anndata (concat_on_disk) | 6.13 s | 450.4 MB | 831.9 MB | no change |
| anndata (in memory) | 12.10 s | 1.8 GB | 580.3 MB | no change |
recast-narrow
Narrow X from float64 values with int64 indices to float32 and int32, as issue #13 asked. X is uncompressed, as write_h5ad leaves it by default, so the output keeps that and the baseline writes it the same way.
| Contender | Wall time | Peak RSS | Input → output | vs previous |
|---|---|---|---|---|
| adata-cli | 2.35 s | 120.3 MB | 815.6 MB → 415.4 MB (-49%) | no change |
| anndata (in memory) | 3.92 s | 1.8 GB | 815.6 MB → 417.3 MB (-49%) | no change |
| scanpy | n/a | n/a | n/a | - |
| no dtype rewrite; scanpy defers to anndata |
recast-narrow-lzf
The same narrowing on an lzf-compressed X. Integer counts in float64 are mostly zero bytes, which lzf already removes, so expect a small saving on disk; the halving is in memory, once the matrix is loaded.
| Contender | Wall time | Peak RSS | Input → output | vs previous |
|---|---|---|---|---|
| adata-cli | 7.80 s | 127.7 MB | 302.5 MB → 291.1 MB (-4%) | no change |
| anndata (in memory) | 8.04 s | 1.8 GB | 302.5 MB → 291.2 MB (-4%) | no change |
convert-layout
Transpose X from CSR to CSC. The output should be the size of the input: 0.7.0 fixed a --layout that wrote it three times larger.
| Contender | Wall time | Peak RSS | Input → output | vs previous |
|---|---|---|---|---|
| adata-cli (streaming) | 14.67 s | 385.3 MB | 291.2 MB → 297.0 MB (+2%) | no change |
| adata-cli (--in-memory) | 9.36 s | 2.3 GB | 291.2 MB → 297.0 MB (+2%) | no change |
| anndata (in memory) | 7.14 s | 984.6 MB | 291.2 MB → 297.0 MB (+2%) | no change |
rss-vs-size
Does peak memory track input size, at a fixed --chunk? This is the README's claim, stated as a measurement.
| Contender | Wall time | Peak RSS | Output | vs previous |
|---|---|---|---|---|
| adata-cli | 12.10 s | 181.6 MB | 583.3 MB | no change |
| anndata (in memory) | 12.18 s | 1.8 GB | 580.3 MB | no change |
Rows marked n/a are operations the baseline does not offer; that is a result, not a gap in the measurement. Where a baseline is faster, the row stands as it is -- the trade this tool makes is memory for time, and hiding the cost would make the table useless.
Output size is the file as the filesystem reports it, which for HDF5 includes allocation slack. On a small tier that slack can be several times the stored bytes and the column says more about the writer's allocation strategy than about the data; at ci and above it is noise. Zarr stores report a file count alongside, because a directory of small chunks measures much larger than it holds. Where a case exists to change a file's size, the column reads input → output, with the change.