Данные
Основная модель данных 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,))