1 逻辑回归

长得有点像线性回归,但逻辑回归(Logistic Regression) 是一个分类算法。通常解决的就是二分类(binary classification) 问题

英文出自wiki

Logistic regression is a supervised machine learning algorithm used for binary classification (e.g., yes/no, 0/1) to predict the probability of an outcome based on input data

很多人会疑问。为什么回归却干分类的事情,因为逻辑回归输出一个概率(probability),再根据概率做分类。

  • 线性回归:直接预测一个数值
  • 逻辑回归:先预测一个概率,再根据概率判断类别。
逻辑回归 = 线性组合 + sigmoid + 按阈值分类
# 这个sigmoid函数现在的理解就是可以把世界上所有的数字都可以搞到(0,1)之间,这不就是一个概率了吗?

使用场景大概就是下面的

  • 判断是不是垃圾邮件
  • 判断用户会不会流失
  • 判断学生会不会通过考试
  • 判断肿瘤是良性还是恶性

1-1 说人话概念

假设你要预测学生是否通过考试。

特征(features)有:
	•	学习时长
	•	出勤率
	•	作业完成率

标签(label)是:
	•	0 = 不通过
	•	1 = 通过

逻辑回归做的事不是直接说:
预测分数是 78
而是说:
	•	这个学生通过的概率是 0.83
然后再判断 你给了一个阈值是0.5
	•	0.83 > 0.5,所以预测他会通过

这就是逻辑回归的基本思路。

为什么线性回归不适合呢?

线性回归的答案可能是,99,-18这样。但分类任务真正想要的是,0 或 1,或至少是 0 到 1 之间的概率。

1-2 逻辑剖析

逻辑回归先做一件和线性回归很像的事

z=w1x1+w2x2++bz = w_1x_1 + w_2x_2 + \cdots + b

只是它不直接把这个 z 当最终输出。它会再经过一个函数,把它压到 0 到 1 之间。这个函数就是sigmoid 函数(也叫 logistic function)

σ(z)=11+ez\sigma(z) = \frac{1}{1 + e^{-z}}

上面的完全可以不用记忆,反正它的作用就是。把任意实数压缩到 0 到 1 之间。这样就可以把结果解释成概率。

2 完整代码

下面给你一个稍微完整一点的逻辑回归案例,数据用 Titanic(泰坦尼克号生存预测),比自己手写数据更接近真实场景。这个案例会包含

  • 读取数据
  • 选特征
  • 缺失值处理
  • 类别编码(One-Hot Encoding)
  • 数值标准化(Standardization)
  • 逻辑回归建模
  • 评估指标
  • 查看系数
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    classification_report,
    confusion_matrix,
    roc_auc_score,
    RocCurveDisplay
)

def main():
    # 1. 读取数据
    # survived: 目标变量,0=死亡,1=生还
    df = sns.load_dataset("titanic")

    print("===== 原始数据前5行 =====")
    print(df.head())
    print()

    # 2. 选择特征和目标
    # 这里故意选一些稍微真实一点的特征
    features = [
        "pclass",     # 船舱等级
        "sex",        # 性别
        "age",        # 年龄
        "sibsp",      # 同行兄弟姐妹/配偶数量
        "parch",      # 同行父母/子女数量
        "fare",       # 票价
        "embarked",   # 登船港口
        "alone"       # 是否独自出行
    ]
    target = "survived"

    X = df[features]
    y = df[target]

    print("===== 特征信息 =====")
    print(X.info())
    print()

    # 3. 区分数值特征和类别特征
    numeric_features = ["age", "fare", "sibsp", "parch", "pclass"]
    categorical_features = ["sex", "embarked", "alone"]

    # 4. 数值特征预处理
    # 缺失值用中位数填充 + 标准化
    numeric_transformer = Pipeline(steps=[
        ("imputer", SimpleImputer(strategy="median")),
        ("scaler", StandardScaler())
    ])

    # 5. 类别特征预处理
    # 缺失值用众数填充 + One-Hot编码
    categorical_transformer = Pipeline(steps=[
        ("imputer", SimpleImputer(strategy="most_frequent")),
        ("onehot", OneHotEncoder(handle_unknown="ignore"))
    ])

    # 6. 合并预处理流程
    preprocessor = ColumnTransformer(transformers=[
        ("num", numeric_transformer, numeric_features),
        ("cat", categorical_transformer, categorical_features)
    ])

    # 7. 建立完整流水线
    # 先预处理,再逻辑回归
    model = Pipeline(steps=[
        ("preprocessor", preprocessor),
        ("classifier", LogisticRegression(max_iter=1000))
    ])

    # 8. 划分训练集和测试集
    X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        test_size=0.2,
        random_state=42, # 每次切出来的结果一样,方便复现
        stratify=y #
    )

    print("训练集大小:", X_train.shape)
    print("测试集大小:", X_test.shape)
    print()

    # 9. 训练模型
    model.fit(X_train, y_train)

    # 10. 预测
    y_pred = model.predict(X_test)
    y_prob = model.predict_proba(X_test)[:, 1]  # 取生还=1的概率

    # 11. 评估
    print("===== 模型评估 =====")
    print("Accuracy:", round(accuracy_score(y_test, y_pred), 4))
    print("ROC-AUC :", round(roc_auc_score(y_test, y_prob), 4))
    print()

    print("Confusion Matrix:")
    print(confusion_matrix(y_test, y_pred))
    print()

    print("Classification Report:")
    print(classification_report(y_test, y_pred, digits=4))

    # 12. 画 ROC 曲线
    RocCurveDisplay.from_predictions(y_test, y_prob)
    plt.title("Logistic Regression ROC Curve")
    plt.show()

    # 13. 查看逻辑回归系数
    # 先拿到预处理后的特征名
    feature_names = model.named_steps["preprocessor"].get_feature_names_out()
    coefficients = model.named_steps["classifier"].coef_[0]

    coef_df = pd.DataFrame({
        "feature": feature_names,
        "coefficient": coefficients
    }).sort_values(by="coefficient", ascending=False)

    print("===== 系数(从大到小) =====")
    print(coef_df)
    print()

    # 14. 看几个新样本的预测概率
    sample_passengers = pd.DataFrame([
        {
            "pclass": 1,
            "sex": "female",
            "age": 28,
            "sibsp": 0,
            "parch": 0,
            "fare": 80,
            "embarked": "S",
            "alone": True
        },
        {
            "pclass": 3,
            "sex": "male",
            "age": 35,
            "sibsp": 0,
            "parch": 0,
            "fare": 8,
            "embarked": "S",
            "alone": True
        }
    ])

    sample_prob = model.predict_proba(sample_passengers)[:, 1]
    sample_pred = model.predict(sample_passengers)

    print("===== 新样本预测 =====")
    for i in range(len(sample_passengers)):
        print(f"样本{i+1}:")
        print(sample_passengers.iloc[i].to_dict())
        print("预测类别:", int(sample_pred[i]))
        print("生还概率:", round(float(sample_prob[i]), 4))
        print()

if __name__ == "__main__":
    main()

2-1 预处理阶段

建模使用的特征和标签是筛选出来的,因为 Titanic 原始数据集里本来就有这些列。它们只是原始数据的一部分,并不代表你一定要拿来建模。

X = df[features] # 这里只是挑选需要的特征
y = df[target]

下面开始慢慢讲解

# 3. 区分数值特征和类别特征
numeric_features = ["age", "fare", "sibsp", "parch", "pclass"]
categorical_features = ["sex", "embarked", "alone"]

# 4. 数值特征预处理
# 缺失值用中位数填充 + 标准化
numeric_transformer = Pipeline(steps=[
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler())
])

# 5. 类别特征预处理
# 缺失值用众数填充 + One-Hot编码
categorical_transformer = Pipeline(steps=[
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore"))
])

把不同类型的特征,按不同方式预处理。

对于数值特征(numeric features) 这些列本身就是数字,可以直接参与计算。对数值特征,要先补缺失值,再做标准化,上面的代码用的是中位数进行标准化的

对于类别特征(categorical features) 这些列虽然有些看起来不是纯文本,但本质上表示类别。因为这些不能直接参与回归计算,所以需要进一步的预处理。sex:male / female,embarked:S / C / Q,alone:True / False。使用的是最常出现的中数进行标准化的。说人话就是这些根本就不是数字,当然你要处理一下咯

# 1 补缺失值
("imputer", SimpleImputer(strategy="median"))
  • 如果某一列有空值(NaN)
  • 就用这一列的中位数(median) 补上

比如年龄有缺失:20, 22, 25, NaN, 30。中位数可能是 23 或 24 左右,就拿它填进去。为什么数值特征常用中位数?因为中位数比平均数更不容易被极端值影响。

#  2标准化
("scaler", StandardScaler())

意思是把数值特征缩放到更统一的尺度,作用是让不同数值列更可比。如果不标准化,数值大的列可能影响更大。

z=xμσz = \frac{x-\mu}{\sigma}
  • age 可能在 0~80
  • fare 可能在 0~500
# 3 类别特征预处理
categorical_transformer = Pipeline(steps=[
    ("imputer", SimpleImputer(strategy="most_frequent")), # 如果某一列有空值 就用这一列最常出现的值补上
    ("onehot", OneHotEncoder(handle_unknown="ignore"))
])

对类别特征,要先补缺失值,再做 One-Hot 编码。

  • 如果某一列有空值
  • 就用这一列最常出现的值补上

比如 embarked 这一列

  • S, S, C, S, NaN

最常出现的是 S,那就用 S 填补缺失。

("onehot", OneHotEncoder(handle_unknown="ignore"))

意思是把类别变量变成机器能计算的数字列

比如 sex:

  • male
  • female
sex_malesex_female
10
01

再比如 embarked。S,C,Q。会变成三列。这就是 One-Hot Encoding(独热编码)

handle_unknown="ignore"

上面代码表示,如果测试集里出现了训练集没见过的新类别,不要报错,直接忽略。但测试集突然来了一个新值。S,C,Q里面突然来了一个Z。如果不加这个参数,程序可能报错。

preprocessor = ColumnTransformer(transformers=[
  	# 给这一步起名叫 num, 用 numeric_transformer 去处理 numeric_features 这些列
    ("num", numeric_transformer, numeric_features),
  	# 下面同理
    ("cat", categorical_transformer, categorical_features)
])

上面就是,把不同列,交给不同的预处理方式。这里就是把它们正式拼起来。

  • numeric_transformer ➡️ 专门处理数值列
  • categorical_transformer ➡️ 专门处理类别列

2-2 训练

model = Pipeline(steps=[
    ("preprocessor", preprocessor),
  	# 最多迭代 1000 次
    ("classifier", LogisticRegression(max_iter=1000))
])

也就是以后你只需要对 model 做 fitpredict它内部会自动完成

  1. 先预处理
  2. 再训练逻辑回归
  3. 预测时也先预处理,再预测
("classifier", LogisticRegression(max_iter=1000))
# 因为逻辑回归底层是迭代优化,不是像普通线性回归那样直接一下解出来。
# 这个参数主要是防止迭代次数不够,导致不收敛(convergence)。
stratify=y

按照 y 的类别比例来切分比如原数据里 0 类占 60%,1 类占 40%。

那训练集和测试集也会尽量保持这个比例。否则可能出现训练集里 1 类太少,测试集里 0 类太多,导致评估不稳定。

model.fit(X_train, y_train)

因为这里的 model 是一个 Pipeline,所以它不是只做逻辑回归训练。它会自动按顺序做。

第一步。对 X_train 做预处理

  • 数值列:补缺失 + 标准化
  • 类别列:补缺失 + One-Hot 编码

第二步。把处理好的训练数据送进逻辑回归

第三步。真正的学习逻辑回归。其实就是具体的算出来w和b

2-3 评估

y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1]  # 取生还=1的概率

predict 这一步是在测试集上预测最终类别。它内部也会自动先做预处理,再做预测。输出的 y_pred 一般是0 = 死亡,1 = 生还

predict_proba 输出的不是类别,是概率。比如输出是[0.2, 0.8],说明可能是死亡概率 0.2,生还概率 0.8。为什么后面要 [:, 1]

因为predict_proba(X_test) 返回的是二维数组,每一行像这样 [属于0类的概率, 属于1类的概率]。也就是说。 [:, 1] = 生还=1的概率

继续说一下指标,下面都属于概念理解+数据分析类的,还是很重要的。

混淆矩阵 confusion matrix

首先你要知道混淆矩阵代表着什么吧。这点需要你自己学习补充知识。

模型预测后,得到这个混淆矩阵(confusion matrix)

[TPFPFNTN][5010832]\begin{bmatrix}TP & FP \\FN & TN\end{bmatrix} \begin{bmatrix}50 & 10 \\8 & 32\end{bmatrix}

意思就是

我们假设测试集一共有 100 个人。
真实情况是:
	•	60 人死亡(0)
	•	40 人生还(1)
它的意思是:
	•	50:真实死亡,预测也死亡
	•	10:真实死亡,但预测成生还
	•	8:真实生还,但预测成死亡
	•	32:真实生还,预测也生还

它的意义它不是只告诉你对了 82 个,而是告诉你哪类错得多 错法是什么

第一行 [50, 10]
真实死亡的 60 个人里:
	•	50 个被正确预测成死亡
	•	10 个被错预测成生还

第二行 [8, 32]
真实生还的 40 个人里:
	•	32 个被正确预测成生还
	•	8 个被错预测成死亡

这个数字的意义
这说明模型有两个具体问题:
	•	会把一部分本来会死的人误判成生还(10 人)
	•	也会把一部分本来会生还的人误判成死亡(8 人)
这两个错误在不同业务里,代价可能不一样。

准确率 Accuracy

预测对的人数总人数=82100=0.82\frac{\text{预测对的人数}}{\text{总人数}} = \frac{82}{100} = 0.82

意思就是 50(死亡预测对) + 32(生还预测对) = 82 所以准确率就是82%。100 个人里,这个模型大概能判断对 82 个人。

但它的问题就是如果数据很不平衡,它可能会骗人。因为比如100个人里面实际上95个人就是死亡了,剩下5人活着了。即使预测了全部死亡,那么准确率仍然有 95/100 = 95%。但这种模型其实很烂,因为它一个生还者都抓不到。所以 Accuracy 只能说明总体对了多少,不能说明错在哪里。

精确率 Precision

先看模型预测为生还的人有多少。10(其实死亡,但你说他生还) + 32(其实生还,你也说他生还) = 42

但是因为真正生还的是42,所以结果就是

Precision=32420.7619Precision = \frac{32}{42} \approx 0.7619

这个76%的数字的意义就是在于,模型每说 100 个人会生还,里面大约有 76 个人真的会生还。就是说你预测成生还的人,靠谱不靠谱。如果你把生还理解成高风险用户肿瘤阳性会买的人,那 precision 就是在看:你抓出来的人里,有多少真的是你要的那类。如果 precision 很低,比如 40%,那说明:你预测成 1 的人里,大量都是误报

召回率 Recall

真实生还的人一共有 40 个。模型成功找出来了 32 个,所以

Recall=3240=0.8Recall = \frac{32}{40} = 0.8

这个数字的意义意思是:真正会生还的人里,模型抓到了 80%。或者说真正的正类,你漏掉了多少。

  • 8 个真实生还的人,被错判成死亡 ⬅️ 这里漏掉了

所以 recall 不满 100%。具体场景理解。如果正类很重要,比如

  • 真正患病的人
  • 真正会流失的客户
  • 真正的欺诈交易

那 recall 就表示你把真正重要的人抓出来了多少。如果 recall 很低,比如 50%,那就表示:一半真正的正类都被你漏掉了。

关于 Precision 和 Recall 的区别,用数字直接看

Precision = 76.19% VS Recall = 80%

Precision 说的是你一共说了42人生还,实际上就32人。最后就是 76.19%

Recall 说的是真实有 40 个生还,你抓到 32 个,所以 80%

大白话就是相当于医院看病⬇️

Precision:医生判定有病的人里,说对的比例是多少 (医生判了 20 个人有病,结果里面 15 个真有病,5 个没病。)

Recall:所有真正有病的人里,医生到底命中了多少 (今天总共有 30 个真的有病的人,医生只找出来了 15 个。)

F1-score

F1 是 Precision 和 Recall 的综合分数。如果:Precision = 0.7619 Recall = 0.8。那么

F10.7805F1 \approx 0.7805

这个数字的意义意思是:如果你既关心 precision,又关心 recall,F1 可以给你一个综合评价。

为什么要它?因为有些模型会这样

  • Precision 很高,Recall 很低
  • 或 Recall 很高,Precision 很低

这时单看一个指标不够。F1 就是在看:这两个是否平衡。

ROC-AUC

这个稍微抽象一点,我也直接用具体数字讲。假设模型给出的是生还概率

比如对 6 个人的预测概率:
人	真实标签	生还概率
A	1	0.92
B	1	0.81
C	1	0.73
D	0	0.65
E	0	0.30
F	0	0.12
你会发现:
	•	大部分真正生还的人,概率更高
	•	大部分真正死亡的人,概率更低

不同 AUC 的实际意义

  • AUC = 0.5 ➡️ 和瞎猜差不多
  • AUC = 0.7 ➡️ 有一定区分能力
  • AUC = 0.8 ➡️ 区分能力不错
  • AUC = 0.9 ➡️ 区分能力很强
  • AUC = 1.0 ➡️ 完美区分

AUC 本质上在看。随便拿一个真正的正类,再随便拿一个真正的负类,模型把正类排在负类前面的概率有多大。如果 AUC = 0.90

意思就是➡️ 随机抽一个生还者和一个死亡者,模型有 90% 的概率把生还者打分更高。这就是个很有力量的解释。

classification_report()会打印出来一个结果

Classification Report:
              precision    recall  f1-score   support

           0     0.8151    0.8818    0.8472       110
           1     0.7833    0.6812    0.7287        69

    accuracy                         0.8045       179
   macro avg     0.7992    0.7815    0.7879       179
weighted avg     0.8029    0.8045    0.8015       179

这里的 support。support 就是每个类别在测试集里有多少样本。

  • 类别 0(死亡):110
  • 类别 1(生还):69

就是告诉你。这些 precision / recall / f1,是在多少样本基础上算出来的。比如某个类别 support 只有 3 个,那即使 recall=100%,你也不能太兴奋,因为样本太少。

总结指标

•	Accuracy = 82%Precision = 76.19%
•	Recall = 80%
•	F1 = 78.05%
•	AUC = 假设 0.86

Accuracy 82% 说明整体上 100 个人里能判对 82 个。

Precision 76.19% 模型预测会生还的人里,大约 76% 真的会生还。说明有一定误报,但不算特别严重。

Recall 80% 真正会生还的人里,模型抓到了 80%。说明漏掉了 20%。

F1 78.05% 如果你同时在意别乱报别漏掉,整体平衡还可以。

AUC 0.86 模型在排序上挺有区分力。即使你以后调整分类阈值,它也有比较好的基础。

再给你两组对比数据,你会更清楚
模型 A
		Accuracy: 90%
		Precision: 95%
		Recall: 40%
这个模型很谨慎。它一旦说 1,大多数都真的是 1。但它漏掉了很多真正的 1
【适合】
		误报代价很高的场景

模型 B
		Accuracy: 82%
		Precision: 70%
		Recall: 92%
这个模型很激进。它抓到了很多真正的 1,但也误报了不少。
【适合】
		漏报代价很高的场景
  • Accuracy:总体对了多少
  • Precision:你报出来的 1,准不准
  • Recall:真正的 1,你抓到多少
  • F1:Precision 和 Recall 的综合平衡
  • AUC:模型给正类打高分、给负类打低分的能力强不强
  • Confusion Matrix:具体错在哪一类、怎么错的