资讯动态

Python实现DSM-5精神障碍数据库设计:SQLAlchemy六张核心表建模

发布时间:2026/10/13 18:38:20 来源:尧图企业网站定制
简介这是一份基于Python语言构建的DSM-5精神障碍数据库设计源码面向精神医学研究人员、临床工作者与数据库开发者用于将《精神障碍诊断与统计手册第五版》所收录的精神障碍分类和诊断信息进行标准化存储、检索与管理。资源共22个文件压缩包约1.03MB包含8个Python脚本、3个JSON数据文件、3个docx文档、2个rst文档另配套TOML配置、PDM项目定义、锁定文件与Git忽略配置目录结构清晰、层次分明。Python脚本承担数据导入、查询、更新与删除等核心逻辑JSON文件存储精神障碍分类与诊断信息docx与rst文档提供项目说明和使用指导配置与管理文件则保证依赖一致与协作规范便于二次开发与实际部署。已有86人学习下载适合需要快速构建DSM-5精神障碍数据库、开展精神医学数据分析和临床研究的开发者参考借鉴。1. DSM-5精神障碍数据库设计为什么先建库再谈智能问诊DSM-5精神障碍数据库设计落到代码层面核心是把诊断标准本身变成可以查询的数据资产。基于Python来做这个设计的源码不等于把手册PDF转成一个大宽表而是要处理好几十个大类、数百个障碍和上千条诊断标准之间的树形与网状关系。很多做心理测评、精神卫生随访系统的团队早期都用Excel维护这些内容三个月后就出现同一个障碍在不同模块里标准不一致的情况。这个方向的价值就是给这类系统一张不会跑偏的底表。做这个项目的门槛不在Python语法而在数据结构。DSM-5本身是一棵分类树树里每个节点又是一个携带多条标准的实体标准之间还有“满足五项中的三项”“持续至少两周”这类判定条件。设计数据库的人一旦把这些条件压进一个text字段后续统计和检索就会全面吃亏。这篇笔记按我实际做过的方式把表结构、建库脚本、数据导入、增删改查和踩坑点依次写清楚新手能照步骤跑通熟手可以直接改字段扩展。2. 先建模再写码六张核心表怎么拆字段与关联理由2.1 DSM-5诊断数据的真实结构三类信息两种关系DSM-5的组织方式不是平铺清单而是严格的树加网状约束。最上层是二十个左右的分类大类比如抑郁障碍、焦虑障碍、强迫及相关障碍每个大类下面是障碍实体比如重性抑郁障碍、持续性抑郁障碍障碍实体再往下是具体的诊断标准条目标准带编号、带计数要求、有时还带病程条件。这就是第一层结构分类到障碍再到标准的树形关系。第二种结构是跨树的网状关系。一个症状可以同时出现在多个障碍的诊断标准里比如睡眠减少既出现在抑郁障碍的A标准里也出现在焦虑障碍的躯体症状条目里障碍与障碍之间还有鉴别诊断关系医生判断时要排除另一种障碍。这些关系一旦用宽表去存就会出现大量空列而且每次改动都要动表结构。我第一个版本就是从这里翻车的后来拆成六张核心表才稳定下来。设计要点很明确树状关系靠外键加物化路径解决网状关系靠独立的关联表解决判定条件单独抽成一张规则表。这三件事分开后面做症状反查、统计满足条目的数量、扩展新的障碍条目都只需要加数据不需要改表结构。2.2 六张核心表字段清单与关系设计第一张是分类表category存DSM-5的大类和子类核心字段是parent_id和path。path存物化路径比如“2/21/214”查某个大类下的所有子分类时直接做前缀匹配不用写递归。第二张是障碍表disorder挂在分类表下面字段里必须同时保留dsm5_code和icd10cm_code两套编码科研和临床记录各用各的不能混在一个字段里。第三张是标准条目表criterion一条标准一行字段包括disorder_id、label、seq、content。label是A、B、C这类标号seq是同一标号下的条目序号content存标准文本的要点摘要。第四张是判定规则表criterion_rule存这个障碍的判定逻辑例如A标准需要满足几条、病程需要多少天。第五张是症状字典表symptom存症状名称和同义词。第六张是障碍与症状的关联表disorder_symptom把障碍、标准条目、症状三者绑在一起。表格整理如下。表名职责关键字段关联方式category分类树节点id, path, parent_id, code, name自引用父子关系disorder障碍实体id, category_id, dsm5_code, icd10cm_code, name多对一categorycriterion诊断标准条目id, disorder_id, label, seq, content多对一disordercriterion_rule判定规则id, disorder_id, label, min_count, duration_days多对一disordersymptom症状词典id, name, synonyms无disorder_symptom障碍标准症状映射id, disorder_id, criterion_id, symptom_id多对多disorder与symptomdisorder_symptom表里我专门加了criterion_id这一点是反复试出来的。如果只做disorder和symptom的多对多你只知道这个障碍和某个症状有关却不知道它属于哪一条标准后面算“A标准命中了几条”时还得绕回criterion表凭空多一层join。把criterion_id放进关联表一次查询就能同时拿到症状、标准和障碍三层信息。2.3 用SQLAlchemy落成Python模型结构代码直接抄建模我用SQLAlchemy 2.x的declarative_base方式模型定义和建表语句都在Python里维护。代码片段如下。from datetime import datetime from sqlalchemy import ( Column, Integer, String, Text, ForeignKey, Boolean, create_engine ) from sqlalchemy.orm import declarative_base, relationship, sessionmaker Base declarative_base() class Category(Base): 分类树节点path存物化路径避免递归取子类 __tablename__ category id Column(Integer, primary_keyTrue) path Column(String(100), nullableFalse, indexTrue) # 如 2/21/214 parent_id Column(Integer, ForeignKey(category.id)) code Column(String(50), uniqueTrue) name Column(String(200), nullableFalse) sort_order Column(Integer, default0) children relationship(Category, backrefparent, remote_side[id]) class Disorder(Base): 障碍实体DSM-5编号与ICD-10-CM编号分开存 __tablename__ disorder id Column(Integer, primary_keyTrue) category_id Column(Integer, ForeignKey(category.id), nullableFalse) dsm5_code Column(String(50), indexTrue) icd10cm_code Column(String(50), indexTrue) name Column(String(300), nullableFalse) en_name Column(String(300)) summary Column(Text) # 自行整理的要点不存官方原文全文 category relationship(Category, backrefdisorders) class Criterion(Base): 诊断标准条目如A1、A2每条一行 __tablename__ criterion id Column(Integer, primary_keyTrue) disorder_id Column(Integer, ForeignKey(disorder.id), nullableFalse) label Column(String(10), nullableFalse) # A / B / C seq Column(Integer, default0) # 同一label内顺序 content Column(Text, nullableFalse) # 标准要点摘要 is_counted Column(Boolean, defaultTrue) # 是否计入计数要求 disorder relationship(Disorder, backrefcriteria) class CriterionRule(Base): 该障碍的判定规则A标准至少命中N条病程持续X天 __tablename__ criterion_rule id Column(Integer, primary_keyTrue) disorder_id Column(Integer, ForeignKey(disorder.id), nullableFalse) label Column(String(10)) # 对应Criterion.label min_count Column(Integer, nullableFalse) # 命中条数下限 max_count Column(Integer, nullableTrue) # 上限一般不需要 duration_days Column(Integer) # 病程要求单位天 class Symptom(Base): 症状词表synonyms存同一症状的多种说法 __tablename__ symptom id Column(Integer, primary_keyTrue) name Column(String(100), nullableFalse, uniqueTrue) synonyms Column(Text) # JSON数组或分号分隔 class DisorderSymptom(Base): 障碍-标准-症状三键关联 __tablename__ disorder_symptom id Column(Integer, primary_keyTrue) disorder_id Column(Integer, ForeignKey(disorder.id), nullableFalse) criterion_id Column(Integer, ForeignKey(criterion.id), nullableFalse) symptom_id Column(Integer, ForeignKey(symptom.id), nullableFalse)字段里有几个值得说明的地方。path字段是给分类查询加速用的写数据时由导入脚本计算读数据时不递归树深只有三层时这是最划算的方案。disorder表里的两套编码各建独立索引因为两类使用场景的查询入口不一样。criterion表刻意用“要点摘要”而不是把诊断标准全文贴进去既控制录入成本也避开官方文本的版权问题实际项目中这也是多数从业团队的默认做法。3. 跑通最小数据库建库、导入清洗后的DSM-5数据与增删改查3.1 最小环境SQLite加SQLAlchemy一个人本地跑起来本地原型阶段我一般直接用SQLite不用MySQL也不需要PostgreSQL。DSM-5数据库在单机场景下就是个几千行的关系数据SQLite一个文件就能扛住配合SQLAlchemy的ORM写法后面要迁到MySQL或PostgreSQL时只需要换engine连接串和相关依赖业务代码基本不动。这个取舍是很多项目的标准路径不要一上来就搭数据库服务端。环境准备很简单Python 3.10以上装一个SQLAlchemy就够了。pip install sqlalchemy如果你在Windows下装完Python后发现命令找不到先检查系统变量PATH里有没有python.exe所在目录这就是常说的python环境变量配置卡在这一步的人比SQLAlchemy报错的还多。装好后建engine和Session。from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from models import Base engine create_engine(sqlite:///dsm5.db, echoFalse) Base.metadata.create_all(engine) SessionLocal sessionmaker(bindengine, autoflushFalse) session SessionLocal()create_all会根据前面定义的六张表在dsm5.db里生成表结构。echo参数在调试时可以开成True正式跑关掉否则控制台会刷大量SQL日志。这里注意一个常见误用不要反复执行create_all它只负责建不存在的表不会帮你改已经存在的字段。3.2 把文字版诊断标准清洗成三张CSV的导入流程从手册或现有Excel整理数据时我习惯先导出三张CSV分别对应category、disorder、criterion。清洗流程分三段走。第一段把分类的层级关系拍平成“path, parent_id, code, name”四列path里的层级用斜杠拼好。第二段把障碍实体整理成“category_code, dsm5_code, icd10cm_code, name, en_name”五列。第三段把每个障碍的标准条目拆成“disorder_code, label, seq, content”四列同一障碍的A标准若干条就依次排开。导入脚本可以写成下面这样用csv模块逐行读取并写入session。import csv from sqlalchemy.orm import Session from models import Category, Disorder, Criterion, engine def load_categories(path: str) - None: session Session(engine) with open(path, encodingutf-8-sig) as f: for row in csv.DictReader(f): session.add(Category( pathrow[path], parent_idint(row[parent_id]) if row[parent_id] else None, coderow[code], namerow[name], )) session.commit() session.close()这段逻辑很简单但有两个参数细节。encoding必须用utf-8-sig否则Excel导出的CSV会带BOM头第一列字段名会变成“\ufeffcode”查询时对不上。parent_id为空时要显式传None不能传空字符串否则外键约束直接报错。数据量小时逐行session.add没问题几百行一次性commit即可没必要为了这点规模上批量插入接口。criterion表的导入逻辑和上面几乎一样区别是它的disorder_id需要通过disorder_code查询出来写入时做一次映射。常见做法是先读一遍disorder表把code和id的对应关系缓存成一个字典避免每条标准都去查一次数据库。def load_criteria(path: str, disorder_map: dict) - None: session Session(engine) with open(path, encodingutf-8-sig) as f: for row in csv.DictReader(f): session.add(Criterion( disorder_iddisorder_map[row[disorder_code]], labelrow[label], seqint(row[seq]), contentrow[content], )) session.commit() session.close()3.3 数据库增删改查四条必写查询和两条维护语句数据库增删改查是这个项目的基本功但真正高频的不是教科书里的“查全表”而是围绕DSM-5结构的四条查询。第一按分类路径查所有障碍第二按症状反查候选障碍第三按ICD编码精确查障碍第四更新某条标准内容。下面这段代码可以直接跑。from sqlalchemy.orm import Session from models import Disorder, Criterion, Symptom, DisorderSymptom, Category def get_disorders_by_category(session: Session, path_prefix: str): 按分类路径前缀取全部障碍 return ( session.query(Disorder) .join(Category, Disorder.category_id Category.id) .filter(Category.path.like(f{path_prefix.rstrip(/)}/%)) .all() ) def find_disorders_by_symptom(session: Session, symptom_name: str): 按症状名反查障碍走症状字典而非LIKE模糊匹配 return ( session.query(Disorder) .join(DisorderSymptom, DisorderSymptom.disorder_id Disorder.id) .join(Symptom, Symptom.id DisorderSymptom.symptom_id) .filter(Symptom.name symptom_name) .all() ) def get_disorder_by_icd(session: Session, icd_code: str): return ( session.query(Disorder) .filter(Disorder.icd10cm_code icd_code) .first() )这些查询的写法有几个前置条件。get_disorders_by_category依赖path字段的前缀匹配所以导入数据时必须保证path格式统一比如统一用“大类/子类/具体障碍”这种三段式。find_disorders_by_symptom看起来简单实际能不能命中完全取决于symptom表里有没有这条症状记录而不是用户输入的原始文本所以我在使用前会把输入先映射到症状字典查不到就先走同义词匹配。get_disorder_by_icd依赖icd10cm_code的索引建表时已经加了indexTrue这里不用再做特殊处理。更新和删除同样要注意关联关系。更新一条标准内容时我一般会同步更新criterion表和disorder表的改动日志删除一个障碍时必须先确认模型的cascade配置覆盖了criterion和disorder_symptom两个方向。# 更新标准条目内容 session.query(Criterion).filter(Criterion.id 15).update( {content: 近两周内几乎每天出现兴趣或愉悦感减退} ) session.commit() # 删除障碍实体连带清理标准与症状映射 disorder session.query(Disorder).filter(Disorder.id 3).first() session.delete(disorder) session.commit()session.delete能级联删掉关联数据依赖前面模型里relationship配置的cascade行为。如果你用的是纯SQL或者没有配cascade删除前要先手动删disorder_symptom和criterion里的记录否则外键约束会把你拦在数据库层这是删除操作最容易碰到的问题。4. DSM-5数据库落地避坑五个让我返工的现场4.1 分类树查询出现N1接口越跑越慢现象写了一个取大类下所有障碍的接口前端调用一次要发几十条SQL本地跑都要几十毫秒数据量上去后直接秒级。原因分类表用的纯adjacency list设计代码里一层层递归取子节点每一层发一次查询树深几层就有几次往返。DSM-5分类树虽然只有三层但这个写法在循环里碰到几百个节点时同样会翻车。解决给category表追加path字段存物化路径查询直接改成一次前缀匹配。树深只有三层时这个方案比闭包表简单比递归CTE直观写入时维护一下路径字符串就够用。4.2 标准全文塞进一个长文本字段统计功能全废现象想统计“有多少案例在抑郁障碍的A标准中命中了至少五条症状”发现根本写不出SQL因为整个A标准被拼成了一整段话存在一个字段里。原因图省事把criterion表设计成了“一个障碍一行、标准内容一长段”完全没有拆条目。结果就是数据库变成了纯存储所有判断逻辑都堆在Python里用字符串处理做又慢又容易漏。解决拆表。每条标准一行label加seq区分位置计数条件单独放criterion_rule表。这样统计满足几条就是一个简单的count加group by不再需要解析文本。4.3 症状反查用LIKE模糊匹配同义词全部漏查现象在数据库里查“失眠”能查到相关障碍换成“入睡困难”就查不到用户觉得系统时灵时不灵。原因症状反查直接写了WHERE Symptom.name LIKE %失眠%只匹配字面量没有处理“早醒”“维持睡眠困难”“入睡困难”这些同义表达却期待用户每次都输入同一套词。解决建symptom表并且带synonyms字段录入阶段把常见同义词收齐。查询时先做一次同义词映射把输入转换成多个symptom_id再join disorder_symptom。这一步非常费功夫但它是整个数据库检索价值所在偷懒会直接拉低后续所有工具的上限。4.4 DSM-5编码和ICD-10-CM编码混在一个字段现象医生做临床记录要填ICD码科研统计要用DSM-5研究编码两边对账时发现同一个障碍在不同记录里编码不一致根本对不上。原因设计表时把两套编码塞进了同一个字符串字段录入人员自己选一套填另一套就丢了。解决disorder表里dsm5_code和icd10cm_code两个字段分开建各加索引。导入时单独做一份编码映射表按source字段区分编码来源避免后续人工二次整理。我已经因为这个坑返工过两次每次都是因为当初图省一个字段。4.5 症状字典没人维护同义词越攒越乱现象运行半年后symptom表里出现“兴趣下降”“兴趣缺失”“对一切都提不起兴趣”三条记录内容重复但都被不同的录入人员当成了新词添加。原因synonyms字段自由录入没有归属人也没有新增词条的审查流程谁需要就插一条。解决给symptom表的synonyms字段约定统一格式用分号分隔新增前先查一次现有记录更稳的做法是把同义词抽成独立表格带created_by和reviewed_at字段交给团队里的临床角色做审核。数据字典这类表不怕慢就怕没人管。5. 进阶做一个按症状反查障碍的辅助CLI并验证它的命中率数据库跑通之后最有用的下一步不是画界面而是写一个候选障碍评分函数。输入一个症状列表输出按命中标准条目数排序的障碍名单让医生或随访人员从候选里做判断而不是对着整个手册翻。from collections import defaultdict from sqlalchemy.orm import Session from models import Disorder, Criterion, Symptom, DisorderSymptom def candidate_score(symptom_names: list[str], session: Session): 输入已规范化的症状名列表返回按命中数排序的候选障碍 sym_ids { sid for (sid,) in session.query(Symptom.id) .filter(Symptom.name.in_(symptom_names)) .all() } if not sym_ids: return [] rows ( session.query(Disorder, Criterion) .join(Criterion, Criterion.disorder_id Disorder.id) .join(DisorderSymptom, DisorderSymptom.criterion_id Criterion.id) .filter(DisorderSymptom.symptom_id.in_(sym_ids)) .all() ) score defaultdict(int) hit_label defaultdict(set) for disorder, criterion in rows: score[disorder.id] 1 hit_label[disorder.id].add(criterion.label) result [] for disorder_id, cnt in score.items(): disorder session.get(Disorder, disorder_id) result.append(( disorder.name, cnt, ,.join(sorted(hit_label[disorder_id])), disorder.icd10cm_code, )) return sorted(result, keylambda x: -x[1])这段逻辑说明如下。先通过症状名查symptom表拿到id集合不存在的名字会在这一步被过滤掉所以调用前应做同义词预处理。然后从disorder表出发沿到criterion再到disorder_symptom的路径做join这里用的是内连接只保留能命中症状映射的障碍。最后按障碍聚合命中的标准条目数同时记录命中了哪些label返回结果按命中数倒序排序。验证方法我习惯用十组模拟主诉来测。比如“近两周对什么都提不起兴趣、早上四点醒来、体重下降”拆成“兴趣下降、早醒、体重下降”三个词跑完看Top3里是否出现重性抑郁障碍。十组里至少要命中八组以上才放行达不到就去补symptom表的同义词多数失败都出在“兴趣缺失”这类同义表达没收录而不是模型代码的问题。做这类工具要守住边界它输出的是候选列表不是诊断结论数据库里的判定规则只做检索排序不替代临床判断。我现在的习惯是每次改动标准文本都同步记一张changelog写清哪条标准被改过、影响哪些查询数据库设计不怕初始模型丑怕的是改完之后没人记得当初为什么这么建。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑