一、问题背景
在使用 SCENIC+ (pycisTopic) 的 export_pseudobulk() 函数,按细胞类型拆分 ATAC fragment 文件并准备进行 consensus peak calling 时,常常遭遇一系列难以排查的“幽灵”错误。
主要表现为:明明输入的数据没有任何问题,程序却频繁提示临时文件不存在,或者在刚创建好临时文件准备写入时直接崩溃闪退。根本原因是底层的 scatac_fragment_tools 解析器对输入文件的格式、字典匹配以及 DataFrame 的索引有着极其严苛(且缺乏友好报错提示)的硬编码限制。
二、报错流程与解决方案
1. split_pattern 与 DataFrame 索引的隐蔽冲突
报错信息
ValueError: Fragment file /data/xujun/.../tmp/F32/F32_Cardiomyocyte.fragments.tsv.gz does not exist.
原因
export_pseudobulk 默认不读取你数据框里的某一列,而是死磕 cell_data 的行索引 (index) 去和 Fragment 文件里的 Barcode 进行比对。
在单细胞多样本合并时,为了防重叠,我们的 index 通常会带有样本前/后缀(如 AAACGAACAAGCGCTG-1_F32)。虽然官方提供了 split_pattern 参数用来切割前缀,但这玩意儿的底层逻辑有很大缺陷:
当设置 split_pattern="_" 时,底层 Rust 代码往往强制默认你的格式是 样本_Barcode,从而去提取下划线后面的部分。如果你的 index 是 Barcode_样本,它提取出来的其实是 F32 这个无用字符串,拿着 F32 去比对,自然匹配数为 0,最终导致生成空文件并报错。
解决方案
放弃让底层瞎猜,直接在 Python 层面用干净的 barcode 列替换掉 index,并彻底关闭 split_pattern 参数,实现 100% 精确匹配:
# 1. 复制数据,用干净且不带样本前后缀的 barcode 列替换原始的 index
cell_data_for_pb = cell_data.copy()
cell_data_for_pb.index = cell_data_for_pb['barcode']
# 2. 运行导出时关闭 split_pattern
bw_paths, bed_paths = export_pseudobulk(
input_data=cell_data_for_pb,
# ... 其他常规参数 ...
split_pattern=None # 核心修改:改为None,拒绝底层切分逻辑
)
2. chromsizes 字典与染色体命名格式不一致
报错信息
(同上,也是报 Fragment file does not exist)
原因
排除了 Barcode 匹配失败的情况后,最容易被忽视的元凶是染色体命名格式。
如果输入的 Fragment 文件中的染色体命名没有前缀(例如 1),而传入的 chromsizes 字典的键带有前缀(例如 chr1),底层的 Rust 解析器会直接静默丢弃所有在字典中找不到对应键的行。几千万行数据读取完被全部丢弃,未能生成任何有效文件,最终再次导致外层 Python 脚本抛出“文件不存在”。
解决方案
在将字典传入 export_pseudobulk 之前,通过 Python 字典推导式,将 chromsizes 的键统一去掉 chr 前缀,强制与 Fragment 文件对齐:
# 将类似于 {'chr1': 248956422} 转换为 {'1': 248956422}
chromsizes_fixed = {str(k).replace('chr', ''): v for k, v in chromsizes.items()}
3. 底层 Rust 崩溃:Invalid number of fields
报错信息
thread '<unnamed>' panicked at src/aggregate_fragments.rs:80:18:
Invalid number of fields in fragment file!
pyo3_runtime.PanicException: Invalid number of fields in fragment file!
原因
经历了前两步,tmp 文件夹下终于成功创建了对应细胞类型的文件,但程序刚开始运行就崩溃了。
原因是部分非标准 10x 流程输出的 Fragment 文件包含了第 6 列(通常是正负链信息,如 + 或 -)。而 SCENIC+ 的底层解析器强制写死了列数检查,仅允许严格的 5 列格式(染色体、起点、终点、Barcode、read count)。只要读取到第 6 列,立马触发 PanicException 强制退出。
解决方案
回到 Linux 命令行,使用文本处理工具强行截断文件保留前 5 列,并重新压缩、建立 tabix 索引:
# 解压读取 -> 截取前 5 列 -> 重新用 bgzip 压缩成新文件
zcat /path/to/original/fragments.tsv.gz | cut -f 1-5 | bgzip > /path/to/fixed/fragments_5col.tsv.gz
# 为处理后的新文件建立 tabix 索引
tabix -p bed /path/to/fixed/fragments_5col.tsv.gz
最后,在 Python 脚本中更新 path_to_fragments 字典指向新的 _5col.tsv.gz 文件,即可顺利跑通整个流程。
三、排坑总结
[!IMPORTANT] SCENIC+ 的
scatac_fragment_tools模块为了追求极高的文本解析速度,大幅牺牲了容错率和报错的友好度。
- 少用 split_pattern:不要依赖底层的切分逻辑。永远在 Python 层把 index 处理成和 Fragment 文件里一模一样的纯 Barcode,然后设为
None。- 静默过滤陷阱:遇到匹配不上的条目(染色体名称对不上
chr1vs1),底层逻辑通通是直接丢弃该行而不抛出 Warning。排查does not exist报错时,优先核对两端数据的格式是否做到字面意义上的完全一致。- 文件结构红线:输入给流程的 Fragment 文件必须严格为 5 列,多一列都会导致底层直接 Panic 崩溃。遇到底层报错,直接查原文件的列数。
