☰
脚本更新----Xenium、CODEX、CosMx范围邻域矩阵的获得与亚群分析:用TaoToken统一Key跑通Leiden聚类
2026/10/2 15:31:17 网站建设 项目流程

1. 从空间坐标到邻域矩阵:Xenium、CODEX、CosMx 下游分析到底卡在哪

如果你手上有 Xenium、CODEX 或 CosMx 的数据,做完细胞分割和注释之后,大概率会碰到同一个问题:单细胞层面的表达矩阵已经拿到了,但细胞和细胞之间的空间关系还没被利用起来。空间转录组和多重免疫荧光真正的价值,不在于每个细胞单独长什么样,而在于一群细胞在 200 µm 这个尺度上如何组织成 niche、如何形成亚群结构。这一步做不好,后面的差异分析、细胞通讯、区域注释都会缺一块拼图。

我先把问题拆清楚。Xenium 输出的是每个转录本的三维坐标加细胞边界,CODEX 输出的是每个细胞的多通道蛋白强度加质心坐标,CosMx 介于两者之间,既有转录本也有蛋白通道。三类平台的数据结构不同,但下游要做的动作高度一致:提取空间坐标,按欧氏距离构建邻域矩阵,把邻域关系转成图,用 Leiden 做图聚类,再把聚类标签写回 AnnData,最后用 UMAP 和空间散点图验证。这套链路的核心检索词就是空间邻域矩阵构建与 Leiden 亚群分析,适合已经跑通上游 pipeline、想快速复现下游聚类的分析人员。

为什么强调 200 µm?这个尺度在组织学上大致对应几个细胞直径到十几个细胞直径的范围,既能捕捉局部微环境,又不会把整个切片混成一团。你可以把它理解成给每个细胞画一个半径 200 µm 的圆,圆里所有其他细胞都是它的邻居。邻居关系确定之后,Leiden 算法会在这个图上找社区,社区内部的细胞连接紧密,社区之间连接稀疏,这些社区就是我们要的亚群。

实际踩过的坑主要有三个。第一,坐标字段名不统一,Xenium 常用x_centroid/y_centroid,CODEX 常用x/y,CosMx 有时是CenterX_global_px,直接写死字段名会报 KeyError。第二,距离矩阵是稠密的,十万级细胞直接算euclidean_distances会吃掉大量内存,必须做稀疏化或分块。第三,Leiden 的resolution_parameter不调,默认结果要么太粗要么太碎,亚群注释根本对不上。下面我把这三件事全部落到可复制的脚本里。

2. TaoToken 统一 Key 前置:一次配置跑通三类平台的下游脚本

在进入脚本之前,先把调用通道统一掉。我做空间组学分析时经常需要在不同机器、不同 notebook 之间切换,如果每个环境都单独配一套鉴权,脚本迁移一次就要改一次配置,非常容易出错。TaoToken 的作用就是提供一个统一的 Key 通道,让 Xenium、CODEX、CosMx 的下游脚本共用同一套接入参数,模型对话、coding-plan、console 和 api-keys 都在同一个控制台里管理。

你需要先拿到三件套:Base URL、API Key、Model ID。Base URL 固定用https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 OpenAI 兼容接口的根路径。API Key 在控制台的 api-keys 页面生成,生成后只显示一次,建议立刻写进环境变量而不是硬编码进脚本。Model ID 根据你的任务选,纯脚本生成和参数调优用 coding-plan 通道更划算,需要边写边问的交互式调试用模型对话通道。

配置方式我推荐用环境变量加配置文件双保险。环境变量负责运行时注入,配置文件负责版本管理。下面这段是.env风格的写法,你可以直接复制:

export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_API_KEY="sk-你的实际Key" export TAOTOKEN_MODEL_ID="你的ModelID"

如果你用的是 Claude Code 或者类似的 CLI 工具,配置文件通常放在~/.claude/settings.json或项目根目录的settings.json,结构如下:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的实际Key", "ANTHROPIC_MODEL": "你的ModelID" } }

注意这里 Base URL、Key、Model ID 三件套必须同时出现,缺任何一个都会在请求阶段报鉴权或模型不存在。如果你用的是 Codex 系的工具,配置文件在~/.codex/auth.json,字段名换成base_url、api_key、model即可,值完全一致。Cline 或 MCP 场景下,把同样的三件套填进 MCP server 的 env 段,不要直连生产数据库,只让它读写本地 h5ad 文件。

配好之后先做一次最小验证,确认通道是通的:

curl -s https://taotoken.net/api/v1/models \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" | head -c 300

返回 JSON 里能看到模型列表就说明 Key 和 Base URL 没问题。这一步不要跳过,后面脚本报错时你才能确定是通道问题还是代码问题。接入文档在 doc 页面有完整的字段说明,遇到 401 先回去核对 Key 有没有多余空格。

3. 可复制配置:邻域矩阵生成与 Leiden 聚类完整脚本

现在进入核心部分。下面这个脚本我按 Xenium 的字段名写,同时给出 CODEX 和 CosMx 的字段映射,你只需要改COORD_FIELDS这一个字典就能切换平台。脚本包含坐标提取、稀疏邻域矩阵构建、igraph 建图、Leiden 聚类、PCA/UMAP 降维、空间可视化、结果保存七个步骤,全部可复制运行。

import argparse import numpy as np import pandas as pd import scanpy as sc import igraph as ig import leidenalg import matplotlib.pyplot as plt from scipy.spatial import cKDTree # 三类平台的坐标字段映射,按需切换 COORD_FIELDS = { "xenium": ("x_centroid", "y_centroid"), "codex": ("x", "y"), "cosmx": ("CenterX_global_px", "CenterY_global_px"), } def build_neighbor_graph(coords, distance_threshold=200.0): """用 KDTree 构建稀疏邻域,避免稠密距离矩阵爆内存""" tree = cKDTree(coords) pairs = tree.query_pairs(r=distance_threshold, output_type="ndarray") if pairs.size == 0: raise ValueError("邻域为空,请检查坐标单位是否为 µm") n = coords.shape[0] edges = pairs.tolist() graph = ig.Graph(n=n, edges=edges, directed=False) return graph def leiden_clustering( data_path, output_path, platform="xenium", distance_threshold=200.0, resolution=1.0, n_pca_components=50, n_neighbors_umap=15, ): adata = sc.read(data_path) print(f"Loaded {adata.n_obs} cells x {adata.n_vars} features") xcol, ycol = COORD_FIELDS[platform] if xcol not in adata.obs.columns: raise KeyError(f"字段 {xcol} 不存在,请检查 platform 参数") coords = adata.obs[[xcol, ycol]].to_numpy(dtype=float) graph = build_neighbor_graph(coords, distance_threshold) print(f"Graph: {graph.vcount()} nodes, {graph.ecount()} edges") partition = leidenalg.find_partition( graph, leidenalg.RBConfigurationVertexPartition, resolution_parameter=resolution, seed=42, ) adata.obs["leiden"] = pd.Categorical( [str(x) for x in partition.membership] ) print(f"Clusters: {adata.obs['leiden'].nunique()}") sc.tl.pca(adata, svd_solver="arpack", n_comps=n_pca_components) sc.tl.umap(adata, n_neighbors=n_neighbors_umap, min_dist=0.1) sc.pl.umap(adata, color="leiden", title="Leiden on UMAP", show=False) plt.savefig(output_path.replace(".h5ad", "_umap.png"), dpi=150, bbox_inches="tight") plt.close() sc.pl.spatial(adata, color="leiden", title="Leiden on Spatial", show=False) plt.savefig(output_path.replace(".h5ad", "_spatial.png"), dpi=150, bbox_inches="tight") plt.close() adata.write(output_path) print(f"Saved to {output_path}") return adata if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--data", required=True) parser.add_argument("--output", required=True) parser.add_argument("--platform", default="xenium", choices=["xenium", "codex", "cosmx"]) parser.add_argument("--distance_threshold", type=float, default=200.0) parser.add_argument("--resolution", type=float, default=1.0) parser.add_argument("--n_pca_components", type=int, default=50) parser.add_argument("--n_neighbors_umap", type=int, default=15) args = parser.parse_args() leiden_clustering( data_path=args.data, output_path=args.output, platform=args.platform, distance_threshold=args.distance_threshold, resolution=args.resolution, n_pca_components=args.n_pca_components, n_neighbors_umap=args.n_neighbors_umap, )

关键改动说明。第一,我用cKDTree.query_pairs替代了原来的euclidean_distances全矩阵计算,十万细胞从几十 GB 内存降到几百 MB,这是能跑通大切片的前提。第二,Leiden 用RBConfigurationVertexPartition而不是ModularityVertexPartition,因为前者支持resolution_parameter,你能通过调参控制亚群粒度。第三,聚类标签强制转成字符串再存进pd.Categorical,避免下游画图时把类别当连续值处理。

运行命令按平台切换:

python leiden_clustering.py \ --data xenium_sample.h5ad \ --output xenium_leiden.h5ad \ --platform xenium \ --distance_threshold 200 \ --resolution 1.0

CODEX 数据把--platform换成codex,CosMx 换成cosmx,其余参数不变。如果你的坐标单位是像素而不是 µm,需要先除以像素物理尺寸再传入,否则 200 这个阈值没有意义。

4. 验证请求与成功结果:怎么确认聚类真的对上了

脚本跑完不等于结果可信。我一般做三层验证,缺一层都不敢往下做注释。

第一层看日志数字。正常输出应该类似Loaded 85000 cells x 300 features、Graph: 85000 nodes, 4200000 edges、Clusters: 12。如果 edges 数量是 0,说明距离阈值相对坐标单位太小;如果 clusters 数量等于细胞数,说明 resolution 太高或者图几乎是空的。这两个极端都要回去调参。

第二层看 UMAP 图。打开生成的_umap.png,健康的聚类结果应该是色块分明、边界清晰,没有大量细胞混在同一个区域却分属不同颜色。如果 UMAP 上颜色完全随机打散,通常是 PCA 之前没有做标准化或高变基因筛选,导致降维空间没有结构。

第三层看空间图。打开_spatial.png,这是最关键的一步。空间转录组的聚类必须和切片上的解剖结构对应,比如肿瘤区域、基质区域、免疫浸润带应该各自成块。如果空间图上颜色像撒胡椒面一样均匀分布,说明邻域矩阵没有真正捕捉到空间关系,大概率是坐标字段取错了,比如把像素坐标当成了 µm 坐标。

验证通过后,用下面这段代码快速检查聚类标签的分布和保存状态:

import scanpy as sc adata = sc.read("xenium_leiden.h5ad") print(adata.obs["leiden"].value_counts()) print(adata.obs[["leiden"]].head()) print("spatial" in adata.obsm)

value_counts()能看出每个亚群的细胞数,如果某个亚群只有个位数细胞,基本是噪声,可以在注释阶段合并。obsm里应该包含spatial键,这是sc.pl.spatial能画图的前提。如果缺这个键,说明读入的 h5ad 本身没有空间坐标矩阵,需要从原始数据重新导出。

模型对话通道在这里的用法是:把报错信息或异常分布截图贴进去,让它帮你判断是参数问题还是数据问题。比如你看到 clusters 数量异常多,可以直接问「Leiden resolution 1.0 在 8 万细胞上产生 60 个 cluster 正常吗」,它会结合图规模和分辨率给出调参建议。这比自己翻文档快很多。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

这一节按真实报错逐条对照。我把接入阶段和运行阶段的问题分开列,方便你定位。

401 Unauthorized 是最常见的接入错误。表现是 curl 或脚本请求直接返回 401,原因通常是 Key 没生效、Key 有多余空格、或者 Base URL 写成了带路径的形式。检查顺序:先确认TAOTOKEN_API_KEY环境变量在当前 shell 里能echo出来,再确认 Base URL 是https://taotoken.net/api而不是/api/v1或带 UTM 的地址。如果用的是 settings.json,注意 JSON 里不能有注释和尾逗号,否则解析失败会退化成空 Key。

local proxy failed 通常出现在 CLI 工具或 MCP 场景。这个报错的意思是本地转发层没起来,不是远端服务的问题。排查动作:确认没有其他进程占用同一端口,确认配置文件里的 Base URL 和 Key 三件套完整,确认工具版本支持当前配置格式。如果你在 Cline 或 MCP 里看到这个错,把 MCP server 的 env 段单独拎出来,用 curl 手动请求一次,能通就说明是工具侧配置问题。

reading choices 报错一般出现在解析模型返回时。表现是脚本拿到响应但解析失败,日志里出现reading 'choices'或undefined is not an object。原因是返回结构不是标准的 OpenAI 格式,或者请求被中间层拦截返回了 HTML。解决方式是先打印原始响应体,确认choices字段存在。如果返回的是错误页,回去检查 Base URL 是否被改写。

OAuth 相关报错出现在用账号体系登录的工具里。如果你用的是 API Key 模式,不应该触发 OAuth 流程。看到 OAuth 报错说明工具被配置成了账号登录模式,需要切回 Key 模式,把三件套填进对应字段。Claude Code 场景下确认ANTHROPIC_API_KEY和ANTHROPIC_BASE_URL同时存在,Codex 场景下确认auth.json里api_key字段非空。

运行阶段的错误还有两类。一是KeyError: 'x_centroid',说明 platform 参数和实际数据不匹配,对照第 3 节的COORD_FIELDS字典改。二是ValueError: 邻域为空,说明距离阈值小于最近邻距离,把--distance_threshold调大或者检查坐标单位。这两类错误都不会消耗模型额度,属于纯本地问题,改完直接重跑即可。

6. 亚群注释与后续分析:把 Leiden 标签变成生物学结论

聚类只是中间产物,真正要交付的是亚群注释。拿到adata.obs["leiden"]之后,我通常做三件事:算每个 cluster 的标记基因或标记蛋白、对照空间位置做区域命名、把注释结果写回 obs 供下游使用。

标记基因计算用 scanpy 一行搞定:

sc.tl.rank_genes_groups(adata, "leiden", method="wilcoxon") sc.pl.rank_genes_groups(adata, n_genes=10, sharey=False, show=False) plt.savefig("marker_genes.png", dpi=150, bbox_inches="tight")

CODEX 数据没有基因表达,改成对蛋白通道做同样的组间比较,把adata.X换成蛋白强度矩阵即可。CosMx 如果同时有转录本和蛋白,可以分别算一遍再交叉验证。

区域命名要结合空间图。比如某个 cluster 在空间上集中在肿瘤巢内部,标记基因又是上皮来源,就命名为 tumor core;另一个 cluster 分布在肿瘤边缘且高表达免疫检查点,就命名为 immune margin。这一步没有固定公式,靠标记加空间位置双重证据。

注释写回 obs 的格式建议用字符串而不是数字:

cluster_map = {"0": "tumor_core", "1": "stroma", "2": "immune_margin"} adata.obs["cell_type"] = adata.obs["leiden"].map(cluster_map).astype("category") adata.write("xenium_annotated.h5ad")

后续做细胞通讯或邻域富集时,直接按cell_type分组,比用数字标签可读性高得多。如果你要长期迭代这套流程,把脚本和参数配置一起放进版本管理,每次调参记录 resolution 和 distance_threshold 的组合,方便回溯哪个参数组合产出了最合理的亚群结构。Coding Plan 通道适合这种长期迭代场景,把脚本骨架和参数说明交给它,让它帮你生成参数扫描的批量任务,比手动改命令行高效。

最后提醒一点,三类平台的坐标单位一定要在脚本开头统一。Xenium 默认 µm,CODEX 常见像素,CosMx 两种都有。单位不统一,200 µm 这个阈值在三个数据集上就是三个不同的物理尺度,聚类结果没法横向比较。把单位换算写成脚本里的一个函数,比每次手动改数字可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询