"""
Lecture 10 - Evaluation Metrics: Regression & Classification

Reproduces every worked number from the lecture page:
  - MSE/MAE on y_true=[3,-0.5,2], y_pred=[2.5,0,2]           -> MSE=0.1667, MAE=0.3333
  - Confusion-matrix metrics for TP=40, FP=10, FN=5, TN=45   -> Precision=0.80, Recall~0.89, F1~0.84, Acc=85%
  - The full 21-row threshold scan (population of 20) used to draw the ROC and PR curves

Run: python lecture-10-metrics.py
"""
import numpy as np


# ---- Regression metrics ----
def mse(y_true, y_pred):
    return np.mean((np.asarray(y_pred) - np.asarray(y_true)) ** 2)


def mae(y_true, y_pred):
    return np.mean(np.abs(np.asarray(y_pred) - np.asarray(y_true)))


# ---- Classification metrics from a confusion matrix ----
def cm_metrics(TP, TN, FP, FN):
    acc = (TP + TN) / (TP + TN + FP + FN)
    prec = TP / (TP + FP) if (TP + FP) else 0.0
    rec = TP / (TP + FN) if (TP + FN) else 0.0
    spec = TN / (TN + FP) if (TN + FP) else 0.0
    f1 = 2 * prec * rec / (prec + rec) if (prec + rec) else 0.0
    return dict(accuracy=acc, precision=prec, recall=rec, specificity=spec, f1=f1)


if __name__ == "__main__":
    # ---- regression cancellation pitfall ----
    actual, predictions = [10, 12], [8, 14]
    diffs = [p - a for a, p in zip(actual, predictions)]
    print("Cancellation pitfall: differences =", diffs, " naive average =", sum(diffs) / len(diffs))

    # ---- regression worked example ----
    y_true = [3, -0.5, 2]
    y_pred = [2.5, 0.0, 2]
    print(f"\nMSE = {mse(y_true, y_pred):.4f}   MAE = {mae(y_true, y_pred):.4f}")

    # ---- classification worked example 2 ----
    ex2 = cm_metrics(TP=40, TN=45, FP=10, FN=5)
    print("\nConfusion matrix example (TP=40,FP=10,FN=5,TN=45):")
    for k, v in ex2.items():
        print(f"  {k:11s} = {v:.4f}")

    # ---- classification worked example 1: threshold 0.5 vs 0.6 ----
    thr_05 = cm_metrics(TP=9, TN=8, FP=2, FN=1)
    thr_06 = cm_metrics(TP=7, TN=8, FP=2, FN=3)
    print("\nThreshold 0.50:", {k: round(v, 3) for k, v in thr_05.items()})
    print("Threshold 0.60:", {k: round(v, 3) for k, v in thr_06.items()})

    # ---- full 21-row threshold scan -> ROC & PR curve points ----
    scan = [
        (1.00, 0, 10, 0, 10), (0.95, 1, 10, 0, 9), (0.90, 2, 10, 0, 8), (0.85, 2, 9, 1, 8),
        (0.80, 3, 9, 1, 7),   (0.75, 4, 9, 1, 6),  (0.70, 5, 9, 1, 5),  (0.65, 5, 8, 2, 5),
        (0.60, 6, 8, 2, 4),   (0.55, 7, 8, 2, 3),  (0.50, 8, 8, 2, 2),  (0.45, 9, 8, 2, 1),
        (0.40, 9, 7, 3, 1),   (0.35, 9, 6, 4, 1),  (0.30, 9, 5, 5, 1),  (0.25, 9, 4, 6, 1),
        (0.20, 9, 3, 7, 1),   (0.15, 9, 2, 8, 1),  (0.10, 9, 1, 9, 1),  (0.05, 10, 1, 9, 0),
        (0.00, 10, 0, 10, 0),
    ]

    print("\nthr    Acc   Prec   Recall Spec   F1     FPR")
    roc_points, prc_points = [], []
    for thr, TP, TN, FP, FN in scan:
        m = cm_metrics(TP, TN, FP, FN)
        fpr = 1 - m["specificity"]
        roc_points.append((fpr, m["recall"]))
        prc_points.append((m["recall"], m["precision"]))
        print(f"{thr:.2f}  {m['accuracy']:.2f}  {m['precision']:.3f}  {m['recall']:.2f}   "
              f"{m['specificity']:.2f}   {m['f1']:.3f}  {fpr:.2f}")

    roc_sorted = sorted(roc_points)
    auroc = np.trapz([r for _, r in roc_sorted], [f for f, _ in roc_sorted])
    print(f"\nApprox AUROC (trapezoidal estimate from this 21-point scan) = {auroc:.3f}")

    # ---- BLEU-style clipped n-gram precision (brief illustration) ----
    def clipped_precision(candidate, references, n=1):
        from collections import Counter
        def ngrams(tokens, n):
            return Counter(tuple(tokens[i:i + n]) for i in range(len(tokens) - n + 1))
        cand_ngrams = ngrams(candidate, n)
        max_ref_counts = Counter()
        for ref in references:
            ref_ngrams = ngrams(ref, n)
            for gram, cnt in ref_ngrams.items():
                max_ref_counts[gram] = max(max_ref_counts[gram], cnt)
        clipped = sum(min(cnt, max_ref_counts[gram]) for gram, cnt in cand_ngrams.items())
        total = sum(cand_ngrams.values())
        return clipped / total if total else 0.0

    ref1 = "He eats a sweet apple".split()
    ref2 = "He is eating a tasty apple".split()
    cand = "He He He eats tasty fruit".split()
    print(f"\nClipped 1-gram precision (repetition example) = {clipped_precision(cand, [ref1, ref2], 1):.3f}  (expected 3/6 = 0.5)")
