Система персонажей ‘Ярмарки Тщеславаия’ У.Теккерея с помощью графового метода

Автор

Смолянинова Дарья

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

20.03.2025

Ярмарка тщеславия

1 О чем

Работа посвящена исследованию системы персонажей романа У.Теккерея ‘Ярмарки Тщеславаия’ путем изучения графов, построенных на основе значений совместной встречаемости персонажей в тексте. В работе задаются два вопроса: покажет ли граф явного протагониста - Реббеку Шарп; и получится ли подтвердить сюжетные особенности романа?

2 Данные

За основу был взят текст романа на русском языке (исследование было проелано и на английском, результаты идентичны, поэтому для простоты восприятия мы оставили именно русский текст). Мы скачали его в fb2 формате с помощью бота, которого и всем советуем, конвертировали в TXT через сторонний ресурс и загрузили на гитхаб, для простоты доступа.

Скачать его можно здесь:

2.1 Подготовка размеченного текста

Текст был, разумеется, в чистом виде, без TEI-разметки, поэтому мы достали упоминания персонажей простым дедовским способом:

# загрузим все библиотеки: 
library(dplyr)
library(stringr)
library(igraph)
library(ggraph)
library(paletteer)
# URL файла на GitHub
url <- 'https://raw.githubusercontent.com/Daria-Smolyaninova/tekkereys_texts/refs/heads/main/Tekkerey.txt'

# чтение и обработка текста 
text <- readLines(url, encoding = "UTF-8") # читаем файл
text <- paste(text, collapse = " ")  # объединяем все строки в один текст

# разделим текст на предложения
sentences <- unlist(str_split(text, "[.!?]"))  #по знакам препинания

Создаем список перонажей

# список персонажей с учётом падежей (мне помог дипсик, я не писала это руками.....)

characters <- list(
  "Ребекка Шарп" = 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 для хранения взаимодействий
interactions <- data.frame(from = character(), to = character(), weight = numeric())

# анализируем каждое предложение
for (sentence in sentences) {
  # ищем, какие персонажи упоминаются в предложении
  mentioned <- names(characters)[sapply(characters, function(patterns) any(str_detect(sentence, patterns)))]
  
  # если в предложении упоминается больше одного персонажа, добавляем их взаимодействия
  if (length(mentioned) > 1) {
    for (i in 1:(length(mentioned) - 1)) {
      for (j in (i + 1):length(mentioned)) {
        # упорядочиваем имена, чтобы избежать дублирования
        pair <- sort(c(mentioned[i], mentioned[j]))
        interactions <- rbind(interactions, data.frame(from = pair[1], to = pair[2], weight = 1))
      }
    }
  }
}

# суммируем вес взаимодействий для каждой пары
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_ru <- graph_from_data_frame(interactions, directed = FALSE)
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 Добавление аттрибутов узлам

# руками добавим нашему графу осмысленности: определим пол и кол-во упоминаний в тексте
# подсчёт упоминаний каждого персонажа
mention_counts <- sapply(names(characters), function(character) {
  # все варианты имени персонажа
  patterns <- characters[[character]]
  # подсчёт упоминаний в тексте
  sum(str_count(text, fixed(patterns)))
})
# задаем вектор, в котором указан пол для каждого персонажа
sex <- c("женский", "женский", "мужской", "мужской", "мужской", "мужской", "женский", "женский", "женский", "мужской", "женский", "мужской", "мужской", "женский", "мужской", "женский", "женский", "мужской", "женский", "мужской", "женский", "женский", "мужской", "женский", "женский", "женский", "женский")
# создаём data.frame с атрибутами узлов
vertices <- data.frame(
  name = names(characters),  # имена персонажей
  gender = sex,
  mentions = mention_counts  # количество их упоминаний
) %>% 
  filter(!mentions == 0)
# обновим граф - добавим наши аттрибуты узлов
graph_ru <- graph_from_data_frame(interactions, directed = FALSE, vertices = vertices)
# выводим атрибуты
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 <- layout_with_kk(graph_ru)
# 
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 Подграф

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

# выбираем вершину "Уильям Доббин"
ego_node <- V(graph_ru)[name == "Уильям Доббин"]
# создаём ego-граф с радиусом 1 (только непосредственные соседи)
ego_graph <- make_ego_graph(graph_ru, order = 1, nodes = ego_node)[[1]]
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
# выбираем вершину "Бекки"
ego_node_becky <- V(graph_ru)[name == "Ребекка Шарп"]
# создаём ego-граф с радиусом 1 (только непосредственные соседи)
ego_graph_becky <- make_ego_graph(graph_ru, order = 1, nodes = ego_node_becky)[[1]]

print(ecount(ego_graph_becky))
[1] 91

Действиетльно, мы можем наблюдать разницу почти в 2 раза, сто довольно любопытно, если помнить, что Доббин тоже числится главным героем романа.

4 Анализ сообществ

Для выявления сообществ возпользуемся двуми разными методами.

4.1 Проба пера номер раз

# создаем объект типа IGRAPH clustering walktrap
cw <- cluster_walktrap(graph_ru)
#первые вершины и их группы
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
csg <- cluster_spinglass(graph_ru)
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 Центральность

Центральность графа нам может показать существование протоганиста. Взвешенная центральность была выбрана взвешенная центральность потому что, как раз, может характеризовать престиж героя.

#взвешенная центральность
wDegree <- strength(graph_ru)
sort(wDegree, decreasing = T)[1]
Ребекка Шарп 
         400 
# добавим в наш граф информацию о центральность
V(graph_ru)$degree <- wDegree
# зададим палитру
cols <- paletteer_d("fishualize::Acanthisthius_brasilianus")

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-ядра
# почитаем распеделение вершин по ядрам
cores_graph <- coreness(graph_ru)
#посмотрим на вершину результата
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()