lookup: обогащение данных из внешней таблицы

Трансформация lookup в Vega и Vega-Lite используется для объединения данных из разных источников. По принципу работы она напоминает SQL-операцию LEFT JOIN: для каждой записи основной таблицы выполняется поиск совпадений во внешнем наборе данных.

lookup особенно полезен в следующих ситуациях:

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

На практике lookup часто применяется для:

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

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

lookup берет:

  1. основной источник данных;
  2. внешний источник;
  3. поле поиска;
  4. поле соответствия.

После этого в каждую запись основной таблицы добавляются найденные значения.

Схема работы:

Основная таблица
[
  { id: 1, value: 120 },
  { id: 2, value: 95 }
]

Внешняя таблица
[
  { id: 1, name: "A" },
  { id: 2, name: "B" }
]

Результат lookup
[
  { id: 1, value: 120, name: "A" },
  { id: 2, value: 95, name: "B" }
]

lookup в Vega-Lite

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

{
  "transform": [
    {
      "lookup": "id",
      "from": {
        "data": {
          "values": [
            { "id": 1, "name": "Alpha" },
            { "id": 2, "name": "Beta" }
          ]
        },
        "key": "id",
        "fields": ["name"]
      }
    }
  ]
}

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

lookup

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

"lookup": "id"

from

Описание внешнего набора данных.

"from": {
  ...
}

key

Поле внешней таблицы, с которым сравнивается lookup.

"key": "id"

fields

Список полей, которые нужно перенести.

"fields": ["name"]

Если поле не указано, может быть присоединен весь объект.


Простейший пример

Основная таблица

[
  { "country": "KZ", "sales": 120 },
  { "country": "RU", "sales": 200 },
  { "country": "US", "sales": 340 }
]

Таблица соответствий

[
  { "code": "KZ", "name": "Kazakhstan" },
  { "code": "RU", "name": "Russia" },
  { "code": "US", "name": "United States" }
]

Vega-Lite

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

  "data": {
    "values": [
      { "country": "KZ", "sales": 120 },
      { "country": "RU", "sales": 200 },
      { "country": "US", "sales": 340 }
    ]
  },

  "transform": [
    {
      "lookup": "country",

      "from": {
        "data": {
          "values": [
            { "code": "KZ", "name": "Kazakhstan" },
            { "code": "RU", "name": "Russia" },
            { "code": "US", "name": "United States" }
          ]
        },

        "key": "code",
        "fields": ["name"]
      }
    }
  ],

  "mark": "bar",

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

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

После выполнения lookup поле name становится частью основной таблицы.


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

lookup позволяет присоединять сразу несколько колонок.

Пример

"fields": [
  "name",
  "region",
  "population"
]

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

{
  "transform": [
    {
      "lookup": "country",

      "from": {
        "data": {
          "url": "countries.json"
        },

        "key": "code",

        "fields": [
          "name",
          "region",
          "population"
        ]
      }
    }
  ]
}

После объединения каждая запись получит:

{
  "country": "KZ",
  "sales": 120,
  "name": "Kazakhstan",
  "region": "Asia",
  "population": 19000000
}

Использование внешнего файла

Подключение JSON

{
  "data": {
    "url": "sales.json"
  },

  "transform": [
    {
      "lookup": "country",

      "from": {
        "data": {
          "url": "countries.json"
        },

        "key": "code",
        "fields": ["name"]
      }
    }
  ]
}

Подключение CSV

{
  "from": {
    "data": {
      "url": "countries.csv"
    },

    "key": "code",
    "fields": ["name"]
  }
}

Работа с географическими данными

Одно из важнейших применений lookup — связывание статистики с GeoJSON.


Пример объединения карты и статистики

Таблица статистики

[
  { "id": 398, "value": 120 },
  { "id": 840, "value": 340 }
]

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

{
  "url": "world.geojson",
  "format": {
    "type": "geojson"
  }
}

Vega-Lite

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

  "data": {
    "url": "world.geojson",
    "format": {
      "type": "geojson"
    }
  },

  "transform": [
    {
      "lookup": "properties.id",

      "from": {
        "data": {
          "values": [
            { "id": 398, "value": 120 },
            { "id": 840, "value": 340 }
          ]
        },

        "key": "id",
        "fields": ["value"]
      }
    }
  ],

  "mark": "geoshape",

  "encoding": {
    "color": {
      "field": "value",
      "type": "quantitative"
    }
  }
}

Использование as

По умолчанию имена присоединяемых полей сохраняются. Иногда это неудобно, особенно при совпадении названий.

Для переименования используется as.


Пример

{
  "lookup": "country",

  "from": {
    "data": {
      "url": "countries.json"
    },

    "key": "code",
    "fields": ["name"]
  },

  "as": ["country_name"]
}

Результат

{
  "country": "KZ",
  "country_name": "Kazakhstan"
}

Присоединение целого объекта

Если fields отсутствует, Vega-Lite может вставить весь найденный объект.


Пример

{
  "lookup": "country",

  "from": {
    "data": {
      "url": "countries.json"
    },

    "key": "code"
  },

  "as": "countryInfo"
}

Результат

{
  "country": "KZ",

  "countryInfo": {
    "code": "KZ",
    "name": "Kazakhstan",
    "region": "Asia"
  }
}

Доступ к вложенным данным

После вставки объекта можно обращаться к вложенным полям.

Пример

"encoding": {
  "tooltip": [
    {
      "field": "countryInfo.name",
      "type": "nominal"
    },

    {
      "field": "countryInfo.region",
      "type": "nominal"
    }
  ]
}

Поведение при отсутствии совпадений

Если соответствие не найдено:

{
  "country": "XX",
  "sales": 100
}

то новые поля получают значение null.


Пример результата

{
  "country": "XX",
  "sales": 100,
  "name": null
}

Обработка null

После lookup часто применяется calculate.

Пример

{
  "transform": [
    {
      "lookup": "country",

      "from": {
        "data": {
          "url": "countries.json"
        },

        "key": "code",
        "fields": ["name"]
      }
    },

    {
      "calculate": "datum.name || 'Unknown'",
      "as": "countryLabel"
    }
  ]
}

Несколько lookup

Трансформации можно комбинировать.


Пример

{
  "transform": [
    {
      "lookup": "country",

      "from": {
        "data": {
          "url": "countries.json"
        },

        "key": "code",
        "fields": ["name"]
      }
    },

    {
      "lookup": "product",

      "from": {
        "data": {
          "url": "products.json"
        },

        "key": "id",
        "fields": ["category"]
      }
    }
  ]
}

Последовательное обогащение

Иногда одна lookup-операция зависит от предыдущей.

Пример

[
  {
    "lookup": "cityId",
    "from": {
      "data": {
        "url": "cities.json"
      },
      "key": "id",
      "fields": ["countryId"]
    }
  },

  {
    "lookup": "countryId",
    "from": {
      "data": {
        "url": "countries.json"
      },
      "key": "id",
      "fields": ["countryName"]
    }
  }
]

lookup в Vega

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


Базовый пример Vega

{
  "data": [
    {
      "name": "sales",

      "values": [
        { "country": "KZ", "sales": 120 },
        { "country": "RU", "sales": 200 }
      ],

      "transform": [
        {
          "type": "lookup",

          "from": "countries",

          "key": "code",

          "fields": ["country"],

          "values": ["name"],

          "as": ["countryName"]
        }
      ]
    },

    {
      "name": "countries",

      "values": [
        { "code": "KZ", "name": "Kazakhstan" },
        { "code": "RU", "name": "Russia" }
      ]
    }
  ]
}

Отличия Vega от Vega-Lite

Vega-Lite

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

Vega

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

Параметры lookup в Vega

from

Имя другого набора данных.

"from": "countries"

key

Поле таблицы-источника.

"key": "code"

fields

Поля текущего набора.

"fields": ["country"]

values

Какие значения переносить.

"values": ["name"]

as

Названия результирующих полей.

"as": ["countryName"]

Lookup как аналог SQL JOIN

Соответствие SQL:

SEL ECT
  s.country,
  s.sales,
  c.name
FR OM sales s
LEFT JOIN countries c
ON s.country = c.code

Эквивалент Vega-Lite:

{
  "lookup": "country",

  "from": {
    "data": {
      "url": "countries.json"
    },

    "key": "code",
    "fields": ["name"]
  }
}

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

lookup может стать дорогой операцией при больших объемах данных.

Особенно это заметно:

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

Практики оптимизации

Минимизация полей

Плохо:

"fields": [
  "a",
  "b",
  "c",
  "d",
  "e",
  "f"
]

Лучше:

"fields": ["name"]

Предварительная агрегация

До объединения полезно сократить объем данных.

{
  "aggregate": [
    {
      "op": "sum",
      "field": "sales",
      "as": "total"
    }
  ],

  "groupby": ["country"]
}

Избегание лишних lookup

Иногда эффективнее подготовить данные заранее на сервере.


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

Несовпадение типов

Частая проблема:

"country": 1

и

"code": "1"

Число и строка не совпадут.


Неправильный key

Ошибка:

"key": "country"

при отсутствии такого поля.


Неверное имя поля

Ошибка:

"fields": ["title"]

если поле называется name.


Дублирование ключей

Если во внешней таблице несколько одинаковых ключей:

[
  { "id": 1, "name": "A" },
  { "id": 1, "name": "B" }
]

результат может быть неоднозначным.


Отладка lookup

Полезно временно выводить данные в tooltip.

Пример

"tooltip": [
  { "field": "country" },
  { "field": "name" }
]

Комбинация с filter

{
  "transform": [
    {
      "lookup": "country",

      "from": {
        "data": {
          "url": "countries.json"
        },

        "key": "code",
        "fields": ["region"]
      }
    },

    {
      "filter": "datum.region === 'Asia'"
    }
  ]
}

Комбинация с calculate

{
  "transform": [
    {
      "lookup": "country",

      "from": {
        "data": {
          "url": "countries.json"
        },

        "key": "code",
        "fields": ["population"]
      }
    },

    {
      "calculate": "datum.sales / datum.population",
      "as": "salesPerCapita"
    }
  ]
}

Lookup и tooltip

Очень распространенный сценарий — загрузка вспомогательных данных только для подсказок.


Пример

{
  "transform": [
    {
      "lookup": "productId",

      "from": {
        "data": {
          "url": "products.json"
        },

        "key": "id",

        "fields": [
          "name",
          "description"
        ]
      }
    }
  ],

  "encoding": {
    "tooltip": [
      { "field": "name" },
      { "field": "description" }
    ]
  }
}

Lookup и интерактивность

lookup часто используется вместе с selection и параметрами.


Пример динамического выделения

{
  "transform": [
    {
      "lookup": "country",

      "from": {
        "data": {
          "url": "countries.json"
        },

        "key": "code",
        "fields": ["region"]
      }
    }
  ],

  "params": [
    {
      "name": "regionSelect",
      "bind": {
        "input": "select",
        "options": [
          "Asia",
          "Europe"
        ]
      }
    }
  ],

  "transform": [
    {
      "filter": "datum.region === regionSelect"
    }
  ]
}

Lookup в реальных проектах

Наиболее частые сценарии:

Сценарий Что объединяется
Карты GeoJSON + статистика
Dashboard факты + справочники
BI ID + текстовые подписи
Аналитика продаж товары + категории
Финансы тикеры + метаданные
Логирование события + описания
IoT датчики + география

Сравнение lookup и joinaggregate

lookup

Используется для объединения разных наборов данных.

{
  "lookup": "id"
}

joinaggregate

Работает внутри одного набора.

{
  "joinaggregate": [
    {
      "op": "mean",
      "field": "sales",
      "as": "avgSales"
    }
  ]
}

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

lookup подходит, если:

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

Когда лучше отказаться от lookup

Иногда объединение лучше выполнить заранее:

  • на backend;
  • в SQL;
  • в Pandas;
  • в ETL-процессе;
  • при очень больших данных.

Это снижает нагрузку на браузер и упрощает спецификацию.


Архитектурные рекомендации

Хранение справочников отдельно

Хорошая практика:

sales.json
countries.json
products.json
regions.json

Это облегчает поддержку и переиспользование.


Использование стабильных ключей

Лучше:

"id": 398

хуже:

"name": "Kazakhstan"

Текстовые поля менее надежны.


Проверка уникальности ключей

Ключи lookup-таблицы должны быть уникальными.


Полный пример dashboard-пайплайна

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

  "data": {
    "url": "sales.json"
  },

  "transform": [
    {
      "lookup": "country",

      "from": {
        "data": {
          "url": "countries.json"
        },

        "key": "code",

        "fields": [
          "name",
          "region",
          "population"
        ]
      }
    },

    {
      "calculate": "datum.sales / datum.population",
      "as": "perCapita"
    },

    {
      "filter": "datum.region !== null"
    }
  ],

  "mark": "circle",

  "encoding": {
    "x": {
      "field": "population",
      "type": "quantitative"
    },

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

    "size": {
      "field": "perCapita",
      "type": "quantitative"
    },

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

    "tooltip": [
      { "field": "name" },
      { "field": "sales" },
      { "field": "population" },
      { "field": "perCapita" }
    ]
  }
}