指定数据#

Altair 使用的基本数据模型是表格数据,类似于电子表格或数据库表。单个数据集假定包含记录(行)的集合,其中可能包含任意数量的命名数据字段(列)。每个顶层图表对象(即 ChartLayerChartVConcatChartHConcatChartRepeatChartFacetChart)都将数据集作为其第一个参数。

有许多不同的方法来指定数据集:

当数据指定为 pandas DataFrame 时,Altair 使用 pandas 提供的数据类型信息自动确定编码所需的数据类型。例如,此处我们通过 pandas DataFrame 指定数据,Altair 自动检测到 x 列应在分类(名义)比例上可视化,y 列应在定量比例上可视化。

import altair as alt
import pandas as pd

data = pd.DataFrame({'x': ['A', 'B', 'C', 'D', 'E'],
                     'y': [5, 3, 6, 7, 2]})
alt.Chart(data).mark_bar().encode(
    x='x',
    y='y',
)

相比之下,所有其他指定数据的方式(包括非 pandas DataFrame)都需要显式声明编码类型。此处我们使用 Data 对象创建与上述相同的图表,数据指定为 JSON 风格的记录列表。

import altair as alt

data = alt.Data(values=[{'x': 'A', 'y': 5},
                        {'x': 'B', 'y': 3},
                        {'x': 'C', 'y': 6},
                        {'x': 'D', 'y': 7},
                        {'x': 'E', 'y': 2}])
alt.Chart(data).mark_bar().encode(
    x='x:N',  # specify nominal data
    y='y:Q',  # specify quantitative data
)

注意编码中所需的额外标记;因为 Altair 无法推断 Data 对象中的类型,我们必须手动指定它们(此处我们使用编码缩写来指定 x名义N)和 y定量Q);参见编码数据类型)。

类似地,通过 URL 引用数据时,我们也必须指定数据类型。

import altair as alt
from vega_datasets import data
url = data.cars.url

alt.Chart(url).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q'
)

编码及其相关类型将在编码中进一步讨论。下面我们将详细介绍在 Altair 图表中指定数据的不同方式。

pandas DataFrame#

包含索引数据#

按照设计,Altair 只访问 dataframe 列,而不访问 dataframe 索引。有时,相关数据出现在索引中。例如:

import numpy as np
rand = np.random.RandomState(0)

data = pd.DataFrame({'value': rand.randn(100).cumsum()},
                    index=pd.date_range('2018', freq='D', periods=100))
data.head()
                   value
    2018-01-01  1.764052
    2018-01-02  2.164210
    2018-01-03  3.142948
    2018-01-04  5.383841
    2018-01-05  7.251399

如果您希望索引对图表可用,可以使用 pandas dataframes 的 reset_index() 方法将其显式转换为列。

alt.Chart(data.reset_index()).mark_line().encode(
    x='index:T',
    y='value:Q'
)

如果索引对象未设置 name 属性,则生成的列将被称为 "index"。更多信息可在 pandas 文档中找到。

长格式与宽格式数据#

在 dataframe 中存储数据有两种常见约定,有时称为长格式宽格式。这两种都是在表格格式中存储数据的合理模式;简而言之,区别在于:

  • 宽格式数据每个独立变量一行,元数据记录在行和列标签中。

  • 长格式数据每个观测值一行,元数据作为记录在表中。

Altair 的语法最适用于长格式数据,其中每行对应一个观测值及其元数据。

一个具体的例子将有助于使这一区别更清晰。考虑一个数据集,其中包含几家公司随时间的股价。宽格式的数据可能如下排列:

wide_form = pd.DataFrame({'Date': ['2007-10-01', '2007-11-01', '2007-12-01'],
                          'AAPL': [189.95, 182.22, 198.08],
                          'AMZN': [89.15, 90.56, 92.64],
                          'GOOG': [707.00, 693.00, 691.48]})
print(wide_form)
             Date    AAPL   AMZN    GOOG
    0  2007-10-01  189.95  89.15  707.00
    1  2007-11-01  182.22  90.56  693.00
    2  2007-12-01  198.08  92.64  691.48

注意,每行对应一个时间戳(此处时间是独立变量),而每个观测值的元数据(即公司名称)存储在列标签中。

相同数据的长格式版本可能看起来像这样:

long_form = pd.DataFrame({'Date': ['2007-10-01', '2007-11-01', '2007-12-01',
                                   '2007-10-01', '2007-11-01', '2007-12-01',
                                   '2007-10-01', '2007-11-01', '2007-12-01'],
                          'company': ['AAPL', 'AAPL', 'AAPL',
                                      'AMZN', 'AMZN', 'AMZN',
                                      'GOOG', 'GOOG', 'GOOG'],
                          'price': [189.95, 182.22, 198.08,
                                     89.15,  90.56,  92.64,
                                    707.00, 693.00, 691.48]})
print(long_form)
             Date company   price
    0  2007-10-01    AAPL  189.95
    1  2007-11-01    AAPL  182.22
    2  2007-12-01    AAPL  198.08
    3  2007-10-01    AMZN   89.15
    4  2007-11-01    AMZN   90.56
    5  2007-12-01    AMZN   92.64
    6  2007-10-01    GOOG  707.00
    7  2007-11-01    GOOG  693.00
    8  2007-12-01    GOOG  691.48

注意,此处每行包含一个观测值(即价格)及其元数据(日期和公司名称)。重要的是,列和索引标签不再包含任何有用的元数据。

如上所述,Altair 最适用于这种长格式数据,因为相关数据和元数据存储在表本身中,而不是存储在行和列的标签中。

alt.Chart(long_form).mark_line().encode(
  x='Date:T',
  y='price:Q',
  color='company:N'
)

宽格式数据也可以使用例如分层进行可视化(参见分层图表),但在 Altair 的语法中远不如长格式方便。

如果您想将数据从宽格式转换为长格式,有两种可能的方法:可以将其作为使用 pandas 的预处理步骤完成,也可以作为图表本身的转换步骤完成。下面我们将详细介绍这两种方法。

使用 pandas 转换#

这种数据操作可以使用 pandas 作为预处理步骤完成,并在 pandas 文档的重塑和透视表部分详细讨论。

对于将宽格式数据转换为 Altair 使用的长格式数据,可以使用 dataframes 的 melt 方法。melt 的第一个参数是将要视为索引变量的列或列列表;其余列将组合成一个指示变量和一个值变量,其名称可以可选地指定。

wide_form.melt('Date', var_name='company', value_name='price')
             Date company   price
    0  2007-10-01    AAPL  189.95
    1  2007-11-01    AAPL  182.22
    2  2007-12-01    AAPL  198.08
    3  2007-10-01    AMZN   89.15
    4  2007-11-01    AMZN   90.56
    5  2007-12-01    AMZN   92.64
    6  2007-10-01    GOOG  707.00
    7  2007-11-01    GOOG  693.00
    8  2007-12-01    GOOG  691.48

有关 melt 方法的更多信息,请参阅pandas melt 文档

如果您想撤销此操作并将长格式转换回宽格式,dataframes 的 pivot 方法很有用。

long_form.pivot(index='Date', columns='company', values='price').reset_index()
    company        Date    AAPL   AMZN    GOOG
    0        2007-10-01  189.95  89.15  707.00
    1        2007-11-01  182.22  90.56  693.00
    2        2007-12-01  198.08  92.64  691.48

有关 pivot 方法的更多信息,请参阅pandas pivot 文档

使用折叠转换转换#

如果您想避免数据预处理,可以使用 Altair 的折叠转换(参见折叠以获取完整讨论)重塑您的数据。通过它,可以按如下方式重现上述图表。

alt.Chart(wide_form).transform_fold(
    ['AAPL', 'AMZN', 'GOOG'],
    as_=['company', 'price']
).mark_line().encode(
    x='Date:T',
    y='price:Q',
    color='company:N'
)

请注意,与 pandas melt 函数不同,我们必须显式指定要折叠的列。as_ 参数是可选的,默认为 ["key", "value"]

生成的数据#

有时,不使用外部数据源而是在图表规范本身中生成用于显示的数据会很方便。这样做的好处是,对于生成的数据,图表规范可以比嵌入数据小得多。

序列生成器#

这是一个使用 sequence() 函数生成 x 数据序列,并使用 计算 来计算 y 数据的示例。

import altair as alt

# Note that the following generator is functionally similar to
# data = pd.DataFrame({'x': np.arange(0, 10, 0.1)})
data = alt.sequence(0, 10, 0.1, as_='x')

alt.Chart(data).transform_calculate(
    y='sin(datum.x)'
).mark_line().encode(
    x='x:Q',
    y='y:Q',
)

经纬网生成器#

另一种方便在图表本身中生成的数据类型是地理可视化中的经纬度线,称为经纬网。可以使用 Altair 的 graticule() 生成器函数创建它们。下面是一个简单的示例:

import altair as alt

data = alt.graticule(step=[15, 15])

alt.Chart(data).mark_geoshape(stroke='black').project(
    'orthographic',
    rotate=[0, -45, 0]
)

球体生成器#

最后,在可视化地球时,可以将球体用作地图中的背景层,以表示地球的范围。此球体数据可以使用 Altair 的 sphere() 生成器函数创建。下面是一个示例:

import altair as alt

sphere_data = alt.sphere()
grat_data = alt.graticule(step=[15, 15])

background = alt.Chart(sphere_data).mark_geoshape(fill='aliceblue')
lines = alt.Chart(grat_data).mark_geoshape(stroke='lightgrey')

alt.layer(background, lines).project('naturalEarth1')

空间数据#

在本节中,我们将解释将空间数据读取到 Altair 的不同方法。要了解如何在使用此数据后使用它,请参阅 地理形状 标记页面。

GeoPandas GeoDataFrame#

使用 GeoPandas 作为空间数据源很方便。GeoPandas 可以读取多种类型的空间数据,并且 Altair 与 GeoDataFrames 配合良好。此处我们将四个多边形几何体定义到一个 GeoDataFrame 中,并使用 mark_geoshape 将其可视化。

from shapely import geometry
import geopandas as gpd
import altair as alt

data_geoms = [
    {"color": "#F3C14F", "geometry": geometry.Polygon([[1.45, 3.75], [1.45, 0], [0, 0], [1.45, 3.75]])},
    {"color": "#4098D7", "geometry": geometry.Polygon([[1.45, 0], [1.45, 3.75], [2.57, 3.75], [2.57, 0], [2.33, 0], [1.45, 0]])},
    {"color": "#66B4E2", "geometry": geometry.Polygon([[2.33, 0], [2.33, 2.5], [3.47, 2.5], [3.47, 0], [3.2, 0], [2.57, 0], [2.33, 0]])},
    {"color": "#A9CDE0", "geometry": geometry.Polygon([[3.2, 0], [3.2, 1.25], [4.32, 1.25], [4.32, 0], [3.47, 0], [3.2, 0]])},
]

gdf_geoms = gpd.GeoDataFrame(data_geoms)
gdf_geoms
         color                                           geometry
    0  #F3C14F      POLYGON ((1.45 3.75, 1.45 0, 0 0, 1.45 3.75))
    1  #4098D7  POLYGON ((1.45 0, 1.45 3.75, 2.57 3.75, 2.57 0...
    2  #66B4E2  POLYGON ((2.33 0, 2.33 2.5, 3.47 2.5, 3.47 0, ...
    3  #A9CDE0  POLYGON ((3.2 0, 3.2 1.25, 4.32 1.25, 4.32 0, ...

由于我们示例中的空间数据不是地理数据,我们使用 project 配置 type="identity", reflectY=True 来绘制几何体而不应用地理投影。通过使用 alt.Color(...).scale(None),我们禁用了 Altair 中的自动颜色分配,而是直接使用提供的 Hex 颜色代码。

alt.Chart(gdf_geoms, title="Vega-Altair").mark_geoshape().encode(
    alt.Color("color:N").scale(None)
).project(type="identity", reflectY=True)

内联 GeoJSON 对象#

如果您的源数据是 GeoJSON 文件,并且您不想将其加载到 GeoPandas GeoDataFrame 中,您可以将其作为字典提供给 Altair Data 类。GeoJSON 文件通常由一个 FeatureCollection 组成,其中包含一个 features 列表,每个几何体的信息在 properties 字典中指定。在下面的示例中,使用包含嵌套列表(此处命名为 features)的 keyproperty 值将 GeoJSON 样式的 数据对象指定到 Data 类中。

obj_geojson = {
    "type": "FeatureCollection",
    "features":[
        {"type": "Feature", "properties": {"location": "left"}, "geometry": {"type": "Polygon", "coordinates": [[[1.45, 3.75], [1.45, 0], [0, 0], [1.45, 3.75]]]}},
        {"type": "Feature", "properties": {"location": "middle-left"}, "geometry": {"type": "Polygon", "coordinates": [[[1.45, 0], [1.45, 3.75], [2.57, 3.75], [2.57, 0], [2.33, 0], [1.45, 0]]]}},
        {"type": "Feature", "properties": {"location": "middle-right"}, "geometry": {"type": "Polygon", "coordinates": [[[2.33, 0], [2.33, 2.5], [3.47, 2.5], [3.47, 0], [3.2, 0], [2.57, 0], [2.33, 0]]]}},
        {"type": "Feature", "properties": {"location": "right"}, "geometry": {"type": "Polygon", "coordinates": [[[3.2, 0], [3.2, 1.25], [4.32, 1.25], [4.32, 0], [3.47, 0], [3.2, 0]]]}}
    ]
}
data_obj_geojson = alt.Data(values=obj_geojson, format=alt.DataFormat(property="features"))
data_obj_geojson
    Data({
      format: DataFormat({
        property: 'features'
      }),
      values: {'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'properties': {'location': 'left'}, 'geometry': {'type': 'Polygon', 'coordinates': [[[1.45, 3.75], [1.45, 0], [0, 0], [1.45, 3.75]]]}}, {'type': 'Feature', 'properties': {'location': 'middle-left'}, 'geometry': {'type': 'Polygon', 'coordinates': [[[1.45, 0], [1.45, 3.75], [2.57, 3.75], [2.57, 0], [2.33, 0], [1.45, 0]]]}}, {'type': 'Feature', 'properties': {'location': 'middle-right'}, 'geometry': {'type': 'Polygon', 'coordinates': [[[2.33, 0], [2.33, 2.5], [3.47, 2.5], [3.47, 0], [3.2, 0], [2.57, 0], [2.33, 0]]]}}, {'type': 'Feature', 'properties': {'location': 'right'}, 'geometry': {'type': 'Polygon', 'coordinates': [[[3.2, 0], [3.2, 1.25], [4.32, 1.25], [4.32, 0], [3.47, 0], [3.2, 0]]]}}]}
    })

每个对象位置的标签存储在 properties 字典中。要访问这些值,您可以在颜色通道编码中指定一个嵌套变量名(此处为 properties.location)。此处我们将颜色编码更改为基于此位置标签,并应用 magma 颜色方案而不是默认方案。:O 后缀表示我们希望 Altair 将这些值视为有序数据,您可以在 编码数据类型 页面中阅读更多关于有序结构化数据的信息。

alt.Chart(data_obj_geojson, title="Vega-Altair - ordinal scale").mark_geoshape().encode(
    alt.Color("properties.location:O").scale(scheme='magma')
).project(type="identity", reflectY=True)

通过 URL 获取 GeoJSON 文件#

Altair 可以直接从 Web URL 加载 GeoJSON 资源。此处我们使用 geojson.xyz 的一个示例。如内联 GeoJSON 对象中所述,我们在 alt.DataFormat() 对象中将 features 指定为 property 参数的值,并将在嵌套字典(其中存储每个几何体信息,此处为 properties)的名称前加上我们要绘制的属性(continent)。

url_geojson = "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson"
data_url_geojson = alt.Data(url=url_geojson, format=alt.DataFormat(property="features"))
data_url_geojson
    Data({
      format: DataFormat({
        property: 'features'
      }),
      url: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson'
    })
alt.Chart(data_url_geojson).mark_geoshape().encode(color='properties.continent:N')

内联 TopoJSON 对象#

TopoJSON 是 GeoJSON 的扩展,其中要素的几何体通过一个名为 arcs 的顶层对象引用。每个共享的弧仅存储一次以减小数据大小。TopoJSON 文件对象可以包含多个对象(例如,边界和省界)。在为 Altair 定义 TopoJSON 对象时,我们指定 topojson 数据格式类型,并使用 feature 参数指定我们想要可视化的对象名称。此处此对象键的名称为 MY_DATA,但这在每个数据集中都不同。

obj_topojson = {
    "arcs": [
        [[1.0, 1.0], [0.0, 1.0], [0.0, 0.0], [1.0, 0.0]],
        [[1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]],
        [[1.0, 1.0], [1.0, 0.0]],
    ],
    "objects": {
        "MY_DATA": {
            "geometries": [
                {"arcs": [[-3, 0]], "properties": {"name": "abc"}, "type": "Polygon"},
                {"arcs": [[1, 2]], "properties": {"name": "def"}, "type": "Polygon"},
            ],
            "type": "GeometryCollection",
        }
    },
    "type": "Topology",
}
data_obj_topojson = alt.Data(
    values=obj_topojson, format=alt.DataFormat(feature="MY_DATA", type="topojson")
)
data_obj_topojson
    Data({
      format: DataFormat({
        feature: 'MY_DATA',
        type: 'topojson'
      }),
      values: {'arcs': [[[1.0, 1.0], [0.0, 1.0], [0.0, 0.0], [1.0, 0.0]], [[1.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0]], [[1.0, 1.0], [1.0, 0.0]]], 'objects': {'MY_DATA': {'geometries': [{'arcs': [[-3, 0]], 'properties': {'name': 'abc'}, 'type': 'Polygon'}, {'arcs': [[1, 2]], 'properties': {'name': 'def'}, 'type': 'Polygon'}], 'type': 'GeometryCollection'}}, 'type': 'Topology'}
    })
alt.Chart(data_obj_topojson).mark_geoshape(
).encode(
    color="properties.name:N"
).project(
    type='identity', reflectY=True
)

通过 URL 获取 TopoJSON 文件#

Altair 可以直接从 Web URL 加载 TopoJSON 资源。如内联 TopoJSON 对象中所述,我们必须使用 feature 参数指定对象名称(此处为 boroughs),并在 alt.DataFormat() 对象中将数据类型定义为 topjoson

from vega_datasets import data

url_topojson = data.londonBoroughs.url

data_url_topojson = alt.Data(
    url=url_topojson, format=alt.DataFormat(feature="boroughs", type="topojson")
)

data_url_topojson
    Data({
      format: DataFormat({
        feature: 'boroughs',
        type: 'topojson'
      }),
      url: 'https://cdn.jsdelivr.net.cn/npm/vega-datasets@v1.29.0/data/londonBoroughs.json'
    })

注意:如果该 TopoJSON 文件可以通过 URL 访问,还有一种提取其中对象的简写方式:alt.topo_feature(url=url_topojson, feature="boroughs")

我们根据行政区名称(存储为唯一标识符 id)对这些行政区进行颜色编码。我们在两列中使用 symbolLimit 为 33 来显示图例中的所有条目,并更改颜色方案以获得更鲜明的颜色。我们还添加了一个工具提示,当鼠标悬停在其上方时,会显示行政区的名称。

alt.Chart(data_url_topojson, title="London-Boroughs").mark_geoshape(
    tooltip=True
).encode(
    alt.Color("id:N").scale(scheme='tableau20').legend(columns=2, symbolLimit=33)
)

类似于 feature 选项,也存在 mesh 参数。此参数提取一组命名的 TopoJSON 对象。与 feature 选项不同,对应的地理数据作为单个统一的网格实例返回,而不是作为单独的 GeoJSON 要素。提取网格对于更有效地绘制边界或其他不需要与特定区域(如单个国家、州或县)关联的地理元素非常有用。

下面我们将绘制伦敦的同一行政区,但现在仅作为网格。

注意:必须显式定义 filled=False 才能绘制没有填充颜色的多(线)。

from vega_datasets import data

url_topojson = data.londonBoroughs.url

data_url_topojson_mesh = alt.Data(
    url=url_topojson, format=alt.DataFormat(mesh="boroughs", type="topojson")
)

alt.Chart(data_url_topojson_mesh, title="Border London-Boroughs").mark_geoshape(
    filled=False
)

嵌套 GeoJSON 对象#

GeoJSON 数据也可以嵌套在另一个数据集中。在这种情况下,可以使用 shape 编码通道结合 :G 后缀将嵌套要素可视化为 GeoJSON 对象。在以下示例中,GeoJSON 对象嵌套在字典列表中的 geo 中。

nested_features = [
    {"color": "#F3C14F", "geo": {"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[1.45, 3.75], [1.45, 0], [0, 0], [1.45, 3.75]]]}}},
    {"color": "#4098D7", "geo": {"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[1.45, 0], [1.45, 3.75], [2.57, 3.75], [2.57, 0], [2.33, 0], [1.45, 0]]]}}},
    {"color": "#66B4E2", "geo": {"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[2.33, 0], [2.33, 2.5], [3.47, 2.5], [3.47, 0], [3.2, 0], [2.57, 0], [2.33, 0]]]}}},
    {"color": "#A9CDE0", "geo": {"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[3.2, 0], [3.2, 1.25], [4.32, 1.25], [4.32, 0], [3.47, 0], [3.2, 0]]]}}},
]
data_nested_features = alt.Data(values=nested_features)

alt.Chart(data_nested_features, title="Vega-Altair").mark_geoshape().encode(
    shape="geo:G",
    color=alt.Color("color:N").scale(None)
).project(type="identity", reflectY=True)

投影#

对于地理数据,最好使用 World Geodetic System 1984 作为其地理坐标参考系,单位为十进制度。尽量避免将投影数据放入 Altair,而是先将空间数据重新投影到 EPSG:4326。如果您的数据采用不同的投影(例如,单位为米)并且您无法重新投影数据,请尝试使用项目配置 (type: 'identity', reflectY': True)。它将在不应用投影的情况下绘制几何体。

绕行顺序#

LineString、Polygon 和 MultiPolygon 几何体包含按一定顺序排列的坐标:线沿特定方向,多边形环也沿特定方向。GeoJSON 样式的 __geo_interface__ 结构建议对 Polygon 和 MultiPolygons 使用右手规则绕行顺序。这意味着外部环应为逆时针方向,内部环为顺时针方向。虽然它建议使用右手规则绕行顺序,但它不拒绝不使用右手规则的几何体。

Altair 不遵循几何体的右手规则,而是使用左手规则。这意味着外部环应为顺时针方向,内部环应为逆时针方向。如果您遇到绕行顺序问题,请尝试在使用 GeoPandas 等工具在 Altair 中使用数据之前强制执行左手规则,例如:

from shapely.ops import orient
gdf.geometry = gdf.geometry.apply(orient, args=(-1,))