Scanpy 多样本分析完全指南:从数据读取到整合报告
一、项目结构规划
project/
├── data/
│ ├── sample1/filtered_feature_bc_matrix.h5
│ ├── sample2/filtered_feature_bc_matrix.h5
│ └── sample3/filtered_feature_bc_matrix.h5
├── metadata/
│ └── sample_info.csv # sample_id, condition, patient_id, etc.
├── scripts/
│ └── analysis.py
└── results/
├── figures/
└── tables/
二、批量读取和预处理
import scanpy as sc
import anndata as ad
import pandas as pd
import numpy as np
# 读取样本元数据
sample_meta = pd.read_csv("metadata/sample_info.csv")
print(sample_meta)
# sample_id condition patient_id batch
# sample1 control P01 batch1
# sample2 treatment P01 batch1
# sample3 control P02 batch2
# 批量读取并添加元数据
adatas = {}
for _, row in sample_meta.iterrows():
sid = row["sample_id"]
h5_path = f"data/{sid}/filtered_feature_bc_matrix.h5"
tmp = sc.read_10x_h5(h5_path)
tmp.var_names_make_unique()
# 添加样本元数据
for col in sample_meta.columns:
tmp.obs[col] = row[col]
# 确保 barcode 全局唯一
tmp.obs_names = [f"{sid}_{bc}" for bc in tmp.obs_names]
adatas[sid] = tmp
print(f"读取 {sid}: {tmp.shape}")
# 合并所有样本
adata = ad.concat(adatas.values(), join="outer", fill_value=0)
print(f"\n合并后: {adata.shape}")
三、统一 QC 和过滤
# 标记线粒体基因(人类)
adata.var["mt"] = adata.var_names.str.startswith("MT-")
sc.pp.calculate_qc_metrics(adata, qc_vars=["mt"], inplace=True)
# 按样本查看 QC 分布
import matplotlib.pyplot as plt
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
for i, qc_var in enumerate(["n_genes_by_counts", "total_counts", "pct_counts_mt"]):
import seaborn as sns
sns.violinplot(data=adata.obs, x="sample_id", y=qc_var, ax=axes[i])
axes[i].set_title(qc_var)
plt.tight_layout()
plt.savefig("results/figures/qc_per_sample.pdf")
# 过滤(基于各样本分布,可以设置不同阈值)
# 方法一:统一阈值
mask_cells = (
(adata.obs["n_genes_by_counts"] >= 500) &
(adata.obs["n_genes_by_counts"] <= 6000) &
(adata.obs["total_counts"] >= 1000) &
(adata.obs["pct_counts_mt"] <= 20)
)
# 方法二:MAD-based 自适应阈值(每个样本单独计算)
def mad_filter(values, n_mads=3):
median = np.median(values)
mad = np.median(np.abs(values - median))
return (values >= median - n_mads * mad) & (values <= median + n_mads * mad)
sample_masks = []
for sid in sample_meta["sample_id"]:
sample_idx = adata.obs["sample_id"] == sid
sample_adata = adata[sample_idx]
mask = (
mad_filter(sample_adata.obs["n_genes_by_counts"].values) &
mad_filter(np.log1p(sample_adata.obs["total_counts"].values)) &
(sample_adata.obs["pct_counts_mt"] <= 20)
)
sample_masks.append(pd.Series(mask, index=sample_adata.obs_names))
adaptive_mask = pd.concat(sample_masks)[adata.obs_names].values
adata = adata[adaptive_mask].