一些地图可视化总结

  • 项目地图可视化总结 Folium && Plotly Express

  • 资料来源:

    https://www.biaodianfu.com/folium.html
    https://python-visualization.github.io/folium/

  • 更新

    1
    2022.01.13 初始

导语

gis 项目绝对逃不过地图可视化…

python 的地图可视化包有很多,不一而足,仅总结项目涉及到的几个库,一些示例代码及坑..

folium: 项目主力使用,官方文档 官方例程

  • 散点图
  • 热力图
  • 静态轨迹图
  • 轨迹时序图
  • 轨迹动图
  • 结果对比图

plotly-express : 代码一般简写 px

  • 3d 轨迹图 (忽悠 验收专用,看起来很高大上)

前排提醒

  • Folium 默认底图是 osm 如果展示访问涉及国家边界,会是很大的风险点.
    • 如果可能务必更换底图,个人推荐是 高德地图 几乎无缝使用.
  • 多图杀猫 堆叠,同一数据的各种炫酷展示,提供几套方案备选,汇报用得上.
  • 3D + 交互 = 300% 通过概率.不是

folium

folium 使用的结果很满意,基本 cover 了项目需要的地图数据展示.

学习folium

  • 中文介绍+入门 私以为这一篇比较合个人口味 Python地图可视化之Folium,如果是简单使用,这一篇就够了.
  • 插件的使用几乎都在 官方例程 中有体现,下载下来一个一个看,总用时不超过 2 个小时.根据需要的效果挑挑拣拣,忽哟 验收的图就出来了.

Folium 默认是 osm 底图,如果可能尽量更换底图.

  • 瓦片底图有高德/百度/腾讯,高德最方便,除了 gcj坐标 基本无缝使用.百度还有 bd 坐标系问题,腾讯地图也不是那么完美.
1
2
3
4
5
6
7
8
9
10
m = folium.Map(
location=[39.917834, 116.397036], #北京
zoom_start=11, #地图初始缩放级别
width='100%',
height='100%',
zoom_control='False',
control_scale=True,
tiles=
'http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}&ltype=6',
attr='AutoNavi')

folium 画图基本上都是在 folium.Map 实例 m 上添加新对象,最后调用 m.save() 保存或者直接在 jupyter 显示.

下面是项目中常见的类型,只总结了几个.给出了示例代码.

散点图

单个点是 folium.Marker 实例.

  • popup 是点击后展示的文本
  • tooltip 是鼠标滑过时展示文本
1
2
3
4
5
6
7
8
9
10
11
folium.Marker(
[39.917834, 116.397036],
popup=(f'点击: {1:.0f}<br>\
点击2: {2:.0f}\
点击3: {3:.0f}<br>'),
tooltip='滑动',
icon=DivIcon(
icon_size=(150, 36),
icon_anchor=(7, 20),
html=f'<div style="font-size: 13pt;">点</div>',
)).add_to(m)

以上只是把文字+事件嵌到了地图上,还不够,还需在地图上对应位置标明一个点.配色随便选的.

1
2
3
4
5
6
7
8
9
10
incidents = folium.FeatureGroup()
incidents.add_child(
folium.CircleMarker(
[39.917834, 116.397036],
radius=7, # define how big you want the circle markers to be
color='yellow',
fill=True,
fill_color='red',
fill_opacity=0.4))
m.add_child(incidents)

51.png

真实的散点图,需要将上面的过程重复若干次.当然颜色也需要重新调整.

热力图

热力图用来展示分布变化屡试不爽,官方提供了两个示例,足矣.

  • Heatmap: 直接传入经纬度的 list 绘制静态热力图
  • HeatMapWithTime: 随时间变化的热力图,展示变化数据时更加实用.

轨迹图

轨迹图实际用到了 3 种

  • 简单连线的轨迹图
  • 轨迹时序图
  • 标明反向的轨迹动图

轨迹图

传入经纬度的 list 之后将点连接.

1
2
3
4
5
6
7
locals = [[39.917834, 116.397036],[39.937834, 116.397036],[39.937834, 116.377036]]
folium.PolyLine( # polyline方法为将坐标用实线形式连接起来
locals, # 将坐标点连接起来
weight=4, # 线的大小为4
color='red', # 线的颜色为红色
opacity=0.8, # 线的透明度
).add_to(m) # 将这条线添加到刚才的区域m内

37.png

轨迹动图

借助 AntPath 插件,可以将轨迹的方向标出来.

1
2
3
4
5
6
7
8
9
locals = [[39.917834, 116.397036],[39.937834, 116.397036],[39.937834, 116.377036]]
folium.plugins.AntPath(
locations=locals,
paused=False,
color='red',
dash_array=[20, 30],
delay=800,
popup=id, #点击
tooltip=id).add_to(m)

28.png

轨迹时序图

来自 How to display a time series of folium maps?

轨迹时序图:

  • 实际上是合成了带时间戳的 GeoJSONs
  • 借助 TimestampedGeoJson 插件显示

假设源数据是 datas

  • [[lat,lon,time],xx]
  • 时间单位必须是字符串

最大的一个坑是 GeoJson 描述点时是 lon 在前..

实际上这样的数据转换定义一个模板文件比较好,时间有限直接强制转换了,请不要直接在生产环境使用,会被同事喷死的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
for i in range(len(datas) - 1):  #取经纬度 时间
lines.append({
'coordinates': [
[datas[i][1], datas[i][0]],
[datas[i + 1][1], datas[i + 1][0]],
],
'dates': [datas[i][2], datas[i + 1][2]],
'weight':
5, #线宽
'color':
'#3388ff', #颜色
'opacity':
0.5, #线不透明度
})

features = [{
'type': 'Feature',
'geometry': {
'type': 'LineString',
'coordinates': line['coordinates'],
},
'properties': {
'times': line['dates'],
'style': {
'color': line['color'],
'weight': line['weight'] if 'weight' in line else 5,
'opacity': line['opacity'] if 'opacity' in line else 0.5,
}
}
} for line in lines]

plugins.TimestampedGeoJson(
{
'type': 'FeatureCollection',
'features': features,
},
period='PT1M', #P1M 一月 P1D 一天 P1H 一小时 PT1M 一分钟
add_last_point=False).add_to(m)

结果对比图

显示两个上下/左右同步显示的地图,可以加载不同数据源以对比.

  • 借助于 DualMap 插件

官方示例 -> plugin-DualMap.ipynb

其他小点

翻阅官方文档时的小点

鼠标位置追踪

MousePosition 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
from folium.plugins import MousePosition
formatter = "function(num) {return L.Util.formatNum(num, 3) + ' º ';};"

MousePosition(
position="topright",
separator=" | ",
empty_string="NaN",
lng_first=True,
num_digits=20,
prefix="Coordinates:",
lat_formatter=formatter,
lng_formatter=formatter,
).add_to(m)

图例

添加一个可拖动图例

  • issue # How can I add a legend to a folium map?
  • 源码来自 How does one add a legend (categorical) to a folium map,效果也见这个页面.
  • 不太懂 html 的描述语言,仅调整到自己能用的状态.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
template = """
{% macro html(this, kwargs) %}

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>jQuery UI Draggable - Default functionality</title>
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">

<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>

<script>
$( function() {
$( "#maplegend" ).draggable({
start: function (event, ui) {
$(this).css({
right: "auto",
top: "auto",
bottom: "auto",
});
}
});
});

</script>
</head>
<body>


<div id='maplegend' class='maplegend'
style='position: absolute; z-index:9999; border:2px solid grey; background-color:rgba(255, 255, 255, 0.8);
border-radius:6px; padding: 10px; font-size:14px; right: 20px; top: 40px;'>

<div class='legend-title'>图例</div>
<div class='legend-scale'>
<ul class='legend-labels'>
<li><span style='background:red;opacity:0.7;'></span>图例1</li>
<li><span style='background:darkblue;opacity:0.7;'></span>图例2</li>
<li><span style='background:green;opacity:0.7;'></span>图例3</li>

</ul>
</div>
</div>

</body>
</html>

<style type='text/css'>
.maplegend .legend-title {
text-align: left;
margin-bottom: 5px;
font-weight: bold;
font-size: 90%;
}
.maplegend .legend-scale ul {
margin: 0;
margin-bottom: 5px;
padding: 0;
float: left;
list-style: none;
}
.maplegend .legend-scale ul li {
font-size: 80%;
list-style: none;
margin-left: 0;
line-height: 18px;
margin-bottom: 2px;
}
.maplegend ul.legend-labels li span {
display: block;
float: left;
height: 16px;
width: 30px;
margin-right: 5px;
margin-left: 0;
border: 1px solid #999;
}
.maplegend .legend-source {
font-size: 80%;
color: #777;
clear: both;
}
.maplegend a {
color: #777;
}
</style>
{% endmacro %}"""

macro = MacroElement()
macro._template = Template(template)

m.get_root().add_child(macro)

显示表格数据

同理不太懂 html 描述语言,仅调整到项目能用状态.

  • data 是 pandas.dataframe
  • 整个过程貌似是转换成 html 再插入地图.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import branca

html = data[['id', 'normal', 'abnormal']].to_html(
classes=
"table table-striped table-hover table-condensed table-responsive",
index=False)

legend_html = """
{% macro html(this, kwargs) %}
<div style="
position: fixed;
bottom: 20px;
right: 20px;
width: auto;
height: auto;
z-index:9999;
font-size:14px;
background-color: #ffffff;
opacity: 0.8;
">
""" + \
html + \
"""
</div>
{% endmacro %}
"""

legend = branca.element.MacroElement()
legend._template = branca.element.Template(legend_html)

m.get_root().add_child(legend)

动态调整缩放

最开始的自定义底图时就有 zoom_start 这个缩放等级,但是随着数据不同,缩放等级最好还是动态调整.

1
m.fit_bounds(m.get_bounds())

但是在 DualMap 的对比图时不可用,会时好时坏.

plotly-express

plotly-express 貌似是 plotly 的使用太繁琐,然后出的一个简化使用的版本.能直接传入 pandas 的 dataframe ,似乎值得深度了解.

3d 轨迹图

实际上是使用了 px.scatter_3d() 这一个方法绘制 时间-经纬度的 3d 图.

  • 支持直接传入 dataframe ..实在是不用都对不起编写这个库的人..
  • 官方文档 + 示例
  • df 是传入的 dataframe
1
2
3
4
5
6
7
8
9
10
fig = px.scatter_3d(
data_frame=df,
x=df[lat],
y=df[lon],
z=df[timekey],# 时间轴的标签
size='lat', # 标记尺寸有关
size_max=size, # 标记最大尺寸
color='color', # 颜色的标签,要求 df 有对应数据,最好是 int 值,这样 px 能直接映射到一个完整的光谱.
)
fig.show()

最后保存与 folium 略有不同

1
plotly.offline.plot(fig, filename='./3d.html')