做数据分析的人,练手作品集或者复习 R 语法的时候,迟早会碰到一套与哺乳动物睡眠相关的公开数据。最近我把 R 语言里这套经常被叫成 sleep 数据集的哺乳动物睡眠记录完整跑了一遍,从数据检查、缺失值处理、可视化,一路做到相关分析和回归建模。整套流程做下来之后我有个很直接的感受:这套数据只有 83 行 11 列,量级不大,但既有数值变量也有分类变量,既有完整记录也有缺失值,几乎涵盖了一次基础数据分析要走的全部环节,很适合刚学完 R 基础语法、想拿真实数据做综合练习的人。这篇文章就把我的完整思路、踩过的坑和可用代码整理出来,给后续想拿它做作品集或练手的人一个参考。1. 数据集底细:sleep 里到底有什么,以及它和 msleep 的关系先解决一个最容易让人困惑的问题。很多网课和博客把这个数据集写成 "sleep 数据集",你如果在 RStudio 里直接敲data(sleep),调出来的其实是另一份经典数据——关于两种药物对学生睡眠影响的配对实验数据,一共才 10 行,和哺乳动物睡眠没有关系。真正包含哺乳动物睡眠记录的数据集,在 R 生态里通常是通过ggplot2包加载的msleep,因为变量名里大量出现 sleep,圈内口语里就习惯叫它 sleep 数据集。为了避免读者跑代码时卡在第一步,我这篇文章统一使用msleep作为数据对象名,但全文内容对应的就是标题里说的那套哺乳动物睡眠时间数据。library(ggplot2) data(msleep)加载之后先确认数据结构,这一步别看它简单,很多人会跳过,结果后面画图报错半天找不到原因。dim(msleep) # 83 11 names(msleep) # 列名列表 str(msleep) # 每一列的类型和取值情况 summary(msleep) # 数值变量的五数概括和缺失量1.1 每一列到底是什么,能回答什么问题msleep收集了 83 种哺乳动物的睡眠和相关生态指标。把变量拆开看,大致可以分成三类:标识信息、睡眠信息、身体与生态信息。变量名含义类型分析作用name物种常用名字符标识,用于查具体物种genus属名字符分类学信息vore食性字符型分类变量可分组比较,有缺失order所属目字符高级分类,可用于分组聚合conservation保护状态分类变量可分析但缺失较多sleep_total每天睡眠总时长(小时)数值核心响应变量sleep_rem快速眼动睡眠时长(小时)数值睡眠质量指标,缺失较多sleep_cycle单个睡眠周期长度(小时)数值睡眠结构指标,缺失较多awake每天清醒时长(小时)数值与 sleep_total 互补brainwt脑重(千克)数值身体特征之一bodywt体重(千克)数值身体特征之一我在分析时最关心的是sleep_total,它是整个分析的响应变量;brainwt和bodywt是考察"体型越大是否睡得越少"这个经典问题的主要自变量;vore则是用来做分组比较的分类变量,比如食虫动物和植食动物在睡眠模式上有没有系统性差异。sleep_rem和sleep_cycle虽然信息量很大,但缺失偏多,使用时要格外留意样本量。1.2 为什么这套数据值得专门写一轮分析这套数据最有价值的地方在于它不是一张冷冰冰的随机数表,而是带有真实生态学背景的观测数据。你可以问出有实际意义的问题,比如"吃肉的是不是普遍比吃草睡得少""脑容量大的动物会不会牺牲睡眠时间",而不是只为了跑一个回归而凑变量。这种"数据背后有故事"的特质,让最后写出的分析报告更像一个完整的小项目,而不是课堂作业。当然,它也是典型的观察数据,不是随机对照实验的结果,所以无论分析出什么模式,都只能描述相关性,不能直接说"食性导致了睡眠差异"。这一点我会在后面的建模解读里反复强调,也是用人单位或者导师看作品集时最容易追问的地方。2. 缺失值处理:加载数据之后先别急着画图很多人拿到数据的第一反应是直接summary()然后开始画图,但其实最容易埋坑的是缺失值。我这次在预处理阶段就把缺失情况全部列了一遍,后面的分析才没有反复被报错打断。missing_counts <- apply(is.na(msleep), 2, sum) missing_counts[missing_counts > 0]在我当前加载的版本里,vore缺失 7 条,conservation缺失 29 条,sleep_rem缺失 22 条,sleep_cycle缺失 30 条。这个缺失量级对只有 83 行的数据集来说相当可观。2.1 缺失值的处理策略:不能无脑drop_na这里是我这次分析里最重要的实操经验之一:不要一上来就drop_na(),否则会吃大亏。如果直接用na.omit(msleep)删掉所有含缺失的行,数据集会从 83 行掉到大约 30 行左右,你后面建模、画分面图、做分组比较时,样本量都不够看。正确做法是根据当前环节需要的变量来决定数据保留范围:画sleep_total分布图时,只需要这一列非缺失,所有物种基本都能用上。按vore分组比较睡眠时长时,只要vore和sleep_total同时非缺失就行。做完整回归建模时,才需要保证进入模型的全部变量非缺失,这时样本量会明显缩减,要在结果解读里明确写出实际样本量。这样"按需裁剪"的处理方式,既能保留尽可能多的信息,也贴合真实工作中的分析习惯。我处理缺失值时用了这样的代码:library(dplyr) msleep_clean_vore <- msleep %>% filter(!is.na(vore), !is.na(sleep_total)) msleep_model <- msleep %>% filter(!is.na(vore), !is.na(sleep_total), !is.na(brainwt), !is.na(bodywt))2.2 变量类型转换与派生变量vore和conservation看起来是字符型,但如果要在回归里当因子使用,最好显式转换成factor。我还顺手创建了两个对数变换变量:log_brainwt和log_bodywt,原因是体重和脑重数据量级差异太大,小到几克的鼩鼱,大到几吨的鲸类,不取对数时散点图基本挤在左下角,看不出任何趋势。msleep <- msleep %>% mutate( vore = factor(vore), conservation = factor(conservation), log_brainwt = log10(brainwt), log_bodywt = log10(bodywt) )这一步做的逻辑在后面可视化章节会体现得更清楚。对数变换不是为了炫技,而是让跨数量级的数据能在同一张图里公平地展示线性关系。3. 探索性可视化:先让数据自己说话数据规整好之后,不要急着建模。先用三张图从不同角度把变量关系摸个底,这个顺序基本适用于所有数据分析项目。3.1 睡眠总时长的分布形态ggplot(msleep, aes(x = sleep_total)) + geom_histogram(bins = 12, fill = "steelblue", color = "white", alpha = 0.9) + labs(title = "哺乳动物每日睡眠总时长分布", x = "睡眠总时长(小时)", y = "物种数量")从分布来看,大多数物种每天睡 5 到 14 个小时,峰值大概落在 8 到 11 小时这个区间,右侧拖着一条较长的尾巴,少数物种像某些蝙蝠可以睡到 19 小时以上,而长颈鹿、大象则只需要睡 2 到 4 小时。这种轻度偏态的分布提醒我,后面如果做线性回归,模型对极端值的敏感度需要留意。3.2 不同食性下的睡眠差异ggplot(msleep_clean_vore, aes(x = vore, y = sleep_total, fill = vore)) + geom_boxplot(outlier.colour = "firebrick", outlier.size = 1.5, alpha = 0.7) + labs(title = "不同食性哺乳动物的睡眠时长对比", x = "食性", y = "睡眠总时长(小时)") + theme(legend.position = "none")这张图信息量很大。直观来看,食虫动物的睡眠时间较长,中间水平大概在 15 小时上下;植食动物睡眠时间相对较短,中位数在 10 小时以下;杂食动物和肉食动物居中。这个模式在生态学上是有解释空间的:食虫动物吃的昆虫能量密度低,可能需要更多时间消化和休息,而植食动物要花大量时间觅食,同时还要警惕捕食者,睡眠时间自然被压缩。不过要提醒一句:箱线图只是描述性观察,组间差异是否显著,还需要后面用方差分析或者回归来控制其他变量。而且各组样本量很不平衡,食虫动物只有 8 种左右,植食动物却有二十多种,解读时要谨慎。3.3 体型与睡眠的关系:对数变换的效果ggplot(msleep, aes(x = log_bodywt, y = sleep_total, color = vore)) + geom_point(size = 2.5, alpha = 0.8) + geom_smooth(method = "lm", se = FALSE, color = "gray40", linewidth = 0.7) + scale_color_viridis_d() + labs(title = "体重与睡眠总时长的关系(对数坐标)", x = "log10(体重,kg)", y = "睡眠总时长(小时)", color = "食性")这是整轮分析里最值得看的一张图。没有对数变换之前,体重从 0.005 kg 到 6000 kg 跨度巨大,绝大多数点挤在原点附近,根本分不清趋势。取对数之后,负相关趋势立刻显现出来:体型越大的哺乳动物,睡眠总时长越短。这个结论和主流睡眠研究的方向完全一致。另外我在图上还发现了一个值得单独拿出来看的点:大熊猫的睡眠时间接近 14 小时,但它的体重超过 100 kg,明显偏离了整体趋势,属于一个"高体重却高睡眠"的特殊物种。这种离群点不用急着删,标注出来反而能体现你在做分析时的细心程度。4. 相关分析:把数值变量之间的关系量化可视化帮我们建立了直观印象,接下来用相关分析把这些关系量化。这一步不仅能验证眼睛看到的趋势,还能发现一些容易被忽略的变量组合。4.1 计算相关系数时要小心缺失值num_vars <- c("sleep_total", "sleep_rem", "sleep_cycle", "brainwt", "bodywt", "awake") cor_mat <- cor(msleep[num_vars], method = "spearman", use = "complete.obs") round(cor_mat, 2)我选择spearman而不是pearson,原因是前面已经确认bodywt和brainwt的分布严重偏态,虽然手动做了对数变换,但spearman基于秩次计算,对离群值更稳健,更适合初步的探索性阶段。如果你想对比两种方法的差异,可以同时算一次pearson,你会发现数值有差别,但方向基本一致。4.2 相关矩阵的可视化library(corrplot) corrplot(cor_mat, method = "color", addCoef.col = "black", tl.col = "black", tl.cex = 0.8, number.cex = 0.7, diag = FALSE)相关矩阵里最值得注意的几个关系:sleep_total与sleep_rem呈明显正相关,说明总睡眠时间长的物种,快速眼动睡眠通常也比较长。sleep_total与brainwt、bodywt呈负相关,和散点图看到的趋势吻合。sleep_total与awake几乎完全负相关,这个很好理解,一天就 24 小时,清醒和睡眠天然互补。这里有一个建模阶段极其重要的提醒:awake和sleep_total存在完全线性冗余关系,明确一点说就是sleep_total + awake = 24。如果做回归时不小心同时放进这两个变量,模型会直接出现多重共线性问题,系数估计完全失真。我在第一次跑模型时就踩过这个坑,后面单独讲。4.3 相关分析阶段就该留意的样本量问题使用use = "complete.obs"计算相关矩阵时,R 会只保留所有变量都非缺失的样本。对于sleep_cycle这种缺失量很大的变量,它参与计算时实际样本量会缩水到一半左右,相关估计的稳定性会下降。所以我在相关分析阶段就决定:sleep_cycle只做探索性参考,不放进最终的回归模型。这个取舍在分析报告里也要写清楚,体现的是你对数据质量的认识。5. 回归建模与诊断:量化睡眠总时长受什么因素影响相关分析只能给出两两之间的关系,但要同时控制多个变量、判断各自的独立贡献,还是得走回归模型。这一节我会展示一套完整的建模流程,包括模型设定、多重共线性检查、简化和诊断。5.1 第一版模型:把核心自变量都放进去考虑到awake的完全冗余问题,第一版模型用log_bodywt、log_brainwt和vore作为自变量:mod1 <- lm(sleep_total ~ log_bodywt + log_brainwt + vore, data = msleep_model) summary(mod1)模型输出里,你会看到log_brainwt或log_bodywt中至少有一个系数显著为负,而vore的三个哑变量可能都不显著。这个结果本身有很强的解读空间:食性差异在睡眠上的表现,很大程度可能由体型差异所解释,一旦把体型放进来,食性就不再有额外解释力。但先别急着下结论,因为我注意到log_brainwt和log_bodywt这两个变量很可能存在共线性问题。脑重和体重在生物演化上是强相关的——大的动物通常脑也更大,同一个模型里放两个高度相关的变量,会让系数估计变得不稳定。5.2 用 VIF 检验多重共线性library(car) vif(mod1)VIF 的通用判断标准是:超过 10 就说明共线性问题明显,超过 5 就要警惕。我这轮跑出来的两个对数体型变量 VIF 都超过了 10,这正是我担心的结果。两个变量在模型里互相"抢功劳",虽然整体拟合没问题,但单个变量的系数含义变得不可靠。解决思路有三个:只保留一个体型变量。如果研究重点是"身体大小如何影响睡眠",保留体重即可。如果研究重点是"脑容量在控制体重后是否还有独立影响",可以采用相对脑重这样的复合指标,也就是brainwt / bodywt。用更复杂的正则化方法或主成分回归,但对这个体量的练习项目来说没有必要。我这次采用第一个思路,按研究问题选了log_bodywt,重新构建模型:mod2 <- lm(sleep_total ~ log_bodywt + vore, data = msleep_model) summary(mod2)5.3 模型结果怎么向非技术读者解释mod2的核心结论可以这样表述:在控制食性之后,体重每提高一个数量级,睡眠总时长平均显著缩短。而vore各组的系数在控制体重后基本不再显著,说明先前箱线图里看到的食性差异,很大程度上是因为不同食性的物种体型本身不同——食虫动物普遍小巧,植食动物普遍庞大。这里我要特意强调一个写作层面的细节:模型结果写进报告时,应该用通俗语言讲清楚"每提高一个数量级睡眠缩短多少小时",而不是只丢一个 p 值。我在解释mod2时会把系数换算成实际量纲,比如体重从 1 kg 增加到 10 kg,预测睡眠时长大约减少多少小时,这样读者有直观感受。5.4 诊断图与扩展思路plot(mod2)诊断图主要看两个:残差与拟合值图、Q-Q 图。sleep_total本身有轻度偏态,所以残差图可能会呈现一定的喇叭形态,Q-Q 图在尾部也可能偏离正态线。作为基线模型可以接受,但如果想让结果更漂亮,可以尝试对sleep_total做对数变换后再建模,或者考虑使用稳健回归。另外,这个数据集后续还能做很多扩展:按order作为随机效应做混合模型、用sleep_rem和sleep_cycle做多变量分析、用聚类算法把 83 个物种按睡眠模式分组,甚至用随机森林做变量重要性排序。如果是要拿这个项目去面试或者交作业,我建议至少往其中一个方向深入一步,而不是停在线性回归。最后分享几点实操体会这套数据我完整跑下来,最深的体会是:拿到一份不太起眼的小数据集,耐心把缺失值、变量关系、模型诊断都走一遍,能学到的东西比单纯背语法多得多。做分析时一定要带着具体问题去跑代码,比如"体型和睡眠到底什么关系""食性能不能解释睡眠差异",而不是机械地逐行执行命令,否则很容易画完图就不知道下一步该干什么。另外还有一个小技巧:msleep里有个别物种和常见印象很不一样,比如大熊猫睡得多、大象睡得少,这些具体物种在画图时如果被标记出来,会让你的分析报告很有记忆点。你可以用filter(msleep, name == "Panda")这类代码找出它们的确切数值,在报告里作为典型案例讨论。如果你打算把它整理成作品集项目,建议报告结构按"问题定义—数据清洗—探索可视化—统计分析—结论与局限"来组织,这套结构本身就是一个很标准的数据分析工作流,比贴一堆代码截图要有说服力得多。
做数据分析的人,练手作品集或者复习 R 语法的时候,迟早会碰到一套与哺乳动物睡眠相关的公开数据。最近我把 R 语言里这套经常被叫成 sleep 数据集的哺乳动物睡眠记录完整跑了一遍,从数据检查、缺失值处理、可视化,一路做到相关分析和回归建模。整套流程做下来之后我有个很直接的感受:这套数据只有 83 行 11 列,量级不大,但既有数值变量也有分类变量,既有完整记录也有缺失值,几乎涵盖了一次基础数据分析要走的全部环节,很适合刚学完 R 基础语法、想拿真实数据做综合练习的人。这篇文章就把我的完整思路、踩过的坑和可用代码整理出来,给后续想拿它做作品集或练手的人一个参考。1. 数据集底细:sleep 里到底有什么,以及它和 msleep 的关系先解决一个最容易让人困惑的问题。很多网课和博客把这个数据集写成 "sleep 数据集",你如果在 RStudio 里直接敲data(sleep),调出来的其实是另一份经典数据——关于两种药物对学生睡眠影响的配对实验数据,一共才 10 行,和哺乳动物睡眠没有关系。真正包含哺乳动物睡眠记录的数据集,在 R 生态里通常是通过ggplot2包加载的msleep,因为变量名里大量出现 sleep,圈内口语里就习惯叫它 sleep 数据集。为了避免读者跑代码时卡在第一步,我这篇文章统一使用msleep作为数据对象名,但全文内容对应的就是标题里说的那套哺乳动物睡眠时间数据。library(ggplot2) data(msleep)加载之后先确认数据结构,这一步别看它简单,很多人会跳过,结果后面画图报错半天找不到原因。dim(msleep) # 83 11 names(msleep) # 列名列表 str(msleep) # 每一列的类型和取值情况 summary(msleep) # 数值变量的五数概括和缺失量1.1 每一列到底是什么,能回答什么问题msleep收集了 83 种哺乳动物的睡眠和相关生态指标。把变量拆开看,大致可以分成三类:标识信息、睡眠信息、身体与生态信息。变量名含义类型分析作用name物种常用名字符标识,用于查具体物种genus属名字符分类学信息vore食性字符型分类变量可分组比较,有缺失order所属目字符高级分类,可用于分组聚合conservation保护状态分类变量可分析但缺失较多sleep_total每天睡眠总时长(小时)数值核心响应变量sleep_rem快速眼动睡眠时长(小时)数值睡眠质量指标,缺失较多sleep_cycle单个睡眠周期长度(小时)数值睡眠结构指标,缺失较多awake每天清醒时长(小时)数值与 sleep_total 互补brainwt脑重(千克)数值身体特征之一bodywt体重(千克)数值身体特征之一我在分析时最关心的是sleep_total,它是整个分析的响应变量;brainwt和bodywt是考察"体型越大是否睡得越少"这个经典问题的主要自变量;vore则是用来做分组比较的分类变量,比如食虫动物和植食动物在睡眠模式上有没有系统性差异。sleep_rem和sleep_cycle虽然信息量很大,但缺失偏多,使用时要格外留意样本量。1.2 为什么这套数据值得专门写一轮分析这套数据最有价值的地方在于它不是一张冷冰冰的随机数表,而是带有真实生态学背景的观测数据。你可以问出有实际意义的问题,比如"吃肉的是不是普遍比吃草睡得少""脑容量大的动物会不会牺牲睡眠时间",而不是只为了跑一个回归而凑变量。这种"数据背后有故事"的特质,让最后写出的分析报告更像一个完整的小项目,而不是课堂作业。当然,它也是典型的观察数据,不是随机对照实验的结果,所以无论分析出什么模式,都只能描述相关性,不能直接说"食性导致了睡眠差异"。这一点我会在后面的建模解读里反复强调,也是用人单位或者导师看作品集时最容易追问的地方。2. 缺失值处理:加载数据之后先别急着画图很多人拿到数据的第一反应是直接summary()然后开始画图,但其实最容易埋坑的是缺失值。我这次在预处理阶段就把缺失情况全部列了一遍,后面的分析才没有反复被报错打断。missing_counts <- apply(is.na(msleep), 2, sum) missing_counts[missing_counts > 0]在我当前加载的版本里,vore缺失 7 条,conservation缺失 29 条,sleep_rem缺失 22 条,sleep_cycle缺失 30 条。这个缺失量级对只有 83 行的数据集来说相当可观。2.1 缺失值的处理策略:不能无脑drop_na这里是我这次分析里最重要的实操经验之一:不要一上来就drop_na(),否则会吃大亏。如果直接用na.omit(msleep)删掉所有含缺失的行,数据集会从 83 行掉到大约 30 行左右,你后面建模、画分面图、做分组比较时,样本量都不够看。正确做法是根据当前环节需要的变量来决定数据保留范围:画sleep_total分布图时,只需要这一列非缺失,所有物种基本都能用上。按vore分组比较睡眠时长时,只要vore和sleep_total同时非缺失就行。做完整回归建模时,才需要保证进入模型的全部变量非缺失,这时样本量会明显缩减,要在结果解读里明确写出实际样本量。这样"按需裁剪"的处理方式,既能保留尽可能多的信息,也贴合真实工作中的分析习惯。我处理缺失值时用了这样的代码:library(dplyr) msleep_clean_vore <- msleep %>% filter(!is.na(vore), !is.na(sleep_total)) msleep_model <- msleep %>% filter(!is.na(vore), !is.na(sleep_total), !is.na(brainwt), !is.na(bodywt))2.2 变量类型转换与派生变量vore和conservation看起来是字符型,但如果要在回归里当因子使用,最好显式转换成factor。我还顺手创建了两个对数变换变量:log_brainwt和log_bodywt,原因是体重和脑重数据量级差异太大,小到几克的鼩鼱,大到几吨的鲸类,不取对数时散点图基本挤在左下角,看不出任何趋势。msleep <- msleep %>% mutate( vore = factor(vore), conservation = factor(conservation), log_brainwt = log10(brainwt), log_bodywt = log10(bodywt) )这一步做的逻辑在后面可视化章节会体现得更清楚。对数变换不是为了炫技,而是让跨数量级的数据能在同一张图里公平地展示线性关系。3. 探索性可视化:先让数据自己说话数据规整好之后,不要急着建模。先用三张图从不同角度把变量关系摸个底,这个顺序基本适用于所有数据分析项目。3.1 睡眠总时长的分布形态ggplot(msleep, aes(x = sleep_total)) + geom_histogram(bins = 12, fill = "steelblue", color = "white", alpha = 0.9) + labs(title = "哺乳动物每日睡眠总时长分布", x = "睡眠总时长(小时)", y = "物种数量")从分布来看,大多数物种每天睡 5 到 14 个小时,峰值大概落在 8 到 11 小时这个区间,右侧拖着一条较长的尾巴,少数物种像某些蝙蝠可以睡到 19 小时以上,而长颈鹿、大象则只需要睡 2 到 4 小时。这种轻度偏态的分布提醒我,后面如果做线性回归,模型对极端值的敏感度需要留意。3.2 不同食性下的睡眠差异ggplot(msleep_clean_vore, aes(x = vore, y = sleep_total, fill = vore)) + geom_boxplot(outlier.colour = "firebrick", outlier.size = 1.5, alpha = 0.7) + labs(title = "不同食性哺乳动物的睡眠时长对比", x = "食性", y = "睡眠总时长(小时)") + theme(legend.position = "none")这张图信息量很大。直观来看,食虫动物的睡眠时间较长,中间水平大概在 15 小时上下;植食动物睡眠时间相对较短,中位数在 10 小时以下;杂食动物和肉食动物居中。这个模式在生态学上是有解释空间的:食虫动物吃的昆虫能量密度低,可能需要更多时间消化和休息,而植食动物要花大量时间觅食,同时还要警惕捕食者,睡眠时间自然被压缩。不过要提醒一句:箱线图只是描述性观察,组间差异是否显著,还需要后面用方差分析或者回归来控制其他变量。而且各组样本量很不平衡,食虫动物只有 8 种左右,植食动物却有二十多种,解读时要谨慎。3.3 体型与睡眠的关系:对数变换的效果ggplot(msleep, aes(x = log_bodywt, y = sleep_total, color = vore)) + geom_point(size = 2.5, alpha = 0.8) + geom_smooth(method = "lm", se = FALSE, color = "gray40", linewidth = 0.7) + scale_color_viridis_d() + labs(title = "体重与睡眠总时长的关系(对数坐标)", x = "log10(体重,kg)", y = "睡眠总时长(小时)", color = "食性")这是整轮分析里最值得看的一张图。没有对数变换之前,体重从 0.005 kg 到 6000 kg 跨度巨大,绝大多数点挤在原点附近,根本分不清趋势。取对数之后,负相关趋势立刻显现出来:体型越大的哺乳动物,睡眠总时长越短。这个结论和主流睡眠研究的方向完全一致。另外我在图上还发现了一个值得单独拿出来看的点:大熊猫的睡眠时间接近 14 小时,但它的体重超过 100 kg,明显偏离了整体趋势,属于一个"高体重却高睡眠"的特殊物种。这种离群点不用急着删,标注出来反而能体现你在做分析时的细心程度。4. 相关分析:把数值变量之间的关系量化可视化帮我们建立了直观印象,接下来用相关分析把这些关系量化。这一步不仅能验证眼睛看到的趋势,还能发现一些容易被忽略的变量组合。4.1 计算相关系数时要小心缺失值num_vars <- c("sleep_total", "sleep_rem", "sleep_cycle", "brainwt", "bodywt", "awake") cor_mat <- cor(msleep[num_vars], method = "spearman", use = "complete.obs") round(cor_mat, 2)我选择spearman而不是pearson,原因是前面已经确认bodywt和brainwt的分布严重偏态,虽然手动做了对数变换,但spearman基于秩次计算,对离群值更稳健,更适合初步的探索性阶段。如果你想对比两种方法的差异,可以同时算一次pearson,你会发现数值有差别,但方向基本一致。4.2 相关矩阵的可视化library(corrplot) corrplot(cor_mat, method = "color", addCoef.col = "black", tl.col = "black", tl.cex = 0.8, number.cex = 0.7, diag = FALSE)相关矩阵里最值得注意的几个关系:sleep_total与sleep_rem呈明显正相关,说明总睡眠时间长的物种,快速眼动睡眠通常也比较长。sleep_total与brainwt、bodywt呈负相关,和散点图看到的趋势吻合。sleep_total与awake几乎完全负相关,这个很好理解,一天就 24 小时,清醒和睡眠天然互补。这里有一个建模阶段极其重要的提醒:awake和sleep_total存在完全线性冗余关系,明确一点说就是sleep_total + awake = 24。如果做回归时不小心同时放进这两个变量,模型会直接出现多重共线性问题,系数估计完全失真。我在第一次跑模型时就踩过这个坑,后面单独讲。4.3 相关分析阶段就该留意的样本量问题使用use = "complete.obs"计算相关矩阵时,R 会只保留所有变量都非缺失的样本。对于sleep_cycle这种缺失量很大的变量,它参与计算时实际样本量会缩水到一半左右,相关估计的稳定性会下降。所以我在相关分析阶段就决定:sleep_cycle只做探索性参考,不放进最终的回归模型。这个取舍在分析报告里也要写清楚,体现的是你对数据质量的认识。5. 回归建模与诊断:量化睡眠总时长受什么因素影响相关分析只能给出两两之间的关系,但要同时控制多个变量、判断各自的独立贡献,还是得走回归模型。这一节我会展示一套完整的建模流程,包括模型设定、多重共线性检查、简化和诊断。5.1 第一版模型:把核心自变量都放进去考虑到awake的完全冗余问题,第一版模型用log_bodywt、log_brainwt和vore作为自变量:mod1 <- lm(sleep_total ~ log_bodywt + log_brainwt + vore, data = msleep_model) summary(mod1)模型输出里,你会看到log_brainwt或log_bodywt中至少有一个系数显著为负,而vore的三个哑变量可能都不显著。这个结果本身有很强的解读空间:食性差异在睡眠上的表现,很大程度可能由体型差异所解释,一旦把体型放进来,食性就不再有额外解释力。但先别急着下结论,因为我注意到log_brainwt和log_bodywt这两个变量很可能存在共线性问题。脑重和体重在生物演化上是强相关的——大的动物通常脑也更大,同一个模型里放两个高度相关的变量,会让系数估计变得不稳定。5.2 用 VIF 检验多重共线性library(car) vif(mod1)VIF 的通用判断标准是:超过 10 就说明共线性问题明显,超过 5 就要警惕。我这轮跑出来的两个对数体型变量 VIF 都超过了 10,这正是我担心的结果。两个变量在模型里互相"抢功劳",虽然整体拟合没问题,但单个变量的系数含义变得不可靠。解决思路有三个:只保留一个体型变量。如果研究重点是"身体大小如何影响睡眠",保留体重即可。如果研究重点是"脑容量在控制体重后是否还有独立影响",可以采用相对脑重这样的复合指标,也就是brainwt / bodywt。用更复杂的正则化方法或主成分回归,但对这个体量的练习项目来说没有必要。我这次采用第一个思路,按研究问题选了log_bodywt,重新构建模型:mod2 <- lm(sleep_total ~ log_bodywt + vore, data = msleep_model) summary(mod2)5.3 模型结果怎么向非技术读者解释mod2的核心结论可以这样表述:在控制食性之后,体重每提高一个数量级,睡眠总时长平均显著缩短。而vore各组的系数在控制体重后基本不再显著,说明先前箱线图里看到的食性差异,很大程度上是因为不同食性的物种体型本身不同——食虫动物普遍小巧,植食动物普遍庞大。这里我要特意强调一个写作层面的细节:模型结果写进报告时,应该用通俗语言讲清楚"每提高一个数量级睡眠缩短多少小时",而不是只丢一个 p 值。我在解释mod2时会把系数换算成实际量纲,比如体重从 1 kg 增加到 10 kg,预测睡眠时长大约减少多少小时,这样读者有直观感受。5.4 诊断图与扩展思路plot(mod2)诊断图主要看两个:残差与拟合值图、Q-Q 图。sleep_total本身有轻度偏态,所以残差图可能会呈现一定的喇叭形态,Q-Q 图在尾部也可能偏离正态线。作为基线模型可以接受,但如果想让结果更漂亮,可以尝试对sleep_total做对数变换后再建模,或者考虑使用稳健回归。另外,这个数据集后续还能做很多扩展:按order作为随机效应做混合模型、用sleep_rem和sleep_cycle做多变量分析、用聚类算法把 83 个物种按睡眠模式分组,甚至用随机森林做变量重要性排序。如果是要拿这个项目去面试或者交作业,我建议至少往其中一个方向深入一步,而不是停在线性回归。最后分享几点实操体会这套数据我完整跑下来,最深的体会是:拿到一份不太起眼的小数据集,耐心把缺失值、变量关系、模型诊断都走一遍,能学到的东西比单纯背语法多得多。做分析时一定要带着具体问题去跑代码,比如"体型和睡眠到底什么关系""食性能不能解释睡眠差异",而不是机械地逐行执行命令,否则很容易画完图就不知道下一步该干什么。另外还有一个小技巧:msleep里有个别物种和常见印象很不一样,比如大熊猫睡得多、大象睡得少,这些具体物种在画图时如果被标记出来,会让你的分析报告很有记忆点。你可以用filter(msleep, name == "Panda")这类代码找出它们的确切数值,在报告里作为典型案例讨论。如果你打算把它整理成作品集项目,建议报告结构按"问题定义—数据清洗—探索可视化—统计分析—结论与局限"来组织,这套结构本身就是一个很标准的数据分析工作流,比贴一堆代码截图要有说服力得多。