← 返回 Tessera
分布5 张图airquality(Tessera Toy,153 行 × 5 个月)2026-08-11

density

一组或几组连续数据主要集中在哪里,分布形状是否不同?

需要的输入
1 个连续数值变量(可选:再加 1 个分组变量)
示例数据
airquality(Tessera Toy,153 行 × 5 个月)
依赖
ggplot2 · ggpubr · ggridges · biopalette
配色
walter_white2
成图预览另有 4 张在配方里
density

什么时候用它

密度图把一列连续观测平滑成一条分布曲线。曲线高的地方表示观测更集中,低而长的尾部表示少量观测延伸得更远。它适合回答“数据主要落在哪里”“是否偏斜”“有没有一个以上的峰”,而不是读取某个精确观测值。

密度图与箱线图互补:箱线图用中位数和四分位数给出稳定摘要,密度图保留完整形状,但形状会受到带宽影响。小样本优先画原始点或箱线图;每组至少有二三十个观测时,密度轮廓才开始有解释价值。

曲线怎么算

核密度估计可以理解为:在每个观测上放一个小的平滑鼓包,再把所有鼓包相加。geom_density() 默认使用高斯核;真正决定曲线长相的主要参数是带宽。

  • 带宽小:保留更多局部起伏,也更容易把随机噪声画成多个峰。
  • 带宽大:曲线更稳定,但相邻峰可能被抹成一个宽峰。
  • 曲线下面积固定为 1:峰高表示相对集中程度,不是样本数。组间样本量不同,默认密度曲线仍然各自归一化。

adjust 是默认带宽的倍数,而不是一个新的统计方法。正式出图前应在合理范围内比较几档带宽,确认结论不是某一个参数制造出来的。

怎么读

  1. 先看位置。 整条曲线向右移动,说明这一组整体取值更高。
  2. 再看宽窄。 分布越宽,观测越分散。
  3. 看偏斜和尾部。 一侧拖得更长,说明极端方向不对称。
  4. 谨慎看多峰。 小鼓包可能只是样本少或带宽过小,不一定存在真实亚群。
  5. 分组图必须共用横轴。 每个面板自行缩放会让不同月份看起来同样宽,失去比较基础。

常见陷阱

  • 把曲线高度当人数。 标准密度的面积是 1;要表达样本量,应另外标注 n,不要靠峰高猜。
  • 组数多仍全部叠在一起。 透明度只能缓解遮挡,还会产生色板里不存在的混合色。三组以上通常改用分面或分层密度。
  • 用透明混色评价色板。 重叠区看到的是浏览器或绘图设备混合后的颜色,不是原始色板;Palette Lab 的主图因此采用互不覆盖的分层布局。
  • 带宽只试一个值。 一个看似明确的双峰,可能把 adjust0.6 改成 1.2 就消失。
  • 忽略缺失值。 geom_density(na.rm = TRUE) 会安静地删掉缺失;应先报告每组真正参与估计的 n
  • 类别顺序交给字母序。 月份、剂量等有序分组必须显式固定 levels,否则曲线顺序可能既不符合时间,也不符合图例。
  • 裁掉密度尾部。scale_x_continuous(limits = ...) 会先删除范围外观测再重新估计密度;只想放大画面应使用 coord_cartesian()

配色

月份从五月到九月有自然顺序,但每个月仍是独立分组。这里使用 walter_white2 的五种颜色保持月份身份;分层图按时间顺序排列,让位置承担主要顺序信息,颜色负责快速定位。

重叠密度需要透明度,但透明度会改变明度并制造混合色,所以它适合分析,不适合单独判断色板。分面和分层版本让每组颜色完整铺开,更适合观察大面积填色、轮廓清晰度以及相邻颜色是否容易混淆。色板不足组数时不要循环用色,应保留曲线并把未编码组改成中性上下文。

配方

方法绘图系统什么时候选它
Ageom_density()ggplot2先看一个连续变量的整体位置、离散程度与峰形
Baes(fill = group, color = group)ggplot2组数很少、曲线重叠有限,需要在同一坐标系直接比较
Cfacet_wrap()ggplot2重叠已经遮住形状,希望各组保持共同坐标但独立阅读
Dgeom_density_ridges()ggplot2(ggridges 扩展)组别有自然顺序,既要比较位置,也要保留每组完整轮廓

数据与公共样式

airqualityTemp 没有缺失,五个月各有 30 或 31 天。月份显式固定为 May–September,所有图使用相同横轴和同一档基础带宽。

library(ggplot2)
library(ggpubr)
library(ggridges)
library(biopalette)

d <- read.csv("content/tessera/data/csv/airquality.csv", na.strings = "NA")
month_levels <- 5:9
month_labels <- c("May", "June", "July", "August", "September")
d$month <- factor(
  d$Month,
  levels = month_levels,
  labels = month_labels,
  ordered = TRUE
)

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)
  )

A · 单组密度

先忽略月份,只看 153 天温度的整体分布。adjust = 1 使用 ggplot2 计算的默认带宽;填充只帮助看面积,不承担分组含义。

#| fig: basic
#| fig-width: 8
#| fig-height: 5.2
ggplot(d, aes(Temp)) +
  geom_density(
    adjust = 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",
    x = "Temperature (°F)",
    y = "Density"
  ) +
  density_theme
density — basic
density-basic

B · 分组重叠

把月份映射给 fillcolor 可以直接比较共同坐标上的位置差异。透明度降低到 0.24 才能看见后画的曲线,但重叠区已经不是原始色板颜色;五组也接近这种画法的可读上限。

#| fig: overlay
#| fig-width: 8
#| fig-height: 5.6
ggplot(d, aes(Temp, fill = month, color = month)) +
  geom_density(adjust = 0.9, alpha = 0.24, linewidth = 0.8) +
  density_scale +
  scale_fill_manual(values = month_colors) +
  scale_color_manual(values = month_colors) +
  labs(
    title = "Overlaid monthly densities",
    subtitle = "Transparency reveals intersections but changes the palette",
    x = "Temperature (°F)",
    y = "Density",
    fill = "Month",
    color = "Month"
  ) +
  density_theme +
  theme(legend.position = "right")
density — overlay
density-overlay

C · 共同坐标的分面

分面消除遮挡,同时保留相同的横轴与纵轴。scales = "fixed" 不应省略:只有共同尺度下,位置、宽度和峰高才可以横向比较。

#| fig: facets
#| fig-width: 8.5
#| fig-height: 8.5
ggplot(d, aes(Temp, fill = month, color = month)) +
  geom_density(adjust = 0.9, alpha = 0.76, linewidth = 0.75, show.legend = FALSE) +
  facet_wrap(~ month, ncol = 1, scales = "fixed") +
  density_scale +
  scale_fill_manual(values = month_colors) +
  scale_color_manual(values = month_colors) +
  labs(
    title = "One density per month",
    subtitle = "Fixed axes preserve comparisons across panels",
    x = "Temperature (°F)",
    y = "Density"
  ) +
  density_theme +
  theme(
    strip.background = element_blank(),
    strip.text = element_text(face = "bold", hjust = 0)
  )
density — facets
density-facets

D · 分层密度

月份有明确顺序时,分层密度比五条透明曲线更清楚。scale = 0.86 让相邻轮廓留出间隙,不发生颜色混合;这也是 Palette Lab 使用的主版本。

#| fig: ridgeline
#| fig-width: 9
#| fig-height: 6.5
ggplot(d, aes(Temp, month, fill = month)) +
  geom_density_ridges(
    adjust = 0.9,
    scale = 0.86,
    rel_min_height = 0.01,
    color = "#333330",
    linewidth = 0.55,
    alpha = 0.92,
    show.legend = FALSE
  ) +
  density_scale +
  scale_fill_manual(values = month_colors) +
  labs(
    title = "Temperature shifts through the summer",
    subtitle = "Five independent density layers · no overlap mixing",
    x = "Temperature (°F)",
    y = NULL
  ) +
  theme_pubr(base_size = 13, legend = "none") +
  theme(
    panel.grid.major.x = element_line(color = "#E5E3DC", linewidth = 0.35),
    panel.grid.major.y = element_blank(),
    axis.line.y = element_blank(),
    axis.ticks.y = element_blank(),
    plot.title = element_text(face = "bold", size = 15, hjust = 0),
    plot.subtitle = element_text(color = "grey35", hjust = 0),
    plot.margin = margin(14, 16, 12, 12)
  )
density — ridgeline
density-ridgeline

E · 带宽敏感性

固定数据与样式,只改变 adjust。较小值保留许多局部峰,较大值把它们合并;如果核心判断只在某一格成立,就不应把它写成稳定结论。

#| fig: bandwidth
#| fig-width: 11
#| fig-height: 4.2
bandwidth_plot <- function(adjust_value) {
  ggplot(d, aes(Temp)) +
    geom_density(
      adjust = adjust_value,
      fill = unname(month_colors[1]),
      color = "#333330",
      alpha = 0.72,
      linewidth = 0.65
    ) +
    density_scale +
    labs(
      title = paste0("adjust = ", adjust_value),
      x = "Temperature (°F)",
      y = NULL
    ) +
    theme_pubr(base_size = 11, legend = "none") +
    theme(
      panel.grid.major.x = element_line(color = "#E5E3DC", linewidth = 0.3),
      panel.grid.major.y = element_blank(),
      plot.title = element_text(face = "bold", size = 12, hjust = 0),
      plot.margin = margin(8, 8, 8, 8)
    )
}

ggarrange(
  bandwidth_plot(0.55),
  bandwidth_plot(1),
  bandwidth_plot(1.6),
  ncol = 3,
  align = "hv"
)
density — bandwidth
density-bandwidth

五种画法怎么选

  1. 只看一个变量的整体形状 → A。
  2. 只有两三组且重叠不严重 → B。
  3. 需要最稳妥地逐组比较 → C。
  4. 组别有自然顺序、希望一张图紧凑展示 → D。
  5. 准备解释峰数或分布细节 → 先用 E 检查结论对带宽是否稳定。

evanverse::plot_density() 把 A–C 中最常用的 group_colfacet_colalphaadjustpalette 封装成统一接口。Recipe 没有依赖它,因为这里需要逐层展示标准 ggplot2 语法;其参数边界与分组逻辑仍是这份配方的设计参考。

运行环境4 个包 · 2026-08-11T17:52:11.288+0800

R version 4.5.1 (2025-06-13 ucrt) · x86_64-w64-mingw32

  • biopalette 0.1.0
  • ggplot2 4.0.3
  • ggpubr 0.6.2
  • ggridges 0.5.7