Files

523 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
深度数据挖掘 - 精修版
修正1: 药材名必须完整(>=2字)才计入统计
修正2: 去重短名(如同时匹配到'白术'和'术',只取'白术')
修正3: 相似度计算排除只有一位共享的情况
修正4: 功效网络按功效关键词汇总,不再保留完整描述
"""
import json
import os
import re
import glob
from collections import Counter, defaultdict, OrderedDict
from itertools import combinations
base = "/home/songyi/Documents/ai_agent_scraper_study/data/大医网"
OUT = os.path.join(base, "03_关联融合/深度挖掘迭代/分析_精修")
os.makedirs(OUT, exist_ok=True)
# 加载药材
herbs = {}
all_herb_names = []
for f in glob.glob(os.path.join(base, "01_来源数据/中药材", "*.json")):
with open(f, encoding='utf-8') as fh:
d = json.load(fh)
n = d.get('名称', '').strip()
if n:
herbs[n] = d
all_herb_names.append(n)
aliases = d.get('别名', '')
if aliases:
for a in re.split(r'[、,,;]', aliases):
a = a.strip()
if a:
all_herb_names.append(a)
# 去重并按长度降序排列
all_herb_names = list(dict.fromkeys(all_herb_names))
sorted_herb = sorted(all_herb_names, key=len, reverse=True)
# 构建纯药材名集合(不含别名,用于后续过滤)
herb_set = set(herbs.keys())
# 加载方剂
formulas = OrderedDict()
for f in glob.glob(os.path.join(base, "01_来源数据/方剂", "*.json")):
with open(f, encoding='utf-8') as fh:
d = json.load(fh)
name = d.get('名称', os.path.basename(f).rsplit('_', 1)[0])
formulas[name] = d
print(f"药材: {len(herbs)} 味")
print(f"方剂: {len(formulas)} 首")
print(f"药材名+别名: {len(all_herb_names)} 个")
# 药材提取 - 精修版
def extract_herbs_exact(text):
"""精确匹配:只匹配完整的药材名,且至少2个字符"""
if not text or len(text) < 3:
return []
clean = re.sub(r'[((][^())]*[))]', '', text)
clean = re.sub(r'\d+[两钱毫升克毫克]', '', clean)
parts = [p.strip() for p in re.split(r'[、,\;;]', clean) if p.strip()]
found = []
for p in parts:
p = p.strip()
if len(p) < 2:
continue
for herb in sorted_herb:
if herb == p or (len(herb) >= 2 and herb in p):
if herb not in found:
found.append(herb)
if len(found) > 30:
break
if len(found) > 30:
break
return found
def extract_herbs_brute(text):
"""暴力匹配:检查所有药材名是否在文本中出现"""
if not text or len(text) < 3:
return []
found = []
for herb in sorted_herb:
if len(herb) >= 2 and herb in text:
if herb not in found:
found.append(herb)
return found
def deduplicate_herbs(herb_list):
"""去重短名:如果同时匹配到'白术'和'术',只保留'白术'"""
if not herb_list:
return herb_list
# 按长度降序去重
result = []
for herb in herb_list:
is_substring = False
for existing in result:
if herb in existing or existing in herb:
is_substring = True
break
if not is_substring:
result.append(herb)
return result
formula_herbs = OrderedDict()
for fname, data in formulas.items():
all_text = ''
for val in data.values():
if isinstance(val, str):
all_text += val
elif isinstance(val, (dict, list)):
all_text += json.dumps(val, ensure_ascii=False)
# 先尝试精确匹配
herbs_found = extract_herbs_exact(all_text)
# 如果结果太少,用暴力匹配
if len(herbs_found) < 3:
herbs_found = extract_herbs_brute(all_text)
# 去重短名
herbs_found = deduplicate_herbs(herbs_found)
# 过滤:只保留在药材库中的名
herbs_found = [h for h in herbs_found if h in herb_set]
formula_herbs[fname] = herbs_found
# 统计
total_links = sum(len(h) for h in formula_herbs.values())
avg = total_links / max(1, len(formula_herbs))
print(f"\n总关联对数: {total_links}")
print(f"平均每方药材数: {avg:.1f}")
# 检查
print("\n精修提取结果 (前10首方):")
for i, (fname, hlist) in enumerate(list(formula_herbs.items())[:10]):
print(f" [{i+1}] {fname}: {len(hlist)}味 -> {hlist[:8]}")
# 逆向索引
herb_formulas = defaultdict(set)
for fname, hlist in formula_herbs.items():
for h in hlist:
herb_formulas[h].add(fname)
# ========== 1. 高频药材 (精修) ==========
print("\n" + "="*60)
print(" 1. 高频药材 Top 30 (精修版)")
print("="*60)
herb_freq = Counter()
for hlist in formula_herbs.values():
for h in hlist:
herb_freq[h] += 1
top30 = herb_freq.most_common(30)
for h, c in top30:
ct = herbs.get(h, {}).get('药材分类', '未知')
func = herbs.get(h, {}).get('功效作用', {})
if isinstance(func, dict):
func_str = func.get('功能', '')
else:
func_str = str(func)[:20]
print(f" {h:10s} {c:4d}首 [{ct:3s}] {func_str[:25]}")
with open(os.path.join(OUT, "精修_高频药材Top30.json"), 'w', encoding='utf-8') as f:
json.dump([{"name": h, "count": c} for h, c in top30], f, ensure_ascii=False, indent=2)
print("\n ✓ 已保存")
# ========== 2. 高频药对 ==========
print("\n" + "="*60)
print(" 2. 高频药对 (2味) Top 20")
print("="*60)
pair_freq = Counter()
for hlist in formula_herbs.values():
u = sorted(list(set(hlist)))
if len(u) >= 2:
for combo in combinations(u, 2):
pair_freq[combo] += 1
top_pairs = [p for p, c in pair_freq.most_common(50) if c >= 5]
for (h1, h2), c in top_pairs[:20]:
func1_data = herbs.get(h1, {}).get('功效作用', {})
func2_data = herbs.get(h2, {}).get('功效作用', {})
func1 = func1_data.get('功能', '')[:10] if isinstance(func1_data, dict) else ''
func2 = func2_data.get('功能', '')[:10] if isinstance(func2_data, dict) else ''
print(f" {h1:>8s} + {h2:>8s}: {c}首方 [{func1[:5]}] [{func2[:5]}]")
with open(os.path.join(OUT, "精修_高频药对Top50.json"), 'w', encoding='utf-8') as f:
json.dump([{"pair": list(p), "count": c} for p, c in pair_freq.most_common(50)], f, ensure_ascii=False, indent=2)
print("\n ✓ 已保存")
# ========== 3. 核心药组 ==========
print("\n" + "="*60)
print(" 3. 核心药组 (3-5味药) Top 10")
print("="*60)
for n in [3, 4, 5]:
triple_freq = Counter()
for hlist in formula_herbs.values():
u = sorted(list(set(hlist)))
if len(u) >= n:
for combo in combinations(u, n):
triple_freq[combo] += 1
top_combs = [(t, c) for t, c in triple_freq.most_common(10) if c >= 5]
print(f"\n Top {n}味药组:")
for combo, c in top_combs[:10]:
names = "+".join(combo)
print(f" {names:>30s}: {c:4d}首方")
# 合并3-5味药组结果
with open(os.path.join(OUT, "精修_核心药组3-5味_Top10.json"), 'w', encoding='utf-8') as f:
res = {}
for n in [3, 4, 5]:
triple_freq = Counter()
for hlist in formula_herbs.values():
u = sorted(list(set(hlist)))
if len(u) >= n:
for combo in combinations(u, n):
triple_freq[combo] += 1
key = f"{n}味药组"
res[key] = [{
"group": "+".join(t),
"count": c
} for t, c in triple_freq.most_common(10) if c >= 5]
json.dump(res, f, ensure_ascii=False, indent=2)
print("\n ✓ 已保存")
# ========== 4. 功效关键词网络 ==========
print("\n" + "="*60)
print(" 4. 功效关键词网络 (按功效词汇总)")
print("="*60)
# 提取核心功效词
func_keywords = Counter()
func_formulas = defaultdict(set)
for h, data in herbs.items():
gua = data.get('功效作用', {})
if isinstance(gua, dict) and gua.get('功能'):
func_str = gua['功能']
# 提取核心功效关键词 (按、和、具有等分隔)
keywords = re.split(r'[、,,具有和的功效]', func_str)
for kw in keywords:
kw = kw.strip()
if len(kw) >= 2 and len(kw) <= 10:
func_keywords[kw] += 1
for fname in herb_formulas.get(h, set()):
func_formulas[kw].add(fname)
# 按出现方剂数排序
top_funcs = [(kw, len(vx)) for kw, vx in func_formulas.items() if len(vx) >= 10]
top_funcs.sort(key=lambda x: -x[1])
print(" Top 30 功效关键词 (按关联方剂数):")
for kw, count in top_funcs[:30]:
examples = list(func_formulas[kw])[:3]
print(f" {kw:12s} 关联 {count:4d}首方 例: {', '.join(examples[:2])}")
with open(os.path.join(OUT, "精修_功效关键词_Top30.json"), 'w', encoding='utf-8') as f:
json.dump([{"keyword": kw, "formula_count": count} for kw, count in top_funcs[:30]], f, ensure_ascii=False, indent=2)
print("\n ✓ 已保存")
# ========== 5. 方剂聚类 ==========
print("\n" + "="*60)
print(" 5. 方剂聚类 (基于最高频功效词)")
print("="*60)
def get_top_func_keyword(herb_list):
"""获取方剂的最高频功效关键词"""
kw_count = Counter()
for herb in herb_list:
if herb in herbs:
gua = herbs[herb].get('功效作用', {})
if isinstance(gua, dict) and gua.get('功能'):
keywords = re.split(r'[、,,具有和的功效]', gua['功能'])
for kw in keywords:
kw = kw.strip()
if len(kw) >= 2 and len(kw) <= 10:
kw_count[kw] += 1
return kw_count
clusters = defaultdict(list)
for fname, hlist in formula_herbs.items():
kw_count = get_top_func_keyword(hlist)
if kw_count:
top_kw = kw_count.most_common(1)[0][0]
clusters[top_kw].append(fname)
big_clusters = [(kw, flst) for kw, flst in clusters.items() if len(flst) >= 5]
big_clusters.sort(key=lambda x: -len(x[1]))
print(f" 发现 {len(clusters)} 个功效簇")
print(f"\n Top 20 大功效簇:")
for kw, flst in big_clusters[:20]:
sample_herbs = set()
for fname in flst[:5]:
for h in formula_herbs[fname]:
sample_herbs.add(h)
print(f" '{kw}' ({len(flst)}首方): 代表药材 -> {', '.join(sorted(sample_herbs)[:8])}")
with open(os.path.join(OUT, "精修_方剂聚类_Top20.json"), 'w', encoding='utf-8') as f:
json.dump([{"cluster": kw, "count": len(flst), "formulas": flst[:20]} for kw, flst in big_clusters[:20]], f, ensure_ascii=False, indent=2)
print("\n ✓ 已保存")
# ========== 6. 配伍禁忌验证 (支持别名匹配) ==========
print("\n" + "="*60)
print(" 6. 配伍禁忌验证 (十八反/十九畏)")
print("="*60)
# 十八反
fan = {
'甘草': ['大戟', '芫花', '甘遂', '京大戟', '红大戟'],
'乌头': ['半夏', '瓜蒌', '天花粉', '贝母', '平贝母', '川贝母', '浙贝母', '白蔹', '白及'],
'藜芦': ['人参', '党参', '丹参', '玄参', '沙参', '苦参', '细辛', '白芍', '赤芍'],
}
# 十九畏
wei = {
'硫黄': ['朴硝', '芒硝', '牙硝'],
'水银': ['铅丹', '砒霜'],
'巴豆': ['牵牛', '牵牛子'],
'丁香': ['郁金'],
'人参': ['五灵脂'],
'肉桂': ['石脂', '赤石脂', '黑锡丹'],
'半夏': ['羊脂', '羊角'],
'厚朴': ['硝石', '滑石'],
}
# 构建别名映射
alias_to_canon = {}
for h, data in herbs.items():
aliases = data.get('别名', '')
if aliases:
for a in re.split(r'[、,,;]', aliases):
a = a.strip()
if a:
alias_to_canon[a] = h
# 扩展禁忌药材:加入别名
def expand_contraindicated(names_list):
"""扩展:将别名也加入匹配列表"""
result = list(names_list)
for name in names_list:
# 检查是否在别名映射中
if name in alias_to_canon:
result.append(alias_to_canon[name])
# 也检查是否在药材名中
if name in herb_set:
pass # 已经在药材名中
# 反过来:查找所有别名
for alt, canon in alias_to_canon.items():
if canon in names_list:
result.append(alt)
return result
contra = defaultdict(int)
contra_details = defaultdict(list)
for fname, hlist in formula_herbs.items():
# 扩展禁忌药材
all_fan = {}
for k, v in fan.items():
all_fan[k] = expand_contraindicated(v)
all_wei = {}
for k, v in wei.items():
all_wei[k] = expand_contraindicated(v)
total_all = {**all_fan, **all_wei}
for herb, contra_set in total_all.items():
if herb in hlist:
for ch in contra_set:
if ch in hlist:
contra[(herb, ch)] += 1
contra_details[(herb, ch)].append(fname)
# 也用别名查找
for alt, canon in alias_to_canon.items():
if alt in hlist and canon == herb:
for ch in contra_set:
if ch in hlist:
contra[(alt, ch)] += 1
contra_details[(alt, ch)].append(fname)
top_contra = sorted(contra.items(), key=lambda x: -x[1])[:15]
if top_contra:
print("\n 发现配伍禁忌:")
for (h1, h2), count in top_contra:
print(f" ⚠️ {h1} + {h2}: {count}首方")
print(f" 方剂: {', '.join(contra_details[(h1, h2)][:3])}")
else:
print("\n 未发现有直接配伍禁忌的方剂 (数据中不存在十八反/十九畏配对)")
# 检查我们找到的药材是否在方剂中出现
print("\n 验证:检索禁忌相关药材在方剂中的出现情况:")
search_names = list(fan.keys()) + list(wei.keys())
search_names.extend(['大戟', '芫花', '半夏', '瓜蒌', '天花粉', '贝母', '人参', '党参', '丹参'])
for sn in search_names:
if sn in herb_formulas:
print(f" {sn}: 出现在 {len(herb_formulas[sn])} 首方中")
print(f" 方剂: {', '.join(list(herb_formulas[sn])[:3])}")
with open(os.path.join(OUT, "精修_配伍禁忌验证.json"), 'w', encoding='utf-8') as f:
json.dump({
"十八反": fan,
"十九畏": wei,
"发现": {f"{h1}+{h2}": c for (h1, h2), c in top_contra},
"详情": {f"{h1}+{h2}": contra_details[(h1, h2)] for (h1, h2) in top_contra}
}, f, ensure_ascii=False, indent=2)
print("\n ✓ 已保存")
# ========== 7. 方剂相似度 (精修) ==========
print("\n" + "="*60)
print(" 7. 方剂相似度 (修正版)")
print("="*60)
def jaccard_fixed(s1, s2):
"""Jaccard相似度,空集=0"""
if not s1 or not s2:
return 0.0
intersection = len(s1 & s2)
if intersection == 0:
return 0.0 # 没有共享药材,相似度为0
union = len(s1 | s2)
return intersection / union
similar_pairs = []
formula_list = list(formulas.keys())
total_formulas = len(formula_list)
print(f" 正在计算 {total_formulas} 首方剂的相似度...")
count = 0
for i in range(len(formula_list)):
f1 = formula_list[i]
h1 = formula_herbs.get(f1, [])
if not h1:
continue
for j in range(i + 1, len(formula_list)):
f2 = formula_list[j]
h2 = formula_herbs.get(f2, [])
if not h2:
continue
s1 = set(h1)
s2 = set(h2)
sim = jaccard_fixed(s1, s2)
if sim >= 0.3:
shared = s1 & s2
similar_pairs.append((f1, f2, sim, len(shared)))
count += 1
print(f" 已检查 {count} 对")
similar_pairs.sort(key=lambda x: (-x[2], -x[3]))
top_similar = similar_pairs[:20]
print(f"\n 找到 {len(similar_pairs)} 对相似方剂 (Jaccard >= 0.3)")
print(f"\n Top 15 相似方剂对:")
for f1, f2, sim, shared_count in top_similar[:15]:
shared_herbs = set(formula_herbs.get(f1, [])) & set(formula_herbs.get(f2, []))
print(f" {f1:30s} + {f2:30s} -> {sim:.3f} (共享{shared_count}味: {', '.join(list(shared_herbs)[:3])})")
with open(os.path.join(OUT, "精修_方剂相似度_Top20.json"), 'w', encoding='utf-8') as f:
json.dump([{"pair1": f1, "pair2": f2, "similarity": round(sim, 4), "shared_count": sc, "shared_herbs": list(formula_herbs.get(f1, []) & formula_herbs.get(f2, []))[:5]} for f1, f2, sim, sc in top_similar], f, ensure_ascii=False, indent=2)
print("\n ✓ 已保存")
# ========== 8. 生成汇总报告 ==========
print("\n" + "="*60)
print(" 8. 汇总报告")
print("="*60)
summary = OrderedDict()
summary["标题"] = "大医网 方剂-中药材 深度挖掘报告 (精修版)"
summary["数据规模"] = {
"药材": len(herbs),
"方剂": len(formulas),
"总关联对数": total_links,
"平均每方药材数": round(total_links / max(1, len(formula_herbs)), 2),
"成功提取方剂": sum(1 for h in formula_herbs.values() if h),
}
# 高频药材前20
summary["高频药材_Top20"] = [{"name": h, "count": c} for h, c in top30[:20]]
# 高频药对前15
summary["高频药对_Top15"] = [{"pair": list(p), "count": c} for p, c in pair_freq.most_common(15)]
# 功效关键词前15
summary["功效关键词_Top15"] = [{"keyword": kw, "formula_count": count} for kw, count in top_funcs[:15]]
# 方剂聚类
summary["方剂聚类"] = [{"cluster": kw, "count": len(flst), "representative": list(flst[:3])} for kw, flst in big_clusters[:10]]
# 配伍禁忌
summary["配伍禁忌"] = {
"十八反": fan,
"十九畏": wei,
"发现": {f"{h1}+{h2}": c for (h1, h2), c in top_contra},
}
# 方剂相似度
summary["方剂相似度_Top10"] = [{"similarity": round(sim, 4), "shared": sc, "pair": f"{f1} + {f2}"} for f1, f2, sim, sc in top_similar[:10]]
summary["输出目录"] = os.path.abspath(OUT)
with open(os.path.join(OUT, "精修_总汇总报告.json"), 'w', encoding='utf-8') as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print("\n 精修深度挖掘完成!")
print(f"\n 输出目录: {os.path.abspath(OUT)}")
print("\n 文件列表:")
for fn in sorted(os.listdir(OUT)):
fp = os.path.join(OUT, fn)
size = os.path.getsize(fp)
print(f" {fn:40s} {size:>10,} B")