简介:这份PDF文档面向R语言初学者与数据分析入门者,系统讲解描述性统计分析的完整方法体系,帮助读者快速掌握从基础统计量到分组分析与可视化的核心技能。内容覆盖summary()、sapply()等内置函数,偏度与峰度的自定义函数实现,Hmisc与psych包的describe()函数,以及aggregate()、by()、doBy包的summaryBy()分组统计方案,并借助ggplot2绘制直方图与箱线图直观呈现数据分布特征。资源包共1个PDF文件,约98KB,篇幅精炼、代码示例完整,便于随查随用。目前已有553人学习下载,适合希望夯实数据探索基础、为后续数据挖掘与机器学习打底的学习者参考。
1. 描述性统计:为什么 summary() 给出的答案经常不够用
很多人第一次在 R 里做描述性统计,都是敲一句summary(df)就完事。最小值、最大值、四分位数、均值,六个数一屏打完,看起来数据已经被“看透”了。但真正踩过坑的人会发现,这六个数字经常骗人:均值被极端值拉走、四分位数掩盖双峰分布、缺失值悄悄吃掉样本量,而summary()对此一言不发。描述性统计分析的本质不是“算几个指标”,而是用尽量少的统计量把数据分布的中心趋势、离散程度和形状刻画清楚,为后续建模和异常识别提供可信的起点。R 语言在这件事上提供了从内置函数、自定义函数到 Hmisc、psych 等扩展包的多层工具链,理解它们各自的计算口径和边界,比记住函数名重要得多。搞懂这套机制,你才知道什么时候该信summary(),什么时候必须自己动手。
2. summary() 与 sapply():内置函数的计算口径和适用边界
2.1 summary() 到底对每一列做了什么
summary()是一个泛型函数,它的行为完全取决于输入对象的类。对数据框调用时,它会逐列分派:数值型列走summary.default,输出最小值、下四分位数、中位数、均值、上四分位数、最大值;因子型列走频数统计;逻辑型列统计 TRUE/FALSE 个数。这个分派机制意味着,同一张表里不同类型的列会得到完全不同形态的输出,这既是便利也是陷阱。
data(mtcars) myvars <- c("mpg", "hp", "wt") summary(mtcars[myvars]) # 输出形如: # mpg hp wt # Min. :10.40 Min. : 52.0 Min. :1.513 # 1st Qu.:15.43 1st Qu.: 96.5 1st Qu.:2.581 # Median :19.20 Median :123.0 Median :3.325 # Mean :20.09 Mean :146.7 Mean :3.217 # 3rd Qu.:22.80 3rd Qu.:180.0 3rd Qu.:3.610 # Max. :33.90 Max. :335.0 Max. :5.424关键点在于:summary()默认不处理缺失值参数,它内部对NA的处理是“统计非缺失部分”,但不会告诉你缺失了多少个。也就是说,如果一列有 30% 的值是NA,你看到的均值只基于剩下 70% 的样本,而输出里没有任何提示。这是最常见的翻车点之一。另外,summary()不输出标准差、偏度、峰度,对分布形状的刻画几乎为零——它只给你位置信息,不给形状信息。
2.2 sapply() 的向量化思维:一次算一族统计量
sapply()的作用是把一个函数应用到列表或向量的每个元素上,并尝试把结果简化为向量或矩阵。在描述性统计场景里,它的价值在于“对多列同时算同一个统计量”,避免手写循环。
# 对三列分别计算均值和标准差 sapply(mtcars[myvars], mean) sapply(mtcars[myvars], sd) # 一次算多个统计量:传入自定义的匿名函数 sapply(mtcars[myvars], function(x) c( n = length(x), mean = mean(x), sd = sd(x), median = median(x) ))第二段代码返回的是一个矩阵:行是统计量名,列是变量名。这个结构非常适合直接喂给后续的报表或绘图。但要注意sapply()的“简化”行为是自动的——如果每次返回的长度不一致,它会退化成列表,这时候下游代码如果假设是矩阵就会报错。常见的实现思路是:需要稳定结构时改用vapply(),显式声明返回模板,比如vapply(mtcars[myvars], mean, numeric(1)),一旦某列算出非标量结果就直接报错,而不是悄悄变形。
还有一个容易被忽略的细节:sapply()对数据框是按列迭代的,如果某一列是字符型,mean()会直接报错并中断整个调用。所以实际使用时,要么先做类型筛选,要么在函数内部加类型判断。这跟summary()的“逐列分派、各算各的”是两种哲学:前者要求你保证输入同质,后者替你做了分派但牺牲了可控性。
3. 自定义统计函数:偏度、峰度与缺失值处理的源码级拆解
3.1 为什么内置函数不给你偏度和峰度
偏度和峰度描述的是分布形状:偏度衡量不对称性,峰度衡量尾部厚度。R 的基础包stats里其实有mean、sd、var、quantile,但没有直接暴露偏度和峰度的计算函数。原因在于这两个统计量的定义存在多种口径——样本偏度有有偏估计和无偏估计之分,峰度有超额峰度(减 3)和非超额之分。基础包选择不替用户做这个决定,把口径选择留给上层包或用户自己。
原文给出的自定义函数是这样的:
mystats <- function(x, na.omit = FALSE) { if (na.omit) { x <- x[!is.na(x)] } m <- mean(x) n <- length(x) s <- sd(x) skew <- sum((x - m)^3 / s^3) / n kurt <- sum((x - m)^4 / s^4) / n - 3 return(c(n = n, mean = m, stdev = s, skew = skew, kurtosis = kurt)) }逐行看:na.omit参数控制是否先剔除缺失值,这是一个显式的开关,默认 FALSE 意味着有NA时结果就是NA——这比悄悄忽略更安全。skew用的是三阶中心矩除以标准差三次方再除以 n,这是有偏的样本偏度估计;kurt是四阶中心矩除以标准差四次方再减 3,即超额峰度。注意这里的s是样本标准差(分母 n-1),而矩计算里除以的是 n,这种混用在统计上并不严格一致,但在描述性场景下足够用。如果要做推断,应该换成基于无偏估计的公式,或者直接用e1071包的skewness()和kurtosis()。
3.2 自定义函数在 sapply 下的返回结构
把mystats套进sapply,返回的是一个 5 行、列数等于变量数的矩阵:
sapply(mtcars[myvars], mystats) # mpg hp wt # n 32.000000 32.00000 32.0000000 # mean 20.090625 146.68750 3.2172500 # stdev 6.026948 68.56287 0.9784574 # skew 0.610655 0.72602 0.4231465 # kurtosis -0.372766 -0.13556 -0.0227108这个矩阵的行名来自return(c(n=..., mean=...))里的命名,列名来自数据框列名。机制上,sapply检测到每次调用返回长度相同的命名向量,就把它们按列拼成矩阵。这里有个血泪经验:如果函数内部因为某列全是NA导致sd()返回NA,整个矩阵对应位置就是NA,不会报错,但下游如果做排序或筛选就会静默出错。所以自定义函数里最好加一句if (anyNA(x) && !na.omit) return(rep(NA_real_, 5))之类的防御,或者干脆在调用前统一做缺失值审计。
4. Hmisc 与 psych 的 describe():同名函数背后的不同设计取舍
4.1 两个 describe() 不是一回事
Hmisc::describe()和psych::describe()名字一样,输出形态和计算口径差别很大。Hmisc的版本偏向“人读”:它输出每个变量的样本量、缺失数、唯一值数、均值、分位数,还会给出最频繁出现的几个值,适合做数据质量初筛。psych的版本偏向“机读”:返回一个数据框,列包括 n、mean、sd、median、min、max、range、skew、kurtosis、se,直接可以拿去做表或继续计算。
library(Hmisc) describe(mtcars[myvars]) library(psych) describe(mtcars[myvars])机制上的差异来自包的设计目标:Hmisc是通用数据描述工具箱,describe()要兼顾数值、因子、字符多种类型;psych是心理测量学背景,describe()默认输出偏度和峰度,因为量表分析里形状指标是刚需。这也解释了为什么psych::describe()的峰度默认是超额峰度,而Hmisc的输出里峰度的呈现方式不同——口径不统一是跨包协作时最容易踩的坑。
4.2 加载顺序导致的函数遮蔽
两个包都导出describe,谁后加载谁生效。如果先library(psych)再library(Hmisc),直接调describe()走的是 Hmisc 版本;反过来则走 psych 版本。这种遮蔽不会报错,只会让输出形态突然变化,排查起来很费时间。稳妥的写法是始终带命名空间调用:psych::describe()或Hmisc::describe()。这不是洁癖,而是可复现性的基本要求——脚本换一台机器、换一个加载顺序,结果就不一样,属于典型的玄学问题,根子就在命名空间冲突。
另外,psych::describe()有一个fast参数,设为 TRUE 时跳过一些耗时计算,适合大数据集快速扫一眼;Hmisc::describe()则有digits参数控制输出精度。这些参数在原文里没展开,但实际使用时直接影响输出可信度,值得翻一下?describe的帮助页确认默认值。
5. 分组描述性统计:aggregate、by 与 summaryBy 的机制差异
5.1 aggregate() 的公式接口与返回值形态
aggregate()支持公式写法,mpg ~ cyl表示按cyl分组算mpg。它的返回值是一个数据框,分组列在前,统计结果在后。机制上,它内部先对分组变量做split,再对每个子集应用函数,最后rbind回来。
aggregate(mpg ~ cyl, data = mtcars, mean) # cyl mpg # 1 4 26.66364 # 2 6 19.74286 # 3 8 15.10000局限在于:一次只能算一个统计量(或者用cbind合并多个公式),且返回的是数据框,多统计量时列名不够直观。如果要对多个变量、多个统计量同时分组,公式会变得很长。
5.2 by() 的分组-应用模型
by()是tapply()在数据框上的推广,它把数据框按分组变量切成子数据框列表,对每个子集调用函数,返回一个列表。
dstats <- function(x) sapply(x, mystats) by(mtcars[myvars], mtcars$cyl, dstats)返回的是一个列表,每个元素对应一个分组,元素内部是mystats算出的矩阵。这个结构信息量大,但不适合直接输出成表——需要再do.call(rbind, ...)或配合plyr/purrr整理。by()的优势是灵活:函数可以是任意复杂逻辑,返回任意结构;代价是结果需要二次加工。
5.3 summaryBy() 的公式化多统计量
doBy::summaryBy()把公式接口和多统计量结合得最自然:
library(doBy) summaryBy(mpg + hp + wt ~ cyl, data = mtcars, FUN = mystats)它返回一个数据框,分组列加上每个变量每个统计量的列,列名形如mpg.mean、hp.skew。机制上,它内部对FUN的返回向量做展开和命名拼接,所以要求FUN返回命名向量——这正是mystats里c(n=..., mean=...)命名的意义所在。如果返回的是无名向量,列名会变成mpg.1、mpg.2这种,可读性骤降。
三种方式的选择逻辑:只要一个统计量、要数据框,用aggregate();要复杂自定义逻辑、能接受列表,用by();要多变量多统计量、要整齐数据框,用summaryBy()。没有哪个绝对更好,差别在返回结构和后续消费方式。
6. 分组统计与可视化的避坑清单
6.1 分组变量类型导致的静默错误
现象:aggregate(mpg ~ cyl, ...)结果里分组数比预期少。原因:cyl如果是数值型但实际是分类含义,aggregate按唯一值分组没问题;但如果分组变量是连续数值,会得到大量只有一两个样本的组,均值毫无意义。解决:分组前用factor()显式转换,并检查table()的组样本量。
6.2 缺失值在分组统计中的连锁反应
现象:某个分组的均值是NA,其他分组正常。原因:该组内目标变量存在NA,而mean()默认na.rm = FALSE。解决:在FUN里显式传na.rm = TRUE,或者用mystats的na.omit开关,并在结果里保留n以便核对有效样本量。
6.3 ggplot2 直方图 binwidth 选错导致分布误判
现象:直方图看起来是单峰,但箱线图显示有离群点。原因:binwidth太大,把双峰抹平了。解决:多试几个binwidth,或者改用geom_density()叠加,箱线图的outlier点也要单独看。原文示例里binwidth = 1对mpg是合理的,但换一个量纲不同的变量就必须重调。
6.4 箱线图分组因子顺序错乱
现象:箱线图 x 轴分组顺序不是 4、6、8 而是按字母或出现顺序。原因:factor(cyl)默认按字典序或唯一值顺序。解决:factor(cyl, levels = c(4, 6, 8))显式指定,否则图的可读性和结论方向都可能被误导。
6.5 包加载顺序引发的 describe 遮蔽
现象:同一段代码昨天输出是数据框,今天输出是文本块。原因:Hmisc和psych的describe互相遮蔽,取决于library()顺序。解决:始终用psych::describe()或Hmisc::describe()带命名空间调用,脚本里不依赖加载顺序。
7. 从统计量到判断:描述性分析的取舍与验证习惯
描述性统计最容易给人的错觉是“算完了就等于看懂了”。summary()给你六个数字,psych::describe()给你十几个,summaryBy()给你一张宽表,但数字多不等于信息多。真正的取舍在于:你到底要回答什么问题。如果只是数据质量初筛,Hmisc::describe()的缺失数、唯一值数、频次Top值比偏度峰度有用得多;如果要做分布形状判断,偏度和峰度必须配合直方图或密度图交叉验证,单看数字容易被样本量小导致的波动带偏。
一个可操作的验证习惯是:任何描述性统计结果,至少用两种口径交叉核对。均值用summary()和sapply(..., mean)各算一遍,确认缺失值处理一致;分组统计用aggregate()和summaryBy()各跑一次,确认分组数和样本量对得上;偏度用自定义函数和psych::describe()对比,确认口径差异在可接受范围内。这种“双算”看起来笨,但能拦住大部分静默错误——描述性统计的坑几乎都不报错,只是悄悄给你一个错的数。
| 需求 | 首选工具 | 注意点 |
|---|---|---|
| 快速扫一眼 | summary() | 不报缺失数,无形状指标 |
| 多列同统计量 | sapply()/vapply() | 返回结构可能退化 |
| 形状指标 | psych::describe() | 峰度为超额峰度 |
| 数据质量初筛 | Hmisc::describe() | 输出为人读文本 |
| 分组多统计量 | doBy::summaryBy() | FUN 必须返回命名向量 |
| 分布可视化 | ggplot2直方图+箱线图 | binwidth 和因子顺序要显式设 |
我自己的习惯是:拿到任何新数据集,先跑一遍summary()看量纲和缺失,再跑psych::describe()看形状,然后按分组变量画箱线图,三件事做完才允许自己下“数据正常”的结论。有一次就是靠箱线图发现某个分组的样本量只有 2,而summary()的均值看起来毫无异常——从那以后我每次做分组统计都强制先看table()的组样本量,再谈均值。希望帮到你。
本文还有配套的精品资源,点击获取