255 lines
13 KiB
Python
255 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
药膳全书库 四季标识补标 (参照《四季调养药膳》资料库方法论)
|
|
=============================================================
|
|
参照库: data/四季调养药膳/ (156条, 季节五分法: 春/夏/秋/冬/四季通用,
|
|
性味标注 平59/温54/凉39/热4, 季节×性味交叉验证)
|
|
|
|
本库现状: 两配方集仅有 category (功效类目), 其中仅61条带"春/夏/秋/冬季养生药膳"
|
|
类目, 其余576条无任何四季信息。
|
|
|
|
补标规则 (每条记录型, 如实保留不臆改):
|
|
R1 书源类目优先: category 含 春/夏/秋/冬季养生药膳 → 直接标注该季节 (依据=书源)
|
|
其余类目 → "四季通用" (依据=书源非季节类目, 无季节限定)
|
|
R2 性味聚合: 收集条目全部成分的"性"(中药286种+食物226种功能表),
|
|
归类: 有热→热; 温>凉且温≥2→温; 凉>温且凉≥2→凉; 其余→平; 无成分数据→None
|
|
R3 交叉验证: 季节类目条目的聚合性味与《四季调养药膳》实测基准比对
|
|
(夏凉≥50% / 冬温热≥70% / 秋平≥40% / 春不设卡), 偏离仅注记不改标
|
|
R4 通用条目若聚合性味为纯温/热 → 备注"性味倾向冬补", 供人工复核 (不改标)
|
|
|
|
输出:
|
|
02_加工数据/四季标注_recipes.json 301条逐条标注
|
|
02_加工数据/四季标注_调理药膳.json 336条逐条标注
|
|
02_加工数据/索引_四季分类.json 春夏秋冬通用5组索引(与四季库同构)
|
|
03_关联融合/药膳全书-四季库_关联条目季节对照.json
|
|
04_分析报告/四季补标报告.md
|
|
幂等可重跑。
|
|
"""
|
|
import json, os, re, collections
|
|
|
|
BASE = os.path.dirname(os.path.abspath(__file__))
|
|
SRC = os.path.join(BASE, '01_来源数据')
|
|
PRO = os.path.join(BASE, '02_加工数据')
|
|
LNK = os.path.join(BASE, '03_关联融合')
|
|
RPT = os.path.join(BASE, '04_分析报告')
|
|
|
|
# ---------------- 性味解析 ----------------
|
|
XI = {'寒': '寒', '热': '热', '温': '温', '凉': '凉', '平': '平'}
|
|
MOD = {'微': '', '大': '', '偏': ''} # 微寒计入寒, 大热计入热
|
|
_re_main = re.compile(r'([微大偏]?)([寒热温凉平])')
|
|
_re_any = re.compile(r'[寒热温凉平]')
|
|
|
|
def parse_xing(text):
|
|
"""从性味归经文本解析"性"; 返回 寒/热/温/凉/平 或 None"""
|
|
t = text or ''
|
|
m = _re_main.search(t)
|
|
if m:
|
|
return XI[m.group(2)]
|
|
# 兜底: 无"性X"句式(如"苦,寒;入…经"/"性味甘、平"/"性辛、凉")取首个性字
|
|
m = _re_any.search(t)
|
|
return XI[m.group(0)] if m else None
|
|
|
|
# ---------------- 功能表性味字典 ----------------
|
|
def load(path):
|
|
d = json.load(open(path, encoding='utf-8'))
|
|
return d if isinstance(d, list) else next(v for v in d.values() if isinstance(v, list))
|
|
|
|
herbs = load(os.path.join(SRC, '药膳常用中药功能表.json'))
|
|
foods = load(os.path.join(SRC, '药膳常用食物功能表.json'))
|
|
xing_by_id = {}
|
|
for x in herbs + foods:
|
|
xing_by_id[x['id']] = parse_xing(x.get('性味归经', ''))
|
|
xing_by_name = {x['名称']: xing_by_id[x['id']] for x in herbs + foods}
|
|
|
|
def aggregate(chengfen_xings):
|
|
"""成分性列表 → 聚合性 (规则R2)
|
|
标尺对齐四季库四级制(平/温/凉/热): 寒并入凉向(寒凉同向, 寒>凉仅程度差)"""
|
|
if not chengfen_xings:
|
|
return None
|
|
norm = ['凉' if x == '寒' else x for x in chengfen_xings]
|
|
c = collections.Counter(norm)
|
|
if c['热']:
|
|
return '热'
|
|
if c['温'] > c['凉'] and c['温'] >= 2:
|
|
return '温'
|
|
if c['凉'] > c['温'] and c['凉'] >= 2:
|
|
return '凉'
|
|
return '平'
|
|
|
|
# ---------------- 逐条提取成分性 ----------------
|
|
recipes = load(os.path.join(PRO, '药膳配方库_recipes_富关联.json'))
|
|
tiaoli = load(os.path.join(PRO, '调理药膳_知识图谱.json'))
|
|
|
|
for r in recipes:
|
|
xs = []
|
|
for c in r.get('食材成分', []) + r.get('药材成分', []):
|
|
v = parse_xing(c.get('性味归经', ''))
|
|
if v:
|
|
xs.append(v)
|
|
r['_xing_list'] = xs
|
|
|
|
for r in tiaoli:
|
|
xs = []
|
|
for c in r.get('关联成分明细', []):
|
|
v = xing_by_id.get(c['id'])
|
|
if v:
|
|
xs.append(v)
|
|
r['_xing_list'] = xs
|
|
|
|
# ---------------- 补标 (规则R1/R3/R4) ----------------
|
|
SEASON_PAT = [('春季养生药膳', '春'), ('夏季养生药膳', '夏'),
|
|
('秋季养生药膳', '秋'), ('冬季养生药膳', '冬')]
|
|
# 《四季调养药膳》实测基准 (建档报告: 季节×性味交叉表)
|
|
VERIFY = {'夏': lambda x: x == '凉', '冬': lambda x: x in ('温', '热'),
|
|
'秋': lambda x: x == '平', '春': lambda x: True}
|
|
|
|
def tag(items, src_name):
|
|
out = []
|
|
for it in items:
|
|
cat = it.get('category', '') or ''
|
|
season, basis, cred = '四季通用', None, '高'
|
|
for pat, s in SEASON_PAT:
|
|
if pat in cat:
|
|
season, basis, cred = s, f'书源类目[{cat}]', '高'
|
|
break
|
|
if basis is None:
|
|
# 书源未明示季节: 类目为功效类目, 标注"四季通用"为保守回退推断
|
|
basis = f'书源类目[{cat or "无"}]为功效类目, 无季节限定'
|
|
cred = '中(回退推断, 书源未明示季节)'
|
|
agg = aggregate(it['_xing_list'])
|
|
notes, dev = [], []
|
|
if season != '四季通用': # R3 交叉验证
|
|
if agg and not VERIFY[season](agg):
|
|
dev.append({'聚合性味': agg, '注记': f'{season}季类目但成分聚合性味为{agg}, 与四季库季节×性味基准偏离, 仅注记不改标'})
|
|
notes.append('性味偏离注记')
|
|
elif agg in ('温', '热'): # R4 通用条目性味提示
|
|
notes.append(f'性味倾向冬补({agg}性), 供人工复核')
|
|
out.append({
|
|
'id': it['id'], '名称': it.get('name') or it.get('名称'),
|
|
'category': cat, '来源集': src_name,
|
|
'四季标注': season, '标注依据': basis, '可信度': cred,
|
|
'聚合性味': agg, '性味成分数': len(it['_xing_list']),
|
|
'成分性分布': dict(collections.Counter(it['_xing_list'])) if it['_xing_list'] else {},
|
|
'数据质量': notes, '性味偏离注记': dev,
|
|
})
|
|
return out
|
|
|
|
tag_recipes = tag(recipes, '药膳配方库')
|
|
tag_tiaoli = tag(tiaoli, '调理药膳')
|
|
|
|
# ---------------- 五组索引 (与四季库 索引_季节分类.json 同构) ----------------
|
|
GUIDE = {
|
|
'春': {'五补': '升补', '脏腑': '肝', '五行': '木'},
|
|
'夏': {'五补': '清补', '脏腑': '心', '五行': '火'},
|
|
'秋': {'五补': '平补', '脏腑': '肺', '五行': '金'},
|
|
'冬': {'五补': '温补', '脏腑': '肾', '五行': '水'},
|
|
'四季通用': {'五补': '平补为主', '脏腑': '—', '五行': '—'},
|
|
}
|
|
index = []
|
|
all_tags = tag_recipes + tag_tiaoli
|
|
for s in ['春', '夏', '秋', '冬', '四季通用']:
|
|
grp = [t for t in all_tags if t['四季标注'] == s]
|
|
index.append({
|
|
'季节': s, '五补原则': GUIDE[s]['五补'], '应脏腑': GUIDE[s]['脏腑'], '五行': GUIDE[s]['五行'],
|
|
'条数': len(grp),
|
|
'条目': [{'id': t['id'], '名称': t['名称'], '来源集': t['来源集'], 'category': t['category']} for t in grp],
|
|
})
|
|
|
|
# ---------------- 与四季库关联条目季节对照 ----------------
|
|
XREF = os.path.join(BASE, '..', '四季调养药膳', '03_关联融合', '四季调养-药膳全书_交叉关联.json')
|
|
compare = {'metadata': {'说明': '既有10条名称关联(见 药膳全书-四季库_交叉关联.json)的两侧季节/性味对照',
|
|
'参照库': '四季调养药膳(156条,季节为书源章节)', '生成脚本': '库根 season_tag.py'},
|
|
'对照': []}
|
|
if os.path.exists(XREF):
|
|
x = json.load(open(XREF, encoding='utf-8'))
|
|
tiaoli_name = {t['名称']: t for t in all_tags if t['来源集'] == '调理药膳'}
|
|
recipes_name = {t['名称']: t for t in all_tags if t['来源集'] == '药膳配方库'}
|
|
sj = json.load(open(os.path.join(BASE, '..', '四季调养药膳', '02_加工数据', '四季调养药膳.json'), encoding='utf-8'))
|
|
sj = sj if isinstance(sj, list) else next(v for v in sj.values() if isinstance(v, list))
|
|
sj_by_id = {r['id']: r for r in sj}
|
|
for ref in x.get('cross_references', []):
|
|
sjr = sj_by_id[ref['本库id']]
|
|
for a in ref['关联']:
|
|
pool = tiaoli_name if a['对方库'] == '调理药膳' else recipes_name
|
|
hit = pool.get(a['对方名称'])
|
|
if hit:
|
|
compare['对照'].append({
|
|
'四季库id': ref['本库id'], '四季库名称': ref['本库名称'],
|
|
'四季库季节': sjr['季节'], '四季库性味': sjr['性味'],
|
|
'药膳全书集': a['对方库'], '药膳全书id': a['对方id'], '药膳全书名称': a['对方名称'],
|
|
'补标季节': hit['四季标注'], '补标依据': hit['标注依据'], '聚合性味': hit['聚合性味'],
|
|
'季节一致': hit['四季标注'] == sjr['季节'] or sjr['季节'] == '四季通用' or hit['四季标注'] == '四季通用',
|
|
})
|
|
|
|
# ---------------- 写盘 ----------------
|
|
os.makedirs(RPT, exist_ok=True)
|
|
def dump(obj, path):
|
|
json.dump(obj, open(path, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
|
|
|
|
dump(tag_recipes, os.path.join(PRO, '四季标注_recipes.json'))
|
|
dump(tag_tiaoli, os.path.join(PRO, '四季标注_调理药膳.json'))
|
|
dump(index, os.path.join(PRO, '索引_四季分类.json'))
|
|
dump(compare, os.path.join(LNK, '药膳全书-四季库_关联条目季节对照.json'))
|
|
|
|
# ---------------- 统计 ----------------
|
|
def stat(tags):
|
|
c = collections.Counter(t['四季标注'] for t in tags)
|
|
xing_by_season = collections.defaultdict(collections.Counter)
|
|
for t in tags:
|
|
if t['聚合性味']:
|
|
xing_by_season[t['四季标注']][t['聚合性味']] += 1
|
|
return c, xing_by_season
|
|
|
|
lines = ['# 药膳全书库 四季标识补标报告', '',
|
|
'> 参照: 《四季调养药膳》资料库方法论 (季节五分法 + 性味聚合 + 季节×性味交叉验证)',
|
|
'> 规则: R1书源类目优先 R2性味聚合 R3交叉验证(仅注记) R4通用条目性味提示', '']
|
|
for name, tags in [('药膳配方库(301条)', tag_recipes), ('调理药膳(336条)', tag_tiaoli)]:
|
|
c, xbs = stat(tags)
|
|
lines += [f'## {name}', '', '| 四季标注 | 条数 | 占比 |', '|---|---|---|']
|
|
tot = len(tags)
|
|
for s in ['春', '夏', '秋', '冬', '四季通用']:
|
|
n = c.get(s, 0)
|
|
lines.append(f'| {s} | {n} | {n/tot:.0%} |')
|
|
lines += ['', '标注依据分布: ' + '; '.join(
|
|
f"{k[:12]}…={v}" if len(k) > 14 else f"{k}={v}"
|
|
for k, v in collections.Counter(t['标注依据'].split('[')[0] for t in tags).most_common()), '']
|
|
lines += ['季节×聚合性味交叉:', '', '| 季节 | 平 | 温 | 凉 | 热 | 无成分数据 |', '|---|---|---|---|---|---|']
|
|
for s in ['春', '夏', '秋', '冬', '四季通用']:
|
|
xb = xbs.get(s, collections.Counter())
|
|
none_n = c.get(s, 0) - sum(xb.values())
|
|
lines.append(f"| {s} | {xb.get('平',0)} | {xb.get('温',0)} | {xb.get('凉',0)} | {xb.get('热',0)} | {none_n} |")
|
|
lines.append('')
|
|
# 偏离清单
|
|
dev_all = [(t['来源集'], t['id'], t['名称'], t['四季标注'], d['聚合性味'])
|
|
for t in all_tags for d in t['性味偏离注记']]
|
|
lines += ['## 性味偏离注记清单 (R3, 不改标)', '']
|
|
if dev_all:
|
|
lines += ['| 来源集 | id | 名称 | 标注 | 聚合性味 |', '|---|---|---|---|---|']
|
|
lines += [f'| {a} | {b} | {c} | {d} | {e} |' for a, b, c, d, e in dev_all]
|
|
else:
|
|
lines += ['无偏离条目 — 书源季节类目条目的成分聚合性味全部符合四季库季节×性味基准。']
|
|
lines.append('')
|
|
r4 = [(t['来源集'], t['id'], t['名称'], t['聚合性味']) for t in all_tags
|
|
if t['四季标注'] == '四季通用' and t['聚合性味'] in ('温', '热')]
|
|
lines += ['## 通用条目性味倾向提示 (R4, 供人工复核)', '',
|
|
f'共 {len(r4)} 条功效类目条目成分聚合为纯温/热性:', '']
|
|
lines += [f'- {a} {b} {c} (聚合{e}性)' for a, b, c, e in r4[:30]]
|
|
if len(r4) > 30:
|
|
lines.append(f'- …其余 {len(r4)-30} 条见 四季标注_*.json 数据质量字段')
|
|
lines += ['', '## 与四季库关联条目季节对照', '',
|
|
f"既有10条名称关联的两侧季节一致性: "
|
|
f"{sum(1 for x in compare['对照'] if x['季节一致'])}/{len(compare['对照'])}", '']
|
|
lines += ['| 四季库 | 库内季节 | 药膳全书 | 补标季节 | 一致 |', '|---|---|---|---|---|']
|
|
for x in compare['对照']:
|
|
lines.append(f"| {x['四季库名称']} | {x['四季库季节']}({x['四季库性味']}) | "
|
|
f"{x['药膳全书名称']} | {x['补标季节']} | {'✓' if x['季节一致'] else '✗'} |")
|
|
open(os.path.join(RPT, '四季补标报告.md'), 'w', encoding='utf-8').write('\n'.join(lines) + '\n')
|
|
|
|
print('=== 四季补标完成 ===')
|
|
print('recipes:', dict(stat(tag_recipes)[0]))
|
|
print('调理药膳:', dict(stat(tag_tiaoli)[0]))
|
|
print('偏离注记:', len(dev_all), '| R4通用温热提示:', len(r4),
|
|
'| 关联对照:', len(compare['对照']), '条, 一致',
|
|
sum(1 for x in compare['对照'] if x['季节一致']))
|