Материалы для самостоятельного изучения

Работа с датафреймами и панелями графиков

Автор

Анна Валяева

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

3 октября 2025 г.

Группированные датафреймы

Для работы с группами наблюдений, которые отделяются друг от друга по значениям категориального признака, удобно использовать функции group_by() для создания группированного датасета и summarise() или mutate() для подсчета каких-то значений для групп. Группировать датафрейм можно как по значениям одного столбца, так и по нескольким столбцам.

starwars %>%
  filter(homeworld %in% c("Naboo", "Tatooine")) %>%
  group_by(eye_color, species) %>%
  summarise(
    n = n(),
    max_height = max(height, na.rm = TRUE)) %>%
  mutate(max_height_of_all = max(max_height, na.rm = TRUE))
1
Отбираем из датафрейма starwars только персонажей с планет Набу и Татуин.
2
Группируем датафрейм по комбинациям значений из столбцов eye_color и species.
3
Для каждой получившейся группы строк рассчитываем их число (записываем в новый столбец n) и максимальное значение по столбцу height (записываем в столбец max_height). Все остальные столбцы, не спользуемые при группировке, исчезают.
4
Добавлям столбец max_height_of_all, в который записываем максимальное значение из столбца max_height. Поскольку, к этому моменту осталась группировка датафрейма по столбцу eye_color, в новом столбце окажутся несколько значений - для каждого цвета глаз.
# A tibble: 7 × 5
# Groups:   eye_color [5]
  eye_color species     n max_height max_height_of_all
  <chr>     <chr>   <int>      <int>             <int>
1 blue      Human       6        188               188
2 brown     Human       5        185               185
3 brown     <NA>        2        185               185
4 orange    Gungan      3        224               224
5 red       Droid       2         97                97
6 yellow    Droid       1        167               202
7 yellow    Human       2        202               202

Применив функцию group_by(), вы получаете группированный датафрейм. Обратите внимание, что функция summarise() по умолчанию снимает один уровень группировки (по одному столбцу), а mutate() не влияет на группировку совсем. Если вы группировали датафрейм, например, по двум столбцам, то после одного использования summarise() датафрейм все еще останется граппированным по первому из перечисленных в group_by() столбцов. Чтобы вручную избавиться от граппировки датафрейма, необходимо применить функцию ungroup().

starwars %>%
  filter(homeworld %in% c("Naboo", "Tatooine")) %>%
  group_by(eye_color, species) %>%
  summarise(
    n = n(),
    max_height = max(height, na.rm = TRUE)) %>%
  ungroup() %>%
  mutate(max_height_of_all = max(max_height, na.rm = TRUE))
1
Отбираем из датафрейма starwars только персонажей с планет Набу и Татуин.
2
Группируем датафрейм по комбинациям значений из столбцов eye_color и species.
3
Для каждой получившейся группы строк рассчитываем их число (записываем в новый столбец n) и максимальное значение по столбцу height (записываем в столбец max_height). Все остальные столбцы, не спользуемые при группировке, исчезают.
4
Снимаем все группировки с датафрейма.
5
Добавлям столбец max_height_of_all, в который записываем максимальное значение из столбца max_height. В этот раз группировок не осталось, поэтому будет посчитано одно значение по всему столбцу max_height.
# A tibble: 7 × 5
  eye_color species     n max_height max_height_of_all
  <chr>     <chr>   <int>      <int>             <int>
1 blue      Human       6        188               224
2 brown     Human       5        185               224
3 brown     <NA>        2        185               224
4 orange    Gungan      3        224               224
5 red       Droid       2         97               224
6 yellow    Droid       1        167               224
7 yellow    Human       2        202               224

Стоит отметить, что в новых версиях пакета {dplyr} у функций mutate() и summarise() появился параметр .by, которым можно заменить использование функции group_by(). Обратите внимание, что в данном случае при использовании функции summarise() с параметром .by в результате получится негруппированный датафрейм.

starwars %>%
  filter(homeworld %in% c("Naboo", "Tatooine")) %>%
  summarise(
    n = n(),
    max_height = max(height, na.rm = TRUE),
    .by = c(eye_color, species)) %>%
  mutate(max_height_of_all = max(max_height, na.rm = TRUE))
1
Отбираем из датафрейма starwars только персонажей с планет Набу и Татуин.
2
Задаем группировку по комбинациям значений из столбцов eye_color и species (параметр by). Для каждой получившейся группы строк рассчитываем их число (записываем в новый столбец n) и максимальное значение по столбцу height (записываем в столбец max_height).
3
Добавлям столбец max_height_of_all, в который записываем максимальное значение из столбца max_height. В этот раз группировок не осталось, поэтому будет посчитано одно значение по всему столбцу max_height.
# A tibble: 7 × 5
  eye_color species     n max_height max_height_of_all
  <chr>     <chr>   <int>      <int>             <int>
1 blue      Human       6        188               224
2 yellow    Droid       1        167               224
3 red       Droid       2         97               224
4 yellow    Human       2        202               224
5 brown     Human       5        185               224
6 orange    Gungan      3        224               224
7 brown     <NA>        2        185               224

Задача подсчета количества наблюдений по группам встречается достоточно часто, поэтому для комбинации функций group_by(groupping_var) %>% summarise(n = n()) есть более лаконичная замена - функция count(groupping_var).

starwars %>%
  filter(homeworld %in% c("Naboo", "Tatooine")) %>%
  count(eye_color, species)
1
Отбираем из датафрейма starwars только персонажей с планет Набу и Татуин.
2
Считаем количество персонажей с различными комбинациями значений из столбцов eye_color и species.
# A tibble: 7 × 3
  eye_color species     n
  <chr>     <chr>   <int>
1 blue      Human       6
2 brown     Human       5
3 brown     <NA>        2
4 orange    Gungan      3
5 red       Droid       2
6 yellow    Droid       1
7 yellow    Human       2

Длинный и широкий форматы датафреймов

Прежде чем начинать создавать график с помощью {ggplot2} важно убедиться в том, что данные, по которым планируется отрисовать график, содержатся в датафрейме в нужном формате. Для того, чтобы получилось корректно задать графические переменные с помощью функции aes(), необходимо, чтобы каждый признак, который будет “отложен” по какой-то из “осей” графика, был записан в свой отдельный столбец.

Широкий формат (wide format) датафрейма подразумевает, что несколько переменных, похожих по своей природе, разнесены по разным столбцам. Например, измерения по нескольким временным точкам записаны в разные столбцы.

# A tibble: 3 × 5
  id    tumor_size_2020 tumor_size_2021 tumor_size_2022 tumor_size_2023
  <chr>           <dbl>           <dbl>           <dbl>           <dbl>
1 Pt 1            29.1            31.7            34.3             36.2
2 Pt 2             4.20            6.61            7.51            10.8
3 Pt 3            10.6            12.5            15.5             19.8

Если такие данные нужно визуализировать с помощью {ggplot2}, то например, чтобы показать временной тренд с помощью линейной диаграммы, необходимо будет перевести датафрейм из широкого формата в длинный. В датафрейме в длинном формате (long format) названия переменных и их значения хранятся в двух столбцах: один для имен переменных, другой - для значений. Преобразование длинного формата в широкий производится с помощью функции pivot_longer() из пакета {tidyr} (входит в tidyverse). При использовании этой функции необходимо указать столбцы, которые будут переведены в “длинный” формат. Дополнительно удобно использовать параметр names_to и values_to, чтобы задать названия новых столбцов, в которые будут записаны названия переменных и их значения.

long_df <- wide_df %>%                                 
  pivot_longer(
    cols = tumor_size_2020:tumor_size_2023, 
    names_to = "time_point",
    values_to = "tumor_size")

long_df %>% head()
# A tibble: 6 × 3
  id    time_point      tumor_size
  <chr> <chr>                <dbl>
1 Pt 1  tumor_size_2020      29.1 
2 Pt 1  tumor_size_2021      31.7 
3 Pt 1  tumor_size_2022      34.3 
4 Pt 1  tumor_size_2023      36.2 
5 Pt 2  tumor_size_2020       4.20
6 Pt 2  tumor_size_2021       6.61

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

long_df %>%
  ggplot(aes(x = time_point, y = tumor_size, color = id, group = id)) +
  geom_point(size = 4) +
  geom_line(linewidth = 1) +
  geom_label(
    mapping = aes(label = id),
    data = filter(long_df, time_point == "tumor_size_2023"),
    x = 4, hjust = -0.5) +
  scale_x_discrete(labels = \(x) str_remove(x, "tumor_size_")) +
  scale_color_manual(
    values = c("slateblue", "thistle2", "burlywood2"),
    guide = "none") +
  labs(x = "Year", y = "Tumor size")
1
Используя “длинный” датафрейм, строим линейную диаграмму.
2
Добавляем идентификаторы пациентов в виде подписей у последней временной точки. На вход этой geom-функции даем фильтрованный датасет, в котором есть данные только по последней временной точке.
3
Для подписей задаем X-координату - поскольку по оси X отложен категориальный признак, то указываем просто номер нужной категории. С помощью hjust немного сдвигаем подписи вправо.
4
Преобразуем подписи по оси X - удаляем лишние символы перед указанием года обследования.
5
Задаем цветовую палитру и убираем легенду.
6
Меняем названия осей X и Y.

Перевести датафрейм из длинного формата в широкий можно с помощью функции pivot_wider(). Чтобы использовать эту функции нужно указать, из каких столбцов брать названия переменных (names_from), которые будут соответствовать названиям новых столбцов, и сами значения (values_from). При использовании этой функции важно, чтобы в датафрейме оставался столбец или несколько, значения в которых были бы уникальны для каждого наблюдения. Иначе не получится восстановить соответствие между значениями переменных внутри индивидуальных наблюдений.

long_df %>% 
  pivot_wider(names_from = "time_point", values_from = "tumor_size")
# A tibble: 3 × 5
  id    tumor_size_2020 tumor_size_2021 tumor_size_2022 tumor_size_2023
  <chr>           <dbl>           <dbl>           <dbl>           <dbl>
1 Pt 1            29.1            31.7            34.3             36.2
2 Pt 2             4.20            6.61            7.51            10.8
3 Pt 3            10.6            12.5            15.5             19.8

Панели графиков

Facets

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

sw_eye_colors <- starwars %>%
  drop_na(height, eye_color, sex) %>%
  mutate(
    fct_eye_color = fct_lump_n(eye_color, 5),
    fct_sex = fct_lump_min(sex, 10, other_level = "Other sex"),
    fct_sex = fct_reorder(fct_sex, height, .fun = max, .desc = TRUE))

sw_eye_colors_pl <- sw_eye_colors %>%
  ggplot(aes(x = height, fill = fct_eye_color)) +
  geom_density(alpha = 0.5) +
  scale_fill_manual(values = c(levels(sw_eye_colors$fct_eye_color)[1:5], "grey"))

sw_eye_colors_pl
1
Удаляем строки с пропущенными значениями в столбцах height, eye_color и sex.
2
Преобразуем цвета глаз (столбец eye_color) в факторы - записываем их в новый столбец fct_eye_color. Оставляем только 5 наиболее часто встречающихся цветов, остальные записываем в категорию Other.
3
Преобразуем информацию о поле персонажа (столбец sex) в факторы - записываем их в новый столбец fct_sex. Категории пола, встретившиеся в датасете менее 10 раз, записываем в категорию Other sex.
4
Сортируем уровни фактора, указывающего на пол персонажа, в зависимости от максимального значения роста персонажа в каждой категории пола.
5
Строим график плотности распределения роста персонажей с разделением на группы по цвету глаз. Записываем график в переменную.
6
Показываем график.

В такой ситуации разумно использовать фасеты (facets) - разбить график на несколько и собрать их в панель. Это можно сделать с помощью функции facet_wrap() из пакета {ggplot2}. Если вы хотите разбить график по двум или более категориальным переменным, то вам пригодится функция facet_grid(). У этих функций немного необычный синтаксис: столбец с категориальной переменной, по которой вы хотите разбить график на несколько, необходимо указывать через тильду ~ или задавать при помощи функции vars().

sw_eye_colors_pl +
  facet_wrap(vars(fct_eye_color)) +
  theme(legend.position = "none")
1
Разбиваем график на панель графиков с помощью facet_wrap() по категориям, соответствующим цвету глаз персонажа.
2
Убираем легенду - каждый график в панели будет подписан, поэтому диблирующая информацию легенда не нужна.

Заголовки графиков в панели настраиваются также необычным образом - с помощью объектов типа labeller. Кроме того, вам может пригодиться параметр scales, который определяет, будет ли для всех графиков использоваться единый масштаб или нет.

sw_eye_colors_pl +
  facet_grid(
    fct_eye_color ~ fct_sex,
    labeller = as_labeller(c(
      "male" = "M",
      "female" = "F",
      "Other sex" = "Other sex",
      "black" = "Black",
      "blue" = "Blue",
      "brown" = "Brown",
      "orange" = "Orange",
      "yellow" = "Yellow",
      "Other" = "Other\neye color")),
    scales = "free_x") +
  theme(
    legend.position = "none",
    strip.text = element_text(face = "bold"))
1
Разбиваем график на панель графиков с помощью facet_grid() по категориям, соответствующим цвету глаз и полу персонажа.
2
Корректируем названия категорий, отображаемые на графике.
3
Делаем шкалу по оси X индивидуальной для каждого столбца графиков.
4
Убираем легенду - каждый график в панели будет подписан, поэтому диблирующая информацию легенда не нужна.
5
Настраиваем отображение названий категорий - делаем текст жирным.

Объединение графиков в панели

Каждый график, нарисованный с помощью {ggplot2} или совместимых пакетов, представляет собой ggplot-объект. Несколько графиков можно собрать в панель, разместив графики на одной странице, согласно определенной схеме (layout). Существует несколько пакетов, которые позволяют создавать такие панели из графиков в R: {patchwork}, {cowplot} и др. Эти пакеты не входят к коллекцию tidyverse - их нужно устанавливать дополнительно.

library(patchwork)

densitypl <- starwars %>% 
  ggplot(aes(x = height)) +
  geom_density(fill = "yellowgreen")

dotpl <- starwars %>%
  filter(species %in% c("Droid", "Gungan")) %>%
  ggplot(aes(x = species, y = height, color = mass)) +
  geom_point(size = 5, alpha = 0.8) +
  scale_color_gradient(
    low = "#455e89", high = "#b7094c",
    breaks = seq(50, 150, 50),
    limits = c(30, 150))

scatterpl <- starwars %>% 
  ggplot(aes(x = height, y = mass)) +
  geom_point(size = 3, alpha = 0.8, color = "coral") +
  scale_y_log10() +
  coord_cartesian(xlim = c(0, 300))


(dotpl + scatterpl) / densitypl +
  plot_annotation(tag_levels = "A")

Задания

Потренируйтесь создавать и сохранять графики. Готовые графики показаны для ориентира, не нужно пытаться их воспроизвести до мелочей.

Задание 1

Задание 1.1

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

lab_people <- tibble(
  day = c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"),
  n_people = c(3, 1, 11, 2, 5, 1, 0))

Задание 1.2

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

lab_people2 <- tibble(
  lab = c("Great R Visualizations Lab", "The Neighbour's Lab"),
  Monday = c(3, 5),
  Tuesday = c(1, 9),
  Wednesday = c(11, 7),
  Thursday = c(2, 4),
  Friday = c(5, 5),
  Saturday = c(1, 4),
  Sunday = c(0, 1))

Задание 2

Прочитайте датасет про исчезнувшие виды растений по ссылке https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2020/2020-08-18/plants.csv.

Изучите датасет!

Что в нем есть:

  • binomial_name - название вида растения,
  • country - страна произрастания,
  • continent - континент произрастания,
  • group - таксономическая группа (Цветковое растение - Flowering plant, и т.д.),
  • year_last_seen - период, когда было замечено в природе последний раз,
  • threat_ и action_ - виды угроз и действий по сохранению вида,
  • red_list_category - категория в Красной книге: Extinct или Extinct in the Wild.

Больше информации о датасете по ссылке: https://github.com/rfordatascience/tidytuesday/blob/master/data/2020/2020-08-18/readme.md.

Постройте столбчатую диаграмму числа исчезнувших видов цветковых растений и не-цветковых растений на каждом континенте. Сохраните график в файл.

Пояснения:

  • В колонке group оставьте группу Flowering plants (цветковые растения), остальные группы растений объедините в группу Non-flowering plants (не-цветковые растения). Используйте возможности пакета forcats.
  • Посчитайте число исчезнувших видов (Extinct) по каждому континенту, в каждой таксономической группе (Flowering plants и Non-flowering plants).
  • Нарисуйте группированную столбчатую диаграмму (2 отдельных столбца, для Flowering plants и Non-flowering plants, расположенные рядом). По горизонтали расположите континенты.
  • Пусть континенты будут расположены в порядке убывания количества стран на них.
  • Осмысленно подпишите оси.
  • Перенесите легенду наверх, уберите название легенды.
  • Обозначьте Flowering plants желтым и Non-flowering plants зеленым.

Задание 3

Загрузите датасет, содержащий динамику использования сои в пищевой и непищевой промышленности за несколько последних десятков лет. Ссылка для скачивания: https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2021/2021-04-06/soybean_use.csv.

Изучите датасет! Про него можно прочитать дополнительно, например, здесь: https://github.com/rfordatascience/tidytuesday/blob/master/data/2021/2021-04-06/readme.md.

Информация про датасет:

  • entity - континент, часть континента или страна,
  • code - код страны,
  • year - год,
  • human_food - сколько сои было использованно в пищевой промышленности (в тоннах),
  • animal_feed - сколько сои было использовано в производстве кормов для животных (в тоннах),
  • processed - сколько сои было использовано в непищевой промышленности (производство биотоплива и т.д.) (в тоннах).

Отберите для анализа данные только по 6 континентам: c("Africa", "Europe", "Asia", "Northern America", "South America", "Australia & New Zealand"), за 33 года - 1981-2013. Нарисуйте столбчатую диаграмму, которая бы изображала среднее количество сои по трем типам промышленности, использованной за 33 года на всех 6 обитаемых континентах. Изобразите разброс 3/4 SD (чтобы линии разброса выровнялись со столбцами, возможно, вам понадобится параметр position = position_dodge(width = 0.9)). Отсортируйте континенты по среднему количеству сои, использованной во всех областях промышленности за исследуемый период. Логарифмируйте ось Y с помощью подходящей функции scale_y_*. Измените формат числовых подписей по оси Y.

Добавьте осознанные названия осей. Поверните названия континентов, чтобы они отображались целиком, на 45 градусов. Уберите название легенды и поменяйте цвета (например, на c("#ffb4a2", "#e5989b", "#b5838d")) и названия категорий промышленности в легенде (чтобы было более приятно читать).

Сохраните график в файл.

Задание 4

Задание 4.1

Напишите функцию, которая бы рассчитывала ядерно-цитоплазматическое соотношение (“N:C” или “karyoplasmic” ratio) по формуле \(\frac{NucleusVol}{CellVol}\), где NucleusVol - объем ядра (мкм3), CellVol - объем клетки (мкм3).

На небольшом примере продемонстрируйте работу этой функции.

Задание 4.2

Прочитайте данные по размерам ядер и клеток разных организмов из разнообразных таксономических групп, они находятся по ссылке https://raw.githubusercontent.com/kirushka/datasets/main/NC_ratio.csv. Эти данные взяты из статьи Malerba et al. 2021.

В прочитанном датафрейме 4 столбца:

  • SpeciesGroup - название таксономической группы,
  • Species - видовое название организма,
  • NucleusVol - объем ядра (мкм3),
  • CellVol - объем клетки (мкм3).

Используя функцию, которую вы создали в предыдущем задании, посчитайте ядерно-цитоплазматическое соотношение (“N:C” или “karyoplasmic” ratio) для каждого организма - запишите результат в новый столбец.

Также преобразуйте строковый столбец SpeciesGroup в столбец с факторами. Отсортируйте уровни фактора по убыванию медианного значения NC ratio.

Задание 4.3

По модифицированному датафрейму постройте точковую диаграмму, на которой по оси X отложен объем клеток, по оси Y - NC ratio, цветом показана таксономическая группа.

Добавьте на график линии тренда, построенные с помощью линейной регрессии, для каждой таксономической группы с помощью geom_smooth(method = "lm").

Чтобы использовать на графиках “математические” обозначения, греческие буквы и т.п., например, в подписях осей, можно использовать функцию expression, которой подать на вход все выражение без кавычек. Например, так: ggplot(...) + labs(x = expression(Cell~volume~(mu*m^3))).

Для настройки “tick labels”, т.е. подписей интервалов на осях, можно использовать пакет scales. Например, scale_x_log10(labels = scales::label_math(format = log10)) преобразит подписи по оси X в формат вида \(10^{log10(x)}\). Поэкспериментируйте и с другими вариантами отображения этих подписей.

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

Сохраните график в файл.

Задание 4.4

Разделите график из задания 4.3 на “панельки” с помощью facet_wrap, так чтобы каждая таксономическая группа оказалась на своем графике. Сохраните график в файл.

Задание 5

Задание 5.1

Зугрузите набор данных по сериалу “Тед Лассо”. Ссылка для скачивания: https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-09-26/richmondway.csv.

Посмотрите на этот датасет и изучите, какая информация в нем есть! Прочитать о том, что это за данные можно по ссылке.

Информация про датасет:

  • Character - персонаж (только Roy Kent),
  • Episode_order, Season, Episode, Season_Episode - номера сезонов и эпизодов сериала,
  • F_count_RK - число матерных слов (F-ck), произнесенных Роем Кентом в данном эпизоде,
  • F_count_total - число матерных слов (F-ck), произнесенных всеми персонажами в данном эпизоде,
  • cum_rk_season, cum_total_season, cum_rk_overall, cum_total_overall - кумулятивное число матерных слов (F-ck) в отдельном сезоне или суммарно за 3 сезона,
  • F_score, F_perc - отношение F_count_RK к F_count_total, то же выраженное в процентах,
  • Dating_flag - встречается ли Рой Кент в этом эпизоде с девушкой Кили,
  • Coaching_flag - работает ли Рой Кент в этом эпизоде тренером,
  • Imdb_rating - рейтинг эпизода на IMDB.

Постройте столбчатую диаграмму, которая бы отражала число произнесенных F-ck слов Роем Кентом и всеми героями сериала. Столбцы должны быть расположены рядом (группированная диаграмма). С помощью фасетов разделите график по сезонам сериала. Явным образом подпишите каждый график в панели (например, “Season 1” вместо “1”). Задайте свою цветовую палитру. Понятным образом подпишите оси. Сохраните график в файл.

Задание 5.2

Работайте с тем же набором данных и с помощью подходящих графиков попробуйте ответить на вопросы:

  • Как соотносится между собой число серий, в которых Рой Кент встречается или не встречается с Кили?
  • Как соотносится между собой число серий, в которых Рой Кент работает или не работает тренером?

Изобразите на одном графике и информацию про отношения с Кили, и информацию про тренерство. Поработайте над читаемостью графика. Сохраните график в файл.

Задание 5.3

Отрисуйте в виде ящика с усами распределение процента F-ck слов, произнесенных Роем Кентом, в течение каждого из трех сезонов. Как зависит этот процент от того, встречается ли Рой с Кили или нет?

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

Сохраните график в файл.