Spaces:
Running
Running
File size: 9,294 Bytes
dd8d145 fefbb42 dd8d145 fefbb42 dd8d145 fefbb42 dd8d145 fefbb42 dd8d145 fefbb42 dd8d145 fefbb42 dd8d145 fefbb42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | """
逐年预测撒网密度 / 分辨力基线实验
=================================
为什么要有这个文件
------------------
`~/.claude/skills/trinity-destiny/SKILL.md`「Step 5.5 证据强度审计」②:
命中次数在没有基线率 / 假阳性率的情况下没有意义。
算不出假阳性率的规律只能标「待追踪」,不能升级为判读规则。
逐年吉凶是付费正文的一部分。要卖它,就得先能回答「闭着眼睛猜能中多少」。
本脚本只产出数字,不产出结论。
三个踩过的坑(写在这里免得下次重犯)
------------------------------------
1. 必须用 BaZiCalculator.calculate_bazi_complete(include_dayun=True)。
用 calculate_bazi() 拿不到大运→拿不到流年→风险年恒为 0,
会得到一份「全 0」的漂亮结果。全 0 是 harness 坏了,不是发现。
2. 用户实际看到的是 analyze_*() 的公开输出;它会先裁到当前窗口,再按显著度
封顶。基线必须同时报告「窗口内候选」与「最终呈现」,否则封顶会被误读成
命理分辨力。
3. 分母是**当前大运窗口长度**,不是一辈子。用户读的是这 12 年;窗口内
被隐藏的候选不能悄悄算进命中率。
用法
----
python3 experiments/baseline_density.py
"""
import sys, os
from collections import defaultdict
sys.path.insert(0, '/Volumes/sparksverse/huggingface-spaces/astrologybazi-api')
os.chdir('/Volumes/sparksverse/huggingface-spaces/astrologybazi-api')
from bazi_calculator import BaZiCalculator
from genju_analyzer import GenjuAnalyzer
from dayun_analyzer import DayunAnalyzer
from marriage_analyzer import MarriageAnalyzer
from official_analyzer import OfficialAnalyzer
from legal_risk_analyzer import LegalRiskAnalyzer
from fortune_window import filter_to_window
CHARTS = [('小鱼','1981-10-04','20:38','female'),('林磊','1983-12-13','13:15','female'),
('Jason','1978-10-30','10:00','male'),('C4','2008-07-31','21:00','female'),
('C5','2012-04-09','07:30','male'),('C6','2014-04-15','16:30','male'),
('C7','2013-05-13','03:40','male'),('C8','1990-01-08','06:30','male'),
('C9','1980-06-07','17:30','female')]
NOW = 2026
DOM = ['婚姻风险','官非风险','升迁','灾劫']
def ages(lst):
return sorted({e['age'] for e in (lst or []) if isinstance(e, dict) and 'age' in e})
class _NoCapList(list):
"""诊断用列表:让婚姻模块跳过展示层 [:3],保留候选供对照。
MarriageAnalyzer 的「全局最多 20 条 + 当前窗口最多 3 条」逻辑目前写在
一个方法里,没有单独的 raw API。这个小替身只在本实验中使用,不能进入
生产路径;结果应理解为“模块内部全局筛选之后、窗口展示封顶之前”的候选。
"""
def __len__(self):
actual = super().__len__()
return min(actual, 3)
def marriage_candidates_before_window(chart, dayun):
"""读取婚姻模块的窗口前候选,不改变生产方法。"""
import fortune_window
original_filter = fortune_window.filter_to_window
try:
fortune_window.filter_to_window = lambda entries, window, age_key='age': _NoCapList(entries or [])
diagnostic = MarriageAnalyzer(chart, dayun).analyze_marriage()
finally:
fortune_window.filter_to_window = original_filter
return (diagnostic.get('marriage_fortune_years') or {}).get('risk_years') or []
def collect(name, date, time, gender):
chart = BaZiCalculator.calculate_bazi_complete(birth_date=date, birth_time=time,
gender=gender, is_lunar=False, include_dayun=True, num_dayun=12)
genju = GenjuAnalyzer(chart).analyze_genju()
age = NOW - int(date[:4])
dayun = DayunAnalyzer(chart, genju, current_age=age).analyze_all_dayun()
mar = MarriageAnalyzer(chart, dayun).analyze_marriage()
off = OfficialAnalyzer(chart, genju, dayun).analyze_official()
leg = LegalRiskAnalyzer(chart, genju, dayun).analyze_legal_risk()
fy = mar.get('marriage_fortune_years') or {}
win = (fy.get('window') or {})
lo, hi = win.get('start_age'), win.get('end_age')
d = {'婚姻风险': ages(fy.get('risk_years')),
'官非风险': ages(off.get('official_risk_years')),
'升迁': ages(off.get('promotion_years')),
'灾劫': ages(leg.get('disaster_years'))}
# 诊断层:公开输出以外,量出窗口内原本有多少候选。这里不把候选当作
# “预测命中”,只是为了识别 [:3] 封顶造成的密度变化。
off_analyzer = OfficialAnalyzer(chart, genju, dayun)
leg_analyzer = LegalRiskAnalyzer(chart, genju, dayun)
raw_entries = {
'婚姻风险': marriage_candidates_before_window(chart, dayun),
'官非风险': off_analyzer._analyze_official_risk_years(),
'升迁': off_analyzer._analyze_promotion_years(),
'灾劫': leg_analyzer._predict_disaster_years(),
}
raw_window = {x: ages(filter_to_window(raw_entries[x], (lo, hi))) for x in DOM}
return {'name': name, 'by': int(date[:4]), 'age': age, 'win': (lo, hi),
'len': (hi - lo + 1) if lo else None, 'd': d,
'raw_window': raw_window,
'raw_total': {x: sum(1 for _ in raw_entries[x]) for x in DOM}}
R = [collect(*c) for c in CHARTS]
print('='*86)
print('Q1 · 上线后的实际密度(analyze_*() 公开方法输出)')
print('='*86)
print(f"{'命盘':<7}{'龄':>4}{'窗口':>9}{'窗长':>5}" + ''.join(f'{x:>11}' for x in DOM) + f"{'并集':>13}")
print('-'*86)
acc = defaultdict(list); uni = []
for r in R:
n = r['len']
if not n:
print(f"{r['name']:<7}{r['age']:>4} 窗口不可用"); continue
winstr = '%d-%d' % (r['win'][0], r['win'][1])
row = f"{r['name']:<7}{r['age']:>4}{winstr:>9}{n:>5}"
allyrs = set()
for x in DOM:
k = len(r['d'][x]); acc[x].append(k/n); allyrs |= set(r['d'][x])
row += f"{k:>3}年{k/n:>6.0%}"
u = len(allyrs)/n; uni.append(u)
row += f"{len(allyrs):>5}年{u:>7.0%}"
print(row)
print('-'*86)
print(f"{'平均':<7}{'':>18}" + ''.join(f"{'':>3} {sum(acc[x])/len(acc[x]):>6.0%}" for x in DOM)
+ f"{'':>5} {sum(uni)/len(uni):>7.0%}")
print()
print('='*86)
print('Q2 · 分辨力:同一公历年,9 张盘里有几张同时被标')
print('='*86)
for x in DOM:
hits = defaultdict(list)
for r in R:
for a in r['d'][x]:
hits[r['by'] + a].append(r['name'])
if not hits: print(f'【{x}】无标记'); continue
w = max(hits.items(), key=lambda kv: len(kv[1]))
tot = sum(len(v) for v in hits.values())
print(f'【{x}】{len(hits)} 个不同年份承载 {tot} 个标记;'
f'最拥挤 {w[0]} 被 {len(w[1])}/9 盘同时标记 ({",".join(w[1])})')
print()
print('='*86)
print('Q3 · 唯一能精确核对的四件事(林磊)')
print('='*86)
ll = [r for r in R if r['name'] == '林磊'][0]
lo, hi = ll['win']
print(f"当前大运窗口 {lo}-{hi} 岁 = 公历 {ll['by']+lo}–{ll['by']+hi}")
print(f"婚姻风险标记年: {[ll['by']+a for a in ll['d']['婚姻风险']]}")
for y, lab in [(2020,'离婚(2020-21)'),(2021,'离婚(2020-21)'),(2010,'儿子出生'),(2012,'女儿出生')]:
a = y - ll['by']
inw = lo <= a <= hi
flag = a in ll['d']['婚姻风险']
print(f' {y}({a}岁) {lab:<14} 落在窗口内={"是" if inw else "否"} 被标记={"是" if flag else "否"}')
print()
print('='*86)
print('Q4 · 排除替代解释:被选中的年份是「最显著」还是「最早」?')
print('='*86)
print('_focus_years 的排序是 (level, age) 再 [:3],同级别时年龄小的胜出。')
print('若被选年份系统性挤在窗口前半段 → 呈现的是「最早」不是「最显著」。')
print()
print(f"{'领域':<10}{'被选年份在窗口中的相对位置(0=窗口头,1=窗口尾)':<20}")
for x in DOM:
pos = []
for r in R:
lo, hi = r['win']
if not lo or hi == lo: continue
for a in r['d'][x]:
pos.append((a - lo) / (hi - lo))
if not pos: continue
pos.sort()
front = sum(1 for p in pos if p <= 0.5) / len(pos)
print(f' {x:<10} 平均位置 {sum(pos)/len(pos):.2f} '
f'落在窗口前半段的比例 {front:.0%} (n={len(pos)})')
print()
print('若随机分布,平均位置应≈0.50、前半段比例应≈50%。')
print()
print('='*86)
print('Q5 · 当前窗口候选 vs 实际呈现(上限效应)')
print('='*86)
print('候选 = 模块已完成自身规则筛选、但尚未套当前窗口展示上限的年份;')
print('呈现 = 用户实际拿到的年份。隐藏不是“被验证为错误”,只是展示层封顶。')
print()
print(f"{'领域':<10}{'窗口候选':>12}{'实际呈现':>12}{'隐藏':>10}{'呈现率':>10}")
for x in DOM:
raw = sum(len(r['raw_window'][x]) for r in R)
shown = sum(len(r['d'][x]) for r in R)
hidden = raw - shown
ratio = shown / raw if raw else 0
print(f"{x:<10}{raw:>12}{shown:>12}{hidden:>10}{ratio:>9.0%}")
print()
print('解释:若“窗口候选”远高于“实际呈现”,密度下降来自产品封顶而非规则本身;')
print('因此在获得真实事件追踪前,逐年模块只能作为阶段提示,不能宣称准确率。')
|