3. 研究方案到数据采集表的映射

临床研究方案通常以自然语言描述“要收集什么”,而Python队列工程需要的是“机器能校验、能落库、能追溯”的结构化定义。两者之间的桥梁,就是数据采集规范(Data Collection Specification)。它不是一张简单的CRF复印件,而是把CRF字段、变量定义、编码规则、多中心差异和版本信息统一收敛为可执行对象的过程。本节的核心任务是:建立从方案到采集规范的映射方法,并让这套规范在Python中真正可运行。

一、映射的三层结构:字段、变量、规则

一个CRF页面上的“性别”输入框,在工程视角下至少对应三层信息。第一层是采集字段:它在表单上的标签、控件类型(单选/文本/日期)、是否必填、所在访视。第二层是变量定义:它在数据库中的变量名(如 sex)、数据类型(枚举/整数/日期)、允许值集合、缺失值编码。第三层是编码规则:原始选项如何转换为分析用编码,例如“男→1、女→2、未知→9”,以及是否允许“未填”与“未知”并存。

很多项目失败的原因,是把这三层混在一张Excel里:字段名用中文、变量名用拼音、编码规则写在批注中。正确做法是让每一层都有明确归属,并用一个稳定的字段ID贯穿始终。字段ID建议采用“表单缩写_序号”的形式,如 DM_001,它不随标签文字变化而变化,是版本控制和多中心合并的锚点。

二、用Python数据结构承载采集规范

采集规范最直接的落地形式,是一个可被程序读取的字典或数据类。下面给出一个最小但完整的示例,覆盖字段、变量、编码和校验四类信息:

from dataclasses import dataclass, field
from typing import Literal

@dataclass
class FieldSpec:
    field_id: str                 # 稳定锚点,如 DM_001
    label: str                    # 表单显示标签
    var_name: str                 # 数据库变量名
    dtype: Literal["enum", "int", "float", "date", "str"]
    required: bool
    allowed: list | None = None   # 枚举允许值
    code_map: dict | None = None  # 原始选项 -> 分析编码
    missing_code: int | None = None

sex_spec = FieldSpec(
    field_id="DM_001",
    label="性别",
    var_name="sex",
    dtype="enum",
    required=True,
    allowed=["男", "女", "未知"],
    code_map={"男": 1, "女": 2, "未知": 9},
    missing_code=9,
)

这段代码的价值在于:字段ID、变量名、编码映射三者被绑定在同一个对象上。后续无论是生成录入界面、写校验函数,还是做多中心合并,都围绕这个对象展开,而不是在各处重复硬编码。

三、多中心表单差异的处理策略

多中心研究中最常见的问题是:A中心用“1=男、2=女”,B中心用“M=男、F=女”,C中心直接填中文。如果每个中心各写一套解析逻辑,维护成本会迅速失控。更稳妥的做法是中心适配层 + 统一规范层:各中心保留自己的原始表单和原始编码,适配层负责把原始值映射到统一规范中的标准编码。

适配层可以用一个“中心编码字典”表达:

center_code_maps = {
    "A": {"1": "男", "2": "女", "9": "未知"},
    "B": {"M": "男", "F": "女", "U": "未知"},
    "C": {"男": "男", "女": "女", "未知": "未知"},
}

def normalize_sex(raw_value, center):
    mapped = center_code_maps[center].get(str(raw_value))
    if mapped is None:
        raise ValueError(f"中心{center}出现未定义性别编码: {raw_value}")
    return sex_spec.code_map[mapped]

这样,统一规范层只认“男/女/未知”,各中心的差异被隔离在适配层。新增中心时只需增加一条映射,不需要改动下游分析代码。需要强调的是,适配层必须对未定义编码显式报错,而不是静默转为缺失,否则会掩盖数据质量问题。

四、版本控制:让规范可追溯、可回滚

采集规范会随方案修订而变化:新增字段、修改选项、调整必填性。如果没有版本控制,历史数据与当前规范就会错位。建议为规范引入语义化版本号(如 v1.2.0),并遵循三条规则:新增可选字段升次版本号;修改编码映射或必填性升主版本号;仅修正文字标签升修订号。

每次变更都应记录变更日志,至少包含:版本号、生效日期、变更字段ID、变更前后内容、影响的数据表。在Python中,可以把版本信息写入规范对象的元数据,并在数据入库时一并保存 spec_version,这样任何一条记录都能追溯到它当时遵循的规范版本。

实践建议:把采集规范存为版本化的JSON或YAML文件,纳入Git管理。每次修改通过Pull Request评审,避免“口头改字段”导致的数据断层。

五、映射关系的整体视图

下图展示了从研究方案到可执行采集规范的完整映射链路,以及多中心适配层和版本控制所处的位置。图中左侧是方案与CRF来源,中部是规范的三层结构,右侧是Python可执行对象与落库结果,底部是贯穿始终的版本线。

研究方案 目标、访视、指标 CRF 表单 字段、控件、选项 编码规则 选项→分析编码 采集规范(三层) ① 采集字段层 field_id / label / 控件 / 必填 ② 变量定义层 var_name / dtype / 缺失编码 ③ 编码规则层 code_map / 允许值 / 校验 Python 可执行对象 FieldSpec 数据类 normalize_sex() 适配函数 validate_record() 校验器 spec_version 元数据 多中心适配层 中心A: 1/2/9 → 男/女/未知 中心B: M/F/U → 男/女/未知 中心C: 中文直填 → 标准化 版本控制线:v1.0.0 → v1.1.0 → v2.0.0 新增可选字段升次版本 · 改编码/必填升主版本 · 仅改标签升修订号 落库记录 含 spec_version 可追溯

图中可以看到,版本控制不是附加项,而是贯穿来源、规范、适配和执行的主线。任何一次字段或编码变更,都应同时更新规范对象、适配映射和版本号,三者缺一不可。

六、落地检查要点

  • 字段ID唯一且稳定:不随标签文字变化,作为多中心合并和版本对比的锚点。
  • 编码映射显式化:所有选项到分析编码的转换必须写在 code_map 中,禁止在分析脚本里临时判断。
  • 缺失值语义明确:区分“未填”“未知”“不适用”,并分别赋予不同编码,避免一律填0或空字符串。
  • 适配层报错而非静默:遇到未定义编码时抛出异常,保留原始值以便回溯。
  • 版本随数据落库:每条记录保存其遵循的 spec_version,使历史数据在规范升级后仍可解释。

把研究方案转化为可执行采集规范,本质上是把“人读的规则”翻译成“机器可校验的对象”。字段ID、变量定义、编码映射和版本号四者绑定后,多中心差异被隔离在适配层,下游分析只面对统一、可追溯的数据。这样,后续的清洗、质控和统计才能建立在稳定可靠的基础之上。