公司动态

08 逻辑回归入门:从概率到二分类预测

📅 2026/8/8 9:22:57
08 逻辑回归入门:从概率到二分类预测
前言上一篇我们用线性回归预测了一个连续数值。现实中的机器学习任务却不总是在问“具体是多少”还经常在问“属于哪一类”一封邮件是不是垃圾邮件、一笔交易是否异常、一位客户会不会流失。这类问题叫作分类任务。本文从最常见的二分类开始使用逻辑回归Logistic Regression判断模拟客户是否流失。我们会从线性得分、Sigmoid 函数和分类阈值出发再用混淆矩阵、准确率、精确率、召回率与 F1 分数评价模型。重点不是背公式而是把“数据怎样变成概率概率又怎样变成类别”这条链路连起来。本文数据全部由代码在本地生成只用于教学演示不代表真实业务客户。示例里的概率是模型在这批模拟数据上的估计不应直接当成真实世界中已经校准的流失风险。分类任务与回归任务的区别回归回归任务预测的是连续数值。例如根据面积、位置和房龄预测房价根据广告投入预测销售额。相邻两个结果之间通常有可解释的数值距离预测 80 元和 100 元相差 20 元。分类分类任务预测的是离散类别。例如“正常/异常”“未流失/流失”。本文把未流失记为 0把流失记为 1但 0 和 1 只是类别编码不表示流失是未流失的“一倍”也不能像连续数值那样任意做加减。只有两个类别时叫二分类Binary Classification类别多于两个时叫多分类Multiclass Classification。本文集中讨论二分类后文会简单说明多分类的处理方式。为什么叫逻辑回归却用于分类“逻辑回归”这个名字容易让初学者误会。它确实先像线性回归一样把各个特征乘以相应权重再相加得到一个线性得分但它不会把这个任意大小的得分直接当作最终预测而是再通过一个函数把得分压缩到 0 和 1 之间。这个位于 0 到 1 之间的结果可以解释为模型对正类也就是类别 1 的估计概率。最后再设置一个阈值例如 0.5把概率转换成类别。因此逻辑回归虽然名字带有“回归”实际最常见的用途是分类。从线性得分到概率假设一个客户有 5 个特征逻辑回归先计算z w1 × x1 w2 × x2 … w5 × x5 b其中x是特征值w是模型学习到的系数b是截距z是线性得分。z可以是任意实数可能为 -3、0.8 或 12不能直接作为概率。接下来模型把z交给 Sigmoid 函数得到p。当p大于或等于分类阈值时预测为 1否则预测为 0。完整过程可以概括为输入特征 → 线性组合 z → Sigmoid → 类别 1 的估计概率 → 阈值判断 → 预测类别Sigmoid 函数是什么Sigmoid 是一个把任意实数映射到 0 和 1 之间的函数σ(z) 1 / (1 e⁻ᶻ)当z很小结果接近 0当z 0结果等于 0.5当z很大结果接近 1。曲线中间变化较快两端逐渐变平因此常被画成 S 形。需要特别注意模型输出 0.8并不等于“现实中一定有 80% 的流失概率”。它只是模型基于训练数据、特征与假设给出的估计。训练数据如果有偏差输出也可能有偏差若业务需要可信概率还应在独立数据上检查概率校准而不只是看分类对不对。分类阈值scikit-learn 的逻辑回归在二分类时通常使用 0.5 作为默认阈值。若类别 1 的估计概率不低于 0.5predict返回 1否则返回 0。阈值并不是不可改变的。降低阈值往往会让更多样本被预测为正类可能提高召回率同时也可能带来更多误报使精确率下降提高阈值通常会减少正类预测可能降低误报但也可能漏掉更多真正的正类。阈值应根据错误成本和任务目标选择。例如初筛场景更在意不要漏掉潜在正类可能偏向较低阈值人工复核成本很高时可能更在意每个告警是否可靠。正式项目应在验证集或交叉验证中选择阈值测试集只留作最终一次评估。本文展示测试集上不同阈值的结果只是帮助理解不把它当成调参依据。本文的模拟客户流失数据示例通过固定随机种子生成 800 个模拟客户每行是一个样本共有 5 个输入特征monthly_fee月费用contract_months合约持续月数support_calls联系客服次数usage_hours使用时长late_payments逾期付款次数。标签churn表示是否流失0 为未流失1 为流失。生成规则让月费用、客服联系次数和逾期次数增加时流失倾向通常上升合约月数和使用时长增加时流失倾向通常下降。不过代码还加入随机噪声并依据概率做伯努利抽样因此两类会有重叠模型无法靠一条简单规则完美分类。这更接近学习分类模型时应面对的情况。实际运行得到的数据形状为(800, 6)没有缺失值和重复行。完整数据中类别 0 有 441 个占 55.12%类别 1 有 359 个占 44.88%。这不是来自真实公司的调查结果而是当前代码和随机种子生成的可复现结果。特征矩阵 X 和标签 y特征矩阵X只包含 5 个输入列形状为(800, 5)标签y是单独的一列形状为(800,)。训练时模型从X学习如何预测y因此不能把churn本身放进特征矩阵否则就相当于提前把答案交给模型形成数据泄漏。代码在训练前检查列顺序、缺失值、无穷大和标签集合。标签必须同时包含 0 和 1。若训练数据只有一个类别逻辑回归无法学习两个类别之间的区别。拆分训练集和测试集我们只拆分一次X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.25, random_state42, stratifyy, )test_size0.25表示 25% 样本作为测试集random_state42固定拆分结果。stratifyy是分层抽样让训练集和测试集中的类别比例尽量接近完整数据。实际拆分后训练集有 600 个样本其中类别 0 为 331 个、类别 1 为 269 个测试集有 200 个样本其中类别 0 为 110 个、类别 1 为 90 个。后续标准化和模型参数都只能从训练集学习测试集不能参与训练。为什么逻辑回归通常需要标准化5 个特征的量纲不同月费用可能是几十到上百逾期次数通常只有几个。如果直接训练数值范围差异可能影响优化过程也会让正则化对不同特征产生不均衡的约束。StandardScaler会使用训练集的均值和标准差把特征转换到相近尺度。我们把它和逻辑回归放进Pipeline流水线调用fit时只在训练集上学习标准化参数调用predict时再自动用相同参数处理新数据。这样既减少重复代码也降低把测试集信息泄漏给训练过程的风险。标准化并不改变样本标签也不代表所有算法都必须标准化。例如树模型通常对特征尺度不敏感。但对逻辑回归、K 近邻和支持向量机等模型标准化经常是值得保留的步骤。建立分类基线模型在评价逻辑回归之前先建立一个简单基线。DummyClassifier(strategymost_frequent)不学习特征规律只把所有测试样本预测成训练集中数量最多的类别。本次运行的多数类基线在测试集上的结果为准确率 0.5500精确率 0.0000召回率 0.0000F1 分数 0.0000混淆矩阵为TN110、FP0、FN90、TP0。它的准确率看起来有 55%却一个流失客户都没有找到。这正说明分类任务不能只看准确率尤其当类别比例不均衡时“总猜多数类”也可能得到不低的准确率。训练逻辑回归模型使用标准化与逻辑回归组成流水线model Pipeline( steps[ (scaler, StandardScaler()), ( classifier, LogisticRegression(max_iter1000, random_state42), ), ] ) model.fit(X_train, y_train)max_iter1000为优化过程提供足够的最大迭代次数。代码不会粗暴屏蔽收敛警告而是捕获并检查ConvergenceWarning如果没有正常收敛就明确报错而不是把未完成的模型当成成功结果。predict 与 predict_probapredict(X_test)直接返回类别 0 或 1。predict_proba(X_test)返回每个类别的估计概率每行概率之和为 1。例如classes_为[0, 1]时两列分别对应类别 0 和类别 1。不要永远假定“第二列就是正类概率”。示例先读取classifier.classes_再查找类别 1 所在的列避免类别编码或顺序变化后取错概率。本次测试集前几个样本的类别 1 估计概率包括 0.2444、0.1575、0.5999 和 0.4591。默认阈值为 0.5 时0.5999 对应类别 10.4591 对应类别 0。概率接近阈值的样本通常也是模型更犹豫的样本。混淆矩阵混淆矩阵把真实类别与预测类别交叉统计。代码固定labels[0, 1]因此confusion_matrix(...).ravel()的顺序始终是 TN、FP、FN、TP。TNTNTrue Negative真正例表示真实为 0、预测也为 0。本文中就是未流失客户被正确判断为未流失。FPFPFalse Positive假正例表示真实为 0、却预测为 1也叫误报。它可能让运营人员把资源花在实际上不会流失的客户身上。FNFNFalse Negative假负例表示真实为 1、却预测为 0也叫漏报。若任务目标是尽早发现潜在流失客户FN 往往是需要重点关注的错误。TPTPTrue Positive真正例表示真实为 1、预测也为 1即成功找到的流失客户。本次逻辑回归在默认阈值下得到TN90、FP20、FN38、TP52。四个数相加为 200正好等于测试集样本数。准确率准确率Accuracy是所有预测中预测正确的比例Accuracy (TP TN) / (TP TN FP FN)本次逻辑回归准确率为 0.7100表示 200 个测试样本中有 142 个分类正确。但如果正负类别极不均衡准确率可能掩盖模型完全忽略少数类的问题所以不能单独使用。精确率精确率Precision回答“被模型预测为正类的样本中有多少确实是正类”Precision TP / (TP FP)本次结果为 0.7222。默认阈值下模型预测了 72 个正类其中 52 个确实为正类。精确率越高通常意味着误报相对越少。召回率召回率Recall回答“所有真实正类中模型找出了多少”Recall TP / (TP FN)本次结果为 0.5778即 90 个真实正类中找到 52 个还有 38 个漏报。更重视“不漏掉正类”的任务通常会重点观察召回率。F1 分数F1 分数F1 Score是精确率和召回率的调和平均F1 2 × Precision × Recall / (Precision Recall)本次 F1 为 0.6420。只有精确率和召回率都不太差时F1 才会较高。它适合需要在误报和漏报之间取得平衡的场景但仍不能替代对具体业务成本的分析。分类指标对比同一测试集、默认阈值 0.5 下的实际结果如下模型准确率精确率召回率F1TNFPFNTP多数类基线0.55000.00000.00000.00001100900逻辑回归0.71000.72220.57780.642090203852逻辑回归不仅提高了准确率也真正识别出一部分正类因此比多数类基线有意义。不过 0.5778 的召回率说明仍有不少漏报不能因为整体准确率超过基线就宣称模型已经足够好。调整分类阈值使用同一个已训练模型只改变把概率转换成类别的阈值实际得到阈值预测为正类的数量精确率召回率F10.351110.58560.72220.64680.50720.72220.57780.64200.65430.74420.35560.4812在这次运行中阈值从 0.50 降到 0.35正类预测从 72 个增加到 111 个召回率从 0.5778 升到 0.7222但精确率降到 0.5856。阈值提高到 0.65 后精确率略升到 0.7442召回率却降到 0.3556。这些变化符合阈值的一般直觉但具体数字只属于当前模拟数据和固定拆分。不能先查看测试集结果再挑一个看起来最好的阈值并把它当作无偏成绩正式流程应在验证集或交叉验证中完成阈值选择然后只在测试集评估一次。逻辑回归的系数逻辑回归为每个特征学习一个系数。系数为正通常表示在其他输入保持不变时该特征增大与类别 1 的更高模型倾向相关系数为负则相反。本例在标准化后的特征上得到截距-0.252296系数如下特征标准化系数monthly_fee0.444903contract_months-0.663760support_calls0.522662usage_hours-0.216267late_payments0.510185由于流水线先做标准化这里的“增加 1 个单位”指增加 1 个训练集标准差而不是月费用增加 1 元或合约增加 1 个月。系数大小还会受特征相关性、正则化、取值范围和预处理方式影响不应只按绝对值机械地给特征排重要性。更重要的是系数描述的是模型在当前数据中学到的关联不是因果结论。正系数不能证明“提高月费用一定导致客户流失”还可能存在未记录的混杂因素或数据采集偏差。对数几率与优势比概率p对应的几率Odds也译作优势为odds p / (1 - p)逻辑回归对几率取自然对数得到对数几率Log-odds再让它与特征的线性组合相等。将某个系数取指数exp(coef)可以得到该特征增加一个单位时对应的优势比Odds Ratio。本例的标准化特征优势比如下特征优势比monthly_fee1.560338contract_months0.514912support_calls1.686511usage_hours0.805520late_payments1.665599例如support_calls的优势比约为 1.6865含义是在模型其他输入不变的条件下标准化后的客服联系次数增加 1 个标准差模型估计的正类几率乘以约 1.6865。它不是“概率增加 68.65%”更不是因果效应。优势比是否稳定、是否有业务意义还需要更严格的数据设计和统计检查。预测一个新客户最后构造一个新样本月费用 88.0、合约 8 个月、联系客服 4 次、使用时长 22.0、逾期付款 2 次。流水线会自动使用训练集学到的标准化参数再交给逻辑回归预测。本次运行读取到classes_[0, 1]预测类别为 1类别 1 的模型估计概率为0.903283。这个结果只说明当前教学模型如何处理这一组模拟输入不能作为真实客户决策依据也不能解释为已经验证过的真实流失概率。多分类问题怎么办逻辑回归也能处理三个及以上类别。scikit-learn 会根据求解器和参数使用适合的多分类策略predict_proba会为每个类别返回一列概率所有列之和为 1。此时必须查看classes_确认每列对应哪个类别。多分类评价还要说明指标的平均方式例如 macro、micro 或 weighted。本文先把二分类中的概率、阈值和混淆矩阵掌握清楚再学习多分类会更自然。逻辑回归常见问题1. 出现收敛警告ConvergenceWarning表示在给定迭代次数内优化没有正常结束。可以检查是否需要标准化、提高max_iter、清理异常数据或调整求解器。不要简单屏蔽所有警告否则可能把训练问题藏起来。2. 训练数据只有一个类别如果y全为 0 或全为 1模型没有可学习的类别差异。应先检查数据筛选、拆分和标签生成流程不能靠修改模型参数绕过根本问题。3. 字符串特征无法直接训练城市、套餐名称等字符串需要编码为数值。无序类别通常使用独热编码并把编码器放进流水线避免训练集和测试集采用不同规则。4. 特征尺度差异很大不同量纲可能让优化和正则化表现不理想。对数值特征使用StandardScaler是常见做法但标准化参数只能从训练集学习。5. 类别不平衡如果正类非常少只看准确率容易误判。应同时查看混淆矩阵、精确率、召回率、F1必要时再讨论类别权重、重采样和适合任务的评价指标。本文主模型没有使用class_weightbalanced避免把额外策略混入入门主线。6. 数据几乎完全可分如果某个特征或规则几乎直接暴露标签系数可能非常大也可能暗示数据泄漏。应先检查特征是否在预测时真实可用而不是因为测试分数很高就立即接受结果。7. 概率未经校准分类正确不代表概率可靠。若业务真的依赖“概率是多少”应使用独立数据检查校准曲线或适当的校准方法。本文没有做概率校准因此只把输出称为模型估计概率。8. 数据泄漏先用全量数据做标准化、把事后才知道的信息放入特征或根据测试集表现反复选阈值都会让评估过于乐观。流水线能解决部分预处理泄漏但不能自动发现所有业务泄漏。9. 线性决策边界能力有限逻辑回归学习的是特征空间中的线性决策边界。真实关系高度弯曲或依赖复杂交互时它可能欠拟合。不过它训练快、解释路径清晰仍然是很有价值的分类基线。逻辑回归适合什么情况逻辑回归适合需要快速建立二分类基线、特征数量适中、希望输出概率并希望理解特征方向的任务。它对高维稀疏特征也常有良好表现并且训练与预测成本通常不高。如果数据关系明显非线性、类别边界非常复杂或者原始数据是图片、音频等非结构化内容单纯逻辑回归可能不够。但即使最终使用复杂模型逻辑回归仍能提供一个清晰的比较起点复杂模型至少应该在统一数据拆分和评价口径下超过这个基线。把完整流程串起来回看这次实践第一步不是急着创建模型而是明确任务的正类含义并检查标签确实为 0 和 1。正类的定义会直接影响精确率、召回率、FP 和 FN 的解释如果团队中有人把 1 理解成“未流失”同一组数字就会得到完全相反的业务含义。第二步是先固定测试集再把标准化与逻辑回归放进流水线。标准化器只在训练集上学习参数新客户和测试数据只能使用已经学到的变换。这样模型在测试时接触的是训练阶段真正没有看过的数据。第三步是建立多数类基线。基线虽然简单却能回答一个关键问题模型的表现是否真的超过“不看特征也能做到”的水平。若一个复杂模型连多数类基线都没有超过应该优先检查数据、评价口径和代码而不是继续增加复杂度。第四步是同时观察类别预测、正类概率和混淆矩阵。概率保留了模型判断的强弱阈值负责把这种连续输出变成离散决策混淆矩阵则把决策错误拆成误报和漏报。三者结合才能解释指标为什么变化。第五步是把阈值、参数解释和最终测试成绩分开。阈值属于需要在验证阶段确定的决策规则系数描述模型中的关联测试集负责给出一次相对客观的最终检查。把三者混在一起反复调整会让测试结果失去可信度。最后新客户必须使用与训练时相同的列名、列顺序和预处理流程。流水线能保存这条处理链但真实项目仍应额外检查输入类型、取值范围和缺失值。模型能返回结果不代表输入一定合理工程检查和模型检查同样重要。完整实践代码下面代码与本地文件08_logistic_regression.py一致使用 Python 3、NumPy、Pandas 和 scikit-learn。本次实际验证环境为 Python 3.11.4、NumPy 1.24.3、Pandas 1.5.3、scikit-learn 1.3.0。用模拟客户数据演示逻辑回归二分类。 from __future__ import annotations import sys import warnings from typing import Any try: import numpy as np import pandas as pd import sklearn from sklearn.base import ClassifierMixin from sklearn.dummy import DummyClassifier from sklearn.exceptions import ConvergenceWarning from sklearn.linear_model import LogisticRegression from sklearn.metrics import ( accuracy_score, 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 except ImportError as exc: print(f缺少运行依赖{exc}) print(请确认当前 Conda 环境已安装 NumPy、Pandas 和 scikit-learn。) raise SystemExit(1) from exc RANDOM_SEED 42 FEATURE_NAMES [ monthly_fee, contract_months, support_calls, usage_hours, late_payments, ] def check_environment() - None: 输出本次运行使用的解释器与核心依赖版本。 print( 运行环境 )print(fPython: {sys.version.split()[0]}) print(fNumPy: {np.__version__}) print(fPandas: {pd.__version__}) print(fScikit-learn: {sklearn.__version__}) print(fInterpreter: {sys.executable}) def sigmoid(values: np.ndarray) - np.ndarray: 数值稳定的 Sigmoid先限制极端输入再计算概率。 safe_values np.clip(values, -500.0, 500.0) return 1.0 / (1.0 np.exp(-safe_values)) def generate_dataset(n_samples: int 800) - pd.DataFrame: 构造带噪声的模拟客户数据标签由伯努利抽样得到。 if n_samples 600: raise ValueError(为保证示例有足够样本n_samples 不能少于 600。) rng np.random.default_rng(RANDOM_SEED) monthly_fee np.clip(rng.normal(75.0, 18.0, n_samples), 25.0, 130.0) contract_months rng.integers(1, 49, n_samples) support_calls np.clip(rng.poisson(2.2, n_samples), 0, 9) usage_hours np.clip(rng.normal(35.0, 12.0, n_samples), 2.0, 80.0) late_payments np.clip(rng.poisson(1.1, n_samples), 0, 6) # 分数只用于生成教学数据正号提高流失倾向负号降低流失倾向。 # 随机噪声和伯努利抽样使两类有重叠不会形成可被完美分开的数据。 score ( -0.25 0.035 * (monthly_fee - 75.0) - 0.050 * (contract_months - 24.0) 0.38 * (support_calls - 2.0) - 0.030 * (usage_hours - 35.0) 0.55 * (late_payments - 1.0) rng.normal(0.0, 0.65, n_samples) ) churn_probability sigmoid(score) churn rng.binomial(1, churn_probability, n_samples) return pd.DataFrame( { monthly_fee: monthly_fee.round(2), contract_months: contract_months, support_calls: support_calls, usage_hours: usage_hours.round(2),late_payments: late_payments, churn: churn, } ) def validate_dataset(data: pd.DataFrame) - None: 检查列、缺失值、有限数值和二分类标签。 expected_columns FEATURE_NAMES [churn] if data.columns.tolist() ! expected_columns: raise ValueError(f数据列不符合预期{data.columns.tolist()}) if data.isna().any().any(): raise ValueError(数据中存在缺失值。) if not np.isfinite(data[FEATURE_NAMES].to_numpy(dtypefloat)).all(): raise ValueError(特征中存在无穷大或非有限数值。) labels set(data[churn].unique().tolist()) if labels ! {0, 1}: raise ValueError(f标签必须同时包含 0 和 1当前为{sorted(labels)}) def split_dataset( data: pd.DataFrame, ) - tuple[pd.DataFrame, pd.DataFrame, pd.Series, pd.Series]: 按 75%/25% 分层拆分一次保持两部分的类别比例接近。 X data[FEATURE_NAMES] y data[churn] return train_test_split( X, y, test_size0.25, random_stateRANDOM_SEED, stratifyy, ) def build_models( X_train: pd.DataFrame, y_train: pd.Series ) - tuple[DummyClassifier, Pipeline]: 只用训练集拟合多数类基线和标准化后的逻辑回归。 baseline DummyClassifier(strategymost_frequent) baseline.fit(X_train, y_train) model Pipeline( steps[(scaler, StandardScaler()), ( classifier, LogisticRegression(max_iter1000, random_stateRANDOM_SEED), ), ] ) with warnings.catch_warnings(recordTrue) as caught_warnings: warnings.simplefilter(always) model.fit(X_train, y_train) convergence_messages [ str(item.message) for item in caught_warnings if issubclass(item.category, ConvergenceWarning) ] if convergence_messages: raise RuntimeError(模型未正常收敛 | .join(convergence_messages)) for item in caught_warnings: warnings.warn(item.message, item.category) return baseline, model def _positive_probabilities( model: ClassifierMixin, X: pd.DataFrame ) - tuple[np.ndarray, np.ndarray]: 根据 classes_ 定位类别 1 的概率列不假定列顺序。 classes np.asarray(model.classes_) positive_columns np.flatnonzero(classes 1) if positive_columns.size ! 1: raise ValueError(f无法唯一定位类别 1classes_{classes.tolist()}) probabilities model.predict_proba(X)[:, int(positive_columns[0])] return probabilities, classes def evaluate_classifier( name: str, model: ClassifierMixin, X_test: pd.DataFrame, y_test: pd.Series, ) - tuple[dict[str, Any], np.ndarray, np.ndarray]: 用同一测试集计算四项指标和固定标签顺序的混淆矩阵。 predictions model.predict(X_test) probabilities, _ _positive_probabilities(model, X_test)tn, fp, fn, tp confusion_matrix( y_test, predictions, labels[0, 1] ).ravel() metrics { model: name, accuracy: accuracy_score(y_test, predictions), precision: precision_score(y_test, predictions, zero_division0), recall: recall_score(y_test, predictions, zero_division0), f1: f1_score(y_test, predictions, zero_division0), TN: int(tn), FP: int(fp), FN: int(fn), TP: int(tp), } return metrics, np.asarray(predictions), probabilities def evaluate_threshold( y_true: pd.Series, positive_probabilities: np.ndarray, threshold: float ) - dict[str, Any]: 在指定阈值下计算正类数量、精确率、召回率和 F1。 predictions (positive_probabilities threshold).astype(int) return { threshold: threshold, positive_predictions: int(predictions.sum()), precision: precision_score(y_true, predictions, zero_division0), recall: recall_score(y_true, predictions, zero_division0), f1: f1_score(y_true, predictions, zero_division0), } def extract_model_details(model: Pipeline) - tuple[np.ndarray, pd.DataFrame]: 提取标准化特征对应的截距、系数与优势比。 classifier model.named_steps[classifier] intercept classifier.intercept_.copy() details pd.DataFrame( { feature: FEATURE_NAMES, coefficient: classifier.coef_[0], odds_ratio: np.exp(classifier.coef_[0]), } ) return intercept, detailsdef predict_new_customer(model: Pipeline) - tuple[pd.DataFrame, int, float, np.ndarray]: 预测一个仅用于演示的新客户样本。 customer pd.DataFrame( [ { monthly_fee: 88.0, contract_months: 8, support_calls: 4, usage_hours: 22.0, late_payments: 2, } ] ) predicted_class int(model.predict(customer)[0]) positive_probability, classes _positive_probabilities(model, customer) return customer, predicted_class, float(positive_probability[0]), classes def _class_distribution(values: pd.Series) - str: counts values.value_counts().sort_index() total len(values) return , .join( f类别 {int(label)}: {int(count)} ({count / total:.2%}) for label, count in counts.items() ) def main() - None: check_environment() data generate_dataset(n_samples800) validate_dataset(data) X_train, X_test, y_train, y_test split_dataset(data) print(\n 数据检查 ) print(f完整数据形状: {data.shape}) print(f特征矩阵 X 形状: {data[FEATURE_NAMES].shape}) print(f标签 y 形状: {data[churn].shape}) print(f缺失值总数: {int(data.isna().sum().sum())}) print(f重复行数: {int(data.duplicated().sum())}) print(f完整数据类别分布: {_class_distribution(data[churn])}) print(f训练集样本数: {len(X_train)}) print(f训练集类别分布: {_class_distribution(y_train)}) print(f测试集样本数: {len(X_test)}) print(f测试集类别分布: {_class_distribution(y_test)})baseline, model build_models(X_train, y_train) baseline_metrics, _, _ evaluate_classifier( 多数类基线, baseline, X_test, y_test ) model_metrics, model_predictions, model_probabilities evaluate_classifier( 逻辑回归, model, X_test, y_test ) metrics_table pd.DataFrame([baseline_metrics, model_metrics]) print(\n 测试集指标阈值 0.50 ) print(metrics_table.to_string(indexFalse, float_formatlambda x: f{x:.4f})) threshold_table pd.DataFrame( [ evaluate_threshold(y_test, model_probabilities, threshold) for threshold in (0.35, 0.50, 0.65) ] ) print(\n 不同分类阈值 ) print(threshold_table.to_string(indexFalse, float_formatlambda x: f{x:.4f})) classifier model.named_steps[classifier] intercept, coefficient_table extract_model_details(model) print(\n 模型参数特征已标准化 ) print(fclasses_: {classifier.classes_.tolist()}) print(fintercept: {intercept[0]:.6f}) print(coefficient_table.to_string(indexFalse, float_formatlambda x: f{x:.6f})) preview X_test.head(8).copy() preview[actual_churn] y_test.head(8).to_numpy() preview[predicted_churn] model_predictions[:8] preview[probability_class_1] model_probabilities[:8] print(\n 测试集预测预览 ) print(preview.to_string(indexFalse, float_formatlambda x: f{x:.4f})) customer, predicted_class, probability, classes predict_new_customer(model) print(\n 新客户预测仅为教学演示 ) print(customer.to_string(indexFalse)) print(fclasses_: {classes.tolist()}) print(f预测类别: {predicted_class}) print(f类别 1 的模型估计概率: {probability:.6f}) if __name__ __main__:try: main() except (ValueError, RuntimeError) as exc: print(f程序检查未通过{exc}, filesys.stderr) raise SystemExit(1) from exc except Exception as exc: # 保留未知异常的具体类型便于定位问题。 print(f程序发生未预期错误{type(exc).__name__}: {exc}, filesys.stderr) raise运行命令# 在项目根目录下使用当前 Conda 环境的 Python 运行 python 08_logistic_regression.py程序会依次输出环境、数据检查、类别分布、基线与逻辑回归指标、不同阈值结果、系数、优势比、测试集预测预览和新客户预测。固定随机种子后在相同依赖版本下可以复现本文数字若库版本或平台不同末尾小数可能出现轻微差异应以自己的实际输出为准。本文总结逻辑回归先对输入特征做线性组合再通过 Sigmoid 得到类别 1 的模型估计概率最后用分类阈值把概率转换成类别。predict给出类别predict_proba给出各类别概率读取概率列前应核对classes_。评价二分类模型不能只看准确率。混淆矩阵说明错误发生在哪里精确率关注误报召回率关注漏报F1 在两者之间做平衡。阈值会改变这些指标应该依据任务成本并在验证数据上选择而不是反复利用测试集。系数和优势比能帮助理解模型学到的方向但它们受到标准化、正则化、特征相关性与数据质量影响只表示当前模型中的关联不能直接解释为因果关系。概率输出同样需要独立校准验证不能仅凭一个小数就做高风险决策。下一篇预告下一篇将学习《K 近邻算法从距离理解分类》。逻辑回归通过线性得分建立分类边界K 近邻则从“附近样本通常更相似”出发做判断。我们会继续关注特征尺度、超参数选择和公平的模型评价。