# 收尾版实验:
# 1) 完整性划分: 训练只用时长实测的在榜车; 时长为推补值的在榜车留作检验集
#    (检验: 模型没见过它们, 在它们内部的排序 tau)
# 2) 输出讲解 BT 本质用的例子: 几对车的分差与模型胜率
import csv, math, random, unicodedata, difflib

random.seed(7)
BASE = '.'
src_code = open(f'{BASE}/class05-bt-rank.py').read()
i0 = src_code.index('STEEL = [')
i1 = src_code.index("(50,'Twister','Knoebels')]") + len("(50,'Twister','Knoebels')]")
exec(src_code[i0:i1])

def parse_dur(s):
    if not s or ':' not in s: return None
    try:
        m, sec = s.split(':'); return int(m)*60 + int(sec)
    except ValueError: return None

def norm(s):
    s = unicodedata.normalize('NFKD', s)
    s = ''.join(c for c in s if not unicodedata.combining(c))
    return ''.join(c for c in s.lower() if c.isalnum())

rows = []
with open(f'{BASE}/rcdb_strict.csv') as f:
    for r in csv.DictReader(f):
        def num(k):
            try: return float(r[k])
            except (ValueError, TypeError): return None
        rows.append({'name': r['CoasterName'].split(' / ')[0], 'park': r['Park'], 'city': r['City'],
                     'status': r['Status'], 'h': num('Height'), 's': num('Speed'), 'len': num('Length'),
                     'inv': num('Inversions'), 'dur': parse_dur(r.get('Duration', ''))})
for r in rows:
    r['nn'] = norm(r['name']); r['np'] = norm(r['park'] + r['city'])

def find(name, park):
    n, p = norm(name), norm(park)
    exact = [r for r in rows if r['nn'] == n]
    if len(exact) == 1: return exact[0]
    if len(exact) > 1:
        pk = [r for r in exact if p[:8] in r['np'] or r['np'][:8] in p]
        return pk[0] if pk else None
    sub = [r for r in rows if n in r['nn'] or (r['nn'] in n and len(r['nn']) >= 5)]
    if sub:
        pk = [r for r in sub if p[:6] in r['np']]
        if len(pk) == 1: return pk[0]
        if len(sub) == 1: return sub[0]
    best, score = None, 0
    for r in rows:
        sc = difflib.SequenceMatcher(None, n, r['nn']).ratio()
        if sc > score: best, score = r, sc
    if score >= 0.85 and (norm(park)[:5] in best['np'] or score >= 0.93):
        return best
    return None

usable = [r for r in rows if r['h'] and r['s'] and r['len']]
bothd = [(r['len']/r['s'], r['dur']) for r in usable if r['dur']]
n_ = len(bothd); mx = sum(x for x, _ in bothd)/n_; my = sum(y for _, y in bothd)/n_
tb = sum((x-mx)*(y-my) for x, y in bothd)/sum((x-mx)**2 for x, _ in bothd)
ta = my - tb*mx
for r in usable:
    r['dur_i'] = r['dur'] if r['dur'] else max(20.0, ta + tb*r['len']/r['s'])
    r['inv_i'] = r['inv'] if r['inv'] is not None else 0.0

FEATS = ['s', 'h', 'len', 'dur_i', 'inv_i']
stats = {}
for f_ in FEATS:
    v = [r[f_] for r in usable]
    m = sum(v)/len(v); sd = math.sqrt(sum((x-m)**2 for x in v)/len(v))
    stats[f_] = (m, sd)
def x_of(r): return [(r[f_]-stats[f_][0])/stats[f_][1] for f_ in FEATS]

def build(list_):
    out = []
    for rank, name, park in list_:
        r = find(name, park)
        if r and r['h'] and r['s'] and r['len']:
            out.append({'rank': rank, 'row': r, 'gname': name})
    return out
steel = build(STEEL); wood = build(WOOD)

def pairs_of(lst):
    ps = []
    for i in range(len(lst)):
        for j in range(len(lst)):
            if lst[i]['rank'] < lst[j]['rank']:
                ps.append((x_of(lst[i]['row']), x_of(lst[j]['row'])))
    return ps

def fit(pairs, lam=0.02, iters=900, lr=0.1):
    k = len(FEATS); w = [0.0]*k
    for _ in range(iters):
        g = [0.0]*k
        for xa, xb in pairs:
            d = sum(w[t]*(xa[t]-xb[t]) for t in range(k))
            pr = 1/(1+math.exp(-max(-30, min(30, d))))
            for t in range(k):
                g[t] += (1-pr)*(xa[t]-xb[t])
        for t in range(k):
            w[t] += lr*(g[t]/len(pairs) - lam*w[t])
    return w

def tau(lst, w):
    c = d = 0
    for i in range(len(lst)):
        for j in range(i+1, len(lst)):
            a, b = lst[i], lst[j]
            if a['rank'] == b['rank']: continue
            sa = sum(w[t]*x_of(a['row'])[t] for t in range(len(w)))
            sb = sum(w[t]*x_of(b['row'])[t] for t in range(len(w)))
            agree = (a['rank'] < b['rank']) == (sa > sb)
            c += agree; d += (not agree)
    return (c-d)/(c+d)

# ---- 完整性划分 ----
tr_s = [e for e in steel if e['row']['dur']]
tr_w = [e for e in wood if e['row']['dur']]
te_s = [e for e in steel if not e['row']['dur']]
te_w = [e for e in wood if not e['row']['dur']]
print(f'训练(实测时长): 钢 {len(tr_s)} 木 {len(tr_w)}; 检验(推补时长): 钢 {len(te_s)} 木 {len(te_w)}')
w2 = fit(pairs_of(tr_s) + pairs_of(tr_w))
print('只用完整样本训练的系数:', {f: round(x, 3) for f, x in zip(FEATS, w2)})
if len(te_s) >= 5: print(f'  推补组检验 tau(钢): {tau(te_s, w2):+.3f} (n={len(te_s)})')
if len(te_w) >= 5: print(f'  推补组检验 tau(木): {tau(te_w, w2):+.3f} (n={len(te_w)})')
w_full = fit(pairs_of(steel) + pairs_of(wood))
print('全部样本训练的系数  :', {f: round(x, 3) for f, x in zip(FEATS, w_full)})

# ---- BT 讲解例子: 分差与胜率 ----
def score(e, w): return sum(w[t]*x_of(e['row'])[t] for t in range(len(w)))
def by(lst, name): return next(e for e in lst if e['gname'] == name)
pairs_demo = [('Fury 325', 'Whizzer'), ('Fury 325', 'Millennium Force'), ('Nitro', 'Diamondback')]
print('\nBT 例子 (全模型):')
for a, b in pairs_demo:
    ea, eb = by(steel, a), by(steel, b)
    d = score(ea, w_full) - score(eb, w_full)
    p = 1/(1+math.exp(-d))
    print(f'  {a}(GTA#{ea["rank"]}) vs {b}(GTA#{eb["rank"]}): 分差 {d:+.2f} -> 模型胜率 {p:.0%}')
