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步,每一步都踩过坑,你注意看标注。

  1. 数据加载与标准化——读入Seurat或Scanpy对象,提取count矩阵
  2. 特征选择——选高变异基因(HVGs),通常2000-5000个
  3. 尺度化——Z-score归一化,避免高表达基因主导聚类
  4. K-means聚类——设置K值,用k-means++初始化,多跑几次选最优
  5. 空间映射与可视化——把簇标签画在组织切片坐标上

我在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 知识结构总结

我把这一章的核心内容整理成了一张图。你把它当作你脑子里的一张地图,后续讲到其他算法时,可以来回对比。

K-means在Visium空间域应用知识体系 K-means聚类 输入:基因表达矩阵 辅助:空间坐标信息 (仅用于可视化) 参数调优 K值选择:肘部法 + 轮廓系数 初始化:k-means++ 后处理与优化 空间平滑过滤 分层聚类合并 输出与验证 空间域标签 + 差异基因分析 适用场景 快速预览、基线对比、 粗糙的空间域划分 局限性 不考虑空间邻域、 K值依赖经验、域边界粗糙

4.7 总结一下

K-means在Visium分析里,就像是一把瑞士军刀——不是最锋利的,但胜在顺手、皮实、哪都能用。我个人认为,它最适合做三件事:第一,快速预览样本的组织结构;第二,为复杂模型提供初始化标签;第三,作为基线方法对比其他算法的提升效果。

但你也要记住它的边界:不碰空间信息、K值拍脑袋、域边界锯齿状。如果你遇到的是边界模糊的肿瘤区域或者层次分明的脑皮层,建议直接跳到后面的Leiden或BayesSpace。