ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用Python和odmantic构建DSM-5精神障碍数据库查询系统

2026/10/6 16:13:59 拓冰建站 浏览量
用Python和odmantic构建DSM-5精神障碍数据库查询系统 简介面向精神医学研究人员、数据工程师与Python开发者的DSM-5精神障碍数据库设计源码依托美国精神医学学会《精神障碍诊断与统计手册》第五版构建了一套可标准化存储、查询与分析精神障碍信息的数据库框架可满足临床科研、数据管理及医学信息化场景下的基础需求。压缩包共22个文件大小仅1.03MB主要包含8个Python脚本、3个Word文档、3个JSON数据文件、2个RST文档以及PDM、TOML、Lock等工程配置脚本承担数据导入、查询、更新与删除等核心逻辑JSON文件用于存放障碍分类与描述数据文档类提供项目介绍、使用说明或API参考。目前已有86人学习。源码目录清晰引入Odmantic模型并通过PDM管理依赖内置docs与json_docs目录便于二次开发对需要快速搭建DSM-5知识库、开展精神障碍数据标准化处理的研究者来说是一份结构完整的落地参考实现。1. 从 DSM-5 到可查询数据库为什么拿 Python 重造这个轮子做精神医学信息化的人大概率都经历过这个场景手头要用 DSM-5美国精神医学学会《精神障碍诊断与统计手册》第五版的分类和诊断标准建一套编码体系结果只能去翻 PDF、查网页、手工复制到表格里。DSM-5 本质是一座庞大的分类学知识库但它的原始形态是书不是数据。这个源码包做的事就是把它拆成结构化 JSON再用 Python 的 odmantic 模型包一层数据库访问层。下载下来是 20 个文件的 Python 工程包含 7 个 Python 脚本、3 个 JSON 数据文件、文档、PDM 配置和一整套依赖锁定文件。它适合三类人要给精神障碍数据建库的医学信息工程师、做心理健康文本分析的算法工程师、以及想用真实诊断数据练手 Python 数据库设计的学生。它不是带界面的成品系统你需要自己跑脚本但这也意味着数据完全在你手里想怎么查就怎么查。2. 先拆 20 个文件DSM-5 数据是怎么被建模成数据库的2.1 从文件清单看项目结构拿到压缩包后我习惯先不急着运行而是把文件名从头过一遍判断这个工程的成熟度。这个包采用的是现代 Python 项目里很主流的 src 布局可导入代码全部放在 src/mydsm5 下文档单独放构建和依赖信息留在根目录。文件/目录类型在工程里的角色src/mydsm5/init.pyPython 脚本包入口导出核心模型和工具函数src/mydsm5 下其余 Python 文件Python 脚本数据模型定义、JSON 导入、查询逻辑docs/文档目录项目说明或 API 文档json_docs/JSON 数据目录3 个 JSON 数据文件存放 DSM-5 精神障碍数据pyproject.tomlTOML 配置PDM 项目定义声明依赖和构建配置pdm.lock锁定文件锁定所有依赖的精确版本.gitignore配置文件让 Git 忽略虚拟环境和缓存LICENSE许可证明确开源使用边界.pdm-python文本记录记录 PDM 关联的 Python 解释器路径readme.rst / readme.txtReStructuredText/文本使用说明从这套布局可以看出两个信号第一作者用了 PDM 而不是裸 requirements.txt说明对依赖一致性有要求第二数据以 JSON 形式放在 json_docs说明数据源本身是可读、可版本管理的而不是塞进二进制数据库里。这一点对医学数据类项目很关键——DSM-5 的内容会随版本修订用文本格式存原始数据git diff 就能看出哪天改了什么。2.2 JSON 数据文件与 DSM-5 领域的映射DSM-5 里的精神障碍不是一张平表它有明显的层级关系先按类别分如抑郁障碍、焦虑障碍、精神分裂症谱系等每个障碍下面又有诊断编码、诊断标准A/B/C 标准逐条列出症状表现、病程特征、鉴别诊断要点、严重程度评定维度。在这种结构下常见做法是拆成 3 个 JSON 文件各管一段别都糊在一个数组里。代码里一般会对应三种数据对象主表记录障碍本身标准表记录诊断标准条目症状表记录可检索的症状词。主表大致长这样。{ code: F32.0, name: 重度抑郁障碍单次发作轻度, category: 抑郁障碍, specifiers: [轻度, 中度, 重度, 伴精神病性特征], criteria_ref: [A, B, C], symptoms: [抑郁心境, 兴趣减退, 体重显著下降, 失眠, 精神运动性激越, 疲乏] }实际解析时症状通常被单独抽到第二个 JSON用 symptom_id 关联而不是像上面这样直接嵌数组。为什么因为 DSM-5 里同一个症状会出现在多种障碍的诊断标准中比如“睡眠障碍”在抑郁障碍、焦虑障碍、创伤后应激障碍里都会出现。嵌入式设计会让数据大量冗余而且想统计“哪些障碍共享某个症状”时你得遍历每个文档再展开数组查询代码会很别扭。第三个 JSON 一般存诊断标准的原文条目字段包含标准分组A/B/C、条目标识、以及关联的主表编码。数据建模到这一步DSM-5 的书本结构就映射成了程序可遍历的树状结构类别目录挂在顶层障碍实体挂在中层标准与症状挂在叶子层。2.3 为什么用 odmantic 而不是裸 MongoDB 或 SQLAlchemy这个工程以 JSON 为数据源用 odmantic 做存取层。odmantic 是构建在 pydantic 之上的异步 ODM面向 MongoDB。选它有几个很实际的理由。第一DSM-5 数据结构异构严重。有的障碍带发作频率字段有的带遗传风险描述有的带着重程度评定量表关系型数据库要预先设计一大堆稀疏列或者拆十几张关联表。MongoDB 的文档模型天然允许每个障碍存自己的补充字段而 odmantic 会在 Python 类型层面把这些字段约束住不会因为文档模型自由就什么脏数据都进得来。第二odmantic 模型用类型注解声明和 pydantic 一样写起来很短。下面这段就是典型的 DSM-5 障碍模型定义方式。from typing import Optional, List from odmantic import Model, Field class DSM5Disorder(Model): code: str Field(uniqueTrue) # 诊断编码如 F32.0 name: str Field(indexTrue) # 障碍名称做普通查询索引 category: str Field(indexTrue) # 所属大类如 抑郁障碍 specifiers: List[str] Field(default_factorylist) # 严重程度/病程标注 criteria_ref: List[str] Field(default_factorylist) # 引用的诊断标准分组 symptoms: List[str] Field(default_factorylist) # 关联症状词条 notes: Optional[str] None # 补充说明允许为空这里Field(uniqueTrue)设唯一约束防止同一个诊断编码被重复导入Field(indexTrue)给名称和类别加索引按类别筛选时不用全表扫描default_factorylist表示这个字段缺省时创建空列表避免多个实例共享同一个可变对象——这是 Python 新手最常见的翻车点直接用[]会让所有实例共享同一份列表改动一个全部跟着变。第三PDM 管依赖比 pip 省心。pdm.lock 文件记录了全部依赖的精确哈希和版本换机器检出新仓库执行pdm install环境会和开发时完全一致。对医学数据工程来说依赖漂移导致的隐性结果差异比算法 bug 更可怕所以我很认可用 PDM 而不是裸 pip。3. 从零跑通搭建环境、导入 JSON 种子数据、完成第一次查询3.1 PDM 环境准备源码包没有打包成独立可执行程序第一步是要恢复 Python 环境。前提是你本机装了 Python 3.10 以上的解释器。PDM 本身可以用 pip 安装也可以走官方脚本我一般直接用 pip 装。pip install pdm pdm install执行pdm install时PDM 会读取 pyproject.toml 和 pdm.lock。pyproject.toml 声明了项目需要的直接依赖pdm.lock 则圈定了每个依赖的精确版本。两者一对上安装过程基本不会出现“在我机器上是好的”这种玄学问题。国内网络环境如果装 PyPI 包慢常见做法是给 pdm 配镜像源在项目根目录执行pdm config pypi.url https://pypi.tuna.tsinghua.edu.cn/simple再重跑 install。注意改镜像源只影响后续下载不会改变 pdm.lock 里锁定的版本。装完后验证一下包能不能正常导入。pdm run python -c from mydsm5 import __version__; print(__version__)能打印版本号说明 src/mydsm5 已经进入 Python 的模块搜索路径。这里要留意pdm run和直接用python的区别PDM 会自动激活虚拟环境直接python命令大概率会调用系统全局解释器然后报ModuleNotFoundError: No module named mydsm5。3.2 写导入脚本把三个 JSON 写进 MongoDB数据源在 json_docs 目录下但数据库不能直接读文件所以要写一个导入脚本。odmantic 的写入接口是异步的脚本整体做成 async 比较干净。下面是我按这个工程的结构会写的导入脚本套路。import asyncio import json from pathlib import Path from odmantic import AIOEngine from motor.motor_asyncio import AsyncIOMotorClient from mydsm5 import DSM5Disorder JSON_DIR Path(json_docs) async def load_disorders(): raw json.loads((JSON_DIR / disorders.json).read_text(encodingutf-8)) engine AIOEngine(clientAsyncIOMotorClient(mongodb://localhost:27017), databasedsm5) items [DSM5Disorder(**item) for item in raw] await engine.save_all(items) print(f导入完成共 {len(items)} 条) asyncio.run(load_disorders())解释一下逻辑先把 JSON 文件读成 Python 字典列表再通过DSM5Disorder(**item)做一次 pydantic 校验和类型转换最后用engine.save_all批量写入。AIOEngine是 odmantic 的异步引擎databasedsm5指定库名集合名默认由模型类名推导为小写复数形式也就是 dsm5disorders。read_text(encodingutf-8)这一步建议写显式编码不然在 Windows 上可能用 GBK 去读中文内容直接乱码。如果机器上没装 MongoDB又只是想验证导入逻辑有个轻量办法用 mongomock 代替真实连接。常见做法是把AsyncIOMotorClient(mongodb://localhost:27017)换成mongomock.MongoClient().async_client这样不启数据库服务也能跑通全流程。但要注意mongomock 只做功能模拟不保证真实 MongoDB 的索引行为和聚合性能。3.3 用诊断编码验证数据落库导入完成后别急着去写复杂查询先用计数接口确认数据真的进去了。import asyncio from odmantic import AIOEngine from motor.motor_asyncio import AsyncIOMotorClient from mydsm5 import DSM5Disorder async def count(): engine AIOEngine(clientAsyncIOMotorClient(mongodb://localhost:27017), databasedsm5) total await engine.count(DSM5Disorder) f_codes await engine.count(DSM5Disorder, DSM5Disorder.code.startswith(F)) print(f总记录数 {total}F 开头编码 {f_codes}) asyncio.run(count())engine.count的第一个参数是模型类第二个是过滤条件。DSM-5 的诊断编码整体落在 ICD-10-CM 的 F00-F99 区间所以统计 F 开头的记录数基本等同于统计精神障碍主条目的总数。如果你的数据源里还有 Z 码影响健康状态的因素或 V 码这个数字会比主条目多算是一个很有用的数据质量探测手段。这一步跑通说明环境、模型、数据三个环节都通了后面再写查询和分析都是在这个骨架上加具体条件。4. 查询与分析实战从名称、编码、症状三个维度挖数据4.1 按分类目录做聚合统计以实际使用场景来说第一步往往不是查某个具体疾病而是看整个人群分类的分布。DSM-5 按类别划分这种层级结构天然适合按 category 做聚合。import asyncio from odmantic import AIOEngine from motor.motor_asyncio import AsyncIOMotorClient from mydsm5 import DSM5Disorder async def category_stat(): engine AIOEngine(clientAsyncIOMotorClient(mongodb://localhost:27017), databasedsm5) docs await engine.find(DSM5Disorder) stat {} for d in docs: stat[d.category] stat.get(d.category, 0) 1 for k, v in sorted(stat.items(), keylambda x: -x[1]): print(f{k}: {v} 条) asyncio.run(category_stat())这段代码把全量数据拉到内存再做字典计数。数据量在千级以内时完全没问题DSM-5 主条目也就是几百条的量级没必要上聚合管道。如果以后接入了症状明细、鉴别诊断全文数据量涨到几十万条再考虑用 MongoDB 的聚合框架做远端计算避免网络传输耗时。4.2 按症状反查障碍做一个迷你鉴别参考这个源码包最有实用价值的查询是给定一组症状反查哪些精神障碍覆盖了这些症状。DSM-5 的学习者和早期诊断辅助系统都有这个需求。常见做法是给每个障碍维护一个症状集合然后计算查询症状集合与障碍症状集合的重合度。import asyncio from odmantic import AIOEngine from motor.motor_asyncio import AsyncIOMotorClient from mydsm5 import DSM5Disorder async def match_symptoms(query_symptoms: list[str], top_n: int 5): engine AIOEngine(clientAsyncIOMotorClient(mongodb://localhost:27017), databasedsm5) docs await engine.find(DSM5Disorder) query_set set(query_symptoms) result [] for d in docs: symptom_set set(d.symptoms) overlap len(query_set symptom_set) jaccard overlap / len(query_set | symptom_set) if query_set | symptom_set else 0 result.append((d.name, d.code, overlap, jaccard)) result.sort(keylambda x: (x[2], x[3]), reverseTrue) for name, code, overlap, jaccard in result[:top_n]: print(f{code} {name} 命中 {overlap} 项Jaccard{jaccard:.2f}) asyncio.run(match_symptoms([抑郁心境, 失眠, 疲乏]))逻辑说明用集合交集算命中症状数 overlap用 Jaccard 系数算两个集合的相似度。排序时先按命中数降序命中数相同再看 Jaccard这样能避免单个症状被大量障碍命中的情况排在靠前。Jaccard 的分母是并集障碍症状越多分母越大天然惩罚那些症状列表写得过长的宽泛诊断。这里必须说清楚边界这只是数据检索和教学演示不能作为临床诊断结论。DSM-5 的诊断要满足病程时长、功能损害、排除其他障碍等多重标准症状反查只能帮你缩小疑似范围真正的诊断判断需要专业人员结合面诊。4.3 导出标准化表格给统计工具Python 做探索性分析没问题但精神医学论文里常用 SPSS、R 或 Excel 做统计。让数据能在这些工具间流动最省事的方式是导出 CSV。import csv import asyncio from odmantic import AIOEngine from motor.motor_asyncio import AsyncIOMotorClient from mydsm5 import DSM5Disorder async def export_csv(path: str dsm5_disorders.csv): engine AIOEngine(clientAsyncIOMotorClient(mongodb://localhost:27017), databasedsm5) docs await engine.find(DSM5Disorder) with open(path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([code, name, category, symptom_count]) for d in docs: writer.writerow([d.code, d.name, d.category, len(d.symptoms)]) asyncio.run(export_csv())注意导出编码用的是utf-8-sig不是utf-8。原因很实际UTF-8 的 CSV 文件用 Excel 打开时中文表头大概率显示成乱码utf-8-sig会在文件头部写入 BOM 标记Excel 识别到 BOM 就知道这是 UTF-8 编码的中文内容。这个坑我踩过不止一次所以现在凡是给非技术人员用的 CSV一律加 BOM。如果你只是自己用 Python 的 pandas 读那utf-8就够了。5. 避坑与排查这 20 个文件最容易绊倒人的四个坑5.1 中文乱码与转义字符现象导入后查询精神障碍名称控制台打印出来是\u91cd\u5ea6这样的转义序列或者直接显示乱码。原因两个层面。第一是读取文件时没指定 UTF-8Windows 下默认编码可能是 GBK中文直接解错第二是 JSON 数据文件本身可能被ensure_asciiTrue写成了全转义形式Python 的 json 库读到转义串默认也能还原但如果你用文本编辑器打开看全是\uXXXX容易误以为数据坏了。解决读取时显式声明编码写入时按需关闭 ASCII 转义。import json # 读取时显式指定 UTF-8 raw json.loads(open(json_docs/disorders.json, encodingutf-8).read()) # 写回时保留中文原样方便 git diff 审阅 with open(disorders_pretty.json, w, encodingutf-8) as f: json.dump(raw, f, ensure_asciiFalse, indent2)ensure_asciiFalse让中文以原始字符写入文件indent2让嵌套结构可读。从那以后我拿到任何含中文的 JSON 工程第一件事就是检查文件头有没有 BOM、读取代码有没有显式编码这两点确认了再谈导入。5.2 odmantic 模型字段和 JSON 嵌套结构对不上现象导入脚本执行后抛odmantic.exceptions.ValidationError提示某个字段缺失但打开 JSON 看数据明明在那里。原因JSON 里数据是嵌套的比如症状放在diagnostic_criteria: {symptoms: [...]}这样的子对象里而模型定义成了顶层字段symptoms。pydantic 的默认行为不会递归去子对象里找字段于是校验失败。解决先展平再做实例化是最直观的办法。常见做法是把嵌套字典拍平或者用 pydantic 的alias机制指向嵌套路径。推荐前者代码更好读排查也容易。def flatten_item(item: dict) - dict: criteria item.get(diagnostic_criteria, {}) return { code: item[code], name: item[name], category: item.get(category), symptoms: criteria.get(symptoms, []), criteria_ref: criteria.get(criteria_ref, []), }这段函数把嵌套的diagnostic_criteria里两个字段提升到顶层。注意get的默认值避免某条障碍没有症状数组时直接 KeyError 崩掉整个批次。实际导入场景我建议再加一层校验日志展平失败就打印原始文档的 code方便回头定位。5.3 pydantic v2 与 odmantic 的版本兼容问题现象按 pdm.lock 安装后运行模型定义报ImportError: cannot import name BaseSettings from pydantic。原因odmantic 早期版本依赖的是 pydantic v1而某些情况下 pdm 会把 pydantic 解析到 v2。pydantic v2 把BaseSettings挪到了pydantic_settings子包直接导入肯定失败。这是生态迁移期常见的依赖错位事故跟代码本身没关系。解决恢复锁定版本就是最稳的方案。既然仓库里带了 pdm.lock就别轻易让 pdm 升级依赖如果已经升上去了回退锁定就好。pdm install --locked--locked参数要求严格按锁文件安装任何写死的版本与 lock 不一致都直接报错而不是自作主张去找新版本。这正是 lock 文件存在的意义宁可安装失败让你知道环境变了也不要静默升级出个测不出来的隐藏问题。5.4 以为有源码就能直接跑结果缺驱动现象ModuleNotFoundError: No module named motor或者No module named odmantic。原因源码仓库的 pyproject.toml 里声明了运行时依赖但代码文件本身不携带依赖。没有执行pdm install或者团队用了别的包管理工具直接python script.py用的是全局环境自然找不到包。这个坑在所有源码包里通用面对陌生工程先看 pyproject.toml 再跑命令是铁律。解决缺哪个补哪个别只补一个。odmantic 和 motor 是配套的。pdm add odmantic motor执行后 PDM 会把两个依赖写进 pyproject.toml 并更新 pdm.lock。补完再重新跑导入脚本。给陌生工程的排查顺序先pdm install再pdm run python不要直接裸调系统 python。5.5 误把数据文件当成数据库现象有人以为有了 JSON 文件就等于数据库已经建好直接去连数据库却查不到数据。原因JSON 只是数据源不是数据库服务。odmantic 的查询引擎必须连接 MongoDB 实例而数据源需要导入脚本写入后才会出现在数据库里。数据文件是种子不是库表。解决把导入脚本当成建库流程的一部分。在项目文档里明确标注首次搭建必须执行导入脚本之后查询才有效。有 MongoDB 环境就导真实实例没有就先用 mongomock 验证逻辑但没有导入这一步后续查询永远只返回 0 条。6. 进阶玩法把 DSM-5 数据从 JSON 直接送进 pandas 做共病分析数据库跑通之后还有一条更轻的路某些分析根本不需要经过 MongoDB直接让 pandas 读三个 JSON 文件就行。DSM-5 数据量不大几千条以内pandas 处理起来毫无压力省掉中间层代码也更短。我最常用的一个分析是“症状共病网络”统计哪些症状频繁出现在不同障碍中。这在精神医学研究里对应一个实际问题——共病现象即一个患者同时满足多种障碍的诊断标准。import json import pandas as pd with open(json_docs/disorders.json, encodingutf-8) as f: disorders json.load(f) df pd.json_normalize(disorders) symptom_series df.explode(symptoms)[symptoms] co_occurrence symptom_series.value_counts().head(20) print(co_occurrence)逻辑说明pd.json_normalize把嵌套 JSON 展平为 DataFrameexplode(symptoms)把每个障碍的症状列表拆成多行一行一个症状value_counts()统计每个症状出现在多少种障碍中。得到的排名表能直观看出哪些是非特异性症状比如睡眠障碍、焦虑情绪这类高频症状跨越多个诊断类别。这份排名反过来也能提醒你恰恰是这些症状鉴别诊断的价值最低因为它们到处都有。如果你想做更正式的共病矩阵用交叉表就能出结果。matrix df.explode(symptoms).assign(present1).pivot_table( indexcode, columnssymptoms, valuespresent, aggfuncsum ).fillna(0)pivot_table生成一个以诊断编码为行、以症状为列的 0/1 矩阵。这个矩阵是后续做因子分析、层次聚类和网络图的标准输入格式。从这份源码包到矩阵一共就这几行代码中间没有任何数据库参与。这一步做完你能把一个静态的 DSM-5 数据源变成可喂给机器学习模型的表格数据。从那以后我拿到任何一个 JSON 结构的医学数据源码包都会先确认 json_normalize 能撑住多少层嵌套再决定要不要上数据库。不少分析场景其实根本不需要 MongoDBpandas 直读 JSON 反而是后悔药最少的那条路。希望这个思路帮到你。本文还有配套的精品资源点击获取