第四章 样本类型分类:组织、血液、细胞系、微生物
样本类型,说白了就是元数据的第一道分水岭。我刚开始做生物信息平台时,觉得把所有样本塞进同一张表里挺省事。结果?下游分析的人跑来问我:“这个字段是给血液样本用的,你填到组织样本里,让我怎么解析?”从那以后,我学乖了——分类先行。
4.1 为什么要在元数据里先定类型?
你想想看,一份血液样本和一份石蜡包埋组织,它们需要记录的字段能一样吗?血液有血浆体积、抗凝剂类型;组织有固定方式、切片厚度;细胞系有传代数、支原体检测结果;微生物有菌株编号、培养条件。如果不在约束里区分开,后续的校验、存储、查询都会乱套。我个人习惯在元数据 Schema 的顶层放一个 sample_type 枚举字段,所有下游校验都依赖这个值做分支。
sample_type 必须出现在每一个样本对象的根路径,且只能从预定义枚举中取值。
4.2 四种常见样本类型
虽然实际项目中会遇到痰液、尿液、脑脊液等等,但最基础的四个大类必须掌握:
| 类型 | 典型标识 | 关键元数据字段举例 | 常见来源 |
|---|---|---|---|
| 组织 (Tissue) | TIS- | 固定方法、蜡块/冰冻、HE 切片编号 | 手术切除、活检 |
| 血液 (Blood) | BLD- | 抗凝剂、离心条件、血浆/血清体积 | 静脉穿刺 |
| 细胞系 (Cell Line) | CL- | 传代数、支原体结果、培养基配方 | ATCC、实验室培养 |
| 微生物 (Microbe) | MIC- | 菌株编号、培养温度、抗生素抗性 | 环境分离、临床分离 |
这里有个坑:我曾经见过有人把“细胞系”写成“Cell_Line”,另一个文件里又写成“cellline”。所以枚举值一定要严格统一大小写和分隔符。我在项目里强制要求全大写 + 下划线,比如 TISSUE、BLOOD、CELL_LINE、MICROBE。
4.3 类型决定字段约束
每种类型都对应一个独立的字段集合。我们用 JSON Schema 的 if-then-else 来实现动态校验。下面是一个简化的示例:
{
"type": "object",
"properties": {
"sample_type": { "enum": ["TISSUE", "BLOOD", "CELL_LINE", "MICROBE"] },
"id": { "type": "string" }
},
"if": { "properties": { "sample_type": { "const": "TISSUE" } } },
"then": {
"required": ["fixation_method", "block_type", "tumor_percent"],
"properties": {
"fixation_method": { "type": "string", "enum": ["FFPE", "FreshFrozen", "OCT"] },
"block_type": { "type": "string", "enum": ["paraffin", "frozen", "TMA"] },
"tumor_percent": { "type": "number", "minimum": 0, "maximum": 100 }
}
},
"if": { "properties": { "sample_type": { "const": "BLOOD" } } },
"then": {
"required": ["anticoagulant", "collection_tube", "volume_ml"],
"properties": {
"anticoagulant": { "enum": ["EDTA", "Heparin", "Citrate", "None"] },
"collection_tube": { "type": "string" },
"volume_ml": { "type": "number", "minimum": 0 }
}
}
}
实际项目中我还会加上 if 条件处理细胞系和微生物。这种写法虽然啰嗦,但可读性强,下游工具(比如 ajv、jsonschema)都能直接支持。注意:如果枚举类型很多,建议把每个类型的 Schema 拆分到独立文件,然后在主文件中用 $ref 引用。但那个是后话了。
pydantic 的 discriminator 字段也可以实现类似效果,而且代码更简洁。不过对于纯元数据交换场景,JSON Schema 仍然是约定通行标准。
4.4 样本类型框架图
下面这张图展示了我通常给团队画的分类与字段依赖关系。它不是一个流程,而是四种类型各自的核心字段域。
为什么每个类型我只列了三个字段?因为实际项目中,光组织就有十几个字段。但核心是让你看到——每个类型都有自己独特的“必须字段”,这些字段在其他类型里根本不该出现。我见过有人把“传代数”写到血液样本里,结果下游质控直接报错。
4.5 实际项目中的避坑指南
我曾经在一个跨中心项目中负责元数据整合。三个中心各自用了不同的分类法:A 中心用“组织/血液/其他”,B 中心用“FFPE/冰冻/血液”,C 中心直接写中文“肿瘤组织”。最后统一时,光映射表就写了 60 行。所以我的建议很直接:
- 从一开始就用枚举值,不要用自由文本字段存类型。
- 枚举值全部大写 + 下划线,例如
WHOLE_BLOOD、PBMC、FORMALIN_FIXED_PARAFFIN_EMBEDDED。别偷懒,后面会省很多事。 - 版本管理:类型列表会增长。我在项目里会把枚举定义放在独立的
sample_type_enum.json文件中,Schema 里用$ref引用,方便后续扩展。 - 预留“其他”类型:永远会有你没想到的样本类型。加一个
OTHER枚举,并强制要求填写other_type_description字段。
anyOf 去定义不同类型的字段合集。那会让校验变得极其复杂且难以调试。老老实实用 if-then-else 或 discriminator。
4.6 一个实战案例:细胞系与组织的混淆
去年有个合作团队发来一批数据,元数据里 sample_type 写的是 Tissue,但字段里却出现了 passage_number(传代数)。显然他们拿细胞系样本套用了组织模板。为什么会这样?因为他们用的是同一个 CSV 模板,只是在备注里注明了类型。人填的时候就容易串。
解决办法:在录入界面做动态表单,选“组织”时只显示组织字段,选“细胞系”时只显示细胞系字段。如果只能用静态模板,至少用条件格式把不相关的列着色警告。我在后端还加了一层校验:如果传类型和字段列表不匹配,直接拒绝入库并返回具体的违规字段名。
# 伪代码:类型-字段合规检查
def validate_sample(data):
type_fields_map = {
"TISSUE": ["fixation_method", "block_type", "tumor_percent"],
"BLOOD": ["anticoagulant", "collection_tube", "volume_ml"],
"CELL_LINE": ["passage_number", "mycoplasma_result", "medium"],
"MICROBE": ["strain_id", "culture_temp", "antibiotic_resistance"]
}
sample_type = data.get("sample_type")
allowed = set(type_fields_map.get(sample_type, []))
keys = set(data.keys()) - {"id", "sample_type", "project_id"}
extra = keys - allowed
if extra:
raise ValueError(f"类型 '{sample_type}' 不允许以下字段: {extra}")
这段代码很简单,但在实际生产环境中跑了两年,拦截了上百个不合规的提交。你想想看,如果没有这个类型分类约束,下游的分析流程会收到多少脏数据?
4.7 小结
样本类型分类是元数据约定的基石。我个人习惯在项目一开始就和生信团队、实验室管理员一起敲定 sample_type 的枚举列表,并且白纸黑字写在《元数据格式约定文档》里。后续每一次新增类型都要走变更评审——这不是官僚,而是为了避免凌晨两点接到“为什么我的数据跑不出来”的求救电话。