Трансформация formula

Трансформация formula в Vega-Lite и Vega предназначена для вычисления новых полей на основе существующих данных. Она позволяет выполнять математические операции, объединять строки, преобразовывать значения, вычислять производные показатели и подготавливать данные непосредственно внутри спецификации визуализации.

formula особенно полезна в ситуациях, когда:

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

Базовый синтаксис

В Vega-Lite трансформация располагается внутри массива transform.

{
  "transform": [
    {
      "formula": "datum.price * datum.count",
      "as": "total"
    }
  ]
}

Основные параметры

Параметр Назначение
formula Выражение для вычисления
as Имя нового поля

Принцип работы

Трансформация проходит по каждой строке набора данных и вычисляет выражение из formula.

Исходные данные:

[
  { "product": "Laptop", "price": 1200, "count": 2 },
  { "product": "Mouse", "price": 25, "count": 5 }
]

После применения:

{
  "formula": "datum.price * datum.count",
  "as": "total"
}

Результат:

[
  { "product": "Laptop", "price": 1200, "count": 2, "total": 2400 },
  { "product": "Mouse", "price": 25, "count": 5, "total": 125 }
]

Объект datum

Внутри выражения используется специальный объект datum.

Он представляет текущую строку данных.

datum.price
datum.count
datum.category

Пример:

{
  "formula": "datum.income - datum.expenses",
  "as": "profit"
}

Простые арифметические операции

Сложение

{
  "formula": "datum.a + datum.b",
  "as": "sum"
}

Вычитание

{
  "formula": "datum.revenue - datum.costs",
  "as": "profit"
}

Умножение

{
  "formula": "datum.price * datum.quantity",
  "as": "total"
}

Деление

{
  "formula": "datum.sales / datum.users",
  "as": "avg"
}

Остаток от деления

{
  "formula": "datum.value % 2",
  "as": "remainder"
}

Использование математических функций

Vega поддерживает большое количество встроенных математических функций.

Округление

{
  "formula": "round(datum.value)",
  "as": "rounded"
}

Округление до двух знаков

{
  "formula": "round(datum.price * 100) / 100",
  "as": "roundedPrice"
}

Минимум и максимум

{
  "formula": "max(datum.a, datum.b)",
  "as": "maxValue"
}
{
  "formula": "min(datum.a, datum.b)",
  "as": "minValue"
}

Возведение в степень

{
  "formula": "pow(datum.value, 2)",
  "as": "square"
}

Квадратный корень

{
  "formula": "sqrt(datum.value)",
  "as": "root"
}

Логарифм

{
  "formula": "log(datum.value)",
  "as": "logValue"
}

Работа со строками

Конкатенация строк

{
  "formula": "datum.firstName + ' ' + datum.lastName",
  "as": "fullName"
}

Добавление текста

{
  "formula": "'$' + datum.price",
  "as": "priceLabel"
}

Преобразование регистра

{
  "formula": "upper(datum.country)",
  "as": "countryUpper"
}
{
  "formula": "lower(datum.category)",
  "as": "categoryLower"
}

Получение длины строки

{
  "formula": "length(datum.name)",
  "as": "nameLength"
}

Условные выражения

В formula поддерживается тернарный оператор.

Простое условие

{
  "formula": "datum.value > 100 ? 'big' : 'small'",
  "as": "group"
}

Несколько условий

{
  "formula": "datum.score >= 90 ? 'A' : datum.score >= 75 ? 'B' : 'C'",
  "as": "grade"
}

Логические операции

Проверка диапазона

{
  "formula": "datum.age >= 18 && datum.age <= 60",
  "as": "workingAge"
}

Проверка нескольких условий

{
  "formula": "datum.sales > 1000 && datum.region === 'EU'",
  "as": "important"
}

Логическое ИЛИ

{
  "formula": "datum.role === 'admin' || datum.role === 'manager'",
  "as": "privileged"
}

Работа с датами

Трансформация formula часто применяется для вычисления временных характеристик.

Извлечение года

{
  "formula": "year(datum.date)",
  "as": "year"
}

Извлечение месяца

{
  "formula": "month(datum.date)",
  "as": "month"
}

День недели

{
  "formula": "day(datum.date)",
  "as": "weekday"
}

Час

{
  "formula": "hours(datum.date)",
  "as": "hour"
}

Создание категорий

Одно из важнейших применений formula — категоризация данных.

Категории по диапазонам

{
  "formula": "datum.income < 3000 ? 'low' : datum.income < 10000 ? 'middle' : 'high'",
  "as": "incomeLevel"
}

Группировка возраста

{
  "formula": "datum.age < 18 ? 'child' : datum.age < 60 ? 'adult' : 'senior'",
  "as": "ageGroup"
}

Подготовка данных для цветового кодирования

formula часто используется совместно с color.

{
  "transform": [
    {
      "formula": "datum.profit > 0 ? 'profit' : 'loss'",
      "as": "status"
    }
  ],
  "encoding": {
    "color": {
      "field": "status",
      "type": "nominal"
    }
  }
}

Вычисление процентов

Процент выполнения

{
  "formula": "(datum.current / datum.target) * 100",
  "as": "progress"
}

Доля продаж

{
  "formula": "datum.sales / datum.totalSales",
  "as": "share"
}

Комбинирование с другими трансформациями

formula редко используется изолированно. Обычно она работает совместно с:

  • filter
  • aggregate
  • calculate
  • window
  • joinaggregate

Связка aggregate + formula

{
  "transform": [
    {
      "aggregate": [
        {
          "op": "sum",
          "field": "sales",
          "as": "totalSales"
        }
      ],
      "groupby": ["category"]
    },
    {
      "formula": "datum.totalSales * 0.2",
      "as": "tax"
    }
  ]
}

Связка joinaggregate + formula

Один из самых распространённых сценариев.

{
  "transform": [
    {
      "joinaggregate": [
        {
          "op": "sum",
          "field": "sales",
          "as": "globalSales"
        }
      ]
    },
    {
      "formula": "datum.sales / datum.globalSales",
      "as": "share"
    }
  ]
}

Связка window + formula

{
  "transform": [
    {
      "window": [
        {
          "op": "rank",
          "as": "rank"
        }
      ],
      "sort": [
        {
          "field": "sales",
          "order": "descending"
        }
      ]
    },
    {
      "formula": "datum.rank <= 10 ? 'top' : 'other'",
      "as": "group"
    }
  ]
}

Полный пример

Исходные данные

[
  { "product": "A", "sales": 120, "cost": 80 },
  { "product": "B", "sales": 200, "cost": 140 },
  { "product": "C", "sales": 50, "cost": 70 }
]

Спецификация Vega-Lite

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",

  "data": {
    "values": [
      { "product": "A", "sales": 120, "cost": 80 },
      { "product": "B", "sales": 200, "cost": 140 },
      { "product": "C", "sales": 50, "cost": 70 }
    ]
  },

  "transform": [
    {
      "formula": "datum.sales - datum.cost",
      "as": "profit"
    },
    {
      "formula": "datum.profit > 0 ? 'positive' : 'negative'",
      "as": "status"
    }
  ],

  "mark": "bar",

  "encoding": {
    "x": {
      "field": "product",
      "type": "nominal"
    },

    "y": {
      "field": "profit",
      "type": "quantitative"
    },

    "color": {
      "field": "status",
      "type": "nominal"
    }
  }
}

Отличие formula от calculate

В Vega-Lite термин calculate используется как псевдоним formula.

Следующие варианты эквивалентны:

{
  "calculate": "datum.a + datum.b",
  "as": "sum"
}
{
  "formula": "datum.a + datum.b",
  "as": "sum"
}

Однако в документации Vega чаще используется formula, а в Vega-Lite — calculate.


Порядок выполнения трансформаций

Порядок трансформаций критически важен.

Правильный порядок

"transform": [
  {
    "joinaggregate": [
      {
        "op": "sum",
        "field": "sales",
        "as": "total"
      }
    ]
  },
  {
    "formula": "datum.sales / datum.total",
    "as": "share"
  }
]

Ошибочный порядок

"transform": [
  {
    "formula": "datum.sales / datum.total",
    "as": "share"
  },
  {
    "joinaggregate": [
      {
        "op": "sum",
        "field": "sales",
        "as": "total"
      }
    ]
  }
]

Во втором случае поле total ещё не существует на момент вычисления.


Обработка null и undefined

Проверка на null

{
  "formula": "datum.value == null ? 0 : datum.value",
  "as": "safeValue"
}

Защита от деления на ноль

{
  "formula": "datum.count === 0 ? 0 : datum.total / datum.count",
  "as": "avg"
}

Вычисление флагов

Флаги часто используются для интерактивности и подсветки.

{
  "formula": "datum.sales > 1000",
  "as": "highlight"
}

Использование регулярных выражений

{
  "formula": "test(/admin/i, datum.role)",
  "as": "isAdmin"
}

Создание URL

{
  "formula": "'https://example.com/user/' + datum.id",
  "as": "profileUrl"
}

Нормализация данных

{
  "formula": "(datum.value - datum.min) / (datum.max - datum.min)",
  "as": "normalized"
}

Логарифмическое преобразование

{
  "formula": "log(datum.population)",
  "as": "logPopulation"
}

Преобразование типов

Строка в число

{
  "formula": "toNumber(datum.value)",
  "as": "numericValue"
}

Число в строку

{
  "formula": "toString(datum.id)",
  "as": "idString"
}

Преобразование в дату

{
  "formula": "toDate(datum.timestamp)",
  "as": "date"
}

Производительность

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

Особенно затратны:

  • регулярные выражения;
  • сложные строковые операции;
  • многократные математические вычисления;
  • глубокие вложенные условия.

При больших объёмах данных вычисления лучше переносить на сервер или этап предварительной обработки.


Типичные ошибки

Отсутствие datum

Неправильно:

{
  "formula": "price * count",
  "as": "total"
}

Правильно:

{
  "formula": "datum.price * datum.count",
  "as": "total"
}

Ошибки в имени поля

{
  "formula": "datum.sales / datum.totalSales",
  "as": "share"
}

Если totalSales отсутствует, результатом станет NaN.


Деление на ноль

{
  "formula": "datum.total / datum.count",
  "as": "avg"
}

Требуется проверка:

{
  "formula": "datum.count === 0 ? 0 : datum.total / datum.count",
  "as": "avg"
}

Практический сценарий: аналитика продаж

{
  "transform": [
    {
      "joinaggregate": [
        {
          "op": "sum",
          "field": "sales",
          "as": "allSales"
        }
      ]
    },

    {
      "formula": "datum.sales - datum.costs",
      "as": "profit"
    },

    {
      "formula": "datum.sales / datum.allSales",
      "as": "share"
    },

    {
      "formula": "datum.profit > 0 ? 'profit' : 'loss'",
      "as": "status"
    },

    {
      "formula": "round(datum.share * 100)",
      "as": "sharePercent"
    }
  ]
}

В этом примере:

  • вычисляется общая сумма продаж;
  • рассчитывается прибыль;
  • определяется доля;
  • создаётся статус;
  • вычисляется процентное представление доли.

Рекомендации по использованию

Использование коротких формул

Сложные выражения ухудшают читаемость.

Плохо:

{
  "formula": "(datum.a + datum.b - datum.c) / (datum.d * datum.e) > 10 ? 'x' : 'y'",
  "as": "result"
}

Лучше разбивать вычисления:

{
  "formula": "datum.a + datum.b - datum.c",
  "as": "temp"
}
{
  "formula": "datum.temp / (datum.d * datum.e)",
  "as": "ratio"
}
{
  "formula": "datum.ratio > 10 ? 'x' : 'y'",
  "as": "result"
}

Когда использовать formula

Трансформация особенно эффективна для:

  • вычисляемых метрик;
  • подготовки подписей;
  • категоризации;
  • создания флагов;
  • расчёта процентов;
  • нормализации данных;
  • условного форматирования;
  • подготовки данных к агрегации;
  • вычисления промежуточных значений.

Когда formula использовать нежелательно

Трансформация не подходит для:

  • тяжёлых аналитических вычислений;
  • обработки миллионов строк в браузере;
  • сложной ETL-логики;
  • многоступенчатой очистки данных;
  • статистических вычислений высокого уровня;
  • обработки сильно вложенных структур JSON.

В подобных случаях подготовка данных выполняется заранее — на сервере, в SQL, Pandas, Spark или ETL-пайплайне.