ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

R语言描述性统计实战:从summary到自定义函数与可视化

R语言描述性统计实战:从summary到自定义函数与可视化

简介:这份PDF资料面向R语言初学者与数据分析入门者,系统梳理描述性统计分析的常用方法,帮助读者快速掌握从基础统计量到分组分析与可视化的完整流程。内容覆盖summary()、sapply()等内置函数,偏度与峰度自定义函数,Hmisc、psych包的describe()函数,以及aggregate()、by()、doBy包的summaryBy()分组统计,并借助ggplot2绘制直方图与箱线图直观呈现数据分布。资源包共1个PDF文件,约98KB,篇幅精炼、代码示例完整,便于随查随用。已有553人学习,适合希望用R完成数据探索、为后续建模打基础的读者参考。

1. 描述性统计不是跑个 summary 就完事:一份能直接复用的 R 脚本包

很多人第一次在 R 里做描述性统计,都是summary(df)一把梭,看到最小值、最大值、四分位数就以为摸清了数据底细。真到建模或者写报告时才发现,均值被极端值拉飞、缺失值悄悄吃掉样本量、分组之后各组的偏度方向完全相反——这些summary()根本不会提醒你。这份资源围绕 R 语言描述性统计的完整链路展开:从内置函数到自定义偏度峰度、从Hmisc/psych两个 describe 到分组聚合、再到ggplot2出图,代码示例都是能直接粘进 RStudio 跑的。适合刚接手一份陌生数据集、需要快速产出数据画像的从业者,也适合想把统计口径固定成模板、避免每次重复造轮子的熟手。下面按“先立口径、再上代码、最后避坑”的顺序拆。

2. 先把统计口径立住:summary、sapply 与自定义函数的取舍

描述性统计最容易翻车的地方不是函数不会用,而是口径不统一。同一份数据,A 用summary()报均值,B 用sapply()报标准差,C 自己写了个偏度公式,三个人算出来的峰度能差出一个量级。所以动手之前先把要算哪些量、缺失值怎么处理、分母用 n 还是 n-1 定下来,比急着敲代码重要得多。

2.1 summary() 的边界:它能给你什么,给不了什么

summary()是 R 里最省事的入口,数值型变量直接吐最小值、下四分位数、中位数、均值、上四分位数、最大值,因子和逻辑向量给频数。拿内置的mtcars试一下,选mpg、hp、wt三个变量:

# 加载内置数据集,无需额外安装 data(mtcars) # 选定要分析的数值变量 myvars <- c("mpg", "hp", "wt") # 基础描述性统计:一行拿到六数概括 summary(mtcars[myvars])

这段代码的逻辑很直白:mtcars[myvars]按列名取子集,summary()对每个数值列分别计算六数概括。参数上没什么可调的,它不接受na.rm之类的开关,缺失值会以NA's单独计数显示在结果里——这一点很关键,看到NA's : 3就说明这一列有 3 个缺失,后续算均值标准差时必须显式处理,否则结果直接变NA。

summary()的短板也很明显:不给标准差、不给方差、不给偏度峰度,四分位数之外没有更多分位点。它适合做第一眼的体检,不适合当最终报告口径。我一般把它当“开机自检”,确认列类型对不对、缺失多不多,再决定往下用哪套函数。

2.2 sapply() 批量套函数:把均值、标准差、分位数一次算齐

sapply()的价值在于“对每一列应用同一个函数”,返回一个命名向量或矩阵,天然适合批量出统计量。基础写法是先算均值再算标准差:

# 对每个变量求均值 sapply(mtcars[myvars], mean) # 对每个变量求标准差 sapply(mtcars[myvars], sd)

mean和sd默认都不处理缺失值,数据干净时没问题;一旦有NA,得改成function(x) mean(x, na.rm = TRUE)这种匿名函数形式。更实用的做法是一次性把多个统计量拼成一个矩阵,方便直接贴进报告:

# 一次性计算多个统计量,结果按变量为行、统计量为列 stats_mat <- sapply(mtcars[myvars], function(x) { c( n = length(x), # 样本量 mean = mean(x), # 均值 sd = sd(x), # 标准差 median = median(x), # 中位数 min = min(x), # 最小值 max = max(x), # 最大值 q25 = quantile(x, 0.25), # 下四分位数 q75 = quantile(x, 0.75) # 上四分位数 ) }) # 转置一下,让变量在行、统计量在列,读起来更顺 t(stats_mat)

这里的关键点是sapply会把匿名函数返回的命名向量按列拼起来,所以原始结果是“统计量 × 变量”的矩阵,t()转置后才是我们习惯的“变量 × 统计量”。quantile(x, 0.25)的第二个参数就是分位点,想要 10%、90% 分位就改成 0.1、0.9,比summary()灵活。参数上唯一要盯的是na.rm,上面这版没加,是因为mtcars本身没有缺失;换成真实数据务必在每个函数里补上na.rm = TRUE,或者先统一清洗。

2.3 自定义 mystats:偏度和峰度为什么必须自己写

summary()和sapply()都覆盖不到偏度(skewness)和峰度(kurtosis),而这两个量恰恰是判断分布形状、识别异常值的关键。R 基础包里没有现成函数,最稳的办法是自己写一个,把样本量、均值、标准差、偏度、峰度打包返回:

# 自定义描述性统计函数 # na.omit = TRUE 时先剔除缺失值再计算 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)) } # 对三个变量分别应用 sapply(mtcars[myvars], mystats)

逻辑说明:偏度用三阶中心矩除以标准差三次方,峰度用四阶中心矩除以标准差四次方再减 3,减 3 是为了让正态分布的峰度归零,得到“超额峰度”。参数na.omit默认FALSE,意味着有缺失就直接返回NA,这是故意的——逼你先确认缺失情况,而不是默默丢掉样本。实际用的时候我一般传na.omit = TRUE,但会在报告里注明剔除了多少条。

提示:这里的偏度峰度用的是总体公式(分母为 n),和psych包默认的样本校正公式(分母为 n-1 并做偏倚校正)算出来会有细微差异。跨工具对比数字前,先确认口径一致,否则会误以为数据出了问题。

3. Hmisc 与 psych 两个 describe:装哪个、看哪列、怎么对齐

自己写函数灵活但费事,日常分析更常见的是直接调包。Hmisc和psych都提供describe(),名字一样、输出不一样,很多人装完发现两边的偏度对不上就开始怀疑人生。这一章把两者的差异和分组聚合一起讲清楚。

3.1 Hmisc::describe():信息最全,但输出是文本块

Hmisc的describe()输出非常细,样本量、缺失数、唯一值数、均值、分位数、极值、偏度峰度都有,缺点是返回的是格式化文本,想拿数字得手动抠。

# 安装并加载 Hmisc install.packages("Hmisc") library(Hmisc) # 对选定变量做详细描述 describe(mtcars[myvars])

参数上,describe()支持digits控制小数位、omit指定要跳过的变量。它最大的实用点是会列出“最低/最高各 5 个值”,排查异常值特别顺手——如果最高 5 个值里出现一个明显离群的数,基本就能定位到录入错误。但它不直接返回数据框,想批量导出得配合capture.output()或者改用psych。

3.2 psych::describe():返回数据框,最适合批量出表

psych的describe()返回的是一个规整的数据框,列包括n、mean、sd、median、min、max、range、skew、kurtosis、se,直接能写进 CSV 或者喂给knitr出表。

# 安装并加载 psych install.packages("psych") library(psych) # 返回数据框,便于后续导出 desc_df <- describe(mtcars[myvars]) desc_df

参数上常用的是fast = TRUE,跳过一些耗时计算,大数据集下明显更快;quant可以指定额外要算的分位点,比如quant = c(.1, .9)。注意psych的skew和kurtosis默认带样本校正,和 2.3 节手写公式的结果不完全相等,这是正常的,不是谁算错了。选型上我的习惯是:要快速看分布细节用Hmisc,要批量出表、接下游流程用psych。

3.3 分组聚合三件套:aggregate、by、summaryBy 怎么选

真实数据几乎都要分组看,比如按某个分类变量拆开算均值。R 里有三条路,适用场景不同。

aggregate()最简洁,公式接口,适合只算一两个统计量:

# 按 cyl 分组计算 mpg 的均值 aggregate(mpg ~ cyl, data = mtcars, mean)

by()适合对每组应用一个返回多值的函数,比如把 2.3 节的mystats套进去:

# 先定义一个对数据框每列应用 mystats 的包装 dstats <- function(x) sapply(x, mystats) # 按 cyl 分组,对 mpg/hp/wt 三列分别算全套统计量 by(mtcars[myvars], mtcars$cyl, dstats)

by()的返回是一个列表,每组一个元素,打印出来层级较深,适合交互式查看,不适合直接导出。

doBy包的summaryBy()则是“分组 + 多统计量 + 返回数据框”一步到位,最适合出报告:

# 安装并加载 doBy install.packages("doBy") library(doBy) # 按 cyl 分组,对三个变量应用 mystats,结果直接是数据框 summaryBy(mpg + hp + wt ~ cyl, data = mtcars, FUN = mystats)

三者的取舍:只要一个均值用aggregate;要套自定义多值函数、临时看看用by;要批量出结构化结果、接可视化或导出,用summaryBy。参数上summaryBy的FUN接受任何返回向量的函数,mystats返回命名向量,所以结果列名会自动带上n、mean、skew这些名字,省去手动改列名。

4. ggplot2 出图:直方图与箱线图里藏着的分布真相

数值表再全,也不如一张图直观。ggplot2的直方图和箱线图是描述性统计的标配,但默认参数经常把真相藏起来——分箱太粗看不出双峰,箱线图把离群点画成一堆点却不说有多少个。

4.1 直方图:binwidth 选错,双峰变单峰

# 安装并加载 ggplot2 install.packages("ggplot2") library(ggplot2) # mpg 的直方图,binwidth 控制分箱宽度 ggplot(mtcars, aes(x = mpg)) + geom_histogram(binwidth = 1, fill = "blue", color = "black")

binwidth = 1表示每个柱子覆盖 1 个单位的区间。这个参数是直方图最容易翻车的地方:设太大,双峰分布会被抹成单峰;设太小,柱子碎成锯齿看不出趋势。实操里我一般先用binwidth = diff(range(x))/30估一个初值,再手动微调。fill和color分别控制填充色和边框色,纯审美参数,不影响统计结果。

4.2 箱线图:分组看离散度和离群点

# 按 cyl 分组看 mpg 的箱线图 ggplot(mtcars, aes(x = factor(cyl), y = mpg)) + geom_boxplot()

x = factor(cyl)这一步不能省,cyl是数值型,不转因子的话 ggplot 会把它当连续轴,箱线图就画不出来。箱线图能同时给出中位数、四分位距和离群点,配合第 3 章的分组统计量一起看,能快速判断“某组均值高是因为整体偏高,还是因为几个极端值拉上去的”。如果离群点特别多,可以加outlier.shape = NA先隐藏,再单独用散点图层标出来,避免图面糊成一团。

注意:箱线图的“离群点”判定是 1.5 倍四分位距,这是统计惯例不是错误。看到一堆点别急着删,先回到Hmisc::describe()看极值列表,确认是录入错误还是真实的长尾。

5. 避坑与排查:五个让描述性统计结果失真的常见问题

这一章是我自己踩过、也见过别人反复踩的坑,每条按“现象 → 原因 → 解决”写,照着排查能省不少返工。

现象一:均值算出来是NA,但数据看着挺完整。原因:某一列存在缺失值,mean()默认na.rm = FALSE,一个NA就让整个结果变NA。解决:先colSums(is.na(df))数一遍缺失,再决定是na.rm = TRUE还是先插补;千万别在没确认缺失比例的情况下直接na.omit,样本量悄悄少一半是常事。

现象二:psych::describe()和手写mystats()的峰度差了好几个点。原因:两者公式不同,psych默认做样本偏倚校正,手写版是总体公式。解决:统一口径,要么全用包函数,要么全用手写,报告里注明用的是哪一种;跨工具对比前先拿一组标准正态随机数验证。

现象三:aggregate()分组后某些组直接消失。原因:分组变量里有NA,aggregate()默认把NA组丢掉且不报错。解决:分组前table(df$group, useNA = "ifany")看一眼有没有NA组,有的话决定是归入“未知”类还是剔除,别让它无声消失。

现象四:直方图看着是单峰,但偏度算出来明显不为零。原因:binwidth太大,把双峰或长尾抹平了。解决:把binwidth调小到原来的三分之一再画一次,同时对照describe()的偏度值和极值列表交叉验证。

现象五:summaryBy()报错说FUN返回长度不一致。原因:传进去的函数在不同组返回的向量长度不同,比如某组全是NA导致sd()返回NA而其他统计量正常。解决:在自定义函数里对NA做显式处理,保证任何输入都返回固定长度的命名向量,这是批量聚合能跑通的前提。

6. 把口径固化成模板:一个可复用的描述性统计函数与验证习惯

前面几章的函数都是散的,真正干活时我习惯把它们收拢成一个入口函数,输入数据框和变量名,一次吐出数值表加两张图,避免每次重新拼代码。核心思路是把 2.3 节的mystats、3.2 节的psych::describe()和 4 章的绘图串起来:

# 一站式描述性统计:返回统计表,并可选输出图 desc_report <- function(df, vars, group = NULL, plot = TRUE) { sub <- df[vars] # 数值统计表:psych 返回数据框,便于导出 stat_tbl <- psych::describe(sub, fast = TRUE) # 分组统计(若指定分组变量) if (!is.null(group)) { grp_tbl <- doBy::summaryBy( reformulate(vars, response = group), data = df, FUN = mystats ) print(grp_tbl) } # 可选:每个变量画一张直方图 if (plot) { for (v in vars) { p <- ggplot2::ggplot(df, ggplot2::aes(x = .data[[v]])) + ggplot2::geom_histogram(binwidth = diff(range(df[[v]], na.rm = TRUE)) / 30, fill = "steelblue", color = "black") + ggplot2::labs(title = paste("Distribution of", v)) print(p) } } return(stat_tbl) } # 调用示例 desc_report(mtcars, c("mpg", "hp", "wt"), group = "cyl")

这段代码里几个参数值得说清楚:reformulate(vars, response = group)动态拼出mpg + hp + wt ~ cyl这种公式,变量名多的时候不用手写;.data[[v]]是 ggplot2 的 tidy 评估写法,循环里传字符串变量名必须这么写,直接写aes(x = v)会把v当成列名去找然后报错;binwidth用diff(range(...))/30自动估初值,比写死数字稳。fast = TRUE在变量多、样本大时能明显提速。

验证习惯上,我每次跑完这套都会做三件事:一是拿summary()的结果和psych的median对一遍,中位数对不上说明子集取错了;二是看n列是否等于预期样本量,少了就是缺失值在作祟;三是随机抽一个变量手算一遍均值和标准差,和函数结果比对。这套“三查”花不了两分钟,但拦下过好几次因为列名拼错、子集取错导致的整表失真。从那以后我每次接手新数据集,都强制先跑一遍desc_report再动任何模型,宁可前面多花十分钟,也不想在建模阶段回头查“这个均值怎么这么怪”。希望这套模板能帮到你,少走几段我走过的弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进