本文详解使用 Plotly Express 与 Dash 构建可交互县级人口热力图的完整方案,重点解决原生 restyle 下因数据索引错位导致的“地图显示错乱”问题,并提供可直接运行的模块化代码。
本文详解使用 plotly express 与 dash 构建可交互县级人口热力图的完整方案,重点解决原生 `restyle` 下因数据索引错位导致的“地图显示错乱”问题,并提供可直接运行的模块化代码。
在使用 Plotly 的 px.choropleth 绘制美国县级人口地图时,若通过 updatemenus + restyle 方式动态切换子集数据(如按人口分段筛选),极易出现地理要素与数值严重错配的问题——例如 King 县(人口超226万)在“行顺序强行映射新数值,而非按 FIPS 键对齐。
✅ 正确解法是:每次筛选都生成全新、结构完整的子集 DataFrame,并重建整个 choropleth 图形对象。这正是 Dash 的核心优势——通过回调(callback)动态重绘图表,确保 locations、color 和 geojson 三者严格一致。
以下是推荐的生产级实现步骤:
1. 数据预处理(关键!)
确保 FIPS 列为字符串类型(长度统一为5位),并与 GeoJSON 中的 id 或 properties.GEOID 格式一致(本例中 geojson-counties-fips.json 的 properties.GEOID 即为5位FIPS):
grouped_fips_pop['FIPS'] = grouped_fips_pop['FIPS'].astype(str).str.zfill(5)
2. 使用 Dash 构建交互式应用(推荐方案)
import dash
from dash import dcc, html, Input, Output, callback_context
import plotly.express as px
import requests
import json
import pandas as pd
# 加载并过滤华盛顿州 GeoJSON(STATE == '53')
r = requests.get('https://raw.githubusercontent.com/plotly/datasets/master/geojson-counties-fips.json')
counties = json.loads(r.text)
counties['features'] = [f for f in counties['features'] if f['properties']['STATE'] == '53']
# 初始化 Dash 应用
app = dash.Dash(__name__)
# 定义布局
app.layout = html.Div([
html.H1("2022 Population in Washington State", style={'textAlign': 'center'}),
dcc.Dropdown(
id='pop-filter',
options=[
{'label': 'All Counties', 'value': 'all'},
{'label': 'Population < 50,000', 'value': 'lt50k'},
{'label': '50,000 ≤ Population < 100,000', 'value': '50k-100k'},
{'label': 'Population ≥ 100,000', 'value': 'gte100k'}
],
value='all',
clearable=False,
style={'width': '300px', 'margin': '20px auto'}
),
dcc.Graph(id='wa-map')
])
# 回调:根据筛选条件返回新图形
@app.callback(
Output('wa-map', 'figure'),
Input('pop-filter', 'value')
)
def update_map(filter_value):
# 动态筛选数据
if filter_value == 'all':
df_filtered = grouped_fips_pop.copy()
elif filter_value == 'lt50k':
df_filtered = grouped_fips_pop[grouped_fips_pop['County Pop 2022'] < 50_000].copy()
elif filter_value == '50k-100k':
mask = (grouped_fips_pop['County Pop 2022'] >= 50_000) & \
(grouped_fips_pop['County Pop 2022'] < 100_000)
df_filtered = grouped_fips_pop[mask].copy()
else: # gte100k
df_filtered = grouped_fips_pop[grouped_fips_pop['County Pop 2022'] >= 100_000].copy()
# 重建 choropleth(关键:完全独立的新图)
fig = px.choropleth(
df_filtered,
geojson=counties,
locations='FIPS', # 确保与 GeoJSON 的 GEOID 匹配
color='County Pop 2022',
color_continuous_scale='Plasma',
range_color=[grouped_fips_pop['County Pop 2022'].min(),
grouped_fips_pop['County Pop 2022'].max()],
scope='usa',
labels={'County Pop 2022': 'Population'},
hover_name='County',
hover_data=['County Pop 2022']
)
# 地图布局优化
fig.update_geos(
fitbounds="locations",
visible=False,
projection_type='albers usa',
center={'lat': 47.5, 'lon': -120}
)
fig.update_layout(
title=f"Washington State Counties — {filter_value.replace('_', ' ').title()}",
title_x=0.5,
margin={"r":0,"t":50,"l":0,"b":0},
coloraxis_colorbar=dict(title="Population")
)
return fig
# 启动服务(开发模式)
if __name__ == '__main__':
app.run_server(debug=True, port=8050)⚠️ 注意事项与最佳实践
- 永远避免 restyle 修改子集数据:restyle 适用于同一数据集的属性变更(如颜色缩放、透明度),不适用于结构变化。
- GeoJSON 一致性:确认 geojson 文件中每个 feature.properties.GEOID 是5位字符串(如 '53033'),且 locations 列格式完全匹配。
- 性能优化:若数据量极大,可在回调中添加 prevent_initial_call=True,或对 range_color 使用固定全局范围(如示例中所示),避免每次重绘时色标跳变。
- 部署提示:生产环境请使用 gunicorn + waitress 替代 run_server(),并设置 debug=False。
该方案彻底规避了索引错位风险,保证每次筛选后地图呈现真实、准确的地理分布,是构建专业级交互式人口地图的标准范式。

















