☰
Folium交互地图可视化入门教程:门店数据轻松制作可点击弹窗地图
2026/10/17 0:44:05 网站建设 项目流程

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_start=10, # 初始缩放级别 tiles='OpenStreetMap', # 底图样式 width='100%', # 画布宽度,可以是整数像素或百分比 height=600 # 画布高度 ) m

location这个参数,无数人第一次都用错。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百分比,整个地图组件会自适应容器宽度,而如果设成width=900,这个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_start=12) folium.Marker( location=[39.909, 116.397], popup="<b>站点</b><br>这里是弹出内容", 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 style="width:240px"> <h4>{store_name}</h4> <p><b>地址:</b>{address}</p> <p><b>接待人数:</b>{visitors} 人/月</p> <p><b>负责人:</b>{manager}</p> <a href="{detail_url}" target="_blank">查看详情报告</a> </div> """ folium.Marker( location=[lat, lon], popup=folium.Popup(popup_content, max_width=300), tooltip=store_name ).add_to(m)

这里有个细节:folium.Popup的max_width要设置,否则你可能发现弹窗挤在一团,尤其当中文文本较长时,浏览器默认的换行逻辑会非常难看。另一个细节是HTML代码建议写在统一模板里,不要每一条数据都现拼字符串,否则代码会极其冗长且难维护。正确做法是提前定义好渲染函数,传入数据字典,返回HTML字符串。

3. 完整实操:从一组门店数据到可分享的地图页面

3.1 环境安装与准备

在开始之前,先确认你的Python环境。推荐直接用Anaconda或Miniconda自带的conda环境来管理,尽量别往全局环境里乱装包。创建并激活一个新环境可以保持依赖隔离,后面减少很多莫名其妙的包冲突问题。

conda create -n mapvis python=3.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_start=11, tiles='OpenStreetMap')

选择OpenStreetMap底图的原因:它是Folium默认的,加载稳定,无需注册就能直接用。注意OSM底图在国内访问速度相对较慢。如果你部署的页面面向公网用户,考虑切换为tiles='CartoDB positron',它是浅色简洁风格,加载速度我个人体感比OSM快。好奇的话,可以试试tiles='Stamen Toner',是那种黑白线稿风,适合展示数据本身而非地形。

3.4 添加Marker和聚类图层

当门店数量少的时候,直接每个门店一个Markers完全没问题,数量到一两百个也能接受。一旦上千,页面交互就会明显掉帧。我这份数据20个点,先展示最基础的添加方式,再引入聚类方案,方便你迁移到大数据集场景。

fg = folium.FeatureGroup(name="门店点位") for _, row in df.iterrows(): popup_html = f""" <div style="width:240px"> <h4>{row['store']}</h4> <p><b>类型:</b>{row['type']}</p> <p><b>月客流:</b>{row['traffic']} 人</p> </div> """ folium.Marker( location=[row["lat_wgs84"], row["lon_wgs84"]], popup=folium.Popup(popup_html, max_width=300), tooltip=row["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="门店聚合", control=False) for _, row in df.iterrows(): folium.Marker( location=[row["lat_wgs84"], row["lon_wgs84"]], popup=folium.Popup(popup_html, max_width=300), tooltip=row["store"] ).add_to(mc) mc.add_to(m)

MarkerCluster的好处是它自动根据当前缩放级别决定展示聚合簇还是单个Marker,地图上有数字气泡提示该区域聚合了多少个点。放大之后气泡消失,点被拆开。视觉体验好,交互性能也省了一大截。设control=False表示不在图层控制列表里单独显示该项,避免用户误关。

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"]], radius=12, color="black", weight=1, fill=True, fill_color=color_map(row["traffic"]), fill_opacity=0.7, popup=folium.Popup(..., max_width=300), tooltip=row["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"]], popup=folium.Popup(popup_html, max_width=300), tooltip=row["store"], icon=folium.CustomIcon(icon_image=icon_url, icon_size=(30, 40)) ).add_to(m)

自定义图标需要注意,icon_size必须设置,否则外部图片会以原始像素尺寸显示,有时候会大到离谱。如果希望用Font Awesome之类的字体图标,可以直接用folium.Icon(icon="cloud", color="green")。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_start=10) # 5. 定义信息弹窗模板 def build_popup(row): return f""" <div style="width:240px"> <h4>{row['store']}</h4> <p><b>类型:</b>{row['type']}</p> <p><b>月客流:</b>{row['traffic']}</p> <p><b>地址:</b>{row['address']}</p> </div> """ # 6. 添加聚合点图层 mc = MarkerCluster() for _, row in df.iterrows(): folium.Marker( location=[row["lat_wgs84"], row["lon_wgs84"]], popup=folium.Popup(build_popup(row), max_width=300), tooltip=row["store"] ).add_to(mc) mc.add_to(m) # 7. 保存 m.save("门店分布图.html") print("生成完成:门店分布图.html")

跑通这个Demo后,你可以根据自己的需求去调整底图样式、信息字段、聚合方式。如果展示内容不包含地址类文本信息,可以把Popup去掉,只用Tooltip,海报感更强,加载也更快。

我自己在实际使用中还有一个习惯:每次交付前都会在浏览器里开隐身模式做一次实测,点几个点位、放大缩小几次,确保弹窗和聚合都正常。因为在Notebook里显示正常不代表在静态页面里完全没问题。另外,如果页面未来要嵌入Web项目,建议再加一行CSS重置,例如设置地图容器的高度和宽度,避免Folium默认样式跟原有站点冲突。踩过几次坑之后,我现在已经养成了“先小规模验证一批点位,再全量生成”的习惯。这个习惯也建议你保留,它能让你在处理成千上万个点位的时候心里更有底。

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

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

立即咨询