4. 源数据接入与解析自动化

临床研究报告的数据来源天然是异构的:EDC系统导出的是带注释的SAS数据集或CSV,中心实验室回传的是固定宽度的文本或Excel,PK/PD分析方给的是XPT传输文件,外部安全数据库可能只提供JSON接口,而研究者发起的问卷又常常是手工整理的Excel。源数据接入与解析自动化的目标,就是把这些形态各异的输入统一收敛为一份结构一致、类型正确、可追溯的中间数据集,让后续的SDTM映射和统计分析不必反复处理格式差异。

一、先建立“接入契约”,再写解析代码

接入自动化的第一步不是打开文件,而是为每个数据源定义一份接入契约。契约至少包含六项:源系统标识、传输方式(SFTP共享目录/API/邮件附件/对象存储)、文件命名规则、格式与编码、期望的列结构与类型、以及到达时间窗口。例如某中心实验室每周三凌晨通过SFTP推送 LAB_<STUDY>_<YYYYMMDD>.csv,UTF-8编码,含 USUBJID、VISIT、LBTESTCD、LBORRES、LBORRESU、LBDTC 等列,其中 LBDTC 为ISO 8601日期时间。把这份契约写成机器可读的配置(YAML或JSON),解析器就能按配置驱动,而不是为每个源硬编码一套逻辑。

契约中特别要明确三件事:一是主键与去重规则,比如实验室数据以 USUBJID + VISIT + LBTESTCD + LBDTC 为唯一键,重复到达时以最新文件覆盖;二是缺失值表示,SAS的 .、CSV的空字符串、JSON的 null 必须统一映射为同一缺失标记;三是日期与数值的解析约定,避免 01/02/2024 被误读为1月2日还是2月1日。

二、按格式选择解析策略

不同格式的解析难点差异很大,下面这张图概括了五类常见格式的读取要点与统一收敛路径。

五类源格式的解析要点与统一收敛 SAS7BDAT 标签/格式/长度 用 pyreadstat 读取 XPT (V5/V8) 传输格式,无标签 注意字符截断 CSV / TXT 分隔符/引号/编码 固定宽度需切片 Excel 多Sheet/合并单元格 日期序列号陷阱 JSON 嵌套/数组展开 键名大小写不一 统一解析层:类型推断 → 缺失归一 → 日期标准化 → 列名对齐 输出:Pandas DataFrame + 解析日志(行数、列数、异常行) 校验层:主键唯一性 · 受试者存在性 · 日期范围 · 单位合法性 不通过 → 隔离到 quarantine 目录并告警;通过 → 进入中间数据集 中间数据集(Parquet)+ 数据血缘记录 供 SDTM 映射与统计分析消费 解析日志必含 • 源文件名与哈希 • 读取行数 / 列数 • 类型转换失败列 • 缺失值统计 • 解析耗时与版本 常见陷阱 • Excel日期变数字 • XPT字符被截断 • CSV千分位逗号 • JSON键名不一致 • 编码GBK乱码

以SAS7BDAT为例,它保留了变量标签、格式(如 DATE9.)和长度信息,用 pyreadstat.read_sas7bdat() 读取后可通过 meta 对象拿到列标签,这对后续映射到SDTM的变量标签很有价值。XPT是SAS的传输格式,V5版本对字符变量有200字节上限,长文本(如不良事件描述)可能被静默截断,解析后应检查最大长度是否触及上限。CSV的坑集中在分隔符与引号:当字段内含逗号时必须有引号包裹,若源文件用分号分隔则需显式指定 sep=';'。Excel要警惕日期被存为序列号(如 45292 实为2024-01-01),以及多Sheet中只有第一个Sheet是数据、其余是说明页。JSON常见于外部安全数据库,往往嵌套 subject 对象和 events 数组,需要 json_normalize 展开并统一键名大小写。

三、把解析写成可复现的管道

解析代码不应是一次性脚本,而应组织为“读取器 + 规范化 + 校验”三段式管道。读取器只负责把文件变成原始DataFrame;规范化负责列名对齐(统一大写、去空格、替换非法字符)、类型转换和缺失归一;校验负责业务规则。下面是一个可参考的配置片段,用YAML描述一个实验室CSV源的接入契约:

source_id: CENTRAL_LAB_A
transport: sftp
path: /upload/lab/
filename_pattern: "LAB_<STUDY>_<YYYYMMDD>.csv"
encoding: utf-8
delimiter: ","
key: [USUBJID, VISIT, LBTESTCD, LBDTC]
columns:
  USUBJID:   {type: string, required: true}
  VISIT:     {type: string, required: true}
  LBTESTCD:  {type: string, required: true}
  LBORRES:   {type: string}
  LBORRESU:  {type: string}
  LBDTC:     {type: datetime, format: "%Y-%m-%dT%H:%M:%S"}
missing_values: ["", ".", "NA", "NULL"]

解析器读取该配置后,按 columns 逐列转换类型,遇到无法转换的值记录到日志并置为缺失,而不是让整批数据失败。校验阶段先检查主键是否唯一,再检查 USUBJID 是否存在于受试者主表,最后检查 LBDTC 是否落在知情同意日期之后、数据截止日期之前。任何一条不通过,该文件整体进入隔离目录并触发告警,人工确认后再决定是否放行。

四、结果如何判断与追溯

一次成功的接入应当能回答四个问题:读了多少行、丢了多少行、哪些列发生了类型转换、数据从哪个文件来。建议在中间数据集旁维护一张接入清单,字段包括 source_id、file_name、file_hash、rows_in、rows_out、rows_quarantined、parsed_at、parser_version。当后续统计分析发现某受试者的实验室值异常时,可以凭 file_hash 回溯到原始文件,确认是源数据问题还是解析问题。这种可追溯性是临床数据自动化区别于普通ETL的关键要求。

对于首次接入的新数据源,建议先用一个受试者的小样本文件跑通全流程,确认列名、类型、日期格式和缺失表示都符合预期后,再放开全量自动调度。这样能把格式问题挡在批量处理之前。
不要用“能读进来”作为解析成功的标准。SAS的 . 被读成字符串、Excel日期被读成整数、XPT长文本被截断,这些都不会报错,但会污染下游分析。类型与长度校验必须显式执行。