直方图展示什么
直方图只回答一个数值变量的一个问题:这些值是怎么分布的?它把数据的取值范围切成一段段相邻的区间(bin),统计每个区间里有多少个观测值,再为每个区间画一根柱子,柱子的高度就是这个计数。数据密集的地方柱子高,数据稀疏的地方柱子矮。
这一张图能告诉你均值和标准差说不出来的东西:数据有一个峰还是两个峰,是对称的还是偏向某一侧的尾部,有没有远离其他数据的离群值。拿到一个新数据集时,它通常就是你画的第一张图,和描述性统计里的那些数字放在一起看。
在 R 中,对应的函数是 hist(),一行就够了。
用 hist() 画第一张直方图
我们来模拟 200 个人的身高,服从均值 170 cm、标准差 10 的正态分布(set.seed() 和 rnorm() 的作用见随机数):
set.seed(42)
heights <- rnorm(200, mean = 170, sd = 10)
hist(heights)
R 会自己选择区间,画出一堆呈钟形的柱子:145 cm 附近的柱子很矮,经过 160 多一路升高,在 170 左右达到峰值,过了 190 后又对称地降下去。纵轴的标签是 “Frequency”,也就是原始计数,标题和坐标轴标签则来自变量名。
默认样式故意做得很朴素。常用的美化参数在这里都能用:
hist(heights,
main = "Distribution of heights",
xlab = "Height (cm)",
col = "steelblue",
border = "white")
col 设置柱子的填充色,border 设置柱子的边框颜色;border = "white" 能得到大多数出版物里那种柱子清爽分开的效果。plot() 指南里讲的颜色和标题用法在这里同样适用。
区间宽度:breaks 参数
hist() 最重要的参数是 breaks,因为区间宽度决定了这张图讲出什么样的故事。同一份数据,两种设置:
hist(heights, breaks = 5) # five wide bins: a crude, blocky bell
hist(heights, breaks = 30) # thirty narrow bins: detail, plus noise
用 breaks = 5 时,直方图粗糙到所有东西都像一个光滑的单峰,数据里的第二个聚集或者空缺都会被掩盖。用 breaks = 30 时,你能看到细节,但随机的波动也开始伪装成特征。两者都谈不上“正确”,老实的做法是多试几种设置,看看哪些特征始终存在。
breaks 接受三种形式:
- 一个数字:
breaks = 30只是对区间数量的建议。R 会调整它,让边界落在整齐的位置,所以你可能得到 28 个或 33 个区间。每个人都会被这一点吓到一次。 - 切分点向量:
breaks = seq(140, 200, by = 5)指定精确的区间边界,没有商量余地。当你要比较几张直方图、需要它们的区间对齐时,就用这种写法。 - 规则名称:
breaks = "FD"表示 Freedman-Diaconis 规则,它根据数据的离散程度和样本量来选择宽度,对偏态数据表现良好。
密度直方图与正态曲线
默认情况下,柱子的高度是计数(freq = TRUE)。设置 freq = FALSE 会重新缩放柱子,让它们的总面积为 1,这就是密度刻度。形状不变,变的是纵轴。这样做的意义在于,密度直方图和概率密度函数处在同一个刻度上,所以你可以直接在数据上叠加一条理论曲线:
hist(heights,
freq = FALSE,
col = "gray90",
main = "Heights vs. a normal curve",
xlab = "Height (cm)")
curve(dnorm(x, mean = mean(heights), sd = sd(heights)),
add = TRUE, col = "tomato", lwd = 2)
带 add = TRUE 的 curve() 会在已有的直方图上画出钟形曲线(dnorm(x, ...) 里的 x 是由 curve() 填入的占位符,不是你自己的变量)。如果柱子紧贴着曲线,说明正态模型是合理的;如果柱子在某处鼓出曲线之外,比如尾部偏重或出现第二个峰,说明模型漏掉了什么。如果省略 freq = FALSE,曲线就会无用地趴在图的底部,因为计数和密度不在同一个刻度上。
柱子背后的数字
hist() 不只是画图,它还会以列表的形式返回分箱结果。加上 plot = FALSE 后,它完全跳过绘图、只做计算,所以下面这段可以直接在这里运行:
h$counts 就是数据形式的直方图:每个数字就是一根柱子的高度。table(cut(...)) 这一行是它的文本版(cut() 把值分到各个区间,table() 统计每个区间),在画图之前或者代替画图把它打印出来,是一个不错的检查手段。如果你赋值时没有加 plot = FALSE(h <- hist(heights)),R 会既画图又返回列表;赋值并不会阻止画图。
用 ggplot2 画直方图
ggplot2 的写法用 binwidth 代替 breaks,这往往是更自然的调节方式:你直接用数据单位说明一个区间有多宽,而不是说想要多少个区间:
library(ggplot2)
ggplot(data.frame(heights), aes(x = heights)) +
geom_histogram(binwidth = 5, fill = "steelblue", color = "white") +
labs(title = "Distribution of heights", x = "Height (cm)", y = "Count")
binwidth = 5 表示每根柱子覆盖 5 cm。如果你不设置 binwidth,ggplot2 会发出警告,并悄悄默认使用 30 个区间。请重视这个警告,因为默认值很少正好适合你的数据。只想快速看一眼某个变量时,hist() 打字更少;当直方图需要分面、分组,或者要和其他图保持一致的主题时,ggplot2 才真正值得用。
要点回顾
- 直方图展示一个数值变量的分布:
hist(x)就搞定了。 - 关键参数是
breaks:数字只是建议,向量指定精确边界,"FD"会选出一个站得住脚的宽度。一定要多试几种设置。 freq = FALSE切换到密度刻度,正因如此,curve(dnorm(...), add = TRUE)叠加的正态曲线才有意义。hist(x, plot = FALSE)以数据形式返回分箱结果($breaks、$counts、$mids);table(cut(x, breaks))是纯文本的等价写法。- 在 ggplot2 中用
geom_histogram(binwidth = ...),并且自己设置binwidth。
下一篇:箱线图,在并排比较多个分组时尤其出彩的分布图。
常见问题
R 语言怎么画直方图?
对一个数值向量调用 hist(x)。R 会把 x 的取值范围切成若干个区间(bin),每个区间画一根柱子,柱子的高度表示落在该区间里的值有多少个。加上 breaks = 可以控制区间数量,加上 col = 可以给柱子上色。
hist() 的 breaks 参数有什么作用?
它控制分箱方式。单个数字如 breaks = 30 只是对区间数量的建议(R 会调整到整齐的边界),向量如 breaks = seq(140, 200, by = 5) 指定精确的切分点,而 breaks = "FD" 使用 Freedman-Diaconis 规则。
R 语言怎么在直方图上叠加正态曲线?
先用 freq = FALSE 在密度刻度上画直方图,再用 curve(dnorm(x, mean = mean(data), sd = sd(data)), add = TRUE) 加上曲线。在频数刻度上,这条曲线会贴在零附近毫无用处,所以 freq = FALSE 必不可少。
直方图和条形图有什么区别?
直方图对一个数值变量分箱,所以横轴是连续的刻度,柱子彼此相连。条形图比较的是彼此独立的类别,所以柱子之间有间隔。在 R 中,数值用 hist(),类别计数用 barplot()。