4. K-means在Visium空间域中的应用归纳总结
聊到K-means,很多人第一反应是“这不就是最基础的聚类算法嘛”。没错,它简单,但恰恰是这种简单,让它在Visium空间转录组分析中占了一个很特别的位置。我个人习惯拿K-means做第一道筛子——在跑那些复杂的图神经网络之前,先用K-means看看数据大概长什么样。
说白了,K-means不关心空间位置,只关心基因表达谱的相似性。但Visium数据偏偏带空间坐标,所以怎么把这种“非空间”的聚类结果映射回组织切片上,就成了一个很有意思的命题。
核心思路:将每个spots的基因表达向量作为输入特征,通过K-means将其划分为K个簇。然后把这些簇标签直接投影到组织切片的空间坐标上,观察它们在组织形态中的分布规律。
4.1 为什么选择K-means?我踩过的坑
刚做Visium分析那会儿,我总想一上来就用复杂模型,结果调参调到崩溃。后来发现,先用K-means做一个“基线”聚类,能让你更快理解数据的结构。
K-means在Visium应用中的三大优势:
- 速度快——一个标准10x Visium样本大约5000个spots,跑K-means只需几秒钟
- 结果可解释——聚类中心就是平均表达谱,可以直接做差异基因分析
- 初始化依赖可控——我建议用k-means++初始化,可以显著提升稳定性
但是,它有个硬伤:不考虑空间近邻性。举个例子,两个在组织上相邻的spots,如果它们属于不同的细胞类型,K-means会强行把它们分到不同的簇里,这没问题。但问题是,同一个空间域内部,基因表达往往是有梯度的,K-means会在这个梯度中间切一刀,造成空间上的“破碎感”。
避坑指南:我曾经在一个脑组织样本上直接跑K-means(K=10),结果同一个皮层区域被切成了4个碎块。原因是K-means只认表达谱的欧氏距离,完全不理会spot是否相邻。后来我加了空间平滑预处理,情况才好转。
4.2 K-means在Visium上的标准流程
我把这个流程分了5步,每一步都踩过坑,你注意看标注。
- 数据加载与标准化——读入Seurat或Scanpy对象,提取count矩阵
- 特征选择——选高变异基因(HVGs),通常2000-5000个
- 尺度化——Z-score归一化,避免高表达基因主导聚类
- K-means聚类——设置K值,用k-means++初始化,多跑几次选最优
- 空间映射与可视化——把簇标签画在组织切片坐标上
我在Scanpy里的代码长这样:
import scanpy as sc
import numpy as np
from sklearn.cluster import KMeans
# 读取Visium数据
adata = sc.read_visium('path/to/spaceranger/output')
sc.pp.normalize_total(adata, inplace=True)
sc.pp.log1p(adata)
sc.pp.highly_variable_genes(adata, n_top_genes=2000)
# 只保留高变异基因
adata_hvg = adata[:, adata.var.highly_variable]
# 尺度化
sc.pp.scale(adata_hvg, max_value=10)
# K-means聚类
kmeans = KMeans(n_clusters=8, init='k-means++',
n_init=10, random_state=42)
clusters = kmeans.fit_predict(adata_hvg.X)
# 将聚类结果写回adata对象
adata.obs['kmeans_8'] = clusters.astype(str)
# 可视化
sc.pl.spatial(adata, color='kmeans_8',
spot_size=50, title='K-means K=8')
我的小技巧:n_init至少要设10,甚至20。我遇到过n_init=3时每次结果都不一样,增加n_init后稳定多了。另外,random_state固定一个值,方便复现。
4.3 K值怎么定?别靠猜
嗯,K值选择是K-means最头疼的问题。我见过有人直接拍脑袋设K=10,结果出来的空间域跟组织形态完全对不上。
我常用的三种方法:
- 肘部法则(Elbow Method)——计算不同K值下的SSE(簇内平方和),取拐点处
- 轮廓系数(Silhouette Score)——衡量簇内紧密度和簇间分离度,选最高值
- 先验知识——如果你知道组织有几种主要细胞类型,直接设那个数
但说实话,在Visium数据上,这些指标往往指向不同的K值。我个人习惯是:先跑2到20的K值范围,然后看哪个K值能产生在空间上连续、形态上合理的域。这个方法虽然主观,但做空间转录组分析,形态学验证是绕不开的。
代码实现肘部法则:
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
sse = []
silhouette_scores = []
K_range = range(2, 21)
for k in K_range:
kmeans = KMeans(n_clusters=k, init='k-means++',
n_init=10, random_state=42)
labels = kmeans.fit_predict(adata_hvg.X)
sse.append(kmeans.inertia_)
silhouette_scores.append(silhouette_score(adata_hvg.X, labels))
# 画图
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(K_range, sse, 'bo-')
axes[0].set_xlabel('K')
axes[0].set_ylabel('SSE')
axes[0].set_title('Elbow Method')
axes[1].plot(K_range, silhouette_scores, 'ro-')
axes[1].set_xlabel('K')
axes[1].set_ylabel('Silhouette Score')
axes[1].set_title('Silhouette Score')
plt.tight_layout()
plt.show()
${1:/core-section}
你注意看,肘部法通常给出的K值比轮廓系数要小。我一般取两者的中间值,或者直接看哪个K值在空间图上“看着顺眼”。
4.4 优缺点对比:K-means vs 其他方法
我把K-means和后面几章要讲的方法做了个对比。这个表你在看后续内容时可以反复回来对照。
| 维度 | K-means | Leiden / Louvain | BayesSpace |
|---|---|---|---|
| 空间信息利用 | ❌ 完全不使用 | ⚠️ 间接使用空间距离 | ✅ 显式建模空间邻域 |
| 聚类速度 | ✅ 极快(秒级) | ✅ 快(分钟级) | ⚠️ 较慢(小时级) |
| 域平滑性 | ❌ 易出现孤点 | ✅ 较好 | ✅ 优秀 |
| 调参复杂度 | ✅ 低(只需K) | ⚠️ 中等(分辨率参数) | ❌ 高(需要先验) |
| 可解释性 | ✅ 高(聚类中心明确) | ✅ 高(社区结构清晰) | ⚠️ 中(概率模型复杂) |
我的建议:如果你是第一次接触Visium数据,或者只是想快速预览样本的组织结构,K-means是个不错的起点。但如果你想做精细的空间域划分,后面几章要介绍的Leiden和BayesSpace会更靠谱。
4.5 K-means在Visium上的高级玩法
你以为K-means就这么简单?其实可以玩出花儿来。
玩法一:K-means + 空间过滤
我先跑K-means,然后用空间邻接矩阵做模式过滤。如果一个spot的簇标签和它周围大多数邻居不同,就把它重新标记为邻居的多数标签。这就是一种简单的空间平滑。
from scipy.spatial import KDTree
# 获取空间坐标
coords = adata.obsm['spatial']
tree = KDTree(coords)
# 对每个spot,找到半径内的邻居
smoothed_clusters = clusters.copy()
for i in range(len(clusters)):
indices = tree.query_ball_point(coords[i], r=150) # 半径150μm
neighbor_labels = clusters[indices]
# 取多数标签
majority = np.argmax(np.bincount(neighbor_labels))
smoothed_clusters[i] = majority
adata.obs['kmeans_smoothed'] = smoothed_clusters.astype(str)
玩法二:K-means + 分层聚类
我有时会跑两轮K-means:第一轮用大的K值(比如K=20)捕获细粒度结构,第二轮把相近的簇合并。这样可以避免一次选错K值。
踩过的坑:我试过直接用聚类中心做层次聚类合并,结果发现两个生物学上完全不同的域(比如白质和灰质)因为噪声被合并了。后来我改用差异基因的Jaccard相似度做合并依据,效果才改善。
4.6 知识结构总结
我把这一章的核心内容整理成了一张图。你把它当作你脑子里的一张地图,后续讲到其他算法时,可以来回对比。
4.7 总结一下
K-means在Visium分析里,就像是一把瑞士军刀——不是最锋利的,但胜在顺手、皮实、哪都能用。我个人认为,它最适合做三件事:第一,快速预览样本的组织结构;第二,为复杂模型提供初始化标签;第三,作为基线方法对比其他算法的提升效果。
但你也要记住它的边界:不碰空间信息、K值拍脑袋、域边界锯齿状。如果你遇到的是边界模糊的肿瘤区域或者层次分明的脑皮层,建议直接跳到后面的Leiden或BayesSpace。