# загрузим все библиотеки:
library(dplyr)
library(stringr)
library(igraph)
library(ggraph)
library(paletteer)
Система персонажей ‘Ярмарки Тщеславаия’ У.Теккерея с помощью графового метода
1 О чем
Работа посвящена исследованию системы персонажей романа У.Теккерея ‘Ярмарки Тщеславаия’ путем изучения графов, построенных на основе значений совместной встречаемости персонажей в тексте. В работе задаются два вопроса: покажет ли граф явного протагониста - Реббеку Шарп; и получится ли подтвердить сюжетные особенности романа?
2 Данные
За основу был взят текст романа на русском языке (исследование было проелано и на английском, результаты идентичны, поэтому для простоты восприятия мы оставили именно русский текст). Мы скачали его в fb2 формате с помощью бота, которого и всем советуем, конвертировали в TXT через сторонний ресурс и загрузили на гитхаб, для простоты доступа.
Скачать его можно здесь:
2.1 Подготовка размеченного текста
Текст был, разумеется, в чистом виде, без TEI-разметки, поэтому мы достали упоминания персонажей простым дедовским способом:
# URL файла на GitHub
<- 'https://raw.githubusercontent.com/Daria-Smolyaninova/tekkereys_texts/refs/heads/main/Tekkerey.txt'
url
# чтение и обработка текста
<- readLines(url, encoding = "UTF-8") # читаем файл
text <- paste(text, collapse = " ") # объединяем все строки в один текст
text
# разделим текст на предложения
<- unlist(str_split(text, "[.!?]")) #по знакам препинания sentences
Создаем список перонажей
# список персонажей с учётом падежей (мне помог дипсик, я не писала это руками.....)
<- list(
characters "Ребекка Шарп" = c("Бекки", "Беккой", "Бекку", "Бекки Шарп", "мисс Шарп", "Ребекка", "Ребекке", "Ребекку", "Ребеккой", "Ребекки"),
"Эмилия Седли" = c("Эмилия", "Эмилии", "Эмилией", "Эмилию", "Эмилия Седли", "мисс Седли", "Эмилии Седли", "Эмилию Седли", "Эмилией Седли"),
"Родон Кроули" = c("Родон", "Родона", "Родоном", "Родону", "Родон Кроули", "капитан Кроули", "Родона Кроули", "Родону Кроули", "Родоном Кроули", "Родоне Кроули"),
"Джордж Осборн" = c("Джордж", "Джорджа", "Джорджу", "Джорджем", "Джордж Осборн", "мистер Осборн", "Джорджа Осборна", "Джорджу Осборну", "Джорджем Осборном"),
"Уильям Доббин" = c("Доббин", "капитан Доббин", "Уильям Доббин", "Доббина", "Доббину", "Доббином", "Доббине"),
"Сэр Питт Кроули" = c("Сэр Питт", "сэра Питта", "сэру Питту", "сэром Питтом", "сэре Питте", "Сэр Питт Кроули", "сэра Питта Кроули", "сэру Питту Кроули", "сэром Питтом Кроули", "сэре Питте Кроули"),
"Мисс Кроули (Матильда)" = c("Мисс Кроули", "Матильда", "Матильды", "Матильде", "Матильдой", "Матильду"),
"Роза Кроули" = c("Роза", "Розы", "Розе", "Розу", "Розой", "Роза Кроули", "Розы Кроули", "Розе Кроули", "Розу Кроули", "Розой Кроули"),
"Миссис Седли" = c("Миссис Седли", "миссис Седли"),
"Мистер Седли" = c("Мистер Седли", "мистер Седли", "мистера Седли", "мистеру Седли", "мистером Седли", "мистере Седли"),
"Миссис Осборн" = c("Миссис Осборн", "миссис Осборн"),
"Мистер Осборн" = c("Мистер Осборн", "мистер Осборн", "мистера Осборна", "мистеру Осборну", "мистером Осборном", "мистере Осборне"),
"Лорд Стайн" = c("Лорд Стайн", "Стайн", "лорда Стайна", "лорду Стайну", "лордом Стайном", "лорде Стайне"),
"Мисс Бриггс" = c("Мисс Бриггс", "мисс Бриггс"),
"Капитан Макмердо" = c("Капитан Макмердо", "Макмердо", "капитана Макмердо", "капитану Макмердо", "капитаном Макмердо", "капитане Макмердо"),
"Леди Джейн Шеппард" = c("Леди Джейн", "Джейн", "леди Джейн Шеппард"),
"Мисс Пинкертон" = c("Мисс Пинкертон", "мисс Пинкертон"),
"Джозеф Седли" = c("Джозеф", "Джозефе", "Джозефу", "Джозефом", "Джозеф Седли", "мистер Седли", "Джозефа Седли", "Джозефу Седли", "Джозефом Седли", "Джозефе Седли"),
"Миссис О’Дауд" = c("Миссис О’Дауд", "миссис О’Дауд"),
"Маленький Джордж Осборн" = c("Маленький Джордж", "Маленький Джордж Осборн"),
"Мисс Хоррокс" = c("Мисс Хоррокс", "мисс Хоррокс"),
"Мисс Шварц" = c("Мисс Шварц", "мисс Шварц"),
"Лорд Саутдаун" = c("Лорд Саутдаун", "Саутдаун", "лорда Саутдауна", "лорду Саутдауну", "лордом Саутдауном", "лорде Саутдауне"),
"Леди Саутдаун" = c("Леди Саутдаун", "леди Саутдаун"),
"Мисс Клоуп" = c("Мисс Клоуп", "мисс Клоуп"),
"Мисс Кроули-младшая" = c("Мисс Кроули-младшая", "мисс Кроули-младшая"),
"Миссис Фиркин" = c("Миссис Фиркин", "миссис Фиркин")
)
# создаем пустой data.frame для хранения взаимодействий
<- data.frame(from = character(), to = character(), weight = numeric())
interactions
# анализируем каждое предложение
for (sentence in sentences) {
# ищем, какие персонажи упоминаются в предложении
<- names(characters)[sapply(characters, function(patterns) any(str_detect(sentence, patterns)))]
mentioned
# если в предложении упоминается больше одного персонажа, добавляем их взаимодействия
if (length(mentioned) > 1) {
for (i in 1:(length(mentioned) - 1)) {
for (j in (i + 1):length(mentioned)) {
# упорядочиваем имена, чтобы избежать дублирования
<- sort(c(mentioned[i], mentioned[j]))
pair <- rbind(interactions, data.frame(from = pair[1], to = pair[2], weight = 1))
interactions
}
}
}
}
# суммируем вес взаимодействий для каждой пары
<- interactions %>%
interactions group_by(from, to) %>%
summarise(weight = sum(weight), .groups = 'drop')
У нас получился датафрейм, который содержит информацию о взаимодействиях персонажей внутри текста
interactions
# A tibble: 95 × 3
from to weight
<chr> <chr> <dbl>
1 Джозеф Седли Джордж Осборн 9
2 Джозеф Седли Миссис Осборн 2
3 Джозеф Седли Миссис О’Дауд 1
4 Джозеф Седли Миссис Седли 1
5 Джозеф Седли Мистер Осборн 2
6 Джозеф Седли Мистер Седли 39
7 Джозеф Седли Ребекка Шарп 26
8 Джозеф Седли Уильям Доббин 6
9 Джозеф Седли Эмилия Седли 18
10 Джордж Осборн Леди Джейн Шеппард 7
# ℹ 85 more rows
2.2 Создание графа
<- graph_from_data_frame(interactions, directed = FALSE) graph_ru
graph_ru
IGRAPH 85a6712 UNW- 21 95 --
+ attr: name (v/c), weight (e/n)
+ edges from 85a6712 (vertex names):
[1] Джозеф Седли --Джордж Осборн
[2] Джозеф Седли --Миссис Осборн
[3] Джозеф Седли --Миссис О’Дауд
[4] Джозеф Седли --Миссис Седли
[5] Джозеф Седли --Мистер Осборн
[6] Джозеф Седли --Мистер Седли
[7] Джозеф Седли --Ребекка Шарп
[8] Джозеф Седли --Уильям Доббин
+ ... omitted several edges
2.3 Характеристики графа
Тип графа:
print(typeof(graph_ru)) #внутренне это список
[1] "list"
Класс графа:
class(graph_ru)
[1] "igraph"
Число вершин (персонажей):
print(vcount(graph_ru))
[1] 21
Число рёбер (взаимодействий):
print(ecount(graph_ru))
[1] 95
Плотность графа:
print(edge_density(graph_ru))
[1] 0.452381
Диаметр графа:
print(diameter(graph_ru))
[1] 9
Среднее расстояние между вершинами:
print(mean_distance(graph_ru))
[1] 3.47619
2.4 Добавление аттрибутов узлам
# руками добавим нашему графу осмысленности: определим пол и кол-во упоминаний в тексте
# подсчёт упоминаний каждого персонажа
<- sapply(names(characters), function(character) {
mention_counts # все варианты имени персонажа
<- characters[[character]]
patterns # подсчёт упоминаний в тексте
sum(str_count(text, fixed(patterns)))
})# задаем вектор, в котором указан пол для каждого персонажа
<- c("женский", "женский", "мужской", "мужской", "мужской", "мужской", "женский", "женский", "женский", "мужской", "женский", "мужской", "мужской", "женский", "мужской", "женский", "женский", "мужской", "женский", "мужской", "женский", "женский", "мужской", "женский", "женский", "женский", "женский")
sex # создаём data.frame с атрибутами узлов
<- data.frame(
vertices name = names(characters), # имена персонажей
gender = sex,
mentions = mention_counts # количество их упоминаний
%>%
) filter(!mentions == 0)
# обновим граф - добавим наши аттрибуты узлов
<- graph_from_data_frame(interactions, directed = FALSE, vertices = vertices)
graph_ru # выводим атрибуты
print(vertex_attr(graph_ru))
$name
[1] "Ребекка Шарп" "Эмилия Седли"
[3] "Родон Кроули" "Джордж Осборн"
[5] "Уильям Доббин" "Сэр Питт Кроули"
[7] "Мисс Кроули (Матильда)" "Роза Кроули"
[9] "Миссис Седли" "Мистер Седли"
[11] "Миссис Осборн" "Мистер Осборн"
[13] "Лорд Стайн" "Капитан Макмердо"
[15] "Леди Джейн Шеппард" "Мисс Пинкертон"
[17] "Джозеф Седли" "Миссис О’Дауд"
[19] "Маленький Джордж Осборн" "Лорд Саутдаун"
[21] "Леди Саутдаун"
$gender
[1] "женский" "женский" "мужской" "мужской" "мужской" "мужской" "женский"
[8] "женский" "женский" "мужской" "женский" "мужской" "мужской" "мужской"
[15] "женский" "женский" "мужской" "женский" "мужской" "мужской" "женский"
$mentions
[1] 1277 900 899 1325 890 191 38 32 81 111 82 149 308 45 190
[16] 62 203 74 3 89 45
# визуализируем
plot(graph_ru,
vertex.color = ifelse(V(graph_ru)$gender == "женский", "pink", "lightblue"), # герои Пелевина меня бы закидали камнями сейчас за эту отсталую гендерную детерминацию
vertex.label.dist = 1,
edge.curved = 0.2,
vertex.size = V(graph_ru)$mentions / max(V(graph_ru)$mentions) * 20) # размер вершины зависит от количества упоминаний
2.5 Настройки красоты
# был выбран алгоритм layout_with_kk, потому что он основывается на расстояниях между вершинами
#(а это может быть полезно визуализировать именно на основе худ.текста)
<- layout_with_kk(graph_ru)
layout #
set.seed(4092348)
ggraph(graph_ru, layout = layout) +
geom_edge_link(edge_colour = "gray", edge_alpha = 0.7) +
geom_node_point(aes(color = gender, size = mentions)) +
geom_node_text(aes(label = name), repel = TRUE, size = 3) +
theme_void() +
theme(legend.position = "bottom") +
labs(title = "Граф взаимодействий персонажей 'Ярмарки Тщеславия'",
color = "Пол",
size = "Количество упоминаний")
3 Подграф
В романа уникальным персонажем (на основании читательского опыта) кажется Уильям Доббин - простодушный капитан, служивший пол жизни, а другую половину - любивший Эмилию Седли. Он, хоть и является центральным героем, не может похвастаться большим кругом знакомств, как, например, Бекки. Хочется это проверить с помощью графа.
# выбираем вершину "Уильям Доббин"
<- V(graph_ru)[name == "Уильям Доббин"]
ego_node # создаём ego-граф с радиусом 1 (только непосредственные соседи)
<- make_ego_graph(graph_ru, order = 1, nodes = ego_node)[[1]]
ego_graph set.seed(28476)
# Визуализируем
plot(ego_graph,
vertex.color = ifelse(V(ego_graph)$gender == "женский", "pink", "lightblue"),
vertex.size = V(ego_graph)$mentions / max(V(ego_graph)$mentions) * 20,
edge.arrow.size = 0.5,
vertex.label.dist = 1,
edge.curved = 0.2,
main = "Ego-граф для Доббина")
Посчитаем кол-во взаимодействий Доббина и Бекки:
# Доббин
print(ecount(ego_graph))
[1] 55
# выбираем вершину "Бекки"
<- V(graph_ru)[name == "Ребекка Шарп"]
ego_node_becky # создаём ego-граф с радиусом 1 (только непосредственные соседи)
<- make_ego_graph(graph_ru, order = 1, nodes = ego_node_becky)[[1]]
ego_graph_becky
print(ecount(ego_graph_becky))
[1] 91
Действиетльно, мы можем наблюдать разницу почти в 2 раза, сто довольно любопытно, если помнить, что Доббин тоже числится главным героем романа.
4 Анализ сообществ
Для выявления сообществ возпользуемся двуми разными методами.
4.1 Проба пера номер раз
# создаем объект типа IGRAPH clustering walktrap
<- cluster_walktrap(graph_ru)
cw #первые вершины и их группы
membership(cw) %>% head()
Ребекка Шарп Эмилия Седли Родон Кроули Джордж Осборн Уильям Доббин
1 3 1 3 3
Сэр Питт Кроули
1
#визуализация
par(mar = rep(0, 4))
plot(cw, graph_ru)
# Модулярность
modularity(cw)
[1] 0.2696773
5 Проба пера номер 2
# создаем объект типа IGRAPH ccluster_spinglass
<- cluster_spinglass(graph_ru)
csg membership(csg) |> head()
Ребекка Шарп Эмилия Седли Родон Кроули Джордж Осборн Уильям Доббин
1 5 1 5 5
Сэр Питт Кроули
1
#первые вершины и их группы
membership(csg) |> head()
Ребекка Шарп Эмилия Седли Родон Кроули Джордж Осборн Уильям Доббин
1 5 1 5 5
Сэр Питт Кроули
1
#визуализация
par(mar = rep(0, 4))
plot(csg, graph_ru)
# Модулярность
modularity(csg)
[1] 0.03251178
Модулярность различается не сильно. Она колеблется в пределах значения 0.3, поэтому можно говорить о достаточно плотных связях между узлами внутри модулей, и средние (не слабые) связи между узлами в различных модулях. В контексте произведения можно сказать, что персонажи в целом довольно зависимы друг от друга и в тексте нет выделенных обособленных групп. Дейсвительно в ходе сюжета мы в основном наблюдаем судебные перепитии именно основных героев. В ходе же развертывания сюжета поялвяются и отдельные кластеры, которые, тем не менее, все еще достаточно сильно связаны с основными группами.
Кроме того, ярко выделяется и влияние сюжетных особенностей на структуру графа: так, в романе ярко разделяется жизнь Бекки до и после замужетсва,что мы видим и на представленных графах(вы можете обратить внимание только на фамилии: Осборн и Седли, с одной стороны, и Кроули и Стайн, с другой, Бекки же на обоих графах располагается примерно в центре)
6 Исследование аттрибутов узлов
6.1 Центральность
Центральность графа нам может показать существование протоганиста. Взвешенная центральность была выбрана взвешенная центральность потому что, как раз, может характеризовать престиж героя.
#взвешенная центральность
<- strength(graph_ru)
wDegree sort(wDegree, decreasing = T)[1]
Ребекка Шарп
400
# добавим в наш граф информацию о центральность
V(graph_ru)$degree <- wDegree
# зададим палитру
<- paletteer_d("fishualize::Acanthisthius_brasilianus")
cols
set.seed(20032025)
# и визуализируем
ggraph(graph_ru, layout = "kk", maxiter = 500) +
# кодируем вес ребер
geom_edge_link(aes(alpha = weight),
color = cols[5],
width = 0.9,
show.legend = FALSE) +
# взвешенная центральность
geom_node_point(aes(size = degree),
color = cols[2],
show.legend = FALSE) +
geom_node_text(aes(filter = (degree > 10),
label = name),
color = cols[1],
repel = TRUE) +
theme_graph()
Действительно, мы можем наблюдать преобладание показателя центральности именно у Бекки Шарп, что подтверждает читательское и авторское понимание роли героини в художественном мире романа.
Кроме того, на графике виден и “костяк” персонажей. Основных героев, развитие характера которых - основной сюжетный двигатель романа.
7 Характеристики централизация графа
Точки сочленения в нашем контексте, показывают, героев, которые к тому же связаны с большинством других. Такие герои не всегда могут повторять “центральных” персонажей, рассмотреных выше.
# централизация для графа
centr_clo(graph_ru)$centralization
[1] 0.5638574
# точка сочленения
articulation_points(graph_ru)
+ 1/21 vertex, named, from 9ccc76f:
[1] Джордж Осборн
# социальная сплоченость (клики)
clique_num(graph_ru)
[1] 8
largest_cliques(graph_ru)
[[1]]
+ 8/21 vertices, named, from 9ccc76f:
[1] Леди Джейн Шеппард Ребекка Шарп Мистер Осборн Миссис Осборн
[5] Уильям Доббин Джордж Осборн Родон Кроули Эмилия Седли
[[2]]
+ 8/21 vertices, named, from 9ccc76f:
[1] Джозеф Седли Ребекка Шарп Уильям Доббин Джордж Осборн Эмилия Седли
[6] Мистер Осборн Мистер Седли Миссис Седли
[[3]]
+ 8/21 vertices, named, from 9ccc76f:
[1] Джозеф Седли Ребекка Шарп Уильям Доббин Джордж Осборн Эмилия Седли
[6] Мистер Осборн Мистер Седли Миссис Осборн
Действительно, таким героем в романе является уже не Бекки, а Джордж Осборн, известный, например, своей болтливостью.
Клика же показывает максимально полный подграф, который на нашем корпусе представлен также почти всеми основными героями романа.
8 K-ядра
Именно на этом графе отчетливее всего видны главные герои романа:
# K-ядра
# почитаем распеделение вершин по ядрам
<- coreness(graph_ru)
cores_graph #посмотрим на вершину результата
head(cores_graph)
Ребекка Шарп Эмилия Седли Родон Кроули Джордж Осборн Уильям Доббин
7 7 7 7 7
Сэр Питт Кроули
6
# добавим в граф значения ядра каждой вершины
V(graph_ru)$core <- cores_graph
set.seed(20032025)
#визуализируем
ggraph(graph_ru, layout = "kk", maxiter = 500) +
geom_edge_link(color = cols[3],
alpha = 0.3,
width = 0.6) +
#
geom_node_point(aes(color = as.factor(core)),
size = 3,
show.legend = TRUE) +
#
geom_node_text(aes(filter = degree > 100,
label = name),
color = cols[3],
repel = TRUE) +
scale_color_brewer("k-ядра", type = "qual") +
theme_void()