ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

R语言ggmap实战:从经纬度数据到街道底图的空间可视化指南

R语言ggmap实战:从经纬度数据到街道底图的空间可视化指南 简介ggmap是R语言中基于ggplot2框架的地图可视化扩展包适用于需要通过地图展示空间数据的数据分析师、科研人员及R语言学习者。它可调用谷歌地图、OpenStreetMap等在线地图服务解决地图瓦片检索、坐标映射与统计图层叠加等问题。压缩包内共含120个文件大小约4.59MB主体为49个R源码和39个rd帮助文档另有rda示例数据、md说明文档、png示意图以及描述文件等目录结构贴近R包标准布局便于按模块研读。目前已有1758人学习下载。借助完整的源码与帮助文档读者可免去在线安装和依赖配置的繁琐直接查看地图获取、坐标系转换及图层绘制等核心函数实现配合实例数据与示意图能系统掌握在ggplot2中集成ggmap绘制点分布图、区域热力图等常见操作并了解如何调整地图样式与处理常见报错适合希望快速上手空间数据可视化的R用户。 做数据分析的人早晚会遇到一个需求手里有一批带经纬度的数据想画一张能看清街道背景的分布图。R里base包自带的maps库画出来像上世纪的古董ggplot2虽然强大却不直接支持背景底图这个时候ggmap就是最顺手的解决方案。ggmap是Hadley Wickham团队开发的扩展包专门负责把在线地图拉下来变成ggplot2可以直接使用的背景图层。这篇文章我从安装配置、底图获取、坐标对齐讲到进阶画法把几年里用ggmap踩过的坑一并整理出来适合刚接触空间可视化、或者已经在用ggplot2但不知道怎么加地图底图的读者。1. 为什么R语言画地图总绕不开ggmap1.1 从ggplot2到ggmap一张底图的诞生ggmap本质上做的事情并不复杂它把在线地图服务商提供的栅格瓦片图片下载到本地然后封装成一个带经纬度坐标范围信息的对象。这个对象可以直接作为ggplot2的绘图背景也就是说你只需要用一行代码召唤地图再叠加各种几何对象点、线、多边形、密度图就能完成空间可视化。library(ggmap) library(ggplot2) # 拉取某区域的底图 map - get_map(location 杭州, zoom 12) # 画底图 ggmap(map)上面这段代码就是ggmap的最小工作流。底图拿到手之后它就不再是“在线服务”了而是一张嵌着坐标系的普通图片后面所有ggplot2的语法 geom_point()、 geom_density2d()都直接往上堆不需要任何额外适配。这正是ggmap的核心价值帮你把“地图服务”和“绘图语法”之间那条裂缝填平。1.2 适合用ggmap的场景和不该用它的情况ggmap适合什么场景我总结下来有三个非常典型的用法经纬度点分布展示比如门店选址分析、采样点分布、用户位置可视化。路径轨迹绘制比如跑步记录、车辆轨迹、物流路线回放。空间密度热力图比如城市事件热点分析、事故频发区域识别。但ggmap并不适合精细的行政区划分析。如果你要画的是省级、市级边界的精确区域图、想做区域间数据对比更应该用sf包配合地图数据比如中国行政区划SHAPE文件那种叫“矢量地图”可以缩放不失真也能做空间计算。ggmap用的是栅格底图放大了会糊也不能做空间拓扑运算。两者各管一摊别混着用。2. 安装与配置密钥和依赖的几个暗坑2.1 安装与依赖ggmap的安装本身不难但如果你直接用install.packages(ggmap)然后马上跑get_map大概率会遇到一堆问题。首先是依赖包ggmap依赖的包比较多包括ggplot2、dplyr、magrittr、tibble、glue、httr等。如果是在全新环境里安装建议先一次性装好install.packages(c(ggplot2, dplyr, ggmap))安装完成后加载ggmap时如果提示缺某个包说明R版本偏旧或者安装源不干净建议先update.packages()再装一次。另一个常见现象是加载ggmap后出现mask警告这正常不影响使用。2.2 API密钥问题老教程最大的坑传统ggmap教程一上来就用get_map()拉Google地图这套流程在2018年前确实没问题但后来Google要求所有地图请求都必须绑定API密钥和结算账号直接用默认接口会返回Error: geocode failed with status OVER_QUERY_LIMIT或者干脆报403 Forbidden。新手照着旧教程跑不通十有八九卡在这里。我自己目前常用的方案是Stadia Maps前身是Stamen Design的地图服务。Stadia提供了免费的开发额度注册之后拿到一个API key配合ggmap使用非常简单register_stadiamaps(你的key, write FALSE)这行代码会把key写入当前R会话的环境变量之后调用get_map时指定maptype stamen_toner或者stamen_terrain即可。这里提醒一句register_stadiamaps()函数是ggmap 3.0之后才加入的如果你安装的ggmap还是老版本请先升级到3.0以上。注意不要把API key硬编码写在公开的R脚本里尤其不要上传到代码托管平台。key是可以被拿去刷流量的轻则限额被耗尽重则产生费用。建议用环境变量保存或者写完脚本后手动清理。3. 抓取底图get_map/get_stadiamaps的完整参数拆解3.1 bbox边界框的定义方式ggmap拉底图的核心函数是get_map()和get_stadiamaps()。两个函数参数高度相似核心是location和bbox。如果你只传一个地名比如“杭州”ggmap会先调用地理编码接口把地名转成经纬度边界框再拉对应区域的图。但地名转码依赖在线服务容易出偏差所以我更推荐直接给bboxbounding box边界框。bbox可以让底图精确覆盖你要的区域。这里有几种常见的定义方式直接看例子# 方式1数字向量格式是 c(left, bottom, right, top) bbox - c(120.1, 30.2, 120.3, 30.4) # 方式2在get_map里直接用location传bbox向量 map - get_map(location bbox, maptype terrain) # 方式3用经纬度点生成bbox适合包围某几个坐标点 library(dplyr) points - data.frame(lon c(120.15, 120.25, 120.2), lat c(30.25, 30.28, 30.35)) bbox - c(left min(points$lon) - 0.01, bottom min(points$lat) - 0.01, right max(points$lon) 0.01, top max(points$lat) 0.01) map - get_map(location bbox)bbox四个数字的顺序一定不能错是“左下右上”的逆时针顺序写成左上右下就全乱了。另外bbox宽度和高度比例尽量和输出图片的长宽比一致不然底图会被拉伸变形。实操中我给一张16:9的图配bbox时会把横向范围调得比纵向大一些出图比例才协调。3.2 zoom缩放级别zoom参数控制地图的缩放级别范围一般是3到20。zoom越小覆盖范围越大细节越少zoom越大街道细节越清楚但覆盖范围变小。这里有个新手容易犯的错认为zoom传得越大底图越清晰。实际上ggmap返回的底图始终是一张固定像素的栅格图zoom只是改变这个像素范围内显示什么内容。zoom15时截图范围小但街道字看得清zoom12时范围大但街道标签可能挤成一团。比较适合城市级展示的zoom在12到14之间小区级要15以上。选择zoom还有一个实用技巧在get_map()之前先用ggmap::calc_zoom(lon_range, lat_range)计算一个合适的zoom值。它根据bbox生成一个推荐缩放级别能避免反复试错。不过它只是参考值实际按需求微调即可。3.3 maptype选型哪张底图适合什么样的数据ggmap支持多种maptype不同风格对数据呈现的影响差异很大。我用过的几种主流底图做个对比maptype风格适合场景注意点stamen_toner黑白高对比点分布、热力图背景不抢数据最通用stamen_terrain彩色地形海拔、自然地理数据城区细节弱stamen_watercolor水彩风格演示、展示型图表信息承载弱osmOpenStreetMap标准交通、路网颜色杂易抢焦点google-maps谷歌标准街道路网展示需要Google密钥数据可视化里最核心的原则是“数据要能从背景中跳出来”。黑白toner底图在我个人经验里是最高频的选择它把街道、水体、绿地都压缩成灰度层级前景点了什么颜色都够醒目。做PPT展示时我偶尔会用watercolor但数据叠加上去容易眼花除非你追求特别强的视觉冲击。4. 把数据画上地图坐标对齐与图层叠加4.1 坐标偏移问题国内数据最大的坑把数据点画上底图看起来是ggmap(map) geom_point(data, aes(lon, lat))一行代码的事但真正操作起来最常翻车的就是坐标偏移。这个坑我在国内项目里踩得最惨明明经纬度是对的点却整体偏到了河对岸或者街道隔壁。根源在于坐标系不统一。国际通用的GPS坐标是WGS-84坐标系而国内大多数地图API高德、百度、腾讯返回的经纬度是加密后的GCJ-02坐标俗称火星坐标或者BD-09坐标百度坐标两者之间差了大约几十米到几百米的偏移。ggmap的底图默认使用WGS-84坐标系所以如果你的数据来源是高德API直接叠加就会整体偏移。怎么判断自己的数据是不是偏移了很简单把点画到地图上看是否落在道路中央而非路侧建筑上。如果偏差在1公里以上基本就是坐标没转换。解决办法是先把GCJ-02转WGS-84网上有很多开源转换函数可以用两行代码的事转换后再画就对齐了。4.2 经度纬度顺序一个被忽略的致命细节这个坑比坐标偏移更隐蔽。ggmap里所有地图函数都是“经度在前纬度在后”lon, lat但很多国内数据集习惯写成“纬度经度”甚至某数据导出时是“lat, lon”的列顺序如果你没注意坐标点会直接飞到另一个大洲。我之前接手过一份数据原文件中列名是“X”“Y”乍一看以为是经度纬度画出来后所有点跑到非洲西海岸去了。后来一查是某采集软件把X定义成了纬度Y定义成了经度。所以画图之前务必先看一眼前几行确认列名和值域经度范围在73到135之间中国区域纬度范围在18到54之间。超出这个范围就该警觉了。4.3 图层叠加顺序与透明度调节坐标对齐后图层的叠加就靠ggplot2的图层机制了。底图是第一个图层后续想叠加什么就加什么ggmap(map) geom_point(data shop_data, aes(lon, lat, color sales), size 3, alpha 0.8) scale_color_gradient(low blue, high red) theme_minimal()有几个经验值得记住叠加几何对象前如果数据点很多比如上万次GPS记录建议先设置alpha透明度防止点重叠成一片黑疙瘩。想突出某个区域可以在底图之上先用geom_rect()或者geom_polygon()画半透明覆盖层再叠加数据。坐标轴标签默认会显示经纬度如果做展示图建议theme_void()去掉坐标轴让视觉聚焦在地图和数据本身。5. 进阶玩法热力图、路线轨迹与分面地图5.1 用stat_density2d做空间热点密度图散点图展示的是“每一个点在哪”但如果点是几千上万个只看散点图很难看出分布规律。这时候可以换一种思路用二维核密度估计绘制热力图ggmap(map) stat_density2d(data gps_data, aes(lon, lat, fill ..level..), geom polygon, alpha 0.3) scale_fill_gradient(low yellow, high red)这样做出来的图能很直观地看到哪些区域是热点聚集区比如城市里的高流量地区、事件多发区域。实际项目里我会先画热力图再加一层半透明散点既能看到密度也能看到原始数据的分布广度。注意..level..是stat_density2d内部计算的密度层级字段老版本ggplot2里这个写法有效如果遇到新版ggplot23.4以上语法变化请先查doc确认字段名。5.2 路线轨迹绘制按时间顺序连接经纬度点绘制GPS轨迹时最容易出的问题是点的连接顺序错乱。geom_path()会按照数据框中行的顺序连接相邻点所以在画轨迹之前必须按时间排序library(dplyr) track_sorted - track_raw %% arrange(timestamp) ggmap(map) geom_path(data track_sorted, aes(lon, lat), color darkorange, size 0.8, alpha 0.7) geom_point(data track_sorted, aes(lon, lat), color red, size 1)排序这个步骤很多新手会忽略直接拿原始数据画结果轨迹线乱成一团蜘蛛网。尤其GPS设备因为信号问题经常出现时间戳乱序画之前一定要确认排序逻辑。另外起点和终点可以单独用两个大点标出来加上颜色区分这样看图的人一眼就能判断运动方向。5.3 分面地图用facet_wrap制作多时相对比图ggmap对象同样支持ggplot2的分面facet功能这是一般在线地图工具做不到的。比如你有某市一周内每天的车辆轨迹数据可以按天切分用facet_wrap出图ggmap(map) geom_path(data track_week, aes(lon, lat, group trip_id, color hour), alpha 0.5) facet_wrap(~ day, ncol 3)这样每一天的轨迹会单独画一张小图方便做逐日对比。这个功能在分析出行规律、运营调度复盘时非常实用。我在地铁客流分析项目中就用facet按小时切分24张图一眼就能看出早晚高峰的空间扩散路径。6. 性能排查与输出的几个实战心得6.1 底图缓存与请求限流用ggmap拉底图是个网络IO操作频繁请求会拖慢脚本也容易触发地图服务的限流。ggmap自身有内存缓存的机制同一个会话里重复请求同一bboxzoommaptype时不会重复向服务器发请求。但如果你退出R会话再跑一次脚本就又要重新拉一次。所以我都会在代码开头加一层自己的文件缓存逻辑save_map - function(map, path) { saveRDS(map, path) } load_map - function(path) { readRDS(path) }第一次运行时拉图并保存之后直接从本地载入既快又不浪费配额。地图底图更新频率很低除非是要体现城市道路近期变化这一招能把脚本从几十秒压缩到几秒。6.2 输出图片的清晰度与尺寸控制如果直接ggsave(map.png, p, width 10, height 8)默认dpi只有300而底图本身是固定像素的栅格图放大尺寸会变糊。建议在保存时多试几个dpi和size的组合找到底图自己原始分辨率的平衡点。我一般这样处理ggsave(output.png, p, width 8, height 6, dpi 300)如果输出需求是大尺寸打印建议在拉底图时就把bbox范围设小一点并调高zoom值保证底图在一个较小范围内有相对更多的像素这样放大后细节还在。6.3 一个兜底的输出方案ggimage如果你做了复杂的ggplot2图表但ggmap底图质量满足不了需求还有一个偏门思路先把地图服务截图下来再用ggimage包把图表合成上去。这个方案能把你喜欢的任何在线地图嵌入ggplot2但操作比较绕推荐只在特殊场景下用。正常需求ggmap自带的那些能直接拉到矢量风格底图的途径已经够用了。7. 画完图之后的几个数据细节说完函数和参数最后聊聊出图前需要检查的数据细节这些东西最容易在报告汇报前翻车。第一个是缺失值。经纬度数据里有NA是常事尤其GPS采集的数据丢星、断电都会产生缺失。ggplot2绘图时NA观测会自动被丢弃但因为丢弃是静默的你根本不知道数据少了多少。建议画图前先sum(is.na(data$lon) | is.na(data$lat))心里有个数如果缺失比例超过5%要么补值要么在汇报里明确写出来。第二个是异常值过滤。经纬度偶尔会蹦出明显不合常理的坐标比如0,0比如经度跑到200这是设备故障导致。画图前用范围过滤把这些脏点清掉不然底图上会出现一个漂到海外的孤点非常影响专业感。第三个是投影变形问题。ggmap底图用的是墨卡托投影纬度越高地面距离被拉伸得越明显。如果你画的是全国级别的大尺度数据直接叠加经纬度做距离判断可能会失真但对于城市级、区县级的地图展示这种形变肉眼几乎无感不用太纠结。我个人经验是ggmap适合90%的城市级空间展示场景但别拿它做精确空间统计。真正做距离计算、面积计算、缓冲区分析时还是要回到sf加矢量数据的正道上来。就拿底图当背景用ggplot2做表达这个组合在大多数场景下已经能打出非常漂亮的效果了。最后分享一个小技巧如果你觉得自己不擅长挑maptype可以同时出三张不同底图的草图——toner、terrain、watercolor把它们并排对比一下。很多时候选哪张底图取决于你的数据主色和你想要传达的情绪而不是哪张更好看。数据是红色系的点用toner数据是绿色系的生态数据用terrain数据是给非专业观众做宣传展示watercolor的效果往往出人意料。本文还有配套的精品资源点击获取
返回列表