Source code for ecotyper.io
"""Readers for EcoTyper's user-facing inputs and conversion to AnnData.
The readers reproduce how the R scripts read the same files (``read.delim``
for bulk/Visium matrices and annotations, ``data.table::fread`` for scRNA-seq
matrices), including R's number parsers, so that a Python run starts from
bit-identical values.
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pandas as pd
from .rcompat.frame import make_names
from .rcompat.io import read_delim
from .rcompat.parse import read_numeric_table
[docs]
def read_bulk_matrix(path) -> pd.DataFrame:
"""``read.delim(path); rownames(data) = data[,1]; data = data[,-1]``.
Returns a genes x samples DataFrame; sample names go through ``make.names``
as in R (``check.names = TRUE``)."""
header, rows, X = read_numeric_table(path, mode="r")
names = make_names(header, unique=True)
cols = names[1:] if len(names) == X.shape[1] + 1 else names[-X.shape[1]:]
return pd.DataFrame(X, index=pd.Index(rows, dtype=object), columns=cols)
[docs]
def read_scrna_matrix(path) -> pd.DataFrame:
"""``fread(path, data.table = F)``, then drop NA and duplicated gene names
(``read_scRNA`` in lib/scRNA.R). Column names are kept verbatim."""
header, rows, X = read_numeric_table(path, mode="fread")
cols = header[1:] if len(header) == X.shape[1] + 1 else header[-X.shape[1]:]
keep = []
seen = set()
for i, g in enumerate(rows):
if g in ("NA", "") or g in seen:
continue
seen.add(g)
keep.append(i)
keep = np.asarray(keep, dtype=np.int64)
return pd.DataFrame(X[keep], index=pd.Index([rows[i] for i in keep], dtype=object), columns=cols)
[docs]
def read_annotation(path) -> pd.DataFrame | None:
"""``read.delim(annotation)``; ``None`` if ``path`` is None/"NULL"/missing."""
if path is None or str(path) == "NULL" or not Path(path).exists():
return None
return read_delim(path)
[docs]
def read_fractions(path) -> pd.DataFrame:
"""CIBERSORTx fractions (``read_fractions`` in lib/misc.R): columns up to
(excluding) ``P.value`` are kept."""
df = read_delim(path)
names = make_names(list(df.columns))
if "P.value" in names:
df = df.iloc[:, : names.index("P.value")]
return df
[docs]
def to_anndata(expr: pd.DataFrame, annotation: pd.DataFrame | None = None):
"""Genes x samples DataFrame (+ annotation with an ``ID`` column) → AnnData
with samples/cells as ``obs`` and genes as ``var``."""
import anndata as ad
obs = pd.DataFrame(index=pd.Index([str(c) for c in expr.columns], dtype=object))
if annotation is not None:
ann = annotation.copy()
ann.index = pd.Index([str(i) for i in ann["ID"]], dtype=object)
ann = ann[~ann.index.duplicated()]
obs = obs.join(ann.drop(columns=["ID"]), how="left")
obs.insert(0, "ID", obs.index.to_numpy())
var = pd.DataFrame(index=pd.Index([str(g) for g in expr.index], dtype=object))
return ad.AnnData(X=np.ascontiguousarray(expr.to_numpy(dtype=np.float64).T), obs=obs, var=var)
[docs]
def from_anndata(adata, layer: str | None = None) -> pd.DataFrame:
"""AnnData → genes x samples DataFrame (dense)."""
X = adata.layers[layer] if layer is not None else adata.X
if hasattr(X, "toarray"):
X = X.toarray()
return pd.DataFrame(np.asarray(X, dtype=np.float64).T, index=pd.Index(list(adata.var_names), dtype=object),
columns=list(adata.obs_names))
[docs]
def annotation_from_anndata(adata) -> pd.DataFrame:
obs = adata.obs.copy()
if "ID" not in obs.columns:
obs.insert(0, "ID", list(adata.obs_names))
cols = ["ID"] + [c for c in obs.columns if c != "ID"]
out = obs[cols].reset_index(drop=True)
for c in out.columns:
if isinstance(out[c].dtype, pd.CategoricalDtype):
out[c] = out[c].astype(object)
return out