线性回归身高预测实战:从特征工程到残差诊断的完整流程 简介这份资源面向机器学习入门者与需要掌握回归建模的开发者围绕线性回归在身高预测中的完整应用展开。包内共2个文件包含1个xlsx数据表与1个py脚本压缩包约80KB体量轻便适合快速上手练习。数据表用于记录身高、年龄、性别、体重等影响因素脚本则借助scikit-learn的LinearRegression类完成数据预处理、模型训练、评估与预测全流程并涉及MSE、RMSE、R²等指标的使用。读者可据此理解线性回归的核心原理掌握分类变量编码、训练测试集划分等关键环节同时也能延伸思考多项式回归、岭回归及随机森林等更复杂模型在同类问题中的取舍。目前已有114人学习适合作为课程实验或自学练手素材。1. 身高预测这件事线性回归到底能不能打用机器学习做身高预测听起来像是个练手玩具但我第一次认真拿它跑完整流程时翻车翻得很彻底。当时手里有一份青少年体测数据特征包括年龄、性别、父母身高、体重、坐高目标变量是本人身高。我天真地以为丢进线性回归就能出结果结果模型在训练集上 R² 有 0.87换到测试集直接掉到 0.61。后来才发现问题不在算法而在我对“线性”这两个字的理解太粗糙——父母身高和子女身高的关系本身带有均值回归特性直接线性拟合会系统性高估高个父母的孩子、低估矮个父母的孩子。线性回归身高预测这个题目本质上是把连续型目标变量的预测问题拆成“特征如何构造、模型如何假设、误差如何度量”三件事。它适合两类人一类是刚入门机器学习、想找一个有物理意义、数据也容易理解的项目把流程跑通另一类是已经会调库、但想搞清楚最小二乘、正规方程、梯度下降这些底层机制在实际数据上到底表现如何的人。身高预测的好处是特征直观、量纲清楚、异常值容易识别坏处是真实关系往往不是纯线性的所以它恰好是一个能让你把“假设检验”和“模型诊断”做扎实的场景。这一篇不讲虚的从数据构造、特征工程、模型训练到残差诊断每一步都给可复现的代码和参数说明。中间会重点讲清楚为什么身高预测里“父母平均身高”比“父亲身高母亲身高”两个独立特征更稳以及什么时候该上多项式特征、什么时候该收手。如果你正在做机器学习入门项目或者被线性回归头歌这类练习卡住这篇能直接抄作业。2. 把身高预测拆成可复现的最小流程2.1 数据从哪来、长什么样、怎么造一份能跑的真实的身高数据往往涉及隐私公开数据集里比较接近的是 CDC 的 NHANES 体测数据但字段多、清洗麻烦。我一般会先用一份模拟数据把流程跑通再换真实数据。模拟数据的关键是让特征和目标之间保持已知的线性关系同时加入噪声和一点非线性这样才能验证模型到底有没有学到东西。import numpy as np import pandas as pd np.random.seed(42) n 800 # 父母平均身高cm围绕 168 波动 parent_mean np.random.normal(168, 6, n) # 性别0 女 1 男 sex np.random.binomial(1, 0.5, n) # 年龄 10-18 岁 age np.random.uniform(10, 18, n) # 体重 kg weight np.random.normal(55, 10, n) # 真实关系身高 ≈ 0.7*父母平均 5*性别 2*年龄 0.1*体重 噪声 height (0.7 * parent_mean 5 * sex 2 * age 0.1 * weight np.random.normal(0, 3, n) 30) df pd.DataFrame({ parent_mean: parent_mean, sex: sex, age: age, weight: weight, height: height }) print(df.describe().round(2))这段代码里parent_mean的系数 0.7 是刻意设的因为真实遗传学里子女身高对父母平均身高的回归系数通常在 0.6 到 0.8 之间这就是前面说的均值回归。sex的系数 5 表示同等条件下男性比女性高约 5 cmage系数 2 表示 10 到 18 岁阶段每大一岁高约 2 cm。噪声标准差 3 cm 模拟测量误差和个体差异。跑完describe()你要重点看height的均值和标准差是否合理如果均值跑到 200 以上说明常数项加错了。2.2 特征矩阵怎么摆父母身高该合并还是分开这是身高预测里最容易踩的一个坑。很多人会把父亲身高和母亲身高作为两个独立特征丢进去结果发现两个系数的置信区间都很宽甚至出现一个正一个负的荒唐情况。原因是父亲身高和母亲身高高度相关直接放进去会造成多重共线性方差膨胀因子飙升。我一般会做两步处理先构造parent_mean (father mother) / 2再 optionally 加一个parent_diff father - mother来捕捉父母身高差异带来的额外信息。但在身高预测这个场景里parent_diff的系数通常不显著加了反而增加过拟合风险所以最小可用版本只用parent_mean。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler feature_cols [parent_mean, sex, age, weight] X df[feature_cols].values y df[height].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) print(训练集均值, X_train_s.mean(axis0).round(3)) print(训练集标准差, X_train_s.std(axis0).round(3))StandardScaler在训练集上fit在测试集上只transform这是铁律。如果你在全量数据上 fit测试集的均值方差信息就泄漏进训练过程评估结果会虚高。标准化之后每个特征的均值接近 0、标准差接近 1这样梯度下降的收敛速度会快很多后面看系数大小也能直接比较相对重要性。2.3 用正规方程和梯度下降各跑一遍看系数差多少线性回归有两种经典解法正规方程直接求解析解梯度下降迭代逼近。身高预测这种特征数只有 4 个的小规模问题正规方程秒出结果但理解梯度下降对后面上更大数据集有帮助。from sklearn.linear_model import LinearRegression, SGDRegressor from sklearn.metrics import mean_squared_error, r2_score # 正规方程 lr LinearRegression() lr.fit(X_train_s, y_train) y_pred_lr lr.predict(X_test_s) # 梯度下降 sgd SGDRegressor( losssquared_error, penaltyNone, learning_rateconstant, eta00.01, max_iter2000, tol1e-4, random_state42 ) sgd.fit(X_train_s, y_train) y_pred_sgd sgd.predict(X_test_s) print(正规方程 R2:, round(r2_score(y_test, y_pred_lr), 4)) print(梯度下降 R2:, round(r2_score(y_test, y_pred_sgd), 4)) print(正规方程系数:, lr.coef_.round(3)) print(梯度下降系数:, sgd.coef_.round(3))LinearRegression默认用 SVD 分解求最小二乘解数值稳定性比直接求逆矩阵好。SGDRegressor里penaltyNone表示不加正则eta00.01是初始学习率max_iter2000给足迭代次数。跑完你会发现两者 R² 差距通常在 0.01 以内但系数可能在小数点后两位有差异这是梯度下降没完全收敛导致的。如果差距很大先把eta0调小到 0.001 再试。2.4 残差图怎么看三个必须检查的模式模型跑完不能只看 R²残差诊断才是判断线性假设是否成立的关键。我一般会画三张图残差 vs 预测值、残差 vs 每个特征、残差的 Q-Q 图。import matplotlib.pyplot as plt residuals y_test - y_pred_lr fig, axes plt.subplots(1, 3, figsize(15, 4)) axes[0].scatter(y_pred_lr, residuals, alpha0.5) axes[0].axhline(0, colorred, linestyle--) axes[0].set_xlabel(预测身高) axes[0].set_ylabel(残差) axes[0].set_title(残差 vs 预测值) axes[1].scatter(X_test_s[:, 0], residuals, alpha0.5) axes[1].axhline(0, colorred, linestyle--) axes[1].set_xlabel(标准化后的父母平均身高) axes[1].set_title(残差 vs 父母平均身高) axes[2].hist(residuals, bins30, edgecolorblack) axes[2].set_title(残差分布) plt.tight_layout() plt.show()第一张图如果出现喇叭口形状说明误差方差随预测值变化违反同方差假设这时候可以考虑对目标变量取对数。第二张图如果出现 U 型曲线说明父母身高和子女身高的关系是非线性的需要加二次项。第三张图如果明显偏斜说明残差不服从正态分布置信区间会不可靠。身高预测里最常见的是第二张图出问题因为遗传关系本身带有饱和效应。3. 多项式特征和正则化什么时候该加、什么时候该收3.1 加二次项之前先看残差有没有 U 型上一节提到残差 vs 父母平均身高如果出现 U 型就说明线性项不够。但加二次项不是无脑加我一般会先做一个分组统计把父母平均身高按分位数切成 5 组看每组子女身高的均值如果中间组和两端组的偏离方向相反才确认需要二次项。df[parent_bin] pd.qcut(df[parent_mean], 5, labelsFalse) group_stats df.groupby(parent_bin)[height].agg([mean, std, count]) print(group_stats.round(2))如果mean列呈现明显的倒 U 或正 U 形状比如中间组均值比两端组高出一截那就加二次项。加的时候用PolynomialFeatures只对parent_mean做二次不要对所有特征都做否则特征数从 4 膨胀到 14小数据集上很容易过拟合。from sklearn.preprocessing import PolynomialFeatures from sklearn.pipeline import Pipeline poly_pipe Pipeline([ (poly, PolynomialFeatures(degree2, include_biasFalse, interaction_onlyFalse)), (scaler, StandardScaler()), (lr, LinearRegression()) ]) poly_pipe.fit(X_train, y_train) y_pred_poly poly_pipe.predict(X_test) print(二次多项式 R2:, round(r2_score(y_test, y_pred_poly), 4))注意这里PolynomialFeatures放在StandardScaler前面因为多项式展开后量纲会变先展开再标准化。include_biasFalse是因为后面LinearRegression会自己处理截距。如果二次项 R² 比线性只高 0.005 以内我一般就不加了因为解释成本上去了泛化收益不明显。3.2 Ridge 和 Lasso 在身高预测上的实际差异身高预测的特征数少正则化的收益不如高维场景明显但用来理解偏差-方差权衡很合适。Ridge 把系数往零压缩但不置零Lasso 会把不重要的特征系数直接压成零。from sklearn.linear_model import Ridge, Lasso alphas [0.01, 0.1, 1.0, 10.0] for a in alphas: ridge Ridge(alphaa) ridge.fit(X_train_s, y_train) print(fRidge alpha{a}, R2{r2_score(y_test, ridge.predict(X_test_s)):.4f}, coef{ridge.coef_.round(3)}) for a in alphas: lasso Lasso(alphaa, max_iter5000) lasso.fit(X_train_s, y_train) print(fLasso alpha{a}, R2{r2_score(y_test, lasso.predict(X_test_s)):.4f}, coef{lasso.coef_.round(3)})alpha越大正则越强。身高预测里weight这个特征系数通常最小Lasso 在alpha1.0左右就可能把它压成零。如果你发现 Lasso 把parent_mean也压没了说明alpha太大模型欠拟合。我一般会画一条alphavs 交叉验证误差的曲线来选而不是拍脑袋定。3.3 交叉验证选超参别用测试集调参很多人调alpha的时候直接看测试集 R²这是数据泄漏。正确做法是在训练集内部做 K 折交叉验证。from sklearn.model_selection import cross_val_score cv_scores [] for a in alphas: ridge Ridge(alphaa) scores cross_val_score(ridge, X_train_s, y_train, cv5, scoringneg_mean_squared_error) cv_scores.append(-scores.mean()) best_alpha alphas[np.argmin(cv_scores)] print(最佳 alpha:, best_alpha, CV MSE:, round(min(cv_scores), 4))cross_val_score的cv5表示 5 折scoringneg_mean_squared_error返回负 MSE取负号后越小越好。选出来的best_alpha再拿去在测试集上评估一次这个测试集 R² 才是可信的。身高预测数据量通常几百到几千条5 折足够数据量小于 200 条时用 10 折更稳。4. 身高预测里最容易翻车的五个地方4.1 现象训练集 R² 0.9测试集 R² 0.5原因特征里混入了和目标变量同期测量的变量比如用“当前体重”预测“当前身高”没问题但如果用“成年后体重”预测“青少年身高”就是时间泄漏。另一个常见原因是把parent_mean在划分训练测试集之前就做了全局标准化。解决所有预处理步骤放进Pipeline只在训练集上fit。检查每个特征在预测时刻是否真的可得。身高预测里age、sex、parent_mean都是出生或早期就确定的weight如果是同期测量也 OK但如果是未来值就不行。4.2 现象父母身高系数出现负值原因父亲身高和母亲身高同时作为特征多重共线性导致系数符号翻转。方差膨胀因子可能超过 10。解决合并成parent_mean或者用 PCA 降维。如果业务上必须保留两个特征改用 Ridge 回归alpha设 1.0 以上能缓解但不根治。我一般直接合并因为解释性更好。4.3 现象残差随预测值增大而增大原因异方差。身高预测里高个子群体的绝对误差天然比矮个子大因为生长激素、营养等未观测因素在高个子群体里方差更大。解决对目标变量取对数y_log np.log(y)预测完再np.exp回去。或者用加权最小二乘权重设成预测值的倒数。取对数更简单但要注意身高单位从 cm 变成 log(cm) 后系数解释要跟着变。4.4 现象Lasso 把重要特征系数压成零原因alpha太大或者特征没有标准化。Lasso 的惩罚项对系数尺度敏感如果parent_mean的量纲是 cm 而sex是 0/1不标准化的话惩罚力度完全不同。解决先StandardScaler再从alpha0.001开始网格搜索。如果标准化后还被压零检查这个特征和目标变量的 Pearson 相关系数小于 0.1 的话被压零也合理。4.5 现象交叉验证误差波动特别大原因数据量太小或者折数太多导致每折样本更少。身高预测如果只有 100 条数据5 折每折只有 20 条方差自然大。解决数据少于 200 条时用 3 折或留一法同时报告均值和标准差。如果标准差超过均值的 30%说明模型不稳定优先加数据而不是调模型。5. 把身高预测做成可解释的临床参考工具线性回归身高预测做到最后最有价值的不是 R² 多高而是每个系数能直接翻译成一句人话。比如parent_mean系数 0.72意思是父母平均身高每高 1 cm子女预期身高高 0.72 cm剩下的 0.28 cm 被均值回归吃掉了。这个解释在遗传咨询里比一个黑箱模型的预测值有用得多。我现在的习惯是跑完模型后强制自己写一段“系数解读”每个系数必须能用非技术语言说清楚方向和量级。如果某个系数解释不了要么是特征构造有问题要么是共线性在作怪。另外我会把预测区间也带上用残差标准差算一个 95% 区间比如预测 175 cm区间 169 到 181这比单点预测诚实得多。验证方法上除了交叉验证我还会做一个“极端值测试”手动构造几组边界输入比如父母平均身高 150 cm 和 190 cm看预测值是否落在合理范围。如果 190 cm 的父母预测出 210 cm 的子女说明模型在高值区外推失控这时候要么限制预测范围要么加二次项让曲线饱和。最后一个技巧把age和sex做交互项。青春期男生和女生的生长曲线差异很大age * sex这个交互项在 10 到 18 岁数据上通常显著。加进去之后 R² 能再涨 0.01 到 0.02而且残差图会更干净。代码就是在PolynomialFeatures里设interaction_onlyTrue或者手动乘一列。我自己踩过最深的坑是早期用全量数据标准化导致测试集 R² 虚高 0.08后来养成所有预处理进 Pipeline 的习惯再也没犯过。希望帮到你。本文还有配套的精品资源点击获取