news 2026/10/5 4:12:32

线性回归身高预测实战:从数据清洗到SHAP解释的完整流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
线性回归身高预测实战:从数据清洗到SHAP解释的完整流程

简介:这份资源面向机器学习入门者与需要掌握回归建模的开发者,围绕身高预测这一典型连续值估算场景,讲解线性回归从原理到落地的完整思路。压缩包共2个文件,包含1个xlsx数据表与1个py脚本,整体约80KB,前者用于存放身高、年龄、性别、体重等样本数据,后者基于scikit-learn实现模型训练与预测。内容覆盖数据清洗、分类变量编码、训练测试集划分、LinearRegression建模,以及MSE、RMSE、R²等指标评估,并延伸讨论多项式回归、岭回归、套索回归与集成方法的适用边界。已有115人学习下载,适合希望用最小体量案例打通回归流程、理解特征与目标间线性依赖关系的读者参考。

1. 线性回归身高预测:一条被低估的入门实战线

很多人第一次接触机器学习,都是从「用线性回归预测身高」这个例子开始的。它看起来简单到有点不起眼——不就是找一条直线去拟合数据吗?但真正动手做过的人会发现,这条直线背后藏着机器学习应用流程的完整骨架:数据清洗、特征选择、模型训练、参数调优、误差评估,一个都不少。身高预测这个场景的好处在于,它足够直观,你能用肉眼判断结果是否离谱;同时它又足够真实,遗传、营养、性别等因素的交互让数据不会完美落在一条直线上。对于刚入门机器学习的人来说,这是一个能让你把「线性回归算法」从公式变成可运行代码的最小闭环。而对于已经工作的一线工程师,它也是一个验证数据处理管道是否可靠的试金石——如果连身高预测这种低维问题都跑不通,那大概率是数据管道出了问题,而不是模型不够复杂。

2. 先搞懂线性回归到底在拟合什么

2.1 从一条直线到最小二乘法

线性回归的核心假设是:目标变量(身高)和特征变量(比如父母身高、营养指数)之间存在近似线性关系。用数学语言说,就是 y = w₁x₁ + w₂x₂ + ... + wₙxₙ + b,其中 w 是权重,b 是偏置。模型要做的,就是找到一组 w 和 b,让预测值和真实值之间的差距尽可能小。

这个「差距」通常用均方误差(MSE)来衡量,也就是把所有样本的预测误差平方后求平均。为什么用平方而不是绝对值?因为平方函数可导,方便用梯度下降求解,而且对大误差的惩罚更重,模型会更倾向于避免离谱的预测。最小二乘法就是让 MSE 最小化的解析解方法,但在特征维度较高或数据量很大时,我们更常用梯度下降来迭代求解。

这里有一个容易被忽略的点:线性回归的「线性」指的是对参数 w 线性,而不是对特征 x 线性。也就是说,你完全可以把 x² 作为一个新特征放进去,模型依然叫线性回归。这个特性在身高预测里很有用——比如年龄对身高的影响显然不是线性的,但你可以把年龄和年龄的平方都作为特征输入。

2.2 为什么身高预测适合用线性回归练手

身高数据有几个天然优势。第一,它是一维连续值,可视化极其方便,你可以直接把数据和拟合直线画在同一张图上,一眼看出欠拟合还是过拟合。第二,身高和父母身高之间的相关性有生物学基础,不是强行找规律,模型学到的权重有实际解释意义。第三,数据获取门槛低,很多公开数据集都包含身高、体重、年龄等字段,你不需要爬虫或申请权限就能拿到。

但要注意,身高预测不是要你做一个「算命工具」。它的真正价值在于让你走通机器学习的完整应用流程:从原始数据到特征工程,从划分训练测试集到评估指标,从欠拟合排查到正则化引入。这些步骤在更复杂的模型里一模一样,只是换了个外壳。

2.3 用 Python 跑通最小可行示例

下面这段代码用 scikit-learn 实现了一个最简版本的身高预测。数据是模拟生成的,你可以直接复制运行。

import numpy as np from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, r2_score # 模拟数据:父母平均身高、营养指数、性别(0女1男) np.random.seed(42) n = 200 parent_height = np.random.normal(170, 6, n) nutrition = np.random.normal(0.5, 0.1, n) gender = np.random.randint(0, 2, n) # 真实身高 = 基础值 + 父母身高影响 + 营养影响 + 性别差异 + 噪声 height = (50 + 0.6 * parent_height + 20 * nutrition + 5 * gender + np.random.normal(0, 2, n)) X = np.column_stack([parent_height, nutrition, gender]) y = height # 划分训练集和测试集,测试集占20% X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 训练线性回归模型 model = LinearRegression() model.fit(X_train, y_train) # 预测并评估 y_pred = model.predict(X_test) mse = mean_squared_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f"权重: {model.coef_}") print(f"偏置: {model.intercept_:.2f}") print(f"MSE: {mse:.2f}") print(f"R²: {r2:.4f}")

这段代码的逻辑很直接:先生成模拟数据,然后把特征拼成矩阵,划分训练测试集,调用LinearRegression拟合,最后用 MSE 和 R² 评估。model.coef_返回每个特征的权重,model.intercept_是偏置项。MSE 越小越好,R² 越接近 1 说明模型解释力越强。

参数方面,test_size=0.2表示留出 20% 的数据做测试,这个比例在数据量小于 1000 时比较常用。random_state=42保证每次划分结果一致,方便复现。如果你把fit_intercept设为 False,模型就不会学偏置项,但身高预测显然需要偏置,所以保持默认 True。

跑完这段代码,你会看到权重大概在 0.6、20、5 附近,和生成数据时的设定接近。这说明模型确实学到了特征和身高之间的关系。如果权重偏离很大,先检查数据是否标准化——不同特征的量纲差异会拖慢梯度下降的收敛速度。

3. 把原始数据变成模型能吃的特征

3.1 缺失值、异常值和编码处理

真实数据不会像模拟数据那么干净。身高数据集里常见的脏数据包括:身高字段为空、年龄写成负数、性别字段混用「男/女」和「M/F」。处理这些问题有一套标准流程。

缺失值方面,如果某一行只有身高缺失,而身高是目标变量,那这一行只能丢弃,因为无监督填充目标变量会引入偏差。如果特征缺失,可以用均值、中位数或 KNN 填充。我一般先用df.isnull().sum()看缺失比例,低于 5% 直接删行,高于 5% 考虑填充。

异常值检测用 IQR 方法比较稳妥:计算第一四分位数 Q1 和第三四分位数 Q3,把小于 Q1-1.5IQR 或大于 Q3+1.5IQR 的值标记为异常。但身高数据里,异常值未必是错误——比如篮球运动员的身高确实会偏离普通人分布。这时候不要急着删,先画箱线图看看偏离程度,再决定是截断还是保留。

类别特征编码方面,性别这种二分类直接用 0/1 映射即可。如果遇到多分类(比如地区),用独热编码(One-Hot Encoding),但要注意维度爆炸问题。身高预测场景里特征本来就不多,独热编码完全扛得住。

import pandas as pd # 假设 df 是原始数据框 # 1. 查看缺失情况 print(df.isnull().sum()) # 2. 删除目标变量缺失的行 df = df.dropna(subset=['height']) # 3. 特征缺失用中位数填充 df['parent_height'] = df['parent_height'].fillna(df['parent_height'].median()) # 4. 性别编码 df['gender'] = df['gender'].map({'男': 1, '女': 0, 'M': 1, 'F': 0}) # 5. IQR 异常值标记(不直接删除,先看看) Q1 = df['height'].quantile(0.25) Q3 = df['height'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR outliers = df[(df['height'] < lower_bound) | (df['height'] > upper_bound)] print(f"异常值数量: {len(outliers)}")

这段代码的关键在于顺序:先处理目标变量缺失,再处理特征缺失,最后做编码和异常值标记。如果把编码放在缺失值处理之前,map函数遇到 NaN 会直接报错。异常值标记后不要急着删,先看看数量——如果超过 5%,说明数据分布本身偏斜,删了反而丢信息。

3.2 特征缩放与多项式特征

线性回归对特征尺度敏感,尤其是用梯度下降求解时。父母身高在 160-180 之间,营养指数在 0-1 之间,如果不做缩放,损失函数的等高线会变成狭长的椭圆,梯度下降要走很多弯路才能收敛。标准化(StandardScaler)把每个特征变成均值 0、方差 1 的分布,是最常用的做法。

from sklearn.preprocessing import StandardScaler, PolynomialFeatures from sklearn.pipeline import Pipeline # 构建管道:先生成多项式特征,再标准化,最后回归 pipeline = Pipeline([ ('poly', PolynomialFeatures(degree=2, include_bias=False)), ('scaler', StandardScaler()), ('lr', LinearRegression()) ]) pipeline.fit(X_train, y_train) y_pred = pipeline.predict(X_test) print(f"R²: {r2_score(y_test, y_pred):.4f}")

PolynomialFeatures(degree=2)会生成所有特征的二次项和交互项。比如原来有 [a, b] 两个特征,它会生成 [a, b, a², ab, b²]。这样模型就能捕捉非线性关系,比如父母身高对子女身高的影响可能随父母身高增加而递减。include_bias=False是因为 LinearRegression 自己会学偏置,不需要多项式模块再加一列全 1。

用 Pipeline 的好处是把所有预处理步骤打包在一起,避免在训练集和测试集上分别做缩放导致数据泄露。你只需要在训练集上 fit,然后直接 predict 测试集,Pipeline 会自动用训练集的统计量处理测试集。

3.3 训练集测试集划分的讲究

train_test_split默认是随机划分,但在身高数据里,如果样本按年龄排序过,随机划分可能导致训练集和测试集的年龄分布不一致。这时候可以用分层抽样(stratified sampling),把年龄分段后作为分层依据。

另一个常见问题是数据泄露。比如你先用整个数据集计算了均值来填充缺失值,然后再划分训练测试集,那测试集的均值信息就泄露到了训练过程。正确做法是先划分,再在训练集上计算填充统计量,然后应用到测试集。Pipeline 能自动帮你规避这个问题,但如果你手动处理,一定要记住这个顺序。

from sklearn.model_selection import train_test_split # 先划分,再处理缺失值 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 在训练集上计算中位数 median_val = X_train['parent_height'].median() X_train['parent_height'] = X_train['parent_height'].fillna(median_val) X_test['parent_height'] = X_test['parent_height'].fillna(median_val)

这段代码展示了正确的处理顺序。median_val只从训练集计算,然后同时应用到训练集和测试集。如果你先用整个数据集的median(),测试集的信息就混进去了,评估结果会偏乐观。

4. 训练完之后怎么判断模型靠不靠谱

4.1 MSE、R² 和残差图

MSE 衡量的是预测值和真实值之间的平均平方差距,单位是身高的平方(cm²),解释起来不太直观。R² 把 MSE 归一化到 0-1 之间,表示模型解释了目标变量多少比例的方差。R²=0.8 意味着模型解释了 80% 的身高变化,剩下 20% 是噪声或未捕捉的因素。

但光看这两个指标不够。残差图能告诉你更多信息:把预测值作为横轴,残差(真实值减预测值)作为纵轴,如果残差随机分布在 0 附近,说明模型没有系统性偏差;如果残差呈现 U 型或倒 U 型,说明线性模型欠拟合,需要考虑多项式特征。

import matplotlib.pyplot as plt residuals = y_test - y_pred plt.scatter(y_pred, residuals, alpha=0.6) plt.axhline(y=0, color='r', linestyle='--') plt.xlabel('预测身高') plt.ylabel('残差') plt.title('残差图') plt.show()

如果残差图显示明显的曲线模式,先别急着换模型,试试加多项式特征。身高和年龄的关系往往是非线性的,二次项通常就能解决大部分问题。

4.2 交叉验证与学习曲线

单次划分的评估结果有随机性。如果恰好测试集里都是容易预测的样本,R² 会虚高。K 折交叉验证能给出更稳健的估计:把数据分成 K 份,每次用 K-1 份训练,剩下 1 份验证,重复 K 次后取平均。

from sklearn.model_selection import cross_val_score scores = cross_val_score(pipeline, X, y, cv=5, scoring='r2') print(f"交叉验证 R²: {scores.mean():.4f} (+/- {scores.std():.4f})")

cv=5表示 5 折交叉验证,scoring='r2'指定用 R² 评估。输出里的标准差告诉你模型在不同数据子集上的稳定性——如果标准差很大,说明模型对数据划分敏感,可能需要更多数据或正则化。

学习曲线则能帮你判断是欠拟合还是过拟合。横轴是训练集大小,纵轴是误差。如果训练误差和验证误差都很高且接近,说明欠拟合,需要增加模型复杂度;如果训练误差低但验证误差高,说明过拟合,需要正则化或更多数据。

4.3 正则化:岭回归和 Lasso 的选择

当特征数量接近或超过样本量时,普通最小二乘法容易过拟合。岭回归(Ridge)在损失函数里加了 L2 正则项,让权重尽量小但不为零;Lasso 加的是 L1 正则项,会把不重要的特征权重直接压到零,相当于自动特征选择。

from sklearn.linear_model import Ridge, Lasso ridge = Ridge(alpha=1.0) ridge.fit(X_train, y_train) print(f"Ridge R²: {ridge.score(X_test, y_test):.4f}") lasso = Lasso(alpha=0.1) lasso.fit(X_train, y_train) print(f"Lasso R²: {lasso.score(X_test, y_test):.4f}") print(f"Lasso 非零权重数量: {sum(lasso.coef_ != 0)}")

alpha控制正则化强度,越大惩罚越重。身高预测场景里特征本来就不多,正则化收益有限,但如果你加了大量多项式特征,正则化就很有必要了。我一般先用 Ridge 试,因为它不会把权重压到零,更稳定;如果怀疑有冗余特征,再换 Lasso 看看哪些特征被淘汰了。

5. 身高预测实战中容易翻车的几个地方

5.1 现象:R² 很高但预测新数据一塌糊涂

原因:数据泄露。最常见的是在划分训练测试集之前做了标准化或缺失值填充,导致测试集信息混入训练过程。另一个可能是特征里包含了目标变量的衍生信息,比如用「体重/身高²」作为特征去预测身高。

解决:把所有预处理步骤放进 Pipeline,确保 fit 只在训练集上调用。检查特征列表,确认没有直接或间接包含目标变量的计算。

5.2 现象:权重符号和常识相反

原因:特征之间存在多重共线性。比如同时放入「父母平均身高」和「父亲身高」「母亲身高」,这三个变量高度相关,模型学到的权重会变得不稳定,甚至出现父亲身高权重为负的荒谬结果。

解决:用方差膨胀因子(VIF)检测共线性,VIF 大于 10 就考虑删掉冗余特征。或者直接用岭回归,L2 正则化能缓解共线性带来的权重震荡。

5.3 现象:MSE 很小但残差图有明显模式

原因:模型欠拟合。线性模型无法捕捉身高随年龄增长先加速后放缓的非线性趋势。MSE 小可能是因为数据集中大部分样本集中在某个年龄段,模型在那一段拟合得不错,但整体趋势没学到。

解决:加多项式特征,尤其是年龄的二次项。画残差图确认模式是否消失。如果仍然存在,考虑分段建模或换用树模型。

5.4 现象:交叉验证结果波动很大

原因:样本量太小或数据分布不均匀。身高数据如果集中在某个地区或某个年龄段,不同折之间的分布差异会很大。

解决:增加数据量,或者用分层交叉验证确保每折的年龄分布一致。如果数据实在少,用留一法交叉验证(LOOCV),虽然计算量大但能最大化利用数据。

5.5 现象:Lasso 把重要特征权重压成了零

原因:alpha 设得太大,正则化惩罚过重。Lasso 的 L1 正则项在 alpha 较大时会强制所有权重趋近于零,导致模型退化成只用偏置预测。

解决:用交叉验证选择最优 alpha,而不是手动拍脑袋。LassoCV会自动在指定范围内搜索最佳 alpha。

from sklearn.linear_model import LassoCV lasso_cv = LassoCV(alphas=np.logspace(-3, 1, 50), cv=5) lasso_cv.fit(X_train, y_train) print(f"最优 alpha: {lasso_cv.alpha_:.4f}") print(f"R²: {lasso_cv.score(X_test, y_test):.4f}")

np.logspace(-3, 1, 50)生成 50 个从 0.001 到 10 的对数等距 alpha 值,LassoCV会逐一尝试并返回交叉验证误差最小的那个。这比手动调参靠谱得多。

6. 用 SHAP 值把线性回归的预测拆开看

线性回归的权重本身就有解释性,但当特征经过标准化或多项式展开后,权重的含义变得模糊。SHAP 值能把每个预测拆解成各个特征的贡献,让你看清模型到底在依赖什么。

import shap # 用训练好的 pipeline 解释 explainer = shap.LinearExplainer(pipeline.named_steps['lr'], pipeline.named_steps['scaler'].transform( pipeline.named_steps['poly'].transform(X_train))) shap_values = explainer.shap_values( pipeline.named_steps['scaler'].transform( pipeline.named_steps['poly'].transform(X_test))) # 汇总图:看全局特征重要性 shap.summary_plot(shap_values, X_test, feature_names=['parent_height', 'nutrition', 'gender'])

这段代码稍微有点绕,因为 Pipeline 里的多项式展开和标准化改变了特征空间。LinearExplainer需要在线性模型的输入空间上工作,所以要先手动做同样的变换。shap_values的每一行对应一个样本,每个值表示该特征对预测结果的贡献量。汇总图里,每个点是一个样本,横轴是 SHAP 值,颜色表示特征值高低。

从身高预测的 SHAP 图里,你通常会发现父母身高的 SHAP 值分布最宽,说明它是最重要的特征;性别的 SHAP 值集中在两个离散区域,对应男女的平均差异;营养指数的 SHAP 值相对集中,说明它的影响比较均匀。如果某个多项式特征的 SHAP 值出现异常大的正负波动,说明模型在那段区间过拟合了,需要考虑降低多项式阶数或加强正则化。

我自己的习惯是,每次调完参都跑一遍 SHAP 汇总图,确认特征重要性排序符合业务直觉。如果模型说「性别比父母身高更重要」,那大概率是数据编码出了问题,而不是模型发现了新规律。这个检查步骤花不了两分钟,但能帮你避开很多玄学翻车。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 4:11:43

C++嵌入Python完整指南:虚拟环境配置与pybind11实践

1. 为什么要把Python塞进C里&#xff1a;动机与场景先聊点实际的。很多做C服务端或桌面客户端的团队&#xff0c;都会遇到一个共同的痛点&#xff1a;业务逻辑迭代太快&#xff0c;C的编译-链接-部署链路太重了。今天改个策略参数&#xff0c;明天调个推荐规则&#xff0c;每次…

作者头像 李华
网站建设 2026/10/5 4:11:29

二维卡尔曼滤波位置速度融合:从原理到工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 4:11:06

鸿蒙游戏服务错误码1002000001排查指南:从AGC配置到签名指纹

看到 1002000001 这个错误码的时候&#xff0c;我第一反应不是翻代码&#xff0c;而是先看一眼签名配置和 AGC 后台。因为“system internal error”这个返回&#xff0c;十有八九不是客户端逻辑写错了&#xff0c;而是某个环境条件没满足&#xff0c;被 SDK 统一收敛成了内部错…

作者头像 李华
网站建设 2026/10/5 4:11:06

Vue2+SpringBoot商城验证码实战:Hutool生成+Redis存储+正则校验

做在线商城项目&#xff0c;登录注册这块你早晚会撞上验证码。Vue2SpringBoot的经典组合里&#xff0c;验证码不是一个孤立功能&#xff0c;它牵扯到后端图形生成、缓存存储、接口校验&#xff0c;以及前端的表单正则预检。这篇文章我把自己在商城用户模块里用Hutool生成图形验…

作者头像 李华
网站建设 2026/10/5 4:09:46

XGBoost Kaggle实战:从原理到调参集成的完整指南

如果你是冲着“在Kaggle拿一个好名次”来读这篇内容的&#xff0c;我的第一个建议可能和你想的不一样&#xff1a;先别急着堆特征&#xff0c;也别急着上深度学习&#xff0c;把XGBoost这一套东西吃透再说。我在Kaggle打比赛这几年的感受是&#xff0c;XGBoost之所以成为表格类…

作者头像 李华
网站建设 2026/10/5 4:09:23

2026本科论文AI平台实测:从选题到答辩的10个工具全流程测评

2026届的论文季比往年来得更早一些&#xff0c;我后台被问得最多的一句话是&#xff1a;“博主&#xff0c;到底哪个 AI 论文平台能救我的论文&#xff1f;” 这个问题背后&#xff0c;其实是本科生面对论文时的普遍焦虑&#xff1a;选题没方向、文献看不完、写出来的东西口语化…

作者头像 李华