news 2026/10/3 10:32:32

中医皮肤病知识图谱构建与辅助诊断:Neo4j与Python实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
中医皮肤病知识图谱构建与辅助诊断:Neo4j与Python实战

简介:本资源是一套基于Python实现的真菌性中医皮肤病知识图谱及辅助诊断系统,面向计算机、中医药信息化等专业的毕业设计、课程设计与项目开发学习者,帮助解决从知识图谱构建到智能问诊落地的完整实践问题。压缩包共20个文件,约12.26MB,包含6个Python源码文件、6张结果图、4个Excel词典与转置数据、2个JSON疾病与病例数据,以及LICENSE和README说明,覆盖建图、信息抽取、层次聚类、关联规则、诊断模型与疗效评估等模块。目前已有210人学习下载。读者可获得可直接运行的源码与清晰目录结构,参考病因、临床表现、药方、治疗方法的层次聚类树状图,理解Neo4j知识图谱搭建、辅助诊断与治疗方案推荐流程,并在此基础上延伸开发,适合作为完整项目方案与排错参考。

1. 真菌性中医皮肤病知识图谱:从散落医案到可查询的诊断网络

手上有一批中医皮肤科的真菌感染医案,几百份,格式五花八门——有的是老大夫手写的扫描件,有的是Excel里零散记录的症候和方剂,还有一部分是论文附录里的结构化数据。想从中快速回答一个问题:“湿热下注型的足癣,常用哪些中药组合?”——靠翻文件,半天找不全;靠全文检索,搜出来的东西没有关联性。这就是我做这个知识图谱的起点。

这个系统要解决的核心问题,是把中医皮肤病领域里散落的“证型—症状—方剂—中药—真菌类型”这几类实体,用图结构串起来,再在上面挂一个辅助诊断的推理层。输入一组症状,系统沿着图谱的关系路径给出可能的证型排序和对应的方剂参考。适合谁看?做中医信息化、医学知识图谱、或者毕业设计选题落在“知识图谱+垂直领域”的同学。Python做图数据构建和推理,Neo4j做存储和可视化,前端给一个简单的查询界面。整套东西不复杂,但坑不少,下面把我走过的路拆开讲。

2. 中医皮肤病知识图谱的数据层:实体、关系怎么定,本体怎么搭

2.1 先定本体:六类实体和它们的关系骨架

知识图谱的第一步不是写代码,是画本体。中医皮肤病这个领域,我最终收敛到六类核心实体:

实体类型说明示例
疾病西医病名+中医病名足癣(脚湿气)
证型中医辨证分型湿热下注、血虚风燥
症状患者表现水疱、糜烂、瘙痒
方剂经典方或经验方龙胆泻肝汤
中药方剂组成龙胆草、黄芩
真菌病原微生物红色毛癣菌、须癣毛癣菌

关系定义同样关键。我用的关系类型包括:疾病-有证型->证型、证型-表现为->症状、证型-推荐方剂->方剂、方剂-包含中药->中药、疾病-由真菌引起->真菌、中药-归经->经络。注意,这里不要贪多。我一开始加了“体质”“季节”“年龄”等实体,结果数据稀疏得没法用,关系边大量为空。本体建模的原则是:先保证核心链路的数据密度,再考虑扩展。

提示:本体设计阶段建议用Protégé或简单的YAML文件先画出来,别急着写代码。改本体的成本远低于改代码。

2.2 数据清洗:从医案文本到结构化三元组

原始数据里最麻烦的是症状描述。比如“足趾间浸渍发白,基底潮红,渗液明显”,这是一句话,但需要拆成多个症状实体。我的做法是维护一个症状词典,用最大正向匹配做初步切分,再人工校对。

import re import pandas as pd # 症状词典(实际项目中约300个词条) symptom_dict = [ "浸渍发白", "基底潮红", "渗液", "水疱", "糜烂", "瘙痒", "脱屑", "角化过度", "皲裂", "丘疹" ] def extract_symptoms(text): """从医案描述中抽取症状实体""" found = [] for sym in symptom_dict: if sym in text: found.append(sym) return list(set(found)) # 读取原始医案数据 df = pd.read_csv("raw_cases.csv") df["symptoms"] = df["description"].apply(extract_symptoms) # 过滤掉没有抽到症状的记录 df = df[df["symptoms"].map(len) > 0] print(f"有效记录数: {len(df)}")

这段代码的逻辑很直白:遍历词典,看哪些症状词出现在描述里。参数方面,symptom_dict的质量直接决定抽取效果,我建议从领域教材和医案中人工整理,不要用通用分词工具硬切——中医术语的边界和普通文本不一样,“浸渍发白”被切成“浸渍”和“发白”就丢了语义。

2.3 证型与方剂的映射:处理一对多和多对多

一个证型可能对应多个方剂,一个方剂也可能服务于多个证型。这种多对多关系在CSV里不好表达,需要拆成独立的关系表。

# 证型-方剂关系表构建 syndrome_formula = [] for _, row in df.iterrows(): syndromes = row["syndrome"].split(";") # 证型可能多个 formulas = row["formula"].split(";") for s in syndromes: for f in formulas: syndrome_formula.append({ "syndrome": s.strip(), "formula": f.strip(), "source": row["source"] }) sf_df = pd.DataFrame(syndrome_formula).drop_duplicates() print(f"证型-方剂关系数: {len(sf_df)}")

这里的关键参数是分隔符。不同来源的数据用的分隔符不一样,有的是中文分号,有的是顿号,有的是逗号。我一般先统计一下所有分隔符的出现频率,再决定用哪个做split。drop_duplicates()是必须的,因为同一对关系可能从多条医案中重复抽取。

3. 用Neo4j构建知识图谱:从CSV到图数据库的完整导入流程

3.1 Neo4j环境准备与约束建立

Neo4j的安装不展开讲,社区版足够用。重点说导入前的准备工作——建约束和索引。没有约束的图数据库,导入速度慢不说,后续查询也容易出重复节点。

// 为每类实体的唯一标识字段建约束 CREATE CONSTRAINT disease_name IF NOT EXISTS FOR (d:Disease) REQUIRE d.name IS UNIQUE; CREATE CONSTRAINT syndrome_name IF NOT EXISTS FOR (s:Syndrome) REQUIRE s.name IS UNIQUE; CREATE CONSTRAINT symptom_name IF NOT EXISTS FOR (sym:Symptom) REQUIRE sym.name IS UNIQUE; CREATE CONSTRAINT formula_name IF NOT EXISTS FOR (f:Formula) REQUIRE f.name IS UNIQUE; CREATE CONSTRAINT herb_name IF NOT EXISTS FOR (h:Herb) REQUIRE h.name IS UNIQUE; CREATE CONSTRAINT fungus_name IF NOT EXISTS FOR (fu:Fungus) REQUIRE fu.name IS UNIQUE;

约束建好之后,导入时如果遇到重复节点,Neo4j会自动走MERGE逻辑而不是CREATE,避免图谱里出现两个“湿热下注”。这个坑我踩过——第一次导入没建约束,结果同一个证型出现了17次,查询结果全乱了。

3.2 用Python驱动批量导入节点和关系

导入方式有两种:LOAD CSV和 Python驱动。我选Python驱动,因为数据清洗逻辑已经在Python里了,直接衔接更顺。

from neo4j import GraphDatabase driver = GraphDatabase.driver( "bolt://localhost:7687", auth=("neo4j", "your_password") ) def create_disease_node(tx, name, tcm_name): tx.run( "MERGE (d:Disease {name: $name}) " "SET d.tcm_name = $tcm_name", name=name, tcm_name=tcm_name ) def create_relation(tx, from_label, from_name, rel_type, to_label, to_name): query = ( f"MATCH (a:{from_label} {{name: $from_name}}) " f"MATCH (b:{to_label} {{name: $to_name}}) " f"MERGE (a)-[:{rel_type}]->(b)" ) tx.run(query, from_name=from_name, to_name=to_name) with driver.session() as session: # 导入疾病节点 for _, row in disease_df.iterrows(): session.execute_write( create_disease_node, row["name"], row["tcm_name"] ) # 导入证型-症状关系 for _, row in ss_df.iterrows(): session.execute_write( create_relation, "Syndrome", row["syndrome"], "MANIFESTS_AS", "Symptom", row["symptom"] )

参数说明:MERGE而不是CREATE,保证幂等性,重复执行不会产生重复节点。关系类型用大写蛇形命名,这是Neo4j社区的惯例。execute_write会自动处理事务,批量导入时比手动开事务省心。

注意:数据量超过1万条关系时,逐条execute_write会明显变慢。我一般每500条攒一个批次,用UNWIND批量写入,速度能提升5到8倍。

3.3 图谱质量校验:三个必查指标

导入完成后,别急着做上层应用。先跑几个校验查询:

// 1. 孤立节点检查:没有任何关系的节点 MATCH (n) WHERE NOT (n)--() RETURN labels(n), n.name LIMIT 25; // 2. 关系密度:每个证型平均关联多少症状 MATCH (s:Syndrome)-[:MANIFESTS_AS]->(sym:Symptom) RETURN s.name, count(sym) AS symptom_count ORDER BY symptom_count DESC LIMIT 10; // 3. 方剂覆盖度:有多少证型还没有关联方剂 MATCH (s:Syndrome) WHERE NOT (s)-[:RECOMMENDS]->(:Formula) RETURN count(s) AS uncovered_syndromes;

孤立节点通常是数据清洗时漏掉的,关系密度反映图谱的信息丰富度,方剂覆盖度直接决定辅助诊断能不能给出方剂建议。我一般要求方剂覆盖度不低于85%,否则回去补数据。

4. 辅助诊断推理层:从症状输入到证型排序的实现

4.1 基于症状匹配的证型评分算法

辅助诊断的核心逻辑是:用户输入一组症状,系统在图谱中查找哪些证型关联了这些症状,按匹配度排序。最简单的做法是Jaccard相似度,但中医辨证有个特点——某些症状是“主症”,权重应该更高。

def score_syndrome(input_symptoms, syndrome_symptoms, main_symptoms): """ input_symptoms: 用户输入的症状列表 syndrome_symptoms: 该证型关联的全部症状 main_symptoms: 该证型的主症列表(权重2.0) """ input_set = set(input_symptoms) syndrome_set = set(syndrome_symptoms) main_set = set(main_symptoms) # 基础匹配分 match = input_set & syndrome_set if not match: return 0.0 # 加权:主症命中得2分,兼症得1分 score = 0.0 for sym in match: if sym in main_set: score += 2.0 else: score += 1.0 # 归一化:除以该证型的理论最高分 max_score = len(main_set) * 2.0 + (len(syndrome_set) - len(main_set)) * 1.0 return score / max_score if max_score > 0 else 0.0

参数方面,主症和兼症的区分需要从数据中标注。我的做法是在医案里统计每个症状在某个证型下出现的频率,频率前30%的自动标为主症,再人工审核一遍。权重2.0和1.0是经验值,实际调参时可以根据诊断准确率做网格搜索。

4.2 从Neo4j查询候选证型并排序

def diagnose(session, input_symptoms, top_k=5): # 查询所有关联了输入症状的证型 query = """ MATCH (s:Syndrome)-[:MANIFESTS_AS]->(sym:Symptom) WHERE sym.name IN $symptoms WITH s, collect(DISTINCT sym.name) AS matched MATCH (s)-[:MANIFESTS_AS]->(all_sym:Symptom) WITH s, matched, collect(DISTINCT all_sym.name) AS all_symptoms RETURN s.name AS syndrome, matched, all_symptoms """ results = session.run(query, symptoms=input_symptoms) scored = [] for record in results: syndrome = record["syndrome"] matched = record["matched"] all_sym = record["all_symptoms"] # 这里main_symptoms需要从缓存或配置中获取 main_sym = get_main_symptoms(syndrome) score = score_syndrome(input_symptoms, all_sym, main_sym) scored.append((syndrome, score, matched)) scored.sort(key=lambda x: x[1], reverse=True) return scored[:top_k]

这个查询分两步:先找到至少命中一个症状的证型,再拉取这些证型的全部症状用于评分。collect(DISTINCT ...)去重是因为一个证型可能通过多条路径关联到同一个症状。返回结果里带上matched列表,前端可以高亮显示哪些症状命中了。

4.3 方剂推荐与药物组成展开

证型排序出来后,沿着图谱关系往下走一层,拿到方剂和中药。

// 给定证型,查询推荐方剂及其组成中药 MATCH (s:Syndrome {name: $syndrome})-[:RECOMMENDS]->(f:Formula) OPTIONAL MATCH (f)-[:CONTAINS]->(h:Herb) RETURN f.name AS formula, collect(h.name) AS herbs

OPTIONAL MATCH是必须的——有些方剂在数据里没有完整的药物组成,用普通MATCH会直接丢掉这些方剂。返回结果里如果herbs为空列表,前端标注“组成待补充”,而不是不显示。

5. 避坑与排查:中医知识图谱项目里最容易翻车的五个地方

5.1 症状实体粒度不一致导致匹配失败

现象:用户输入“脚趾缝烂了”,系统匹配不到任何证型。
原因:图谱里的症状实体是“趾间糜烂”,用户口语和标准术语之间有鸿沟。
解决:加一层同义词映射表。我维护了一个synonym_map字典,把口语表达映射到标准症状名。这个表需要持续迭代,初期可以只覆盖高频的20个口语表达,就能解决80%的匹配失败。

5.2 Neo4j导入时中文编码问题

现象:CSV里的中文导入后变成乱码或问号。
原因:Neo4j默认编码不是UTF-8,或者CSV文件本身带了BOM头。
解决:CSV保存时选“UTF-8无BOM”格式。Python读取时指定encoding='utf-8-sig'。导入前用file -i yourfile.csv确认编码。

5.3 证型-方剂关系出现环形依赖

现象:查询某个证型的方剂时,返回结果里出现了证型本身。
原因:数据清洗时把“方剂”字段里混入的证型名也当成了方剂实体。
解决:在导入前加一层实体类型校验。维护一个方剂名词典,不在词典里的值标记为待审核,不直接入库。这个校验步骤花了我半天时间写,但省了后面两周的排查。

5.4 辅助诊断结果总是偏向症状多的证型

现象:不管输入什么症状,“湿热下注”永远排第一。
原因:归一化时没有考虑证型的症状总数差异。“湿热下注”关联了30个症状,而“血虚风燥”只关联了8个,前者的匹配绝对数天然更高。
解决:在评分函数里引入TF-IDF思想——症状越常见的证型,匹配上的权重应该越低。具体做法是给每个症状算一个逆证型频率,乘到匹配分上。

5.5 前端可视化加载慢

现象:图谱节点超过500个时,前端页面卡死。
原因:一次性把所有节点和关系推给前端渲染。
解决:分页加载,或者按查询结果动态渲染。用户查什么证型,就只返回该证型周围两跳以内的子图。Neo4j的apoc.path.subgraphNodes可以帮上忙,但社区版需要额外装APOC插件。

6. 把图谱用起来:三个进阶技巧和我的调试习惯

第一个技巧是用图谱做方剂配伍规律挖掘。辅助诊断只是最浅层的应用。你可以跑这样的查询:找出所有治疗“湿热下注”的方剂中,共同出现频率最高的三味药组合。这比单纯的证型推荐更有临床参考价值。

MATCH (s:Syndrome {name: "湿热下注"})-[:RECOMMENDS]->(f:Formula)-[:CONTAINS]->(h:Herb) WITH h.name AS herb, count(DISTINCT f) AS freq ORDER BY freq DESC LIMIT 10 RETURN collect(herb) AS top_herbs, collect(freq) AS frequencies

第二个技巧是给关系加权重。目前图谱里的关系都是布尔型的——存在或不存在。但“龙胆泻肝汤”和“湿热下注”的关联强度,显然比某个冷门方剂要高。我一般用医案中出现频次做权重,写入关系属性weight,查询时按权重排序。

第三个技巧是定期做图谱的连通性分析。用apoc.path.expandConfig检查从疾病节点出发,平均多少跳能到达中药节点。如果超过4跳,说明中间层数据有缺失,需要补全。

调试习惯方面,我每次改完数据导入逻辑,都会先在一个测试库上跑一遍,用MATCH (n) RETURN count(n)对比节点数,用MATCH ()-[r]->() RETURN count(r)对比关系数。数字对不上,绝不往生产库导。这个习惯帮我省了至少三次全量重导的麻烦。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 10:32:15

AI工作流可靠性加固:契约、状态机与CI三道护栏

1. 一次漏调引发的思考:AI 工作流为什么总在关键时刻掉链子前阵子我在用 MiMo V2.6 搭一套自动化内容处理流程,遇到一个特别典型的问题:模型明明在系统提示里被明确告知“每次输出前必须调用format_check这个 skill”,结果十次里有…

作者头像 李华
网站建设 2026/10/3 10:32:15

OpenAI兼容协议接入GLM实战:5分钟跑通与避坑指南

1. 为什么我最终选了 Ace Data Cloud 接 GLM,而不是自己直连先说结论:如果你手上已经有一套跑在 OpenAI 接口协议上的代码,想换成 GLM 系列模型,最省事的路径不是去改 SDK、改请求体、改鉴权逻辑,而是找一个兼容 OpenA…

作者头像 李华
网站建设 2026/10/3 10:31:56

HOOPS Visualize Web 2026.1.0:工业级三维可视化引擎技术解析

韦博偏向”为用户构建专业工程可视化应用,这些场景都不允许“差不多就行”的渲染水准,它们要求的是一套真正为工业模型设计、能直接嵌入企业级软件流程的完整技术栈。HOOPS Visualize Web给出的答案,是把在CAD/CAM/CAE领域沉淀了几十年的图形…

作者头像 李华
网站建设 2026/10/3 10:31:20

C51单片机驱动ILI9341彩屏实战:时序、资源与Proteus仿真全解析

1. 项目概述:为什么一个51单片机驱动彩屏的项目值得花时间深挖? C51单片机驱动ILI9341彩屏,听起来像是教科书里一笔带过的“外设扩展”案例,但实际动手做过的人才知道——这根本不是简单接几根线、调几个寄存器就能点亮的事。我第…

作者头像 李华
网站建设 2026/10/3 10:30:45

OpenShell:AI自然语言终端命令助手解析与实战

先把结论放在前面:如果你是个每天要在终端里待好几个小时的人,OpenShell 绝对值得你抽一天时间把它折腾明白。我接触这个开源项目是在一次临时要处理两个集群配置同步的时候,手动敲命令敲到怀疑人生,然后顺手试了一下 OpenShell&a…

作者头像 李华
网站建设 2026/10/3 10:30:28

东方财富股吧爬虫实战:异步接口分析、并发采集与MongoDB存储

简介:这份东方财富股吧爬虫项目以Selenium模拟用户操作,抓取指定股票的帖子与评论信息,并写入MongoDB,支持多线程同时抓取多支股票。实现上分为入口、抓取、解析、存储等模块,另附readme.pdf与README.md详细操作说明、…

作者头像 李华