"""16-01 / 16-02: 標準ライブラリのみの閾値学習・指標計算。
すべて架空データ。少数例で実用的な性能を示すものではない。
"""
import sys

TRAIN = [(1.0,0),(2.0,0),(3.0,0),(4.0,1),(5.0,1),(6.0,1)]
TEST = [(2.5,0),(3.2,0),(4.5,0),(5.5,1)]


def fit_threshold(train):
    values = sorted({x for x, _ in train})
    if not values:
        raise ValueError('学習データが必要')
    candidates = [values[0]-1] + [(a+b)/2 for a,b in zip(values,values[1:])] + [values[-1]+1]
    # テストデータは一切参照しない。学習誤りが最小の閾値を選択。
    return min(candidates, key=lambda t: sum(int(x>=t)!=y for x,y in train))


def confusion(data, predict):
    result = {'tp':0,'fp':0,'fn':0,'tn':0}
    for x, true in data:
        pred = predict(x)
        if true not in (0,1) or pred not in (0,1):
            raise ValueError('0/1の分類だけを扱う')
        key = 'tp' if true and pred else 'fp' if pred else 'fn' if true else 'tn'
        result[key] += 1
    return result


def metrics(c):
    tp,fp,fn,tn = (c[k] for k in ('tp','fp','fn','tn'))
    def divide(a,b):
        return a/b if b else None  # 分母ゼロは未定義として扱う
    return {'precision':divide(tp,tp+fp),'recall':divide(tp,tp+fn),'accuracy':divide(tp+tn,tp+fp+fn+tn),'cost':fn*100+fp*5}


def self_test():
    threshold = fit_threshold(TRAIN)
    assert threshold == 3.5
    c = confusion(TEST, lambda x:int(x>=threshold))
    assert c == {'tp':1,'fp':1,'fn':0,'tn':2}
    assert metrics(c)['accuracy'] == .75
    baseline = confusion(TEST,lambda x:0)
    assert metrics(baseline)['accuracy'] == .75
    assert metrics(baseline)['precision'] is None
    a = metrics({'tp':8,'fp':12,'fn':2,'tn':78})
    b = metrics({'tp':6,'fp':3,'fn':4,'tn':87})
    assert a['precision'] == .4 and a['recall'] == .8 and a['cost'] == 260
    assert abs(b['precision']-2/3)<1e-12 and b['recall']==.6 and b['cost']==415
    print('PASS: 学習閾値・評価混同行列・指標・未定義分母・費用を確認。')


if __name__ == '__main__':
    if '--self-test' in sys.argv:
        self_test()
    else:
        t=fit_threshold(TRAIN)
        print('学習データで選んだ閾値:',t)
        for name,predict in [('閾値モデル',lambda x:int(x>=t)),('全件正常',lambda x:0)]:
            c=confusion(TEST,predict)
            print(name,c,metrics(c))
        for name,c in [('判定A',{'tp':8,'fp':12,'fn':2,'tn':78}),('判定B',{'tp':6,'fp':3,'fn':4,'tn':87})]:
            print(name,metrics(c))
