资讯动态

R语言钻石价格预测:多元线性回归与数据清洗实战

发布时间:2026/10/27 2:13:56 来源:尧图企业网站定制
简介围绕ggplot2自带diamonds数据集展开的钻石价格分析实战文档面向已掌握R基础语法、希望系统练习数据清洗与多元线性回归的学习者。内容从54000余条钻石记录入手覆盖缺失值与重复值检查、箱线图异常值识别与剔除、价格分布直方图及核密度解读以及切工、颜色、净度、克拉重量等变量与价格关系的可视化探索并逐步过渡到变量类型转换、新指标构造和多元线性回归建模预测。资源包共1个docx文件约2.4MB以图文混排方式呈现分析过程、代码结果与结论解读便于对照复现和整理笔记。已有284人学习适合作为回归分析与机器学习入门的完整案例也可为课程作业、数据分析报告撰写提供参考。1. 54000 颗钻石的价格账本R 数据分析能问到什么程度钻石价格分析这件事用 R 语言做最顺手的入口是 ggplot2 包里自带的 diamonds 数据集53940 条记录、10 个变量carat、cut、color、clarity、depth、table、price 加 x、y、z 三个尺寸一条不缺。它干净、够大又保留着真实业务里的脏东西——尺寸为 0 的错行、被 IQR 判成离群的高价样本、ordered factor 默认跑偏的对比矩阵这些都会在建模时一个接一个冒出来。这份分析要回答两个问题哪些因素真正在推动价格以及给定一颗钻石的 4C 参数能把价格估到什么精度。适合刚学完线性回归想找完整项目落地的人也适合要给业务方解释「凭什么这颗贵」的人。2. 数据体检缺失值、重复值、异常值三道关怎么过拿到一份 5 万多行的数据集很多人第一反应是直接lm()一把梭。我一般会先花二十分钟做体检因为回归分析里最难补的后悔药就是一开始没看清数据形状。这一章把加载、缺失、重复、异常值、分布形态拆成四步每步都给你能直接抄的代码和判断标准。2.1 装包与加载三个包就够跑通全流程这套流程不需要重型依赖。ggplot2 提供数据本身和作图能力dplyr 负责分组汇总mice 用来把缺失值模式画成图——即便最后确认没有缺失值这张图也值得出一遍它是写进报告里的证据。# ggplot2 既是数据源也是绘图主力mice 只用来做缺失模式可视化 install.packages(c(ggplot2, dplyr, mice, car)) library(ggplot2) library(dplyr) library(mice) # diamonds 是 ggplot2 的内置数据集data() 加载后直接可用 data(diamonds, package ggplot2) dim(diamonds) # 期望 53940 行 10 列 str(diamonds) # 看每一列的类型三个 factor七个 numericdim()返回的行列数是第一道校验如果数字对不上说明你加载的不是原版数据集后面所有结论都要打问号。str()的重点是看类型cut、color、clarity应该是 Factor其余七个是 num。如果carat或price显示成 chr多半是被某次读入操作污染了得先as.numeric()转回来再往下走。2.2 缺失值与重复值先证明它干净再动手缺失值检查不要只写is.na()就完事得把结果摊开看。逐列求和能告诉你哪一列有洞md.pattern()能告诉你洞和洞之间是不是同时出现——这两件事在后续填补策略上完全不同。# 逐列统计缺失数量输出应全为 0 colSums(is.na(diamonds)) # 缺失模式矩阵图行是观测列是变量红色方块代表缺失 md.pattern(diamonds, plot TRUE) # 重复行检查按整行完全一致来判定 sum(duplicated(diamonds))colSums()的输出全 0说明没有缺失md.pattern()会把每一列画成一条左侧数字是该模式下的观测数右侧是该模式含缺失的变量个数。这套数据里通常只会出现一行「53940 0」即所有行都完整。duplicated()返回 FALSE 的布尔向量求和为 0 就代表没有完全重复的记录。这里有个容易踩的细节duplicated()判的是整行完全相同而钻石数据里有大量「carat 价格参数都接近但不完全一样」的记录它们不是重复是不同实物。不要因为看着像就拿distinct()去重那是把真实样本删掉。2.3 异常值1.5 倍 IQR 不能直接套在价格上箱线图那套Q1 - 1.5*IQR到Q3 1.5*IQR的规则教科书上讲得很顺套到这份数据上会翻车。先看代码再说为什么。# 通用 IQR 边界函数k 默认 1.5可调成 3 做保守剔除 iqr_bound - function(v, k 1.5) { q - quantile(v, c(0.25, 0.75), na.rm TRUE) r - q[2] - q[1] c(low q[1] - k * r, high q[2] k * r) } # 分别看价格和三个尺寸的边界 sapply(diamonds[, c(price, x, y, z)], iqr_bound) # 用 boxplot.stats 直接捞出被判为离群的价格 out_price - boxplot.stats(diamonds$price)$out length(out_price) range(out_price)sapply()会把四组上下界打成矩阵boxplot.stats()$out直接返回被判定为离群的具体数值。跑完你会发现价格这一列被判出局的数量以千计而它们的下限不过一千多美元——这些恰恰是最贵的、最有商业价值的那批钻石。所以在价格上按 IQR 剔除等于亲手把模型的右尾砍掉训练出来的模型在 1.5 克拉以上会系统性低估。我的做法是价格一律不按 IQR 删改用对数变换把右偏压平真正要删的是物理上不可能的尺寸记录。# 尺寸为 0 的记录物理上不存在且会让 depth 计算出 Inf bad_dim - subset(diamonds, x 0 | y 0 | z 0) nrow(bad_dim) summary(bad_dim[, c(carat, price, x, y, z)]) # 只保留三个尺寸都大于 0 的记录 df - subset(diamonds, x 0 y 0 z 0) nrow(df)bad_dim通常只有个位数行但破坏力不小depth的定义是2*z/(xy)*100分母为 0 时直接产生 Inf 或 NaN回归会整段报错。用subset()三条件同时过滤删完再做一次nrow()确认剩余量级仍在 53900 以上说明只动了该动的部分。提示删异常值前先想清楚「这条记录是录入错误还是真实的极端业务样本」。前者删后者留。2.4 价格分布右偏、双峰和 log 变换的取舍价格的直方图是整份分析里信息密度最高的一张图。它同时告诉你三件事分布形状、峰值位置、以及该不该做变换。ggplot(df, aes(x price)) geom_histogram(aes(y after_stat(density)), bins 60, fill grey75, colour white) geom_density(colour firebrick, linewidth 0.9) geom_vline(xintercept mean(df$price), linetype dashed) geom_vline(xintercept median(df$price), linetype dotted) labs(x 价格美元, y 密度)after_stat(density)是新版 ggplot2 的写法老教程里的..density..会弹弃用警告虽然还能跑但别在新代码里用。bins 60是经验值5 万多条数据给到 60 组左右既能看出峰值又不至于碎成噪声。两条竖线一条均值一条中位数两者差距越大右偏越重。跑完你会看到均值明显在中位数右侧分布长尾拖到一万多美元。这个形状直接决定了后面两件事第一残差正态性假设很难满足QQ 图尾部一定翘第二如果做对数变换log(price)会把这个形状压得接近钟形模型残差表现会好很多。但 log 变换不是免费的。系数从「每增加一单位 X价格变动多少美元」变成「价格变动百分之多少」给业务方讲的时候要多绕一道弯并且反变换回原尺度时exp()之后的预测区间并不对称直接取中位数会对不上均值。我的习惯是两套模型都建用原始尺度的模型讲系数用对数尺度的模型做预测和区间最后在测试集上用同一个指标比。到这里数据体检的四道关就过完了。剩下的 53900 多条记录类型干净、无缺失、尺寸合法可以进入变量工程。3. 变量工程与可视化把 cut、color、clarity 变成模型吃得下的形状回归模型只认识数值不认「理想切工」这种字面值。把三个定性变量编码进模型这一步看着简单实际藏着这份分析里最容易翻车的一个坑。这一章讲清楚因子水平的顺序怎么定、哑变量的参考组怎么选、分组对比图怎么看以及怎么用非参数检验给「不同切工价格有显著差异」这句话提供证据。3.1 有序因子排序和建模是两回事三个定性变量都有天然顺序。切工从 Fair 到 Ideal 逐级变好颜色从 J 到 D 逐级变白净度从 I1 到 IF 逐级变纯。用factor()加上levels和ordered TRUE能把顺序固化下来图表的横轴就会按业务逻辑排不会按字母序乱跳。df - df %% mutate( cut factor(cut, levels c(Fair, Good, Very Good, Premium, Ideal), ordered TRUE), color factor(color, levels c(J, I, H, G, F, E, D), ordered TRUE), clarity factor(clarity, levels c(I1, SI2, SI1, VS2, VS1, VVS2, VVS1, IF), ordered TRUE) ) # 看一眼有序因子在建模时会被编码成什么样 contrasts(df$cut)最后一行contrasts()是这套流程里我建议每个人都跑一次的检查。有序因子默认使用多项式对比contr.poly输出会是.L、.Q、.C这样的列名——线性、二次、三次趋势。这意味着模型给你的不是「Ideal 比 Fair 贵多少」而是「切工等级每升一级的线性趋势贡献」。对做预测影响不大对讲结论就是灾难业务方问「Premium 和 Ideal 差多少」你没法从.L的系数里直接读出来。所以我的处理是有序因子只用于排序和画图建模前统一转回无序因子让 R 按 treatment 对比生成哑变量。# 保留 levels 顺序但取消 ordered 属性让 lm 生成哑变量 df_model - df %% mutate(across(c(cut, color, clarity), ~ factor(.x, levels levels(.x), ordered FALSE))) contrasts(df_model$cut) # 想以 Ideal 为参照组显式 relevel否则默认拿第一水平当基准 df_model - df_model %% mutate(cut relevel(cut, ref Ideal))across()配合levels(.x)的写法能保住业务顺序的同时换掉对比矩阵。relevel()那一步常被忽略R 默认拿第一水平当参考组也就是 Fair。结果就是所有切工系数都在讲「比 Fair 贵多少」而 Fair 在全量数据里只占 3% 左右这个基准既不稳也不好讲。改成 Ideal 之后系数直接变成「比理想切工便宜多少」读起来顺得多。3.2 分组对比图小提琴图比箱线图多告诉你什么价格和切工的关系有个反直觉现象第一次做这份分析的人几乎都会被绊一下理想切工的钻石价格中位数反而低于优质切工。ggplot(df_model, aes(x cut, y price, fill cut)) geom_violin(trim FALSE, alpha 0.55) geom_boxplot(width 0.12, outlier.shape NA, fill white) scale_y_log10() labs(x 切工, y 价格美元对数轴) theme(legend.position none)trim FALSE让小提琴的两端保留完整尾部能看出分布厚度scale_y_log10()把右偏压平否则箱子全挤在底部看不清outlier.shape NA关掉箱线图自带的离群点避免和小提琴的点重复。图上能读出的信息比箱线图多一层不只是中位数高低还有各组的分布形状。理想切工那组的分布明显在低价比其他组更厚说明大量小克拉钻石被切成了理想切工——这本身是合理的商业行为小石头切坏了损失小切好了溢价高。要解释「中位数反而低」这个矛盾直接把平均克拉数拉出来对照df_model %% group_by(cut) %% summarise( n n(), median_price median(price), mean_carat round(mean(carat), 3), mean_x round(mean(x), 3) )跑完会看到 Ideal 组的mean_carat是最小的。这就把因果链说清楚了不是理想切工不值钱是理想切工这批样本整体克拉数偏小。这是典型的混杂变量效应也是回归分析存在的理由——单变量对比会骗人控制住 carat 之后切工的真实溢价才会显现。同样的画法换到 color 和 clarity 上方向就符合直觉了颜色从 J 到 D价格中位数稳定上移净度从 I1 到 IF 也整体上移但到了 VVS1 之后曲线会变平说明顶级净度的边际溢价开始递减。这三张图摆在一起就是后面建模时「为什么 clarity 要保留、为什么要加交互项」的直观依据。3.3 显著性检验为什么这里不该用方差分析「不同切工的价格有显著差异」这句话得有统计证据。教科书默认走 ANOVA但 ANOVA 的两个前提在这份数据上都不成立正态性和方差齐性。# 正态性样本量 5 万时 shapiro 几乎必然拒绝参考价值有限 tapply(df_model$price, df_model$cut, function(v) shapiro.test(v[1:5000])$p.value) # 方差齐性 bartlett.test(price ~ cut, data df_model)tapply()分组跑正态性检验注意每组只取前 5000 个——shapiro.test()的样本上限就是 5000超了直接报错。结果多半是 p 值小到打印成 0但这里要清醒样本量这么大任何微小偏离都会被判定为显著正态性检验在这个规模下几乎失去判别力。真正该看的是 QQ 图和偏度。bartlett.test()对价格做方差齐性检验右偏分布下各组方差差异通常也是显著的。两个前提一破F 检验的 p 值就不可信了。换成不依赖分布假设的 Kruskal-Walliskruskal.test(price ~ cut, data df_model) # 显著之后做两两比较bonferroni 控制多重比较带来的假阳性 pairwise.wilcox.test(df_model$price, df_model$cut, p.adjust.method bonferroni)kruskal.test()是秩和检验的多组版本只比较中位数位置对分布形状不敏感。pairwise.wilcox.test()会输出一张下三角 p 值矩阵p.adjust.method参数负责多重比较校正——五组两两比要跑 10 次不校正的话假阳性率会从 5% 膨胀到 40% 左右。bonferroni 最保守也可以换BH控制 FDR样本量大时 BH 更实用。这两步跑完你手上有了一张完整的证据链切工、颜色、净度三组变量组间差异显著且差异方向与业务认知一致。这才是后面敢把三个变量都塞进回归模型的前提。3.4 共线性初筛depth 其实是 x、y、z 的派生变量建模前还有一件事必须做看数值变量之间的相关结构。这份数据集里埋了一个恒等式不注意的话会让模型系数彻底没法解释。num_vars - c(carat, x, y, z, depth, table, price) round(cor(df_model[, num_vars]), 3) # 验证 depth 的定义depth 2*z/(xy)*100 df_model %% summarise(max_err max(abs(depth - 100 * 2 * z / (x y))))cor()输出的矩阵里carat 和 x 的相关系数通常在 0.97 以上x 和 y 也在 0.95 以上——这是物理必然克拉数就是体积换算出来的。第二段代码验证的是depth的公式恒等性max_err会非常接近 0说明 depth 完全由 x、y、z 决定没有独立信息。这个发现直接影响建模策略carat、x、y、z、depth 五个变量同时进模型等于把同一份信息喂了五遍VIF 会爆表系数符号甚至可能反过来。我的取舍是主模型用carat cut color clarity depth table把 x、y、z 拿掉如果确实想利用尺寸信息就单独建一个含尺寸的版本做对比两个模型的 R² 和残差摆在一起看别混着解释。到这里变量工程收尾进入真正的建模环节。4. 多元线性回归从基准模型到交互项的四步迭代建模这部分最怕两种写法一种是一上来就price ~ .全变量梭哈跑出个 R² 就收工另一种是死磕 R² 数字反复调变量直到 0.99 还觉得自己赢了。这一章按「基准 → 加交互 → 逐步回归 → 诊断 → 对数模型」的顺序推每一步都说清楚为什么这么走以及哪一步该停下来。4.1 基准模型R² 0.88 到底算不算好先建一个不加任何交互项的模型只用 4C 加 depth、table。fit_base - lm(price ~ carat cut color clarity depth table, data df_model) summary(fit_base) # 把三个尺寸变量也放进去看 R² 能提多少 fit_size - lm(price ~ carat cut color clarity depth table x y z, data df_model) summary(fit_size) AIC(fit_base, fit_size)summary()输出里重点看三处Estimate列的系数符号和量级、Pr(|t|)列的显著性、以及底部的Multiple R-squared和Adjusted R-squared。第一个模型的 R² 大致落在 0.85 上下加上 x、y、z 之后能到 0.88 附近——这和原文记录的 0.8798 是一个量级。数字本身说明不了太多。R² 的含义是「价格的总变异里被这组变量解释掉的比例」0.88 意味着还剩 12% 的变异没被覆盖。放在钻石这个场景里那 12% 里装的是品牌溢价、证书机构、销售渠道、切割比例的美学评价——这些变量这份数据集里根本没有所以 0.88 基本就是这套变量组合的天花板再往上抠就是过拟合。判断模型好不好我的标准不是 R² 的绝对值而是三件事残差有没有明显结构4.3 节展开、调整 R² 和 R² 差多少差太多说明变量冗余、以及交叉验证后的误差是否稳定第 6 章展开。AIC()那行是给两个嵌套模型做比较用的值越小越好但差值小于 2 时基本可以认为没有实质差别。Adjusted R-squared这个指标值得单独说一句它会对变量个数做惩罚所以在往模型里塞变量时Adj R² 不升反降就是个明确信号——新加的变量没带来信息只带来了复杂度。4.2 交互项与逐步回归carat 的斜率不是常数基准模型有个隐含假设克拉数每增加一单位价格增加固定的美元数。这个假设在钻石上明显不成立——1 克拉到 1.1 克拉的涨价幅度和 3 克拉到 3.1 克拉完全不是一个量级。# 三个交互项克拉数对价格的影响随切工/颜色/净度而变 fit_int - lm(price ~ carat * cut carat * color carat * clarity depth table x, data df_model) # 向后逐步回归按 AIC 准则逐个剔除贡献不足的项 fit_step - step(fit_int, direction backward, trace 0) summary(fit_step) AIC(fit_int, fit_step)carat * cut这种写法等价于carat cut carat:cut即主效应加交互项一起进不要在公式里只写carat:cut——那样会漏掉主效应模型设定就不完整了。三个交互项同时进模型参数会明显变多这也是为什么需要有下一步。step()的三个参数各有讲究direction backward从全模型开始逐步删项比 forward 更稳因为起点就包含了所有候选trace 0关掉每一步的打印否则控制台会刷出几十行如果不设scope它只在当前模型的项里做减法不会凭空造出新交互。跑完拿AIC()对比两个模型值小的一方胜出。这里要泼一盆冷水AIC 逐步回归不是万能钥匙。它做的是单路径贪心搜索换一批数据、甚至换个随机种子如果涉及抽样选出来的变量集可能就变了。所以我一般把step()的结果当参考不当终审——真正拍板的是交叉验证的误差以及模型在业务上说不说得通。原文的做法是三个交互项全部保留、所有变量都被选入。这在样本量 5 万多的前提下可以接受样本量撑得住参数膨胀。如果你手上的数据只有几千行同样的操作大概率过拟合该砍就砍。4.3 回归诊断四张图分别该看什么模型跑完不看诊断图等于开车不看后视镜。plot()一次出四张每张对应一个假设。par(mfrow c(2, 2), mar c(4, 4, 2, 1)) plot(fit_step, which 1:4) par(mfrow c(1, 1))par(mfrow c(2, 2))把画布切成两行两列which 1:4指定输出哪四张。mar参数调边距默认边距下轴标签容易被裁掉。跑完记得把画布还原成 1×1否则后面所有图都会被挤在小格子里——这个坑我踩过不止一次。第一张 Residuals vs Fitted 看线性假设和方差齐性。理想状态是点云均匀铺在 0 线两侧没有喇叭口、没有弯曲趋势。价格这种右偏变量这里通常会看到一个明显的漏斗——残差随拟合值增大而扩散。漏斗形状说明存在异方差普通最小二乘的置信区间会偏窄系数显著性被高估。第二张 Normal Q-Q 看残差正态性。点贴着对角线走是理想两端翘起来是厚尾。5 万条数据下尾部翘几乎是必然的别指望它完全贴合。真正会伤到推断的是中间段也明显偏离那说明分布形状不是「近似正态」而是「压根不是正态」。第三张 Scale-Location 是残差绝对值的平方根对拟合值作图比第一张更容易看出方差的趋势线是否水平。第四张 Residuals vs Leverage 看杠杆值和 Cook 距离。右上角和右下角的点是有影响力的观测——它们本身残差不一定大但杠杆高删掉一个能让整条回归线转动。虚线标出的 Cook 距离等值线之外的点值得逐条回看尤其是价格极高、克拉数极大的那几条。除了四张图多重共线性还得单独查library(car) vif(fit_step)vif()输出每个变量的方差膨胀因子经验阈值是 10超过就说明该变量能被其余变量线性组合出来。含 x、y、z 的模型里这几个变量和 carat 的 VIF 会明显偏高这也是 3.4 节建议主模型去掉它们的理由。4.4 对数模型与预测反变换那一步最容易错价格右偏这么重对数模型往往比原始尺度表现更好。fit_log - lm(log(price) ~ log(carat) cut color clarity depth table, data df_model) summary(fit_log)这里有两个变换因变量取log(price)自变量取log(carat)。前者把右偏压平让残差更接近正态后者把克拉数和价格之间的幂律关系拉直——钻石价格大致正比于克拉数的某个幂次取对数之后斜率就是这个幂次指数直接可读。系数含义也随之变化log(carat)的系数是弹性即克拉数每增加 1%价格平均变化百分之几。预测的时候predict()有三个参数必须搞明白newd - data.frame( carat 0.9, cut factor(Premium, levels levels(df_model$cut)), color factor(G, levels levels(df_model$color)), clarity factor(VS2, levels levels(df_model$clarity)), depth 61.5, table 58 ) # 对数尺度上的预测与区间 pred_log - predict(fit_log, newdata newd, interval prediction, level 0.95) pred_log # 反变换回美元尺度 exp(pred_log)newdata必须是数据框列名和训练数据完全一致。interval prediction给的是预测区间覆盖单个新观测不是置信区间覆盖期望值前者一定比后者宽报告里给哪个取决于你要回答的问题——估算某颗具体钻石的合理成交价用 prediction。level控制置信水平默认 0.95做价格区间报价时可以放到 0.90 收窄一些。最后exp()那一步是整套流程里最容易翻车的地方。对数尺度上的预测区间是左右对称的exp()之后变成右偏中点不再是中位数。严格意义上要从对数正态反变换得到无偏的均值预测得乘上一个exp(sigma^2/2)的修正因子。做点估计时这个差别可能只有几百美元但做区间报价时不能忽略。5. 踩坑排查这份分析里最容易翻车的五个地方这份数据集因为太常见教程满天飞但真正跑起来会遇到的坑教程里基本不写。下面五条是我在不同场合反复见到的翻车现场每条按「现象 → 原因 → 解决」写遇到对得上的直接抄解决方案。5.1 现象summary 里冒出一堆 cut.L、cut.Q跑完summary()发现切工的系数不是四行而是cut.L、cut.Q、cut.C、cut^4这种名字系数值还特别大。原因是factor(..., ordered TRUE)默认使用多项式对比矩阵模型拟合的是「切工等级的线性趋势、二次趋势、三次趋势」而不是各水平相对基准的偏移量。这是统计上合法的做法但业务上没法解释。解决办法是在建模前把 ordered 属性去掉或者直接显式指定对比方式# 方案一取消 ordered保留 levels 顺序 df_model - df %% mutate(cut factor(cut, levels levels(cut), ordered FALSE)) # 方案二显式指定 treatment 对比 contrasts(df_model$cut) - contr.treatment(n 5, base 5)方案一更干净推荐。方案二里base 5表示以第五个水平Ideal为参照等价于relevel()。5.2 现象剔完异常值R² 漂亮了但大克拉钻石全被低估按 1.5 倍 IQR 把 price 的离群点删掉模型 R² 从 0.86 涨到 0.92看着很美。但拿几颗 2 克拉以上的真实报价去测预测值普遍低几百到上千美元。原因是价格分布的右尾不是噪声是信号。IQR 规则假设数据近似对称而价格是典型的重尾右偏分布被判为「离群」的那些点恰恰承载了高端市场的信息。删掉它们模型就只在低中价位段学过外推到高端必然偏低。解决办法是价格不做 IQR 剔除改用两种手段处理尾部一是对 price 取对数把尾部压缩二是改用分位数回归quantreg::rq()直接对中位数或 90 分位数建模不受极端值拉扯。如果确实要删只删尺寸为 0 或明显录入错误的记录。5.3 现象模型报错或者出现负价格预测lm()直接报NA/NaN/Inf in y或者模型能跑通但预测出来的价格是负数。原因是数据里有 x、y、z 为 0 的记录。depth的定义是2*z/(xy)*100分母为 0 时产生 Inf如果某行 z 为 0 而 x、y 正常depth 算出 0这个值在物理上不可能会把 depth 的系数往错误方向拉。解决办法是先过滤再做任何计算df - subset(diamonds, x 0 y 0 z 0) # 顺手验证一遍 depth 的恒等式误差应该接近 0 max(abs(df$depth - 100 * 2 * df$z / (df$x df$y)))第二行是个便宜又好用的自检。如果误差超过 0.1说明 depth 列被改过或者单位不一致得回头查数据来源。5.4 现象predict 报错「factor has new levels」训练集跑得好好的拿新数据predict()直接抛错提示因子出现了新水平。原因是newdata里的因子水平和训练时不一致。哪怕字面值一样只要水平集合不同少一个水平、或者顺序不同R 就认为这是另一个因子。手写data.frame(cut Ideal)尤其容易出这个问题因为默认会按字母序生成水平。解决办法是把训练集的 levels 显式传给新数据newd - data.frame( cut factor(Ideal, levels levels(df_model$cut)), color factor(G, levels levels(df_model$color)) )养成这个习惯所有进predict()的因子都走一遍factor(x, levels levels(train$x))能省掉大量排查时间。5.5 现象样本内 R² 高得离谱上线后误差上千美元模型在训练集上 R² 达到 0.95 以上残差标准差只有几百信心满满拿去预测新钻石误差动辄上千。原因通常是两个叠加一是没有留出测试集所有评估都在样本内做二是模型里塞了 x、y、z 这类和 carat 高度共线的变量它们在训练集上能靠微小的数值差异「记住」样本泛化能力极差。样本内 R² 衡量的是拟合能力不是预测能力这两件事在变量冗余时差距会被放大。解决办法是划分训练测试集并且评估指标换到原始尺度上算set.seed(2024) idx - sample(nrow(df_model), size floor(0.7 * nrow(df_model))) train - df_model[idx, ] test - df_model[-idx, ] fit_tr - lm(log(price) ~ log(carat) cut color clarity depth table, data train) pred - exp(predict(fit_tr, newdata test)) rmse - sqrt(mean((pred - test$price)^2)) mape - mean(abs(pred - test$price) / test$price)注意exp()的位置模型在 log 尺度上训练预测出来必须反变换回美元再和真实价格比。如果忘了反变换RMSE 会小得离谱因为你在拿 log 单位和美元比。6. 进阶技巧一克拉关口、交叉验证和误差的真实含义这份数据里有个现象第一次看到会以为是数据错误把 carat 在 1.0 附近的点单独画出来价格在 1.00 处有一个明显的跳变0.99 克拉和 1.01 克拉之间能差出几百美元。ggplot(subset(df_model, carat 0.85 carat 1.15), aes(x carat, y price)) geom_point(alpha 0.12, size 0.7) geom_vline(xintercept 1, linetype dashed, colour firebrick) scale_y_log10() labs(x 克拉数, y 价格美元对数轴)这不是数据问题是消费心理一克拉是个整数关口跨过去就是另一个价格带。可惜线性模型捕捉不到这个跳变carat的系数是一个平滑的斜率1.00 和 1.01 在模型眼里几乎没有区别。想把这个效应吃进模型常见做法是加一个「是否跨过一克拉」的指示变量或者按克拉数分箱建分段模型df_model - df_model %% mutate( carat_bin cut(carat, breaks c(0, 0.5, 1, 1.5, 2, 5), include.lowest TRUE), over_one as.integer(carat 1) ) fit_seg - lm(log(price) ~ carat_bin cut color clarity depth table, data df_model) summary(fit_seg)cut()在这里是 base R 的分箱函数和 dplyr 的cut变量名撞了写的时候要留意作用域——这是 R 里一个经典的命名陷阱dplyr::cut并不存在但变量名cut会遮蔽函数。稳妥写法是base::cut(carat, ...)。over_one这个指示变量的系数直接读出来就是「跨过一克拉的平均溢价比例」比从平滑斜率里反推直观得多。carat_bin版本则更彻底每个区间一个截距代价是参数变多、边界处不连续。交叉验证这块我一般跑 5 折比单次划分稳library(caret) set.seed(2024) ctrl - trainControl(method cv, number 5) fit_cv - train( log(price) ~ log(carat) cut color clarity depth table, data df_model, method lm, trControl ctrl ) fit_cv$resultstrainControl(method cv, number 5)指定 5 折交叉验证method lm表示内部还是线性回归不做正则化。fit_cv$results会给出 RMSE、R²、MAE 在不同折上的均值和标准差——标准差这一项特别值得看它反映模型在不同数据子集上的稳定性。标准差大的模型哪怕均值好看上线也得谨慎。最后说一个我踩过的坑评估指标一定要在原尺度上算。在对数尺度上算出来的 RMSE单位是「log 美元」数值看着很小很漂亮但没有任何业务含义也没法和实际报价误差对照。我现在的习惯是所有汇报给别人的误差数字一律先exp()回到美元再用 RMSE 和 MAPE 两个指标交叉看——RMSE 会被高价钻石主导MAPE 对低价钻石更敏感两个一起看才知道模型在哪个价位段表现差。从那以后我每次跑这类价格回归都强制走一遍「划分训练测试 → log 尺度训练 → 反变换 → 原尺度算误差」的流程中间任何一步偷懒都会让最后的结论失真。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑