地理图形 (Geoshape)#

mark_geoshape 表示任意形状,其几何形状由指定的空间数据确定。

地理图形标记属性#

一个 geoshape 标记可以包含任何标准标记属性

基本地图#

Altair 可以处理许多不同的地理数据格式,包括 geojson 和 topojson 文件。通常,最方便的输入格式是 GeoDataFrame。这里我们加载 Natural Earth 110m Cultural Vectors 数据集,并使用 mark_geoshape 创建一个基本地图

import altair as alt
from vega_datasets import data
import geopandas as gpd

url = "https://naciscdn.org/naturalearth/110m/cultural/ne_110m_admin_0_countries.zip"
gdf_ne = gpd.read_file(url)  # zipped shapefile
gdf_ne = gdf_ne[["NAME", "CONTINENT", "POP_EST", 'geometry']]

alt.Chart(gdf_ne).mark_geoshape()

在上面的示例中,Altair 应用了默认的蓝色 fill 颜色,并使用了默认的地图投影 (equalEarth)。我们可以使用标准标记属性自定义颜色和边界描边宽度。使用 project 方法,我们还可以手动定义自定义地图投影

alt.Chart(gdf_ne).mark_geoshape(
    fill='lightgrey', stroke='white', strokeWidth=0.5
).project(
    type='albers'
)

聚焦 & 过滤#

默认情况下,Altair 会自动调整投影,以便所有数据都适合图表的宽度和高度。可以使用多种方法聚焦空间数据的特定区域。具体来说

  1. 在 GeoDataFrame 中过滤源数据。

  2. 使用 transform_filter 过滤源数据。

  3. project 方法中指定 scale (缩放级别) 和 translate (平移)。

  4. project 方法中指定 fit (范围),并在标记属性中指定 clip=True

以下示例应用这些方法聚焦非洲大陆

  1. 在 GeoDataFrame 中过滤源数据

gdf_sel = gdf_ne.query("CONTINENT == 'Africa'")

alt.Chart(gdf_sel).mark_geoshape()
  1. 使用 transform_filter 过滤源数据

alt.Chart(gdf_ne).mark_geoshape().transform_filter(
    alt.datum.CONTINENT == 'Africa'
)
  1. project 方法中指定 scale (缩放级别) 和 translate (平移)

alt.Chart(gdf_ne).mark_geoshape().project(
    scale=200,
    translate=[160, 160]  # lon, lat
)
  1. project 方法中指定 fit (范围),并在标记属性中指定 clip=True

extent_roi = gdf_ne.query("CONTINENT == 'Africa'")
xmin, ymin, xmax, ymax = extent_roi.total_bounds

# fit object should be a GeoJSON-like Feature or FeatureCollection
extent_roi_feature = {
    "type": "Feature",
    "geometry": {"type": "Polygon",
                 "coordinates": [[
                     [xmax, ymax],
                     [xmax, ymin],
                     [xmin, ymin],
                     [xmin, ymax],
                     [xmax, ymax]]]},
    "properties": {}
}

alt.Chart(gdf_ne).mark_geoshape(clip=True).project(
    fit=extent_roi_feature
)

笛卡尔坐标#

Altair 的默认投影是 equalEarth,它准确地表示世界陆地面积的相对关系。此默认设置假定您的几何图形以度为单位,并参考经度和纬度值。另一种广泛用于数据可视化的坐标系是二维笛卡尔坐标系。此坐标系不考虑地球的曲率。

在以下示例中,输入几何图形未进行投影,而是使用 identity 投影类型直接以原始坐标渲染。我们还需要定义 reflectY,因为 Canvas 和 SVG 将正 y 轴视为向下。

alt.Chart(gdf_sel).mark_geoshape().project(
    type='identity',
    reflectY=True
)

映射多边形 (Mapping Polygons)#

以下示例使用 color 编码映射 NAME 列的可视属性。

alt.Chart(gdf_sel).mark_geoshape().encode(
    color='NAME:N'
)

由于每个国家都由一个(多)多边形表示,我们可以将 strokefill 定义分开如下

alt.Chart(gdf_sel).mark_geoshape(
    stroke='white',
    strokeWidth=1.5
).encode(
    fill='NAME:N'
)

映射线条 (Mapping Lines)#

默认情况下,对于 mark_geoshape,Altair 假定标记的颜色用于填充颜色而不是描边颜色。这意味着如果您的源数据包含(多)线条,您必须显式将 filled 值定义为 False

比较

gs_line = gpd.GeoSeries.from_wkt(['LINESTRING (0 0, 1 1, 0 2, 2 2, -1 1, 1 0)'])
alt.Chart(gs_line).mark_geoshape().project(
    type='identity',
    reflectY=True
)

gs_line = gpd.GeoSeries.from_wkt(['LINESTRING (0 0, 1 1, 0 2, 2 2, -1 1, 1 0)'])
alt.Chart(gs_line).mark_geoshape(
    filled=False
).project(
    type='identity',
    reflectY=True
)

使用这种方法,也可以将多边形样式化,就像它们是线串一样

alt.Chart(gdf_sel).mark_geoshape(
    filled=False,
    strokeWidth=1.5
).encode(
    stroke='NAME:N'
)

映射点 (Mapping Points)#

当点在 GeoDataFrame 中定义为 Points 时,可以使用 mark_geoshape 绘制。我们首先将多边形的质心指定为点几何图形并绘制它们

# .copy() to prevent changing the original `gdf_sel` variable
# derive centroid in a projected CRS (in meters) and visualize in a geographic CRS (in degrees).
gdf_centroid = gpd.GeoDataFrame(
    data=gdf_sel.copy(),
    geometry=gdf_sel.geometry.to_crs(epsg=3857).centroid.to_crs(epsg=4326)
)

alt.Chart(gdf_centroid).mark_geoshape()

注意:要对点使用 size 编码,您需要结合 latitudelongitude 编码通道定义来使用 mark_circle

gdf_centroid["lon"] = gdf_centroid.geometry.x
gdf_centroid["lat"] = gdf_centroid.geometry.y

alt.Chart(gdf_centroid).mark_circle().encode(
    longitude="lon:Q", latitude="lat:Q", size="POP_EST:Q"
)

Altair 还包含与地理要素相关的表达式。例如,我们可以使用 geoCentroid 表达式定义 centroids

basemap = alt.Chart(gdf_sel).mark_geoshape(
     fill='lightgray', stroke='white', strokeWidth=0.5
)

bubbles = alt.Chart(gdf_sel).transform_calculate(
    centroid=alt.expr.geoCentroid(None, alt.datum)
).mark_circle(
    stroke='black'
).encode(
    longitude='centroid[0]:Q',
    latitude='centroid[1]:Q',
    size="POP_EST:Q"
)

(basemap + bubbles).project(
    type='identity', reflectY=True
)

分层设色图 (Choropleths)#

除了将人口规模显示为气泡外,另一种方法是创建“分层设色图”。这些是地理热力图,其中每个区域的颜色映射到数据框中某一列的值。

alt.Chart(gdf_sel).mark_geoshape().encode(
    color='POP_EST'
)

当我们创建分层设色图时,需要小心,因为尽管颜色会根据我们感兴趣的列的值而变化,但大小与每个国家的面积相关,我们可能会忽略小国家中有趣的值,仅仅因为我们在地图上不容易看到它们(例如,如果要可视化人口密度)。

查找数据集#

有时您的数据分成了两个数据集。一个包含数据的 DataFrame 和一个包含几何图形的 GeoDataFrame。在这种情况下,您可以使用 lookup 转换从另一个数据集收集相关信息。

您可以双向使用 lookup 转换

  1. 使用包含几何图形的 GeoDataFrame 作为源,并在另一个 DataFrame 中查找相关信息。

  2. 使用 DataFrame 作为源,并在 GeoDataFrame 中查找相关几何图形。

取决于您的用例,其中一种或另一种方法可能更有利。

首先我们展示第一种方法的示例。这里我们从 df_us_unemp DataFrame 中查找字段 rate,其中 gdf_us_counties GeoDataFrame 用作源

import altair as alt
from vega_datasets import data
import geopandas as gpd

gdf_us_counties = gpd.read_file(data.us_10m.url, driver='TopoJSON', layer='counties')
df_us_unemp = data.unemployment()

alt.Chart(gdf_us_counties).mark_geoshape().transform_lookup(
    lookup='id',
    from_=alt.LookupData(data=df_us_unemp, key='id', fields=['rate'])
).encode(
    alt.Color('rate:Q')
).project(
    type='albersUsa'
)

接下来,我们展示第二种方法的示例。这里我们通过字段 geometrytypegdf_us_counties GeoDataFrame 中查找几何图形,其中 df_us_unemp DataFrame 用作源。

alt.Chart(df_us_unemp).mark_geoshape().transform_lookup(
    lookup='id',
    from_=alt.LookupData(data=gdf_us_counties, key='id', fields=['geometry', 'type'])
).encode(
    alt.Color('rate:Q')
).project(
    type='albersUsa'
)

分层设色图分类#

除了显示连续的定量变量外,分层设色图还可以用于显示变量的离散级别。虽然在将连续变量离散化时通常应小心不要创建人为的分组,但当变量存在我们希望清楚展示的自然截止级别时,这可能非常有用。我们首先定义一个实用函数 classify(),我们将用它来展示制作分层设色图的不同方法。我们应用它来使用 linear 尺度定义 2018 年美国各县失业统计数据的分层设色图。

import altair as alt
from vega_datasets import data
import geopandas as gpd

def classify(type, domain=None, nice=False, title=None):
    # define data
    us_counties = alt.topo_feature(data.us_10m.url, "counties")
    us_unemp = data.unemployment.url

    # define choropleth scale
    if "threshold" in type:
        scale = alt.Scale(type=type, domain=domain, scheme="inferno")
    else:
        scale = alt.Scale(type=type, nice=nice, scheme="inferno")

    # define title
    if title is None:
        title = type

    # define choropleth chart
    choropleth = (
        alt.Chart(us_counties, title=title)
        .mark_geoshape()
        .transform_lookup(
            lookup="id", from_=alt.LookupData(data=us_unemp, key="id", fields=["rate"])
        )
        .encode(
            alt.Color(
                "rate:Q",
                scale=scale,
                legend=alt.Legend(
                    direction="horizontal", orient="bottom", format=".1%"
                ),
            )
        )
        .project(type="albersUsa")
    )
    return choropleth

classify(type='linear')

我们使用 mark_geoshape() 可视化 2018 年失业 rate 百分比,使用 linear 尺度范围,以在地图上呈现空间模式。每个值/县都定义了一个独特的颜色。这提供了一些洞察力,但我们通常喜欢将分布分组到类别中。

通过将值分组到类别中,您可以对数据集进行分类,从而使每个类别中的所有值/几何图形分配相同的颜色。

这里我们介绍一些 Altair 可以使用的尺度方法

  • quantile,这种类型将您的数据集 (domain) 划分成大小相似的区间。每个类别包含或多或少相同数量的值/几何图形 (equal counts)。尺度定义如下所示

alt.Scale(type='quantile')

并在我们的实用函数中应用

classify(type='quantile', title=['quantile', 'equal counts'])
  • quantize,这种类型将您的数据集的范围 (range) 划分成相等的区间。每个类别包含不同数量的值,但步长是相等的 (equal range)。尺度定义如下所示

alt.Scale(type='quantize')

并在我们的实用函数中应用

classify(type='quantize', title=['quantize', 'equal range'])

quantize 方法也可以与 nice 结合使用。这会在应用量化之前对领域进行“美化”。例如

alt.Scale(type='quantize', nice=True)

并在我们的实用函数中应用

classify(type='quantize', nice=True, title=['quantize', 'equal range nice'])
  • threshold,这种类型通过手动指定截止值将您的数据集划分为不同的类别。每个类别由定义的类别分隔。尺度定义如下所示

alt.Scale(type='threshold', domain=[0.05, 0.20])

并在我们的实用函数中应用

classify(type='threshold', domain=[0.05, 0.20])

上面的定义将创建 3 个类别。一个类别的值低于 0.05,一个类别的值从 0.050.20,另一个类别的值高于 0.20

那么哪种方法为分层设色图提供了最佳数据分类?像往常一样,这取决于具体情况。

还有另一种常用方法有助于确定类别断点。此方法将最大化类别内值的相似性,同时最大化类别之间的距离 (natural breaks)。此方法也称为 Fisher-Jenks 算法,类似于一维的 k-Means

  • 通过使用外部 Python 包 jenskpy,我们可以导出这些最佳断点,如下所示

>>> from jenkspy import JenksNaturalBreaks
>>> jnb = JenksNaturalBreaks(5)
>>> jnb.fit(df_us_unemp['rate'])
>>> jnb.inner_breaks_
[0.061, 0.088, 0.116, 0.161]

并在我们的实用函数中应用

classify(type='threshold', domain=[0.061, 0.088, 0.116, 0.161],
        title=['threshold Jenks','natural breaks'])

注意事项

  • 对于 quantizequantile 尺度类型,我们观察到默认类别数为 5。您可以使用 SchemeParams() 对象更改类别数。在上面的规范中,我们可以将 scheme='turbo' 更改为 scheme=alt.SchemeParams('turbo', count=2),以手动指定在尺度内使用 2 个类别方案。

  • 自然断点法将根据所需的类别数确定最佳类别断点,但您应该选择多少个类别?可以使用方差拟合优度 (GVF),也称为 Jenks 优化方法来确定这一点。

重复地图#

通过 Chart.repeat() 方法访问的 RepeatChart 模式为多维数据集的特定类型的水平或垂直连接提供了方便的接口。

在以下示例中,我们有一个引用为 source 的数据集,我们使用其中的三列定义每个美国州的 populationengineershurricanes

states 的定义使用了 topo_feature(),使用 urlfeature 作为参数。这是一个从 topojson url 提取要素的便利函数。

我们将这些变量作为列表提供给 .repeat() 操作符,在颜色编码中,我们将其引用为 alt.repeat('row')

import altair as alt
from vega_datasets import data

states = alt.topo_feature(data.us_10m.url, 'states')
source = data.population_engineers_hurricanes.url
variable_list = ['population', 'engineers', 'hurricanes']

alt.Chart(states).mark_geoshape(tooltip=True).encode(
    alt.Color(alt.repeat('row'), type='quantitative')
).transform_lookup(
    lookup='id',
    from_=alt.LookupData(source, 'id', variable_list)
).project(
    type='albersUsa'
).repeat(
    row=variable_list
).resolve_scale(
    color='independent'
)

分面地图#

通过 Chart.facet() 方法访问的 FacetChart 模式为数据集的特定类型的水平或垂直连接提供了方便的接口,其中一个字段包含多个 variables

不幸的是,在 vega/altair#2369 解决之前,常规分面不适用于地理可视化

source = data.population_engineers_hurricanes().melt(id_vars=['state', 'id'])
us_states = gpd.read_file(data.us_10m.url, driver='TopoJSON', layer='states')
gdf_comb = gpd.GeoDataFrame(source.join(us_states, on='id', rsuffix='_y'))

alt.Chart(gdf_comb).mark_geoshape().encode(
    color=alt.Color('value:Q'),
    facet=alt.Facet('variable:N').columns(3)
).properties(
    width=180,
    height=130
).resolve_scale('independent')

目前,有两种可能的解决方法。您可以通过转换查找(而不是通过 Chart)传递地理数据,如美国各州收入阶层:带换行的分面图库示例所示。或者,您可以在 pandas 中手动过滤数据,并通过连接创建小倍数图表,如下例所示

alt.concat(
    *(
        alt.Chart(gdf_comb[gdf_comb.variable == var], title=var)
        .mark_geoshape()
        .encode(
            color=alt.Color(
                "value:Q", legend=alt.Legend(orient="bottom", direction="horizontal")
            )
        )
        .project('albersUsa')
        .properties(width=180, height=130)
        for var in gdf_comb.variable.unique()
    ),
    columns=3
).resolve_scale(color="independent")

交互#

地图通常不是单独出现的,而是与其他图表结合使用。这里我们提供一个交互式可视化示例,包含一个条形图和一个地图。

数据显示了美国各州以及一个显示人口最多的 15 个州的条形图。使用 alt.selection_point(),我们定义了一个连接到鼠标左键点击的选择参数。

import altair as alt
from vega_datasets import data
import geopandas as gpd

# load the data
us_states = gpd.read_file(data.us_10m.url, driver="TopoJSON", layer="states")
us_population = data.population_engineers_hurricanes()[["state", "id", "population"]]

# define a pointer selection
click_state = alt.selection_point(fields=["state"])
# define a condition on the opacity encoding depending on the selection
opacity = alt.when(click_state).then(alt.value(1)).otherwise(alt.value(0.2))

# create a choropleth map using a lookup transform
choropleth = (
    alt.Chart(us_states)
    .mark_geoshape()
    .transform_lookup(
        lookup="id", from_=alt.LookupData(us_population, "id", ["population", "state"])
    )
    .encode(
        color="population:Q",
        opacity=opacity,
        tooltip=["state:N", "population:Q"],
    )
    .project(type="albersUsa")
)

# create a bar chart with the same conditional ``opacity`` encoding.
bars = (
    alt.Chart(
        us_population.nlargest(15, "population"), title="Top 15 states by population"
    )
    .mark_bar()
    .encode(
        x="population",
        opacity=opacity,
        color="population",
        y=alt.Y("state").sort("-x"),
    )
)

(choropleth & bars).add_params(click_state)

交互是双向的。如果您点击(按住 Shift 进行多选)几何图形或条形,则选中的元素将获得 opacity1,其余的将获得 opacity0.2。也可以创建带有区间选择的图表,如地图上的区间选择图库示例所示。

表达式#

Altair 表达式可用于地理可视化。以下示例使用 orthographic 投影可视化地球上的地震。在此,我们可以沿单轴旋转地球 (rotate0`)。采用实用函数 sphere() 作为地球背景盘。包含地震数据的 GeoDataFrame 具有 XYZ` 点几何图形,其中每个坐标分别表示 lonlatdepth。我们在此使用一种巧妙的方式直接访问几何图形列中的嵌套点坐标来绘制圆。使用此方法,我们无需先将它们分配到三个单独的列中。

import altair as alt
from vega_datasets import data
import geopandas as gpd

# load data
gdf_quakies = gpd.read_file(data.earthquakes.url, driver="GeoJSON")
gdf_world = gpd.read_file(data.world_110m.url, driver="TopoJSON")

# define parameters
range0 = alt.binding_range(min=-180, max=180, step=5, name='rotate longitude ')
rotate0 = alt.param(value=120, bind=range0)
hover = alt.selection_point(on="pointerover", clear="pointerout")

# world disk
sphere = alt.Chart(alt.sphere()).mark_geoshape(
    fill="aliceblue", stroke="black", strokeWidth=1.5
)

# countries as shapes
world = alt.Chart(gdf_world).mark_geoshape(
    fill="mintcream", stroke="black", strokeWidth=0.35
)

# earthquakes as circles with fill for depth and size for magnitude
# the hover param is added on the mar_circle only
quakes = (
    alt.Chart(gdf_quakies)
    .mark_circle(opacity=0.35, tooltip=True, stroke="black")
    .transform_calculate(
        lon="datum.geometry.coordinates[0]",
        lat="datum.geometry.coordinates[1]",
        depth="datum.geometry.coordinates[2]",
    )
    .transform_filter(
        ((rotate0 * -1 - 90 < alt.datum.lon) & (alt.datum.lon < rotate0 * -1 + 90)).expr
    )
    .encode(
        longitude="lon:Q",
        latitude="lat:Q",
        strokeWidth=alt.when(hover, empty=False).then(alt.value(1)).otherwise(alt.value(0)),
        size=alt.Size(
            "mag:Q",
            scale=alt.Scale(type="pow", range=[1, 1000], domain=[0, 6], exponent=4),
        ),
        fill=alt.Fill(
            "depth:Q", scale=alt.Scale(scheme="lightorange", domain=[0, 400])
        ),
    )
    .add_params(hover, rotate0)
)

# define projection and add the rotation param for all layers
comb = alt.layer(sphere, world, quakes).project(
    type="orthographic",
    rotate=alt.expr(f"[{rotate0.name}, 0, 0]")
)
comb

地震使用 mark_geoshape 显示,并在视野之外的可见部分进行过滤。添加了悬停高亮效果,以便更好地了解每次地震。

基于瓦片的地图#

要使用基于瓦片的地图(例如 OpenStreetMap)作为 mark_geoshape 的背景,您可以将 Altair Tiles 包与 Altair 一起使用。