尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

R语言实战:ggplot2与scatterpie绘制热力地图复合气泡饼图

R语言实战:ggplot2与scatterpie绘制热力地图复合气泡饼图 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上绘制比例饼图并且饼图的大小半径可以映射到另一个变量比如总数值从而形成“气泡饼图”。它的工作原理是在每个点位绘制一个由多个扇形组成的圆每个扇形对应一个类别其角度由该类别的比例决定。因此我们的复合图技术路径就清晰了底层背景层使用ggplot2的相关几何对象或统计变换生成热力地图图层用以展示数据的整体空间分布密度。上层前景层使用scatterpie包的geom_scatterpie()在具体的坐标点上叠加气泡饼图用以展示每个点的多类别构成。这种分层绘制的思路完美契合了ggplot2的图层语法。选择这个组合而不是尝试寻找一个“万能”的单一函数原因在于其灵活性和可控性极高。我们可以分别调整热力图的色阶、平滑度和气泡饼图的大小、颜色、边框直到获得最满意的视觉效果。2.2 数据结构的核心要求工欲善其事必先利其“数据”。这是制作此类图表最关键也最容易出错的一步。你的原始数据必须整理成特定的“长格式”或“中间格式”。假设我们研究10个观测站点点位每个站点测量了3种污染物A, B, C的浓度并且我们有点位的经纬度坐标。错误/不兼容的格式常见宽格式站点经度纬度污染物A浓度污染物B浓度污染物C浓度S1116.439.915080120S2121.531.29011095这种格式geom_scatterpie()无法直接使用。geom_scatterpie()需要的格式长格式变体它要求有一列指定x坐标一列指定y坐标然后为每个需要展示在饼图中的类别单独设置一列这一列的值代表该类别在该点的“量”可以是绝对数值scatterpie内部会计算比例也可以是比例本身但需确保每行比例之和为1。因此我们需要将数据转换为站点经度x纬度yABCS1116.439.915080120S2121.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数据框已经是我们需要的格式了每一行是一个站点有lonx坐标laty坐标以及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)这里有几个至关重要的细节半径映射r参数r决定了每个饼图的大小。通常我们会将其映射到一个代表该点“总强度”的变量。在上面的代码中我使用了total_pollution / 200但我们的原始数据里并没有total_pollution这一列。这是一个常见的疏忽。我们必须先创建这个变量。颜色冲突热力地图和饼图都使用了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 处理饼图重叠与遮挡当观测点非常密集时气泡饼图会严重重叠导致无法辨认。有几种策略减小半径这是最直接的方法调整r的乘数因子。对数据点进行空间稀疏化如果业务允许可以使用聚类算法如k-means对邻近的点进行聚合用聚合后的中心点和加总的污染物数据来绘图。使用geom_scatterpie()的pie_scale参数这个参数可以整体缩放所有饼图的大小但不如直接控制r灵活。分面显示如果数据有另一个维度如时间可以考虑使用facet_wrap()按时间分面每张图上的点就少了。4.3 添加地理背景Shapefile让热力地图更有意义可以叠加真实的地理边界。这需要sfSimple 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) # datumNA移除网格和经纬线5. 常见问题与排查实录即使按照步骤操作你也可能会遇到一些“坑”。这里记录了几个最常见的问题及其解决方法。5.1 错误: “object ‘xxx’ not found” 或饼图不显示问题描述运行geom_scatterpie()时提示某列不存在或者图形上完全没有饼图。原因排查cols参数错误检查cols c(“A”, “B”, “C”)中的字符串是否与数据框中的列名完全一致大小写、空格。数据格式错误确认你的数据框格式是“宽格式”即每个类别是单独的一列而不是“长格式”下的一列。r参数计算错误如果用于计算半径的列包含NA、Inf或负值可能导致饼图尺寸为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 热力图颜色完全覆盖饼图或饼图颜色异常问题描述热力图的颜色太深盖住了气泡饼图或者饼图的颜色不是预设的颜色。原因排查图层顺序在ggplot2中后添加的图层会绘制在先添加的图层之上。确保geom_scatterpie()在热力图图层之后添加。透明度alpha设置热力图的alpha值太低太透明会被饼图完全覆盖太高不透明则会遮盖饼图。通常热力图alpha设在0.4-0.7饼图alpha设在0.8-1.0。颜色标度冲突如前所述fill美学被重复定义。必须使用fill2或其他方式分离两个图层的填充色标度。解决方案严格按照3.4节中的代码结构先画热力图并使用fill2美学再画饼图使用fill美学并分别为它们配置独立的scale_*函数。5.3 图形保存后分辨率低或元素模糊问题描述在RStudio的预览窗口里图形很清晰但用ggsave()保存为PNG或PDF后文字或图形边缘模糊。原因与解决DPI设置过低ggsave()默认DPI是300对于印刷或高清展示可能不够。可以尝试dpi 600。PDF保存的字体嵌入问题保存为PDF时默认的ggsave()可能不会将字体完全嵌入。使用device cairo_pdf可以更好地处理字体。图形尺寸过小如果设置的width和height太小再高的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()的计算和渲染会非常慢。优化策略热力图降低stat_density_2d()中的bins参数值或使用geom_bin2d()矩形分箱替代后者速度通常更快。气泡饼图这是性能瓶颈的主要来源。每个点都要绘制一个复杂的扇形多边形。抽样如果业务允许对数据进行随机抽样或系统抽样减少绘图点数。聚合如前所述将邻近的点进行空间聚类聚合。简化对于比例构成非常接近的点可以考虑用纯色气泡代替饼图用图例说明整体平均构成。关闭图形实时预览在脚本中将绘图和保存命令放在最后避免在交互环境中反复渲染大型图形。制作热力地图复合气泡饼图是一个对数据结构和ggplot2图层语法理解程度的综合考验。它没有一键生成的魔法函数但通过拆解为“热力背景”和“饼图标记”两个逻辑层并精心处理数据、美学映射和图例你就能创造出信息密度极高且视觉效果专业的分析图表。记住所有的参数调整尤其是颜色、大小和透明度都需要服务于清晰、准确地传达信息这一最终目的。多尝试多调整这张复合图将成为你数据可视化工具箱中一件得心应手的利器。
返回列表