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 当最终输出。它会再经过一个函数,把它压到 0 到 1 之间。这个函数就是sigmoid 函数(也叫 logistic function)
上面的完全可以不用记忆,反正它的作用就是。把任意实数压缩到 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())
意思是把数值特征缩放到更统一的尺度,作用是让不同数值列更可比。如果不标准化,数值大的列可能影响更大。
- 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_male | sex_female |
|---|---|
| 1 | 0 |
| 0 | 1 |
再比如 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 做 fit,predict它内部会自动完成
- 先预处理
- 再训练逻辑回归
- 预测时也先预处理,再预测
("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)
意思就是
我们假设测试集一共有 100 个人。
真实情况是:
• 60 人死亡(0)
• 40 人生还(1)
它的意思是:
• 50:真实死亡,预测也死亡
• 10:真实死亡,但预测成生还
• 8:真实生还,但预测成死亡
• 32:真实生还,预测也生还
它的意义它不是只告诉你『对了 82 个』,而是告诉你哪类错得多 错法是什么。
第一行 [50, 10]
真实死亡的 60 个人里:
• 50 个被正确预测成死亡
• 10 个被错预测成生还
第二行 [8, 32]
真实生还的 40 个人里:
• 32 个被正确预测成生还
• 8 个被错预测成死亡
这个数字的意义
这说明模型有两个具体问题:
• 会把一部分本来会死的人误判成生还(10 人)
• 也会把一部分本来会生还的人误判成死亡(8 人)
这两个错误在不同业务里,代价可能不一样。
准确率 Accuracy
意思就是 50(死亡预测对) + 32(生还预测对) = 82 所以准确率就是82%。100 个人里,这个模型大概能判断对 82 个人。
但它的问题就是如果数据很不平衡,它可能会骗人。因为比如100个人里面实际上95个人就是死亡了,剩下5人活着了。即使预测了全部死亡,那么准确率仍然有 95/100 = 95%。但这种模型其实很烂,因为它一个生还者都抓不到。所以 Accuracy 只能说明总体对了多少,不能说明错在哪里。
精确率 Precision
先看『模型预测为生还的人』有多少。10(其实死亡,但你说他生还) + 32(其实生还,你也说他生还) = 42
但是因为真正生还的是42,所以结果就是
这个76%的数字的意义就是在于,模型每说 100 个人会生还,里面大约有 76 个人真的会生还。就是说你预测成『生还』的人,靠谱不靠谱。如果你把『生还』理解成『高风险用户』、『肿瘤阳性』、『会买的人』,那 precision 就是在看:你抓出来的人里,有多少真的是你要的那类。如果 precision 很低,比如 40%,那说明:你预测成 1 的人里,大量都是误报。
召回率 Recall
真实生还的人一共有 40 个。模型成功找出来了 32 个,所以
这个数字的意义意思是:真正会生还的人里,模型抓到了 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。那么
这个数字的意义意思是:如果你既关心 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:具体错在哪一类、怎么错的