Skip to content

feat: make obs/var backend-agnostic with a DataFrameLike contract - #2516

Open
srivarra wants to merge 18 commits into
scverse:mainfrom
srivarra:dataframe-backends
Open

feat: make obs/var backend-agnostic with a DataFrameLike contract#2516
srivarra wants to merge 18 commits into
scverse:mainfrom
srivarra:dataframe-backends

Conversation

@srivarra

@srivarra srivarra commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Using narwhals as the conversion layer between DataFrame implementations, and makes Dataset2D "dataframe-like" in the eyes of narwhals.

  • Release note not necessary because: dunno about this one

I built a thin Narwhals translation layer between AnnData.obs/AnnData.var and the DataFrame backends Narwhals supports. .obs and .var now accept native DataFrames instead of requiring pandas.

The DataFrameLike contract builds on #2328. Indexed frames retain their native index, while index-less frames store the index in an obs_names or var_names column. DataFrame ops like slicing, copying, transposing, renaming categories, and updating names use this contract.

I also added AnnData.obs_as(...) and AnnData.var_as(...) to convert annotations to pandas, Polars, PyArrow, Modin, or cuDF. Concatenation preserves Polars or PyArrow when all inputs use the same backend, including annotations combined through the merge strategies. At write time, frames without a native writer convert to pandas so the existing HDF5 and Zarr writers continue to work.

A small Narwhals plugin makes Dataset2D compatible with Narwhals, allowing conversion to other DataFrame formats. This conversion materializes the Dataset2D as an in-memory pandas DataFrame during conversion. Lazy-to-lazy conversion should probably come in a separate PR, for example, converting Dataset2D to a Dask DataFrame without loading it in memory. Right now, converting Dataset2D to an eager backend such as Polars still materializes the frame.

I used AI tools to help with the implementation.

Remaining TODOS:

  • Play around to see if it works well with cuDF
  • Fix poor test coverage (mostly)
  • Dataset2D -> Narwhals LazyFrame somehow? Deferred for later

srivarra and others added 2 commits June 26, 2026 10:32
Using narwhals as the conversion layer between DataFrame implementations, makes Dataset2D "dataframe-like" in the eyes of narwhals.

Signed-off-by: Sricharan Reddy Varra <sricharanvarra@gmail.com>
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.25243% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.70%. Comparing base (8660a9d) to head (5224d54).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/anndata/_core/_dataframe_backend.py 98.84% 3 Missing ⚠️
src/anndata/_core/storage.py 81.81% 2 Missing ⚠️
src/anndata/acc/__init__.py 81.81% 2 Missing ⚠️
src/anndata/_core/aligned_mapping.py 94.11% 1 Missing ⚠️
src/anndata/_core/anndata.py 97.91% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2516      +/-   ##
==========================================
- Coverage   87.83%   86.70%   -1.13%     
==========================================
  Files          49       50       +1     
  Lines        7799     8183     +384     
==========================================
+ Hits         6850     7095     +245     
- Misses        949     1088     +139     
Files with missing lines Coverage Δ
src/anndata/_core/aligned_df.py 100.00% <100.00%> (+3.27%) ⬆️
src/anndata/_core/index.py 95.23% <100.00%> (ø)
src/anndata/_core/merge.py 84.88% <100.00%> (-8.92%) ⬇️
src/anndata/_core/raw.py 84.41% <100.00%> (+3.49%) ⬆️
src/anndata/_core/views.py 86.69% <100.00%> (-4.82%) ⬇️
src/anndata/_core/xarray.py 97.60% <100.00%> (+0.55%) ⬆️
src/anndata/_io/specs/lazy_methods.py 96.31% <100.00%> (ø)
src/anndata/_io/specs/registry.py 95.15% <100.00%> (+0.27%) ⬆️
src/anndata/utils.py 86.49% <ø> (-0.85%) ⬇️
src/anndata/_core/aligned_mapping.py 95.96% <94.11%> (+1.37%) ⬆️
... and 4 more

... and 4 files with indirect coverage changes

srivarra added 16 commits June 26, 2026 11:06
Signed-off-by: Sricharan Reddy Varra <sricharanvarra@gmail.com>
Signed-off-by: Sricharan Reddy Varra <sricharanvarra@gmail.com>
Preserve eager dataframe backends while representing axis identity through an index or canonical obs_names and var_names columns.

Signed-off-by: Sricharan Reddy Varra <sricharanvarra@gmail.com>
Normalize only the index name when axis values already match. Keep PyArrow
  helper coverage independent of Polars.

Signed-off-by: Sricharan Reddy Varra <sricharanvarra@gmail.com>
Signed-off-by: Sricharan Reddy Varra <sricharanvarra@gmail.com>
Signed-off-by: Sricharan Reddy Varra <sricharanvarra@gmail.com>
n

Signed-off-by: Sricharan Reddy Varra <sricharanvarra@gmail.com>
Signed-off-by: Sricharan Reddy Varra <sricharanvarra@gmail.com>
`nw.DataFrame` was rejected as `obs`/`var` while `nw.LazyFrame` was
accepted, and the rejection contained nothing: both satisfy
`DataFrameLike` structurally, so they slipped into `obsm`/`uns` and were
stored as the wrapper, which IO then could not write.

`unwrap_narwhals` peels the wrapper at the ingest boundaries. For
`obs`/`var` it is a `_gen_dataframe` registration that unwraps and
dispatches again, so a pandas-backed narwhals frame lands on the pandas
handler and keeps its index instead of being treated as index-less.

Move the `narwhals.plugins` entry point to `anndata._core.xarray`, next
to the `Dataset2D` it describes, and drop the banner comments and other
stylistic drift from `_dataframe_backend.py` so the module reads like
the rest of the codebase.
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.

DataFrame API for obs and var keys via runtime-checkable Protocol

1 participant