ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CMID医学数据提取JSON数据文件:从宽表到嵌套结构的完整实践

CMID医学数据提取JSON数据文件:从宽表到嵌套结构的完整实践 简介临床医学信息CMID场景下的JSON数据文件面向医学科研人员、临床数据分析师及医院信息化开发者用于把患者基本信息、病史、检验结果、影像资料等多源医疗信息整理为结构化数据便于统计分析、系统集成与跨平台交换。压缩包内仅含1个JSON格式文件整体大小约1.05MB采用键值对和嵌套层级组织字段既可展示医学数据从HIS、LIS、PACS等系统抽取后的标准形态也能帮助理解字段命名、分组结构与数组表达方式这类文件体积小、可读性强适合作为电子病历研究、医学数据建模、算法验证和教学演示的参考样例。目前已有169人学习下载对于希望快速掌握JSON医疗数据解析、或需要可直接测试的轻量数据文件的学习者尤其实用借助这份数据可以直观了解医学信息数字化采集与存储的基本思路为后续科研或系统开发奠定基础。1. 先把CMID医学数据提取JSON数据文件这件事的边界说清楚做临床科研数据处理的人拿到“把CMID医学数据提取成JSON数据文件”这个需求时会发现它比想象中麻烦医院系统导出的往往是宽表一行里同时带着诊断、手术、检验的多个字段而下游要的JSON是按患者/就诊维度嵌套好的结构一个CMID下面挂一个诊断数组。CMID在不同机构叫法不一样可能是患者主索引、住院就诊号、病例号岗位角色都一样——它是把诊断表、手术表、检验表里同一患者的记录串起来的那把钥匙。这篇文章写给三类人要交付临床数据集的数据工程师、被科室领导要求“把系统数据导成JSON”的信息员、正在给医学大模型准备微调数据的算法工程师。下面按一条能直接照做的路径展开定位CMID再做JSON转换再批量落地最后讲清最容易踩的坑。2. 先定位CMID再谈JSON转换别被数据库的“导出JSON”按钮骗了很多数据库客户端自带“导出为JSON”点一下就能把查询结果变成JSON数组。但那个按钮做的是逐行转换不会把同一个CMID下的多条诊断合并成一个数组。所以要先把CMID定位清楚再设计后续转换逻辑顺序反了会白做一上午。2.1 确认哪张表里的哪个字段才是真正的CMID不要一上来就写关联查询。先把候选字段摆出来逐个判断粒度。常见字段包括patient_id、visit_id、admission_id、case_id、medical_record_no。它们长得都像主键但语义完全不一样patient_id是患者级一个患者多次住院会重复visit_id是就诊级一次住院一条case_id通常是病案首页编号可能只覆盖部分住院记录。如果你的JSON想表达“一次就诊包含多条诊断”CMID就应该选就诊级字段而不是患者级字段。我一般先跑一条SQL看空值率和唯一性-- PostgreSQL / MySQL 通用按实际表名替换 SELECT COUNT(*) AS total_rows, COUNT(DISTINCT cmid) AS unique_cmid, COUNT(*) - COUNT(DISTINCT cmid) AS duplicate_cmid_rows, SUM(CASE WHEN cmid IS NULL THEN 1 ELSE 0 END) AS null_cmid FROM his_inpatient_visit;逻辑说明total_rows是原始记录数unique_cmid是去重后的主标识数duplicate_cmid_rows若大于0说明一个CMID下面确实有多行记录后面必须做聚合null_cmid若过大说明该字段不是系统强制必填的主键不适合当CMID用。参数上注意cmid要按实际字段名替换表名也别用只读视图代替基表视图可能已经过滤掉部分记录会误导你对粒度判断。通常还需要一张小表来辅助判断候选字段的语义我习惯用下面这个方式来横向比较候选字段粒度一个患者多次就诊时是否重复适合做CMID吗patient_id患者级会重复不适合粒度太粗visit_id就诊级不重复适合admission_id住院流水号不重复适合case_id病案首页编号可能不连续看覆盖范围medical_record_no病历号患者级重复通常不适合如果拿不准可以随手查两个已知患者的就诊记录直接看同一个visit_id是否贯穿诊断表、手术表、检验表。贯穿得上它才是这把钥匙。2.2 最小SQL查询样本先把原始数据捞出来看全貌确认CMID后不要写大而全的导出脚本先用最小查询把原始数据的样子摸清楚。我通常会把就诊主表和诊断表先关联起来限定一个短时间范围取前200行SELECT v.visit_id AS cmid, v.patient_name, v.admission_date, v.discharge_date, d.diagnosis_code, d.diagnosis_name FROM his_inpatient_visit v LEFT JOIN his_diagnosis d ON v.visit_id d.visit_id WHERE v.admission_date 2025-01-01 AND v.admission_date 2025-04-01 ORDER BY v.visit_id, d.diagnosis_code LIMIT 200;逻辑说明用LEFT JOIN而不是INNER JOIN因为要保留没有诊断记录的就诊行后续JSON里诊断字段该是一个空数组而不是把整个CMID丢掉。ORDER BY v.visit_id, d.diagnosis_code让同一个CMID的行挨在一起肉眼检查聚合效果。LIMIT 200是为了快速看到数据形态避免一上来就全表扫描拖垮只读库。如果是SQL Server把LIMIT 200改成TOP 200即可。这一步能暴露出不少问题诊断名称里可能带逗号、换行同一患者可能有十几条诊断日期可能有空值。这些都会影响后面JSON转换的参数设置。2.3 用Python连接数据库并导出最初的中间表我不建议在数据库客户端里直接导出JSON因为之后要做嵌套、类型修正、脱敏这些操作在Python里更好控制。常见的做法是用pandas加SQLAlchemy把查询结果先落成一个CSV中间表import pandas as pd from sqlalchemy import create_engine engine create_engine( postgresqlpsycopg2://readonly:your_password10.0.0.5:5432/his_readonly ) query SELECT v.visit_id AS cmid, v.patient_name, v.admission_date, v.discharge_date, d.diagnosis_code, d.diagnosis_name FROM his_inpatient_visit v LEFT JOIN his_diagnosis d ON v.visit_id d.visit_id WHERE v.admission_date 2025-01-01 AND v.admission_date 2025-04-01 chunks [] for chunk in pd.read_sql_query(query, engine, chunksize5000): chunks.append(chunk) df pd.concat(chunks, ignore_indexTrue) df.to_csv( his_diagnosis_raw.csv, indexFalse, encodingutf-8-sig, quotechar, quoting1, )逻辑说明chunksize5000让数据库分批返回结果避免几百万行一次性塞进内存ignore_indexTrue在拼接分块后重新生成连续索引。encodingutf-8-sig是为了让CSV在Windows Excel里打开不乱码方便给别人先肉眼看一遍quoting1对应csv.QUOTE_ALL把所有字段都加引号防止诊断名称里的逗号把列拆错。参数说明quotechar是默认的引号字符显式写出来是为了让人一眼看出这里定了什么生产环境里连接串里的密码不要明文写在脚本里建议从环境变量或密钥管理服务读取。这一步导出的CSV只是中间产物不是最终交付文件。3. 把宽表重构成JSON嵌套结构从JSON数组构建到json python实操中间表是宽表一行是一条诊断记录最终交付的是嵌套JSON一个CMID对应一个患者对象。这个转换是整条流水线的核心。3.1 表驱动建模先画出你需要的JSON结构再写转换脚本在动手写代码之前先和下游确认JSON结构。我见过最省事的确认方式是让对方给出一个样例JSON然后照着样例写转换逻辑。临床场景下典型结构是这样的{ cmid: V202501010001, patient_name: 张三, admission_date: 2025-01-05, discharge_date: 2025-01-12, diagnoses: [ { code: I10, name: 原发性高血压 }, { code: E11, name: 2型糖尿病 } ], surgeries: [] }这个结构里的diagnoses是JSON数组宽表里同一患者的三行诊断记录会合并到这里。不要指望df.to_json(orientrecords)一步到位它只会把每行数据变成数组里独立的对象无法按CMID聚合。画结构时可以用一张字段映射表帮助确认每个字段去向宽表字段JSON路径说明visit_idcmid顶层主键patient_namepatient_name顶层患者基本信息admission_dateadmission_date统一为YYYY-MM-DDdiagnosis_codediagnoses[].code数组元素字段diagnosis_namediagnoses[].name数组元素字段如果下游还需要手术、检验、药品信息就继续扩展数组字段。结构定得越早后面改动越少。3.2 按CMID聚合生成嵌套JSON的Python脚本结构确认后用groupby按CMID聚合构建嵌套字典列表import json import pandas as pd df pd.read_csv(his_diagnosis_raw.csv, dtype{cmid: str}) records [] for cmid, group in df.groupby(cmid, sortFalse): diagnoses [] for row in group.to_dict(orientrecords): diagnoses.append({ code: row[diagnosis_code], name: row[diagnosis_name], }) first group.iloc[0] records.append({ cmid: cmid, patient_name: first[patient_name], admission_date: first[admission_date], discharge_date: first[discharge_date], diagnoses: diagnoses, }) with open(cmid_patient_diagnoses.json, w, encodingutf-8) as f: json.dump(records, f, ensure_asciiFalse, indent2)逻辑说明groupby(cmid, sortFalse)保持原始数据顺序不额外排序group.to_dict(orientrecords)把这一组里的每一行转成字典取diagnosis_code和diagnosis_name构造成诊断列表。first group.iloc[0]取该组第一条记录用来填充顶层字段因为一个患者的基本信息在同一组里是重复的。diagnoses在没有任何诊断记录时会是空列表不会导致后续下游解析报数组类型错误。参数说明ensure_asciiFalse很关键不加的话中文字段会被转写成\uXXXXJSON文件看起来像乱码人工检查非常痛苦很多线上数据又确实是中文诊断名。indent2是为了让文件可读如果你要交付的是几百MB的大文件建议去掉indent以减小体积否则换行和空格会白白占用存储。如果你还要把手术表、检验表一起嵌套进去思路相同但不要在主循环里反复对全表做df[df[cmid] cmid]这种过滤那样时间复杂度会接近O(N*M)。我一般先把手术、检验两个DataFrame各自按CMID分组生成映射字典surgery_map { cmid: rows.to_dict(orientrecords) for cmid, rows in df_surgery.groupby(cmid) }然后主循环里直接在records.append处取surgery_map.get(cmid, [])几百万行数据时这个差异非常明显。3.3 输出前用JSON Schema把字段类型钉死JSON转换脚本跑通后先别急着发出去。医学数据里的类型漂移很常见同名字段在不同来源里可能是字符串、整数、时间戳。用JSON Schema做一次导出校验能在交付前拦住绝大多数低级错误。from jsonschema import validate, ValidationError schema { type: object, properties: { cmid: {type: string}, patient_name: {type: string}, admission_date: {type: string}, diagnoses: { type: array, items: { type: object, properties: { code: {type: string}, name: {type: string} }, required: [code, name] } } }, required: [cmid, patient_name, diagnoses] } with open(cmid_patient_diagnoses.json, r, encodingutf-8) as f: data json.load(f) for idx, item in enumerate(data): try: validate(instanceitem, schemaschema) except ValidationError as e: print(f记录 {idx} 校验失败: {e.message}) raise逻辑说明required锁定了顶层必填字段items.properties锁定了诊断数组里每个对象的字段类型required: [code, name]保证数组里的对象不会缺字段。additionalProperties这里没有设置jsonschema默认允许未知字段如果你想严格禁止多余字段需要加上additionalProperties: False。参数说明这个Schema只是基础版实际项目里我会把日期字段的正则也加上例如pattern: ^\\d{4}-\\d{2}-\\d{2}$防止日期被序列化成2025-01-05T00:00:00.000Z这种带时间的格式。注意JSON文件很大时不要在Python里用json.load一次性读入可以改用逐行读取JSONL的方式校验内存占用会小很多。这里用离线的JSON Schema就能搞定不必去依赖在线JSON格式化工具做验证那些工具只适合看结构不适合做字段级校验。4. 批量落地为JSON数据文件分页、JSONL与文件组织的三个边界单次能跑通和能稳定跑完一百万条是两码事。数据量大之后最先出问题的往往是查询超时、内存耗尽和文件组织混乱。4.1 分页拉取 vs 一次性全量limit offset 与 keyset 分页上一章的脚本把中间表全部读进了内存对于一百万行起步的临床数据来说并不安全。更稳的常见做法是分页拉取并边读边处理。最简单的是LIMIT / OFFSET分页SELECT v.visit_id AS cmid, v.patient_name, d.diagnosis_code, d.diagnosis_name FROM his_inpatient_visit v LEFT JOIN his_diagnosis d ON v.visit_id d.visit_id ORDER BY v.visit_id LIMIT 5000 OFFSET 0;翻页时改OFFSET为5000、10000。这个写法简单但到深页码时会越来越慢因为数据库需要扫描并丢弃前面所有行。Offset到几十万时查询耗时会从几百毫秒涨到几十秒整条跑批链路都跟着变慢。高频场景下我更推荐keyset分页也叫seek分页用上一页最后一个CMID作为下一页起点SELECT v.visit_id AS cmid, v.patient_name, d.diagnosis_code, d.diagnosis_name FROM his_inpatient_visit v LEFT JOIN his_diagnosis d ON v.visit_id d.visit_id WHERE v.visit_id V202501019999 ORDER BY v.visit_id LIMIT 5000;参数说明每次查询都复用visit_id索引不需要跳过已读行数据量越大优势越明显。前提是CMID本身可比较排序像V202501010001这种固定前缀加流水号的格式就很好用。如果CMID是UUID或MD5这种无序字符串号无法语义化分页就得额外引入created_at等连续字段做游标。分页方案直接影响后续脚本结构。我通常把分页和转换放在同一个循环里每拉一页就处理一页避免把所有中间表都堆在磁盘上。4.2 用JSON Lines代替单一JSON数组增量导出的后悔药单个JSON文件里装一个巨型数组下游拿到手会非常痛苦json.load一次读入内存稍微大点就卡死想追加一条新记录几乎不可能只能重写整个文件。所以临床数据批量交付时我习惯直接用JSON Lines格式扩展名用.jsonl一行一个JSON对象with open(cmid_export.jsonl, w, encodingutf-8, newline\n) as f: for record in records: f.write(json.dumps(record, ensure_asciiFalse) \n)逻辑说明每写完一个对象就换行整个文件可以逐行读取内存占用稳定在一个对象的大小。增量导出时直接继续在文件尾部追加不用重写历史数据这一点在后面对比多批次数据时很有价值。参数说明newline\n在Windows下很重要否则Python默认会做换行符转换文件里可能混入\r\n下游split(\n)解析时每行末尾会残留\r。如果交付对象是Spark或HiveJSONL也是它们原生友好的格式不需要额外转换。只要没人明确要求必须交付单个JSON数组文件我会优先给JSONL这是给未来自己留的后悔药。4.3 文件命名与CMID映射方便你三个月后还能找到人批次文件最容易出现的坑是三个月后有人拿着一个JSON文件来问“这是哪批数据”而你完全看不出来。我一般按日期加批次命名并在同目录放一个批次说明文件文件内容cmid_export_20260801_batch_01.jsonl第一批导出的JSONL数据cmid_export_20260801_batch_01.schema.json对应的JSON Schemacmid_export_20260801_batch_01.query.sql抽取用SQLcmid_export_20260801_batch_01.manifest.json批次说明含记录数与时间范围批次说明文件里的内容不需要复杂能回答三个问题就行数据从哪张表来筛选条件是什么CMID用的是哪个字段。我用一个简单JSON做manifest字段和值都很直观{table: his_inpatient_visit, cmid_field: visit_id, time_range: 2025-01-01~2025-03-31, row_count: 123456}。这样即使原始表已经改了结构也能靠这个manifest把黑匣子重新打开。文件组织这件事没有高深技巧但它决定你要不要半夜被叫起来解释字段含义。5. 避坑CMID医学数据提取JSON文件时最容易翻车的5个位置这部分是我反复踩过的坑每一条都按“现象、原因、解决”写照着排查能省不少时间。5.1 现象导出的JSON文件打不开编辑器卡死Excel提示内存不足原因把几十万条记录写成了一个超大的JSON数组读文件的人又习惯用json.load一次性载入内存当场爆掉。这和数据本身关系不大是格式选择造成的。解决改成JSONL逐行写入见4.2。如果下游坚持要单个JSON数组至少用流式写法控制内存先写[每条记录后写,最后写]不要让Python替你在内存里维护整个数组。注意网上下载的JSON格式化工具大多也有同样的毛病大文件格式化一次能卡十分钟不要指望它来处理生产数据。5.2 现象同一个CMID对应多条记录但导出的JSON里只留下最后一条原因转换脚本里用了records[cmid] {...}这种直接赋值而不是往列表里追加。同一个CMID第二次出现时旧数据被覆盖。解决用defaultdict(list)或dict.setdefault(cmid, []).append(...)。用上一章3.2的groupby方式最不容易出错因为groupby天然把同一组行聚在一起diagnoses通过append构建列表不存在覆盖问题。写完脚本后我习惯抽查一个已知有多条诊断的CMID确认数组长度和原始宽表行数一致。5.3 现象Java下游报错“JsonParseException: cannot deserialize value of typejava.util.Datefrom String”原因宽表里的日期字段被pandas处理成了时间戳或ISO字符串比如2025-01-05T00:00:00.000ZJava侧Jackson默认无法把这种字符串反序列化成java.util.Date。解决在导成JSON前统一日期格式。纯日期字段用df[admission_date].dt.strftime(%Y-%m-%d)带时间字段统一为ISO8601并保持字符串类型。如果Java侧一定要Date对象就要求下游加JsonFormat(pattern yyyy-MM-dd)。关键是提前和下游约定格式而不是等报错再猜。这类日期问题在临床数据里尤其常见不同来源的日期有的精确到天有的精确到秒不统一就是定时炸弹。5.4 现象Excel打开JSON文件中文乱码或Linux工具读到的字段名带“uFEFF”原因文件用UTF-8无BOM写入时Windows Excel默认按ANSI打开中文变成乱码文件用UTF-8带BOM写入时Java、Spark等工具会把BOM字符读进第一个字段名。解决分场景处理。机器读取的JSONL用encodingutf-8不带BOM给人用Excel预览的文件单独导一份encodingutf-8-sig。不要试图一个文件同时满足两边我因为这个教训曾经被下游投诉过字段名多了个看不见的字符。5.5 现象连续抽取过程中连接中断或OpenAPI返回限流错误原因夜间跑批和医院其他报表任务争抢只读库资源数据库连接被断开接口侧有并发数限制连续请求触发限流。解决加重试逻辑并记录断点。每次成功处理一批后把最后一个CMID写到本地断点文件下次重启时从断点继续而不是从头开始import time def fetch_and_process(cmid_start: str, max_retry: int 5): for attempt in range(max_retry): try: rows query(cmid_startcmid_start) process(rows) return except ConnectionError: time.sleep(2 ** attempt) raise RuntimeError(f重试{max_retry}次仍失败, 断点: {cmid_start})逻辑说明2 ** attempt实现指数退避第一次失败等2秒第二次等4秒后面8秒、16秒给对方接口留出恢复时间。参数说明max_retry5是常见保守值超过后直接抛出错误并让外层记录断点比无限重试更可控。6. 用jq和抽样统计验证JSON质量把这次提取变成可复用资产导出JSONL文件之后我会先做一轮jq检查再交付而不是直接发压缩包。jq是Linux环境里处理JSON最顺手的工具命令短验证快。先看三个指标# 统计总条数 jq -s length cmid_export_20260801_batch_01.jsonl # 统计缺少cmid的记录数 jq -r select(.cmid null or .cmid ) | .cmid \ cmid_export_20260801_batch_01.jsonl | wc -l # 查看diagnoses数组长度分布 jq -r .diagnoses | length \ cmid_export_20260801_batch_01.jsonl \ | sort | uniq -c | sort -rn | head逻辑说明第一条命令看行数是否和HIS导出口径一致第二条检查必填主键是否缺失第三条看诊断数组长度如果大量记录都是空数组或某个异常大数字通常是关联条件写错了。参数说明jq -s会把所有行读入内存文件超过1GB时不要这样用改用while read -r line; do echo $line | jq ...; done流式处理。验证通过后我会把JSONL、Schema、SQL、manifest四件套放到同一个目录作为这批数据的基线。后续医学大模型微调时这个JSONL可以直接加工成对话样本做数据中台共享时它就是稳定、有字段约束的数据接口。我以前图省事直接把手里的宽表CSV丢给下游结果对方用Java接的时候日期解析挂了一对多关系丢了中文也乱码来回折腾了三天。现在养成的习惯是“先定CMID再画JSON结构再写Schema再导出JSONL最后用jq验数”刚开始确实多花一小时但后面省的是好几天。希望帮到你。本文还有配套的精品资源点击获取
返回列表