资讯动态

DSM-5精神障碍数据库设计:从表结构到诊断判定的工程实践

发布时间:2026/10/9 19:53:47 来源:尧图企业网站定制
简介这份源码面向精神医学信息化开发者、医疗数据分析人员及Python数据库设计学习者提供基于DSM-5精神障碍分类体系的数据库构建方案解决精神障碍数据标准化存储与查询的问题。资源包共22个文件约1.03MB以8个Python脚本为核心配合3个docx文档、3个JSON数据文件、2个rst说明及pdm-python、toml、lock等工程配置文件覆盖数据导入、查询、更新等核心逻辑与依赖管理。项目采用PDM进行包管理通过锁定文件保证构建一致性并附带许可证与Git忽略配置目录结构清晰便于二次开发与维护。目前已有86人学习下载。读者可获取完整的数据模型设计思路、JSON数据组织方式及项目工程化配置范例适合作为精神医学数据平台或课程设计的参考起点。1. 从一份 DSM-5 精神障碍数据库说起为什么临床数据结构比算法更难写很多做医疗信息化的开发者第一次接触 DSM-5 精神障碍数据库设计时都会下意识觉得“不就是建几张表、录点诊断条目吗”。真动手才发现DSM-5 的诊断体系本身是多轴、层级、带时间维度的一个障碍类别下有若干诊断条目每个条目又由一组症状标准、病程标准、功能损害标准、排除标准共同约束还要记录每次评估的变化。用一张扁平表硬塞三个月后必然翻车。这个方向适合三类人一是做临床科研数据管理的开发者需要把量表、访谈、诊断结论结构化二是做心理健康类应用的后端工程师要给用户建立可追溯的评估档案三是医学信息方向的学生想找一个真实够复杂的领域练手数据库设计。它解决的核心问题只有一个——让“诊断”这件事从自由文本变成可查询、可统计、可回溯的结构化数据同时不丢失 DSM-5 原有的临床语义。下面我按自己实际搭过的一套方案把表结构、约束、查询和踩坑讲清楚。2. 先想清楚 DSM-5 的数据模型五类实体与三种关系2.1 为什么不能把诊断标准直接塞进一个字段最常见的错误做法是建一张diagnosis表字段写成name、criteria_text、note。这样做的直接后果是你无法回答“有多少患者满足 A 类症状中的至少 5 条”也无法做跨诊断的症状共现统计。DSM-5 的诊断标准本质上是可计数的条目集合必须拆成独立实体。我一般把整个模型拆成五类实体实体作用关键字段示例disorder障碍类别顶层分类如抑郁障碍、焦虑障碍code, name, chapterdiagnosis具体诊断某个可下结论的诊断条目code, name, disorder_idcriterion诊断标准某诊断下的一条标准diagnosis_id, criterion_type, min_countsymptom症状条目标准下可勾选的具体症状criterion_id, text, order_noassessment评估记录某患者某次评估的结果patient_ref, assessed_at, status三种关系分别是障碍类别到诊断是一对多诊断到标准是一对多标准到症状是一对多。评估记录则通过一张关联表assessment_symptom与症状多对多挂钩记录“这次评估里该症状是否命中”。2.2 用 SQLAlchemy 定义核心表结构下面这段是核心模型的骨架用 SQLAlchemy 声明式写法SQLite 和 PostgreSQL 都能跑。注意criterion_type用枚举区分“症状计数型”“病程型”“排除型”这是后面查询逻辑的分水岭。from sqlalchemy import ( Column, Integer, String, Text, ForeignKey, DateTime, Enum, Boolean ) from sqlalchemy.orm import declarative_base, relationship import enum Base declarative_base() class CriterionType(enum.Enum): SYMPTOM_COUNT symptom_count # 需满足若干条症状 DURATION duration # 病程时长要求 IMPAIRMENT impairment # 功能损害 EXCLUSION exclusion # 排除标准 class Disorder(Base): __tablename__ disorder id Column(Integer, primary_keyTrue) code Column(String(16), uniqueTrue, nullableFalse) name Column(String(128), nullableFalse) chapter Column(String(64)) diagnoses relationship(Diagnosis, back_populatesdisorder) class Diagnosis(Base): __tablename__ diagnosis id Column(Integer, primary_keyTrue) code Column(String(16), uniqueTrue, nullableFalse) name Column(String(128), nullableFalse) disorder_id Column(Integer, ForeignKey(disorder.id)) disorder relationship(Disorder, back_populatesdiagnoses) criteria relationship(Criterion, back_populatesdiagnosis) class Criterion(Base): __tablename__ criterion id Column(Integer, primary_keyTrue) diagnosis_id Column(Integer, ForeignKey(diagnosis.id)) criterion_type Column(Enum(CriterionType), nullableFalse) min_count Column(Integer, default0) # 症状计数型才有意义 description Column(Text) diagnosis relationship(Diagnosis, back_populatescriteria) symptoms relationship(Symptom, back_populatescriterion) class Symptom(Base): __tablename__ symptom id Column(Integer, primary_keyTrue) criterion_id Column(Integer, ForeignKey(criterion.id)) text Column(Text, nullableFalse) order_no Column(Integer, default0) criterion relationship(Criterion, back_populatessymptoms)逻辑说明Criterion是整套设计的枢纽。min_count只在SYMPTOM_COUNT类型下参与判定其他类型留 0 即可。order_no保证症状展示顺序和量表原文一致别小看这个字段临床上顺序错乱会直接影响访谈节奏。参数说明code建议用 DSM-5 官方编码体系长度 16 足够chapter存章节名便于按大类聚合Enum在 SQLite 里会存成字符串迁移到 PostgreSQL 时记得同步建枚举类型否则 Alembic 迁移会报类型不匹配。2.3 评估记录表把“某次评估”变成可回溯事件评估表是很多人漏掉的一环。没有它数据库只能存“标准”不能存“某个人在某个时间点的状态”。class Assessment(Base): __tablename__ assessment id Column(Integer, primary_keyTrue) patient_ref Column(String(64), indexTrue) # 外部患者标识不存真实身份 diagnosis_id Column(Integer, ForeignKey(diagnosis.id)) assessed_at Column(DateTime, nullableFalse) is_met Column(Boolean, defaultFalse) # 该诊断是否成立 note Column(Text) class AssessmentSymptom(Base): __tablename__ assessment_symptom id Column(Integer, primary_keyTrue) assessment_id Column(Integer, ForeignKey(assessment.id)) symptom_id Column(Integer, ForeignKey(symptom.id)) hit Column(Boolean, defaultFalse)patient_ref刻意用外部标识而非自增主键关联患者表是为了让这套库能嵌进已有系统不强制接管患者主数据。hit字段记录单条症状是否命中判定逻辑放在应用层数据库只负责存事实。3. 把诊断判定写成可复现的查询从症状计数到排除标准3.1 症状计数型标准的判定 SQLDSM-5 里大量标准是“满足以下 9 条中的至少 5 条”。这条规则落到 SQL 就是一个分组计数加阈值比较。-- 给定 assessment_id判断某条 symptom_count 型标准是否满足 SELECT c.id AS criterion_id, c.min_count, COUNT(*) FILTER (WHERE a_s.hit TRUE) AS hit_count, (COUNT(*) FILTER (WHERE a_s.hit TRUE) c.min_count) AS is_satisfied FROM criterion c JOIN symptom s ON s.criterion_id c.id LEFT JOIN assessment_symptom a_s ON a_s.symptom_id s.id AND a_s.assessment_id :assessment_id WHERE c.id :criterion_id AND c.criterion_type symptom_count GROUP BY c.id, c.min_count;逻辑说明FILTER (WHERE ...)是 PostgreSQL 语法SQLite 不支持需要改成SUM(CASE WHEN a_s.hit THEN 1 ELSE 0 END)。LEFT JOIN保证即使一条症状都没勾选也能返回hit_count 0而不是空行这对前端展示“未满足”状态很关键。参数说明:assessment_id和:criterion_id是绑定参数别用字符串拼接评估数据涉及敏感信息注入风险不值得冒。min_count直接来自标准表不要硬编码在 SQL 里否则改标准就得改代码。3.2 排除标准为什么要单独处理排除标准比如“排除物质使用所致”在逻辑上是否决项只要命中整个诊断不成立无论症状计数多高。所以判定顺序必须是先查排除再查计数。def evaluate_diagnosis(session, assessment_id, diagnosis_id): criteria session.query(Criterion).filter_by(diagnosis_iddiagnosis_id).all() # 第一步排除标准一票否决 for c in criteria: if c.criterion_type CriterionType.EXCLUSION: if _any_symptom_hit(session, assessment_id, c.id): return False # 第二步计数型标准逐条判定 for c in criteria: if c.criterion_type CriterionType.SYMPTOM_COUNT: if not _count_satisfied(session, assessment_id, c.id, c.min_count): return False return True逻辑说明把排除标准放在最前面是因为临床上排除项一旦成立后续症状再多也没有诊断意义。这个顺序如果写反会出现“物质所致症状被误判为独立障碍”的严重问题。参数说明_any_symptom_hit和_count_satisfied是两个内部函数前者查是否存在hitTrue后者复用 3.1 的计数逻辑。判定函数保持纯逻辑不碰事务方便单元测试。3.3 用 Alembic 管理表结构演进DSM-5 文本会随修订更新标准条目可能增删。用 Alembic 做迁移别手动改表。alembic init migrations alembic revision --autogenerate -m add criterion_type enum alembic upgrade head逻辑说明--autogenerate会对比模型和数据库差异生成迁移脚本但枚举类型变更它经常识别不全生成后必须人工检查。upgrade head前先在测试库跑一遍评估数据一旦迁移失败回滚成本很高。参数说明迁移脚本里对已有数据的criterion_type新增枚举值要写server_default否则已有行会因非空约束报错。4. 避坑与排查五个真实踩过的坑4.1 症状顺序错乱导致访谈结果偏差现象前端展示的症状顺序和量表原文不一致访谈者按屏幕顺序提问漏掉了靠后的关键条目。原因symptom表插入时没写order_no查询默认按主键排序而批量导入的顺序和原文顺序不一致。解决导入脚本里显式写入order_no查询一律ORDER BY order_no。导入后跑一次校验比对条目数和顺序。4.2 枚举类型在 SQLite 与 PostgreSQL 间不兼容现象本地用 SQLite 开发一切正常部署到 PostgreSQL 后插入criterion_type报类型不存在。原因SQLAlchemy 的Enum在 SQLite 存字符串在 PostgreSQL 会尝试建原生枚举类型而迁移脚本没同步创建。解决迁移脚本里显式CREATE TYPE或改用String加应用层校验。我一般选后者跨库省心。4.3 评估记录时间戳时区混乱现象同一患者两次评估的先后顺序在报表里颠倒。原因assessed_at有的写入本地时间有的写入 UTC混在一起排序就乱。解决统一存 UTC展示层再转本地时区。字段类型用带时区的DateTime(timezoneTrue)别用裸DateTime。4.4 排除标准被当成普通症状计数现象某诊断明明有排除项命中系统仍判定成立。原因判定函数先跑了计数逻辑排除逻辑写在后面或者根本没实现。解决按 3.2 的顺序排除优先。加一条单元测试构造“排除命中 计数满足”的用例断言结果为 False。4.5 患者标识直接存了真实信息现象审计时发现patient_ref里存了姓名拼音加出生日期。原因开发图方便直接把业务系统的展示名塞进来了。解决patient_ref只存不可逆的哈希或业务系统内部 ID真实身份留在原系统。这条不是技术问题是红线问题返工代价极大。5. 进阶技巧把诊断判定做成可测试的纯函数写到这儿数据库能跑、查询能出结果但真正决定这套设计能不能长期维护的是判定逻辑有没有被隔离成可测试的纯函数。我见过太多项目把判定散落在视图、模板、存储过程里改一条标准要翻五个文件最后没人敢动。我的习惯是数据库只存事实症状命中与否判定逻辑全部收敛到一个evaluator模块输入是评估 ID 和诊断 ID输出是布尔值和一份“哪条标准没过”的明细。这样带来三个好处一是可以写单元测试构造各种边界组合二是前端能展示“差 1 条症状就满足”的提示三是标准修订时只改数据不改代码。def evaluate_with_detail(session, assessment_id, diagnosis_id): detail [] criteria session.query(Criterion).filter_by(diagnosis_iddiagnosis_id).all() for c in criteria: if c.criterion_type CriterionType.EXCLUSION: hit _any_symptom_hit(session, assessment_id, c.id) detail.append({criterion: c.id, type: exclusion, passed: not hit}) if hit: return False, detail elif c.criterion_type CriterionType.SYMPTOM_COUNT: cnt _count_hit(session, assessment_id, c.id) passed cnt c.min_count detail.append({criterion: c.id, type: count, hit: cnt, need: c.min_count, passed: passed}) if not passed: return False, detail return True, detail逻辑说明返回(bool, detail)二元组detail里带上每条标准的命中数和阈值前端可以直接渲染成进度条。排除标准命中时立即返回不再继续判定符合临床逻辑。参数说明_count_hit只统计hitTrue的行数和 3.1 的 SQL 保持一致避免两处逻辑漂移。测试时用内存 SQLite 建库构造最小数据集跑 pytest 断言各种组合。验证方法上我一般会准备三组用例全满足、差一条、排除命中。三组都过才认为判定逻辑可信。这套东西搭完后面接量表、接随访、接统计报表都是顺水推舟。血泪经验就一句判定逻辑和数据存储一定要分家否则标准一改整个系统跟着塌。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑