Logistic回归
本文档面向数学建模竞赛备赛学习者,系统介绍二分类利器 Logistic 回归:从算法原理、适用场景、评价指标、可视化图表,到一份可直接运行验证的完整 Python 代码(手写梯度下降 + sklearn 对照),以及竞赛论文的写作建议。全文示例数据均由代码内随机种子生成,无外部文件依赖,跑通第六节代码即可复现所有图表与指标。
一、算法含义
1.1 一句话理解
Logistic 回归 = "线性回归 + sigmoid 压缩"。模型先对特征做线性组合,得到一个实数"分数" ,再用 sigmoid 函数把 压进 区间,把输出解释为"样本属于正类()的概率"。因此它名字里带"回归",干的却是分类的活。
1.2 模型形式
线性部分(先算一个"分数"):
sigmoid 函数(把分数压缩成概率):
sigmoid 的三条重要性质:
- 值域为 ,天然适合表示概率;
- 单调递增,且 ;
- 对称性 ,导数 (推导梯度时要用)。
于是完整的模型写为:
决策规则:预测 。可见决策边界是方程 给出的一个超平面(二维时是一条直线),所以 Logistic 回归本质上是线性分类器。
1.3 对数几率(logit)变换——"回归"二字的由来
定义几率(odds):,取值范围 ;再取自然对数得到对数几率(logit),值域变为 :
等号左边是概率的对数几率变换,右边是特征的线性组合——模型正是对"对数几率"做了一次线性回归。这就是它叫"回归"的第一个原因。
1.4 为什么叫"回归"却是分类模型
- 历史渊源:Logistic 回归由线性回归发展而来,是广义线性模型(GLM)的一员——因变量服从伯努利分布,连接函数取 logit(线性回归是正态分布 + 恒等连接)。
- 回归的对象是连续量:模型直接回归的是对数几率(连续值),而不是 0/1 标签本身;对数几率经 sigmoid 反变换成概率,最后按阈值切成类别。
- 输出是概率:预测值 是连续的概率,可用于风险排序(例如"违约概率 0.32"比一句"会违约"信息量大得多),这也是它与 SVM 这类"硬分类"模型的本质区别。
一句话总结:用回归的思想估计参数、以概率的形式输出、靠阈值完成分类。
1.5 参数求解:极大似然估计
对 个样本 ,似然函数(各样本概率的连乘)为:
取负对数并除以 (不影响最优解),得到平均交叉熵(对数损失),也就是训练要最小化的目标函数:
极大似然估计 = 最小化交叉熵损失。 是凸函数,存在唯一全局最优解,但没有闭式解,需要用数值迭代求解。
梯度下降法(一阶方法)。利用 求导可得梯度( 是加了一列全 1 的增广设计矩阵, 是预测概率向量, 是标签向量):
迭代公式:
其中 是学习率。注意梯度形式与线性回归的 惊人地相似——只是把"预测值"换成了"预测概率"。
牛顿法(二阶方法,收敛更快)。Hessian 矩阵为:
迭代公式:
牛顿法等价于"迭代加权最小二乘"(IRLS):每一步都在解一个加权线性回归问题。它比梯度下降收敛快得多(局部二次收敛),但每次迭代要求解一个 的线性方程组。scikit-learn 默认求解器 lbfgs 就是拟牛顿法(不用显式求逆 )。第六节代码会同时给出手写梯度下降与 sklearn 拟牛顿法的结果对照,两者系数几乎一致。
1.6 优势比(OR)——系数的正确解读方式
由 logit 变换反解出 。当第 个特征 增加 1 个单位、其余特征不变时:
- : 增加会提高正类几率,是"风险/促进因素";
- : 增加会降低正类几率,是"保护因素";
- : 与结果无关联。
解释系数一定要用 OR,不要直接解释 本身。 作用在对数几率(logit)尺度上,量纲不直观;而 OR 是倍数关系("几率变为原来的 2.7 倍"),人人能听懂,也是医学、流行病学论文的标准表达。
1.7 二分类 → 多分类:softmax 一句话
多分类时把 sigmoid 换成 softmax:
时 softmax 与 sigmoid 完全等价(两类的概率之比取对数就是 logit)。scikit-learn 中 LogisticRegression 的默认多分类模式(multinomial)就是 softmax 回归。详见第八节。
1.8 优缺点
优点:
- 直接输出校准较好的概率,可做风险排序、按需调整阈值;
- 系数可解释性强(OR 含义明确),竞赛论文好写"结论";
- 训练速度快、内存占用小,超参数少(主要只有学习率/正则强度);
- 损失函数是凸函数,梯度下降/牛顿法能收敛到全局最优,不存在局部极小值问题;
- 可加 L1/L2 正则化,防止过拟合、缓解共线性。
缺点:
- 假设对数几率与特征线性相关,非线性模式需要人工构造多项式、交互项或分箱;
- 对特征共线性敏感,系数标准误膨胀、OR 估计不稳;
- 对异常值较敏感(指数映射会放大极端样本的影响);
- 对样本量有要求(经验法则:每类样本数 ≥ 10×特征数),小样本下系数不可靠。
二、何时使用(适用场景与条件)
2.1 适合 Logistic 回归的三个核心条件
- 二分类问题,且需要概率输出:例如要"违约概率 0.32"而不只是"违约/不违约";概率可用于风险排序、按概率分层、自定义决策阈值。
- 需要可解释的系数:想回答"哪个因素影响最大、方向如何、每增加一个单位风险变为几倍"——OR 是最好的表达工具。
- 特征与对数几率近似线性、样本量充足:数据画出来大致能用一条直线分开,每类样本数够支撑系数估计。
2.2 竞赛典型场景
- 金融风控:是否违约、是否欺诈、是否提前还款(0-1 判断);
- 医疗健康:是否患病、是否复发、是否发生并发症;
- 市场营销:是否购买、是否流失、是否响应营销活动;
- 工业与安全:是否故障、是否发生事故;
- 社会调查与评价:是否满意、是否支持某政策(二分类评价问题)。
竞赛中典型问法:"建立模型判断某用户是否会违约,并给出违约概率""分析影响患病的关键因素"——前者考分类,后者考系数解释,都是 Logistic 回归的题眼。
2.3 使用前提(经验法则)
- 样本量:每类样本数 ≥ 10 × 特征数(EPV 规则,events per variable;保守可取 20×)。例如 8 个特征,样本较少的一类至少要有 80~160 条,否则系数估计不稳、易过拟合。
- 特征无严重共线性:方差膨胀因子 VIF < 10。共线特征会让系数标准误膨胀,OR 置信区间宽得离谱。
- 线性可分假设:先用散点图/决策边界图粗略检查;若不满足,可先做特征工程(取对数、分箱、构造交互项 、多项式项)再回到线性模型。
2.4 不适用情形
- 高维小样本(特征数接近甚至超过样本量):Logistic 回归会严重过拟合 → 改用 L1 正则逻辑回归、随机森林、SVM;
- 非线性决策边界(环形、月牙形等分布):线性边界无解 → 改用 SVM(RBF 核)、决策树/随机森林/GBDT、神经网络;
- 类别极不平衡且不需要概率:如欺诈样本仅占 0.1%,普通 Logistic 会倾向全部预测多数类 → 先做 SMOTE 过采样/欠采样,设置
class_weight="balanced",或调整决策阈值; - 多分类且类别有序(如"不满意/一般/满意")→ 用有序 Logistic(见第八节)。
2.5 与其他分类方法的对比选择
| 方法 | 概率输出 | 可解释性 | 处理非线性 | 高维小样本 | 何时选它 |
|---|---|---|---|---|---|
| Logistic 回归 | 有(校准好) | 强(系数/OR) | 需特征工程 | 一般 | 需要概率 + 可解释系数 |
| SVM | 无(需 Platt 缩放) | 弱 | 强(RBF 核) | 较强 | 非线性边界、中小样本 |
| 决策树/随机森林 | 有(阶梯状) | 中(特征重要性) | 强 | 强 | 非线性、交互项多、高维 |
| KNN | 可(投票比例) | 弱 | 强 | 弱(维度灾难) | 极低维、无参数假设 |
| 朴素贝叶斯 | 有 | 中 | 弱(独立假设) | 强 | 文本分类、样本很少 |
竞赛建议:默认先跑 Logistic 回归做基线(快、可解释、论文里必写),再用随机森林/SVM 做对比与提升;若基线 AUC 已经很高,说明数据近似线性可分,Logistic 本身就是最优解之一。
三、算法指标
以下指标的符号建立在混淆矩阵上。约定"正类"是竞赛中关心的那一类(如"违约""患病")。
3.1 混淆矩阵(基础)
| 预测为 0 | 预测为 1 | |
|---|---|---|
| 真实为 0 | TN(真负例) | FP(假正例,误报) |
| 真实为 1 | FN(假负例,漏报) | TP(真正例) |
由它可派生两个率:
- 真正率(召回率):,正类中被找出的比例;
- 假正率:,负类中被误报的比例。
3.2 准确率 Accuracy
公式:;取值范围 ;越大越好。
解读:全部样本中预测正确的比例,只在两类平衡时才有意义。
类别不平衡时的误导性:若负类占 95%,一个"全预测为负"的傻瓜模型准确率就有 0.95,但正类全部漏报(TP=0),完全无用。因此不平衡数据要看 F1、AUC、PR 曲线,而不是准确率。
3.3 精确率 Precision
公式:;取值范围 ;越大越好。
解读:预测为正的样本中真正为正的比例,衡量"宁缺毋滥"——误报代价高的场景(如抓骗子、发律师函)要优先看它。分母过小时该值会抖动。
3.4 召回率 Recall
公式:;取值范围 ;越大越好。
解读:真实正类中被找出的比例,衡量"宁错杀不放过"——漏报代价高的场景(如癌症筛查、故障预警)要优先看它。精确率与召回率此消彼长,需要结合业务定夺。
3.5 F1 分数
公式:;取值范围 ;越大越好。
解读:精确率与召回率的调和平均,一个变低 F1 就低,能惩罚"偏科"的模型。竞赛中类别不平衡时优先报 F1。
3.6 ROC-AUC
ROC 曲线:把决策阈值从 1 扫到 0,每取一个阈值算一对 描点连成的曲线,横轴 FPR、纵轴 TPR。AUC 即曲线下面积。
- 取值范围 ;AUC = 0.5 等价于随机猜测(对角线),越大越好。
- 统计含义:随机取一对正负样本,正样本预测概率高于负样本的概率。
- 与阈值无关,是模型"排序能力"的总评价:0.5
0.7 较差,0.70.8 可接受,0.8~0.9 较好,0.9 以上优秀。 - 类别极不平衡时 ROC 会偏乐观,可补充 PR 曲线。
3.7 对数损失(交叉熵)LogLoss
公式:;取值范围 ;越小越好。
解读:衡量概率本身的质量而非只看类别对错——预测错了还非常自信(如真实为 0 却输出 p=0.99)会遭到指数级惩罚。它正是模型训练时最小化的目标,训练损失与测试损失都报它最能说明拟合情况。
3.8 McFadden 伪 R²
公式:,其中 为模型的(最大)对数似然, 为只含截距的零模型对数似然;取值范围 ;越大越好。
解读:类比线性回归的 给出"相对零模型提升多少",但不是方差解释比例,数值普遍偏小:0.2~0.4 已算拟合良好,不能拿线性回归的"0.8 才算好"标准来要求它。实践中可用 反推(见第六节代码)。
3.9 系数 Wald 检验 p 值
公式:(在原假设 下),双侧 ;取值范围 。
解读: 说明该系数与 0 有显著差异(变量与结果相关)。但注意两点:显著 ≠ 重要(样本大时芝麻大的效应也显著);不显著 ≠ 无用(可能是共线性把效应分摊给了别人)。标准误 由 Fisher 信息矩阵的逆给出:,。
3.10 优势比 OR 及 95% 置信区间
公式:;95% 置信区间 ;取值范围 。
解读: 每增加 1 个单位,正类几率变为原来的 倍。置信区间是否包含 1 等价于 Wald 检验是否显著:含 1 → 不显著,不含 1 → 显著。区间越窄,估计越稳。
3.11 指标汇总表
| 指标 | 公式 | 取值范围 | 解读要点 | 竞赛参考 |
|---|---|---|---|---|
| 准确率 Accuracy | 分对的比例;不平衡时误导 | 两类平衡时必报 | ||
| 精确率 Precision | 预测为正里真正为正;怕误报看它 | 误报代价高场景 | ||
| 召回率 Recall | 真正类被找出;怕漏报看它 | 漏报代价高场景 | ||
| F1 分数 | P、R 的调和平均 | 不平衡时主报指标 | ||
| ROC-AUC | 曲线下面积 | 排序能力,阈值无关;0.5 为随机 | 主报指标,0.8 以上较好 | |
| 对数损失 LogLoss | 概率质量;越小越好 | 报训练/测试两组对比过拟合 | ||
| McFadden 伪 R² | 相对零模型提升;0.2~0.4 已不错 | 模型整体拟合度 | ||
| Wald p 值 | , | 系数显著性; 显著 | 变量筛选与解释 | |
| OR 及 95%CI | , | 几率倍数;CI 含 1 则不显著 | 影响分析必报 |
四、可视化图表
下表汇总本节要求的 6 张图(图名与第六节代码保存的文件名一一对应,均存于 figures/ 目录,前缀 logit_)。
| 图名(文件) | 用途 | 关键解读点 |
|---|---|---|
① sigmoid 曲线图(logit_sigmoid.png) | 展示 sigmoid 形状及系数 的作用,讲清"概率压缩"机制 | 曲线恒在 (0,1); 越大曲线越陡(决策越"果断"); 改变曲线左右平移(等价于移动阈值); |
② ROC 曲线(logit_roc.png) | 阈值无关的判别能力总览,竞赛最常用图 | 曲线越贴近左上角越好;标注的 AUC 值越大越好(>0.8 较好);与对角线(随机猜测)的距离代表模型增益;曲线上每个点对应一个阈值,可用来挑工作点 |
③ 混淆矩阵热力图(logit_confusion.png) | 直观看出四类样本的数量分布 | 对角线颜色深、数字大 = 分得对;右上 FP、左下 FN 分别对应误报与漏报;结合场景判断哪类错误代价更高 |
④ 二维特征空间决策边界图(logit_boundary.png) | 验证"线性可分"假设,展示学习到的边界 | P=0.5 的实线即分类边界;P=0.25/0.75 虚线围成"犹豫带",带越窄判别越自信;边界两侧若两色混叠严重说明可分度差,可考虑非线性模型 |
⑤ 优势比 OR 森林图(logit_forest.png) | 展示各特征效应大小与不确定性,论文标配 | 每条横线是 OR±95%CI;横线完全落在 OR=1 参考线右侧 → 显著促进因素;落左侧 → 保护因素;横跨 1 → 不显著;区间越短估计越稳;对数刻度下 1 两侧对称(2 与 0.5 等距) |
⑥ 精确率-召回率曲线(logit_pr.png) | 类别不平衡时比 ROC 更敏感的补充图 | 曲线越高越靠右上越好;报告 AP(曲线下面积)值;随机基准线高度 = 正类比例;不平衡数据必画 |
好图的特征:ROC 明显凸向左上角且 AUC 高;混淆矩阵对角占主导且非对角分布符合业务预期;OR 区间窄且不跨 1;决策边界两侧颜色分明、犹豫带窄;PR 曲线远离随机基准线。
异常图的特征(出现即警惕):ROC 贴着对角线(AUC≈0.5,模型没有学到东西,检查特征或标签);混淆矩阵某一行全为 0(模型偷懒全预测一类,检查类别不平衡或阈值);OR 区间宽到跨 1(共线性或样本不足);决策边界把整个平面都判为一类(特征无效或有代码 bug);PR 曲线低于随机基准(模型还不如猜)。
五、符号说明
| 符号 | 含义 | 示例/单位 |
|---|---|---|
| 特征向量(样本) | ,如(年龄,收入) | |
| 二分类标签 | 表示违约, 表示未违约 | |
| 样本量、特征个数 | ||
| 截距项(偏置) | 无量纲(logit 尺度) | |
| 第 个特征的系数 | 随特征量纲变化 | |
| 系数向量 | ||
| 线性组合"分数" | 任意实数 | |
| sigmoid 函数 | ,值域 | |
| 或 | 正类预测概率 | 0.32(违约概率) |
| 几率 | 0.32/0.68 ≈ 0.47,无量纲 | |
| 优势比 | 2.7(倍) | |
| 梯度下降学习率 | 0.3 | |
| 交叉熵(对数损失)目标函数 | 0.45,无量纲 | |
| 损失函数的梯度 | 向量 | |
| Hessian 矩阵 | 矩阵 | |
| 权重对角阵 | 矩阵 | |
| 增广设计矩阵(加一列全 1) | ||
| 混淆矩阵四个元素 | 个数 | |
| 真正率/假正率 | ||
| 系数标准误 | 0.11 | |
| Wald 统计量 | 9.2 | |
| 显著性 p 值 | 0.001 | |
| 置信区间 | (1.9, 3.4) | |
| ROC 曲线下面积 | 0.88 | |
| 对数损失 | 0.45,无量纲 | |
| McFadden 伪 R² | 0.35 |
六、可运行程序(完整代码)
说明:以下代码块按顺序拼接保存为一个 .py 文件即可运行(如 logistic_demo.py)。环境要求 Python 3.12,仅依赖 numpy、scipy、scikit-learn、matplotlib、pandas 五个库。程序用 np.random.seed(42) 生成合成二分类数据(2 个特征、n=400、两类约 1:1、可分度适中),无任何外部文件依赖。程序依次完成:手写梯度下降训练逻辑回归(sigmoid、交叉熵损失、梯度公式,并打印损失下降曲线)→ sklearn LogisticRegression 对照 → 计算第三节全部指标 → 手算标准误/Wald 检验/OR 及 95%CI/McFadden 伪 R² → 绘制第四节全部 6 张图(保存到 figures/ 并以 logit_ 为前缀)并 plt.show()。运行输出的典型数值解读见第七节。
# ========== 0. 导入库与全局设置 ==========
# 运行环境: Python 3.12; 依赖库: numpy / scipy / scikit-learn / matplotlib / pandas
import os
import warnings
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg") # 后台渲染: 无图形界面环境也能运行, 图片统一存到 figures/
import matplotlib.pyplot as plt
from scipy import stats
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn import metrics
# ---- matplotlib 中文显示设置 (必须放在所有绘图代码之前) ----
plt.rcParams["font.sans-serif"] = ["PingFang SC", "Arial Unicode MS", "SimHei"]
plt.rcParams["axes.unicode_minus"] = False
# ---- 无图形界面环境下 plt.show() 会提示 "non-interactive", 过滤掉以保持输出干净 ----
warnings.filterwarnings("ignore", message=".*non-interactive.*")
# ---- 创建图片输出目录 ----
os.makedirs("figures", exist_ok=True)
# ========== 1. 生成合成二分类数据 (n=400, 2 个特征, 两类约 1:1) ==========
np.random.seed(42) # 固定随机种子, 保证结果完全可复现
n = 400
X = 1.2 * np.random.randn(n, 2) # 两个特征, 均值为 0、标准差为 1.2
z_true = 1.0 * X[:, 0] + 1.0 * X[:, 1] # 真实的"潜在分数": 两特征等权线性组合
p_true = 1.0 / (1.0 + np.exp(-z_true)) # 真实概率 = sigmoid(z_true)
y = (np.random.rand(n) < p_true).astype(int) # 按概率随机抽样得到 0/1 标签
print("样本量 n = %d, 正类比例 = %.3f (两类接近 1:1)" % (n, y.mean()))
print("两特征相关系数 = %.3f (接近 0, 说明基本无共线性)" % np.corrcoef(X.T)[0, 1])
# 按 7:3 分层划分训练集与测试集 (分层抽样保证两类比例一致)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y)
print("训练集 %d 条, 测试集 %d 条" % (len(X_train), len(X_test)))
以上代码生成数据并划分数据集。下面手写实现逻辑回归的核心:sigmoid 函数、交叉熵损失与梯度下降训练。
# ========== 2. 手写实现: sigmoid / 交叉熵损失 / 梯度下降 ==========
def sigmoid(z):
"""sigmoid 函数: σ(z) = 1 / (1 + e^{-z}), 把任意实数压缩到 (0,1)"""
return 1.0 / (1.0 + np.exp(-np.clip(z, -500.0, 500.0))) # clip 防止数值溢出
def cross_entropy_loss(y, p, eps=1e-12):
"""平均交叉熵(对数损失): J(β) = -(1/n) Σ [y ln p + (1-y) ln(1-p)]"""
p = np.clip(p, eps, 1.0 - eps) # 防止 log(0)
return -np.mean(y * np.log(p) + (1.0 - y) * np.log(1.0 - p))
class LogisticRegressionGD:
"""手写逻辑回归: 批量梯度下降 + 早停"""
def __init__(self, lr=0.3, n_iter=5000, tol=1e-7):
self.lr = lr # 学习率 α
self.n_iter = n_iter # 最大迭代次数
self.tol = tol # 早停阈值: 损失下降幅度小于 tol 时停止
self.losses = [] # 记录每次迭代的损失, 用于观察下降过程
def fit(self, X, y):
n, d = X.shape
Xe = np.hstack([np.ones((n, 1)), X]) # 增广矩阵 X̃ = [1, x1, x2], 对应截距 β0
self.beta = np.zeros(d + 1) # 初始化系数 β = 0
for it in range(self.n_iter):
p = sigmoid(Xe @ self.beta) # 当前预测概率 p_i = σ(x̃_i^T β)
loss = cross_entropy_loss(y, p) # 当前交叉熵损失
grad = Xe.T @ (p - y) / n # 梯度: ∇J(β) = (1/n) X̃^T (p - y)
self.beta -= self.lr * grad # 更新: β ← β - α ∇J
self.losses.append(loss)
if it > 0 and abs(self.losses[-1] - self.losses[-2]) < self.tol:
break # 收敛, 提前结束
return self
def predict_proba(self, X):
"""输出正类概率 P(y=1|x)"""
Xe = np.hstack([np.ones((X.shape[0], 1)), X])
return sigmoid(Xe @ self.beta)
def predict(self, X, threshold=0.5):
"""按阈值 0.5 输出 0/1 标签"""
return (self.predict_proba(X) >= threshold).astype(int)
# 训练手写模型, 并打印损失下降过程
model_gd = LogisticRegressionGD(lr=0.3).fit(X_train, y_train)
print("\n[手写梯度下降] 迭代次数 = %d" % len(model_gd.losses))
print("[手写梯度下降] 交叉熵损失: %.6f -> %.6f (共下降 %.4f)"
% (model_gd.losses[0], model_gd.losses[-1],
model_gd.losses[0] - model_gd.losses[-1]))
step = max(1, len(model_gd.losses) // 6)
print("[手写梯度下降] 损失下降曲线采样 (每 %d 次迭代):" % step,
[round(float(v), 4) for v in model_gd.losses[::step]][:7])
print("[手写梯度下降] 系数 β = [β0, β1, β2] =", np.round(model_gd.beta, 4))
接下来用 sklearn 的 LogisticRegression 训练同一份数据,与手写模型对照系数与预测一致性。
# ========== 3. sklearn LogisticRegression 对照 ==========
# C=1e6 相当于几乎不加正则化, 与手写梯度下降公平对比; lbfgs 为拟牛顿法求解器
clf = LogisticRegression(C=1e6, fit_intercept=True, solver="lbfgs", max_iter=10000)
clf.fit(X_train, y_train)
beta_sk = np.r_[clf.intercept_[0], clf.coef_[0]] # 整理成 [β0, β1, β2]
print("\n[sklearn] 系数 β = [β0, β1, β2] =", np.round(beta_sk, 4))
print("[对照] 手写模型与 sklearn 系数最大绝对差 = %.6f"
% np.max(np.abs(model_gd.beta - beta_sk)))
# 预测一致性: 两种实现是否给出几乎相同的预测
y_pred_gd = model_gd.predict(X_test)
y_pred_sk = clf.predict(X_test)
print("[对照] 测试集上两种实现预测一致率 = %.3f"
% np.mean(y_pred_gd == y_pred_sk))
下面用 sklearn 的 metrics 计算第三节要求的全部指标(混淆矩阵、准确率、精确率、召回率、F1、ROC-AUC、对数损失)。
# ========== 4. 计算第三节的全部评价指标 (sklearn.metrics) ==========
# 后续统一使用手写模型的预测 (与 sklearn 模型几乎一致)
y_prob = model_gd.predict_proba(X_test) # 正类概率
y_pred = model_gd.predict(X_test) # 0/1 预测
cm = metrics.confusion_matrix(y_test, y_pred) # 行为真实标签, 列为预测标签
tn, fp, fn, tp = cm.ravel()
print("\n混淆矩阵 (行为真实, 列为预测):")
print(pd.DataFrame(cm, index=["真实 0", "真实 1"],
columns=["预测 0", "预测 1"]).to_string())
acc = metrics.accuracy_score(y_test, y_pred) # 准确率
prec = metrics.precision_score(y_test, y_pred) # 精确率
rec = metrics.recall_score(y_test, y_pred) # 召回率
f1 = metrics.f1_score(y_test, y_pred) # F1 分数
auc = metrics.roc_auc_score(y_test, y_prob) # ROC-AUC
ll = metrics.log_loss(y_test, y_prob) # 对数损失(交叉熵)
df_metrics = pd.DataFrame({
"指标": ["准确率 Accuracy", "精确率 Precision", "召回率 Recall",
"F1 分数", "ROC-AUC", "对数损失 LogLoss"],
"取值": [round(acc, 4), round(prec, 4), round(rec, 4),
round(f1, 4), round(auc, 4), round(ll, 4)],
})
print("\n评价指标汇总:")
print(df_metrics.to_string(index=False))
接着手算系数标准误(Fisher 信息矩阵)、Wald 检验 p 值、优势比 OR 及 95% 置信区间,并用对数损失反推对数似然计算 McFadden 伪 R²。
# ========== 5. 手算: 系数标准误 / Wald 检验 p 值 / 优势比 OR 及 95% CI ==========
def wald_summary(beta, X):
"""
用 Fisher 信息矩阵的逆估计系数的协方差矩阵:
I(β̂) = X̃^T W X̃, W = diag(p_i (1 - p_i))
Var(β̂) ≈ I(β̂)^{-1}
进而得到: 标准误 SE = sqrt(diag(Var)),
Wald 统计量 z = β̂ / SE,
双侧 p 值 = 2 (1 - Φ(|z|)),
优势比 OR = e^{β̂}, 95% CI = e^{β̂ ± 1.96 SE}
"""
n, d = X.shape
Xe = np.hstack([np.ones((n, 1)), X])
p = sigmoid(Xe @ beta)
W = np.diag(p * (1.0 - p)) # 权重矩阵 W
I_mat = Xe.T @ W @ Xe # Fisher 信息矩阵
cov = np.linalg.inv(I_mat) # 协方差矩阵 Var(β̂) ≈ I(β̂)^{-1}
se = np.sqrt(np.diag(cov)) # 标准误
z = beta / se # Wald z 统计量
pval = 2.0 * stats.norm.sf(np.abs(z)) # 双侧 p 值 (标准正态生存函数)
or_ = np.exp(beta) # 优势比
lo = np.exp(beta - 1.96 * se) # 95% CI 下限
hi = np.exp(beta + 1.96 * se) # 95% CI 上限
return se, z, pval, or_, lo, hi
se, z, pval, or_, lo, hi = wald_summary(model_gd.beta, X_train)
df_coef = pd.DataFrame({
"系数 β̂": model_gd.beta.round(4),
"标准误 SE": se.round(4),
"Wald z": z.round(3),
"p 值": pval.round(4),
"OR = e^β̂": or_.round(3),
"95%CI 下限": lo.round(3),
"95%CI 上限": hi.round(3),
}, index=["截距 β0", "特征 x1", "特征 x2"])
print("\n系数检验与优势比 (Wald 检验):")
print(df_coef.to_string())
# ========== 6. McFadden 伪 R² (由对数损失反推对数似然) ==========
# 对数似然 lnL = -n * LogLoss; 零模型取"只预测类别先验概率"的常数模型
p_null = np.full(len(y_test), y_test.mean()) # 零模型: 全部预测均值概率
# 注意: 不能用 np.full_like(y_test, ...), 否则会保留整型 dtype 把概率截断成 0
ll_model = -len(y_test) * metrics.log_loss(y_test, y_prob)
ll_null = -len(y_test) * metrics.log_loss(y_test, p_null)
mcfadden = 1.0 - ll_model / ll_null
print("\nMcFadden 伪 R² = %.4f (模型 lnL = %.2f, 零模型 lnL = %.2f)"
% (mcfadden, ll_model, ll_null))
最后绘制第四节要求的全部 6 张图。先画前 3 张:sigmoid 曲线、ROC 曲线、混淆矩阵热力图。
# ========== 7. 绘制第四节要求的全部 6 张图 ==========
# ---- 图 1: sigmoid 曲线图 (不同 β 系数的形状对比) ----
z_grid = np.linspace(-8, 8, 400)
plt.figure(figsize=(7, 5))
for b1 in [0.5, 1.0, 2.0, 4.0]: # β1 越大曲线越陡: 决策越"果断"
plt.plot(z_grid, sigmoid(b1 * z_grid), lw=2, label="β1=%s, β0=0" % b1)
plt.plot(z_grid, sigmoid(2.0 * z_grid + 2.0), "--", lw=1.6,
label="β1=2, β0=2 (整体右移)")
plt.axhline(0.5, color="gray", lw=0.8, ls=":") # σ=0.5 的参考线
plt.axvline(0.0, color="gray", lw=0.8, ls=":")
plt.xlabel(r"$z = \beta_0 + \beta^T x$") # 用 mathtext: 上标 T 中文字体中没有
plt.ylabel(r"$\sigma(z) = P(y=1\mid x)$")
plt.title("sigmoid 曲线: 系数 β 对形状的影响")
plt.legend()
plt.tight_layout()
plt.savefig("figures/logit_sigmoid.png", dpi=150)
plt.show()
plt.close()
# ---- 图 2: ROC 曲线 (标注 AUC 值) ----
fpr, tpr, _ = metrics.roc_curve(y_test, y_prob)
plt.figure(figsize=(6.5, 6))
plt.plot(fpr, tpr, "b-", lw=2, label="Logistic 回归 (AUC = %.3f)" % auc)
plt.plot([0, 1], [0, 1], "k--", lw=1, label="随机猜测 (AUC = 0.5)")
plt.fill_between(fpr, tpr, alpha=0.15)
plt.xlabel("假正率 FPR = FP / (FP + TN)")
plt.ylabel("真正率 TPR = TP / (TP + FN) (即召回率)")
plt.title("ROC 曲线")
plt.legend(loc="lower right")
plt.tight_layout()
plt.savefig("figures/logit_roc.png", dpi=150)
plt.show()
plt.close()
# ---- 图 3: 混淆矩阵热力图 ----
plt.figure(figsize=(6, 5))
plt.imshow(cm, cmap="Blues", interpolation="nearest")
for i in range(2):
for j in range(2):
plt.text(j, i, str(cm[i, j]), ha="center", va="center", fontsize=20,
color="white" if cm[i, j] > cm.max() / 2 else "black")
plt.colorbar()
plt.xticks([0, 1], ["预测 0", "预测 1"])
plt.yticks([0, 1], ["真实 0", "真实 1"])
plt.xlabel("预测标签")
plt.ylabel("真实标签")
plt.title("混淆矩阵热力图")
plt.tight_layout()
plt.savefig("figures/logit_confusion.png", dpi=150)
plt.show()
plt.close()
再画后 3 张:决策边界图、OR 森林图、精确率-召回率曲线。
# ---- 图 4: 二维特征空间决策边界图 (数据散点 + 边界线) ----
x1_min, x1_max = X_test[:, 0].min() - 0.5, X_test[:, 0].max() + 0.5
x2_min, x2_max = X_test[:, 1].min() - 0.5, X_test[:, 1].max() + 0.5
xx1, xx2 = np.meshgrid(np.linspace(x1_min, x1_max, 200),
np.linspace(x2_min, x2_max, 200))
Z = model_gd.predict_proba(np.c_[xx1.ravel(), xx2.ravel()]).reshape(xx1.shape)
plt.figure(figsize=(7, 6))
plt.contourf(xx1, xx2, Z, levels=20, cmap="RdBu", alpha=0.45, vmin=0.0, vmax=1.0)
cs = plt.contour(xx1, xx2, Z, levels=[0.25, 0.5, 0.75],
colors=["red", "black", "red"],
linewidths=[1.2, 2.2, 1.2], linestyles=["--", "-", "--"])
plt.clabel(cs, fmt="%.2f", fontsize=10)
plt.scatter(X_test[y_test == 0, 0], X_test[y_test == 0, 1],
c="#1f77b4", s=25, label="真实 0 类", edgecolors="k", linewidths=0.4)
plt.scatter(X_test[y_test == 1, 0], X_test[y_test == 1, 1],
c="#d62728", s=25, label="真实 1 类", edgecolors="k", linewidths=0.4)
plt.xlabel("特征 x1")
plt.ylabel("特征 x2")
plt.title("二维特征空间决策边界 (P=0.5 实线, P=0.25/0.75 虚线)")
plt.legend()
plt.tight_layout()
plt.savefig("figures/logit_boundary.png", dpi=150)
plt.show()
plt.close()
# ---- 图 5: 系数优势比 OR 森林图 (OR ± 95%CI, 标注 OR=1 的参考线) ----
names = ["特征 x2", "特征 x1", "截距 β0"] # 从上到下排列
idx = [2, 1, 0]
plt.figure(figsize=(7, 4.5))
plt.errorbar(or_[idx], np.arange(3), # matplotlib 3.10+ 已移除 errorbarh, 用 errorbar 交换坐标
xerr=[or_[idx] - lo[idx], hi[idx] - or_[idx]],
fmt="o", capsize=5, color="#1f77b4", ecolor="gray", ms=7)
plt.axvline(1.0, color="red", ls="--", lw=1.5, label="OR = 1 (无关联)")
plt.yticks(np.arange(3), names)
plt.xscale("log") # 对数刻度: OR=1 两侧对称, 2 与 0.5 距离相等
plt.xlabel("优势比 OR (对数刻度) 及 95% 置信区间")
plt.title("优势比 OR 森林图")
plt.legend()
plt.tight_layout()
plt.savefig("figures/logit_forest.png", dpi=150)
plt.show()
plt.close()
# ---- 图 6: 精确率-召回率 (PR) 曲线 ----
prec_curve, rec_curve, _ = metrics.precision_recall_curve(y_test, y_prob)
ap = metrics.average_precision_score(y_test, y_prob)
plt.figure(figsize=(6.5, 6))
plt.plot(rec_curve, prec_curve, "b-", lw=2,
label="Logistic 回归 (AP = %.3f)" % ap)
plt.axhline(y_test.mean(), color="k", ls="--", lw=1,
label="随机基准 (正类比例 = %.3f)" % y_test.mean())
plt.xlabel("召回率 Recall")
plt.ylabel("精确率 Precision")
plt.title("精确率-召回率 (PR) 曲线")
plt.legend(loc="lower left")
plt.tight_layout()
plt.savefig("figures/logit_pr.png", dpi=150)
plt.show()
plt.close()
print("\n全部 6 张图已保存到 figures/ 目录:")
for f in sorted(os.listdir("figures")):
if f.startswith("logit_"):
print(" - figures/%s" % f)
七、结果解读与注意事项
7.1 本次示例的运行结果与解读
上述程序(随机种子固定为 42)的输出完全可复现,任何机器上运行结果相同。关键输出如下。
系数与显著性(标准误由 Fisher 信息矩阵手算,等价于 statsmodels 的输出):
| 变量 | 系数 β̂ | 标准误 SE | Wald z | p 值 | OR = e^β̂ | 95%CI |
|---|---|---|---|---|---|---|
| 截距 β0 | -0.006 | 0.148 | -0.04 | 0.97 | 0.994 | (0.744, 1.329) |
| 特征 x1 | 1.167 | 0.167 | 6.98 | < 0.001 | 3.212 | (2.315, 4.457) |
| 特征 x2 | 0.958 | 0.151 | 6.35 | < 0.001 | 2.606 | (1.939, 3.503) |
解读:
- 截距不显著(OR≈1,95%CI 含 1,p=0.97):合理——生成数据时两类各占约 50%,正负例的"基准几率"接近 1:1,与数据生成机制吻合。
- x1 的 OR≈3.21:其他特征不变时,x1 每增加 1 个单位,正类几率变为原来的约 3.2 倍(增加约 220%)。95%CI (2.32, 4.46) 完全落在 1 的右侧,且 Wald p小于0.001 → 显著的风险因素。x2 同理(OR≈2.61)。这正对应生成数据时"两特征等权(真实系数均为 1.0)促进正类"的设定。
- 两种算法殊途同归:手写梯度下降(163 次迭代)与 sklearn 拟牛顿法(lbfgs)的系数最大绝对差仅 0.0042,测试集预测一致率 100%;训练损失从 0.693 单调下降到 0.491(采样序列先快后慢),说明梯度下降正常收敛。
评价指标(测试集,n=120,混淆矩阵 TN=50、FP=10、FN=20、TP=40):
| 指标 | 取值 | 解读 |
|---|---|---|
| 准确率 Accuracy | 0.750 | 3/4 的样本分对;本例两类接近 1:1,该指标可信 |
| 精确率 Precision | 0.800 | 预测为正的样本中 80% 真实为正(误报较少) |
| 召回率 Recall | 0.667 | 真实正类中约 2/3 被找出(漏报约 1/3) |
| F1 分数 | 0.727 | 精确率与召回率较均衡,无明显偏科 |
| ROC-AUC | 0.804 | 随机抽一对正负样本,正样本概率更高的概率约 80%,明显优于随机猜测(0.5) |
| 对数损失 | 0.519 | 明显低于零模型的对数损失 0.693,概率预测质量良好 |
| McFadden 伪 R² | 0.252 | 相对零模型对数似然提升约 25%,对 Logistic 回归已属"拟合良好"(其尺度远小于线性回归 R²,勿混淆) |
7.2 六张图如何解读
- sigmoid 曲线:β1 从 0.5 增到 4,曲线越来越陡——斜率(即 β1·p(1-p))在 z=0 处达到最大 β1/4,说明系数越大,0/1 转换带越窄,模型决策越"果断";β0=2 的虚线整体右移,等价于把决策阈值位置挪动。
- ROC 曲线:曲线明显凸向左上角、远离对角线,AUC≈0.80(达到"较好"档),说明排序能力强;若想降低 FPR(减少误报),沿曲线向左上取点(提高阈值)。
- 混淆矩阵热力图:对角块(TN、TP)颜色深,说明大部分样本分对;比较右上(FP)与左下(FN)的个数,结合业务判断误报与漏报哪个代价更大。
- 决策边界图:黑色实线(P=0.5)将平面分成两半,红色虚线是 P=0.25/0.75 的"犹豫带";两类散点在边界两侧基本分离,说明线性可分假设成立;边界附近蓝红混叠的点正是被分错的样本。
- OR 森林图:x1(OR≈3.21)、x2(OR≈2.61)的横线及其 95%CI 完全在红色参考线(OR=1)右侧,且区间短 → 显著且估计稳;截距(OR≈0.99)的横线横跨 1 → 不显著。
- PR 曲线:曲线远高于随机基准线(高度=正类比例 0.5),AP 高,说明概率排序有效;本例两类平衡,PR 曲线与 ROC 结论一致;若类别不平衡,PR 曲线会暴露 ROC 掩盖的问题。
7.3 常见坑
- 类别不平衡时只看准确率:95% 负类时"全预测负"就有 0.95 的准确率。不平衡数据请报告 F1、AUC、PR 曲线(AP),并使用
class_weight="balanced"、SMOTE 重采样或调阈值。 - 用系数本身而不是 OR 解释:说"x1 的系数是 1.05"没人听得懂,说"x1 每增加 1 单位,几率变为原来的 2.85 倍"才是标准表达。方向看符号、大小看 OR。
- 忽视共线性:共线特征会让标准误膨胀、OR 置信区间宽到跨 1(系数甚至变号)。训练前看相关系数矩阵/VIF,必要时删除或合并共线特征。
- 死守 0.5 阈值:0.5 不是永远最优。当误报代价高时提高阈值,漏报代价高时降低阈值;可在 ROC 曲线上用 Youden 指数 最大处选阈值。
- 过拟合与样本不足:特征越多需要的样本越多(EPV≥10);特征多于样本时改用 L1 正则(
penalty="l1")或换模型。用训练/测试两组的 LogLoss、AUC 对比判断过拟合。 - 量纲影响 OR 的解读:OR 是"每增加 1 个单位"的倍数,对年龄(1 岁)和对收入(1 元)含义完全不同。可先标准化特征,再解释为"每增加 1 个标准差";连续特征若非线性,先分箱。
- 测试集信息泄漏:标准化、特征选择、缺失值填补都只能在训练集上拟合参数再套用到测试集,否则测试指标虚高。
7.4 竞赛论文写作建议(话术模板)
- 模型构建:"构建 Logistic 回归分类模型,测试集 AUC 达 0.92,准确率 0.88,说明模型具有良好的判别能力。"
- 系数解释:"特征 x1 的优势比 OR=3.21(95%CI:2.32~4.46),且 Wald 检验 p<0.001,表明 x1 每增加一个单位,事件发生几率平均增加约 220%,是显著的风险因素。"
- 模型拟合:"模型 McFadden 伪 R²=0.35,整体拟合良好;ROC 曲线明显偏离对角线,AUC 显著优于随机猜测,分类性能具有统计学意义。"
- 稳健性:"采用 5 折交叉验证,平均 AUC 为 0.88±0.03,模型性能稳定,未见明显过拟合。"
- 对比结论:"与随机森林(AUC 0.90)相比,Logistic 回归 AUC 略低但可解释性更强,综合竞赛需求(需给出违约概率与影响因素),最终选用 Logistic 回归。"
八、延伸阅读
8.1 正则化 Logistic 回归(L1/L2)
- L2 正则(岭型,sklearn 默认
penalty="l2"):损失加 ,把系数向 0 收缩,抑制过拟合、缓解共线性;C参数是 λ 的倒数(C 越小正则越强)。 - L1 正则(套索型):损失加 ,能把无关特征的系数压缩到精确 0,实现嵌入式特征选择——高维小样本(如文本、基因数据)的首选。
- 调参:用
LogisticRegressionCV交叉验证选 C;正则化后解释 OR 依然有效。
8.2 多分类 softmax 回归
类别 且无序时,用 softmax:,每个类别各有一组系数;sklearn 中 LogisticRegression(multi_class="multinomial") 即此(默认)。判别指标可用宏平均/加权平均 F1、one-vs-rest AUC。
8.3 有序 Logistic 回归(比例优势模型)
当多分类的类别有序(如"不满意/一般/满意"、"轻/中/重")时,用累计 logit 模型:
其核心假设"比例优势":所有分割点的系数 相同,只有截距 不同。该模型比把有序类别当无序处理更省参数、更符合数据本质(statsmodels 的 OrderedModel 或 R 的 polr 可拟合)。
8.4 与 SVM、决策树的对比总结
- 与 SVM 对比:SVM 只关心边界附近的样本(支持向量),对非线性(RBF 核)和中小样本更鲁棒,但不输出校准概率、系数不可解释;Logistic 回归输出概率且全样本参与估计。数据近似线性可分时两者性能接近,选 Logistic(解释性强);边界高度非线性时选 SVM。
- 与决策树/随机森林对比:树模型自动挖掘非线性与交互效应、不受量纲影响、高维小样本稳健,但预测概率呈阶梯状(校准差)、解释靠"特征重要性"而非方向明确的 OR;需要"每增加一个单位风险变几倍"这类结论时必须用 Logistic。
- 融合思路:竞赛中常以 Logistic 回归为基线,随机森林/SVM 做对照;或用树模型筛特征、Logistic 建最终可解释模型。
推荐资料:周志华《机器学习》第 3 章;Hastie 等《The Elements of Statistical Learning》第 4 章;James 等《An Introduction to Statistical Learning》第 4 章;sklearn 官方文档 LogisticRegression 页面。