Lonboard
以GeoArrow和GeoParquet等尖端技术为基础,结合基于 GPU 的地图渲染,旨在通过简单的界面以交互方式可视化大型地理空间数据集。
它能做什么:
怎么运行的:
主要特征:
如何使用:
命令行
pip install lonboard
Jupyter
! pip install lonboard
示例中的数据:lonboard示例数据
数据提取密码:1111
本示例将使用从 Ookla 的 "速度测试"应用程序收集并在 AWS 开放数据注册中心公开共享的数据:
import geopandas as gpd
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import shapely
from palettable.colorbrewer.diverging import BrBG_10
from lonboard import Map, ScatterplotLayer
from lonboard.colormap import apply_continuous_cmap
下面是 2019 年第一季度移动网络速度单一数据文件的 URL。
url = "https://ookla-open-data.s3.us-west-2.amazonaws.com/parquet/performance/type=mobile/year=2019/quarter=1/2019-01-01_performance_mobile_tiles.parquet"
可以直接从 AWS 获取该数据文件中的两列数据。该 pd.read_parquet 命令将对数据文件执行网络请求,因此在网络连接速度较慢的情况下可能需要一段时间。
# avg_d_kbps 是该数据点的平均下载速度,单位为千比特/秒
# tile 是表示给定 zoom-16 Web Mercator tile 的 WKT 字符串
columns = ["avg_d_kbps", "tile"]
df = pd.read_parquet(url, columns=columns)
df
tile
列包含代表几何图形的字符串,我们需要将这些字符串解析为几何图形,为了简单起见,将其转换为中心点。
tile_geometries = shapely.from_wkt(df["tile"])
tile_centroids = shapely.centroid(tile_geometries)
现在,可以根据下载速度和形状几何图形创建一个 geopandas GeoDataFrame。
gdf = gpd.GeoDataFrame(df[["avg_d_kbps"]], geometry=tile_centroids)
为确保该演示在大多数电脑上都能快速运行,过滤欧洲上空的边界框。
gdf = gdf.cx[-11.83:25.5, 34.9:59]
gdf
要呈现点数据,首先要创建ScatterplotLayer,然后将其添加到Map对象中:
layer = ScatterplotLayer.from_geopandas(gdf)
map_ = Map(layers=[layer])
map_
可以查看ScatterplotLayer的文档,了解它还允许哪些渲染选项。
layer.get_fill_color = [0, 0, 200, 200]
map_
在本例中,数据据有与每个地点相关的下载速度,因此可以根据下载速度分色表示。给定最小界限为 5000,最大界限为 50000,归一化速度的线性比例为 0 至 1。
# min_bound = gdf['avg_d_kbps'].min()
# max_bound = gdf['avg_d_kbps'].max()
min_bound = 5000
max_bound = 50000
download_speed = gdf['avg_d_kbps']
normalized_download_speed = (download_speed - min_bound) / (max_bound - min_bound) # 下载速度归一化
现在,normalized_download_speed会根据上面提供的界限进行线性缩放。请记住,色谱的输入范围是 0-1。因此,任何低于 0 的值都将获得颜色表中最左侧的颜色,而任何高于 1 的值都将获得颜色表中最右侧的颜色。
sns.boxplot(normalized_download_speed)
可以使用palettable
软件包提供的任何色谱,看看下面的BrBG_10
分歧色谱。
BrBG_10.mpl_colormap
现在,使用lonboard提供的辅助工具将颜色映射应用到normalized_download_speed上。可以在layer.get_fill_color中设置它,以更新现有颜色。
layer.get_fill_color = apply_continuous_cmap(normalized_download_speed, BrBG_10)
map_
可以将数组传递到该层支持的任何"accessors"中(这是以get_*开头的任何属性)。
# 在层更新为支持 pandas 系列之前,暂时将其转换为 numpy 数组
layer.get_radius = np.array(normalized_download_speed) * 200
layer.radius_units = "meters"
layer.radius_min_pixels = 0.5
map_
import matplotlib.pyplot as plt
import seaborn as sns
import geopandas as gpd
import palettable.colorbrewer.diverging
from lonboard import Map, PathLayer
from lonboard.colormap import apply_continuous_cmap
使用GeoPandas通过互联网获取这些数据(45MB),并将其加载到GeoDataFrame中。这使用的是pyogrio引擎,速度更快。请确保已安装pyogrio。
url = 'https://naciscdn.org/naturalearth/10m/cultural/ne_10m_roads_north_america.zip'
gdf = gpd.read_file(url, engine="pyogrio")
gdf
gdf = gdf[gdf["state"] == "California"]
layer = PathLayer.from_geopandas(gdf, width_min_pixels=0.8)
map_ = Map(layers=[layer])
map_
可以查看PathLayer的文档,了解它还允许哪些渲染选项。
layer.get_color = [200, 0, 200]
map_
根据属性来定制渲染效果。字段scalerank
显示道路在道路网络中的重要程度。
gdf['scalerank'].value_counts().sort_index().plot(kind='bar')
数值范围在3到12之间,要为这一列分配颜色映射,需要跨度在0和1之间的标准化值。
normalized_scale_rank = (gdf['scalerank'] - 3) / 9
sns.boxplot(normalized_scale_rank)
# 映射颜色
cmap = palettable.colorbrewer.diverging.PuOr_10
cmap.mpl_colormap
将在这个数组上使用apply_continuous_cmap来为数据生成颜色,只需将这个新数组设置到现有图层上,就能看到地图以新颜色更新!
layer.get_color = apply_continuous_cmap(values=normalized_scale_rank, cmap=cmap, alpha=0.8)
map_
案例来源:https://deck.gl/examples/brushing-extension
该数据集最初来自美国人口普查局,代表了2009-2013年间各县的人口迁入和迁出情况。
import geopandas as gpd
import numpy as np
import pandas as pd
import pyarrow as pa
import requests
import shapely
from matplotlib.colors import Normalize
from lonboard import Map, ScatterplotLayer
from lonboard.experimental import ArcLayer, BrushingExtension
从1deck.gl-data1资源库中的版本获取数据。
url = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/arc/counties.json"
r = requests.get(url)
source_data = r.json()
将原始数据(即数据图表)归一化为表格结构,其中每一行代表一个来源县和目标县之间的一条"弧"。
arcs = []
targets = []
sources = []
pairs = {}
features = source_data["features"]
for i, county in enumerate(features):
flows = county["properties"]["flows"]
target_centroid = county["properties"]["centroid"]
total_value = {
"gain": 0,
"loss": 0,
}
for to_id, value in flows.items():
if value > 0:
total_value["gain"] += value
else:
total_value["loss"] += value
# 忽略过小的值
if abs(value) < 50:
continue
pair_key = "-".join(map(str, sorted([i, int(to_id)])))
source_centroid = features[int(to_id)]["properties"]["centroid"]
gain = np.sign(flows[to_id])
# add point at arc source
sources.append(
{
"position": source_centroid,
"target": target_centroid,
"name": features[int(to_id)]["properties"]["name"],
"radius": 3,
"gain": -gain,
}
)
# 消除重复弧
if pair_key in pairs.keys():
continue
pairs[pair_key] = True
if gain > 0:
arcs.append(
{
"target": target_centroid,
"source": source_centroid,
"value": flows[to_id],
}
)
else:
arcs.append(
{
"target": source_centroid,
"source": target_centroid,
"value": flows[to_id],
}
)
# 在弧形目标上添加点
targets.append(
{
**total_value,
"position": [target_centroid[0], target_centroid[1], 10],
"net": total_value["gain"] + total_value["loss"],
"name": county["properties"]["name"],
}
)
# 按半径排序,大目标 -> 小目标
targets = sorted(targets, key=lambda d: abs(d["net"]), reverse=True)
normalizer = Normalize(0, abs(targets[0]["net"]))
对于数据集中的每一行数据,可以将其传递给任何接受ColorAccessor的参数。
# 迁出
SOURCE_COLOR = [166, 3, 3]
# 迁入
TARGET_COLOR = [35, 181, 184]
# 合并为单个记录,用作查找表
COLORS = np.vstack(
[np.array(SOURCE_COLOR, dtype=np.uint8), np.array(TARGET_COLOR, dtype=np.uint8)]
)
SOURCE_LOOKUP = 0
TARGET_LOOKUP = 1
brushing_extension = BrushingExtension(brushing_radius=200000)
将来源字典列表转换为GeoPandas的GeoDataFrame,然后传入ScatterplotLayer。
为源点创建ScatterplotLayer。
source_arr = np.array([source["position"] for source in sources])
source_positions = shapely.points(source_arr[:, 0], source_arr[:, 1])
source_gdf = gpd.GeoDataFrame(
pd.DataFrame.from_records(sources)[["name", "radius", "gain"]],
geometry=source_positions,
)
# 使用查找表 (COLORS),将目标颜色或源颜色应用到数组中
source_colors_lookup = np.where(source_gdf["gain"] > 0, TARGET_LOOKUP, SOURCE_LOOKUP)
source_fill_colors = COLORS[source_colors_lookup]
source_layer = ScatterplotLayer.from_geopandas(
source_gdf,
get_fill_color=source_fill_colors,
radius_scale=3000,
pickable=False,
extensions=[brushing_extension],
)
为目标点创建ScatterplotLayer。
targets_arr = np.array([target["position"] for target in targets])
target_positions = shapely.points(targets_arr[:, 0], targets_arr[:, 1])
target_gdf = gpd.GeoDataFrame(
pd.DataFrame.from_records(targets)[["name", "gain", "loss", "net"]],
geometry=target_positions,
)
# 使用查找表 (COLORS),将目标颜色或源颜色应用到数组中
target_line_colors_lookup = np.where(target_gdf["net"] > 0, TARGET_LOOKUP, SOURCE_LOOKUP)
target_line_colors = COLORS[target_line_colors_lookup]
target_ring_layer = ScatterplotLayer.from_geopandas(
target_gdf,
get_line_color=target_line_colors,
radius_scale=4000,
pickable=True,
stroked=True,
filled=False,
line_width_min_pixels=2,
extensions=[brushing_extension],
)
注意:ArcLayer目前无法从GeoDataFrame创建,因为它需要两个点列,而不是一个,这也是它仍被标记为"experimental"模块的主要原因。
在这里,为每一列点传递一个numpy数组。只要数组的形状是(N, 2)或(N, 3)(即二维或三维坐标)。
value = np.array([arc["value"] for arc in arcs])
get_source_position = np.array([arc["source"] for arc in arcs])
get_target_position = np.array([arc["target"] for arc in arcs])
table = pa.table({"value": value})
arc_layer = ArcLayer(
table=table,
get_source_position=get_source_position,
get_target_position=get_target_position,
get_source_color=SOURCE_COLOR,
get_target_color=TARGET_COLOR,
get_width=1,
opacity=0.4,
pickable=False,
extensions=[brushing_extension],
)
根据上述三个图创建一个地图。将鼠标悬停在地图上时,它应该只显示光标附近的弧线。可以修改brushing_extension.brushing_radius来控制光标周围刷子的大小。
map_ = Map(layers=[source_layer, target_ring_layer, arc_layer], picking_radius=10)
map_
import geopandas as gpd
from palettable.colorbrewer.sequential import Blues_8
from lonboard import Map, PathLayer
from lonboard.colormap import apply_continuous_cmap
url = "https://storage.googleapis.com/fao-maps-catalog-data/geonetwork/aquamaps/rivers_asia_37331.zip"
gdf = gpd.read_file(url, engine="pyogrio")
gdf['Strahler'].value_counts()
layer = PathLayer.from_geopandas(gdf)
layer.get_color = apply_continuous_cmap(gdf['Strahler'] / 7, Blues_8)
layer.get_width = gdf['Strahler']
layer.width_scale = 3000
layer.width_min_pixels = 0.5
m = Map(layers=[layer])
m
lonboard学习文档:https://developmentseed.org/lonboard/latest/
lonboard仓库:https://github.com/developmentseed/lonboard