Introduction
密度图把一列连续观测平滑成一条分布曲线。曲线高的地方观测更集中,低而长的尾部表示少量观测延伸得更远。它回答"数据主要落在哪里""是否偏斜""有没有一个以上的峰",不回答某个精确观测值是多少。
它和箱线图互补:箱线图用中位数和四分位数给出稳定摘要,密度图保留完整形状,但形状会受带宽影响。小样本优先画原始点或箱线图;每组至少二三十个观测时,密度轮廓才开始有解释价值。
曲线是怎么算出来的:在每个观测上放一个小的平滑鼓包,再把所有鼓包相加(geom_density() 默认高斯核)。真正决定曲线长相的是带宽——带宽小保留更多局部起伏,也更容易把随机噪声画成多个峰;带宽大更稳定,但相邻的峰可能被抹成一个宽峰。adjust 是默认带宽的倍数,不是另一种统计方法。
读的时候按这个顺序:
- 先看位置。 整条曲线向右移动,说明这一组整体取值更高。
- 再看宽窄。 分布越宽,观测越分散。
- 看偏斜和尾部。 一侧拖得更长,说明极端方向不对称。
- 谨慎看多峰。 小鼓包可能只是样本少或带宽过小,不一定存在真实亚群。
- 分组图必须共用横轴。 每个面板自行缩放,会让不同月份看起来同样宽,失去比较基础。
Example Data
密度图要的输入是:1 个连续数值变量,可选再加 1 个分组变量。一行一个观测,不需要事先汇总。
airquality 是纽约 1973 年五到九月的逐日观测,153 行。这里用日最高气温 Temp——它没有缺失,五个月各有 30 或 31 天。
library(ggplot2)
library(ggpubr)
library(ggridges)
library(biopalette)
# 公开地址,和数据集页上「下载 CSV」给的是同一个 —— 不写仓库相对路径:
# 那个目录不进仓库,读者 clone 下来也没有这个文件,这段代码就跑不了
d <- read.csv("https://assets.evanzhou.org/tessera/csv/airquality.csv",
na.strings = "NA")
month_levels <- 5:9
month_labels <- c("May", "June", "July", "August", "September")
# 显式固定 levels:月份有时间顺序,交给字母序会排成 August / July / June…,
# 分层图上曲线的上下顺序就既不合时间、也不合图例
d$month <- factor(
d$Month,
levels = month_levels,
labels = month_labels,
ordered = TRUE
)
Palettes
月份从五月到九月有自然顺序,但每个月仍是独立分组。用 walter_white2 的五色保持月份身份;分层图按时间顺序排列,让位置承担顺序信息,颜色只负责快速定位。
重叠密度需要透明度,而透明度会改变明度并制造色板里不存在的混合色——重叠区看到的不是原始色板。所以它适合分析,不适合单独用来判断配色。分面和分层版本让每组颜色完整铺开,更适合观察大面积填色、轮廓清晰度以及相邻颜色是否容易混淆。
色板装不下组数时不要循环用色:保留曲线,把未编码的组改成中性上下文。
month_colors <- setNames(
get_palette("walter_white2", type = "qualitative")[1:5],
month_labels
)
# 五张图共用同一条横轴:位置和宽度要横着比,坐标就不能各画各的
density_scale <- scale_x_continuous(
breaks = seq(55, 95, 10),
expand = expansion(mult = c(0.03, 0.04))
)
density_theme <- theme_pubr(base_size = 13) +
theme(
panel.grid.major.y = element_blank(),
panel.grid.major.x = element_line(color = "#E5E3DC", linewidth = 0.35),
axis.line = element_line(color = "#333330", linewidth = 0.45),
axis.ticks = element_line(color = "#333330", linewidth = 0.4),
plot.title = element_text(face = "bold", size = 15, hjust = 0),
plot.subtitle = element_text(color = "grey35", hjust = 0),
legend.title = element_text(face = "bold"),
plot.margin = margin(14, 16, 12, 12)
)
Recipe
| No. | Method | Input Data | Palettes |
|---|---|---|---|
| 1 | ggplot2 |
d |
month_colors[1] |
| 2 | ggplot2 |
d |
month_colors |
| 3 | ggplot2 |
d |
month_colors |
| 4 | ggridges |
d |
month_colors |
| 5 | ggplot2 |
d |
month_colors[1] |
1 · 单组密度
先忽略月份,只看 153 天温度的整体分布。
#| fig: basic
#| fig-width: 8
#| fig-height: 5.2
ggplot(d, aes(Temp)) +
geom_density(
adjust = 1, # ggplot2 计算的默认带宽,1 倍
# 填充只帮助看面积,不承担分组含义 —— 这张图里没有分组
fill = unname(month_colors[1]),
color = "#333330",
alpha = 0.72,
linewidth = 0.75
) +
density_scale +
labs(
title = "Daily temperature distribution",
subtitle = "New York · May–September 1973 · n = 153", # n 写进副标题
x = expression("Temperature (" * degree * "F)"),
y = "Density"
) +
density_theme



