Кодирование

Чтобы визуализация понятно передавала информацию, нужно сопоставить свойства данных с визуальными свойствами. В Altair такая связь называется кодированием (encoding) и обычно задаётся методом Chart.encode.

Например, отобразим набор данных об автомобилях через четыре канала кодирования: x — значение по оси x, y — значение по оси y, color — цвет и shape — форму символа точки:

import altair as alt
from altair.datasets import data


cars = data.cars()

alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color='Origin',
    shape='Origin'
)

Параметры каналов

У каждого канала есть параметры, позволяющие дополнительно настраивать диаграмму. В Altair 5.0 появился синтаксис на основе методов — более удобная альтернатива традиционной передаче атрибутов. Оба способа остаются доступны.

Синтаксис на основе методов

Именованные аргументы заменяются вызовами методов. Например, традиционная настройка оси x — x=alt.X('Horsepower', axis=alt.Axis(tickMinStep=50)). Тот же объект X можно записать короче: x=alt.X('Horsepower').axis(tickMinStep=50).

Этот подход работает для всех каналов и их параметров. Аналогично зададим title канала y. Получится та же диаграмма, что и в предыдущем примере, с настроенными осями:

alt.Chart(cars).mark_point().encode(
    alt.X('Horsepower').axis(tickMinStep=50),
    alt.Y('Miles_per_Gallon').title('Миль на галлон'),
    color='Origin',
    shape='Origin'
)

Методы можно объединять в цепочку. Здесь методы axis, bin и scale задают соответствующие параметры канала x. Определение x разбито на несколько строк для удобства чтения; это допустимо благодаря охватывающим скобкам encode:

alt.Chart(cars).mark_point().encode(
    alt.X('Horsepower')
        .axis(ticks=False)
        .bin(maxbins=10)
        .scale(domain=(30,300), reverse=True),
    alt.Y('Miles_per_Gallon').title('Миль на галлон'),
    color='Origin',
    shape='Origin'
)

Синтаксис на основе атрибутов

Традиционный вариант первого примера настройки каналов:

alt.Chart(cars).mark_point().encode(
    alt.X('Horsepower', axis=alt.Axis(tickMinStep=50)),
    alt.Y('Miles_per_Gallon', title="Миль на галлон"),
    color='Origin',
    shape='Origin'
)

Если параметров много, такая запись становится более объёмной:

alt.Chart(cars).mark_point().encode(
    alt.X(
        'Horsepower',
        axis=alt.Axis(ticks=False),
        bin=alt.Bin(maxbins=10),
        scale=alt.Scale(domain=(30,300), reverse=True)
    ),
    alt.Y('Miles_per_Gallon', title='Миль на галлон'),
    color='Origin',
    shape='Origin'
)

Типы данных кодирования

Способ отображения зависит от типа данных. Altair поддерживает пять основных типов:

Тип Краткое обозначение Описание
quantitative Q Непрерывная числовая величина.
ordinal O Дискретная упорядоченная величина.
nominal N Дискретная категория без заданного порядка.
temporal T Дата или время.
geojson G Географическая геометрия.

Для DataFrame Altair может автоматически определить тип каждого канала, подобрать шкалы и легенды. Если типы явно не заданы, числовые данные обычно считаются quantitative, даты и время — temporal, строки — nominal. Эти значения по умолчанию не всегда подходят задаче.

Тип можно указать полной записью через классы каналов, например X и Y, или краткой записью. Следующие способы дают одинаковые диаграммы:

alt.Chart(cars).mark_point().encode(
    x='Acceleration:Q',
    y='Miles_per_Gallon:Q',
    color='Origin:N'
)
alt.Chart(cars).mark_point().encode(
    alt.X('Acceleration', type='quantitative'),
    alt.Y('Miles_per_Gallon', type='quantitative'),
    alt.Color('Origin', type='nominal')
)

Краткая форма x="name:Q" удобна для быстрого исследования данных. Полная форма alt.X('name', type='quantitative') позволяет подробнее настраивать параметры канала: группировку по интервалам, ось, шкалу и другие свойства.

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

Влияние типа на цветовую шкалу

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

base = alt.Chart(cars).mark_point().encode(
    x='Horsepower:Q',
    y='Miles_per_Gallon:Q',
).properties(
    width=140,
    height=140
)

alt.hconcat(
   base.encode(color='Cylinders:Q').properties(title='Количественный'),
   base.encode(color='Cylinders:O').properties(title='Порядковый'),
   base.encode(color='Cylinders:N').properties(title='Номинальный'),
)

Тип определяет, какую цветовую шкалу Altair через Vega-Lite использует для значений и будет ли легенда дискретной или непрерывной.

Влияние типа на шкалы осей

Для каналов x и y тип также влияет на шкалу и свойства примитивов. Рассмотрим столбец с целочисленными значениями года, показанный с порядковой, количественной и временной шкалами:

pop = data.population()

base = alt.Chart(pop).mark_bar().encode(
    alt.Y('mean(people):Q').title('Численность населения')
).properties(
    width=140,
    height=140
)

alt.hconcat(
    base.encode(x='year:O').properties(title='Порядковый'),
    base.encode(x='year:Q').properties(title='Количественный'),
    base.encode(x='year:T').properties(title='Временной')
)

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

Чтобы четырёхзначные целые числа отображались как годы без разделителя тысяч, сначала преобразуйте их в строки и задайте временной тип в Altair. Формат можно изменить и через .axis(format='i'), но предпочтительнее правильно указать тип данных:

pop['year'] = pop['year'].astype(str)

base.mark_bar().encode(x='year:T').properties(title='Временной')

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

Краткая запись кодирования

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

Краткая запись Эквивалентная полная запись
x='name' alt.X('name')
x='name:Q' alt.X('name', type='quantitative')
x='sum(name)' alt.X('name', aggregate='sum')
x='sum(name):Q' alt.X('name', aggregate='sum', type='quantitative')
x='count():Q' alt.X(aggregate='count', type='quantitative')

Экранирование специальных символов в названиях столбцов

Двоеточие : отделяет тип в краткой записи. Если оно входит в имя столбца, переименуйте столбец или экранируйте символ. То же относится к точке . и скобкам [], используемым для обращения к вложенным данным.

Рекомендуется переименовать столбцы. Например, в pandas можно заменить двоеточия подчёркиваниями: df.rename(columns=lambda x: x.replace(':', '_')). Если переименование не подходит, используйте необрабатываемую строку Python (raw string) и обратную косую черту:

import pandas as pd

source = pd.DataFrame({
    'col:colon': [1, 2, 3],
    'col.period': ['A', 'B', 'C'],
    'col[brackets]': range(3),
})

alt.Chart(source).mark_bar().encode(
    x=r'col\:colon',
    # Убираем обратную косую черту из заголовка
    y=alt.Y(r'col\.period').title('col.period'),
    # Указываем тип данных
    color=r'col\[brackets\]:N',
)

Тип можно не указывать, как и для обычных имён. По умолчанию заголовки осей содержат экранирующие обратные косые черты; чтобы убрать их, задайте строки заголовков вручную.

В полной форме двоеточие экранировать не нужно, если тип указан явно: alt.X(field='col:colon', type='quantitative'). Точки и скобки всё ещё требуют экранирования, если они не обозначают обращение к вложенным структурам.

Группировка по интервалам и агрегирование

Помимо простого сопоставления каналов, Altair использует группировку и агрегирование в стиле баз данных — подход split–apply–combine: разделить данные, применить операцию и объединить результаты.

Например, гистограмма одномерных данных строится так: записи разделяются по интервалам, в каждом интервале вычисляется число записей, затем результаты объединяются в диаграмму:

alt.Chart(cars).mark_bar().encode(
    alt.X('Horsepower').bin(),
    y='count()'
    # Можно также использовать alt.Y(aggregate='count', type='quantitative')
)

Здесь используется краткая запись с count — единственной операцией агрегирования, для которой не требуется имя поля.

Для двумерной гистограммы можно кодировать число записей размером точек сетки. Такой вариант иногда называют пузырьковой диаграммой:

alt.Chart(cars).mark_point().encode(
    alt.X('Horsepower').bin(),
    alt.Y('Miles_per_Gallon').bin(),
    size='count()',
)

Агрегирование не ограничивается подсчётом. Например, цвет точки может показывать среднее значение третьей величины — ускорения:

alt.Chart(cars).mark_circle().encode(
    alt.X('Horsepower').bin(),
    alt.Y('Miles_per_Gallon').bin(),
    size='count()',
    color='mean(Acceleration):Q'
)

Полный список см. в таблице функций агрегирования.

Параметр Sort

Некоторые каналы принимают параметр sort, определяющий порядок значений шкалы. По умолчанию используется возрастающий алфавитный порядок. Исключение — упорядоченный категориальный столбец pandas без явно заданного типа: Altair использует его собственный порядок.

Способы настройки сортировки:

  • sort='ascending' — по возрастанию; для строк это обычный алфавитный порядок. Используется по умолчанию.
  • sort='descending' — по убыванию.
  • Имя канала, например sort='x' или sort='y', — по его значениям. Префикс минус задаёт убывание: sort='-x'.
  • Последовательность значений — явно заданный порядок.
  • Параметры field и op — по выбранному полю и результату операции агрегирования.

Применим пять подходов к оси x набора данных о ячмене:

import altair as alt
from altair.datasets import data

barley = data.barley()

base = alt.Chart(barley).mark_bar().encode(
    y='mean(yield):Q',
    color=alt.Color('mean(yield):Q').legend(None)
).properties(width=100, height=100)

# Сортируем x по возрастанию
ascending = base.encode(
    alt.X('site:N').sort('ascending')
).properties(
    title='По возрастанию'
)

# Сортируем x по убыванию
descending = base.encode(
    alt.X('site:N').sort('descending')
).properties(
    title='По убыванию'
)

# Задаём порядок x явно
explicit = base.encode(
    alt.X('site:N').sort(
        ['Duluth', 'Grand Rapids', 'Morris', 'University Farm', 'Waseca', 'Crookston']
    )
).properties(
    title='Заданный порядок'
)

# Сортируем по каналу кодирования
sortchannel = base.encode(
    alt.X('site:N').sort('y')
).properties(
    title='По каналу'
)

# Сортируем по другому полю
sortfield = base.encode(
    alt.X('site:N').sort(field='yield', op='mean')
).properties(
    title='По урожайности'
)

alt.concat(
    ascending,
    descending,
    explicit,
    sortchannel,
    sortfield,
    columns=3
)

Последние две диаграммы одинаковы: операция агрегирования сортировки по умолчанию — mean. Чтобы увидеть разницу между сортировкой по каналу и полю, не будем агрегировать точки и через op зададим другую операцию сортировки:

import altair as alt
from altair.datasets import data

barley = data.barley()
base = alt.Chart(barley).mark_point().encode(
    y='yield:Q',
).properties(width=200)

# Сортируем по каналу кодирования
sortchannel = base.encode(
    alt.X('site:N').sort('y')
).properties(
    title='По каналу'
)

# Сортируем по другому полю
sortfield = base.encode(
    alt.X('site:N').sort(field='yield', op='max')
).properties(
    title='По максимуму урожайности'
)
sortchannel | sortfield

Сортировка легенд

Подобно сортировке осей через X и Y, порядок легенды можно задать в связанном канале: color, shape, size и других. Пример для Color:

alt.Chart(barley).mark_bar().encode(
    alt.X('mean(yield):Q'),
    alt.Y('site:N').sort('x'),
    alt.Color('site:N').sort([
        'Morris', 'Duluth', 'Grand Rapids', 'University Farm', 'Waseca', 'Crookston'
    ])
)

Ось y сортируется по значениям x, а цветовая легенда — в указанном порядке, начиная с 'Morris'.

В следующем примере field, op и order сортируют легенду по выбранному полю и операции:

alt.Chart(barley).mark_bar().encode(
    alt.X('mean(yield):Q'),
    alt.Y('site:N').sort('x'),
    color=alt.Color('site').sort(field='yield', op='max', order='ascending')
)

Datum и Value

До сих пор каналы связывались со столбцами данных. Иногда полезно задать одну константу:

  • datum — постоянное значение в области данных. Оно проходит через шкалу и использует те же единицы, что и данные.
  • value — постоянное визуальное значение: точное положение в пикселях, имя или RGB-код цвета, название формы и т. п.

datum удобно для обозначения конкретного значения данных. Например, примитив rule может показывать порог цены акции в 300 долларов:

import altair as alt
from altair.datasets import data

source = data.stocks()
base = alt.Chart(source)
lines = base.mark_line().encode(
    x="date:T",
    y="price:Q",
    color="symbol:N"
)
rule = base.mark_rule(strokeDash=[2, 2]).encode(
    y=alt.datum(300)
)

lines + rule

Если вместо него использовать alt.value, линия окажется на расстоянии 300 пикселей от верхней границы диаграммы, а не на уровне 300 долларов. При стандартной высоте 300 пикселей пунктирная линия попадёт на ось x:

rule = base.mark_rule(strokeDash=[2, 2]).encode(
    y=alt.value(300)
)

lines + rule

Чтобы отметить год на временной оси x через datum, недостаточно передать целое число: используйте объект DateTime. Здесь цвет линии также связан со шкалой и соответствует серии MSFT благодаря alt.datum("MSFT"):

import altair as alt
from altair.datasets import data

source = data.stocks()
base = alt.Chart(source)
lines = base.mark_line().encode(
    x="date:T",
    y="price:Q",
    color="symbol:N"
)
rule = base.mark_rule(strokeDash=[2, 2]).encode(
    x=alt.datum(alt.DateTime(year=2006)),
    color=alt.datum("MSFT")
)

lines + rule

Для datum разные каналы могут поддерживать параметры band, scale, axis, legend, format, condition, как и для поля. Но преобразования aggregate, bin, timeUnit и sort к константе применить нельзя.

Чтобы цвет rule не зависел от цветовой шкалы линий, используйте value, например alt.value("red"):

import altair as alt
from altair.datasets import data

source = data.stocks()
base = alt.Chart(source)
lines = base.mark_line().encode(
    x="date:T",
    y="price:Q",
    color="symbol:N"
)
rule = base.mark_rule(strokeDash=[2, 2]).encode(
    x=alt.datum(alt.DateTime(year=2006)),
    color=alt.value("red")
)

lines + rule

У alt.datum и alt.value нет синтаксиса методов настройки параметров, появившегося в Altair 5.0. Например, для константы канала y и метода scale используйте YDatum:

import altair as alt

alt.Chart().mark_bar().encode(
    y=alt.YDatum(220).scale(domain=(0,500)),
    color=alt.value("darkkhaki")
)

Вариант y=alt.datum(220).scale(domain=(0,500)) вызывает AttributeError: alt.datum(220) возвращает словарь Python без атрибута scale. Можно передать словарь вручную: y=alt.datum(220, scale={"domain": (0,500)}), но предпочтительнее alt.YDatum.

В частности, alt.YDatum поддерживает автодополнение: в JupyterLab после alt.YDatum(220).scale(do клавиша Tab предложит domain, domainMax, domainMid, domainMin.