Данные

Основная модель данных Altair — таблица, похожая на электронную таблицу или таблицу базы данных. Набор состоит из записей — строк, каждая из которых может содержать любое число именованных полей — столбцов. Все объекты диаграмм верхнего уровня (Chart, LayerChart, VConcatChart, HConcatChart, RepeatChart, FacetChart) принимают набор данных первым аргументом.

Передать данные можно несколькими способами:

  • Как pandas DataFrame.
  • Как DataFrame с поддержкой DataFrame Interchange Protocol, то есть с атрибутом __dataframe__: например, из polars или pyarrow. Эта возможность экспериментальная.
  • Как объект Data или связанный с ним объект: UrlData, InlineData, NamedData.
  • Как строку URL текстового файла в формате json или csv.
  • Как GeoPandas GeoDataFrame, геометрию Shapely, объект GeoJSON или другой объект с поддержкой __geo_interface__.
  • Как генерируемый набор: например, числовую последовательность или элементы географической сетки.

При использовании pandas DataFrame Altair автоматически определяет типы данных каналов кодирования по сведениям pandas. В следующем примере столбец 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',
)

При других способах передачи данных, включая DataFrame не из pandas, типы кодирования нужно указывать явно. Создадим такую же диаграмму через 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',  # Номинальные данные
    y='y:Q',  # Количественные данные
)

Обратите внимание на дополнительные обозначения в кодировании. Altair не может вывести типы полей объекта Data, поэтому они заданы вручную: в краткой записи N означает номинальный тип для x, а Q — количественный для y. Подробнее см. типы данных кодирования.

При обращении к данным по URL типы также нужно указывать:

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

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

Каналы и их типы подробно рассмотрены в разделе «Кодирование». Далее разберём способы задания данных.

pandas DataFrame

Использование данных из индекса

Altair обращается только к столбцам 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()

Чтобы сделать индекс доступным диаграмме, явно превратите его в столбец методом pandas reset_index():

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

Если у объекта индекса не задан атрибут name, новый столбец будет называться "index". Дополнительные сведения есть в документации pandas по DataFrame.reset_index.

Длинный и широкий форматы данных

Есть два распространённых способа организации DataFrame — широкий (wide-form) и длинный (long-form) форматы. Оба подходят для табличного хранения. Разница следующая:

  • В широком формате каждой строке соответствует значение независимой переменной, а метаданные находятся в названиях строк и столбцов.
  • В длинном формате каждой строке соответствует наблюдение, а метаданные записаны как значения внутри таблицы.

Грамматика 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)

Каждая строка соответствует одному моменту времени — независимой переменной. Имя компании, то есть метаданные наблюдения, находится в названии столбца.

Те же данные в длинном формате:

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)

Теперь строка содержит одно наблюдение — цену — и его метаданные: дату и компанию. Названия столбцов и метки индекса больше не хранят метаданные конкретных наблюдений.

Такой формат удобен для Altair: все необходимые данные находятся внутри таблицы, а не в названиях строк и столбцов:

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

Широкий формат тоже можно визуализировать, например через наложение слоёв, но в грамматике Altair это гораздо менее удобно.

Перевести широкий формат в длинный можно двумя способами: заранее обработать данные в pandas или выполнить преобразование внутри диаграммы. Рассмотрим оба.

Преобразование с помощью pandas

Такую обработку можно выполнить до построения диаграммы средствами pandas. Подробно она описана в разделе документации pandas Reshaping and Pivot Tables.

Для получения длинного формата используется метод melt. Первый аргумент — столбец или список столбцов, которые нужно сохранить как идентифицирующие переменные. Остальные столбцы объединяются в столбец названий переменных и столбец их значений. Имена двух новых столбцов можно задать:

wide_form.melt('Date', var_name='company', value_name='price')

Дополнительные сведения есть в документации pandas по DataFrame.melt.

Для обратного перехода из длинного формата в широкий используется pivot:

long_form.pivot(index='Date', columns='company', values='price').reset_index()

Подробности см. в документации pandas по DataFrame.pivot.

Преобразование через Fold

Если не хотите предварительно обрабатывать данные, используйте преобразование Fold. Оно позволяет получить предыдущую диаграмму следующим образом:

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

В отличие от melt, здесь нужно явно перечислить объединяемые столбцы. Аргумент as_ необязательный; по умолчанию используются имена ["key", "value"].

Генерируемые данные

Иногда удобнее создавать данные прямо в спецификации диаграммы, чем загружать внешний набор. Спецификация с генерируемыми данными может быть значительно меньше, чем со встроенными записями.

Генератор последовательности

Функция sequence создаёт последовательность значений x, а преобразование Calculate вычисляет y:

import altair as alt

# Этот генератор по результату аналогичен следующему DataFrame:
# 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',
)

Генератор географической сетки

Ещё один удобный вариант генерируемых данных — линии широты и долготы, образующие географическую сетку, или graticule. Их создаёт функция graticule:

import altair as alt

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

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

Генератор сферы

При отображении земного шара сферу можно использовать как фоновый слой карты, обозначающий поверхность Земли. Такие данные создаёт функция 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. О дальнейшей работе с ними см. страницу примитивов Geoshape.

GeoPandas GeoDataFrame

GeoPandas удобно использовать как источник пространственных данных: он читает множество форматов, а Altair поддерживает 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

Пространственные данные примера не являются географическими. Поэтому project(type="identity", reflectY=True) рисует геометрию без географической проекции. Вызов alt.Color(...).scale(None) отключает автоматическое назначение цветов: используются заданные шестнадцатеричные коды.

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. В следующем примере в Data передаётся объект подобной структуры. Параметр property указывает ключ вложенного списка — features:

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

Название положения каждого объекта находится в properties. Обращайтесь к вложенному полю через properties.location в канале цвета. Здесь вместо стандартной схемы используется magma, а суффикс :O задаёт порядковый тип. Подробнее см. типы данных кодирования.

alt.Chart(data_obj_geojson, title="Vega-Altair — порядковая шкала").mark_geoshape().encode(
    alt.Color("properties.location:O").scale(scheme='magma')
).project(type="identity", reflectY=True)

Файл GeoJSON по URL

Altair может загружать GeoJSON непосредственно по веб-адресу. В примере используется набор geojson.xyz. Как и для встроенного объекта, параметр property объекта alt.DataFormat() равен features. Перед именем отображаемого поля continent указывается словарь properties, в котором хранятся сведения о геометрии:

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
alt.Chart(data_url_geojson).mark_geoshape().encode(color='properties.continent:N')

Встроенный объект TopoJSON

TopoJSON — расширение GeoJSON, в котором геометрия объектов описывается ссылками на дуги из поля верхнего уровня arcs. Каждая общая дуга хранится один раз, что уменьшает размер данных. Файл TopoJSON может содержать несколько объектов: например, границы страны и границы областей. В Altair нужно задать тип формата 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
alt.Chart(data_obj_topojson).mark_geoshape(
).encode(
    color="properties.name:N"
).project(
    type='identity', reflectY=True
)

Файл TopoJSON по URL

Altair может загружать TopoJSON по веб-адресу. Как и для встроенного объекта, параметр feature задаёт имя объекта — здесь boroughs, а в alt.DataFormat() указывается тип topojson:

from altair.datasets import data

url_topojson = data.london_boroughs.url

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

data_url_topojson

Есть и краткая запись для извлечения объектов из TopoJSON по URL: alt.topo_feature(url=url_topojson, feature="boroughs").

Закодируем районы цветом по их названиям, хранящимся в уникальном идентификаторе id. Значение symbolLimit=33 и два столбца легенды позволяют показать все элементы. Также изменим цветовую схему, чтобы цвета лучше различались, и добавим подсказку с названием района:

alt.Chart(data_url_topojson, title="Районы Лондона").mark_geoshape(
    tooltip=True
).encode(
    alt.Color("id:N").scale(scheme='tableau20').legend(columns=2, symbolLimit=33)
)

Кроме feature, существует параметр mesh. Он извлекает именованный набор объектов TopoJSON как единую сеть линий, а не отдельные объекты GeoJSON. Это удобно для эффективного рисования границ и других географических элементов, которые не нужно связывать с конкретными странами, штатами или округами.

Нарисуем те же районы Лондона только как сеть границ. Чтобы отобразить линии без заливки, явно укажите filled=False:

from altair.datasets import data

url_topojson = data.london_boroughs.url

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

alt.Chart(data_url_topojson_mesh, title="Границы районов Лондона").mark_geoshape(
    filled=False
)

Вложенные объекты GeoJSON

Объекты GeoJSON могут находиться внутри другого набора данных. Чтобы отображать их как геометрию, используйте канал shape с суффиксом :G. В примере объект 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 с координатами в десятичных градусах. По возможности сначала преобразуйте пространственные данные в EPSG:4326, а затем передавайте их в Altair.

Если данные имеют другую проекцию — например, с координатами в метрах — и преобразовать их нельзя, попробуйте project(type="identity", reflectY=True). Геометрия будет нарисована без применения географической проекции.

Порядок обхода вершин

Координаты LineString, Polygon и MultiPolygon перечислены в определённом направлении. Структура, подобная GeoJSON, используемая __geo_interface__, рекомендует правило правой руки: внешний контур полигона — против часовой стрелки, внутренние — по часовой. При этом геометрия с другим порядком обхода не отвергается.

Altair использует правило левой руки: внешние контуры должны идти по часовой стрелке, внутренние — против часовой. Если возникли проблемы с порядком вершин, попробуйте заранее привести данные к этому правилу, например средствами GeoPandas:

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