Folium | Folium教程(DVwP笔记)前两天有个朋友拿着一个门店分布数据来找我说想做一个能发给老板看的页面老板要求能点击、能弹窗、能展示每个门店的业务信息。我第一反应就是他该用Folium。国内提到地图可视化很多人的第一选择是百度地图或高德地图的API但如果你做的是数据展示、数据分析报告、内部系统嵌入而没有一个独立的地图应用后台Folium几乎是最省事的方案。它是Python生态里基于Leaflet.js封装的地图可视化库几行代码就能把DataFrame里的数据映射成一张交互式地图输出成一个独立的HTML文件既能本地双击打开也能嵌入到任意网页里。这篇文章完全是DVwP笔记的整理版把我自己用Folium做过的几个实际项目踩过的坑、沉淀下来的正确姿势一步一步讲清楚。我写的所有笔记都是围绕一个目标让人看完能直接上手。不会扔一堆概念让你自己猜而是告诉你这里为什么这么做、那里为什么不能那么做。Folium这个东西入门门槛很低但真正把它用得顺手有几个关键点得搞明白比如坐标系统的选择、Popup传参的坑、图层叠加的顺序、以及保存成HTML后路径资源到底放到哪里。下面我按自己的整理顺序逐块展开。1. Folium是什么为什么做地图可视化我会先选它1.1 聊需求之前先看它解决了什么问题Folium解决的问题非常聚焦把带经纬度的数据快速变成可交互的地图页面。你要做的事就是把数据准备好然后调用它提供的方法。它不要求你懂JavaScript也不用你去注册地图平台的开发者账号、申请密钥这在国内环境下是很友好的。很多Python写数据分析的朋友都遇到过这样的尴尬pandas处理了一堆城市数据matplotlib画个散点图标个经纬度丑不说信息展示极其有限既不能放大缩小也不能点击查看详细信息。Folium的方案是生成一个HTML文件内部集成了Leaflet地图壳子底图默认走OpenStreetMap以下简称OSM的瓦片服务。地图渲染、缩放、拖拽这些都是Leaflet在浏览器端完成的Folium只负责把你的数据和配置翻译成浏览器能识别的JSON和JS代码。这个架构决定了它天生适合与Jupyter Notebook配合使用Notebook里直接显示交互地图需要分发的场景导出HTML即可。它和纯静态图表最大的区别在于交互层次。打个比方matplotlib画的散点图像是一张冲洗出来的照片信息是固定的Folium生成的地图则更像一张可以放大、拖动、点开看详情的外卖App地图信息可以根据需要一层一层展开。对于汇报、数据探查、项目展示这些场景这种交互能力带来的信息增量是巨大的。1.2 市面上这么多地图方案为什么单独把它拎出来做Python地图可视化市面常见的有多套方案我先做个横向对比方便你判断什么情况下选什么。方案交互能力上手难度是否需申请密钥典型应用场景matplotlib Basemap/Cartopy弱输出静态图中等否论文插图、画等值线、投影转换Plotly Mapbox较强支持动态交互中等需要Mapbox token需要图表联动、大量动态筛选Pyecharts 地图中等国内地图友好低否中国省级、市级分布专题图Kepler.gl强海量点数据流畅低否大规模轨迹、聚合分析Folium强灵活度高低否地理数据页面化、Web嵌入、快速分享矩阵看完Folium的定位很清楚如果你看重“页面交付”这个结果它是最顺手的。它有几点是我个人长期使用的直接原因。第一它完全基于经纬度和GeoJSON不受国内地图偏移坐标系限制。这里的重点是你得用WGS84标准坐标。国内很多数据源给的坐标是GCJ02火星坐标,直接画会偏几百米需要先把数据转成WGS84再喂进去。这个问题后面单独讲。第二它支持直接将结果保存为HTML分享不需要搭建在线服务。我做过一个实际案例把全市几十个网点的客流数据做成地图页面生成HTML之后直接放到公司内网静态服务器上同事点开浏览器就能看。没有后端渲染压力也没有API调用成本。第三它的底层基于Leaflet的可扩展能力你可以在Folium生成的页面基础上自由改动往里加自定义JavaScript或CSS。这意味着它不是一个封闭的黑盒而是一个带脚手架的半成品页面懂一点前端的人会非常喜欢它。2. 动手前必须搞懂的几个地图细节2.1 地图尺寸与经纬度第一行参数就藏着坑Folium创建一张地图很多人就直接folium.Map()一把梭然后发现地图缩成一团或者显示位置不对回头怀疑库有问题。其实不是你只需要掌握几个最关键的基础参数。import folium m folium.Map( location[39.909, 116.397], # 初始中心点 [纬度, 经度] zoom_start10, # 初始缩放级别 tilesOpenStreetMap, # 底图样式 width100%, # 画布宽度可以是整数像素或百分比 height600 # 画布高度 ) mlocation这个参数无数人第一次都用错。Folium里任何位置参数都是先纬度后经度也就是通常说的[lat, lon]不是[lon, lat]。这个顺序和GeoJSON里的[经度, 纬度]是反着来的。你如果用folium.Map([116.397, 39.909])地图会直接漂到一个奇怪的位置因为纬度116显然不在地球上任何合理范围。这个问题我后来在帮别人排查的时候至少遇到过三次每次都是同样的原因。zoom_start建议以5为基准值做微调。值的含义是Level of Detail数值越大地图放大倍数越高显示范围越小。做全国级别分布建议3到5做市级区域建议9到12。一个友好的做法是先跑一行代码看定位是否准确再调缩放级别不必纠结一次性到位。width和height推荐用百分比加整数混搭。因为Folium在Jupyter Notebook里默认宽度是“100%”在直接生成HTML的时候会保留这个比例但如果你需要嵌入到某个固定区域的网页中就可以显式写像素宽度。需要注意如果你设置了width百分比整个地图组件会自适应容器宽度而如果设成width900这个900是像素值在移动端的适配性会比较差。2.2 图层对象与add_child理解堆叠的原理Folium里有一个核心思想地图是一个容器你往里面添加的每一个元素都是一层。这些层包括Markers、Polygon、GeoJSON、TileLayer、ColorMap等全部通过add_to(m)方法挂到地图容器上。理解这个堆叠模型是Folium进阶的第一步。import folium m folium.Map(location[39.909, 116.397], zoom_start12) folium.Marker( location[39.909, 116.397], popupb站点/bbr这里是弹出内容, tooltip鼠标悬停提示 ).add_to(m) m.save(my_map.html)add_child的逻辑有点像一个箱子套箱子。m是最大的箱子Marker是直接放进去的物体而Marker的Popup又是Marker这个箱子里的一个小箱子。如果你想控制图层显隐顺序可以用folium.FeatureGroup把一类Marker统一放进去再整体加入地图。这么做的好处是后续可以通过LayerControl开关控制整个组的显示。这里有一个经常被忽略的性能问题当你有成千上万个Marker时直接逐个add_to(m)会导致画布上节点数量暴涨页面卡到怀疑人生。合理的做法是利用MarkerCluster做聚合它会把邻近点折叠成一个聚合簇缩放级别变化时自动拆分展示。这个功能对应folium.plugins.MarkerCluster用法后面实操部分会写完整。2.3 Marker、Popup与Tooltip信息展示主次分明地图上的信息展示有三个层级按鼠标操作深度排列默认看到的图标、悬停出现的tooltip、点击出现的popup。很多人不做区分一股脑全丢进popup里体验反而不好。我自己总结的做法是tooltip只放核心标识比如门店名称或编号popup放详细信息比如地址、负责人、联系方式、月度指标等。popup_content f div stylewidth:240px h4{store_name}/h4 pb地址/b{address}/p pb接待人数/b{visitors} 人/月/p pb负责人/b{manager}/p a href{detail_url} target_blank查看详情报告/a /div folium.Marker( location[lat, lon], popupfolium.Popup(popup_content, max_width300), tooltipstore_name ).add_to(m)这里有个细节folium.Popup的max_width要设置否则你可能发现弹窗挤在一团尤其当中文文本较长时浏览器默认的换行逻辑会非常难看。另一个细节是HTML代码建议写在统一模板里不要每一条数据都现拼字符串否则代码会极其冗长且难维护。正确做法是提前定义好渲染函数传入数据字典返回HTML字符串。3. 完整实操从一组门店数据到可分享的地图页面3.1 环境安装与准备在开始之前先确认你的Python环境。推荐直接用Anaconda或Miniconda自带的conda环境来管理尽量别往全局环境里乱装包。创建并激活一个新环境可以保持依赖隔离后面减少很多莫名其妙的包冲突问题。conda create -n mapvis python3.9 -y conda activate mapvis pip install folium pandas如果你处于网络受限的环境可以把pip源切换为国内镜像。另外Jupyter Notebook是体验Folium的最佳方式因为代码编写时能即时渲染可视化结果非常适合边写边调节。没有Notebook条件的用Python脚本直接生成HTML也可以往下每一步都不受影响。pandas是用来读取和清洗数据的。大多数项目里你的数据不会是一行一行写死的经纬度而是以CSV、Excel或数据库查询结果的形式存在。本篇案例就以CSV为准。3.2 数据准备与坐标系转换我设计了一个模拟数据集内容是某城市20家门店的分布信息包含门店名、地址、经度、纬度、类型、月客流、评分等字段。为了讲解通用性我先现场生成这批数据以方便演示完整的处理链条。import pandas as pd df pd.DataFrame({ store: [f门店{i:02d} for i in range(1, 21)], lat: [39.90 round(i * 0.01, 3) for i in range(20)], lon: [116.40 round(i * 0.011, 3) for i in range(20)], traffic: [800 i * 75 for i in range(20)], type: [旗舰店 if i % 5 0 else 社区店 for i in range(20)] }) df.head()这里有个逻辑我之前强调过真实业务里拿到的坐标很可能是火星坐标系GCJ02而不是WGS84。判断标准很简单——你在地图API拾取器里拿到的坐标通常是GCJ02你在GPS设备或Google Earth里拿到的通常是WGS84。如果两者混用点会被画偏几百米。针对这种情况建议使用第三方库完成转换。from coordinates_converter import wgs84_to_gcj02, gcj02_to_wgs84 # 如果确认数据是GCJ02坐标就统一转WGS84 df[lat_wgs84] df[lat].apply(lambda x: gcj02_to_wgs84(x, 0)[0]) df[lon_wgs84] df[lon].apply(lambda x: gcj02_to_wgs84(0, x)[1])提示我上面代码中的coordinates_converter是示意库名具体可以找专门处理坐标转换的库代替。无论采用哪种方式转换完成后务必抽查几个点把它映射到Folium的底图上去对比道路位置是否吻合。这一步用肉眼验证很快却能省去后续大堆麻烦。3.3 初始化地图和图层地图的中心点和缩放级别最好根据数据分布动态计算。你点的平均中心未必是最佳中心但至少是一个合理起点。我写了一个自动计算中心点的小函数好处是换数据集时不用手动改参数。center_lat df[lat_wgs84].mean() center_lon df[lon_wgs84].mean() m folium.Map(location[center_lat, center_lon], zoom_start11, tilesOpenStreetMap)选择OpenStreetMap底图的原因它是Folium默认的加载稳定无需注册就能直接用。注意OSM底图在国内访问速度相对较慢。如果你部署的页面面向公网用户考虑切换为tilesCartoDB positron它是浅色简洁风格加载速度我个人体感比OSM快。好奇的话可以试试tilesStamen Toner是那种黑白线稿风适合展示数据本身而非地形。3.4 添加Marker和聚类图层当门店数量少的时候直接每个门店一个Markers完全没问题数量到一两百个也能接受。一旦上千页面交互就会明显掉帧。我这份数据20个点先展示最基础的添加方式再引入聚类方案方便你迁移到大数据集场景。fg folium.FeatureGroup(name门店点位) for _, row in df.iterrows(): popup_html f div stylewidth:240px h4{row[store]}/h4 pb类型/b{row[type]}/p pb月客流/b{row[traffic]} 人/p /div folium.Marker( location[row[lat_wgs84], row[lon_wgs84]], popupfolium.Popup(popup_html, max_width300), tooltiprow[store] ).add_to(fg) fg.add_to(m) folium.LayerControl().add_to(m) m逐个添加Markers这种方式代码直白适合Demo和小数据量。考虑到实际项目扩量建议从一开始就把数据套上MarkerCluster聚类。from folium.plugins import MarkerCluster mc MarkerCluster(name门店聚合, controlFalse) for _, row in df.iterrows(): folium.Marker( location[row[lat_wgs84], row[lon_wgs84]], popupfolium.Popup(popup_html, max_width300), tooltiprow[store] ).add_to(mc) mc.add_to(m)MarkerCluster的好处是它自动根据当前缩放级别决定展示聚合簇还是单个Marker地图上有数字气泡提示该区域聚合了多少个点。放大之后气泡消失点被拆开。视觉体验好交互性能也省了一大截。设controlFalse表示不在图层控制列表里单独显示该项避免用户误关。3.5 数据分级与颜色映射单独的红点虽然清晰但如果能用颜色表达一个数值维度的差异比如用颜色深浅表示客流高低地图的信息密度会瞬间提升。Folium里这层逻辑是通过CircleMarker的color fillColor 分级函数实现的。min_traffic df[traffic].min() max_traffic df[traffic].max() def color_map(value): ratio (value - min_traffic) / (max_traffic - min_traffic) if ratio 0.33: return green elif ratio 0.66: return orange else: return red for _, row in df.iterrows(): folium.CircleMarker( location[row[lat_wgs84], row[lon_wgs84]], radius12, colorblack, weight1, fillTrue, fill_colorcolor_map(row[traffic]), fill_opacity0.7, popupfolium.Popup(..., max_width300), tooltiprow[store] ).add_to(m)这里用三分位数将客流映射为红、橙、绿三档你还可以换成连续渐变色谱。连续渐变可以用branca.colormap.LinearColormap定义然后手动给每个点取色。标记视觉观感上CircleMarker比普通Marker更像“数据点”因为它没有图标形状只有圆形适合表达连续数值。要说清楚的是CircleMarker属于矢量渲染跟图标型Marker相比并没有明显的加载性能差异大量点场景中两者基本一样。3.6 保存与分享地图在Notebook里显示正常接下来要交付。Folium的关键优势在此体现页面已经自我完整直接保存即可。m.save(output/门店分布图.html)生成的HTML是内联样式和脚本的不需要外部图片、字体或JS依赖。你可以直接把它发给别人让对方用浏览器打开。你也可以用Python的HTTP服务临时给同事共享cd output python -m http.server 8080如果有线上WEB服务把它放进静态目录即可。如果是内网环境就传到内网的静态文件服务器。不需要数据库不需要后端只要浏览器能访问到这个HTML地图就能正常加载。这里有一个我必须重点提醒的细节生成HTML之后地图瓦片仍然来自外部URL。也就是说别人打开这个页面需要能访问到OpenStreetMap或你所指定的瓦片服务器域名。如果对方处于完全离线或内网隔离的环境中地图底图会显示为灰色网格。解决办法之一在生成HTML以后换一个内网可访问的瓦片服务地址或者干脆部署一个瓦片服务。这个场景比较复杂但如果你的企业有内网地图服务可以把URL替换到对应的瓦片地址模板上去。4. 我踩过的坑和排查思路4.1 常见问题速查表以下是我在实际使用中反复遇到、且社区里高频出现的问题直接整理成表。现象根本原因解决方向地图显示空白瓦片URL无法访问或服务器未加载更换tiles参数或检查访问网络图标位置整体偏移几百米坐标用了GCJ02或GPS坐标未转换转换为WGS84坐标系点很多页面卡顿直接逐个添加Marker改用MarkerCluster聚合Popup中文显示乱码编码问题或HTML未声明字符集保存时指定utf-8编码页面head加meta charset颜色设置无效把fillColor与color搞混color控制边框fillColor控制填充生成HTML后双击没反应浏览器安全策略限制本地JS用本地HTTP服务或嵌入Web项目地图范围不对zoom_start太大或太小根据覆盖级别动态计算缩放值无法显示GeoJSON属性字段名与代码不匹配打印GeoJSON结构确认geometry类型4.2 典型问题排查实录举两个我实际遇到并花费不少时间才定位的问题。第一个是关于地图显示一片灰底。某次我生成了一版地图在本地浏览器预览正常部署到客户服务器以后打开只有灰色背景和几个点。排查了很久最后定位到是客户服务器环境不能访问OSS瓦片服务。找到了根因以后我把地图的tiles参数从默认的OpenStreetMap换成了企业内部瓦片地址问题随即消失。这个案例提醒我凡是要交付出去的地图页面建一个瓦片可达性检查的动作。第二个问题是关于GeoJSON数据多条线渲染不出来的情况。排查时我发现数据有的Geometry是Polygon有的是MultiPolygon我在代码中统一按Polygon处理导致后者丢失。GeoJSON这种结构多样性的问题最容易从边角数据上翻车。后来我在加载数据前会加一步结构检查统计geometry类型分布再做类型防御。4.3 几个让效果提升一个档次的细节建议下面这些都是长期实践中积累出来的细节写在这作为硬经验分享。第一善用branca创建自定义颜色映射。Folium生态里很多高级容器都来自branca它支持ColorMap、Element、MacroElement等底层结构。你如果想让图例和颜色映射自动适配可以直接用branca生成一个colormap并add_to地图省去手工拼接图例HTML。第二尽量把Tooltip和Popup写成模板函数。不要每行数据都粘贴复制HTML字符串定义一个渲染函数接收一组字段返回格式化HTML这样字段一旦变多也不会撑爆代码。第三把生成的HTML做一次体积优化。虽然Folium生成的HTML已经可以正常使用但实际内部会有大量样式与脚本字符串体积可能不小。利用HTML压缩工具或者移除不必要的插件代码传输和加载速度会得到改善。4.4 关于Marker图标自定义如果你觉得默认的蓝色水滴图标太单调想换成自定义图标可以使用folium.Marker配合Icon类实现。icon_url https://example.com/pin_icon.png folium.Marker( location[row[lat_wgs84], row[lon_wgs84]], popupfolium.Popup(popup_html, max_width300), tooltiprow[store], iconfolium.CustomIcon(icon_imageicon_url, icon_size(30, 40)) ).add_to(m)自定义图标需要注意icon_size必须设置否则外部图片会以原始像素尺寸显示有时候会大到离谱。如果希望用Font Awesome之类的字体图标可以直接用folium.Icon(iconcloud, colorgreen)。Font Awesome图标在不同的底图风格下呈现效果有差异建议实测几个页面再统一。5. 把上面的东西放在一起玩一个小综合Demo为了照顾习惯“直接跑通再研究细节”的朋友我把上面的核心要素合并成一个完整的Demo脚本。你拿到后直接复制到本地改一改数据字段就能用。import folium import pandas as pd from folium.plugins import MarkerCluster # 1. 读取数据 df pd.read_csv(store_data.csv) # 2. 如果数据坐标不是WGS84这里统一转换 if not df[[lat, lon]].empty: df[lat_wgs84] df[lat] df[lon_wgs84] df[lon] # 3. 计算中心点 center_lat df[lat_wgs84].mean() center_lon df[lon_wgs84].mean() # 4. 创建地图 m folium.Map(location[center_lat, center_lon], zoom_start10) # 5. 定义信息弹窗模板 def build_popup(row): return f div stylewidth:240px h4{row[store]}/h4 pb类型/b{row[type]}/p pb月客流/b{row[traffic]}/p pb地址/b{row[address]}/p /div # 6. 添加聚合点图层 mc MarkerCluster() for _, row in df.iterrows(): folium.Marker( location[row[lat_wgs84], row[lon_wgs84]], popupfolium.Popup(build_popup(row), max_width300), tooltiprow[store] ).add_to(mc) mc.add_to(m) # 7. 保存 m.save(门店分布图.html) print(生成完成门店分布图.html)跑通这个Demo后你可以根据自己的需求去调整底图样式、信息字段、聚合方式。如果展示内容不包含地址类文本信息可以把Popup去掉只用Tooltip海报感更强加载也更快。我自己在实际使用中还有一个习惯每次交付前都会在浏览器里开隐身模式做一次实测点几个点位、放大缩小几次确保弹窗和聚合都正常。因为在Notebook里显示正常不代表在静态页面里完全没问题。另外如果页面未来要嵌入Web项目建议再加一行CSS重置例如设置地图容器的高度和宽度避免Folium默认样式跟原有站点冲突。踩过几次坑之后我现在已经养成了“先小规模验证一批点位再全量生成”的习惯。这个习惯也建议你保留它能让你在处理成千上万个点位的时候心里更有底。