4. 第一个BBKNN实例:快速上手

这一节,咱们直接动手。先跑通一个最简单的BBKNN流程,让你看看它到底能干什么,再回头理解原理。

我个人习惯是“先见森林,再见树木”。硬啃一堆理论参数,不如亲自跑一遍代码来得实在。我会用单细胞RNA-seq数据集来演示,因为这是BBKNN最典型的应用场景,但你放心,数据量很小,电脑跑得动。

4.1 环境准备:把工具装好

别一上来就装几十个包。你需要的核心工具就这几个:

  • Python 3.8+(推荐3.9或3.10)
  • scanpy(单细胞数据分析的事实标准)
  • bbknn(你要学的主角)
  • scipy + numpy(底层数学支持)

安装命令很简单。我建议用conda创建干净环境,避免和别的项目打架。

conda create -n bbknn_demo python=3.9
conda activate bbknn_demo
pip install scanpy bbknn

嗯,这里要注意:scanpy会自动帮你装好scipy和numpy,所以不需要单独装它们。

小提示:如果你在Mac M1/M2芯片上装,记得用arm64版本的Python,不然编译某些依赖会慢到让你怀疑人生。

4.2 数据准备:用现成的测试数据

scanpy自带了一些示例数据集。我们用 pbmc3k(3,000个人类外周血单核细胞),这是单细胞领域的“Hello World”。

我曾经在这个数据集上踩过坑——第一次跑BBKNN时没有做标准化,结果聚类结果乱七八糟。所以请你跟着我,一步步来。

import scanpy as sc
import bbknn

# 加载数据(会自动下载)
adata = sc.datasets.pbmc3k()
print(adata)
# 输出:AnnData object with n_obs × n_vars = 2700 × 32738

这个 AnnData 对象里包含了基因表达矩阵。2700个细胞,32738个基因。数据量不算大,跑起来很快。

4.3 数据预处理:标准流程走一遍

跑BBKNN之前,得先做标准预处理。说白了就是:过滤低质量细胞、归一化、找高变基因、降维。这一步和普通单细胞分析一模一样,但后面我们会把“邻居计算”这一步换成BBKNN。

注意:千万别跳过归一化!BBKNN是基于距离的算法,如果基因表达量范围差太大(比如有的基因表达100,有的只有1),距离计算会完全被高表达基因支配。结果就是——聚类效果差到没法看。

完整预处理代码:

# 1. 质量控制:过滤掉线粒体基因比例过高的细胞
adata.var['mt'] = adata.var_names.str.startswith('MT-')
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], percent_top=None, log1p=False)
adata = adata[adata.obs.pct_counts_mt < 20, :]
print(f"质控后细胞数: {adata.n_obs}")

# 2. 归一化(log标准化)
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)

# 3. 找高变基因(前2000个)
sc.pp.highly_variable_genes(adata, n_top_genes=2000, batch_key=None)
adata = adata[:, adata.var.highly_variable]

# 4. 主成分分析(PCA降维到50个主成分)
sc.pp.scale(adata, max_value=10)
sc.tl.pca(adata, n_comps=50, svd_solver='arpack')

print("预处理完成。PCA后的数据形状: ", adata.obsm['X_pca'].shape)
# 输出:预处理完成。PCA后的数据形状: (2638, 50)

现在我们有2638个细胞(过滤掉了62个线粒体比例高的),每个细胞用50个主成分表示。这个维数已经很低了,很适合直接跑BBKNN。

4.4 运行BBKNN:核心一步

这里就是主角登场的地方了。BBKNN的全称是“Batch Balanced KNN”,它做的事情是:在找k近邻的时候,主动平衡不同批次之间的邻居数量,避免某个批次因为细胞多就占据所有邻居。

你只要调用一个函数:bbknn.bbknn()。它会自动修改 adata.obsp['connectivities'] 和 adata.uns['neighbors'],直接替代scanpy原生的邻居计算。

import bbknn

# 假设你的数据来自不同批次(batch)
# 这里为了演示,我们人为把数据分成两个批次,模拟真实场景
adata.obs['batch'] = ['batch1'] * (adata.n_obs // 2) + ['batch2'] * (adata.n_obs - adata.n_obs // 2)

# 核心:调用BBKNN!
bbknn.bbknn(adata, batch_key='batch', neighbors_within_batch=3, n_pcs=50)

print("BBKNN邻居计算完成")
print("连接矩阵形状: ", adata.obsp['connectivities'].shape)
# 输出:BBKNN邻居计算完成
# 输出:连接矩阵形状: (2638, 2638)

这里 neighbors_within_batch=3 是什么意思?说白了,就是让每个细胞在自己所在的批次里至少找3个邻居,然后再去其他批次里补足剩下的k个邻居(默认k=30)。这个参数很关键——值太大,批内抱团太严重,无法消除批次效应;值太小,批次校正力度太强,可能丢失生物异质性。

经验之谈:我个人习惯先从 neighbors_within_batch=3 开始尝试。如果聚类结果里还能看到明显的批次簇(比如所有batch1聚在一起),就把这个值降到1或2。如果过度校正导致本来不同的细胞类型被混在一起,再把它升到5以上。

4.5 可视化效果:看一眼就知道成不成

邻居算完了,接下来用UMAP降维到2D,看看BBKNN到底有没有消除批次效应。

# 用BBKNN得到的连接矩阵做UMAP
sc.tl.umap(adata)
sc.pl.umap(adata, color='batch', title='BBKNN: 批次分布', save='_bbknn_batch.png')

# 也可以看细胞类型(这里我们用聚类结果示意)
sc.tl.leiden(adata, resolution=0.5)
sc.pl.umap(adata, color='leiden', title='BBKNN: 聚类结果', save='_bbknn_cluster.png')

你会看到:不同批次的细胞均匀混合在一起,而生物意义上的细胞类型(由leiden聚类展示)依然保持分离。这意味着批次效应被有效消除了,但真正的生物差异保留了下来。

我第一次跑出这个结果时,确实愣了一下——就这么简单?三行代码就解决了?但事实就是这样。BBKNN的魔力在于它只在邻居计算这一步做文章,后面的降维和聚类全都不用改。

4.6 效果评估:别光说好,得量化

光看UMAP图不够,得有量化指标。我常用的两个指标:

指标 用途 理想值
ASW(轮廓系数) 衡量批次混合程度(按批次标签) 接近0(负值越小越好)
NMI(归一化互信息) 衡量生物差异保留程度(按聚类标签) 0.6 ~ 1.0

用scanpy自带的工具就能算:

from sklearn.metrics import silhouette_score, normalized_mutual_info_score

# 1. 批次混合度(基于UMAP坐标)
umb = adata.obsm['X_umap']
batch_labels = adata.obs['batch'].values
sil_score = silhouette_score(umb, batch_labels)
print(f"批次ASW: {sil_score:.3f}")

# 2. 聚类一致性(基于Leiden标签和真实细胞类型)
# 这里我们用模拟的真实标签(实际项目中替换为真实的细胞类型)
true_labels = adata.obs['leiden'].sample(frac=1, random_state=42).values  # 只是为了演示用法
nmi_score = normalized_mutual_info_score(adata.obs['leiden'], true_labels)
print(f"聚类NMI: {nmi_score:.3f}")
避坑指南:千万不要只看ASW!有时候批次完全混合了,但生物差异也没了,ASW会非常接近0,但聚类结果一团糟。一定要结合NMI来看。我曾经在一个公共数据集上踩过这个坑,模型跑出来批次混合度很好看,但真正的细胞类型基本丢失。后来加了NMI监控才避免。

4.7 核心流程总结

整个快速上手流程,其实就几个步骤。我用一张图帮你串起来:

BBKNN快速上手核心流程 1. 加载数据 scanpy.datasets 2. 标准预处理 归一化+PCA 3. BBKNN核心 bbknn.bbknn() 4. UMAP降维 sc.tl.umap() 5. 聚类 sc.tl.leiden() 6. 量化评估 ASW + NMI 关键参数 batch_key: 指定批次标签列名 neighbors_within_batch: 控制批内邻居数量(推荐3~8) n_pcs: 使用的主成分数量(通常30~50)

整个流程走下来,代码量不超过30行。但你要理解每一个参数的作用和风险,才能在真实项目中用好它。

4.8 本章小结

通过这个快速上手实例,你应该已经掌握:

  • 如何安装和导入BBKNN
  • 标准预处理流程(质控→归一化→PCA)
  • 核心调用:bbknn.bbknn() 的用法
  • 如何用UMAP和量化指标评估效果