大型数据集#

如果您尝试创建一个将直接嵌入行数超过 5000 行的数据集的图表,您将看到一个 MaxRowsError 错误。

import altair as alt
import pandas as pd

data = pd.DataFrame({"x": range(10000)})
alt.Chart(data).mark_point()
MaxRowsError: The number of rows in your dataset is greater than the maximum allowed (5000).

Try enabling the VegaFusion data transformer which raises this limit by pre-evaluating data
transformations in Python.
    >> import altair as alt
    >> alt.data_transformers.enable("vegafusion")

Or, see https://vega-altair.cn/user_guide/large_datasets.html for additional information
on how to plot large datasets.

这并不是因为 Altair 无法处理大型数据集,而是因为用户仔细考虑如何处理大型数据集非常重要。以下各节描述了处理大型数据集的各种考虑因素和方法。

如果您确定要将完整的、未转换的数据集嵌入到可视化规范中,可以禁用 MaxRows 检查。

alt.data_transformers.disable_max_rows()

挑战#

按照设计,Altair 生成的图表不是由像素组成的,而是由数据加上可视化规范组成的。例如,这是一个使用包含三行数据的 dataframe 创建的简单图表:

import altair as alt
import pandas as pd
data = pd.DataFrame({'x': [1, 2, 3], 'y': [2, 1, 2]})

chart = alt.Chart(data).mark_line().encode(
     x='x',
     y='y'
)

from pprint import pprint
pprint(chart.to_dict())
{'$schema': 'https://vega.github.io/schema/vega-lite/v2.4.1.json',
 'config': {'view': {'height': 300, 'width': 300}},
 'data': {'values': [{'x': 1, 'y': 2}, {'x': 2, 'y': 1}, {'x': 3, 'y': 2}]},
 'encoding': {'x': {'field': 'x', 'type': 'quantitative'},
              'y': {'field': 'y', 'type': 'quantitative'}},
 'mark': 'line'}

生成的规范包含转换为 JSON 格式的数据表示,该规范嵌入在 notebook 或网页中,可由 Vega-Lite 用于渲染图表。随着数据大小的增长,这种显式数据存储可能导致非常大的规范,从而带来各种负面影响:

  • 大型 notebook 文件,可能拖慢您的 notebook 环境,例如 JupyterLab

  • 如果您在网站上显示图表,会降低页面加载速度

  • 转换计算评估缓慢,因为计算是在 JavaScript 中执行的,而 JavaScript 并非处理大量数据的最快语言

VegaFusion 数据转换器#

解决 MaxRowsError 最简单灵活的方法是启用 "vegafusion" 数据转换器,该转换器在 Altair 5.1 中添加。VegaFusion 是一个外部项目,提供了大多数 Altair 数据转换的高效 Rust 实现。通过在 Python 中评估数据转换(例如分箱和聚合),最终图表规范中必须包含的数据集大小通常会大大减小。此外,VegaFusion 会自动删除未使用的列,即使对于没有数据转换的图表也能减小数据集大小。

"vegafusion" 数据转换器激活时,数据转换将在显示 Altair 图表保存 Altair 图表、将图表转换为字典以及将图表转换为 JSON 时进行预评估。与 JupyterChart"jupyter" 渲染器结合使用时(参见自定义渲染器),数据转换也会在 Python 中动态评估,以响应图表选择事件。

VegaFusion 的开发由 Hex 赞助。

安装 VegaFusion#

可以使用 pip 安装 VegaFusion 依赖项

pip install "vegafusion[embed]"

或 conda

conda install -c conda-forge vegafusion vegafusion-python-embed vl-convert-python

启用 VegaFusion 数据转换器#

使用以下命令激活 VegaFusion 数据转换器:

import altair as alt
alt.data_transformers.enable("vegafusion")

激活 VegaFusion 数据转换器后创建的所有图表都可以处理包含多达 100,000 行的数据集。VegaFusion 的行数限制是在应用所有支持的数据转换之后应用的。因此,对于直方图等图表,您不太可能达到此限制,但在大型散点图或未使用 JupyterChart"jupyter" 渲染器时包含交互性的图表的情况下,您可能会达到此限制。

如果您需要处理更大的数据集,可以禁用最大行数限制或切换到使用下文描述的 JupyterChart"jupyter" 渲染器。

转换为 JSON 或字典#

将 VegaFusion 图表使用 chart.to_json 转换为 JSON 或使用 chart.to_dict 转换为 Python 字典时,format 参数必须设置为 "vega",而不是默认的 "vega-lite"。例如:

chart.to_json(format="vega")
chart.to_dict(format="vega")

这是因为 VegaFusion 处理的是 Vega 图表规范,而不是 Altair 生成的 Vega-Lite 规范。启用 VegaFusion 数据转换器时,使用 vl-convert 库执行从 Vega-Lite 到 Vega 的转换。

本地时区配置#

一些 Altair 转换(例如 时间单位)基于本地时区。通常使用浏览器的本地时区。然而,由于 VegaFusion 在渲染之前在 Python 中评估这些转换,因此并非总是能够访问浏览器的时区。默认情况下将使用 Python 内核的本地时区。在使用云端 notebook 服务的情况下,这可能与浏览器的本地时区不同。

可以使用 vegafusion.set_local_tz 函数自定义 VegaFusion 的本地时区。例如:

import vegafusion as vf
vf.set_local_tz("America/New_York")

使用 JupyterChart"jupyter" 渲染器时,使用的是浏览器的本地时区。

DuckDB 集成#

VegaFusion 提供了与 DuckDB 的可选集成。由于 DuckDB 可以在 pandas DataFrame 上执行查询而无需通过 Arrow 转换,因此通常比 VegaFusion 默认的需要此转换的查询引擎更快。有关更多信息,请参见 VegaFusion DuckDB 文档。

交互性#

当对使用选择进行交互式数据过滤的图表使用默认的 "html" 渲染器时,VegaFusion 数据转换器会将参与交互的所有数据包含在生成的图表规范中。这使得它不适合用于构建过滤大型数据集(例如,对包含一百多万行的数据集进行交叉过滤)的交互式图表。

JupyterChart 小部件和 "jupyter" 渲染器旨在与 VegaFusion 数据转换器配合使用,以响应选择事件动态评估数据转换。这避免了将整个数据集传输到浏览器的需要,从而支持对百万行量级聚合数据集的交互式探索。

可以直接使用 JupyterChart

import altair as alt
alt.data_transformers.enable("vegafusion")
...
alt.JupyterChart(chart)

或者,启用 "jupyter" 渲染器并像往常一样显示图表

import altair as alt
alt.data_transformers.enable("vegafusion")
alt.renderers.enable("jupyter")
...
chart

以这种方式渲染的图表需要运行中的 Python 内核和 Jupyter Widget 扩展才能显示,这在许多前端都有效,包括本地的经典 notebook、JupyterLab 和 VSCode,以及远程的 Colab 和 Binder。

通过 URL 传递数据#

处理大型数据集时,一种常见方法不是直接嵌入数据,而是将其单独存储并通过 URL 传递给图表。这不仅解决了大型 notebook 的问题,还能提高大型数据集的交互性能。

本地数据服务器#

一个方便的方法是使用 altair_data_server 包。它通过本地线程服务器提供数据。首先安装该包:

pip install altair_data_server

然后启用数据转换器:

import altair as alt
alt.data_transformers.enable('data_server')

请注意,此方法可能在某些基于云的 Jupyter notebook 服务上不起作用。这种方法的一个缺点是,如果重新打开 notebook,图表可能不再显示,因为数据服务器已停止运行。

本地文件系统#

您也可以将数据持久化到磁盘,然后将路径传递给 Altair:

url = 'data.json'
data.to_json(url, orient='records')

chart = alt.Chart(url).mark_line().encode(
    x='x:Q',
    y='y:Q'
)
pprint(chart.to_dict())
{'$schema': 'https://vega.github.io/schema/vega-lite/v2.4.1.json',
 'config': {'view': {'height': 300, 'width': 300}},
 'data': {'url': 'data.json'},
 'encoding': {'x': {'field': 'x', 'type': 'quantitative'},
              'y': {'field': 'y', 'type': 'quantitative'}},
 'mark': 'line'}

Altair 还有一个 JSON 数据转换器,启用后它会透明地完成此操作:

alt.data_transformers.enable('json')

还有一个类似的 CSV 数据转换器,但必须更谨慎使用,因为 CSV 不像 JSON 那样保留数据类型。

请注意,文件系统方法可能在某些基于云的 Jupyter notebook 服务上不起作用。这种方法的一个缺点是会失去可移植性:如果 notebook 被移动,数据文件必须随之移动,否则图表可能无法显示。

Vega 数据集#

如果您使用的是 Vega 数据集之一,可以使用 url 属性通过 URL 传递数据:

from vega_datasets import data
source = data.cars.url

alt.Chart(source).mark_point() # etc.

PNG 和 SVG 渲染器#

通过 URL 传递数据 中介绍的方法的缺点是数据不再包含在 notebook 中,因此会失去可移植性,或者在重新打开 notebook 时看不到图表。此外,数据仍然需要发送到前端(例如您的浏览器),并且所有计算都将在那里进行。

您可以通过启用 Altair 的渲染器框架 中描述的 PNG 或 SVG 渲染器来提高速度。它们不会生成 Vega-Lite 规范,而是会预渲染可视化并仅将静态图像发送到您的 notebook。这可以大大减少传输的数据量。这种方法的缺点是,您会失去 Altair 的所有交互性功能。

这两种渲染器都需要您安装 vl-convert 包,请参阅 PNG、SVG 和 PDF 格式

在 pandas 中预聚合和过滤#

另一种常见方法是使用 pandas 在将数据传递给 Altair 之前执行数据转换,例如聚合和过滤。

例如,要为 barley 数据集创建一个按 site 分组并对 yield 求和的条形图,将未聚合的数据直接传递给 Altair 非常方便:

import altair as alt
from vega_datasets import data

source = data.barley()

alt.Chart(source).mark_bar().encode(
    x="sum(yield):Q",
    y=alt.Y("site:N").sort("-x")
)

上述方法对于较小的数据集效果很好,但假设 barley 数据集更大,生成的 Altair 图表拖慢了您的 notebook 环境。为了减少传递给 Altair 的数据,您可以将 dataframe 子集化到仅包含必要的列:

alt.Chart(source[["yield", "site"]]).mark_bar().encode(
    x="sum(yield):Q",
    y=alt.Y("site:N").sort("-x")
)

您还可以在 pandas 中预先计算总和,这将进一步减小数据集的大小:

import altair as alt
from vega_datasets import data

source = data.barley()
source_aggregated = (
    source.groupby("site")["yield"].sum().rename("sum_yield").reset_index()
)

alt.Chart(source_aggregated).mark_bar().encode(
    x="sum_yield:Q",
    y=alt.Y("site:N").sort("-x")
)

预聚合箱线图#

箱线图是一种可视化数据分布的有用方法,在 Altair 中创建它也很简单。

import altair as alt
from vega_datasets import data

df = data.cars()

alt.Chart(df).mark_boxplot().encode(
    x="Miles_per_Gallon:Q",
    y="Origin:N",
    color=alt.Color("Origin").legend(None)
)

如果您有大量数据,可以在 pandas 中执行必要的计算,然后只将结果汇总统计量传递给 Altair。

首先,定义几个参数,其中 k 代表用于计算须线边界的乘数。

import altair as alt
import pandas as pd
from vega_datasets import data

k = 1.5
group_by_column = "Origin"
value_column = "Miles_per_Gallon"

下一步,我们将计算箱线图所需的汇总统计量。

df = data.cars()

agg_stats = df.groupby(group_by_column)[value_column].describe()
agg_stats["iqr"] = agg_stats["75%"] - agg_stats["25%"]
agg_stats["min_"] = agg_stats["25%"] - k * agg_stats["iqr"]
agg_stats["max_"] = agg_stats["75%"] + k * agg_stats["iqr"]
data_points = df[[value_column, group_by_column]].merge(
    agg_stats.reset_index()[[group_by_column, "min_", "max_"]]
)
# Lowest data point which is still above or equal to min_
# This will be the lower end of the whisker
agg_stats["lower"] = (
    data_points[data_points[value_column] >= data_points["min_"]]
    .groupby(group_by_column)[value_column]
    .min()
)
# Highest data point which is still below or equal to max_
# This will be the upper end of the whisker
agg_stats["upper"] = (
    data_points[data_points[value_column] <= data_points["max_"]]
    .groupby(group_by_column)[value_column]
    .max()
)
# Store all outliers as a list
agg_stats["outliers"] = (
    data_points[
        (data_points[value_column] < data_points["min_"])
        | (data_points[value_column] > data_points["max_"])
    ]
    .groupby(group_by_column)[value_column]
    .apply(list)
)
agg_stats = agg_stats.reset_index()

# Show whole dataframe
pd.set_option("display.max_columns", 15)
print(agg_stats)
       Origin  count       mean       std   min   25%   50%    75%   max   iqr  \
    0  Europe   70.0  27.891429  6.723930  16.2  24.0  26.5  30.65  44.3  6.65   
    1   Japan   79.0  30.450633  6.090048  18.0  25.7  31.6  34.05  46.6  8.35   
    2     USA  249.0  20.083534  6.402892   9.0  15.0  18.5  24.00  39.0  9.00   
    
         min_    max_  lower  upper                              outliers  
    0  14.025  40.625   16.2   37.3  [43.1, 41.5, 44.3, 43.4, 40.9, 44.0]  
    1  13.175  46.575   18.0   44.6                                [46.6]  
    2   1.500  37.500    9.0   36.1                    [39.0, 38.0, 38.0]  

最后,我们可以创建与上面相同的箱线图,但只将计算出的汇总统计量传递给 Altair,而不是整个数据集。

base = alt.Chart(agg_stats).encode(
    y="Origin:N"
)

rules = base.mark_rule().encode(
    x=alt.X("lower").title("Miles_per_Gallon"),
    x2="upper",
)

bars = base.mark_bar(size=14).encode(
    x="25%",
    x2="75%",
    color=alt.Color("Origin").legend(None),
)

ticks = base.mark_tick(color="white", size=14).encode(
    x="50%"
)

outliers = base.transform_flatten(
    flatten=["outliers"]
).mark_point(
    style="boxplot-outliers"
).encode(
    x="outliers:Q",
    color="Origin",
)

rules + bars + ticks + outliers