# -*- coding: utf-8 -*-
"""
auc_bootstrap.py
----------------
Mesure honnete de l'AUC avec intervalle de confiance par bootstrap.

POURQUOI CE SCRIPT
  Une AUC ponctuelle ne dit rien de sa fiabilite. Sur 100 observations,
  une AUC de 0.62 peut parfaitement correspondre a un modele qui ne
  predit rien : l'intervalle de confiance a 95% s'etend alors souvent
  de 0.48 a 0.75.

  La question utile n'est pas "quelle est mon AUC" mais "l'intervalle
  de confiance exclut-il 0.50". Si 0.50 est dans l'intervalle, le
  modele n'a pas demontre de pouvoir predictif.

DEUX SIGNAUX D'ALERTE
  1. AUC > 0.95 sur des donnees financieres : presque toujours une
     fuite de donnees (le modele lit la reponse au lieu de la predire).
  2. AUC nettement < 0.50 avec un IC entierement sous 0.50 : le modele
     predit a l'envers, ce qui indique generalement une inversion de
     label ou une erreur d'appariement signal/resultat.

USAGE
  python auc_bootstrap.py --csv donnees.csv --y-col y_real --pred-col pb
"""

import argparse
import sys


def auc_with_confidence(y_true, y_pred, n_boot=2000, seed=42):
    """
    AUC avec intervalle de confiance a 95% par bootstrap.

    Retourne (auc, borne_basse, borne_haute).
    Si l'intervalle contient 0.50, le modele n'a pas demontre
    de pouvoir predictif sur cet echantillon.
    """
    import numpy as np
    from sklearn.metrics import roc_auc_score

    rng = np.random.default_rng(seed)
    y_true = np.asarray(y_true)
    y_pred = np.asarray(y_pred)
    n = len(y_true)

    if n < 20:
        raise ValueError(f"echantillon trop petit ({n} observations)")
    if len(np.unique(y_true)) < 2:
        raise ValueError("une seule classe presente dans y_true")

    point = roc_auc_score(y_true, y_pred)

    scores = []
    for _ in range(n_boot):
        idx = rng.integers(0, n, n)
        if len(np.unique(y_true[idx])) < 2:
            continue
        scores.append(roc_auc_score(y_true[idx], y_pred[idx]))

    if not scores:
        raise ValueError("bootstrap impossible : classes trop desequilibrees")

    lo, hi = np.percentile(scores, [2.5, 97.5])
    return point, lo, hi


def interpret(auc, lo, hi, n):
    """Interprete le resultat de facon explicite."""
    lines = []
    lines.append(f"  observations       : {n}")
    lines.append(f"  AUC                : {auc:.4f}")
    lines.append(f"  IC 95%             : [{lo:.4f} ; {hi:.4f}]")
    lines.append("")

    if auc > 0.95:
        lines.append("  ALERTE : AUC > 0.95 sur des donnees financieres.")
        lines.append("  Une performance quasi parfaite indique presque")
        lines.append("  toujours une FUITE DE DONNEES : le modele accede,")
        lines.append("  directement ou non, a l'information qu'il doit")
        lines.append("  predire. Verifier :")
        lines.append("    - la fenetre d'appariement signal/resultat")
        lines.append("    - l'absence de reecriture des lignes de signal")
        lines.append("    - qu'aucune variable ne derive du resultat")
    elif lo <= 0.50 <= hi:
        lines.append("  PAS DE POUVOIR PREDICTIF DEMONTRE.")
        lines.append("  L'intervalle de confiance contient 0.50 : sur cet")
        lines.append("  echantillon, le modele n'est pas distinguable du")
        lines.append("  hasard. Ce n'est pas necessairement un mauvais")
        lines.append("  modele - l'echantillon peut simplement etre trop")
        lines.append("  petit pour trancher.")
    elif hi < 0.50:
        lines.append("  MODELE INVERSE.")
        lines.append("  L'intervalle est entierement SOUS 0.50 : le modele")
        lines.append("  predit systematiquement a l'envers. Causes")
        lines.append("  frequentes :")
        lines.append("    - inversion de label (win/loss echanges)")
        lines.append("    - mauvais appariement signal/resultat")
        lines.append("    - direction du signal inversee a l'execution")
    else:
        lines.append("  POUVOIR PREDICTIF MESURABLE.")
        lines.append(f"  L'intervalle est entierement au-dessus de 0.50.")
        lines.append("  A confirmer hors echantillon avant toute conclusion :")
        lines.append("  une AUC positive sur les donnees d'entrainement ne")
        lines.append("  garantit pas la performance future.")

    return "\n".join(lines)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--csv", required=True,
                    help="fichier CSV contenant labels et predictions")
    ap.add_argument("--sep", default=";")
    ap.add_argument("--y-col", default="y_real",
                    help="colonne du label reel (0/1)")
    ap.add_argument("--pred-col", default="pb",
                    help="colonne de la probabilite predite")
    ap.add_argument("--n-boot", type=int, default=2000)
    args = ap.parse_args()

    try:
        import pandas as pd
    except ImportError as e:
        print(f"[FATAL] {e}")
        sys.exit(1)

    try:
        df = pd.read_csv(args.csv, sep=args.sep)
    except Exception as e:
        print(f"[FATAL] lecture impossible : {e}")
        sys.exit(1)

    for col in (args.y_col, args.pred_col):
        if col not in df.columns:
            print(f"[FATAL] colonne '{col}' absente. "
                  f"Colonnes disponibles : {list(df.columns)}")
            sys.exit(1)

    d = df[[args.y_col, args.pred_col]].dropna()
    y = d[args.y_col].astype(int).values
    p = d[args.pred_col].astype(float).values

    print("=" * 66)
    print("AUC AVEC INTERVALLE DE CONFIANCE")
    print("=" * 66)

    try:
        auc, lo, hi = auc_with_confidence(y, p, n_boot=args.n_boot)
    except ValueError as e:
        print(f"[FATAL] {e}")
        sys.exit(1)

    print(interpret(auc, lo, hi, len(y)))
    print("=" * 66)


if __name__ == "__main__":
    main()
