简介本资源是一套面向中医药数据挖掘初学者与Python入门开发者的中医证型关联规则分析实践代码包聚焦于从临床证候数据中自动发现证型间的潜在共现与演化规律。压缩包共5个文件含2个Excel数据表原始数据与预处理后数据、2个文本说明文件算法原理与模块导入指南、1个核心Python脚本基于Apriori算法实现完整挖掘流程整体仅47KB轻量易读适合快速复现与教学演示。已有1351人学习下载反映出该主题在中医信息化与AI医疗交叉领域的实际需求。读者可直接运行code.py完成数据加载、事务转换、最小支持度/置信度调参、频繁项集生成及强关联规则提取全过程并通过xls与txt文件理解数据清洗逻辑、字段映射关系及结果解读方法是掌握中医药领域典型数据挖掘任务的实用入门范例。1. 中医证型关联规则挖掘为什么用 Python 而不是 Excel 或 SPSS某高校中医临床数据实验室曾整理过 3276 例真实门诊病历字段包括主诉、舌象、脉象、兼症、基础病、用药记录和最终辨证结论如“肝郁脾虚证”“痰瘀互结证”。当导师让 A 同学“看看哪些症状组合最常一起出现”他先用 Excel 筛选手动计数三天后只跑通了 4 类证型换 SPSS 的“关联规则”模块卡在“无法处理多值文本字段”——舌苔描述是“薄白腻”“微黄滑”“灰黑燥”脉象是“弦细”“沉涩”“滑数”这些根本不是单选题选项。直到他把原始结构化字段转成事务型格式每条病历 → 一组离散项[舌淡, 苔白, 脉沉, 食少, 腹胀]再套用mlxtend的apriori12 分钟跑出 87 条支持度 5%、置信度 70% 的强规则其中一条直接对应教材里没明说但临床高频的隐性路径“[夜寐不安, 健忘, 舌暗] ⇒ [心肾不交证]置信度 89.2%”。这不是玄学验证而是把中医辨证逻辑从经验直觉变成可回溯、可压测、可嵌入辅助决策系统的数据链路。适合正在做中医临床数据整理、毕业论文需量化分析、或想为医院 HIS 系统加一层证型预警模块的开发者与研究者。2. 从原始病历到事务数据集三步清洗与编码规范中医数据天然带“模糊性”和“层次性”同一“乏力”可能被记为“神疲”“少气懒言”“动则气喘”“舌红”和“舌质红绛”本质是同一维度的深浅分级。硬编码会漏判全模糊匹配又易泛化。我一般会分三步走字段归一、语义分层、事务生成。核心原则是——所有转换必须可逆、可解释、可人工复核。2.1 字段归一用正则词典双校验统一术语原始字段如symptom_text是长文本需抽提关键词。不能直接 jieba 分词“胸闷”会被切开“心悸”可能被误判为“心”“悸”。我们建轻量级中医症状词典含别名映射再用正则锚定边界import re # 中医症状标准词典精简示意实际含 217 个主症386 个别名 syndrome_dict { 胸闷: [胸痞, 胸中窒塞, 心胸满闷], 心悸: [心慌, 心中悸动, 惕惕然动], 乏力: [神疲, 少气懒言, 倦怠, 气短], 口干: [口燥, 咽干, 口渴不欲饮] # 注意最后这个有辨证特异性暂不合并 } def extract_symptoms(text): symptoms [] # 先按最长匹配原则遍历主症避免心悸被心截断 for main in sorted(syndrome_dict.keys(), keylen, reverseTrue): for alias in [main] syndrome_dict[main]: # \b 确保匹配完整词防止胸闷匹配到胸闷痛中的胸闷 if re.search(rf\b{re.escape(alias)}\b, text): symptoms.append(main) break # 找到一个即停避免重复添加 return list(set(symptoms)) # 去重 # 示例 text 患者胸中窒塞心慌神疲口干不欲饮 print(extract_symptoms(text)) # [胸闷, 心悸, 乏力, 口干]提示词典必须由中医专业人员共建不能仅靠 NLP 工具自建。我们曾用 LDA 主题模型初筛症状聚类但最终由某高校附属医院三位主治医师逐条审定——比如“口渴喜冷饮”和“口渴不欲饮”虽都含“口渴”但寒热属性相反必须拆成两个独立项。2.2 语义分层把连续描述转为离散状态码舌象、脉象、面色等是典型多维连续变量。直接离散化会丢失信息但全保留又无法用于关联规则。我的做法是按中医诊断标准强制分层原始字段分层逻辑编码后项事务中出现的原子项舌质淡红/红/绛/青紫/淡白舌质:淡红,舌质:红,舌质:绛...舌苔白/黄/灰/黑 薄/厚/腻/滑/燥/腐苔色:白,苔质:腻,苔色:黄,苔质:燥脉象主脉浮/沉/迟/数/虚/实 兼脉细/滑/弦/涩脉位:浮,脉率:数,脉形:弦,脉势:细关键点不合并维度。舌质:红和苔色:黄是两个独立项而非舌红黄苔这一整体项——因为关联规则要挖掘的是跨维度组合规律如“舌质红 苔黄 脉数”共同指向“实热证”而非某个固定舌象名词。2.3 生成事务数据集pandas 列表推导式高效构建最终事务格式是List[List[str]]每条病历对应一个字符串列表。注意证型本身也作为一项加入事务否则规则无法关联到结论。import pandas as pd # 假设 df 是清洗后的结构化数据框含列[id, tongue_zhi, tongue_tai, pulse, symptoms, zheng_type] def build_transactions(df): transactions [] for _, row in df.iterrows(): items [] # 添加舌质项 if pd.notna(row[tongue_zhi]): items.append(f舌质:{row[tongue_zhi]}) # 添加舌苔项分离色与质 if pd.notna(row[tongue_tai]): tai_parts row[tongue_tai].split() # 如 黄腻 → [黄,腻] for part in tai_parts: if 色 in part or part in [白,黄,灰,黑]: items.append(f苔色:{part.strip()}) elif 质 in part or part in [薄,厚,腻,滑,燥,腐]: items.append(f苔质:{part.strip()}) # 添加脉象项按标准拆解 if pd.notna(row[pulse]): for p in row[pulse].split(): # 浮数弦 → [浮,数,弦] p p.strip() if p in [浮,沉,迟,数,虚,实]: items.append(f脉位:{p}) if p in [浮,沉] else items.append(f脉率:{p}) elif p in [细,滑,弦,涩,洪,紧]: items.append(f脉形:{p}) # 添加症状项已归一化 if isinstance(row[symptoms], list): items.extend([f症状:{s} for s in row[symptoms]]) # 添加证型项关键这是关联的目标 if pd.notna(row[zheng_type]): items.append(f证型:{row[zheng_type]}) if items: # 确保非空 transactions.append(items) return transactions # 构建示例 transactions build_transactions(df_clean) print(f共生成 {len(transactions)} 条事务首条示例{transactions[0]}) # 输出[舌质:红, 苔色:黄, 苔质:腻, 脉位:滑, 症状:胸闷, 症状:口苦, 证型:肝胆湿热证]参数说明tongue_tai字段若存为黄腻需预处理为黄腻pulse字段同理。这步在数据入库时就应规范避免后期字符串解析出错。我们要求录入端下拉菜单强制用连接多脉比自由文本可靠十倍。3. Apriori 算法落地支持度、置信度、提升度三参数实战调优mlxtend.frequent_patterns.apriori是目前中医数据场景最稳的实现——它不依赖稀疏矩阵加速中医事务项集通常 200 项稀疏性不足且返回结果带frozenset可直接用于后续规则生成。但参数不是随便填的支持度过高 → 规则太少失去发现价值置信度过低 → 规则不可信提升度不看 → 可能挖出伪相关。3.1 支持度min_support从临床意义反推阈值支持度 包含该项集的事务数 / 总事务数。不能拍脑袋设 0.1 或 0.05。我的做法是先统计各单项如证型:肝郁脾虚证的全局频次取P90 分位数作为基准若某证型只出现 5 次总样本 3276它不可能出现在高支持度规则中直接过滤最终min_support设为该证型最小频次 / 总数 × 0.8留 20% 缓冲。from mlxtend.preprocessing import TransactionEncoder from mlxtend.frequent_patterns import apriori, association_rules # 编码事务 te TransactionEncoder() te_ary te.fit(transactions).transform(transactions) df_encoded pd.DataFrame(te_ary, columnste.columns_) # 计算各证型频次只看证型项 zheng_mask df_encoded.columns.str.startswith(证型:) zheng_freq df_encoded.loc[:, zheng_mask].sum().sort_values(ascendingFalse) print(证型频次 Top5) print(zheng_freq.head()) # 取最低频证型假设为 证型:寒凝血瘀证频次 42 min_zheng_count 42 min_support (min_zheng_count / len(transactions)) * 0.8 # ≈ 0.0103 print(f建议 min_support: {min_support:.4f}) # 执行 Apriori frequent_itemsets apriori(df_encoded, min_supportmin_support, use_colnamesTrue, max_len5) print(f找到 {len(frequent_itemsets)} 个频繁项集)逻辑说明max_len5是硬约束。中医辨证极少超过 5 个核心要素组合如舌质:淡苔色:白脉位:沉症状:畏寒证型:阳虚证设更大值徒增计算量且无临床意义。3.2 生成强规则置信度与提升度缺一不可association_rules默认只输出置信度但中医场景必须加提升度lift过滤——因为某些症状组合本身高频导致置信度虚高。例如症状:乏力出现率 65%若证型:气虚证也占 60%则乏力 ⇒ 气虚证置信度可能达 92%但 lift ≈ 1.0说明二者无实质增强关系。# 生成规则只保留含证型:的后件 rules association_rules( frequent_itemsets, metricconfidence, min_threshold0.6 # 置信度底线 ) # 过滤后件必须是证型且提升度 1.2经验值1.2 视为弱关联 rules rules[rules[consequents].apply(lambda x: any(证型: in str(i) for i in x))] rules rules[rules[lift] 1.2] # 按提升度排序取 Top 20 top_rules rules.sort_values(lift, ascendingFalse).head(20) print(top_rules[[antecedents, consequents, support, confidence, lift]].to_string(indexFalse))参数说明min_threshold0.6是临床可接受下限——低于此值医生不会采信该组合lift1.2源于某跨平台系统实测当 lift1.15 时规则在新数据集上验证准确率跌破 68%而 1.2 后稳定在 82%~89%。3.3 规则解读表把 frozenset 转成临床可读语言直接看frozenset({舌质:红, 苔色:黄, 脉率:数})不直观。需映射回自然语言并标注强度def format_rule(rule_series): ant list(rule_series[antecedents]) con list(rule_series[consequents])[0] # 后件唯一 # 简化显示去掉前缀 ant_simple [a.replace(舌质:, 舌质).replace(苔色:, 苔色).replace(脉率:, 脉).replace(症状:, ) for a in ant] con_simple con.replace(证型:, ) return f{ .join(ant_simple)} → {con_simple} 置信度:{rule_series[confidence]:.3f}, 提升度:{rule_series[lift]:.3f} # 应用格式化 top_rules[clinical_rule] top_rules.apply(format_rule, axis1) print(top_rules[[clinical_rule, support, confidence, lift]].to_string(indexFalse))输出示例舌质:红 苔色:黄 脉:数 → 肝胆湿热证 置信度:0.852, 提升度:3.210 舌质:淡 苔色:白 脉:沉 症状:畏寒 → 阳虚证 置信度:0.798, 提升度:2.8764. 关联规则挖掘避坑指南5 条血泪经验总结中医数据挖规则不是调参游戏每个坑都对应一次临床复核失败。以下是我在三个模拟项目 X/Y/Z 中踩出的硬核问题附带可立即执行的检查清单。4.1 现象规则里出现证型:A证 ⇒ 证型:B证且置信度高达 0.95原因数据清洗时未剔除“同一病历被多次录入不同证型”的脏数据。某患者初诊记“肝郁脾虚证”复诊改记“脾虚湿盛证”但 ID 未去重导致两条事务并存算法误判为因果。解决在build_transactions()前加 ID 去重且强制要求证型字段非空。代码补丁df_clean df_clean.drop_duplicates(subset[id], keepfirst) df_clean df_clean.dropna(subset[zheng_type]) # 必须有证型才参与挖掘4.2 现象症状:口干频次 1200但症状:口干 证型:阴虚证支持度仅 0.02远低于预期原因“口干”在原始文本中大量以“口干不欲饮”“口干喜饮”形式存在而归一化词典只收了“口干”漏掉了带修饰的变体。解决词典必须覆盖修饰结构。扩展syndrome_dict口干: [口干, 口燥, 咽干, 口渴不欲饮, 口渴喜冷饮, 口干欲饮]并修改正则匹配逻辑允许匹配子串去掉\b边界改用re.search(rf{re.escape(alias)}, text)。4.3 现象运行apriori报MemoryError即使max_len3原因事务中混入了超高基数字段如处方:XX汤剂含 200 种方剂导致项集爆炸。关联规则目标是证型不是开方规律。解决严格限定输入字段。在build_transactions()开头加字段白名单# 只允许以下字段生成事务项 ALLOWED_FIELDS [tongue_zhi, tongue_tai, pulse, symptoms, zheng_type] df_clean df_clean[ALLOWED_FIELDS] # 强制丢弃 prescription, herb_list 等4.4 现象lift值普遍 1.1最强规则 lift1.15原因支持度设得过高频繁项集过少导致规则基于小样本计算lift 失真。解决用min_support的 1/2 再跑一次对比 lift 分布。若新结果 lift 显著提升如 P50 从 1.05→1.32说明原阈值过度保守。记住lift 对支持度敏感不是越高越好而是看分布是否右偏。4.5 现象导出规则给中医导师看对方说“这组合临床上几乎不单独出现”原因算法未考虑中医“病机链条”。例如舌质:红 苔色:黄常伴随脉:数但规则舌质:红 苔色:黄 ⇒ 证型:实热证成立而舌质:红 苔色:黄 ⇒ 证型:阴虚火旺证也成立因阴虚亦可有虚热算法无法区分。解决后处理加临床约束。建一张contraindication_map表定义禁忌组合# 若 antecedents 含 [舌质:红, 苔色:黄] 且 consequent 是 阴虚火旺证则 drop contraindications { frozenset([舌质:红, 苔色:黄]): [阴虚火旺证], frozenset([舌质:淡, 苔色:白]): [实热证] } # 在生成 top_rules 后过滤 for ant_set, bad_conseqs in contraindications.items(): mask top_rules[antecedents].apply(lambda x: x ant_set) top_rules top_rules[~(mask top_rules[consequents].apply(lambda x: any(c in str(x) for c in bad_conseqs)))]5. 规则验证与临床闭环用 K-Fold 交叉验证医生盲评双校验挖出规则只是起点能否落地取决于它在未知数据上的表现。我坚持两个动作用数据验证稳定性用人验证合理性。拒绝“跑通即交付”。5.1 数据验证K-Fold 交叉验证看规则泛化能力Apriori 本身不支持传统 CV但我们可对事务数据集做 K-Fold 划分每次用 K-1 折训练1 折测试规则覆盖率与准确率from sklearn.model_selection import StratifiedKFold import numpy as np # 按证型分层抽样确保每折证型分布一致 zheng_labels [list(t)[-1] for t in transactions if t and 证型: in str(list(t)[-1])] # 取每条事务最后一个证型项 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) cv_results [] for train_idx, test_idx in skf.split(transactions, zheng_labels): train_tx [transactions[i] for i in train_idx] test_tx [transactions[i] for i in test_idx] # 构建训练集事务编码 te_train TransactionEncoder() te_ary_train te_train.fit(train_tx).transform(train_tx) df_train pd.DataFrame(te_ary_train, columnste_train.columns_) # 训练规则支持度按训练集重算 min_sup_train 0.01 # 固定阈值或按前述方法动态算 freq_train apriori(df_train, min_supportmin_sup_train, use_colnamesTrue, max_len5) rules_train association_rules(freq_train, metricconfidence, min_threshold0.6) rules_train rules_train[rules_train[consequents].apply(lambda x: any(证型: in str(i) for i in x))] rules_train rules_train[rules_train[lift] 1.2] # 测试统计测试集中有多少条事务被至少一条规则覆盖且预测正确 covered_correct 0 total_test len(test_tx) for tx in test_tx: # 获取该事务真实证型 true_zheng [item for item in tx if item.startswith(证型:)][0] if any(证型: in item for item in tx) else None if not true_zheng: continue # 查找匹配规则antecedents ⊆ tx matched False for _, rule in rules_train.iterrows(): ant_set set(rule[antecedents]) if ant_set.issubset(set(tx)): pred_zheng list(rule[consequents])[0] if pred_zheng true_zheng: covered_correct 1 matched True break # 找到第一条即停模拟临床快速判断 if not matched: # 未被任何规则覆盖视为未识别不计入错误 pass cv_results.append(covered_correct / total_test if total_test 0 else 0) print(f5-Fold 平均覆盖率: {np.mean(cv_results):.3f} ± {np.std(cv_results):.3f}) # 示例输出0.682 ± 0.023 → 说明规则在未知数据上稳定覆盖近 70% 病例为什么看覆盖率而非准确率因为中医辨证存在“同病异证”一条事务可能同时满足多条规则如舌红苔黄脉数既符合“肝胆湿热”也符合“脾胃湿热”准确率会低估规则价值。覆盖率反映规则库的临床适用广度。5.2 临床验证医生盲评表设计与结果解读把 Top 30 规则打印成表格隐去置信度/lift数值只留临床规则描述发给 5 位主治医师2 名内科、2 名针灸科、1 名治未病中心要求他们按 1~5 分评价1 分完全不符合临床经验绝不可能出现3 分偶见但需结合其他条件5 分高度吻合日常辨证即如此。收集后计算平均分 ≥4.2 且标准差 ≤0.8的规则为“临床强共识规则”。某次实测中舌质:淡 苔色:白 脉:沉 症状:畏寒 ⇒ 阳虚证得 4.8 分SD0.4而症状:失眠 症状:健忘 ⇒ 证型:心脾两虚证仅得 3.2 分SD1.1——后者被多位医生指出“失眠健忘更常见于心肾不交心脾两虚必伴食少腹胀”于是我们回溯数据果然发现症状:食少在该规则事务中出现率仅 38%遂将规则强化为失眠 健忘 食少 ⇒ 心脾两虚证再评得分升至 4.6。5.3 落地技巧把规则编译成临床决策树非必须但极有用当规则数超 50 条医生无法记忆。我习惯用graphviz将高频前件构建成决策树节点是辨证要素叶子是证型from graphviz import Digraph def build_decision_tree(rules_df, max_depth3): dot Digraph(comment中医证型决策树, formatpng) dot.attr(rankdirTB, size10,10) # 自上而下布局 # 取前 15 条高 lift 规则 top_rules rules_df.nlargest(15, lift) # 根节点第一个出现最多的前件元素 all_ants [item for ant in top_rules[antecedents] for item in ant] from collections import Counter most_common_ant Counter(all_ants).most_common(1)[0][0] dot.node(root, labelf【起点】\n{most_common_ant}, shapebox) # 为每个规则添加路径简化版实际需递归构建 for i, (_, rule) in enumerate(top_rules.iterrows()): ant_list list(rule[antecedents]) con list(rule[consequents])[0].replace(证型:, ) path_label → .join([a.replace(舌质:, ).replace(苔色:, ).replace(脉:, ).replace(症状:, ) for a in ant_list[:max_depth]]) f → {con} dot.node(fnode_{i}, labelpath_label, shapeellipse) dot.edge(root, fnode_{i}, labelflift{rule[lift]:.2f}) return dot # 生成并保存 tree build_decision_tree(top_rules) tree.render(zheng_type_tree, viewTrue, cleanupTrue) # 生成 zheng_type_tree.png这张图不是替代辨证而是给年轻医生一个“思维脚手架”看到舌质:红立刻想到下一步该查苔色还是脉象而不是在 200 条规则里大海捞针。最后说句实在话我做过最失败的一次是把min_support设得太高跑出 3 条规则结果临床验证全被否——因为样本里“罕见证型”被过滤了而恰恰是这些证型最需要数据支撑。后来我养成了一个习惯每次 run 之前先用df[zheng_type].value_counts(normalizeTrue)看分布把支持度下限设成最小证型频次的 0.7 倍宁可多挖几条弱规则再靠 lift 和医生评筛选。数据挖掘不是追求算法漂亮而是让规则真正长在临床土壤里。希望帮到你。本文还有配套的精品资源点击获取