Clarify benchmark boundaries and improve annual workflow surfaces

This commit is contained in:
732642856
2026-06-27 19:09:31 +08:00
parent 4d875ea242
commit 04d901e1be
18 changed files with 506 additions and 156 deletions
+54 -54
View File
@@ -175,7 +175,7 @@ def skill_detect_yogas(planets, asc):
# ============================================================
# PyJhora 接口封装
# ============================================================
def init_pyjhora():
def init_external_benchmark():
"""初始化 PyJhora,返回是否成功"""
try:
import jhora.horoscope.chart.yoga as py_yoga
@@ -188,13 +188,13 @@ def init_pyjhora():
return False
def pyjhora_jd(year, month, day, hour_frac):
def external_benchmark_jd(year, month, day, hour_frac):
"""计算 Julian Day(与 PyJhora 一致)"""
import swisseph as swe
return swe.julday(year, month, day, hour_frac)
def pyjhora_get_yogas(jd, lat, lon, tz, divisional_chart_factor=1):
def external_benchmark_get_yogas(jd, lat, lon, tz, divisional_chart_factor=1):
"""
调用 PyJhora 获取 D1 宫盘的 Yoga 检测结果。
返回:{yoga_function_name: {"name": ..., "desc": ..., "benefits": ...}}
@@ -329,10 +329,10 @@ def _canonical_name(key: str) -> str:
return CROSS_NAME_MAP.get(key, key)
def get_pyjhora_yoga_keys(pyjhora_results: Dict[str, dict]) -> Set[str]:
def get_external_benchmark_yoga_keys(external_benchmark_results: Dict[str, dict]) -> Set[str]:
"""从 PyJhora 检测结果中提取归一化名称集合"""
keys = set()
for fname in pyjhora_results.keys():
for fname in external_benchmark_results.keys():
# 去掉 _from_jd_place 等后缀
base = re.sub(r"_(from_jd_place|from_planet_positions|calculation|calc)$",
"", fname)
@@ -347,7 +347,7 @@ def get_pyjhora_yoga_keys(pyjhora_results: Dict[str, dict]) -> Set[str]:
# ============================================================
# 核心验证逻辑
# ============================================================
def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
def validate_one_case(case: dict, run_external_benchmark: bool = True) -> dict:
"""
验证单个测试用例。
@@ -363,10 +363,10 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
"birth": f"{case['year']}-{case['month']:02d}-{case['day']:02d} "
f"{case.get('hour', 12):02d}:{case.get('minute', 0):02d}",
"skill_yogas": [],
"pyjhora_yogas": [],
"external_benchmark_yogas": [],
"matched": [],
"skill_only": [], # false positive
"pyjhora_only": [], # false negative
"external_benchmark_only": [], # false negative
"error": None,
}
@@ -399,16 +399,16 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
return result
# --- PyJhora 检测 ---
if run_pyjhora:
if run_external_benchmark:
try:
jd_py = pyjhora_jd(year, month, day, hour + minute / 60.0)
pyjhora_results = pyjhora_get_yogas(jd_py, lat, lon, tz,
jd_py = external_benchmark_jd(year, month, day, hour + minute / 60.0)
external_benchmark_results = external_benchmark_get_yogas(jd_py, lat, lon, tz,
divisional_chart_factor=1)
result['pyjhora_yogas'] = list(pyjhora_results.keys())
result['pyjhora_count'] = len(pyjhora_results)
result['pyjhora_details'] = []
for fname, details in pyjhora_results.items():
result['pyjhora_details'].append({
result['external_benchmark_yogas'] = list(external_benchmark_results.keys())
result['external_benchmark_count'] = len(external_benchmark_results)
result['external_benchmark_details'] = []
for fname, details in external_benchmark_results.items():
result['external_benchmark_details'].append({
'function': fname,
'name': details.get('name', ''),
'desc': details.get('desc', ''),
@@ -419,11 +419,11 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
# --- 对比 ---
skill_keys = get_skill_yoga_keys(skill_yogas)
pyjhora_keys = get_pyjhora_yoga_keys(pyjhora_results)
external_benchmark_keys = get_external_benchmark_yoga_keys(external_benchmark_results)
result['matched'] = sorted(skill_keys & pyjhora_keys)
result['skill_only'] = sorted(skill_keys - pyjhora_keys)
result['pyjhora_only'] = sorted(pyjhora_keys - skill_keys)
result['matched'] = sorted(skill_keys & external_benchmark_keys)
result['skill_only'] = sorted(skill_keys - external_benchmark_keys)
result['external_benchmark_only'] = sorted(external_benchmark_keys - skill_keys)
# --- 分类:功能性 Yoga vs 经典 Yoga ---
cat_map = get_skill_rule_categories()
@@ -439,17 +439,17 @@ def validate_one_case(case: dict, run_pyjhora: bool = True) -> dict:
return result
def run_validation(cases: List[dict], run_pyjhora: bool = True) -> dict:
def run_validation(cases: List[dict], run_external_benchmark: bool = True) -> dict:
"""运行批量验证"""
report = {
"total_cases": len(cases),
"cases": [],
"summary": {
"total_skill_yogas": 0,
"total_pyjhora_yogas": 0,
"total_external_benchmark_yogas": 0,
"total_matched": 0,
"total_skill_only": 0,
"total_pyjhora_only": 0,
"total_external_benchmark_only": 0,
"total_skill_only_functional": 0,
"total_skill_only_classic": 0,
}
@@ -458,7 +458,7 @@ def run_validation(cases: List[dict], run_pyjhora: bool = True) -> dict:
for case in cases:
name = case.get('name', 'unknown')
print(f"🔍 验证: {name} ({case['year']}-{case['month']:02d}-{case['day']:02d})")
r = validate_one_case(case, run_pyjhora=run_pyjhora)
r = validate_one_case(case, run_external_benchmark=run_external_benchmark)
report['cases'].append(r)
if r['error']:
@@ -466,21 +466,21 @@ def run_validation(cases: List[dict], run_pyjhora: bool = True) -> dict:
continue
sc = r.get('skill_count', 0)
pc = r.get('pyjhora_count', '?')
pc = r.get('external_benchmark_count', '?')
func_n = len(r.get('skill_only_functional', []))
classic_n = len(r.get('skill_only_classic', []))
print(f" Skill: {sc} | PyJhora: {pc}")
print(f" 匹配: {len(r['matched'])} | "
f"Skill独有: {len(r['skill_only'])} (功能性{func_n}, 经典{classic_n}) | "
f"PyJhora独有: {len(r['pyjhora_only'])}")
f"PyJhora独有: {len(r['external_benchmark_only'])}")
report['summary']['total_skill_yogas'] += sc
if isinstance(pc, int):
report['summary']['total_pyjhora_yogas'] += pc
report['summary']['total_external_benchmark_yogas'] += pc
report['summary']['total_matched'] += len(r['matched'])
report['summary']['total_skill_only'] += len(r['skill_only'])
if isinstance(pc, int):
report['summary']['total_pyjhora_only'] += len(r['pyjhora_only'])
report['summary']['total_external_benchmark_only'] += len(r['external_benchmark_only'])
report['summary']['total_skill_only_functional'] += func_n
report['summary']['total_skill_only_classic'] += classic_n
@@ -535,27 +535,27 @@ def print_report(report: dict):
summary = report['summary']
total_skill = summary['total_skill_yogas']
total_pyjhora = summary['total_pyjhora_yogas']
total_external_benchmark = summary['total_external_benchmark_yogas']
matched = summary['total_matched']
skill_only = summary['total_skill_only']
pyjhora_only = summary['total_pyjhora_only']
external_benchmark_only = summary['total_external_benchmark_only']
func_only = summary.get('total_skill_only_functional', 0)
classic_only = summary.get('total_skill_only_classic', 0)
print(f"\n📊 汇总:")
print(f" 测试用例数: {report['total_cases']}")
print(f" Skill 检测总数: {total_skill}")
print(f" PyJhora 检测总数: {total_pyjhora}")
print(f" PyJhora 检测总数: {total_external_benchmark}")
print(f" 匹配总数: {matched}")
print(f" Skill 独有: {skill_only} (功能性{func_only}, 经典{classic_only})")
print(f" PyJhora 独有 (skill 缺失): {pyjhora_only}")
print(f" PyJhora 独有 (skill 缺失): {external_benchmark_only}")
if total_skill > 0:
precision = matched / total_skill * 100
print(f"\n 总体精确率 (Precision): {precision:.1f}%")
if total_pyjhora > 0:
recall = matched / total_pyjhora * 100
if total_external_benchmark > 0:
recall = matched / total_external_benchmark * 100
print(f" 总体召回率 (Recall): {recall:.1f}%")
# --- 核心经典 Yoga 准确率(排除 skill 特色功能性 Yoga---
@@ -564,21 +564,21 @@ def print_report(report: dict):
classic_precision = matched / classic_skill_total * 100
print(f"\n 🎯 核心经典 Yoga 精确率: {classic_precision:.1f}%")
print(f" (排除 {func_only} 条功能性 Yoga 后: {matched}/{classic_skill_total})")
if total_pyjhora > 0:
classic_recall = matched / total_pyjhora * 100
if total_external_benchmark > 0:
classic_recall = matched / total_external_benchmark * 100
print(f" 🎯 核心经典 Yoga 召回率: {classic_recall:.1f}%")
# --- 全局不匹配统计 ---
print(f"\n🔍 全局不匹配分析:")
all_skill_classic = set()
all_skill_func = set()
all_pyjhora_missing = set()
all_external_benchmark_missing = set()
for r in report['cases']:
if r.get('error'):
continue
all_skill_classic.update(r.get('skill_only_classic', []))
all_skill_func.update(r.get('skill_only_functional', []))
all_pyjhora_missing.update(r.get('pyjhora_only', []))
all_external_benchmark_missing.update(r.get('external_benchmark_only', []))
print(f" Skill 经典 Yoga 不匹配(可能误判): {len(all_skill_classic)}")
if all_skill_classic:
@@ -586,9 +586,9 @@ def print_report(report: dict):
print(f" Skill 功能性 YogaPyJhora 无对应,属 skill 特色): {len(all_skill_func)}")
if all_skill_func:
print(f" {sorted(all_skill_func)[:15]}")
print(f" PyJhora 有但 Skill 缺失的 Yoga: {len(all_pyjhora_missing)}")
if all_pyjhora_missing:
print(f" {sorted(all_pyjhora_missing)[:15]}")
print(f" PyJhora 有但 Skill 缺失的 Yoga: {len(all_external_benchmark_missing)}")
if all_external_benchmark_missing:
print(f" {sorted(all_external_benchmark_missing)[:15]}")
# --- 改进建议 ---
print(f"\n💡 改进建议:")
@@ -599,13 +599,13 @@ def print_report(report: dict):
if len(all_skill_classic) > 10:
print(f" ... 等共 {len(all_skill_classic)}")
missing_top = sorted(all_pyjhora_missing)[:20]
print(f"\n 2. 【规则补齐】以下 {len(all_pyjhora_missing)} 种 Yoga PyJhora 已实现但 skill 缺失,")
missing_top = sorted(all_external_benchmark_missing)[:20]
print(f"\n 2. 【规则补齐】以下 {len(all_external_benchmark_missing)} 种 Yoga PyJhora 已实现但 skill 缺失,")
print(f" 建议按优先级补充(推荐先补充高频出现的):")
for name in missing_top[:15]:
print(f" - {name}")
if len(all_pyjhora_missing) > 15:
print(f" ... 等共 {len(all_pyjhora_missing)}")
if len(all_external_benchmark_missing) > 15:
print(f" ... 等共 {len(all_external_benchmark_missing)}")
print(f"\n 3. 【名称映射】当前 CROSS_NAME_MAP 已覆盖常见别名,")
print(f" 如仍有新别名发现,请添加到映射表中。")
@@ -617,20 +617,20 @@ def print_report(report: dict):
print(f"{r['error'][:300]}")
continue
sc = r.get('skill_count', 0)
pc = r.get('pyjhora_count', '?')
pc = r.get('external_benchmark_count', '?')
print(f" Skill ({sc}): {r['skill_yogas'][:5]}{'...' if len(r['skill_yogas']) > 5 else ''}")
if 'pyjhora_yogas' in r:
print(f" PyJhora ({pc}): {r['pyjhora_yogas'][:5]}{'...' if len(r['pyjhora_yogas']) > 5 else ''}")
if 'external_benchmark_yogas' in r:
print(f" PyJhora ({pc}): {r['external_benchmark_yogas'][:5]}{'...' if len(r['external_benchmark_yogas']) > 5 else ''}")
func_n = len(r.get('skill_only_functional', []))
cls_n = len(r.get('skill_only_classic', []))
print(f" 匹配: {len(r['matched'])} | Skill独有: {len(r['skill_only'])}(功能{func_n},经典{cls_n}) | PyJhora独有: {len(r['pyjhora_only'])}")
print(f" 匹配: {len(r['matched'])} | Skill独有: {len(r['skill_only'])}(功能{func_n},经典{cls_n}) | PyJhora独有: {len(r['external_benchmark_only'])}")
if r.get('skill_only_classic'):
print(f" Skill 经典不匹配: {r['skill_only_classic'][:10]}")
if r.get('skill_only_functional'):
print(f" Skill 功能性: {r['skill_only_functional'][:10]}")
if r.get('pyjhora_only'):
print(f" PyJhora 独有: {r['pyjhora_only'][:10]}")
if r.get('external_benchmark_only'):
print(f" PyJhora 独有: {r['external_benchmark_only'][:10]}")
def save_report(report: dict, output_file: str):
@@ -653,8 +653,8 @@ def main():
args = parser.parse_args()
# 检查 PyJhora
if not args.skip_pyjhora:
if not init_pyjhora():
if not args.skip_external_benchmark:
if not init_external_benchmark():
print("❌ PyJhora 不可用,请先安装: pip install pyjhora swisseph")
print(" 提示:也可用 --skip-pyjhora 只测试 skill 侧")
return 1
@@ -672,7 +672,7 @@ def main():
return 1
# 运行验证
report = run_validation(cases, run_pyjhora=not args.skip_pyjhora)
report = run_validation(cases, run_external_benchmark=not args.skip_external_benchmark)
# 输出报告
print_report(report)