R语言实战:ggplot2与scatterpie绘制热力地图复合气泡饼图
2026/8/14 18:52:43 网站建设 项目流程

1. 项目概述:当热力地图遇上气泡饼图

在数据可视化的世界里,我们总是在寻找更高效、更直观的方式去呈现复杂多维度的信息。如果你经常用R语言做数据分析,尤其是涉及地理空间、生物信息学或者任何需要同时展示位置、强度和类别构成的数据时,你可能会遇到一个经典的难题:如何在一张图上,既能看到数据的空间分布密度(热力),又能看清每个具体点位上的多类别组成比例?单独的热力地图擅长展示宏观趋势,而气泡饼图则精于微观解构。将两者复合,就能创造出一种“既见森林,又见树木”的强大视图。这就是“热力地图复合气泡饼图”要解决的问题。

这个项目听起来有点炫技,但它的实用价值极高。想象一下,你手头有一份全国各城市的商业数据,你既想了解哪个区域的整体市场热度最高(热力),又想分析每个城市内部不同产品线的销售额占比(饼图)。传统的做法可能是画两张图,或者用复杂的分面,但都不如将信息叠加在一张图上来得直接震撼。在生物信息学领域,这种图的应用更为广泛,比如在展示基因表达量空间分布的同时,呈现不同功能通路的富集情况。

实现这个效果的核心,在于R语言中两个强大工具的联姻:用于绘制高质量统计图形的ggplot2生态系统,以及一个名为scatterpie的扩展包。网络上流传的许多代码片段,往往只给出了一个骨架,缺乏对数据准备、美学调整和问题排查的深度讲解。今天,我就从一个实战者的角度,带你从零开始,一步步拆解如何用R语言打造一张专业级的热力地图复合气泡饼图,并分享那些只有踩过坑才知道的细节。

2. 核心思路与工具选型解析

2.1 为什么是 ggplot2 + scatterpie?

在R的可视化江湖里,ggplot2早已是事实上的标准。它的图层(Layer)语法哲学,允许我们像搭积木一样组合不同的图形元素。热力地图本质上是一种基于网格的二维密度估计图,在ggplot2中可以通过geom_density_2d()stat_density_2d()或者结合geom_tile()与统计变换来实现。而气泡饼图,并不是ggplot2的原生几何对象。

这时,scatterpie包就登场了。它提供了一个名为geom_scatterpie()的函数,专门用于在指定坐标(x, y)上绘制比例饼图,并且饼图的大小(半径)可以映射到另一个变量(比如总数值),从而形成“气泡饼图”。它的工作原理是在每个点位,绘制一个由多个扇形组成的圆,每个扇形对应一个类别,其角度由该类别的比例决定。

因此,我们的复合图技术路径就清晰了:

  1. 底层(背景层):使用ggplot2的相关几何对象或统计变换,生成热力地图图层,用以展示数据的整体空间分布密度。
  2. 上层(前景层):使用scatterpie包的geom_scatterpie(),在具体的坐标点上叠加气泡饼图,用以展示每个点的多类别构成。

这种分层绘制的思路,完美契合了ggplot2的图层语法。选择这个组合,而不是尝试寻找一个“万能”的单一函数,原因在于其灵活性和可控性极高。我们可以分别调整热力图的色阶、平滑度和气泡饼图的大小、颜色、边框,直到获得最满意的视觉效果。

2.2 数据结构的核心要求

工欲善其事,必先利其“数据”。这是制作此类图表最关键,也最容易出错的一步。你的原始数据必须整理成特定的“长格式”或“中间格式”。

假设我们研究10个观测站点(点位),每个站点测量了3种污染物(A, B, C)的浓度,并且我们有点位的经纬度坐标。

错误/不兼容的格式(常见宽格式):

站点经度纬度污染物A浓度污染物B浓度污染物C浓度
S1116.439.915080120
S2121.531.29011095

这种格式geom_scatterpie()无法直接使用。

geom_scatterpie()需要的格式(长格式变体):它要求有一列指定x坐标,一列指定y坐标,然后为每个需要展示在饼图中的类别,单独设置一列,这一列的值代表该类别在该点的“量”(可以是绝对数值,scatterpie内部会计算比例;也可以是比例本身,但需确保每行比例之和为1)。

因此,我们需要将数据转换为:

站点经度(x)纬度(y)ABC
S1116.439.915080120
S2121.531.29011095

注意,这里的A、B、C三列是平行的。在geom_scatterpie(aes(x=经度, y=纬度))中,我们需要通过cols = c(“A”, “B”, “C”)参数来指定哪些列是饼图的组成部分。

实操心得:数据转换是第一步,也是最容易卡住新手的一步。我强烈建议使用tidyr包中的pivot_longer()pivot_wider()函数来完成这种变形,它们比传统的reshape2包更直观。例如,将上面的宽格式变成长格式再变回适合scatterpie的格式,可以加深你对数据结构的理解。

3. 分步实战:从数据到成图

下面,我将用一个模拟的环境监测数据案例,演示完整的流程。假设我们在一个区域内布设了50个传感器,监测PM2.5、SO2、NO2三种污染物的浓度。

3.1 环境准备与数据模拟

首先,加载必要的R包并创建模拟数据。

# 加载必要的包 library(ggplot2) library(scatterpie) # 核心工具包 library(viridis) # 提供优美的色阶,用于热力图 library(tidyverse) # 包含dplyr, tidyr等,用于数据操作 # 设置随机种子保证结果可重现 set.seed(123) # 模拟50个观测点的经纬度(在一个限定区域内) n_points <- 50 data <- data.frame( site_id = paste0("Site_", 1:n_points), lon = runif(n_points, min = 115.0, max = 118.0), # 模拟经度范围 lat = runif(n_points, min = 35.0, max = 41.0), # 模拟纬度范围 pm25 = rgamma(n_points, shape = 2, rate = 0.05), # 模拟PM2.5浓度,右偏分布 so2 = rgamma(n_points, shape = 3, rate = 0.1), # 模拟SO2浓度 no2 = rgamma(n_points, shape = 4, rate = 0.08) # 模拟NO2浓度 ) # 查看数据结构 head(data)

此时,data数据框已经是我们需要的格式了:每一行是一个站点,有lon(x坐标),lat(y坐标),以及pm25,so2,no2三个类别的数值列。

3.2 绘制基础热力地图

热力地图可以通过二维核密度估计来生成,它展示了观测点空间分布的密集程度。我们使用stat_density_2d()结合geom = “polygon”aes(fill = after_stat(level))来实现填充色的密度图。

# 创建基础热力地图图层 heatmap_layer <- ggplot(data, aes(x = lon, y = lat)) + stat_density_2d( aes(fill = after_stat(level)), geom = “polygon”, # 使用多边形几何对象 contour_var = “density”, bins = 15, # 设置等高线/密度等级数 alpha = 0.6 # 设置一定透明度,避免完全遮盖底图或后续饼图 ) + scale_fill_viridis_c( option = “plasma”, # 使用viridis色系的plasma方案 name = “密度”, guide = guide_colorbar(barwidth = 10, barheight = 0.5) # 调整图例样式 ) + labs(x = “经度”, y = “纬度”) + theme_minimal(base_size = 12) + theme( legend.position = “bottom”, # 将图例放在底部 panel.grid = element_blank() # 去除网格线,让图面更干净 ) # 先查看热力图层 print(heatmap_layer)

这一步生成了一个基于点分布密度的热力背景。bins参数控制颜色的平滑度,值越大,颜色过渡越细致,但计算量也越大。alpha参数设置为0.6,是为了让后续叠加的气泡饼图能够清晰可见。

3.3 叠加气泡饼图图层

接下来,我们在热力图层上叠加气泡饼图。关键函数是geom_scatterpie()。我们需要指定饼图组成部分的列名,并映射饼图的大小。

# 定义饼图组成部分的列名 pie_cols <- c(“pm25”, “so2”, “no2”) # 在热力地图上叠加气泡饼图 composite_plot <- heatmap_layer + geom_scatterpie( data = data, aes(x = lon, y = lat, r = total_pollution / 200), # r 定义饼图半径,通过一个变量进行缩放 cols = pie_cols, # 指定构成饼图的列 color = “grey40”, # 饼图扇形边框颜色 alpha = 0.85 # 饼图整体透明度 ) + # 为饼图的每一部分(污染物)指定颜色 scale_fill_manual( name = “污染物”, # 图例标题 values = c(“pm25” = “#E69F00”, # 橙色 “so2” = “#56B4E9”, # 蓝色 “no2” = “#009E73”), # 绿色 labels = c(“PM2.5”, “SO2”, “NO2”) # 修改图例标签 ) + # 注意:此处使用了scale_fill_manual,它会覆盖热力图的fill图例。 # 更复杂的图例控制需要用到guides()系统或其它方法。 # 查看复合图 print(composite_plot)

这里有几个至关重要的细节:

  1. 半径映射(r参数)r决定了每个饼图的大小。通常,我们会将其映射到一个代表该点“总强度”的变量。在上面的代码中,我使用了total_pollution / 200,但我们的原始数据里并没有total_pollution这一列。这是一个常见的疏忽。我们必须先创建这个变量。
  2. 颜色冲突:热力地图和饼图都使用了fill美学(热力图的填充色和饼图扇形的填充色)。当我们使用scale_fill_manual为饼图指定颜色时,它会覆盖之前热力地图的scale_fill_viridis_c,导致热力图的色阶图例消失,变成污染物图例。

3.4 解决关键问题:数据预处理与图例管理

让我们修正上述两个问题。

首先,创建总浓度列并重新绘图:

# 数据预处理:计算每个站点的总污染浓度,用于决定气泡大小 data <- data %>% mutate(total_pollution = pm25 + so2 + no2) # 重新定义热力图层(与之前相同) heatmap_layer <- ggplot(data, aes(x = lon, y = lat)) + stat_density_2d(aes(fill = after_stat(level)), geom = “polygon”, bins = 15, alpha = 0.6) + scale_fill_viridis_c(option = “plasma”, name = “点位密度”) + labs(x = “经度”, y = “纬度”) + theme_minimal() + theme(legend.position = “bottom”, panel.grid = element_blank()) # 重新创建复合图,使用正确的半径映射 composite_plot <- heatmap_layer + geom_scatterpie( data = data, aes(x = lon, y = lat, r = total_pollution / max(total_pollution) * 0.1), # 动态计算半径 cols = pie_cols, color = “grey40”, alpha = 0.85 ) + scale_fill_manual( name = “污染物构成”, values = c(“pm25” = “#E69F00”, “so2” = “#56B4E9”, “no2” = “#009E73”), labels = c(“PM2.5”, “SO2”, “NO2”) ) print(composite_plot)

这里对半径r的计算做了优化:total_pollution / max(total_pollution) * 0.1。这会将最大的气泡半径设置为0.1(单位与坐标轴相同),其余气泡按比例缩小。乘数0.1需要根据你实际坐标轴的范围手动调整,以确保气泡大小适中,既不重叠严重,也不至于太小。

其次,解决图例冲突问题。ggplot2不允许同一个美学映射(fill)有两个独立的标度(scale)。一个巧妙的解决方法是,将热力图的fill美学重命名为另一个名字,比如fill2,然后为它单独设置标度。这可以通过在stat_density_2d内部修改美学映射,并使用guides()guide_colorbar()来分别控制两个图例。

final_plot <- ggplot(data, aes(x = lon, y = lat)) + # 热力图层:使用 fill2 作为填充美学名称 stat_density_2d( aes(fill2 = after_stat(level)), # 注意这里改为 fill2 geom = “polygon”, bins = 15, alpha = 0.6 ) + # 为 fill2 美学设置颜色标度(热力图) binned_scale( aesthetics = “fill2”, scale_name = “custom_viridis”, palette = function(x) viridis::viridis_pal(option=“plasma”)(x), name = “观测点密度”, guide = guide_colorbar( barwidth = 10, barheight = 0.5, order = 1 # 控制图例顺序 ) ) + # 气泡饼图层 geom_scatterpie( aes(x = lon, y = lat, r = total_pollution / max(total_pollution) * 0.08), data = data, cols = pie_cols, color = NA, # 去掉扇形边框,让图更简洁 alpha = 0.9 ) + # 为饼图的 fill 美学设置颜色标度(污染物) scale_fill_manual( name = “污染物构成”, values = c(“pm25” = “#FF6B6B”, “so2” = “#4ECDC4”, “no2” = “#556270”), # 更换一组更协调的颜色 labels = c(“PM2.5”, “SO2”, “NO2”), guide = guide_legend(order = 2) # 控制图例顺序 ) + labs( x = “经度”, y = “纬度”, title = “区域污染物观测分布与构成分析”, subtitle = “背景色表示传感器空间密度,气泡饼图表示各点位三种污染物浓度比例与总量” ) + theme_minimal(base_size = 13) + theme( legend.position = “bottom”, legend.box = “horizontal”, # 图例水平排列 legend.spacing.x = unit(0.5, ‘cm’), # 调整图例间距 panel.grid = element_blank(), plot.title = element_text(hjust = 0.5, face = “bold”), plot.subtitle = element_text(hjust = 0.5, size = 10, color = “grey40”) ) + # 协调坐标轴比例,避免图形被拉伸 coord_fixed(ratio = 1.2) # 根据经纬度范围调整ratio值,使地图看起来更自然 # 输出最终图形 print(final_plot) # 保存图形 ggsave(“heatmap_scatterpie_composite.png”, final_plot, width = 12, height = 8, dpi = 300)

这段代码是最终的核心。我们通过aes(fill2 = …)将热力图的填充美学“重命名”,从而为fill(饼图)和fill2(热力图)分别配置了独立的标度和图例。binned_scale函数用于为这个非标准的fill2美学创建颜色条。guide函数中的order参数用于精确控制两个图例的上下排列顺序。

4. 高级定制与美化技巧

一张能用于报告或出版的图,离不开细节的打磨。

4.1 气泡大小的智能映射

之前我们简单地将半径与总浓度线性关联。但在实际中,如果数据跨度大(比如有几个极大值),线性映射会导致大多数气泡很小,个别气泡巨大。更好的方法是使用平方根或对数变换,因为人眼对面积的感知更接近线性,而非半径。

# 方法一:平方根变换,使气泡面积与总浓度呈线性关系 data <- data %>% mutate(bubble_radius = sqrt(total_pollution) / max(sqrt(total_pollution)) * 0.1) # 在geom_scatterpie中,使用 aes(r = bubble_radius) # 方法二:对数变换,压缩极值的影响 data <- data %>% mutate(bubble_radius = log10(total_pollution + 1) / max(log10(total_pollution + 1)) * 0.12)

实操心得:我通常先尝试平方根变换,因为它计算简单且物理意义明确(面积代表总量)。用+1是为了防止总浓度为0时对数计算错误。务必通过max()进行归一化,再乘以一个基准半径(如0.08-0.12),这个基准半径需要你根据绘图区域的坐标轴范围反复调试,直到视觉效果最佳。

4.2 处理饼图重叠与遮挡

当观测点非常密集时,气泡饼图会严重重叠,导致无法辨认。有几种策略:

  1. 减小半径:这是最直接的方法,调整r的乘数因子。
  2. 对数据点进行空间稀疏化:如果业务允许,可以使用聚类算法(如k-means)对邻近的点进行聚合,用聚合后的中心点和加总的污染物数据来绘图。
  3. 使用geom_scatterpie()pie_scale参数:这个参数可以整体缩放所有饼图的大小,但不如直接控制r灵活。
  4. 分面显示:如果数据有另一个维度(如时间),可以考虑使用facet_wrap()按时间分面,每张图上的点就少了。

4.3 添加地理背景(Shapefile)

让热力地图更有意义,可以叠加真实的地理边界。这需要sf(Simple Features)包的支持。

library(sf) library(rnaturalearth) # 方便获取世界地图数据 # 获取中国省级地图(示例,需确保rnaturalearthhires包已安装) china_province <- ne_states(country = “china”, returnclass = “sf”) # 在绘图时,将地图作为第一个图层(geom_sf) ggplot() + geom_sf(data = china_province, fill = “white”, color = “grey60”, size = 0.2) + # 先画地图背景 stat_density_2d(data = data, aes(x = lon, y = lat, fill2 = after_stat(level)), … ) + # 热力图层 geom_scatterpie(…) + # 饼图图层 # … 其他缩放、主题设置 # 重要:限制坐标轴范围,使其与地图区域匹配 coord_sf(xlim = c(115, 118), ylim = c(35, 41), datum = NA) # datum=NA移除网格和经纬线

5. 常见问题与排查实录

即使按照步骤操作,你也可能会遇到一些“坑”。这里记录了几个最常见的问题及其解决方法。

5.1 错误: “object ‘xxx’ not found” 或饼图不显示

  • 问题描述:运行geom_scatterpie()时提示某列不存在,或者图形上完全没有饼图。
  • 原因排查
    1. cols参数错误:检查cols = c(“A”, “B”, “C”)中的字符串,是否与数据框中的列名完全一致(大小写、空格)。
    2. 数据格式错误:确认你的数据框格式是“宽格式”,即每个类别是单独的一列,而不是“长格式”下的一列。
    3. r参数计算错误:如果用于计算半径的列包含NAInf或负值,可能导致饼图尺寸为0或计算错误而不显示。使用summary(data$your_radius_column)检查数据。
  • 解决方案
    # 1. 精确匹配列名 print(names(data)) # 2. 确保数据格式正确 head(data) # 3. 清理半径计算数据 data <- data %>% mutate(total = rowSums(across(all_of(pie_cols)), na.rm = TRUE)) %>% filter(total > 0) # 移除总量为0或NA的点

5.2 热力图颜色完全覆盖饼图或饼图颜色异常

  • 问题描述:热力图的颜色太深,盖住了气泡饼图;或者饼图的颜色不是预设的颜色。
  • 原因排查
    1. 图层顺序:在ggplot2中,后添加的图层会绘制在先添加的图层之上。确保geom_scatterpie()在热力图图层之后添加。
    2. 透明度(alpha)设置:热力图的alpha值太低(太透明)会被饼图完全覆盖,太高(不透明)则会遮盖饼图。通常热力图alpha设在0.4-0.7,饼图alpha设在0.8-1.0。
    3. 颜色标度冲突:如前所述,fill美学被重复定义。必须使用fill2或其他方式分离两个图层的填充色标度。
  • 解决方案:严格按照3.4节中的代码结构,先画热力图(并使用fill2美学),再画饼图(使用fill美学),并分别为它们配置独立的scale_*函数。

5.3 图形保存后分辨率低或元素模糊

  • 问题描述:在RStudio的预览窗口里图形很清晰,但用ggsave()保存为PNG或PDF后,文字或图形边缘模糊。
  • 原因与解决
    1. DPI设置过低ggsave()默认DPI是300,对于印刷或高清展示可能不够。可以尝试dpi = 600
    2. PDF保存的字体嵌入问题:保存为PDF时,默认的ggsave()可能不会将字体完全嵌入。使用device = cairo_pdf可以更好地处理字体。
    3. 图形尺寸过小:如果设置的widthheight太小,再高的DPI也无法容纳足够多的细节。
    # 高质量保存示例 ggsave(“my_plot.png”, plot = final_plot, width = 16, height = 10, dpi = 600, bg = “white”) ggsave(“my_plot.pdf”, plot = final_plot, width = 16, height = 10, device = cairo_pdf)

5.4 性能优化:当数据点过多时绘图缓慢

  • 问题描述:当有成千上万个点时,stat_density_2d()geom_scatterpie()的计算和渲染会非常慢。
  • 优化策略
    1. 热力图:降低stat_density_2d()中的bins参数值,或使用geom_bin2d()(矩形分箱)替代,后者速度通常更快。
    2. 气泡饼图:这是性能瓶颈的主要来源。每个点都要绘制一个复杂的扇形多边形。
      • 抽样:如果业务允许,对数据进行随机抽样或系统抽样,减少绘图点数。
      • 聚合:如前所述,将邻近的点进行空间聚类聚合。
      • 简化:对于比例构成非常接近的点,可以考虑用纯色气泡代替饼图,用图例说明整体平均构成。
    3. 关闭图形实时预览:在脚本中,将绘图和保存命令放在最后,避免在交互环境中反复渲染大型图形。

制作热力地图复合气泡饼图是一个对数据结构和ggplot2图层语法理解程度的综合考验。它没有一键生成的魔法函数,但通过拆解为“热力背景”和“饼图标记”两个逻辑层,并精心处理数据、美学映射和图例,你就能创造出信息密度极高且视觉效果专业的分析图表。记住,所有的参数调整,尤其是颜色、大小和透明度,都需要服务于清晰、准确地传达信息这一最终目的。多尝试,多调整,这张复合图将成为你数据可视化工具箱中一件得心应手的利器。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询