用 Python 做朴素贝叶斯筛查与后验推理
前面的表格让你看见了公式在做什么;下面把同一件事交给 Python。代码沿用本章配套 c01-code.html 的练习主线:10,000 条合成信用记录、以信用评分构造课堂标签、训练高斯朴素贝叶斯、输出后验概率,并用明确的阈值完成风险筛查。为公开静态站点,数据只保留去标识化数值字段。
先分清三件事模型先计算 P(低风险 | 特征) 与 P(高风险 | 特征);标签只是按某个阈值得到的简写;业务动作则可能是常规处理、进入人工复核、要求补充材料。三者不能混为一谈,尤其不能把“高风险”直接等同于拒贷。
自学附件:完整代码实践本节的完整 Notebook 导出页面已作为附件放在课件中。它包含数据读取、贝叶斯筛查、后验概率、可视化和实践建议,适合课后按顺序自学。点击下方链接可在新页面打开。
打开 c01-code.html 完整代码实践
配套教学数据:去标识化的合成信用记录
原始笔记中的数据为合成数据;此处删去姓名、出生日期、城市和职业等不必要字段。high_risk = 1 的课堂定义为 CREDIT_SCORE < 650,仅用于演示,不是现实机构的授信规则。
下载去标识化教学数据(CSV)
读取数据:先核验规模、字段和基础率
把 CSV 下载后放在 Notebook 同级目录的 data 文件夹。Path 用来明确文件位置;features 明确哪些列可以成为证据,刻意不把编号放入模型。
读取与核验 · Python
from pathlib import Path
import pandas as pd
DATA_PATH = Path("data/credit_nb_teaching.csv")
df = pd.read_csv(DATA_PATH)
features = [
"AGE", "INCOME", "EXP_YRS", "SAVINGS", "DEBT_TOTAL",
"CC_UTIL", "LATE_30", "LATE_60", "NUM_CC", "ACCT_AGE",
]
y = df["high_risk"]
print("数据维度:", df.shape)
print("高风险样本比例:", f"{y.mean():.2%}")
print(df[["applicant_id", *features[:4], "high_risk"]].head())数据维度: (10000, 12)
高风险样本比例: 1.73%
applicant_id AGE INCOME EXP_YRS SAVINGS high_risk
0 A0001 56 56909.45 38 621744.90 0
1 A0002 32 68834.83 13 105990.18 0
…
把类别比例画出来 · Python
import matplotlib.pyplot as plt
counts = y.value_counts().sort_index()
labels = ["低风险(0)", "高风险(1)"]
plt.bar(labels, counts.values, color=["#6f9e8d", "#b65a2b"])
plt.ylabel("样本数")
plt.title("教学数据中的类别不平衡")
for i, value in enumerate(counts.values):
plt.text(i, value, f"{value:,}({value / len(y):.2%})", ha="center", va="bottom")
plt.show()
运行上方代码后的图形输出。数据类别明显不平衡,因此“准确率很高”不能证明模型已经识别出高风险申请人。
第一项重要发现1.73% 意味着每 100 人中约只有 2 人被标为高风险。若模型把所有人都预测为低风险,准确率仍会很高。因此这里不能只看 accuracy;还要看召回率、精确率、AUC 和被送去复核的人数。
训练高斯朴素贝叶斯:为每一个类别估计每一项证据
GaussianNB 是数值特征场景下的朴素贝叶斯实现。fit(X_train, y_train) 会按类别估计各列的均值和方差,同时记录类别先验;predict_proba(X_test) 的每一行都给出两个相加为 1 的后验概率。stratify=y 确保测试集保留与原数据相近的高风险比例。
训练与默认标签 · Python
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB
from sklearn.metrics import accuracy_score, recall_score, roc_auc_score
X_train, X_test, y_train, y_test = train_test_split(
df[features], y, test_size=0.30, random_state=42, stratify=y
)
model = GaussianNB(var_smoothing=1e-9)
model.fit(X_train, y_train)
pred_default = model.predict(X_test)
prob_high_risk = model.predict_proba(X_test)[:, 1]
print("训练集 / 测试集:", X_train.shape[0], "/", X_test.shape[0])
print("类别先验:", model.class_prior_.round(4))
print("AUC:", round(roc_auc_score(y_test, prob_high_risk), 4))
print("默认阈值下的 accuracy:", round(accuracy_score(y_test, pred_default), 4))
print("默认阈值下的 recall:", round(recall_score(y_test, pred_default, zero_division=0), 4))训练集 / 测试集: 7000 / 3000
类别先验: [0.9827 0.0173]
AUC: 0.8291
默认阈值下的 accuracy: 0.9827
默认阈值下的 recall: 0.0
怎样读这个结果AUC 为 0.8291,说明模型对风险概率有一定区分能力;但默认 0.50 阈值下一个高风险客户也没有被标出。不是代码错误,而是类别极不平衡,后验概率超过 50% 的条件很苛刻。
普惠金融含义不要把默认标签为低风险理解成没有风险。对于首贷、缺少征信记录或额度较大的申请,后验概率本身比 0/1 标签更有用。
需要自问漏掉一位高风险客户的成本,与把一位正常客户送去人工复核的成本,哪一个更高?阈值应由这个业务问题决定。
查看 ROC 曲线与预测概率分布 · Python
from sklearn.metrics import roc_curve
import matplotlib.pyplot as plt
fpr, tpr, _ = roc_curve(y_test, prob_high_risk)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(fpr, tpr, color="#1f6b65", label="GaussianNB")
axes[0].plot([0, 1], [0, 1], "--", color="#9aa1a8")
axes[0].set(xlabel="假阳性率", ylabel="真阳性率", title="ROC 曲线")
axes[0].legend()
axes[1].hist(prob_high_risk[y_test == 0], bins=30, alpha=.7, label="低风险")
axes[1].hist(prob_high_risk[y_test == 1], bins=20, alpha=.8, label="高风险")
axes[1].set(xlabel="预测的高风险后验概率", ylabel="样本数", title="后验概率分布")
axes[1].legend()
plt.tight_layout()
plt.show()
运行上方代码后的图形输出。左图考察不同阈值下的区分能力;右图显示大多数样本的高风险后验都很低,这与类别不平衡相互印证。
后验推理:对一位新申请人读出筛查概率
下面这位申请人默认标签为低风险,因为其高风险后验约为 14.61%,低于 50%。但它远高于训练样本中的 1.73% 高风险先验;若使用“10% 进入人工复核”的规则,他应进入复核队列,而不是被自动放行。
单个申请人的后验推理 · Python
new_application = pd.DataFrame([{
"AGE": 32, "INCOME": 75000, "EXP_YRS": 6, "SAVINGS": 120000,
"DEBT_TOTAL": 28000, "CC_UTIL": 0.28, "LATE_30": 0,
"LATE_60": 0, "NUM_CC": 2, "ACCT_AGE": 54,
}])
posterior = model.predict_proba(new_application)[0]
print(f"低风险后验概率:{posterior[0]:.2%}")
print(f"高风险后验概率:{posterior[1]:.2%}")
print("默认标签:", "高风险" if model.predict(new_application)[0] == 1 else "低风险")
print("10% 复核规则:", "进入人工复核" if posterior[1] >= 0.10 else "常规处理")低风险后验概率:85.39%
高风险后验概率:14.61%
默认标签: 低风险
10% 复核规则: 进入人工复核
把新申请人的后验画成柱状图 · Python
import matplotlib.pyplot as plt
labels = ["低风险后验", "高风险后验"]
colors = ["#6f9e8d", "#b65a2b"]
plt.bar(labels, posterior, color=colors)
plt.axhline(0.10, color="#8b4f24", linestyle="--", label="10% 复核阈值")
plt.ylim(0, 1)
plt.ylabel("后验概率")
plt.title("单个申请人的模型后验输出")
plt.legend()
plt.show()
运行上方代码后的图形输出。默认的 50% 标签规则给出“低风险”,但 14.61% 高风险后验超过了 10% 的人工复核线;这正是“概率、筛查标签和业务动作要分开”的例子。
把分类改成筛查:明确写出阈值与代价
阈值不是模型自动给出的真理,而是一项可审计的业务设置。本例明确采用 10% 高风险后验概率 作为进入人工复核的教学阈值:若 P(高风险 | 特征) >= 10%,则进入复核;否则常规处理。这里的代价也必须写清楚:漏筛高风险客户会带来坏账和后续催收成本;误筛正常客户会带来等待时间、人工审核成本和金融服务可得性的损失。真实应用还必须加入审核能力、客诉、拒贷申诉与群体公平性检查。
阈值筛查与评估 · Python
from sklearn.metrics import confusion_matrix, precision_score, recall_score
review_threshold = 0.10
review_flag = (prob_high_risk >= review_threshold).astype(int)
tn, fp, fn, tp = confusion_matrix(y_test, review_flag).ravel()
print("进入人工复核:", int(review_flag.sum()), "人")
print("precision:", round(precision_score(y_test, review_flag, zero_division=0), 4))
print("recall:", round(recall_score(y_test, review_flag, zero_division=0), 4))
print("混淆矩阵 [[TN, FP], [FN, TP]]:")
print([[int(tn), int(fp)], [int(fn), int(tp)]])进入人工复核: 631 人
precision: 0.0586
recall: 0.7115
混淆矩阵 [[TN, FP], [FN, TP]]:
[[2354, 594], [15, 37]]
比较不同复核阈值 · Python
thresholds = [0.05, 0.10, 0.15, 0.20, 0.30]
review_counts = [(prob_high_risk >= t).sum() for t in thresholds]
recalls = [recall_score(y_test, prob_high_risk >= t) for t in thresholds]
fig, ax1 = plt.subplots(figsize=(8, 4.5))
ax1.plot(thresholds, review_counts, marker="o", color="#1f6b65")
ax1.set(xlabel="人工复核阈值", ylabel="进入复核的人数")
ax2 = ax1.twinx()
ax2.plot(thresholds, recalls, marker="s", color="#b65a2b")
ax2.set_ylabel("高风险召回率")
plt.title("阈值改变时的筛查规模与召回率")
plt.show()
运行上方代码后的图形输出。阈值越低,送去人工复核的人越多,高风险样本也越不容易漏掉;这是一项资源配置与消费者权益之间需要明确讨论的权衡,而不是模型替人做出的决定。
不能省略的解释631 人中只有一小部分是课堂标签中的高风险样本,所以复核不等于拒绝;它表示需要更多信息或人工判断。降低阈值会提高召回率,也会增加复核量和误报。提高阈值会节省人工审核资源,但可能漏掉更多真实高风险样本。正确选择取决于漏筛代价、误筛代价、服务成本、风险承受能力和消费者权益,而不能只追求某一个指标。
从零写出高斯证据的核心:为什么要使用对数?
原始笔记中的 SimpleNaiveBayes 类会为每一类、每一个特征估计均值和标准差,再把各特征的概率密度相乘。许多很小的概率相乘会发生数值下溢,因此实现时先相加对数概率。GaussianNB 已经完成这些细节;理解下面函数有助于理解工具在做什么,但练习应优先使用上面的标准实现。
核心计算逻辑 · Python
import numpy as np
def gaussian_log_density(x, mean, std):
"""一个数值 x 在给定类别的高斯分布下的对数密度。"""
std = max(std, 1e-8)
return -0.5 * np.log(2 * np.pi * std**2) - 0.5 * ((x - mean) / std)**2
# 对每个候选类别:log(先验) + 每个特征的 log(似然)
# 比较所有类别的结果并归一化,便得到后验概率。