# Credit Card Fraud Detection：逻辑回归不平衡分类完整参考实现
# 数据集下载：https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud
# 默认使用仓库内的 creditcard_fraud_sample.csv；完整数据也可作为参数传入：
#   pip install pandas scikit-learn numpy
#   python creditcard_fraud_lab.py
#   python creditcard_fraud_lab.py creditcard.csv

from pathlib import Path
import sys

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    average_precision_score,
    classification_report,
    confusion_matrix,
    f1_score,
    precision_score,
    recall_score,
)
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler


DATA_PATH = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("creditcard_fraud_sample.csv")
RANDOM_STATE = 42


def choose_threshold(y_true, probabilities):
    """在验证集选择 F1 最高的阈值；真实业务应按审核成本与漏报成本设定。"""
    candidates = np.arange(0.05, 0.96, 0.01)
    scores = [f1_score(y_true, probabilities >= threshold) for threshold in candidates]
    best_index = int(np.argmax(scores))
    return float(candidates[best_index]), float(scores[best_index])


if not DATA_PATH.exists():
    raise FileNotFoundError(
        f"未找到 {DATA_PATH}。默认数据为 creditcard_fraud_sample.csv；"
        "完整数据请从 Kaggle 下载 creditcard.csv 后作为命令行参数传入。"
    )

# 1. 加载数据并检查不平衡程度
df = pd.read_csv(DATA_PATH)
if "Class" not in df.columns:
    raise ValueError("CSV 中未找到 Class 列，请确认使用 Kaggle 原始 creditcard.csv。")

X = df.drop(columns="Class")
y = df["Class"].astype(int)
print("== Credit Card Fraud Detection：逻辑回归实验 ==")
print(f"样本数：{len(df):,}；欺诈笔数：{int(y.sum()):,}；欺诈占比：{y.mean():.4%}")

# 2. 先留出最终测试集，再从训练部分切出验证集。
# stratify=y 保证三个集合中均保留稀有欺诈样本。
X_train_val, X_test, y_train_val, y_test = train_test_split(
    X, y, test_size=0.20, stratify=y, random_state=RANDOM_STATE
)
X_train, X_valid, y_train, y_valid = train_test_split(
    X_train_val,
    y_train_val,
    test_size=0.20,
    stratify=y_train_val,
    random_state=RANDOM_STATE,
)

# 3. 标准化后训练加权逻辑回归。
# class_weight='balanced' 会让少数类的错误具有更高损失权重。
model = Pipeline(
    steps=[
        ("scaler", StandardScaler()),
        (
            "classifier",
            LogisticRegression(
                class_weight="balanced", solver="lbfgs", max_iter=1000,
                random_state=RANDOM_STATE,
            ),
        ),
    ]
)
model.fit(X_train, y_train)

# 4. 不在测试集上调参：只根据验证集概率选择阈值。
valid_prob = model.predict_proba(X_valid)[:, 1]
threshold, valid_f1 = choose_threshold(y_valid, valid_prob)
print(f"验证集选出的阈值：{threshold:.2f}（F1 = {valid_f1:.4f}）")

# 5. 在从未参与训练和阈值选择的测试集上报告结果。
test_prob = model.predict_proba(X_test)[:, 1]
test_pred = (test_prob >= threshold).astype(int)

print("\n== 测试集评估 ==")
print(f"PR-AUC（Average Precision）：{average_precision_score(y_test, test_prob):.4f}")
print(f"Precision：{precision_score(y_test, test_pred, zero_division=0):.4f}")
print(f"Recall：{recall_score(y_test, test_pred, zero_division=0):.4f}")
print(f"F1：{f1_score(y_test, test_pred, zero_division=0):.4f}")
print("混淆矩阵 [[TN, FP], [FN, TP]]：")
print(confusion_matrix(y_test, test_pred))
print("\n分类报告：")
print(classification_report(y_test, test_pred, target_names=["正常", "欺诈"], zero_division=0))

print(
    "提示：阈值不是固定的 0.5。若漏掉一笔欺诈的代价更高，可降低阈值以提高 Recall，"
    "但同时会增加 FP 和人工审核量。"
)
