scCODA / tascCODA¶
细胞组成的主模型。scCODA 看大类,tascCODA 看亚型(树:大类→亚型),对照为 CTRL。每个点是一例的占比。模型处理「占比加总为 100%」。看可信区间是否离开无变化;树图上先看中性粒、Pre-Neut、GMP。
In [1]:
import os
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
os.environ.setdefault("JAX_PLATFORMS", "cpu")
import anndata as ad
import jax.numpy as jnp
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from scipy.stats import mannwhitneyu
from statsmodels.stats.multitest import multipletests
from session_info2 import session_info
import numpyro
import pertpy as pt
from pertpy.tools._coda._base_coda import CompositionalModel2
from pertpy.tools._coda._base_coda import MCMC as _CodaMCMC
numpyro.enable_validation(False)
# pertpy writes n_total into AnnData.obsm as (n, 1); zeros become 0.5.
if not getattr(CompositionalModel2, "_n_total_squeezed", False):
_orig_run_mcmc = CompositionalModel2._CompositionalModel2__run_mcmc
def _run_mcmc_squeeze_n_total(
self, sample_adata, kernel, rng_key, copy=False, *args, **kwargs
):
_mcmc_run = _CodaMCMC.run
def _run(
mcmc_self,
rng_key,
counts,
covariates,
n_total,
ref_index,
sample_adata,
extra_fields=(),
):
# obsm forces n_total (n,1); 0.5 pseudocounts are not integers.
# numpyro DirichletMultinomial then rejects total_count.
counts_np = np.asarray(counts, dtype=np.float64)
counts_np = np.where(np.isclose(counts_np, 0.5), 1.0, counts_np)
counts_np = np.rint(counts_np).astype(np.int32)
n_total = jnp.asarray(counts_np.sum(axis=1).astype(np.int32))
counts = jnp.asarray(counts_np)
ref = np.asarray(ref_index).reshape(-1)
ref_index = int(ref[0]) if ref.size == 1 else jnp.asarray(ref)
return _mcmc_run(
mcmc_self,
rng_key,
counts,
covariates,
n_total,
ref_index,
sample_adata,
extra_fields=extra_fields,
)
_CodaMCMC.run = _run
try:
return _orig_run_mcmc(
self, sample_adata, kernel, rng_key, copy, *args, **kwargs
)
finally:
_CodaMCMC.run = _mcmc_run
CompositionalModel2._CompositionalModel2__run_mcmc = _run_mcmc_squeeze_n_total
CompositionalModel2._n_total_squeezed = True
# Same order as codes/plot_colors.R (annot_main_col / annot_sub_col).
ANNOT_MAIN_ORDER = [
"HSPC", "B", "Plasma", "T", "NK",
"Mono/DC", "pDC", "Pre-Neut", "Neut", "Ery",
]
ANNOT_SUB_ORDER = [
"HSC", "MEP", "GMP", "MDP",
"Pro-B", "Large Pre-B", "Small Pre-B", "Trans B", "Naive B", "Memory B",
"Plasma", "pDC",
"CD4 Tnaive", "CD4 Tcm", "CD4 Treg",
"CD8 Tnaive", "CD8 Tem", "CD8 CTL", "CD8 NKT", "MAIT", "gdT",
"CD56bright NK", "CD56dim NK",
"CD14 Mono", "CD16 Mono", "cDC2",
"Promyelocyte", "MMP8 Pre-Neut", "MMP9 Pre-Neut",
"S100A12 Neut", "SLC8A1 Neut", "ISG15 Neut",
"Early Ery", "Late Ery",
]
SUB_TO_MAIN = {
"HSC": "HSPC", "MEP": "HSPC", "GMP": "HSPC", "MDP": "HSPC",
"Pro-B": "B", "Large Pre-B": "B", "Small Pre-B": "B",
"Trans B": "B", "Naive B": "B", "Memory B": "B",
"Plasma": "Plasma",
"CD4 Tnaive": "T", "CD4 Tcm": "T", "CD4 Treg": "T",
"CD8 Tnaive": "T", "CD8 Tem": "T", "CD8 CTL": "T", "CD8 NKT": "T",
"MAIT": "T", "gdT": "T",
"CD56bright NK": "NK", "CD56dim NK": "NK",
"CD14 Mono": "Mono/DC", "CD16 Mono": "Mono/DC", "cDC2": "Mono/DC",
"pDC": "pDC",
"Promyelocyte": "Pre-Neut", "MMP8 Pre-Neut": "Pre-Neut", "MMP9 Pre-Neut": "Pre-Neut",
"S100A12 Neut": "Neut", "SLC8A1 Neut": "Neut", "ISG15 Neut": "Neut",
"Early Ery": "Ery", "Late Ery": "Ery",
}
def _node_label(n):
return (n.name or "").removeprefix("annot_main_")
def _node_rank(n):
name = _node_label(n)
main = name if name in ANNOT_MAIN_ORDER else SUB_TO_MAIN.get(name)
main_i = ANNOT_MAIN_ORDER.index(main) if main in ANNOT_MAIN_ORDER else 999
sub_i = ANNOT_SUB_ORDER.index(name) if name in ANNOT_SUB_ORDER else -1
return (main_i, sub_i, name)
def sort_coda_tree(tree):
"""Reorder children to match plot_colors.R lineage order (HSPC at top)."""
for n in tree.traverse("postorder"):
if n.children:
n.children.sort(key=_node_rank)
return tree
def plot_coda_tree(
tree,
ax=None,
node_value=None,
figsize=(8, 9),
pos_color="#F07A7A",
neg_color="#7EB8E8",
):
"""Draw an ete4 tree with matplotlib. Avoids ete4.treeview / Qt (hangs Jupyter)."""
if ax is None:
_, ax = plt.subplots(figsize=figsize)
sort_coda_tree(tree)
leaves = [n for n in tree.traverse("preorder") if n.is_leaf]
nleaf = len(leaves)
ymap = {id(leaf): float(nleaf - 1 - i) for i, leaf in enumerate(leaves)}
def y_of(node):
k = id(node)
if k not in ymap:
ymap[k] = float(np.mean([y_of(c) for c in node.children]))
return ymap[k]
def depth_of(node):
d = 0
cur = node
while cur.up is not None:
d += 1
cur = cur.up
return d
y_of(tree)
xs = {id(n): depth_of(n) for n in tree.traverse()}
vmax = 1.0
if node_value is not None and len(node_value):
vmax = max(float(np.nanmax(np.abs(np.asarray(node_value, dtype=float)))), 1e-8)
for n in tree.traverse():
xn, yn = xs[id(n)], y_of(n)
for c in n.children:
xc, yc = xs[id(c)], y_of(c)
ax.plot([xn, xn], [yn, yc], color="#8A8A8A", lw=0.8)
ax.plot([xn, xc], [yc, yc], color="#8A8A8A", lw=0.8)
name = _node_label(n)
if n.is_leaf:
ax.text(xn + 0.08, yn, name, va="center", ha="left", fontsize=8)
elif name:
ax.text(
xn - 0.06, yn + 0.15, name,
va="bottom", ha="right", fontsize=7, color="#666666",
)
if node_value is not None and n.name in getattr(node_value, "index", []):
v = float(node_value.loc[n.name])
if v != 0:
ax.scatter(
[xn], [yn],
s=25 + 200 * abs(v) / vmax,
c=pos_color if v > 0 else neg_color,
zorder=3, edgecolors="white", linewidths=0.4,
)
ax.set_axis_off()
ax.set_xlim(-0.8, max(xs.values()) + 2.8)
ax.set_ylim(-1, nleaf)
return ax
GROUP_COL = {"CTRL": "#7EB8E8", "ACI": "#F07A7A"}
GROUP_ORDER = ["CTRL", "ACI"]
def _canon_cell(name):
s = str(name)
for prefix in ("annot_main_", "annot_sub_"):
if s.startswith(prefix):
return s[len(prefix) :]
return s
def ordered_cell_types(names, kind="sub"):
"""Order cell types as names() of annot_main_col / annot_sub_col."""
order = ANNOT_MAIN_ORDER if kind == "main" else ANNOT_SUB_ORDER
rank = {n: i for i, n in enumerate(order)}
uniq = list(dict.fromkeys(map(str, names)))
return sorted(uniq, key=lambda n: (rank.get(_canon_cell(n), 999), n))
def reorder_coda_modality(mdata, kind="main", modality_key="coda"):
"""Put coda columns in plot_colors.R order (also fixes pertpy.plot_boxplots)."""
coda = mdata[modality_key]
order = ordered_cell_types(list(coda.var_names), kind=kind)
mdata.mod[modality_key] = coda[:, order].copy()
return order
def _wilcox_stars(p):
if p < 0.001:
return "***"
if p < 0.01:
return "**"
if p < 0.05:
return "*"
return "ns"
def plot_coda_boxplots(
data,
modality_key="coda",
cell_kind="sub",
figsize=(18, 6),
add_stats=True,
):
"""Grouped boxplots in plot_colors.R order, with Wilcoxon CTRL vs ACI."""
coda = data[modality_key]
x = np.asarray(coda.X, dtype=float)
props = x / np.clip(x.sum(axis=1, keepdims=True), 1e-12, None)
df = pd.DataFrame(props, columns=[str(c) for c in coda.var_names], index=coda.obs_names)
df["group"] = pd.Categorical(
coda.obs["group"].astype(str), categories=GROUP_ORDER, ordered=True
)
plot_df = df.melt(id_vars="group", var_name="Cell type", value_name="Proportion")
order = ordered_cell_types(plot_df["Cell type"].unique(), kind=cell_kind)
plot_df["Cell type"] = pd.Categorical(
plot_df["Cell type"], categories=order, ordered=True
)
fig, ax = plt.subplots(figsize=figsize)
sns.boxplot(
data=plot_df,
x="Cell type",
y="Proportion",
hue="group",
order=order,
hue_order=GROUP_ORDER,
palette=GROUP_COL,
fliersize=0,
width=0.7,
ax=ax,
)
sns.stripplot(
data=plot_df,
x="Cell type",
y="Proportion",
hue="group",
order=order,
hue_order=GROUP_ORDER,
palette=["#444444", "#444444"],
dodge=True,
size=3.5,
alpha=0.85,
legend=False,
ax=ax,
)
stats_rows = []
n_hue = 2
box_width = 0.7
step = box_width / n_hue
offsets = [-box_width / 2 + step / 2 + j * step for j in range(n_hue)]
yspan = max(float(plot_df["Proportion"].max()), 1e-6)
for i, ct in enumerate(order):
ctrl = plot_df.loc[
(plot_df["Cell type"] == ct) & (plot_df["group"] == "CTRL"), "Proportion"
]
aci = plot_df.loc[
(plot_df["Cell type"] == ct) & (plot_df["group"] == "ACI"), "Proportion"
]
p = float(
mannwhitneyu(
aci.to_numpy(), ctrl.to_numpy(), alternative="two-sided"
).pvalue
)
stats_rows.append(
{
"cell": _canon_cell(ct),
"n_ctrl": int(ctrl.size),
"n_aci": int(aci.size),
"median_ctrl": float(ctrl.median()) if len(ctrl) else np.nan,
"median_aci": float(aci.median()) if len(aci) else np.nan,
"p": p,
}
)
if not add_stats or p >= 0.05:
continue
star = _wilcox_stars(p)
y = max(ctrl.max(), aci.max()) + 0.04 * yspan
x0, x1 = i + offsets[0], i + offsets[1]
h = 0.015 * yspan
ax.plot([x0, x0, x1, x1], [y, y + h, y + h, y], color="0.35", lw=0.8)
ax.text(
(x0 + x1) / 2, y + h, star,
ha="center", va="bottom", fontsize=8, color="0.15",
)
stats = pd.DataFrame(stats_rows)
stats["p_adj"] = multipletests(stats["p"], method="fdr_bh")[1]
handles, labels = ax.get_legend_handles_labels()
ax.legend(
handles[:2], labels[:2], title="group",
loc="upper left", bbox_to_anchor=(1, 1),
)
ax.set_xlabel(None)
ax.tick_params(axis="x", rotation=90)
ax.set_ylim(0, plot_df["Proportion"].max() * 1.28)
ax.set_title(
"Wilcoxon rank-sum CTRL vs ACI; stars: uncorrected p "
"(* <0.05, ** <0.01, *** <0.001). BH FDR in the CSV.",
loc="left", fontsize=9, pad=8,
)
fig.tight_layout()
return ax, stats
ROOT = Path("..").resolve()
META = ROOT / "data/rdata/composition/cell_meta.csv"
RES = ROOT / "results/composition"
RES.mkdir(parents=True, exist_ok=True)
cell_meta = pd.read_csv(META)
cell_meta["group"] = pd.Categorical(
cell_meta["group"], categories=["CTRL", "ACI"], ordered=True
)
cell_meta.head()
/home/han/anaconda3/envs/pertpy/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
Out[1]:
| barcode | sample | group | annot_main | annot_sub | |
|---|---|---|---|---|---|
| 0 | ctrl_1_AAACCTGCAAGCGCTC-1 | CTRL-1 | CTRL | HSPC | HSC |
| 1 | ctrl_1_AAACCTGCAGACGCAA-1 | CTRL-1 | CTRL | B | Memory B |
| 2 | ctrl_1_AAACGGGCAGATGGCA-1 | CTRL-1 | CTRL | T | CD4 Tnaive |
| 3 | ctrl_1_AAACGGGCAGGTCCAC-1 | CTRL-1 | CTRL | Mono/DC | CD14 Mono |
| 4 | ctrl_1_AAACGGGGTACCATCA-1 | CTRL-1 | CTRL | B | Naive B |
In [2]:
adata = ad.AnnData(
X=np.zeros((cell_meta.shape[0], 1), dtype=np.float32),
obs=cell_meta.copy(),
)
adata.obs_names = cell_meta["barcode"].astype(str).values
adata
Out[2]:
AnnData object with n_obs × n_vars = 61190 × 1
obs: 'barcode', 'sample', 'group', 'annot_main', 'annot_sub'
1. scCODA (annot_main)¶
reference_cell_type="automatic" 选相对稳定的一类作参照。成分变化都是相对该参照。
大类水平。优先看中性粒这一枝的效应和可信区间。
In [3]:
sccoda = pt.tl.Sccoda()
sccoda_data = sccoda.load(
adata,
type="cell_level",
generate_sample_level=True,
cell_type_identifier="annot_main",
sample_identifier="sample",
covariate_obs=["group"],
)
reorder_coda_modality(sccoda_data, kind="main")
print(list(sccoda_data["coda"].var_names))
sccoda_data
['HSPC', 'B', 'Plasma', 'T', 'NK', 'Mono/DC', 'pDC', 'Pre-Neut', 'Neut', 'Ery']
Out[3]:
MuData object with n_obs × n_vars = 61201 × 11
2 modalities
rna: 61190 × 1
obs: 'barcode', 'sample', 'group', 'annot_main', 'annot_sub', 'scCODA_sample_id'
coda: 11 × 10
obs: 'sample', 'group'
var: 'n_cells'
In [4]:
reorder_coda_modality(sccoda_data, kind="main")
ax, sccoda_box_stats = plot_coda_boxplots(
sccoda_data, modality_key="coda", cell_kind="main", figsize=(12, 5)
)
sccoda_box_stats.to_csv(RES / "03_sccoda_box_wilcox.csv", index=False)
plt.savefig(RES / "03_sccoda_box.pdf", bbox_inches="tight")
plt.show()
sccoda_box_stats
Out[4]:
| cell | n_ctrl | n_aci | median_ctrl | median_aci | p | p_adj | |
|---|---|---|---|---|---|---|---|
| 0 | HSPC | 6 | 5 | 0.064219 | 0.055247 | 0.329004 | 0.329004 |
| 1 | B | 6 | 5 | 0.128233 | 0.019295 | 0.004329 | 0.007215 |
| 2 | Plasma | 6 | 5 | 0.021307 | 0.001527 | 0.008658 | 0.010823 |
| 3 | T | 6 | 5 | 0.311607 | 0.099641 | 0.004329 | 0.007215 |
| 4 | NK | 6 | 5 | 0.045914 | 0.015226 | 0.082251 | 0.091390 |
| 5 | Mono/DC | 6 | 5 | 0.097562 | 0.043791 | 0.008658 | 0.010823 |
| 6 | pDC | 6 | 5 | 0.015436 | 0.002356 | 0.004329 | 0.007215 |
| 7 | Pre-Neut | 6 | 5 | 0.048508 | 0.279479 | 0.004329 | 0.007215 |
| 8 | Neut | 6 | 5 | 0.030536 | 0.499515 | 0.004329 | 0.007215 |
| 9 | Ery | 6 | 5 | 0.181554 | 0.003193 | 0.004329 | 0.007215 |
In [5]:
sccoda_data = sccoda.prepare(
sccoda_data,
modality_key="coda",
formula="C(group, Treatment('CTRL'))",
reference_cell_type="automatic",
)
sccoda.run_nuts(sccoda_data, modality_key="coda")
sccoda.summary(sccoda_data, modality_key="coda")
sample: 100%|██████████| 11000/11000 [00:42<00:00, 258.61it/s, 255 steps of size 1.86e-02. acc. prob=0.75]
Compositional Analysis summary ┌────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────┐ │ Name │ Value │ ├────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ Data │ Data: 11 samples, 10 cell types │ │ Reference cell type │ HSPC │ │ Formula │ C(group, Treatment('CTRL')) │ └────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Intercepts │ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Final Parameter Expected Sample │ │ annot_main │ │ HSPC 1.045 526.577 │ │ B 1.498 828.319 │ │ Plasma -0.304 136.647 │ │ T 2.162 1609.048 │ │ NK 0.434 285.830 │ │ Mono/DC 1.184 605.102 │ │ pDC -0.295 137.882 │ │ Pre-Neut 0.452 291.022 │ │ Neut -0.125 163.432 │ │ Ery 1.665 978.869 │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Effects │ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Final Parameter Expected Sample log2-fold change │ │ Covariate Cell Type │ │ C(group, Treatment('CTRL'))T.ACI HSPC 0.000 237.568 -1.148 │ │ B -1.167 116.321 -2.832 │ │ Plasma 0.000 61.649 -1.148 │ │ T 0.000 725.930 -1.148 │ │ NK 0.000 128.954 -1.148 │ │ Mono/DC 0.000 272.995 -1.148 │ │ pDC 0.000 62.206 -1.148 │ │ Pre-Neut 2.545 1673.599 2.524 │ │ Neut 3.409 2228.287 3.769 │ │ Ery -2.079 55.219 -4.148 │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
In [6]:
sccoda.plot_effects_barplot(
sccoda_data, modality_key="coda", parameter="Final Parameter"
)
plt.savefig(RES / "03_sccoda_effects.pdf", bbox_inches="tight")
plt.show()
cred = sccoda.credible_effects(sccoda_data, modality_key="coda")
cred.to_csv(RES / "03_sccoda_credible.csv")
cred
/home/han/anaconda3/envs/pertpy/lib/python3.11/site-packages/seaborn/axisgrid.py:854: FutureWarning: Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect. func(*plot_args, **plot_kwargs)
<Figure size 640x480 with 0 Axes>
Out[6]:
Covariate Cell Type
C(group, Treatment('CTRL'))[T.ACI] HSPC False
B True
Plasma False
T False
NK False
Mono/DC False
pDC False
Pre-Neut True
Neut True
Ery True
Name: Final Parameter, dtype: bool
2. tascCODA (annot_sub tree)¶
pen_args={"phi": 0} 不偏向聚到上层还是叶子。11 个样本后验会比较宽。
树图用 matplotlib(plot_coda_tree),不要调用 plot_draw_tree / plot_draw_effects:它们走 ete4+Qt 的 %%inline,无显示器时会卡死或把 kernel 打挂。
亚型挂在大类下面。先看 GMP、Pre-Neut、成熟粒所在的枝。
In [7]:
tasccoda = pt.tl.Tasccoda()
tasc_data = tasccoda.load(
adata,
type="cell_level",
cell_type_identifier="annot_sub",
sample_identifier="sample",
covariate_obs=["group"],
levels_orig=["annot_main", "annot_sub"],
add_level_name=True,
)
reorder_coda_modality(tasc_data, kind="sub")
# Do not display MuData/ete Tree — IPython may call Qt treeview and crash the kernel.
print(tasc_data)
print(tasc_data["coda"])
print(list(tasc_data["coda"].var_names))
print(tasc_data["coda"].uns["tree"])
MuData object with n_obs × n_vars = 61201 × 35
2 modalities
rna: 61190 × 1
obs: 'barcode', 'sample', 'group', 'annot_main', 'annot_sub', 'scCODA_sample_id'
coda: 11 × 34
obs: 'sample', 'group'
var: 'n_cells'
uns: 'tree'
AnnData object with n_obs × n_vars = 11 × 34
obs: 'sample', 'group'
var: 'n_cells'
uns: 'tree'
['HSC', 'MEP', 'GMP', 'MDP', 'Pro-B', 'Large Pre-B', 'Small Pre-B', 'Trans B', 'Naive B', 'Memory B', 'Plasma', 'pDC', 'CD4 Tnaive', 'CD4 Tcm', 'CD4 Treg', 'CD8 Tnaive', 'CD8 Tem', 'CD8 CTL', 'CD8 NKT', 'MAIT', 'gdT', 'CD56bright NK', 'CD56dim NK', 'CD14 Mono', 'CD16 Mono', 'cDC2', 'Promyelocyte', 'MMP8 Pre-Neut', 'MMP9 Pre-Neut', 'S100A12 Neut', 'SLC8A1 Neut', 'ISG15 Neut', 'Early Ery', 'Late Ery']
╭╴HSC
╭─┼╴MDP
│ ├╴MEP
│ ╰╴GMP
│ ╭╴Memory B
│ ├╴Naive B
├─┼╴Small Pre-B
│ ├╴Large Pre-B
│ ├╴Pro-B
│ ╰╴Trans B
│ ╭╴CD4 Tnaive
│ ├╴CD4 Tcm
│ ├╴gdT
│ ├╴CD8 NKT
├─┼╴CD8 Tem
│ ├╴CD8 CTL
─┤ ├╴MAIT
│ ├╴CD8 Tnaive
│ ╰╴CD4 Treg
│ ╭╴CD14 Mono
├─┼╴CD16 Mono
│ ╰╴cDC2
│ ╭╴MMP9 Pre-Neut
├─┼╴Promyelocyte
│ ╰╴MMP8 Pre-Neut
├─┬╴Early Ery
│ ╰╴Late Ery
├─┬╴CD56bright NK
│ ╰╴CD56dim NK
├─╌╴pDC
├─╌╴Plasma
│ ╭╴S100A12 Neut
╰─┼╴SLC8A1 Neut
╰╴ISG15 Neut
In [8]:
fig, ax = plt.subplots(figsize=(8, 9))
plot_coda_tree(tasc_data["coda"].uns["tree"], ax=ax)
fig.savefig(RES / "03_tasccoda_tree.pdf", bbox_inches="tight")
plt.show()
ax, tasc_box_stats = plot_coda_boxplots(
tasc_data, modality_key="coda", cell_kind="sub", figsize=(18, 6)
)
tasc_box_stats.to_csv(RES / "03_tasccoda_box_wilcox.csv", index=False)
plt.savefig(RES / "03_tasccoda_box.pdf", bbox_inches="tight")
plt.show()
tasc_box_stats
Out[8]:
| cell | n_ctrl | n_aci | median_ctrl | median_aci | p | p_adj | |
|---|---|---|---|---|---|---|---|
| 0 | HSC | 6 | 5 | 0.010360 | 0.002078 | 0.004329 | 0.007747 |
| 1 | MEP | 6 | 5 | 0.006436 | 0.000556 | 0.004329 | 0.007747 |
| 2 | GMP | 6 | 5 | 0.007718 | 0.038173 | 0.004329 | 0.007747 |
| 3 | MDP | 6 | 5 | 0.030616 | 0.011799 | 0.008658 | 0.011775 |
| 4 | Pro-B | 6 | 5 | 0.010138 | 0.000831 | 0.004329 | 0.007747 |
| 5 | Large Pre-B | 6 | 5 | 0.014560 | 0.001156 | 0.004329 | 0.007747 |
| 6 | Small Pre-B | 6 | 5 | 0.026845 | 0.001669 | 0.004329 | 0.007747 |
| 7 | Trans B | 6 | 5 | 0.013884 | 0.001527 | 0.017316 | 0.019625 |
| 8 | Naive B | 6 | 5 | 0.038385 | 0.004850 | 0.004329 | 0.007747 |
| 9 | Memory B | 6 | 5 | 0.023412 | 0.005112 | 0.004329 | 0.007747 |
| 10 | Plasma | 6 | 5 | 0.021307 | 0.001527 | 0.008658 | 0.011775 |
| 11 | pDC | 6 | 5 | 0.015436 | 0.002356 | 0.004329 | 0.007747 |
| 12 | CD4 Tnaive | 6 | 5 | 0.050911 | 0.005001 | 0.008658 | 0.011775 |
| 13 | CD4 Tcm | 6 | 5 | 0.036778 | 0.014225 | 0.004329 | 0.007747 |
| 14 | CD4 Treg | 6 | 5 | 0.008849 | 0.002622 | 0.008658 | 0.011775 |
| 15 | CD8 Tnaive | 6 | 5 | 0.024592 | 0.002208 | 0.008658 | 0.011775 |
| 16 | CD8 Tem | 6 | 5 | 0.064117 | 0.015130 | 0.004329 | 0.007747 |
| 17 | CD8 CTL | 6 | 5 | 0.048812 | 0.018115 | 0.125541 | 0.133387 |
| 18 | CD8 NKT | 6 | 5 | 0.032628 | 0.014714 | 0.246753 | 0.246753 |
| 19 | MAIT | 6 | 5 | 0.008898 | 0.004554 | 0.017316 | 0.019625 |
| 20 | gdT | 6 | 5 | 0.012588 | 0.003187 | 0.008658 | 0.011775 |
| 21 | CD56bright NK | 6 | 5 | 0.011407 | 0.001104 | 0.017316 | 0.019625 |
| 22 | CD56dim NK | 6 | 5 | 0.036080 | 0.014337 | 0.246753 | 0.246753 |
| 23 | CD14 Mono | 6 | 5 | 0.081585 | 0.041223 | 0.017316 | 0.019625 |
| 24 | CD16 Mono | 6 | 5 | 0.006582 | 0.002078 | 0.004329 | 0.007747 |
| 25 | cDC2 | 6 | 5 | 0.008561 | 0.001656 | 0.004329 | 0.007747 |
| 26 | Promyelocyte | 6 | 5 | 0.006574 | 0.047250 | 0.004329 | 0.007747 |
| 27 | MMP8 Pre-Neut | 6 | 5 | 0.007998 | 0.130525 | 0.004329 | 0.007747 |
| 28 | MMP9 Pre-Neut | 6 | 5 | 0.033103 | 0.128034 | 0.004329 | 0.007747 |
| 29 | S100A12 Neut | 6 | 5 | 0.013892 | 0.193920 | 0.004329 | 0.007747 |
| 30 | SLC8A1 Neut | 6 | 5 | 0.009561 | 0.212148 | 0.004329 | 0.007747 |
| 31 | ISG15 Neut | 6 | 5 | 0.005018 | 0.016699 | 0.013497 | 0.017650 |
| 32 | Early Ery | 6 | 5 | 0.115489 | 0.003193 | 0.004329 | 0.007747 |
| 33 | Late Ery | 6 | 5 | 0.067271 | 0.000514 | 0.053027 | 0.058159 |
In [9]:
tasc_data = tasccoda.prepare(
tasc_data,
modality_key="coda",
tree_key="tree",
formula="C(group, Treatment('CTRL'))",
reference_cell_type="automatic",
pen_args={"phi": 0},
)
# pertpy default: num_warmup=1000, num_samples=10000, rng_key=0
tasccoda.run_nuts(tasc_data, modality_key="coda")
tasccoda.summary(tasc_data, modality_key="coda")
sample: 100%|██████████| 11000/11000 [00:27<00:00, 405.39it/s, 63 steps of size 7.42e-02. acc. prob=0.93]
Compositional Analysis summary ┌────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────┐ │ Name │ Value │ ├────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ Data │ Data: 11 samples, 34 cell types │ │ Reference cell type │ CD16 Mono │ │ Formula │ C(group, Treatment('CTRL')) │ └────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Intercepts │ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Final Parameter Expected Sample │ │ annot_sub │ │ HSC 0.001 91.615 │ │ MEP -0.606 49.929 │ │ GMP 0.216 113.590 │ │ MDP 0.828 209.473 │ │ Pro-B 0.003 91.799 │ │ Large Pre-B 0.172 108.701 │ │ Small Pre-B 0.504 151.502 │ │ Trans B 0.070 98.160 │ │ Naive B 0.831 210.103 │ │ Memory B 0.670 178.859 │ │ Plasma 0.017 93.093 │ │ pDC 0.087 99.843 │ │ CD4 Tnaive 0.866 217.587 │ │ CD4 Tcm 1.060 264.171 │ │ CD4 Treg -0.127 80.608 │ │ CD8 Tnaive 0.259 118.581 │ │ CD8 Tem 1.406 373.380 │ │ CD8 CTL 1.068 266.293 │ │ CD8 NKT 0.816 206.975 │ │ MAIT 0.020 93.372 │ │ gdT 0.116 102.781 │ │ CD56bright NK -0.304 67.532 │ │ CD56dim NK 0.884 221.539 │ │ CD14 Mono 1.754 528.793 │ │ CD16 Mono -0.390 61.967 │ │ cDC2 -0.279 69.241 │ │ Promyelocyte -0.550 52.805 │ │ MMP8 Pre-Neut 0.373 132.900 │ │ MMP9 Pre-Neut 0.470 146.437 │ │ S100A12 Neut 0.165 107.942 │ │ SLC8A1 Neut 0.107 101.860 │ │ ISG15 Neut -1.766 15.652 │ │ Early Ery 1.899 611.305 │ │ Late Ery 0.898 224.662 │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Effects │ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Effect Expected Sample log2-fold change │ │ Covariate Cell Type │ │ C(group, Treatment('CTRL'))T.ACI HSC 0.000 36.844 -1.314 │ │ MEP 0.000 20.080 -1.314 │ │ GMP 1.221 154.888 0.447 │ │ MDP 0.000 84.243 -1.314 │ │ Pro-B -0.598 20.302 -2.177 │ │ Large Pre-B -0.598 24.040 -2.177 │ │ Small Pre-B -0.598 33.505 -2.177 │ │ Trans B -0.598 21.708 -2.177 │ │ Naive B -0.598 46.465 -2.177 │ │ Memory B -0.598 39.555 -2.177 │ │ Plasma 0.000 37.439 -1.314 │ │ pDC 0.000 40.153 -1.314 │ │ CD4 Tnaive 0.000 87.506 -1.314 │ │ CD4 Tcm 0.000 106.240 -1.314 │ │ CD4 Treg 0.000 32.418 -1.314 │ │ CD8 Tnaive 0.000 47.689 -1.314 │ │ CD8 Tem 0.000 150.160 -1.314 │ │ CD8 CTL 0.000 107.094 -1.314 │ │ CD8 NKT 0.000 83.238 -1.314 │ │ MAIT 0.000 37.551 -1.314 │ │ gdT 0.000 41.335 -1.314 │ │ CD56bright NK 0.000 27.159 -1.314 │ │ CD56dim NK 0.000 89.095 -1.314 │ │ CD14 Mono 0.000 212.662 -1.314 │ │ CD16 Mono 0.000 24.921 -1.314 │ │ cDC2 0.000 27.846 -1.314 │ │ Promyelocyte 2.513 262.094 2.311 │ │ MMP8 Pre-Neut 2.513 659.646 2.311 │ │ MMP9 Pre-Neut 2.513 726.838 2.311 │ │ S100A12 Neut 3.195 1059.660 3.295 │ │ SLC8A1 Neut 3.195 999.948 3.295 │ │ ISG15 Neut 3.195 153.654 3.295 │ │ Early Ery -1.612 49.043 -3.640 │ │ Late Ery -1.612 18.024 -3.640 │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Nodes │ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Covariate=C(group, Treatment('CTRL'))[T.ACI]_node │ ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ │ Final Parameter Is credible │ │ Node │ │ annot_main_HSPC 0.00 False │ │ annot_main_B -0.60 True │ │ annot_main_T 0.00 False │ │ annot_main_NK 0.00 False │ │ annot_main_Mono/DC 0.00 False │ │ annot_main_Pre-Neut 2.51 True │ │ annot_main_Neut 3.19 True │ │ annot_main_Ery -1.61 True │ │ Plasma 0.00 False │ │ pDC 0.00 False │ │ HSC 0.00 False │ │ MEP 0.00 False │ │ GMP 1.22 True │ │ MDP 0.00 False │ │ Pro-B 0.00 False │ │ Large Pre-B 0.00 False │ │ Small Pre-B 0.00 False │ │ Trans B 0.00 False │ │ Naive B 0.00 False │ │ Memory B 0.00 False │ │ CD4 Tnaive 0.00 False │ │ CD4 Tcm 0.00 False │ │ CD4 Treg 0.00 False │ │ CD8 Tnaive 0.00 False │ │ CD8 Tem 0.00 False │ │ CD8 CTL 0.00 False │ │ CD8 NKT 0.00 False │ │ MAIT 0.00 False │ │ gdT 0.00 False │ │ CD56bright NK 0.00 False │ │ CD56dim NK 0.00 False │ │ CD14 Mono 0.00 False │ │ CD16 Mono 0.00 False │ │ cDC2 0.00 False │ │ Promyelocyte 0.00 False │ │ MMP8 Pre-Neut 0.00 False │ │ MMP9 Pre-Neut 0.00 False │ │ S100A12 Neut 0.00 False │ │ SLC8A1 Neut 0.00 False │ │ ISG15 Neut 0.00 False │ │ Early Ery 0.00 False │ │ Late Ery 0.00 False │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
In [10]:
cov = tasc_data["coda"].uns["scCODA_params"]["covariate_names"][0]
node_df = tasc_data["coda"].uns["scCODA_params"]["node_df"]
node_key = cov + "_node"
if node_key in node_df.index.get_level_values(0):
node_effs = node_df.loc[(node_key,)].copy()
node_effs.index = node_effs.index.get_level_values("Node")
node_vals = node_effs["Final Parameter"]
else:
node_vals = None
print("node effects not found for", node_key, node_df.index[:5])
fig, ax = plt.subplots(figsize=(8, 9))
plot_coda_tree(tasc_data["coda"].uns["tree"], ax=ax, node_value=node_vals)
fig.savefig(RES / "03_tasccoda_effects_tree.pdf", bbox_inches="tight")
plt.show()
cred_t = tasccoda.credible_effects(tasc_data, modality_key="coda")
cred_t.to_csv(RES / "03_tasccoda_credible.csv")
cred_t
Out[10]:
Covariate Node
C(group, Treatment('CTRL'))[T.ACI]_node annot_main_HSPC False
annot_main_B True
annot_main_T False
annot_main_NK False
annot_main_Mono/DC False
annot_main_Pre-Neut True
annot_main_Neut True
annot_main_Ery True
Plasma False
pDC False
HSC False
MEP False
GMP True
MDP False
Pro-B False
Large Pre-B False
Small Pre-B False
Trans B False
Naive B False
Memory B False
CD4 Tnaive False
CD4 Tcm False
CD4 Treg False
CD8 Tnaive False
CD8 Tem False
CD8 CTL False
CD8 NKT False
MAIT False
gdT False
CD56bright NK False
CD56dim NK False
CD14 Mono False
CD16 Mono False
cDC2 False
Promyelocyte False
MMP8 Pre-Neut False
MMP9 Pre-Neut False
S100A12 Neut False
SLC8A1 Neut False
ISG15 Neut False
Early Ery False
Late Ery False
Name: Final Parameter, dtype: bool
3. Session info¶
软件版本,便于复现。
In [11]:
print(session_info())
pandas 2.3.3 anndata 0.12.19 jax 0.10.2 matplotlib 3.11.2 numpy 2.4.6 seaborn 0.13.2 scipy 1.17.1 statsmodels 0.15.0 numpyro 0.22.0 pertpy 1.0.3 mudata 0.3.10 ---- ---- Python 3.11.16 (main, Sep 2 2026, 23:39:52) [GCC 15.3.0] OS Linux-6.11.0-26-generic-x86_64-with-glibc2.39 Updated 2026-09-26 23:48