|
| 1 | +"""Columns that files from older AnnData writers store but do not list. |
| 2 | +
|
| 3 | +A dataframe in the layout anndata wrote before 0.8 names its columns in a |
| 4 | +``column-order`` attribute, and the current anndata reads exactly the columns |
| 5 | +listed there. SeuratDisk's ``Convert()`` lists only some of them, so reading |
| 6 | +such a file through anndata silently drops the rest — a cell field the web |
| 7 | +app's own reader shows would be missing from ``cellucid serve``. This restores |
| 8 | +them, read from the file itself, so both readers show the same fields: |
| 9 | +
|
| 10 | +- a column stored with its categories under ``__categories/<column>`` is a |
| 11 | + categorical (``ordered`` as the categories record it, codes of ``-1`` are |
| 12 | + missing); |
| 13 | +- any other column is its stored values, text decoded as text. |
| 14 | +""" |
| 15 | + |
| 16 | +from __future__ import annotations |
| 17 | + |
| 18 | +from typing import Any |
| 19 | + |
| 20 | +import numpy as np |
| 21 | +import pandas as pd |
| 22 | + |
| 23 | +__all__ = ["restore_unlisted_columns"] |
| 24 | + |
| 25 | + |
| 26 | +def _attribute_strings(value: Any) -> list[str]: |
| 27 | + if value is None: |
| 28 | + return [] |
| 29 | + return [item.decode() if isinstance(item, bytes) else str(item) for item in np.ravel(value)] |
| 30 | + |
| 31 | + |
| 32 | +def _read_values(array: Any) -> np.ndarray: |
| 33 | + """An array's values, with text as ``str`` whatever the store encodes.""" |
| 34 | + try: |
| 35 | + import h5py |
| 36 | + except ImportError: # pragma: no cover - h5py is an anndata dependency |
| 37 | + h5py = None |
| 38 | + if h5py is not None and isinstance(array, h5py.Dataset): |
| 39 | + if h5py.check_string_dtype(array.dtype) is not None: |
| 40 | + return np.asarray(array.asstr()[...], dtype=object) |
| 41 | + return np.asarray(array[...]) |
| 42 | + values = np.asarray(array[...]) |
| 43 | + if values.dtype.kind in ("S", "O"): |
| 44 | + return np.array( |
| 45 | + [item.decode() if isinstance(item, bytes) else item for item in values], |
| 46 | + dtype=object, |
| 47 | + ) |
| 48 | + return values |
| 49 | + |
| 50 | + |
| 51 | +def restore_unlisted_columns(adata: Any, store: Any) -> list[str]: |
| 52 | + """Add every ``obs``/``var`` column the store holds but anndata dropped. |
| 53 | +
|
| 54 | + Parameters |
| 55 | + ---------- |
| 56 | + adata |
| 57 | + The AnnData object read from ``store``. |
| 58 | + store |
| 59 | + The open file (``h5py.File``) or store (``zarr.Group``) it was read |
| 60 | + from. |
| 61 | +
|
| 62 | + Returns |
| 63 | + ------- |
| 64 | + list of str |
| 65 | + ``"<axis>/<column>"`` for every column restored. |
| 66 | + """ |
| 67 | + restored: list[str] = [] |
| 68 | + for axis in ("obs", "var"): |
| 69 | + if axis not in store: |
| 70 | + continue |
| 71 | + group = store[axis] |
| 72 | + if not hasattr(group, "keys"): |
| 73 | + continue # anndata 0.6 compound dataframes list every column |
| 74 | + frame: pd.DataFrame = getattr(adata, axis) |
| 75 | + index_key = _attribute_strings(group.attrs.get("_index")) |
| 76 | + stored = [ |
| 77 | + key |
| 78 | + for key in group |
| 79 | + if key != "__categories" and key not in index_key and key not in frame.columns |
| 80 | + ] |
| 81 | + if not stored: |
| 82 | + continue |
| 83 | + categories = group.get("__categories") |
| 84 | + for key in stored: |
| 85 | + if not hasattr(group[key], "shape"): |
| 86 | + continue # an encoded element (a group), which anndata reads itself |
| 87 | + values = _read_values(group[key]) |
| 88 | + if values.ndim != 1 or values.shape[0] != frame.shape[0]: |
| 89 | + continue # not a per-row column |
| 90 | + if categories is not None and key in categories: |
| 91 | + labels = _read_values(categories[key]) |
| 92 | + ordered = bool(np.ravel(categories[key].attrs.get("ordered", False))[0]) |
| 93 | + frame[key] = pd.Categorical.from_codes(values.astype(np.int64), labels, ordered=ordered) |
| 94 | + else: |
| 95 | + frame[key] = values |
| 96 | + restored.append(f"{axis}/{key}") |
| 97 | + return restored |
0 commit comments