Пиво и карты

R
Тематические визуализации: несколько примеров в ggplot2
Автор

Е. Тымченко

Дата публикации

9 февраля 2025 г.

Визуализация данных - творческий процесс

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

Сегодня мы разберём технические особенности визуализации в ggplot2 на данных об алкогольных предпочтениях россиян.

cat beer

Гистограммы малоинформативны

Почему?

  • Они не рассказывают распределении данных в выборке.
  • Это может запутать вас и читателя.

В идеале каждый график рассказывает какую-то историю. Наверняка в ваших любимых книгах есть не один слой смысла а несколько. Поэтому вы их и любите. В приведённой столбиковой диаграмме слой смысла ровно один поэтому в современной аналитике подача информации может быть менее примитивной. В конце концов, многие восхищаются иллюстрациями журнала The Economist или Bloomberg, но когда дело доходит до собственных визуализаций всё деградирует до столбиков с односложным посылом.

Делаем график про алкогольные предпочтения

Загрузка библиотек и шрифтов

{
library(tidyverse) # Основная библиотека в экосистеме
library(showtext) # Рендер текста
library(ggtext) # Рендер текста
library(sysfonts) # Загрузка шрифтов
library(ggridges) # Красивые геометрии
library(formattable) # Для визуализации таблички
library(viridis) # Цветовые палитры
library(rcartocolor) # Цветовые палитры

# Для работы с картами:

library(sf) 
library(terra)
library(tidyterra)

showtext_auto()

# Загружаем шрифт:

GET('https://github.com/ETymch/Econometrics_2023/raw/main/Plotting/HSESans-Regular.otf', write_disk('HSESans-Regular.otf', overwrite = T))
GET('https://github.com/ETymch/Econometrics_2023/raw/main/Plotting/HSESans-Bold.otf', write_disk('HSESans-Bold.otf', overwrite = T))
GET('https://github.com/ETymch/Econometrics_2023/raw/main/Plotting/HSESans-Italic.otf', write_disk('HSESans-Italic.otf', overwrite = T))
GET('https://github.com/ETymch/Econometrics_2023/raw/main/Plotting/HSESans-SemiBold.otf', write_disk('HSESans-SemiBlod.otf', overwrite = T))

font_add(family = 'HSE Sans',
         regular = "HSESans-Regular.otf",
         bold = 'HSESans-Bold.otf',
         italic = 'HSESans-Italic.otf',
         bolditalic = 'HSESans-SemiBlod.otf'
) 
}

Загрузка данных

Моя большая признательность за данные проекту RLMS HSE. В рамках добросовестного использования прилагаю ссылку на источник: Российский мониторинг экономического положения и здоровья населения НИУ ВШЭ (RLMS HSE)», проводимый Национальным исследовательским университетом “Высшая школа экономики” и ООО «Демоскоп» при участии Центра народонаселения Университета Северной Каролины в Чапел Хилле и Института социологии Федерального научно-исследовательского социологического центра РАН. (Сайты обследования RLMS HSE: http://www.hse.ru/rlms и https://rlms-hse.cpc.unc.edu).

df <- read.csv("https://raw.githubusercontent.com/ETymch/Econometrics_2023/refs/heads/main/Datasets/tutorial_gg_rlms.csv")

Как выглядит табличка?

df %>%
  select(bb_age, name) %>%
  head(14) %>%
  formattable(align =c("c","l"),
            list(`Возраст`= color_tile('transparent', 'lightpink')
))

Сначала всегда точки: geom_point

df %>%
   ggplot(aes(x = Возраст, y = Напиток)) +
   geom_point()

Ящик с усами: geom_boxplot

df %>%
   ggplot(aes(x = Возраст, y = Напиток)) +
   geom_boxplot(outlier.shape = NA, coef = 0)

Разбросанные точки: geom_jitter

df %>% ggplot(aes(x = Возраст, y = Напиток)) +
   geom_boxplot(outlier.shape = NA, coef = 0) +
   geom_jitter()

Прозрачность: alpha = [0,1]

df %>%
   ggplot(aes(x = Возраст, y = Напиток)) +
   geom_boxplot(outlier.shape = NA, coef = 0) +
   geom_jitter(alpha = 0.2)

Можно раскрасить точки

В шапке добавим aes(color =Какая-то переменная)`.

df %>%
   ggplot(aes(x = Возраст, y = Напиток, color = Возраст)) +
   geom_boxplot(outlier.shape = NA, coef = 0) +
   geom_point(position=position_jitterdodge(0.2), alpha = 1)

Другие геометрии

Функции плотности распределения: ggridges.

зlot <- df %>%
   ggplot(aes(x = Возраст, y = Напиток, color = Возраст)) +
   geom_point(position=position_jitterdodge(0.2), alpha = 0.5) +
   ggridges::geom_density_ridges()
plot

Палитры viridis

plot <- plot + scale_color_viridis_c()
plot

Темы для графиков

plot +
   theme_classic()

Темы для графиков

plot <- plot + theme_minimal()
plot

Шрифты и размеры текста

Единый шрифт добавляется при помощи включения base_family, base_size в настройках темы. Шрифты загружаются и рисуются при помощи библиотек sysfonts, showtext и ggtext. Последняя добавляет html инструменты для управления шрифтами.

plot <- plot + theme_minimal(base_family = 'HSE Sans', base_size = 14)
plot

Можно создать свою тему

Или изменить уже существующую: +theme(). Также я изменил формат легенды при помощи guides().

plot <- plot + 
   theme("Здесь можно менять: формат заголовков, шкал, осей и вообще всего, что можно предстваить.") +
   guides(color = guide_colorbar(barwidth = 12, barheight = 0.5))
plot

Можно добавить текст

Для удобства создадим отдельную табличку anno с данными для аннотации.

anno <- df %>% count(Напиток) # Табличка

plot <- plot + 
  annotate('text', # Аннотация может быть не только текстом, но и линией, линией со стрелкой, прямоугольником и многим другим.
           x = 95, y = anno$Напиток, # Координаты аннотаций.
           label = paste0('n = ', anno$Напиток), # формат аннотации
           vjust = -0.8) # Сдвинуть по вертикали вниз на 0.8
plot

Заголовки

Добавляем заголовки labs, модифицируем тему. Привожу полную версию кода.

df %>%
  ggplot(aes(x = Возраст, y = Напиток, color = Возраст)) +
  geom_point(position=position_jitterdodge(0.2), alpha = 0.15) +
  ggridges::geom_density_ridges(fill = '#442F3D', quantile_lines = T, quantiles = 2, color = 'grey20', linewidth = 0.5, alpha = 0.2) +
  annotate('text', x = 95, y = anno$Напиток, label = paste0('n = ', anno$Напиток), vjust = -0.8) +
  theme_minimal(base_family = 'HSE Sans', base_size = 14) +
  theme(legend.position = 'bottom',
        legend.margin=margin(t = -0.2, unit='cm'),
        panel.grid.minor.x = element_blank(),
        panel.grid.minor.y = element_blank(),
        panel.grid.major.y = element_blank(),
        axis.title.y = element_blank(),
        legend.title = element_blank(),
        plot.title.position = 'plot',
        plot.caption = element_text(size = 7, face = 'italic'),
        plot.subtitle = element_markdown(hjust = 0.0, size = 13),
        plot.title = element_markdown(hjust = 0.0,
                                      size = 18, face = 'bold')
  ) +
  guides(color = guide_colorbar(barwidth = 12, barheight = 0.5)) +
  scale_fill_carto_c(palette = "TealRose", direction = -1) +
  scale_color_carto_c(palette = "TealRose", direction = -1) +
  labs(x = 'Возраст, лет',
       title = 'Каждому возрасту - свой напиток',
       subtitle = "<span style = 'color:#D98994;'>Более молодые</span> предпочитают коктейли или пиво,<br> <span style = 'color:#009392;'> познавшие жизнь </span>- водку, самогон и вино.",
       caption = 'Данные: Российский мониторинг экономического положения и здоровья населения НИУ ВШЭ (RLMS HSE), http://www.hse.ru/rlms')

Также цветом можно выделить пол

Мне эта версия нравится даже больше, потому что информация о возрасте не дублируется.

df %>%
  ggplot(aes(x = Возраст, y = Напиток, color = factor(Пол, levels = c(1,2)))) +
  geom_point(position=position_jitterdodge(0.5), alpha = 0.25) +
  ggridges::geom_density_ridges(fill = '#442F3D', quantile_lines = T, quantiles = 2, color = 'grey20', linewidth = 0.5, alpha = 0.15) +
  annotate('text', x = 95, y = anno$Напиток, label = paste0('n = ', anno$Напиток), vjust = -0.8) +
  theme_minimal(base_family = 'HSE Sans', base_size = 14) +
  theme(legend.position = 'none',
        legend.margin=margin(t = -0.2, unit='cm'),
        panel.grid.minor.x = element_blank(),
        panel.grid.minor.y = element_blank(),
        panel.grid.major.y = element_blank(),
        axis.title.y = element_blank(),
        legend.title = element_blank(),
        plot.title.position = 'plot',
        plot.caption = element_text(size = 7, face = 'italic'),
        plot.subtitle = element_markdown(hjust = 0.0, size = 13),
        plot.title = element_markdown(hjust = 0.0,
                                      size = 18, face = 'bold')
  ) +
  scale_fill_manual(values = c('#E39921', '#E482B5')) +
  scale_color_manual(values = c('#E39921', '#E482B5')) +
  labs(x = 'Возраст, лет',
       title = 'Каждому - свой напиток',
       subtitle = "<span style = 'color:#E482B5;'>Женщины </span>заметно чаще <span style = 'color:#E39921;'>мужчин</span> выбирают вино и коктейли.<br><span style = 'color:#E482B5;'>Женщины</span> начинают употреблять самогон в более позднем возрасте, чем <span style = 'color:#E39921;'>мужчины</span>.",
       caption = 'Данные: Российский мониторинг экономического положения и здоровья населения НИУ ВШЭ (RLMS HSE), http://www.hse.ru/rlms')

Что предлагает ИИ? phi4 от Microsoft.

Я попросил языковую модель предложить свой вариант визуализации. Её код выглядел так:

df %>%
  mutate(age_group = ifelse(Возраст < 40, "<40", ">=40")) %>%
  mutate(Пол = ifelse(Пол == 1, 'Мужчины', 'Женщины')) %>%
  ggplot(aes(x = Напиток, fill = factor(Пол, levels = c('Мужчины', 'Женщины')))) +
  geom_bar(position = position_dodge(), stat = 'count') +
  facet_wrap(~age_group) +
  theme_minimal() +
  scale_fill_manual(values = c('blue', 'pink')) + # неплохое сочетание цветов
  labs(title = 'Распределение предпочпочтений по возрастным группам', # Да, именно предпочпочтений😊
       x = "Напиток",
       y = "Кол-во людей")

Довольно средне, не правда ли? Зная ggplot2 за минуту этот график можно улучшить для более приятной версии.

После ручных правок

df %>%
  mutate(age_group = ifelse(Возраст < 40, "Возраст < 40 лет", " Возраст >= 40 лет") %>% factor(levels = c("Возраст < 40 лет", " Возраст >= 40 лет"))) %>%
  mutate(Пол = ifelse(Пол == 1, 'Мужчины', 'Женщины')) %>%
  ggplot(aes(x = Напиток, fill = factor(Пол, levels = c('Мужчины', 'Женщины')))) +
  geom_bar(position = position_dodge(), stat = 'count', alpha = 0.9) +
  facet_wrap(~age_group) + # отличная находка phi4
  theme_classic(base_family = 'HSE Sans', base_size = 14) + # Такая тема мне нравится больше
  scale_fill_manual(values = c('blue', 'pink')) +
  labs(title = 'Распределение предпочтений по возрастным группам',
       x = "Напиток",
       y = "Кол-во людей") +
  coord_flip() + # так намного лучше 
  theme(legend.position = 'bottom',
        legend.title = element_blank(),
        axis.title.y = element_blank(),
        plot.title.position = 'plot',
        plot.title = element_markdown(hjust = 0.0,
                                      size = 16, face = 'bold'))

Мне нравится, что phi4 предложил разбить график на две клетки, позволил продемонстрировать замечательную команду facet_wrap(), которая позволяет создавать композиции из нескольких графиков.

Плюсы:

  • В ванильной ggplot2 около 70 геометрий. Если добавить геометрии из модификаций, то счёт идёт на сотни. Есть также могучая библиотека ggforce и многие другие, которые расширяют набор геометрий.

  • Можно делать свои шаблоны для графиков.

  • Удобно автоматизировать.

  • Можно строить карты.

  • Можно делать анимации, но их применение в аналитике ограничено.

Композиции графиков

  • Библиотека patchwork.

  • Соединение графиков в ансамбль в одну строчку: plot_1 + plot_2

  • Есть более простой способ: добавить +facet_wrap в ggplot.

Пример композиции

Мне очень нравится эта композиция из графиков, потому что эта композиция рассказывает монолитную историю. Как сделать такой график я показывал здесь.

Карты городов

Можно построить карту Москвы на кадастровых данных, которые в своём обучающем материале собрал Марсель Салихов. Данные, возможно, опубликую в будущем. В основе визуализации библиотеки sf и tidyterra.

kadastr %>% # Табличка
  ggplot() + # Строим график
    geom_sf(aes(fill = `Объект культурного наследия`, color = `Объект культурного наследия`)) + # слой полигонов
    geom_sf(data = kadastr_na, color = '#4D483E', size = 1, alpha =0.9) + # слой точек для домов, которые не измерены
    scale_color_manual(values = c('#D05546', '#4D483E')) + # Цветовая шкала color - это, как правило, про контуры объекта
    scale_fill_manual(values = c('#D05546', '#4D483E')) + # Шкала заливки, fill - как правило, про внутреннюю заливку объекта.
    theme_void(base_family = 'HSE Sans') + # Пустая тема
    theme(legend.position = 'bottom',
          text = element_text(colour = "white"),
          plot.background = element_rect(fill = 'black', color = '#D05546'),
          legend.margin = margin(-20,0,0,0, 'pt'),
          plot.margin = margin(-16, -16, 0, -16, "pt")
    )

Заключение

Сегодня мы рассмотрели несколько мотивирующих примеров визуализации в ggplot2. Строить графики - не сложно, в большинстве случаев можно уложиться в несколько строчек кода. Современные языки программирования, такие как R, достаточно просты в освоении, а код на них легко читается и выглядит лаконично. С помощью кода и инструментов парсинга можно автоматизировать построение графиков для регулярных отчётов или углубить понимание данных при помощи комбинирования геометрий и игры с цветом. Спасибо за чтение, приятного путешествия в мир визуализации данных.