简介:围绕R语言展开的钻石价格分析文档,面向具备基础R操作能力、希望练习数据清洗、可视化与回归建模的数据分析学习者。内容依托ggplot2自带的diamonds数据集,覆盖缺失值与重复值检查、异常值识别剔除、价格分布直方图、箱线图与小提琴图、变量类型转换及新变量构造,并对价格右偏分布与低价区间集中等现象作出解读;同时建立线性回归模型,探讨克拉重量、切工、颜色、净度等因素对价格的影响与预测思路。压缩包共1个docx文件,约2.4MB,以图文结合方式完整呈现数据读取、描述统计、可视化探索、模型建立与结果解释等环节,便于对照复现、课程汇报和整理分析报告。已有284人学习下载,适合作为R语言课程作业、数据分析入门案例或回归分析练习参考。
1. R 自带的 diamonds 数据集:五万四千颗钻石的价格,十个变量能解释多少
钻石定价这件事,玄学成分不小——同样一克拉,柜台上能差出三倍价格。ggplot2里躺着一份diamonds数据集,53940 条记录、10 个变量,克拉重量、切工、颜色、净度、深度百分比、台宽连同三维尺寸和成交价都记在里面,做 R 数据分析时它几乎是回归分析的默认练手素材,也是很多人接触机器学习的第一份真实分布数据。
这份基于 R 语言的钻石价格分析资源,围绕它走了一遍完整链路:数据读取与体检、缺失值重复值异常值三连排查、定性变量因子化、一元与多元线性回归、交互项设计与向后逐步回归、显著性检验和残差诊断。适合刚上手 R、想拿一份五万条量级的数据把回归流程从头走通的人;也适合已经把模型跑出来、但说不清系数该怎么解释、残差图该怎么看的熟手。
2. 数据读取与体检:从 data() 到箱线图,先看清再动手
数据体检这一步,我见过太多人直接跳过。拿到diamonds就lm(price ~ ., data = diamonds),跑出来 R² 零点八几,感觉挺好,然后系数解释一塌糊涂。问题往往不在模型,在于读数据的时候没看清里面藏着什么。
2.1 装包与加载:三行代码拿到数据源
diamonds不是独立数据集,它挂在ggplot2包下面。加载ggplot2之后用data()显式取一次,比依赖副作用更稳,尤其是脚本里同时 library 了好几个包、存在同名对象覆盖风险的时候。
# 首次运行才需要,装过就跳过这一行 install.packages(c("ggplot2", "dplyr", "mice", "car")) library(ggplot2) library(dplyr) # 显式加载内置数据集,避免依赖 package attach 的副作用 data(diamonds, package = "ggplot2") # 行数、列数、字段名 dim(diamonds) names(diamonds)dim()返回53940 10,这个数字要记住,后面所有清洗操作都要能解释行数怎么变的。names()输出的十个字段里,cut、color、clarity是名义变量,carat、depth、table、price、x、y、z是数值变量。字段含义和取值范围如下表,建模前建议对着表过一遍,因为后面判断异常值时全靠它。
| 字段 | 含义 | 典型范围 | 类型 |
|---|---|---|---|
| carat | 克拉重量 | 0.20 – 5.01 | 连续 |
| cut | 切工等级 | Fair / Good / Very Good / Premium / Ideal | 有序分类 |
| color | 颜色等级 | J(差)→ D(好) | 有序分类 |
| clarity | 净度等级 | I1 → IF | 有序分类 |
| depth | 总深度百分比 = 2z/(x+y)×100 | 43 – 79 | 连续 |
| table | 台宽百分比 | 43 – 95 | 连续 |
| price | 成交价(美元) | 326 – 18823 | 连续 |
| x / y / z | 长 / 宽 / 深(毫米) | 0 – 10.74 / 0 – 58.9 / 0 – 31.8 | 连续 |
2.2 结构体检:str、缺失值与重复值三连
str()给出字段类型和前后几条样例,比head()信息密度高。紧接着做缺失值排查,colSums(is.na())是最直接的一行,mice::md.pattern()会把缺失模式画成矩阵图,字段多了以后一眼能看出是不是成块缺失。
str(diamonds) # 各列缺失数量 colSums(is.na(diamonds)) # 缺失模式可视化:0 表示缺失,1 表示有值 library(mice) md.pattern(diamonds, rotate.names = TRUE)这份数据的结果是干净的:十列全零缺失。很多人看到这里就收工了,其实diamonds的"缺失"是隐性的——x、y、z里存在取值为 0 的记录,长度为零的钻石在物理上不存在,这属于用 0 冒充缺失值。这一条后面单独讲。
重复值排查同样简单,duplicated()返回逻辑向量,sum()汇总。要注意这里判的是整行完全一致,不是主键重复——钻石没有唯一编号,两颗参数完全相同的钻石在数据里就会被判成重复,处理策略要自己定。
# 完全重复的行数 sum(duplicated(diamonds)) # 去重后保留原行名,方便回溯 diamonds_uniq <- diamonds[!duplicated(diamonds), ] nrow(diamonds_uniq)去重之后行数会掉一点,具体掉多少取决于你判重的字段范围。我的习惯是先按全字段去重,如果掉得异常多(比如超过 1%),再回头逐字段核对是不是某几列精度截断导致的伪重复。
2.3 异常值判定:1.5 倍 IQR 只是第一道筛子
price是右偏严重的分布,箱线图上须外点一大片。标准做法是算四分位距,上下各留 1.5 倍 IQR 作为围栏。这套规则对近似对称的分布很好用,对价格这种长尾分布就会误杀——高价位本来就该有长尾,那不算错误,那是市场结构。
# 画 price 的箱线图,先看清尾巴有多长 boxplot(diamonds_uniq$price, horizontal = TRUE, main = "price 箱线图", xlab = "美元") q1 <- quantile(diamonds_uniq$price, 0.25) q3 <- quantile(diamonds_uniq$price, 0.75) iqr <- q3 - q1 lower <- q1 - 1.5 * iqr # 约 -1262,负数无意义 upper <- q3 + 1.5 * iqr # 约 11894 c(lower = lower, upper = upper)算出来的下围栏是负数,说明价格分布压根不对称,机械套 1.5 倍 IQR 会把上千条真实高价记录删掉。所以我在这个项目里把异常值拆成两类处理:物理上不可能的记录直接删,统计上的长尾保留或者做对数变换。物理判断就三条——y、z不能为 0,y不能超过 20 毫米,z不能超过 20 毫米。数据集里y最大 58.9、z最大 31.8,明显是录入错误,这种才是真正的脏数据。
diamonds_clean <- diamonds_uniq %>% filter(y > 0, z > 0, x > 0) %>% # 尺寸为 0 的物理上不存在 filter(y < 20, z < 20, x < 20) %>% # 超过 20mm 的属于录入错误 filter(depth > 40, depth < 80) # 总深度百分比超出常识区间 nrow(diamonds_clean)每一步 filter 后面我都会nrow()打一次,确认删了多少条、占比多少。删掉超过 3% 就要停下来重新想,这是血泪经验:有一次顺手加了个price < 15000的条件,结果把整条高价长尾砍了,模型 R² 从 0.88 涨到 0.94,看着漂亮,实际上模型再也不会预测超过一万五的钻石。
2.4 清洗后的基线统计
清洗完先把summary()打出来,把清洗前后的均值中位数对一遍。这一步能暴露不少问题:如果清洗后均值掉得厉害,说明删掉的主要是高价位;如果中位数几乎没变,说明删除是均匀的。
summary(diamonds_clean[, c("carat", "depth", "table", "price")]) # 清洗前后对比 rbind( 清洗前 = c(n = nrow(diamonds_uniq), mean_price = mean(diamonds_uniq$price), median_price = median(diamonds_uniq$price)), 清洗后 = c(n = nrow(diamonds_clean), mean_price = mean(diamonds_clean$price), median_price = median(diamonds_clean$price)) )这份数据清洗后的均值在 3900 美元上下,中位数 2400 美元左右,均值明显高于中位数,右偏特性保留下来了——这是个好信号,说明没有把长尾删干净。这份"清洗后仍右偏"的基线,是后面判断模型残差是否正常的锚点。
3. 变量编码与特征工程:有序因子、派生比率和可视化取舍
模型能不能解释,七成看变量编码。cut、color、clarity这三个字段在原始数据里是字符型,直接扔进lm()会被当成三水平以上的分类变量处理,R 默认按字母序排水平,结果就是Fair变成基准组,所有系数都是相对"一般切工"而言的——这个基准在业务上毫无意义。
3.1 有序因子:level 顺序直接决定系数符号
cut的五档从 Fair 到 Ideal 是有序的,color从 J 到 D、clarity从 I1 到 IF 也是。用factor(levels = ...)显式指定顺序,再考虑ordered = TRUE。区别在于:无序因子进模型会生成 k-1 个哑变量,有序因子默认生成线性对比(.L)、二次(.Q)这类正交多项式项,解释起来更接近"每升一级价格变化多少"。
diamonds_clean <- diamonds_clean %>% mutate( # 切工从差到好 cut = factor(cut, levels = c("Fair", "Good", "Very Good", "Premium", "Ideal"), ordered = TRUE), # 颜色 J 最差、D 最好,注意方向别写反 color = factor(color, levels = c("J", "I", "H", "G", "F", "E", "D"), ordered = TRUE), # 净度 I1 最差、IF 最好 clarity = factor(clarity, levels = c("I1", "SI2", "SI1", "VS2", "VS1", "VVS2", "VVS1", "IF"), ordered = TRUE) ) # 检查水平顺序是否生效 levels(diamonds_clean$cut) levels(diamonds_clean$color)这里最容易翻车的是color。数据集的取值是 D 到 J,D 最好、J 最差,而字母序恰好是 D 在前。如果偷懒不指定 levels,R 会把 D 当基准组,回归系数全变成"比 D 差多少",方向全反。我一般在建模前固定跑一次levels()检查,这个习惯救过不止一次。
3.2 派生变量:体积、台宽比和那个不能用的每克拉单价
原始字段之外,工程上常补几个派生量。x*y*z近似体积,比单看某一维更能刻画"看起来多大";depth字段本身已经是 2z/(x+y)×100,原始数据里直接可用,但清洗后可以重算一遍做交叉校验,差值大的记录说明原始字段有误。
diamonds_clean <- diamonds_clean %>% mutate( volume = x * y * z, # 近似体积,立方毫米 depth_calc = 2 * z / (x + y) * 100, # 重算总深度百分比 depth_diff = abs(depth - depth_calc) # 与原字段的偏差,用于校验 ) # 偏差超过 1 个百分点的记录有多少 sum(diamonds_clean$depth_diff > 1, na.rm = TRUE)还有一个特征必须点名:price / carat,也就是每克拉单价。它跟目标变量price直接相关,放进模型等于把答案提前告诉模型,R² 能瞬间冲到 0.99——这是典型的数据泄露,在机器学习里属于最低级的错误之一,但拿它做描述性分析、看不同切工的溢价水平是合理的。用之前先问一句:这个变量在预测时拿得到吗?拿不到就不能进特征集。
3.3 描述性可视化:直方图看形状,核密度看峰,小提琴看分组
可视化这一步不是画着好看,是给建模定方向。price的直方图决定要不要做对数变换,分组小提琴图决定交互项要不要加。
library(ggplot2) # 价格分布:直方图叠加核密度 ggplot(diamonds_clean, aes(x = price)) + geom_histogram(aes(y = after_stat(density)), bins = 60, fill = "grey75", colour = "white") + geom_density(colour = "#c0392b", linewidth = 1) + labs(x = "价格(美元)", y = "密度", title = "钻石价格分布") # 切工 × 价格,纵轴取对数才能看清组间差异 ggplot(diamonds_clean, aes(x = cut, y = price, fill = cut)) + geom_violin(trim = FALSE, alpha = 0.7) + scale_y_log10() + theme(legend.position = "none") + labs(x = "切工等级", y = "价格(对数刻度)")直方图出来能看见明显的单峰右偏,峰值在 1000 美元附近,2000、4000、8000 附近有几处断层。小提琴图纵轴必须取对数,否则五个组的箱体全被压扁在底部,什么也看不出来——这是新手最常犯的绘图错误,不取对数就断言"各组没差异",纯属误判。
从分组图能读出的信息是:切工越好价格中位数越高,但 Ideal 组的下须拖得很长,说明理想切工里也有一批小克拉的低价货。这个"切工与价格不是单调关系"的现象,正是后面要加carat:cut交互项的直接依据。
4. 回归建模:从 carat 单变量到交互项 + AIC 逐步回归
建模的推进顺序很重要,一上来就全变量加交互,跑出来的东西自己都解释不了。我的习惯是先跑单变量建立基准,再逐步加变量,每加一步都看调整 R² 和残差标准误的变化,心里有一本账。
4.1 一元线性回归:7760 美元/克拉这个系数怎么读
carat和price的正相关肉眼可见,先用它单独建一个模型,作为后续所有模型的比较基准。
fit1 <- lm(price ~ carat, data = diamonds_clean) summary(fit1) # 取出系数和置信区间 coef(fit1) confint(fit1, "carat", level = 0.95)系数约 7760,p 值远小于 0.001,字面解释是"克拉重量每增加 1 克拉,价格平均上涨约 7760 美元"。但这个解释只在 0.2 到 5 克拉这个区间内成立,而且模型强行假设了线性关系。实际上钻石定价是阶梯式的,整克拉和 0.99 克拉能差出一大截,线性模型捕捉不到。confint()给出的是系数的 95% 置信区间,用来判断这个估计稳不稳,区间越窄越可靠。
一元模型的调整 R² 只有 0.85 左右,残差标准误一千多美元。作为基准够用,但说明还有大量信息没被利用——切工、颜色、净度这三维度一颗都没进模型。
4.2 无交互项多元模型:R²=0.8798 卡在什么地方
把三个定性变量和三个尺寸变量全加进来,先不加任何交互项。
fit2 <- lm(price ~ carat + cut + color + clarity + depth + table + x + y + z, data = diamonds_clean) summary(fit2) AIC(fit2) # 方差膨胀因子,看共线性 car::vif(fit2)调整 R² 大约到 0.88,比一元模型涨了一点,但没到预期。问题出在两个地方。一是x、y、z和carat高度相关,三个尺寸基本都是克拉重量的函数,VIF 很容易超过 10,共线性把系数估计的方差撑大了。二是模型假设"切工对价格的影响是固定加项",可现实中切工的价值恰恰是通过放大克拉重量的溢价来实现的——一克拉的 Ideal 和三克拉的 Ideal,切工带来的绝对差价完全不同。
VIF 那块要具体看数值。carat的 VIF 通常在 10 以上,x、y、z也在 5 到 15 之间浮动。处理方式不是无脑删,而是想清楚:如果目标是预测,共线性不影响预测精度,可以留着;如果目标是解释系数,就得删掉一组。我在解释型任务里一般保留carat,把x、y、z拿掉,或者只保留volume。
4.3 交互项设计与向后逐步回归:AIC 到底选了什么
交互项的加法有讲究。全上cut*color*clarity会生成几百个参数,样本再多也撑不住。我的做法是先只给carat配交互——carat:cut、carat:color、carat:clarity,因为克拉重量是价格的乘性驱动因子,其余变量更像调节项。
fit3 <- lm(price ~ carat * cut + carat * color + carat * clarity + depth + table + volume, data = diamonds_clean) summary(fit3)$adj.r.squared length(coef(fit3)) # 参数个数,确认没有爆炸参数个数会到几十个,比fit2多不少,调整 R² 也有提升。接下来用step()做向后逐步回归,按 AIC 剔除贡献不足的项。
# k = 2 是 AIC;k = log(n) 是 BIC,惩罚更重、模型更精简 fit4 <- step(fit3, direction = "backward", k = 2, trace = 1) summary(fit4) AIC(fit3, fit4)step()的direction参数选"backward"是从全模型开始逐个删,"both"允许增删交替、搜索空间更大但更慢。k参数是关键:k = 2对应 AIC,偏向预测精度;k = log(nrow)对应 BIC,惩罚项随样本量增长,更偏向精简模型。五万多条样本,log(n)约等于 10.8,比 2 大得多,BIC 会砍掉更多交互项。我用哪个取决于目的——做预测用 AIC,做解释用 BIC。
| 模型 | 变量构成 | 调整 R² | 参数量 | 用途 |
|---|---|---|---|---|
| fit1 | carat | ≈0.85 | 2 | 基准 |
| fit2 | carat + 三个定性 + depth/table/xyz | ≈0.88 | 约 25 | 可解释性对照 |
| fit3 | carat 与三个定性全交互 + 尺寸 | ≈0.93 | 约 60 | 预测主力候选 |
| fit4 | step 向后筛选后 | ≈0.93 | 视 k 而定 | 最终模型 |
trace = 1会把每一步删掉的变量和对应的 AIC 变化打出来,这个输出别关,它记录了模型是怎么一步步瘦下来的。某一步 AIC 只降了 0.3,说明那个变量删不删都行,属于可解释性和精度的权衡点。
4.4 系数怎么念给非技术同事听
模型跑完,最难的往往不是跑,是解释。有序因子在模型里以.L、.Q、.C这类正交多项式项出现,系数没法直接读成"升一级涨多少钱"。要给出人能听懂的口径,可以固定其他变量,做一个预测网格。
# 固定其余变量在中位数水平,看切工从 Fair 到 Ideal 的预测价格 grid <- data.frame( carat = median(diamonds_clean$carat), cut = factor(levels(diamonds_clean$cut), levels = levels(diamonds_clean$cut), ordered = TRUE), color = factor("G", levels = levels(diamonds_clean$color), ordered = TRUE), clarity = factor("VS2", levels = levels(diamonds_clean$clarity), ordered = TRUE), depth = median(diamonds_clean$depth), table = median(diamonds_clean$table), volume = median(diamonds_clean$volume) ) # 用最终模型预测 grid$pred <- predict(fit4, newdata = grid) grid[, c("cut", "pred")]这个网格的预测差值,就是这个数据集里"切工升一档"的平均溢价,比读系数直观得多,拿去跟业务方沟通也更容易被接受。注意网格里的color、clarity必须用 factor 且 levels 跟训练集一致,否则predict()直接报错——这是新手最常撞的一堵墙。
5. 常见问题排查:模型跑通但结论不能信的五种情况
跑出summary()不等于做出分析。下面五条是我在这份数据上反复踩过的坑,按"现象 → 原因 → 解决"列出来,中一条就值得回头查。
5.1 现象:R² 突然从 0.88 跳到 0.99
原因:特征里混进了price / carat或者任何由price推导出来的量。这是数据泄露,模型在训练时已经看到了答案。另一种隐蔽变体是先把全量数据做了标准化或者 PCA 再切分训练测试集,测试集的信息渗进了训练过程。
解决:建模前把所有字段过一遍,凡是目标变量的函数一律剔除。标准化、降维这类操作只在训练集上fit,再用同一组参数去transform测试集。这条不分语言、不分框架,是所有机器学习的通用红线。
5.2 现象:高价钻石永远预测偏低,残差在尾部堆成一片
原因:为了"清洗异常值",把price的上围栏之外全删了。统计意义上的长尾被当成错误删除,模型压根没见过一万五千美元以上的样本,自然预测不出来。
解决:区分"物理不可能"和"统计上少见"。y=0、z=58.9这类违反常识的删掉;价格长尾保留,或者对price取对数后建模,log变换能把右偏压平,残差表现通常明显改善。取了对数记得在预测结果上做exp()还原,否则报出来的数字会小一大截。
5.3 现象:切工越差,系数越高,跟常识完全相反
原因:factor()没指定levels,R 按字母序排水平,基准组变成了Fair,所有系数都是相对"一般"而言,符号自然反了。color同理,字母序 D 在前,不指定就会把最好的 D 当基准。
解决:三个有序变量一律显式写levels,建模前跑一次levels()核对顺序。我现在的习惯是把这三个变量的 levels 定义抽成一个常量列表,在脚本顶部统一定义,全流程复用,避免不同脚本里顺序不一致。
5.4 现象:交互项全上之后,VIF 飙到几十,系数标准误大得离谱
原因:carat * cut * color * clarity展开后参数几百个,各交互项之间高度共线,加上主效应,设计矩阵接近奇异。样本量再大也压不住标准误。
解决:交互项只加有业务含义的,且优先跟主驱动变量carat配。加完跑car::vif(),超过 10 的项回头查共线性来源。还可以先做中心化scale(carat, scale = FALSE),把主效应和交互项的相关性降下来,系数会稳定不少。
5.5 现象:模型在训练集上 R² 0.93,换一批数据就掉到 0.7
原因:五万多条数据里有很多近乎重复的样本(参数雷同、只有价格差几十美元),随机切分时训练集和测试集里同时出现"同一颗钻石",等于变相泄漏。另外如果在全量数据上做完清洗再切分,切分边界也会受清洗规则影响。
解决:先切分再清洗,训练集得到的清洗规则(四分位阈值、上下围栏)直接套到测试集上,不要各自算各自的分位数。有近似重复的样本,按参数组合做分组切分,保证同一组只出现在一边。
# 先切分,再在训练集上算阈值 set.seed(2024) idx_train <- sample(seq_len(nrow(diamonds_clean)), size = 0.7 * nrow(diamonds_clean)) train <- diamonds_clean[idx_train, ] test <- diamonds_clean[-idx_train, ] # 阈值从 train 算,套用到 test q1_tr <- quantile(train$price, 0.25) q3_tr <- quantile(train$price, 0.75) upper_tr <- q3_tr + 1.5 * (q3_tr - q1_tr) test_f <- test[test$price <= upper_tr, ] nrow(test_f)代码本身很短,重点在顺序:切分在前,阈值计算在后,且只算一次。反过来做,评估结果会虚高,而且这种虚高在数据量越大、重复样本越多的时候越明显,五万条这个量级已经足够让偏差显形。
6. 残差四图与显著性检验:给模型做一次完整检查
模型跑完不算完,残差图是必看项。par(mfrow = c(2,2))之后plot(fit4)一次性出四张诊断图,每一张对应一个假设。
par(mfrow = c(2, 2), mar = c(4, 4, 2, 1)) plot(fit4, which = 1:4) par(mfrow = c(1, 1))第一张残差对拟合值,理想状态是点在零线附近均匀散布、没有喇叭口。价格数据上常见的是右端开口,说明高价位方差更大,是异方差,可以试log(price)或者加权最小二乘。第二张 QQ 图看正态性,两端翘起说明残差厚尾,样本量五万多的情况下轻微偏离可以接受,只要不是 S 形。第三张看标准化残差方差的稳定性,第四张看杠杆值和 Cook 距离,挑出影响力过大的个别点——diamonds里那几个尺寸录入错误的记录,往往就是在这里露头的。
<|im_start|>注意:五万多条样本做shapiro.test()会直接报错,样本量上限是 5000。要看正态性改用 QQ 图,或者对残差做ks.test()、ad.test(),别硬上。
分组差异的显著性检验也要跟上。不同切工组的价格不满足正态性,教科书式的方差分析前提不成立,用 Kruskal-Wallis 非参数检验更稳妥。
# 不同切工组的价格分布是否存在显著差异 kruskal.test(price ~ cut, data = diamonds_clean) # 两组比较就用 Wilcoxon wilcox.test(price ~ cut, data = subset(diamonds_clean, cut %in% c("Fair", "Ideal")))kruskal.test()的零假设是各组分布位置相同,p 值极小说明组间有差异,这跟模型里cut项显著是互相印证的。要注意 p 值小只说明有差异,不说明差异多大,效应量还得靠前面那份预测网格去量化。五万条样本下几乎任何组间差异都会显著,这是大样本检验的通病,别把统计显著当成业务重要。
最后补一句:predict()做预测一定要在newdata里把三个因子变量的 levels 对齐,缺一个水平就会报factor has new levels。我吃过一次亏,清洗后的测试集里刚好没有Fair这一档,模型直接拒绝预测。从那以后我每次做完清洗都先跑一遍sapply(diamonds_clean[, c("cut","color","clarity")], levels),确认水平没塌,再往下走建模流程。希望帮到你。
本文还有配套的精品资源,点击获取