Большие наборы данных
Если попытаться построить диаграмму, напрямую встраивающую набор из более чем 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://altair-viz.github.io/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 замедляют среду, например JupyterLab.
- На веб-сайте диаграмма замедляет загрузку страницы.
- Преобразования выполняются медленнее: расчёты идут в JavaScript, который не является самым быстрым языком для обработки больших объёмов данных.
Преобразователь данных VegaFusion
Самый простой и гибкий способ справиться с MaxRowsError — включить преобразователь "vegafusion", добавленный в Altair 5.1. VegaFusion — отдельный проект с эффективными реализациями большинства преобразований Altair на Rust. Выполняя разбиение на интервалы, агрегирование и другие операции в Python, он часто существенно уменьшает объём данных итоговой спецификации. Он также автоматически удаляет неиспользуемые столбцы, даже если у диаграммы нет преобразований.
Когда активен "vegafusion", преобразования предварительно вычисляются при отображении, сохранении, преобразовании диаграммы в словарь и в JSON. В сочетании с JupyterChart или средством отображения "jupyter" (см. настройку средств отображения) преобразования также динамически выполняются в Python в ответ на события выбора.
Разработка VegaFusion спонсируется Hex.
Установка VegaFusion
Зависимости можно установить через pip:
pip install vegafusion
или conda:
conda install -c conda-forge vegafusion vl-convert-python
Включение преобразователя VegaFusion
Активируйте его так:
import altair as alt
alt.data_transformers.enable("vegafusion")
После включения VegaFusion диаграммы работают с наборами до 100 000 строк. Ограничение применяется после выполнения всех поддерживаемых преобразований. Для гистограммы достичь его маловероятно, но оно может сработать для большой диаграммы рассеяния или интерактивной диаграммы без JupyterChart или средства "jupyter".
Для ещё больших наборов отключите ограничение числа строк или используйте JupyterChart либо "jupyter", как описано ниже.
Преобразование в JSON или словарь
При вызове chart.to_json или chart.to_dict для диаграммы VegaFusion параметр format должен быть равен "vega", а не стандартному "vega-lite":
chart.to_json(format="vega")
chart.to_dict(format="vega")
VegaFusion работает со спецификациями Vega, а не со спецификациями Vega-Lite, создаваемыми Altair. При включённом преобразователе преобразование Vega-Lite в Vega выполняет библиотека vl-convert.
Настройка локального часового пояса
Некоторые преобразования Altair, например единиц времени, зависят от локального часового пояса. Обычно используется пояс браузера. Но VegaFusion выполняет эти операции в Python до отрисовки, поэтому не всегда может получить пояс браузера. По умолчанию он использует локальный пояс ядра Python; в облачной среде тот может отличаться от пояса браузера.
Пояс VegaFusion можно задать функцией vegafusion.set_local_tz:
import vegafusion as vf
vf.set_local_tz("America/New_York")
При использовании JupyterChart или средства "jupyter" применяется часовой пояс браузера.
Интеграция с DuckDB
VegaFusion предоставляет необязательную интеграцию с DuckDB. Он выполняет запросы к DataFrame pandas без промежуточного преобразования в 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 Widgets. Это поддерживается многими интерфейсами: локально в классическом Notebook, JupyterLab и VS Code, удалённо в Colab и Binder.
Передача данных по URL
Распространённый подход — хранить большой набор отдельно и передавать диаграмме его URL, вместо того чтобы встраивать данные. Это уменьшает размер 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 файл данных должен перемещаться вместе с ним, иначе диаграмма может не отображаться.
Наборы данных Vega
Для наборов Vega можно передать URL через атрибут url:
from altair.datasets import data
source = data.cars.url
alt.Chart(source).mark_point() # etc.
Средства отображения PNG и SVG
У передачи по URL есть недостаток: данные больше не находятся внутри notebook, поэтому теряется переносимость, а при повторном открытии диаграммы могут быть недоступны. Кроме того, данные всё равно передаются интерфейсу, например браузеру, и расчёты выполняются там.
Можно ускорить работу, включив PNG или SVG, как описано в разделе средств отображения. Вместо спецификации Vega-Lite они заранее отрисовывают визуализацию и передают notebook только статическое изображение. Объём передаваемых данных может существенно сократиться, но вся интерактивность Altair теряется.
Оба средства требуют пакет vl-convert; см. сохранение изображений.
Предварительное агрегирование и фильтрация в pandas
Ещё один распространённый подход — агрегировать и фильтровать данные в pandas до передачи Altair.
Например, для столбчатой диаграммы набора barley, показывающей сумму yield по группам site, удобно передать Altair исходные данные:
import altair as alt
from altair.datasets import data
source = data.barley()
alt.Chart(source).mark_bar().encode(
x="sum(yield):Q",
y=alt.Y("site:N").sort("-x")
)
Для небольших наборов это работает хорошо. Но если представить гораздо больший barley, диаграмма может замедлить notebook. Чтобы уменьшить передаваемые данные, можно оставить только необходимые столбцы:
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 altair.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 altair.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 altair.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)
Наконец, построим ту же диаграмму размаха, передавая только вычисленные статистики вместо полного набора:
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