{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# F1. 信用评分卡决策系统 - 交互式实践\n",
    "\n",
    "> **案例文档**: `/workspace/docs/case-studies/finance/credit-scorecard-case.md`  \n",
    "> **决策类型**: 信贷审批自动化  \n",
    "> **方法论**: WOE 编码 + IV 筛选 + Logistic 回归  \n",
    "> **预计时间**: 60 分钟\n",
    "\n",
    "---\n",
    "\n",
    "## 学习目标\n",
    "\n",
    "完成本笔记本后，你将能够：\n",
    "1. 理解信用评分卡的工作原理和应用场景\n",
    "2. 实现 WOE (Weight of Evidence) 编码和 IV (Information Value) 筛选\n",
    "3. 构建 Logistic 回归评分卡模型\n",
    "4. 制定基于评分的决策规则\n",
    "5. 评估评分卡的区分能力和稳定性\n",
    "\n",
    "---\n",
    "\n",
    "## 1. 业务背景回顾\n",
    "\n",
    "### 问题描述\n",
    "\n",
    "某消费金融公司面临以下挑战：\n",
    "- 日均贷款申请：**5,000+** 笔\n",
    "- 人工审批时效：**2-3 个工作日**\n",
    "- 坏账率：**8.5%** (行业平均 6%)\n",
    "- 审批标准不一致：不同审批员通过率差异达 **30%**\n",
    "\n",
    "### 决策目标\n",
    "\n",
    "| 指标 | 当前 | 目标 |\n",
    "|------|------|------|\n",
    "| 审批时效 | 2-3 天 | < 5 分钟 |\n",
    "| 自动化率 | 0% | 80%+ |\n",
    "| 坏账率 | 8.5% | < 6% |\n",
    "| 通过率波动 | 30% | < 5% |\n",
    "\n",
    "---\n",
    "\n",
    "## 2. 数据准备"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import roc_auc_score, classification_report, confusion_matrix\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "\n",
    "plt.style.use('seaborn-v0_8')\n",
    "plt.rcParams['font.size'] = 12\n",
    "\n",
    "print(\"✓ 库加载完成\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.1 生成模拟数据\n",
    "\n",
    "我们生成 10,000 个贷款申请的模拟数据，包含以下特征："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.random.seed(42)\n",
    "n_samples = 10000\n",
    "\n",
    "# 生成特征数据\n",
    "data = pd.DataFrame({\n",
    "    # 信用记录特征\n",
    "    'credit_score': np.random.normal(680, 80, n_samples).clip(300, 850),\n",
    "    'delinquency_count': np.random.poisson(1.5, n_samples),\n",
    "    'credit_utilization': np.random.beta(2, 5, n_samples),\n",
    "    'credit_history_years': np.random.exponential(5, n_samples).clip(0, 30),\n",
    "    \n",
    "    # 还款能力特征\n",
    "    'annual_income': np.random.lognormal(10.5, 0.8, n_samples),\n",
    "    'debt_to_income': np.random.beta(2, 5, n_samples),\n",
    "    'employment_years': np.random.exponential(4, n_samples).clip(0, 40),\n",
    "    \n",
    "    # 申请信息\n",
    "    'loan_amount': np.random.lognormal(9, 0.6, n_samples),\n",
    "    'loan_term_months': np.random.choice([12, 24, 36, 48, 60], n_samples),\n",
    "    \n",
    "    # 行为数据\n",
    "    'inquiries_last_6m': np.random.poisson(2, n_samples),\n",
    "    'existing_accounts': np.random.poisson(5, n_samples)\n",
    "})\n",
    "\n",
    "# 生成目标变量 (是否违约)\n",
    "# 基于特征构建违约概率\n",
    "default_prob = (\n",
    "    0.4 * (850 - data['credit_score']) / 550 +  # 信用分数越低，违约概率越高\n",
    "    0.2 * data['delinquency_count'] / 10 +  # 逾期次数越多，违约概率越高\n",
    "    0.15 * data['credit_utilization'] +  # 信用使用率越高，违约概率越高\n",
    "    0.1 * (1 - data['debt_to_income']) +  # 债务收入比越高，违约概率越高\n",
    "    0.1 * np.random.uniform(0, 1, n_samples)  # 随机因素\n",
    ")\n",
    "\n",
    "# 转换为二分类目标\n",
    "data['default'] = (default_prob > np.random.uniform(0, 1, n_samples)).astype(int)\n",
    "\n",
    "print(f\"数据集形状：{data.shape}\")\n",
    "print(f\"违约样本数：{data['default'].sum()} ({data['default'].mean()*100:.2f}%)\")\n",
    "print(f\"\\n特征统计:\")\n",
    "data.describe().T"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.2 数据集划分"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 划分训练集和测试集\n",
    "train_data, test_data = train_test_split(\n",
    "    data, \n",
    "    test_size=0.2, \n",
    "    random_state=42,\n",
    "    stratify=data['default']\n",
    ")\n",
    "\n",
    "print(f\"训练集：{len(train_data)} 样本\")\n",
    "print(f\"测试集：{len(test_data)} 样本\")\n",
    "print(f\"\\n训练集违约率：{train_data['default'].mean()*100:.2f}%\")\n",
    "print(f\"测试集违约率：{test_data['default'].mean()*100:.2f}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 3. 特征工程：WOE 与 IV\n",
    "\n",
    "### 3.1 WOE (Weight of Evidence)\n",
    "\n",
    "**公式**:\n",
    "$$\\text{WOE}_i = \\ln\\left(\\frac{\\text{Good}_i / \\text{Total Good}}{\\text{Bad}_i / \\text{Total Bad}}\\right)$$\n",
    "\n",
    "**含义**:\n",
    "- WOE > 0: 该组\"好客户\"比例高于平均\n",
    "- WOE < 0: 该组\"坏客户\"比例高于平均\n",
    "- WOE = 0: 该组与平均风险相同"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def calculate_woe(df, feature, target, n_bins=10):\n",
    "    \"\"\"\n",
    "    计算特征的 WOE 值\n",
    "    \n",
    "    参数:\n",
    "        df: 数据框\n",
    "        feature: 特征名\n",
    "        target: 目标变量名\n",
    "        n_bins: 分箱数量\n",
    "    \n",
    "    返回:\n",
    "        bin_stats: 分箱统计和 WOE 值\n",
    "    \"\"\"\n",
    "    # 等频分箱\n",
    "    df_temp = df[[feature, target]].copy()\n",
    "    df_temp['bin'] = pd.qcut(df_temp[feature], q=n_bins, duplicates='drop')\n",
    "    \n",
    "    # 计算每个分箱的统计\n",
    "    bin_stats = df_temp.groupby('bin').agg(\n",
    "        total=(target, 'count'),\n",
    "        bad=(target, 'sum'),\n",
    "        feature_mean=(feature, 'mean')\n",
    "    ).reset_index()\n",
    "    \n",
    "    bin_stats['good'] = bin_stats['total'] - bin_stats['bad']\n",
    "    \n",
    "    # 计算总的好/坏客户数\n",
    "    total_good = (df[target] == 0).sum()\n",
    "    total_bad = (df[target] == 1).sum()\n",
    "    \n",
    "    # 计算 WOE\n",
    "    bin_stats['woe'] = np.log(\n",
    "        (bin_stats['good'] / total_good) / \n",
    "        (bin_stats['bad'] / total_bad + 1e-10) + 1e-10\n",
    "    )\n",
    "    \n",
    "    return bin_stats\n",
    "\n",
    "# 测试 WOE 计算\n",
    "woe_result = calculate_woe(train_data, 'credit_score', 'default', n_bins=5)\n",
    "print(\"信用分数 WOE 分析:\")\n",
    "woe_result"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.2 IV (Information Value)\n",
    "\n",
    "**公式**:\n",
    "$$\\text{IV} = \\sum_i \\left(\\frac{\\text{Good}_i}{\\text{Total Good}} - \\frac{\\text{Bad}_i}{\\text{Total Bad}}\\right) \\times \\text{WOE}_i$$\n",
    "\n",
    "**评估标准**:\n",
    "| IV 值 | 预测能力 |\n",
    "|-------|----------|\n",
    "| < 0.02 | 无预测能力 |\n",
    "| 0.02-0.1 | 弱 |\n",
    "| 0.1-0.3 | 中等 |\n",
    "| 0.3-0.5 | 强 |\n",
    "| > 0.5 | 过强 (可能过拟合) |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def calculate_iv(bin_stats):\n",
    "    \"\"\"\n",
    "    计算特征的 IV 值\n",
    "    \n",
    "    参数:\n",
    "        bin_stats: 分箱统计 (包含 WOE)\n",
    "    \n",
    "    返回:\n",
    "        iv_value: IV 值\n",
    "    \"\"\"\n",
    "    total_good = bin_stats['good'].sum()\n",
    "    total_bad = bin_stats['bad'].sum()\n",
    "    \n",
    "    bin_stats['pct_good'] = bin_stats['good'] / total_good\n",
    "    bin_stats['pct_bad'] = bin_stats['bad'] / total_bad\n",
    "    \n",
    "    bin_stats['iv_component'] = (\n",
    "        (bin_stats['pct_good'] - bin_stats['pct_bad']) * bin_stats['woe']\n",
    "    )\n",
    "    \n",
    "    return bin_stats['iv_component'].sum()\n",
    "\n",
    "# 计算所有特征的 IV 值\n",
    "feature_cols = [col for col in data.columns if col not in ['default']]\n",
    "iv_summary = []\n",
    "\n",
    "for feature in feature_cols:\n",
    "    try:\n",
    "        woe_stats = calculate_woe(train_data, feature, 'default', n_bins=10)\n",
    "        iv_value = calculate_iv(woe_stats)\n",
    "        iv_summary.append({\n",
    "            'feature': feature,\n",
    "            'iv': iv_value,\n",
    "            'predictive_power': get_predictive_power(iv_value)\n",
    "        })\n",
    "    except Exception as e:\n",
    "        print(f\"特征 {feature} 计算失败：{str(e)}\")\n",
    "\n",
    "iv_df = pd.DataFrame(iv_summary).sort_values('iv', ascending=False)\n",
    "\n",
    "print(\"\\n特征 IV 值排序:\")\n",
    "iv_df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_predictive_power(iv):\n",
    "    \"\"\"根据 IV 值评估预测能力\"\"\"\n",
    "    if iv < 0.02:\n",
    "        return '无预测能力'\n",
    "    elif iv < 0.1:\n",
    "        return '弱'\n",
    "    elif iv < 0.3:\n",
    "        return '中等'\n",
    "    elif iv < 0.5:\n",
    "        return '强'\n",
    "    else:\n",
    "        return '过强 (可能过拟合)'\n",
    "\n",
    "# 可视化 IV 值\n",
    "fig, ax = plt.subplots(figsize=(12, 6))\n",
    "\n",
    "colors = pd.Categorical(iv_df['predictive_power'], \n",
    "                        categories=['无预测能力', '弱', '中等', '强', '过强 (可能过拟合)'],\n",
    "                        ordered=True)\n",
    "\n",
    "bars = ax.barh(iv_df['feature'], iv_df['iv'], color=plt.cm.viridis(colors.codes / 5))\n",
    "ax.set_xlabel('Information Value (IV)')\n",
    "ax.set_title('特征预测能力评估')\n",
    "ax.axvline(x=0.02, color='gray', linestyle='--', alpha=0.5, label='阈值：0.02')\n",
    "ax.axvline(x=0.1, color='orange', linestyle='--', alpha=0.5, label='阈值：0.1')\n",
    "ax.axvline(x=0.3, color='red', linestyle='--', alpha=0.5, label='阈值：0.3')\n",
    "ax.legend()\n",
    "ax.grid(axis='x', alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 4. 构建评分卡模型"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.1 特征选择\n",
    "\n",
    "选择 IV > 0.1 的特征作为模型输入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 选择 IV > 0.1 的特征\n",
    "selected_features = iv_df[iv_df['iv'] > 0.1]['feature'].tolist()\n",
    "print(f\"选择的特征 ({len(selected_features)}个): {selected_features}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.2 WOE 转换"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def apply_woe_encoding(df, feature, woe_stats):\n",
    "    \"\"\"\n",
    "    将特征转换为 WOE 编码\n",
    "    \n",
    "    参数:\n",
    "        df: 数据框\n",
    "        feature: 特征名\n",
    "        woe_stats: WOE 统计表\n",
    "    \n",
    "    返回:\n",
    "        df_encoded: WOE 编码后的特征\n",
    "    \"\"\"\n",
    "    df_temp = df.copy()\n",
    "    \n",
    "    # 创建分箱边界\n",
    "    if pd.api.types.is_numeric_dtype(df[feature]):\n",
    "        # 数值特征：使用等频分箱\n",
    "        try:\n",
    "            df_temp['bin'] = pd.qcut(df_temp[feature], q=len(woe_stats), duplicates='drop')\n",
    "        except:\n",
    "            df_temp['bin'] = pd.cut(df_temp[feature], bins=len(woe_stats))\n",
    "    else:\n",
    "        # 类别特征\n",
    "        df_temp['bin'] = df_temp[feature]\n",
    "    \n",
    "    # 创建 WOE 映射字典\n",
    "    woe_map = dict(zip(woe_stats['bin'], woe_stats['woe']))\n",
    "    \n",
    "    # 应用映射\n",
    "    df_temp[f'{feature}_woe'] = df_temp['bin'].map(woe_map)\n",
    "    \n",
    "    return df_temp[f'{feature}_woe']\n",
    "\n",
    "# 对训练集和测试集进行 WOE 编码\n",
    "train_woe = train_data[selected_features].copy()\n",
    "test_woe = test_data[selected_features].copy()\n",
    "\n",
    "for feature in selected_features:\n",
    "    woe_stats = calculate_woe(train_data, feature, 'default', n_bins=10)\n",
    "    train_woe[feature] = apply_woe_encoding(train_data, feature, woe_stats)\n",
    "    test_woe[feature] = apply_woe_encoding(test_data, feature, woe_stats)\n",
    "\n",
    "print(\"✓ WOE 编码完成\")\n",
    "print(f\"训练集形状：{train_woe.shape}\")\n",
    "print(f\"测试集形状：{test_woe.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.3 Logistic 回归模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 准备训练数据\n",
    "X_train = train_woe.fillna(0)\n",
    "y_train = train_data['default']\n",
    "X_test = test_woe.fillna(0)\n",
    "y_test = test_data['default']\n",
    "\n",
    "# 训练 Logistic 回归模型\n",
    "model = LogisticRegression(\n",
    "    penalty='l2',\n",
    "    C=1.0,\n",
    "    max_iter=1000,\n",
    "    random_state=42\n",
    ")\n",
    "\n",
    "model.fit(X_train, y_train)\n",
    "\n",
    "# 模型评估\n",
    "train_pred = model.predict(X_train)\n",
    "train_proba = model.predict_proba(X_train)[:, 1]\n",
    "test_pred = model.predict(X_test)\n",
    "test_proba = model.predict_proba(X_test)[:, 1]\n",
    "\n",
    "print(\"=\" * 50)\n",
    "print(\"模型评估结果\")\n",
    "print(\"=\" * 50)\n",
    "print(f\"\\n训练集:\")\n",
    "print(f\"  准确率：{model.score(X_train, y_train):.4f}\")\n",
    "print(f\"  AUC: {roc_auc_score(y_train, train_proba):.4f}\")\n",
    "print(f\"\\n测试集:\")\n",
    "print(f\"  准确率：{model.score(X_test, y_test):.4f}\")\n",
    "print(f\"  AUC: {roc_auc_score(y_test, test_proba):.4f}\")\n",
    "print(\"\\n分类报告 (测试集):\")\n",
    "print(classification_report(y_test, test_pred))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.4 模型系数 (评分卡核心)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 提取模型系数\n",
    "coefficients = pd.DataFrame({\n",
    "    'feature': selected_features,\n",
    "    'coefficient': model.coef_[0],\n",
    "    'abs_coefficient': np.abs(model.coef_[0])\n",
    "}).sort_values('abs_coefficient', ascending=False)\n",
    "\n",
    "print(\"模型系数 (按重要性排序):\")\n",
    "coefficients[['feature', 'coefficient']]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 5. 评分转换与决策规则"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.1 评分刻度设置\n",
    "\n",
    "业界标准评分转换公式：\n",
    "\n",
    "$$\\text{Score} = \\text{Offset} - \\text{Slope} \\times \\ln(\\text{Odds})$$\n",
    "\n",
    "常用设置:\n",
    "- **基础分**: 600 分 (对应 odds=1:60)\n",
    "- **PDO**: 50 分 (Odds 翻倍时分数增加)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 评分刻度参数\n",
    "base_score = 600  # 基础分\n",
    "pdo = 50  # Odds 翻倍时的分数增量\n",
    "\n",
    "# 计算 Slope 和 Offset\n",
    "slope = pdo / np.log(2)\n",
    "offset = base_score + slope * np.log(1/60)  # 假设基准 odds=1:60\n",
    "\n",
    "print(f\"评分参数:\")\n",
    "print(f\"  基础分：{base_score}\")\n",
    "print(f\"  PDO: {pdo}\")\n",
    "print(f\"  Slope: {slope:.2f}\")\n",
    "print(f\"  Offset: {offset:.2f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 将违约概率转换为评分\n",
    "def probability_to_score(prob, slope, offset):\n",
    "    \"\"\"\n",
    "    将概率转换为评分\n",
    "    \n",
    "    参数:\n",
    "        prob: 违约概率\n",
    "        slope: 斜率\n",
    "        offset: 截距\n",
    "    \n",
    "    返回:\n",
    "        score: 信用评分\n",
    "    \"\"\"\n",
    "    odds = prob / (1 - prob + 1e-10)\n",
    "    score = offset - slope * np.log(odds + 1e-10)\n",
    "    return score.clip(300, 850)  # 限制在 300-850 范围\n",
    "\n",
    "# 计算测试集评分\n",
    "test_scores = probability_to_score(test_proba, slope, offset)\n",
    "\n",
    "print(f\"测试集评分统计:\")\n",
    "print(f\"  最小分：{test_scores.min():.1f}\")\n",
    "print(f\"  最大分：{test_scores.max():.1f}\")\n",
    "print(f\"  平均分：{test_scores.mean():.1f}\")\n",
    "print(f\"  中位数：{np.median(test_scores):.1f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.2 决策规则制定"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 定义决策阈值\n",
    "approve_threshold = 650  # 批准阈值\n",
    "reject_threshold = 550   # 拒绝阈值\n",
    "\n",
    "# 制定决策规则\n",
    "test_data['score'] = test_scores\n",
    "test_data['decision'] = pd.cut(\n",
    "    test_data['score'],\n",
    "    bins=[0, reject_threshold, approve_threshold, 1000],\n",
    "    labels=['拒绝', '人工复核', '批准']\n",
    ")\n",
    "\n",
    "# 分析各决策组的表现\n",
    "decision_analysis = test_data.groupby('decision').agg(\n",
    "    样本数=('default', 'count'),\n",
    "    违约数=('default', 'sum'),\n",
    "    违约率=('default', 'mean'),\n",
    "    平均分=('score', 'mean')\n",
    ").reset_index()\n",
    "\n",
    "decision_analysis['违约率_formatted'] = (decision_analysis['违约率'] * 100).round(2).astype(str) + '%'\n",
    "decision_analysis"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.3 决策效果评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 计算不同阈值下的指标\n",
    "thresholds = np.arange(500, 750, 25)\n",
    "results = []\n",
    "\n",
    "for threshold in thresholds:\n",
    "    predicted_default = test_scores < threshold\n",
    "    \n",
    "    tn, fp, fn, tp = confusion_matrix(y_test, predicted_default).ravel()\n",
    "    \n",
    "    results.append({\n",
    "        'threshold': threshold,\n",
    "        'approval_rate': (test_scores >= threshold).mean(),\n",
    "        'default_rate': y_test[predicted_default == False].mean(),\n",
    "        'accuracy': (tp + tn) / len(y_test),\n",
    "        'precision': tp / (tp + fp + 1e-10),\n",
    "        'recall': tp / (tp + fn + 1e-10)\n",
    "    })\n",
    "\n",
    "results_df = pd.DataFrame(results)\n",
    "\n",
    "# 可视化\n",
    "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n",
    "\n",
    "axes[0, 0].plot(results_df['threshold'], results_df['approval_rate'], 'b-', linewidth=2)\n",
    "axes[0, 0].set_xlabel('评分阈值')\n",
    "axes[0, 0].set_ylabel('批准率', color='b')\n",
    "axes[0, 0].set_title('阈值对批准率的影响')\n",
    "axes[0, 0].grid(alpha=0.3)\n",
    "\n",
    "axes[0, 1].plot(results_df['threshold'], results_df['default_rate'], 'r-', linewidth=2)\n",
    "axes[0, 1].set_xlabel('评分阈值')\n",
    "axes[0, 1].set_ylabel('违约率', color='r')\n",
    "axes[0, 1].set_title('阈值对违约率的影响')\n",
    "axes[0, 1].grid(alpha=0.3)\n",
    "\n",
    "axes[1, 0].plot(results_df['threshold'], results_df['precision'], 'g-', linewidth=2, label='精确率')\n",
    "axes[1, 0].plot(results_df['threshold'], results_df['recall'], 'orange', linewidth=2, label='召回率')\n",
    "axes[1, 0].set_xlabel('评分阈值')\n",
    "axes[1, 0].set_ylabel('指标值')\n",
    "axes[1, 0].set_title('精确率 vs 召回率')\n",
    "axes[1, 0].legend()\n",
    "axes[1, 0].grid(alpha=0.3)\n",
    "\n",
    "axes[1, 1].plot(results_df['threshold'], results_df['accuracy'], 'purple', linewidth=2)\n",
    "axes[1, 1].set_xlabel('评分阈值')\n",
    "axes[1, 1].set_ylabel('准确率')\n",
    "axes[1, 1].set_title('阈值对准确率的影响')\n",
    "axes[1, 1].grid(alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 6. 模型评估"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6.1 ROC 曲线与 KS 统计量"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import roc_curve\n",
    "\n",
    "# 计算 ROC 曲线\n",
    "fpr, tpr, thresholds_roc = roc_curve(y_test, test_proba)\n",
    "auc_score = roc_auc_score(y_test, test_proba)\n",
    "\n",
    "# 计算 KS 统计量\n",
    "ks_statistic = max(tpr - fpr)\n",
    "ks_threshold = thresholds_roc[np.argmax(tpr - fpr)]\n",
    "\n",
    "print(f\"模型区分能力:\")\n",
    "print(f\"  AUC: {auc_score:.4f}\")\n",
    "print(f\"  KS 统计量：{ks_statistic:.4f}\")\n",
    "print(f\"  KS 最优阈值：{ks_threshold:.4f}\")\n",
    "\n",
    "# 可视化\n",
    "fig, ax = plt.subplots(figsize=(10, 8))\n",
    "\n",
    "ax.plot(fpr, tpr, 'b-', linewidth=2, label=f'ROC 曲线 (AUC={auc_score:.4f})')\n",
    "ax.plot([0, 1], [0, 1], 'r--', linewidth=1, label='随机猜测')\n",
    "ax.set_xlabel('假阳性率 (FPR)')\n",
    "ax.set_ylabel('真阳性率 (TPR)')\n",
    "ax.set_title('ROC 曲线')\n",
    "ax.legend()\n",
    "ax.grid(alpha=0.3)\n",
    "\n",
    "# 添加 KS 点标记\n",
    "ax.scatter([fpr[np.argmax(tpr - fpr)]], [tpr[np.argmax(tpr - fpr)]], \n",
    "           color='green', s=200, zorder=5, label=f'KS={ks_statistic:.4f}')\n",
    "ax.legend()\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6.2 评分分布分析"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 评分分布\n",
    "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
    "\n",
    "# 左图：好/坏客户评分分布\n",
    "good_scores = test_scores[y_test == 0]\n",
    "bad_scores = test_scores[y_test == 1]\n",
    "\n",
    "axes[0].hist(good_scores, bins=30, alpha=0.5, label='好客户', color='green', edgecolor='black')\n",
    "axes[0].hist(bad_scores, bins=30, alpha=0.5, label='违约客户', color='red', edgecolor='black')\n",
    "axes[0].axvline(x=approve_threshold, color='blue', linestyle='--', linewidth=2, label='批准阈值')\n",
    "axes[0].axvline(x=reject_threshold, color='red', linestyle='--', linewidth=2, label='拒绝阈值')\n",
    "axes[0].set_xlabel('信用评分')\n",
    "axes[0].set_ylabel('样本数')\n",
    "axes[0].set_title('好/坏客户评分分布')\n",
    "axes[0].legend()\n",
    "axes[0].grid(alpha=0.3)\n",
    "\n",
    "# 右图：评分密度曲线\n",
    "from scipy import stats\n",
    "\n",
    "good_density = stats.gaussian_kde(good_scores)\n",
    "bad_density = stats.gaussian_kde(bad_scores)\n",
    "x_range = np.linspace(300, 850, 200)\n",
    "\n",
    "axes[1].plot(x_range, good_density(x_range), 'g-', linewidth=2, label='好客户')\n",
    "axes[1].plot(x_range, bad_density(x_range), 'r-', linewidth=2, label='违约客户')\n",
    "axes[1].axvline(x=approve_threshold, color='blue', linestyle='--', linewidth=2)\n",
    "axes[1].axvline(x=reject_threshold, color='red', linestyle='--', linewidth=2)\n",
    "axes[1].set_xlabel('信用评分')\n",
    "axes[1].set_ylabel('密度')\n",
    "axes[1].set_title('评分密度分布')\n",
    "axes[1].legend()\n",
    "axes[1].grid(alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 7. 业务影响评估"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 假设业务参数\n",
    "avg_loan_amount = 50000  # 平均贷款金额 (元)\n",
    "interest_rate = 0.12    # 年利率 12%\n",
    "loss_given_default = 0.7  # 违约损失率 70%\n",
    "daily_applications = 5000  # 日均申请量\n",
    "\n",
    "# 计算当前策略 (假设全部人工审批，坏账率 8.5%)\n",
    "current_default_rate = 0.085\n",
    "current_approval_rate = 0.6  # 假设人工审批通过率 60%\n",
    "\n",
    "# 计算新策略下指标\n",
    "new_default_rate = test_data[test_data['score'] >= approve_threshold]['default'].mean()\n",
    "new_approval_rate = (test_data['score'] >= approve_threshold).mean()\n",
    "\n",
    "print(\"=\" * 60)\n",
    "print(\"业务影响对比\")\n",
    "print(\"=\" * 60)\n",
    "print(f\"\\n审批效率:\")\n",
    "print(f\"  当前：2-3 个工作日 → 新：实时审批\")\n",
    "print(f\"  自动化率：0% → {new_approval_rate*100:.1f}%\")\n",
    "\n",
    "print(f\"\\n风险控制:\")\n",
    "print(f\"  当前坏账率：{current_default_rate*100:.2f}%\")\n",
    "print(f\"  新坏账率：{new_default_rate*100:.2f}%\")\n",
    "print(f\"  风险降低：{(current_default_rate - new_default_rate)*100:.2f}%\")\n",
    "\n",
    "print(f\"\\n经济价值 (年化):\")\n",
    "\n",
    "# 年贷款总额\n",
    "annual_loan_volume = daily_applications * 365 * avg_loan_amount\n",
    "\n",
    "# 当前坏账损失\n",
    "current_loss = annual_loan_volume * current_approval_rate * current_default_rate * loss_given_default\n",
    "\n",
    "# 新策略坏账损失\n",
    "new_loss = annual_loan_volume * new_approval_rate * new_default_rate * loss_given_default\n",
    "\n",
    "# 节省的坏账损失\n",
    "saved_loss = current_loss - new_loss\n",
    "\n",
    "# 利息收入变化\n",
    "current_interest = annual_loan_volume * current_approval_rate * interest_rate\n",
    "new_interest = annual_loan_volume * new_approval_rate * interest_rate\n",
    "interest_change = new_interest - current_interest\n",
    "\n",
    "print(f\"  当前坏账损失：¥{current_loss/1e8:.2f}亿\")\n",
    "print(f\"  新坏账损失：¥{new_loss/1e8:.2f}亿\")\n",
    "print(f\"  节省坏账损失：¥{saved_loss/1e8:.2f}亿\")\n",
    "print(f\"  利息收入变化：¥{interest_change/1e8:.2f}亿\")\n",
    "print(f\"  净收益：¥{(saved_loss + interest_change)/1e8:.2f}亿\")\n",
    "\n",
    "print(\"\\n\" + \"=\" * 60)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 8. 总结与练习"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8.1 关键知识点\n",
    "\n",
    "1. **WOE 编码**: 将特征转换为与违约风险相关的标准化值\n",
    "2. **IV 筛选**: 评估特征预测能力，选择有效特征\n",
    "3. **评分转换**: 将违约概率转换为直观的信用评分\n",
    "4. **阈值设定**: 平衡批准率和违约率的业务决策\n",
    "\n",
    "### 8.2 动手练习\n",
    "\n",
    "尝试完成以下任务：\n",
    "\n",
    "```python\n",
    "# 练习 1: 调整 PDO 参数，观察评分分布变化\n",
    "new_pdo = 40  # 改为 40\n",
    "# 重新计算评分并分析\n",
    "\n",
    "# 练习 2: 修改批准阈值，评估对业务指标的影响\n",
    "new_threshold = 680  # 提高到 680 分\n",
    "# 计算新的批准率和坏账率\n",
    "\n",
    "# 练习 3: 添加新的特征，重新训练模型\n",
    "# 例如：年龄、教育程度、婚姻状况\n",
    "# 观察 AUC 和 KS 的变化\n",
    "\n",
    "# 练习 4: 实现 PSI (Population Stability Index) 监控\n",
    "# 评估训练集和测试集的分布差异\n",
    "```\n",
    "\n",
    "### 8.3 下一步\n",
    "\n",
    "- 阅读案例文档：`/workspace/docs/case-studies/finance/credit-scorecard-case.md`\n",
    "- 学习下一个案例：客户流失预测 (`02-churn-prediction.ipynb`)\n",
    "- 深入理解决策框架指南：`/workspace/docs/DECISION_FRAMEWORK_GUIDE.md`\n",
    "\n",
    "---\n",
    "\n",
    "**完成时间**: ______  \n",
    "**难度评分**: □ 简单 □ 适中 □ 困难  \n",
    "**掌握程度**: □ 理解 □ 能实现 □ 能教授他人"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
