简介:面向精神医学研究人员、数据工程师与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.py | Python 脚本 | 包入口,导出核心模型和工具函数 |
| src/mydsm5 下其余 Python 文件 | Python 脚本 | 数据模型定义、JSON 导入、查询逻辑 |
| docs/ | 文档目录 | 项目说明或 API 文档 |
| json_docs/ | JSON 数据目录 | 3 个 JSON 数据文件,存放 DSM-5 精神障碍数据 |
| pyproject.toml | TOML 配置 | PDM 项目定义,声明依赖和构建配置 |
| pdm.lock | 锁定文件 | 锁定所有依赖的精确版本 |
| .gitignore | 配置文件 | 让 Git 忽略虚拟环境和缓存 |
| LICENSE | 许可证 | 明确开源使用边界 |
| .pdm-python | 文本记录 | 记录 PDM 关联的 Python 解释器路径 |
| readme.rst / readme.txt | ReStructuredText/文本 | 使用说明 |
从这套布局可以看出两个信号:第一,作者用了 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(unique=True) # 诊断编码,如 F32.0 name: str = Field(index=True) # 障碍名称,做普通查询索引 category: str = Field(index=True) # 所属大类,如 抑郁障碍 specifiers: List[str] = Field(default_factory=list) # 严重程度/病程标注 criteria_ref: List[str] = Field(default_factory=list) # 引用的诊断标准分组 symptoms: List[str] = Field(default_factory=list) # 关联症状词条 notes: Optional[str] = None # 补充说明,允许为空这里Field(unique=True)设唯一约束,防止同一个诊断编码被重复导入;Field(index=True)给名称和类别加索引,按类别筛选时不用全表扫描;default_factory=list表示这个字段缺省时创建空列表,避免多个实例共享同一个可变对象——这是 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(encoding="utf-8")) engine = AIOEngine(client=AsyncIOMotorClient("mongodb://localhost:27017"), database="dsm5") 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 的异步引擎,database="dsm5"指定库名,集合名默认由模型类名推导为小写复数形式,也就是 dsm5disorders。read_text(encoding="utf-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(client=AsyncIOMotorClient("mongodb://localhost:27017"), database="dsm5") 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(client=AsyncIOMotorClient("mongodb://localhost:27017"), database="dsm5") 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(), key=lambda 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(client=AsyncIOMotorClient("mongodb://localhost:27017"), database="dsm5") 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(key=lambda x: (x[2], x[3]), reverse=True) 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(client=AsyncIOMotorClient("mongodb://localhost:27017"), database="dsm5") docs = await engine.find(DSM5Disorder) with open(path, "w", newline="", encoding="utf-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-8,Windows 下默认编码可能是 GBK,中文直接解错;第二是 JSON 数据文件本身可能被ensure_ascii=True写成了全转义形式,Python 的 json 库读到转义串默认也能还原,但如果你用文本编辑器打开看,全是\uXXXX,容易误以为数据坏了。
解决:读取时显式声明编码,写入时按需关闭 ASCII 转义。
import json # 读取时显式指定 UTF-8 raw = json.loads(open("json_docs/disorders.json", encoding="utf-8").read()) # 写回时保留中文原样,方便 git diff 审阅 with open("disorders_pretty.json", "w", encoding="utf-8") as f: json.dump(raw, f, ensure_ascii=False, indent=2)ensure_ascii=False让中文以原始字符写入文件,indent=2让嵌套结构可读。从那以后我拿到任何含中文的 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", encoding="utf-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 展平为 DataFrame;explode("symptoms")把每个障碍的症状列表拆成多行,一行一个症状;value_counts()统计每个症状出现在多少种障碍中。得到的排名表能直观看出哪些是非特异性症状,比如睡眠障碍、焦虑情绪这类高频症状跨越多个诊断类别。这份排名反过来也能提醒你:恰恰是这些症状,鉴别诊断的价值最低,因为它们到处都有。
如果你想做更正式的共病矩阵,用交叉表就能出结果。
matrix = df.explode("symptoms").assign(present=1).pivot_table( index="code", columns="symptoms", values="present", aggfunc="sum" ).fillna(0)pivot_table生成一个以诊断编码为行、以症状为列的 0/1 矩阵。这个矩阵是后续做因子分析、层次聚类和网络图的标准输入格式。从这份源码包到矩阵,一共就这几行代码,中间没有任何数据库参与。
这一步做完,你能把一个静态的 DSM-5 数据源变成可喂给机器学习模型的表格数据。从那以后我拿到任何一个 JSON 结构的医学数据源码包,都会先确认 json_normalize 能撑住多少层嵌套,再决定要不要上数据库。不少分析场景其实根本不需要 MongoDB,pandas 直读 JSON 反而是后悔药最少的那条路。希望这个思路帮到你。
本文还有配套的精品资源,点击获取