library(tidyverse)
3 Визуализации
3.1 Графические системы
В R есть несколько графических систем: базовый R, lattice
и ggplot2
. В этом курсе мы будем работать лишь с ggplot2
как с наиболее современной. Если вам интересны первые две, то вы можете обратиться к версии курса 2023/2024 г. и к интерактивным урокам swirl
.
Настоящая графическая сила R – это пакет ggplot2
. В его основе лежит идея “грамматики графических элементов” Лиланда Уилкинсона (Мастицкий 2017) (отсюда “gg” в названии). С одной стороны, вы можете постепенно достраивать график, добавляя элемент за элементом (как в базовом R); с другой – множество параметров подбираются автоматически, как в Lattice.
О различных видах графиков можно почитать по ссылке. В этом уроке мы научимся строить диаграмму рассеяния (scatter plot) и столбиковую диаграмму (bar chart). Вот к чему мы стремимся.
3.2 Датасет: метаданные романов XIX-XX вв.
Знакомиться с ggplot2
мы будем на примере датасета из коллекции “NovelTM Datasets for English-Language Fiction, 1700-2009”, подготовленного Тедом Андервудом, Патриком Кимутисом и Джессикой Уайт. Они собрали метаданные о 210,266 томах художественной прозы в HathiTrust Digital Library и составили из них несколько датасетов.
Мы возьмем небольшой датасет, который содержит провернные вручную метаданные, а также сведения о категории художественной прозы для 2,730 произведений, созданных в период 1799-2009 г. (равные выборки для каждого года). Об особенностях сбора и подготовки данных можно прочитать по ссылке.
Мы попробуем проверить наблюдение, сделанное Франко Моретти в статье “Корпорация стиля: размышления о 7 тысячах заглавий (британские романы 1740-1850)” (2009 г., рус. перевод в книге “Дальнее чтение”, 2016 г.). Моретти заметил, что на протяжении XVIII-XIX вв. названия становятся короче, причем уменьшается не только среднее, но и стандартное отклонение (т.е. разброс значений). В публикации он предлагает несколько возможных объяснений для этого тренда. В датасете NovelTM есть не только романы (и не только британские), но тем более интересно будет сравнить результат.
Для этого урока данные были немного трансформированы: в частности, мы добавили столбец n_words
, в котором хранятся сведения о числе слов в названии. Файл в формате .Rdata
надо забрать из репозитория курса и прочитать в окружение.
load("../data/noveltm.Rdata")
noveltm
3.3 Диаграмма рассеяния с geom_point()
Функция ggplot()
имеет два основных аргумента: data
и mapping
. Аргумент mapping
задает эстетические атрибуты геометрических объектов. Обычно используется в виде mapping = aes(x, y)
, где aes()
означает aesthetics.
Под “эстетикой” подразумеваются графические атрибуты, такие как размер, форма или цвет. Вы не увидите их на графике, пока не добавите какие-нибудь “геомы” – геометрические объекты (точки, линии, столбики и т.п.). Эти объекты могут слоями накладываться друг на друга (Wickham и Grolemund 2016).
Диаграмма рассеяния, которая подходит для отражения связи между двумя переменными, делается при помощи geom_point()
. Попробуем настройки по умолчанию.
|>
noveltm ggplot(aes(inferreddate, n_words)) +
geom_point()
Упс. Точек очень много, и они накладываются друг на друга, так как число слов – дискретная величина. Поступим так же, как Моретти, который отразил на графике среднее для каждого года.
3.4 Среднее со stat_summary()
Для этого у нас есть два пути. Первый: обобщить данные при помощи group_by()
и summarise()
, как мы делали в прошлом уроке. Второй: воспользоваться возможностями stat_summary()
в самом ggplot2
.
|>
noveltm filter(!is.na(n_words)) |>
ggplot(aes(inferreddate, n_words)) +
geom_point(color = "grey80") +
stat_summary(fun.y = "mean", geom = "point", color = "steelblue")
Оставим только среднее и добавим линию тренда, а также уберем подпись оси X.
|>
noveltm filter(!is.na(n_words)) |>
ggplot(aes(inferreddate, n_words)) +
stat_summary(fun.y = "mean", geom = "point", color = "steelblue") +
geom_smooth(color = "tomato") +
labs(x = NULL)
Нисходящая тенденция, о которой писал Моретти, хорошо прослеживается. Но, возможно, она характерна не для всех стран?
3.5 Сравнение двух групп
В столбце nationality
хранятся данные о происхождении писателя.
|>
noveltm group_by(nationality) |>
summarise(n = n()) |>
arrange(-n)
Отберем только английских и американских авторов и сравним тенденции в этих двух группах. Категориальную переменную (национальность) в нашем случае проще всего закодировать цветом. Также добавим заголовок и подзаголовок и поменяем тему.
|>
noveltm filter(nationality %in% c("uk", "us")) |>
add_count(nationality, inferreddate) |>
# убираем 1799, для которого только одно наблюдение
filter(n > 1) |>
# код как выше, но убираем цвет для геомов
ggplot(aes(inferreddate, n_words, color = nationality)) +
stat_summary(fun.y = "mean", geom = "point") +
geom_smooth() +
# новая тема
theme_bw() +
# заголовки
labs(
title = "Title Length in UK and US",
subtitle = "NovelTM Data 1800-2009",
x = NULL
)
Для разведывательного анализа данных вполне достаточно настроек по умолчанию, но для публикации вы, вероятно, захотите вручную поправить шрифтовое и цветовое оформление.
3.6 Цветовые шкалы
Ggplot2
дает возможность легко поменять цветовую палитру и шрифтовое оформление, а также добавить фон.
Функции scale_color_brewer()
и scale_fill_brewer()
позволяют использовать специально подобранные палитры хорошо сочетаемых цветов.
Общее правило для выбора таково.
Если дана качественная переменная с упорядоченными уровнями (например, “холодный”, “теплый”, “горячий”) или количественная переменная, и необходимо подчеркнуть разницу между высокими и низкими значениями, то для визуализации подойдет последовательная шкала.
Если дана количественная переменная с осмысленным средним значением, например нулем, 50%, медианой, целевым показателем и т.п., то выбираем расходящуюся шкалу.
Если дана качественная переменная, уровни которой невозможно упорядочить (названия городов, имена авторов и т.п.), ищем качественную шкалу.
Вот основные (но не единственные!) цветовые шкалы в R. Также цвета можно задавать и вручную – по названию или коду.
# тут все по-старому
|>
noveltm filter(nationality %in% c("uk", "us")) |>
add_count(nationality, inferreddate) |>
filter(n > 1) |>
ggplot(aes(inferreddate, n_words, color = nationality)) +
stat_summary(fun.y = "mean", geom = "point") +
geom_smooth() +
theme_bw() +
labs(
title = "Title Length in UK and US",
subtitle = "NovelTM Data 1800-2009",
x = NULL
+
) # тут новое
scale_color_brewer(palette = "Dark2")
3.7 Шрифты
Пакет ggplot2
и расширения для него дают возможность использовать пользовательские шрифты.
# тут новое
library(showtext)
font_add_google("Special Elite", family = "special")
showtext_auto()
# тут почти все по-старому...
|>
noveltm filter(nationality %in% c("uk", "us")) |>
add_count(nationality, inferreddate) |>
filter(n > 1) |>
ggplot(aes(inferreddate, n_words, color = nationality)) +
stat_summary(fun.y = "mean", geom = "point") +
geom_smooth() +
theme_bw() +
labs(
title = "Title Length in UK and US",
subtitle = "NovelTM Data 1800-2009",
x = NULL
+
) scale_color_brewer(palette = "Dark2") +
# кроме этих строк, тут новое
theme(
axis.title = element_text(family = "special"),
title = element_text(family = "special")
)
3.8 Изображения
Изображения можно добавлять и в качестве фона, и вместо отдельных геомов, например точек. Поправим цвета, чтобы они лучше сочетались с цветом изображения.
library(ggimage)
<- "./images/book.jpg"
url
# почти все по-старому...
font_add_google("Special Elite", family = "special")
showtext_auto()
# ...но график сохраняем в окружение под именем g
<- noveltm |>
g filter(nationality %in% c("uk", "us")) |>
add_count(nationality, inferreddate) |>
filter(n > 1) |>
ggplot(aes(inferreddate, n_words, color = nationality)) +
stat_summary(fun.y = "mean", geom = "point") +
geom_smooth() +
theme_bw() +
labs(
title = "Title Length in UK and US",
subtitle = "NovelTM Data 1800-2009",
x = NULL
+
) # подбираем новые цвета, в т.ч. для текста
scale_color_manual("country", values = c("#A03B37", "#50684E")) +
theme(
axis.title = element_text(family = "special", color = "#8B807C"),
title = element_text(family = "special", color = "#52211E"),
axis.text = element_text(color = "#52211E"),
axis.ticks = element_blank(),
# расширяем правое поле, чтобы все влезло
plot.margin = unit(c(0.4, 3, 0.4, 0.4), "inches"), # t, r, b, l
# рамочка
panel.border = element_rect(color = "#8B807C"),
# перемещаем легенду
legend.position = c(0.8, 0.8)
)
# let the magic start!
ggbackground(g, url)
3.9 Столбиковая диаграмма
Для визуализации распределений качественных переменных подходит стобиковая диаграмма, которая наглядно показывает число наблюдений в каждой группе. В датасете NovelTM представлены следующие категории литературы.
|>
noveltm ggplot(aes(category, fill = category)) +
geom_bar()
Нас будет интересовать категория longfiction, т.к. именно сюда попадает популярный в XIX в. жанр романа. Известно, что примерно до 1840 г. почти половина романистов были женщинами, но к началу XX в. их доля снизилась (Underwood 2019, 133). Отчасти это объясняется тем, что после середины XIX в. профессия писателя становится более престижной, а его социальный статус повышается, что приводит к “джентрификации” романа. Посмотрим, что на этот счет могут сказать данные NovelTM. Переменная gender
хранит данные о гендере автора.
|>
noveltm ggplot(aes(gender, fill = gender)) +
geom_bar()
Отберем лишь одну категорию и два гендера.
<- noveltm |>
noveltm_new select(inferreddate, gender, category) |>
filter(gender != "u", category == "longfiction") |>
select(-category)
noveltm_new
Можно предположить, что соотношение мужчин и женщин в разные десятилетия менялось. Чтобы это выяснить, нам надо преобразовать данные, указав для каждого года соответствующую декаду, и посчитать число мужчин и женщин в каждой декаде.
<- noveltm_new |>
noveltm_new mutate(decade = (inferreddate %/% 10) * 10)
noveltm_new
Этого уже достаточно для визуализации, но она будет не очень наглядная.
|>
noveltm_new ggplot(aes(decade, fill = gender)) +
geom_bar(position = "dodge")
Найдем долю мужчин и женщин по декадам.
<- noveltm_new |>
noveltm_new_prop add_count(decade, name = "total") |>
select(-inferreddate) |>
add_count(gender, decade, name = "counts") |>
distinct(gender, decade, counts, total) |>
mutate(share = counts / total)
|>
noveltm_new_prop # тот же график, но...
ggplot(aes(decade, share, fill = gender)) +
# тут просим ничего не считать, а брать что дают
geom_bar(stat = "identity") +
coord_flip()
Код выше хорошо читается (и ничего плохого в нем нет), но то же самое можно сделать и более лаконично:
|>
noveltm_new ggplot(aes(decade, fill = gender)) +
# вся магия здесь
geom_bar(position = "fill") +
coord_flip()
3.10 Информативный дизайн
Поскольку нас интересует доля женщин, логично поменять группы местами.
|>
noveltm_new ggplot(aes(decade, fill = gender)) +
# меняем местами группы
geom_bar(position = position_fill(reverse = TRUE)) +
coord_flip() +
# разные мелочи
ylab(NULL) +
xlab(NULL) +
theme_void()
Также поменяем порядок, в котором идут декады (от меньшей к большей).
|>
noveltm_new ggplot(aes(decade, fill = gender)) +
geom_bar(position = position_fill(reverse = TRUE)) +
# меняем порядок лет
scale_x_reverse() +
coord_flip() +
ylab(NULL) +
xlab(NULL) +
theme_void()
Убавим цвет в мужской части диаграммы и добавим заголовки.
|>
noveltm_new ggplot(aes(decade, fill = gender)) +
geom_bar(position = position_fill(reverse = TRUE),
# обводим столбики
color = "darkred",
# убираем легенду
show.legend = FALSE) +
scale_x_reverse() +
# беремся за палитру
scale_fill_manual(values = c("lightcoral", "white")) +
coord_flip() +
theme_void() +
labs(
x = NULL,
y = NULL,
title = "Women Share per Decade",
subtitle = "NovelTM Data 1800-2009"
+
) # меняем цвет и шрифт текста
theme(text=element_text(size=12, family="serif", color = "darkred"),
axis.text = element_text(color = "darkred"))
Стоит подвинуть заголовок и убрать просветы между столбцами.
# почти ничего нового!
|>
noveltm_new ggplot(aes(decade, fill = gender)) +
geom_bar(position = position_fill(reverse = TRUE),
color = "darkred",
show.legend = FALSE,
# столбик во всю ширину
width = 10
+
) # добавляем делений на оси
scale_x_reverse(breaks = seq(1800, 2000, 10)) +
scale_fill_manual(values = c("lightcoral", "white")) +
coord_flip() +
theme_void() +
labs(
x = NULL,
y = NULL,
title = "Women Share per Decade",
subtitle = "NovelTM Data 1800-2009"
+
) theme(text=element_text(size=12, family="serif", color = "darkred"),
axis.text = element_text(color = "darkred"),
# выравниваем заголовок
plot.title.position = "plot")
3.11 Подписи с geom_text()
Функции geom_text()
можно передать таблицу, которую мы сделали выше и которая хранит сведения о доле женщин по декадам. Обратите внимание: у геомов могут быть разные данные!
<- noveltm_new_prop |>
label_data filter(gender == "f")
# тут все старое
|>
noveltm_new ggplot(aes(decade, fill = gender)) +
geom_bar(position = position_fill(reverse = TRUE),
color = "darkred",
show.legend = FALSE,
width = 10
+
) scale_x_reverse(breaks = seq(1800, 2000, 10)) +
scale_fill_manual(values = c("lightcoral", "white")) +
coord_flip() +
theme_void() +
labs(
x = NULL,
y = NULL,
title = "Women Share per Decade",
subtitle = "NovelTM Data 1800-2009"
+
) theme(text=element_text(size=12, family="serif", color = "darkred"),
axis.text = element_text(color = "darkred"),
plot.title.position = "plot") +
# тут чуть-чуть нового
geom_text(data = label_data,
aes(label = round(share, 2),
y = share),
family = "serif",
hjust = 1.2,
color = "darkred")
Отличная работа! Все сестры Бронте вами гордятся.
3.12 Экспорт графиков из среды R
Способы:
- реализованные в R драйверы стандартных графических устройств;
- функция
ggsave()
- меню программы RStudio.
# код сохранит pdf в рабочую директорию
pdf(file = "plot.pdf")
g
dev.off()
Еще один способ сохранить последний график из пакета ggplot2
.
ggsave(
filename = "plot.png",
plot = last_plot(),
device = "png",
scale = 1,
width = NA,
height = 500,
units = "px",
dpi = 300
)
# загружаем нужные пакеты
library(languageR)
library(ggplot2)
# загружаем датасет
<- oldFrenchMeta
meta
# допишите ваш код ниже
# постройте столбиковую диаграмму,
# показывающую распределение произведений по темам; цветом закодируйте жанр;
# уберите названия осей;
# поверните координатную ось;
# поменяйте тему оформления на черно-белую,
# а шрифт -- на Palatino;
# добавьте заголовок "Plot by [Your Name]"
# экспортируйте график в формате jpg
# с раширением 300 dpi;
# в названии файла должна быть
# ваша фамилия и номер группы