ARTICLE DETAIL

资讯详情

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

Python绘制彩色美国地图:数据可视化与Choropleth地图实战

Python绘制彩色美国地图:数据可视化与Choropleth地图实战 1. 项目拆解这类带颜色标注的美国地图到底在标注什么我最早接到类似需求时对方就发来一张截图说“我要在线绘制带颜色标注的美国地图”下面配了个红红绿绿的例子。截图里其实就是一个典型的 Choropleth Map中文叫“分级统计图”或“等值区域图”。翻译成大白话就是把美国各州填上不同深浅的颜色每个颜色代表某个数值的大小一眼就能看出哪个州高、哪个州低。这类图的价值在于把一张冷冰冰的数据表变成空间直觉。比如你有美国 50 个州的人口、失业率、销售额、房价中位数、投票率、疫情数据、门店数量做成一张 Excel 表别人看完只觉得是数字但当你把数字映射到地图上东南部一片深红、西部一片浅黄决策者立刻能看出区域差异后面“为什么这个区域这么高”“接下来要不要去那边铺渠道”这些问题就自然被激发出来了。做这个项目的关键从来不是“画地图”本身而是先想清楚三件事你要标注的地理粒度是什么。是美国 50 个州、某个州的县还是国会选区粒度不同边界数据的体量和获取难度完全不同。50 个州的 data series 很小县级的则要几 MB 甚至几十 MB。你手上的业务数据用什么字段和地理边界对上。通常靠州名、州缩写或 FIPS 编码美国人口普查局给每个州分配的两位数字编码关联。这一步没对上颜色就全错位比不画还糟。最终给谁看、在哪看。自己分析看可以生成静态 PNG嵌入网页给客户点选要做成交互图打印成海报又得考虑分辨率和投影。我这边最终选定的一条主力技术路线是Python Cartopy Matplotlib必要的时候上Folium 或 Plotly。为什么不是 Tableau、Power BI 或在线工具不是它们不行而是代码方案可定制程度最高你可以改投影、改标注、改颜色映射规则、输出任意尺寸高清图还能把整个流程嵌进定时报表里。后面我会把每一步详细展开包括我踩过的坐标系、阿拉斯加缩放、颜色误导这些坑。2. 准备工作边界数据和业务数据的正确姿势2.1 州界边界数据从哪拿画美国地图必须先有“边界”也就是每个州的轮廓坐标串。最权威、最省事的来源是US Census Bureau 的 TIGER/Line 数据在 Census.gov 搜索“TIGER/Line State”可以下载到美国各州边界 shapefile。拿到手是压缩包解压后至少有.shp、.shx、.dbf、.prj四个文件缺一不可读取时需要放在同一目录。也有人用 GeoJSON 格式数据量差不多但处理起来更直接。Github 上有经典仓库比如PublicaMundi/MappingAPI或者johan/world.geo.json都收录了美国州界。我个人的建议是如果你追求权威性、愿意花五分钟下载用 Census TIGER。如果你只是快速试验直接用 GeoJSON省去坐标系转换。2.2 自己拼一份业务数据假设我们要做一个最简单的 demo给每个州一个“满意度指数”数值 60 到 90 之间然后画到地图上。数据表里至少要有两列一列是地理标识一列是数值。地理标识我强烈建议用FIPS 编码比如加州是06纽约是36德州是48。为什么不用州名因为名称存在“New York 州”和“New York 市”这种混淆而且不同来源的数据有时叫NY、有时叫New York、有时叫NY State清洗起来很烦。FIPS 编码是两位数字唯一且稳定。示例数据可以长这样import pandas as pd # 仅展示前 5 行示意实际请包含 50 个州 DC data pd.DataFrame({ fips: [01, 02, 04, 05, 06], score: [72.5, 65.1, 80.3, 77.8, 88.2] })注意 FIPS 必须保留成字符串不要用整数因为01会被读成1关联时就对不上了。这是新手最容易踩的坑。2.3 关联数据结构左右连接边界数据里面每个州也是一个多边形对象通常自带 FIPS 属性。拿到后把业务数据 merge 到 GeoDataFrame 上import geopandas as gpd states gpd.read_file(tl_2023_us_state.shp) states states.merge(data, left_onSTATEFP, right_onfips, howleft)Merge 完成后务必检查一遍哪些州没关联上missing states[states[score].isna()] print(missing[NAME].tolist())如果出现缺失最常见的原因是 FIPS 编码长度不一致或者业务数据里写了DC这种缩写而不是 FIPS。不要觉得这是小问题——我见过真有人整张图画出去了后来才发现波多黎各、关岛等海外领地也在边界文件里它们的 score 是空的图上一片灰色。2.4 坐标系先别慌但要懂基本逻辑边界数据的坐标最初是经纬度也就是通常说的 WGS84 坐标系。经纬度是一个球面坐标而我们要在平面上画图必然涉及投影。投影会影响美国地图长什么样有的投影会放大阿拉斯加有的投影让美国看起来像歪着的。现在很多人直接用 Geopandas 默认的经纬度画图Matplotlib 也能画出来但美国北部州会显得很大观感不太对。更专业的做法是用Albers 等面积圆锥投影这恰好也是美国人口普查局绘制美国地图时的经典投影。后面我会单独讲投影的处理第 4 章会给出完整代码。准备工作阶段你只需要记住不要直接拿经纬度画最终成品图至少要知道你面对的是经纬度数据并且有办法做投影转换。3. 核心实现用 Python 画第一张彩色美国地图3.1 最小可用脚本Geopandas Matplotlib如果你急用一分钟就能出一张能看的图。先把依赖装好pip install geopandas matplotlib pandas然后运行import geopandas as gpd import matplotlib.pyplot as plt # 读取边界 path tl_2023_us_state.shp gdf gpd.read_file(path) # 造一个演示字段这里用 ALAND陆地面积当演示真实场景换成你的业务字段 gdf[land_area_plot] gdf[ALAND] / 1e10 # 转成万平方公里量级 fig, ax plt.subplots(figsize(12, 8)) gdf.plot( columnland_area_plot, cmapYlGnBu, legendTrue, axax, edgecolorwhite, linewidth0.5 ) ax.set_axis_off() plt.show()这张图能跑通你就已经完成了一个 Choropleth Map 的三分之二。剩下的是把它从“能看”变成“好看且可信”。3.2 颜色映射逻辑数据到颜色的对应关系column参数指定哪一列参与上色cmap指定调色板。默认情况下Matplotlib 会把数据最小值映射到调色板一端最大值映射到另一端中间按线性切分。这里藏着几个问题如果你的数据里有极端离群值比如绝大多数州在 60 分某一个州是 100 分线性映射会让其他州颜色几乎一样看不出差异。如果你的数据有明显领域含义分数从 0 到 100你可能希望颜色范围固定而不是每次都按数据最大最小值动态拉伸。解决办法是传入vmin和vmax强制颜色映射到固定区间。比如我们想让 60 对应最浅色、90 对应最深色gdf.plot( columnscore, cmapRdYlGn, vmin60, vmax90, legendTrue, axax, edgecolorwhite, linewidth0.5 )这段代码背后就是 Matplotlib 的Normalize 机制先用 vmin/vmax 把数据线性变换到 [0,1]再通过 cmap 查表得到最终颜色。理解这个机制后面做分段色阶、数据分箱时就能得心应手。3.3 图例怎么读上面的代码加legendTrue就能出现颜色条但颜色条默认只显示数值没有标题。进阶一点可以把图例标题加上from matplotlib.lines import Line2D fig, ax plt.subplots(figsize(12, 8)) gdf.plot(columnscore, cmapRdYlGn, vmin60, vmax90, legendTrue, axax, edgecolorwhite, linewidth0.5) ax.set_title(各州满意度指数, fontsize16) ax.set_axis_off()图例标题如果缺了看图人得猜颜色代表什么。建议图例旁边一定写清楚“单位”或“指标名称”。3.4 第一版最常见的意外阿拉斯加和夏威夷用美国全境数据直接画图阿拉斯加会出现在左上角而且因为面积太大占了几乎四分之一画布夏威夷则孤零零悬在西侧海外离本土非常远。如果直接按经纬度画Rodrigues 旋转后还能凑合但网格下观感很差。针对这个问题最粗暴的做法是画图时过滤掉这两个州gdf_conus gdf[~gdf[STUSPS].isin([AK, HI])]只画本土 48 州加 DC图面会清爽很多但严格说这就不是“美国全境”了。如果需求必须包含阿拉斯加和夏威夷有两种处理惯例把它们平移插到底图左下角/右下角并加小框示意新闻媒体常用这个方式。使用合适的投影比如 Albers 等面积投影阿拉斯加虽然仍然偏大但至少位置关系比经纬度图协调。具体怎么做下一章展开。4. 投影与布局让阿拉斯加和夏威夷不毁掉整张图4.1 为什么 Cross 换图观感差别这么大经纬度图的问题本质是地球是球体平面图上越靠近高纬度面积失真越严重。美国本土大致在北纬 25 度到 49 度之间阿拉斯加的一部分已经伸到北纬 70 度附近。用简单的经纬度平面展开高纬度的阿拉斯加会被横向拉伸看起来极不协调。专业制图里美国全境图常用Albers Conical Equal Area也就是阿尔伯斯等积圆锥投影。它把圆锥面切过美国这块区域尽量保证每个州的面积比例关系正确。美国人口普查局出图时基本默认就是它。Cartopy 库里已经内置了这种投影调用很方便import cartopy.crs as ccrs import cartopy.feature as cfeature fig plt.figure(figsize(12, 8)) ax fig.add_subplot(1, 1, 1, projectionccrs.AlbersEqualArea()) ax.set_extent([-125, -65, 24, 50]) ax.add_feature(cfeature.STATES, edgecolorwhite, linewidth0.5)用 Cartopy 后边界数据坐标如果还是经纬度你不需要手动转换Cartopy 会自动投影到图上指定的坐标系。这就是它比裸用 Matplotlib 舒服的地方。4.2 第一次切投影后的踩坑边界“飞了”我当时第一次用 Cartopy 时直接把 GeoDataFrame 用plot()画到ax上结果图形一团乱麻有的州跑到海里去了有的州变形得看不出轮廓。排查了半天问题出在GeoDataFrame.plot()默认还是按它自身的坐标系统去绘制没有经过 Cartopy 的投影转换。正确写法是手动把几何体画成“投影后坐标”或者把 GeoDataFrame 先转换到目标 CRS。以 Geopandas 为例先把边界投影到阿尔伯斯target_crs ccrs.AlbersEqualArea().proj4_init gdf_albers gdf.to_crs(target_crs)然后用普通 Matplotlib 的方式绘制就不会和 Cartopy 冲突。或者你用 Cartopy 提供的ax.add_geometries()方法直接添加带投影信息的几何体ax.add_geometries(gdf.geometry, crsccrs.PlateCarree(), colorlightgray, edgecolorwhite)这里crsccrs.PlateCarree()表示“我这批几何体的坐标是经纬度”Cartopy 会负责转换。这条经验很重要两个库混合使用时要么统一用 Cartopy 的 add_geometries要么统一先转 CRS 再用 geopandas.plot()不要左右横跳。4.3 取与舍阿拉斯加和夏威夷的三种摆法我做过几种方案各有适用场景方案 A只画 CONUS本土 48 州适合业务数据本身不涉及阿拉斯加和夏威夷的场景。图面干净制图最快。标题里记得写明“不含阿拉斯加和夏威夷”。方案 B全境一起画但不做特殊处理用 Albers 等积投影后阿拉斯加虽然在图上偏大但位置关系合理。如果你只是想展示相对格局这个方案可以接受。缺点是美国本土会被压到图幅下半部细节损失。方案 C全境图加“缩略子图”主图画 CONUS然后在右下角或左下角插入两个小方框分别放阿拉斯加和夏威夷。政治报章上的美国地图很常见。代码思路from mpl_toolkits.axes_grid1.inset_locator import inset_axes # 主图 fig, ax plt.subplots(figsize(12, 8)) # 阿拉斯加缩略图注意这个小 ax 坐标系要手动设范围 ax_ak ax.inset_axes([0.05, 0.05, 0.15, 0.15]) ax_ak.set_extent([-175, -130, 50, 72]) ax_ak.add_geometries(gdf[gdf[STUSPS] AK].geometry, crsccrs.PlateCarree(), colorlightgray, edgecolorwhite) ax_ak.set_title(AK, fontsize8)这个方案最灵活但需要你把小图的位置、比例、图例单独调一遍属于“后面再加的锦上添花”。第一次做我建议你先用方案 B跑通全部流程后再回头调方案 C。4.4 指北针、比例尺要不要加纯软件工程背景的人常常纠结地图是不是必须加指北针和比例尺我的答案是如果是面向业务汇报的 Choropleth通常不需要因为读者看的是颜色分布不看方向导航但如果这张图要对外发布或印刷最好加上比例尺。加指北针在 Matplotlib 里没有现成组件得用 annotation 画一个简单的箭头或者在图角放一个字母 N。比例尺则涉及投影后距离计算容易被忽略。个人经验优先保证州界清楚、颜色准确、图例清晰这三样做好比任何装饰都加分。5. 交互化从静态图到可直接用的 Choropleth5.1 什么时候必须上交互静态 PNG 永远不等于“在线绘制”。对方需求里写了“在线”意味着你最终产出可能是一个网页页面用户鼠标悬停某个州能看到数值点击某个州能下钻或者能切换不同指标。这种需求用 Matplotlib 就力不从心了需要切换到Folium或Plotly。静态图和交互图的取舍我可以用一个简单公式判断只有我一个人看、每天生成一次嵌入式图表 → 静态图足够。数据要展示给多位非技术同事、需要在浏览器里反复点击探索 → 交互图。最终要嵌到公司大屏 / 数据门户 → 一般要走交互图。5.2 Folium 实现 Choropleth 的要点Folium 是 Leaflet 的地图封装输出一个 HTML 文件使用非常简单。核心代码import folium m folium.Map(location[39.5, -98.35], zoom_start4) folium.Choropleth( geo_dataus-states.json, namechoropleth, datadata, columns[fips, score], key_onfeature.id, fill_colorYlGnBu, fill_opacity0.7, line_opacity0.2, legend_name各州满意度指数, bins8, ).add_to(m) m.save(us_map.html)Folium 的好处在浏览器里直接出图不用配置服务缺点是它默认使用墨卡托投影和在线瓦片如果你在离线内网环境工作需要把底图瓦片下载下来或者关掉底图否则页面显示出来是一张空白网格。这是我在内网交付时踩过的一个实际大坑。离线方案我后面会提一嘴但那套做法涉及网络限制这里不展开。5.3 Plotly一句话生成可交互图如果你更喜欢纯 Python 出图不想引入太多前端概念Plotly Express 更省事import plotly.express as px fig px.choropleth( data, locationsfips, locationmodeUSA-states, # 关键参数按 FIPS 匹配美国州 colorscore, scopeusa, # 只显示美国 color_continuous_scaleYlGnBu, title各州满意度指数 ) fig.show()Plotly 的优势是悬停提示几乎不需要配置默认显示 locations 和 color 对应值。而且返回值是一个Figure对象后续还可以加注释、改布局比 Folium 少一层抽象。缺点是对于精确控制州界形状、复杂投影它没有 Cartopy 灵活。5.4 部署的小建议做“在线”场景时即便你用了 Folium 或 Plotly本质输出还是一个 HTML 页面。部署选择很朴素公司内网 → 把 HTML 文件放到任一 Web 服务器目录即可。数据门户 → 把图表片段嵌入已有框架。只给同事看 → 直接发 HTML 文件浏览器打开就能交互。伪技术与合规顾虑HTML 里如果引用了在线地图瓦片用户没外网时地图会残缺数据敏感时不要绑定外部在线底图。更好的做法是导入已经下载的瓦片包或使用纯本地 GeoJSON 渲染。6. 配色与标注地图好不好看差距都在细节6.1 色盲安全调色板我见过太多失败案例都出在配色上红配绿看着分明但全色盲用户根本分不出来。这就是为什么很多专业地图默认用ColorBrewer色板。Matplotlib 和 Folium 都内置了 ColorBrewer 系列常见几个YlGnBu适合连续性数据从浅黄到深蓝几乎不会错。RdYlGn适合有正负方向的数据但要注意红绿色盲用户。viridis/magmaMatplotlib 自带连续性好但地图观感偏“数据科学风”。如果团队里有人是色盲或者你不想冒风险直接用YlGnBu是最稳妥的。深浅方向一定要符合直觉数值高通常用深色数值低用浅色。反向会直接误导。6.2 连续数据和分类数据的用色差异Choropleth 不只是连续数值能用。如果你要标注的是“哪个州属于哪个区域”比如东北部、南部、中西部分区那就更适合用分类色板。分类色板要求“不同类别之间区分度大”而不是深浅渐变。实操上连续数据可以用bins做分箱比如把满意度分成低、中、高三档调节bins[60, 70, 80, 90]。好处是图例变成离散图例读者更容易理解坏处是数据在边界附近本来差异很小却会被分配到不同颜色造成“误读”。我的经验是如果数据是自然分为几档或者业务上确实按区间管理才用分箱否则一律用连续渐变。6.3 图例数值格式与单位颜色条上经常出现7.153042e06这种科学计数法或者小数点后拖着长长的数字。这种细节决定了图是不是“能直接发给客户”。我习惯在绘图前把数据列格式化或者对颜色条做 formatterimport matplotlib.ticker as ticker colorbar ax.collections[0].colorbar tick_locator ticker.MaxNLocator(nbins6) colorbar.locator tick_locator colorbar.update_ticks() colorbar.set_label(满意度指数) colorbar.ax.yaxis.set_major_formatter(ticker.FormatStrFormatter(%.1f))另外色条数字务必标注单位或说明。如果数值含义是百分比就在标题或图例标签里写“单位%”。6.4 标注州名可加可不加尺寸够大时给每个州加上两位缩写会让图更专业。做法是在 Matplotlib 里通过每个州的代表点添加文本from shapely.geometry import Point for _, row in gdf.iterrows(): representative_point row.geometry.representative_point() ax.annotate( row[STUSPS], xy(representative_point.x, representative_point.y), hacenter, vacenter, fontsize6, colorblack )如果州很多、图幅小标签会重叠成一团。那时的补救措施是缩小字号、只标数值有特点的州或者干脆不标州名只留图例。别追求“每个州都标”信息过载比信息不足更糟。7. 完整复盘一个真实交付场景的踩坑与取舍7.1 需求沟通客户说“在线地图”背后的潜台词一次实际项目里客户提供的唯一信息就是“在线绘制带颜色标注的美国地图”。我最初假设只是要一张静态图结果进一步追问发现客户希望地图更新时自动补色、点击州能看到该州的详细报表、并且要适配手机端。这三个需求彻底改变了技术选型。所以拿到标题后一定先问几个问题数据更新频率是多少日更、周更、还是“就画这一次”使用者是分析师还是领导领导喜欢大屏分析师要能下载数据。数据全部缺省时怎么展示灰色还是特殊符号包含哪些地理范围50 州 DC要不要阿拉斯加和夏威夷是否包含波多黎各这些问题先问清楚能省掉一半返工。7.2 文件组织单人在小项目里怎么放如果是我单独开发一个轻量交付文件组织会是这样us_map_project/ ├── data/ │ ├── raw/ │ │ ├── tl_2023_us_state.shp │ │ ├── tl_2023_us_state.dbf │ │ ├── tl_2023_us_state.shx │ │ └── tl_2023_us_state.prj │ ├── business_data.csv │ └── merged_data.geojson ├── scripts/ │ ├── 01_prepare.py │ ├── 02_static_map.py │ └── 03_interactive_map.py └── output/ ├── us_map.png └── us_map.html不建议把 shapefile 解压后放在一个很深的临时目录。边界数据属于可重复生成的依赖但业务数据要定期更新所以二者分开存放。合并后的 GeoJSON 缓存下来能加速后续绘图尤其是反复调试配色时不用每次都重新 merge。7.3 性能数据量从 50 个州涨到 3000 个县怎么办美国县级边界数据比州级大一个量级。如果你要标注到县一遍遍重画整个 GeoDataFrame 会很慢。几个优化技巧提前过滤掉不需要的行政区比如有些县数据缺失绘图时直接排除。用simplify对几何体做简化gdf.geometry gdf.geometry.simplify(tolerance0.01)能降低文件体积但过度简化会让边界锯齿明显。如果只是生成静态图可以把转换好的 CRS 缓存成 GeoJSON后续只读取缓存文件。7.4 上线后的维护自动刷新方案如果需求是每日更新静态页面会做砸。我会写一个定时脚本调用02_static_map.py和03_interactive_map.py生成最新 PNG / HTML然后同步到服务器目录。这个流程足够简单运行起来也稳定。点赞和分享并不是重点重点是“刷新后别人拿到的图是新的且不会因为数据缺失变丑”。8. 常见问题速查这里整理一组做美国 Choropleth 地图时的高频问题都是我实际遇到过的。问题可能原因解决方式画出来的地图空白 / 只有图例边界数据加载失败或 GeoDataFrame 为空检查 shapefile 路径、文件是否齐全颜色图例显示NaN或灰块业务数据未关联上检查 FIPS 是否字符串、merge 的 key 是否一致阿拉斯加占画面太大投影选择不合适 / 未处理 AK改用 Albers 投影或使用 CONUS 子图方案Folium 页面空白在线底图瓦片加载失败改用本地瓦片或关闭底图颜色区分不明显离群值导致线性映射范围过大设置vmin/vmax或按分箱绘制导出 PNG 模糊默认 figsize/dpi 太小设置figsize(16, 10), dpi200或bbox_inchestight县界密密麻麻挤在一起县级边界线太粗减小linewidth如 0.1标题中文显示为方块Matplotlib 缺中文字体指定字体参数plt.rcParams[font.sans-serif] [SimHei]这个表格本质上是排查手册。遇到问题时先按“数据有没有”“颜色对不对”“边界全不全”的顺序查90% 的报错都能快速定位。不要一上来就怀疑投影投影错误通常表现为图形变形而不是程序报错。9. 从踩坑里总结的几条实用技巧最后分享一段我自己的使用体会不一定严谨但很实在。第一先做最小可行图再谈美观。不要第一次写代码就想要同时处理好阿拉斯加、缩略图、州名标注、交互、配色。先用 10 行代码把数据关联和基础颜色跑通哪怕图很丑至少验证了链路。后面所有优化都是在这个链路上叠加。一上来做大全可能出现“边界文件路径错了、数据 merge 没对齐、投影搞混、图例科学计数法”等一串问题混杂在一起根本分不清从哪里排查。第二地图项目里“数据对上了”比“画得好看”重要得多。任何一张 Choropleth一旦某个州的颜色和真实数值不匹配整张图的公信力就崩了。所以每次 merge 完都打印isna()检查一遍导出前随机抽查三五个州的颜色深浅是否符合常识。第三图例是地图最容易出错的地方。色条单位、vmin/vmax、分类色阶的边界值都会影响读者对数据的理解。你可以请一个没看过原始数据的人来看这张图让 TA 说说图中最高值在哪、最低值在哪、颜色深浅代表什么。如果 TA 说得对这张图基本过关了如果 TA 理解有误图上再好看也没用。写到这里这套“在线绘制带颜色标注的美国地图”的方案已经足够支撑一个真实项目从零到交付了。我的建议是先跑通第 3 章的最小脚本再根据实际数据形态决定要不要切到 Cartopy、要不要加交互。地图可视化这个东西工具永远不是瓶颈数据关联的严密性和表达的有效性才是真正拉开差距的地方。
返回列表