library(XML)
library(dplyr)
library(tidyr)
library(tidyverse)
library(ggraph)
library(igraph)
library(extrafont)
library(paletteer)
Центральные персонажи и сообщества в романе ‘Война и мир’
На материале первого тома
Библиотеки
Сбор данных
Из xml файла были собраны все реплики персонажей. У определенного узла(said) , который отвечал за это, собирались аттрибуты. В них содержалась информация о том, кто говорит реплику, а также кому персонаж ее говорит.
<- "War_and_Peace.xml"
file_path <- xmlTreeParse(file_path, useInternalNodes = TRUE)
doc <- xmlRoot(doc)
rootnode
<- getNodeSet(rootnode, "/tei:TEI//tei:text//tei:div//tei:div//tei:div//tei:p//tei:said", namespaces = c(tei = "http://www.tei-c.org/ns/1.0"))
said_list #отдельно задаем пространство имен, иначе получим пустой список
#функция, которая проходит по всем узлам и собирает заданные аттрибуты
<- function(said_node)
parse_wap
{tibble(
source = xmlGetAttr(said_node, "who"),
target = xmlGetAttr(said_node, "corresp")
)
}
#представляем в табличном формате
<- map_df(said_list, parse_wap)
wap_data
#убираем реплики бед адресата и адресанта(таких не было, но на всякий случай)
#группируем, считаем количество реплики сортируем
#у нас сразу появился аттрибут веса ребра, потому что мы посчитали количество пар адресант/адресат
<- wap_data |>
wap_data filter(target != "")|>
filter(source != "")|>
count(source, target, sort = T)|>
rename(weight = "n")
Получение графа из собранных данных
После получения графа мы можем описать его.
Граф направленный, именнованный, с весом у ребер.
451 вершина, 1124 ребра
Аттрибуты вершин:
- name - имя вершины,
- degree - вес вершины (по количеству связей одной вершины с другими)
- wDegree - взвешенная центральность
- core - принадлежность к группе по k-ядрам
- Впоследствии добавится аттрибут community
Аттрибуты ребер:
- weight - вес(по количеству взаимодействий через одно ребро)
#получаем объект igraph
<- graph_from_data_frame(wap_data)
wap_g
<- as.numeric(degree(wap_g))
d V(wap_g)$degree <- d
<- strength(wap_g)
wDegree V(wap_g)$wDegree <- wDegree #аттрибут взвешенной центральности
<- coreness(wap_g) #аттрибут для k-ядер
cores_wap V(wap_g)$core <- cores_wap
# плостность
edge_density(wap_g)
# 0.00553831
Первоначальная визуализация
Если мы попробуем визуализировать все полученные данные, то получим огромный граф, который будет нечитаем. Понимая, что многие персонажи имеют мало связей с другими, не будем их учитывать. Но даже так мы не получим много полезного, потому что еще недостаточно отфильтровали данные.
<- delete.vertices(wap_g, V(wap_g)[ degree(wap_g) <=4 ])
wap_g_1
<- paletteer_d("ButterflyColors::chorinea_licursis")
cols
set.seed(20032025)
ggraph(wap_g_1, layout = "kk", maxiter = 500) +
geom_edge_link(color = "grey70")+
geom_node_point(aes(size = degree))+
geom_node_text(aes(filter = (degree >= 10), #подписываем только те вершины,
#которые взаимодейтсвуют с 10 др. вершинами и более
label = name),
color = cols[1],
repel = TRUE) +
scale_size(guide = 'none') +
scale_fill_manual(values = cols) +
set_graph_style(
family = "Candara",
face = "plain",
#size = 10,
text_size = 7)
Визуализация графа по взвешенной центральности
Теперь попробуем получить визуализацию, где толщина ребра будет зависеть от параметра взвешенной центральности. Таким образом мы получим понимание того, какие персонажи наиболее важны. Вот 10 персонажей с наибольшим показателем взвешенной центральности (сортировка по убыванию):
- Наташа Ростова
- Пьер Безухов
- Николай Ростов
- Андрей Болконский
- Мария Болконская
- Наталья Ростова
- Соня Ростова
- Васька Денисов
- Федор Долохов
- Борис Друбецкой
#удаляем еще чуть-чуть, чтобы стало видно хоть что-то. Никаких ценных героев мы при таком подходе не теряем
<- delete.vertices(wap_g, V(wap_g)[ degree(wap_g) <=5 ])
wap_g_2
#граф с фильтрацией по взвешенной центральности
set.seed(20032025)
ggraph(wap_g_2, layout = "kk", maxiter = 500) +
#кодируем ширину ребер параметру их веса
geom_edge_link(aes(alpha = weight),
color = cols[1],
width = 0.7,
show.legend = FALSE) +
#взвешенная центральность задает размер точки
geom_node_point(aes(size = wDegree),
color = cols[2],
show.legend = FALSE) +
#подписываем только определенные вершины
geom_node_text(aes(filter = (wDegree > 70),
label = name),
color = cols[3],
repel = TRUE) +
set_graph_style(
family = "Candara",
face = "plain",
size = 9,
text_size = 9,
text_colour = "black")
На данном этапе отдельно стоит рассмотреть точки сочленения.
Если мы будем искать их по всему графу, то получим список из 75 имен. Объясняется это таким образом: при удалении точки сочленения некоторые узлы больше не могут взаимодействовать друг с другом. Поскольку в изначально графе еще не были убраны узлы с минимальным количеством связей, они и стали теми самыми точками сочленения.
articulation_points(wap_g)
+ 75/451 vertices, named, from 248c66a:
[1] Wolzogen
[2] Prince_Kozlovsky
[3] Ferapontov
[4] Yakov_Alpatych
[5] Балага
[6] Anatole_Kuragin
[7] HeleneKuragin
[8] viscount
[9] l'homme à l'esprit profond
[10] Princess_Elisabeta__Lisa__Karlovna_Bolkonskaya
+ ... omitted several vertices
Однако, если мы возьмем полученный подграф wap_g_2, в котором были удалены вершины с маленьким весом (маленьким по сравнению с остальными в романе), то получим список из одного имени:
articulation_points(wap_g_2)
+ 1/69 vertex, named, from 260f365:
[1] Mikhail_Ilarionovich_Kutuzov
Это интересный результат, потому что Кутузов объединяет военный и политический кластеры графа.
А если мы попробуем посмотреть на клики, то получим информацию о том, что самая большая клика равна 7:
clique_num(wap_g_2)
[1] 7
Вот из кого она состоит:
cliques(wap_g_2, min = 7)
[[1]]
+ 7/69 vertices, named, from 260f365:
[1] NatashaRostova AndreyBolkonsky
[3] Pierre_Bezukhov Nikolai_Rostov
[5] Sonya_Rostova Countess_Natalya_Rostova
[7] Princess_Mariya_Bolkonskaya
Визуализация k-ядер
Если с наиболее важными персонажами все стало понятно, то пришло время перейти к сообществам. В случае k-ядер мы получим группы персонажей, объединенных по весу вершины.
Наибольшее количество связей с другими вершинами имеют не только персонажи, попавшие в топ-10 по аттрибуту взвешенной центральности. Выделяются как другие члены семьи Ростовых(Вера), так и связанные с ними персонажи (Берг). Это можно объяснить большим эпизодом с именинами двух Ростовых, в котором присутсвует большое количество персонажей, которые коммуницируют между собой, например, за праздничным обедом.
<- delete.vertices(wap_g_2, V(wap_g_2)[ coreness(wap_g_2) <=6 ])
wap_g_3
#граф с информацией по k-ядрам
ggraph(wap_g_3, layout = "kk", maxiter = 500) +
# здесь кодируем вес ребер
geom_edge_link(color = "grey60",
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 >= 5,
label = name),
color = cols[3],
repel = TRUE) +
scale_color_brewer("k-ядра", type = "qual") +
theme_void()
Визуализация сообществ
Используя алгоритм под названием “главный собственный вектор”, добавляем к последнему подграфу аттрибут, который будет указывать на принадлежность вершины к определенной группе.
Алгоритм выдавал наибольшую модулряность, поэтому в итоге был использован именно он. В дополонение, этот граф направленный, что накладывает ограничения на использование определенных алгоритмов, поэтому выбор был невелик. Однако, показатель модулярности (0.2982867) говорит о том, что сообщества есть, но не очень четкие. Это также можно объяснить тем, что мы намеренно отбросили большое количество вершин на предыдущем этапе с k-ядрами.
<- delete.vertices(wap_g_3, V(wap_g_3)[ coreness(wap_g_3) <=6 ])
wap_g_4
<- cluster_leading_eigen(wap_g_4)
cev V(wap_g_4)$community <- membership(cev)
modularity(cev)
#0.2982867 -- такой показатель говорит, что сообщества есть, но не очень четкие
# граф с сообществами
ggraph(wap_g_3, layout = "kk", maxiter = 500) +
geom_edge_link(color = "grey60", alpha = 0.3, width = 0.6) +
geom_node_point(
aes(color = as.factor(community)), # Красим по сообществам
size = 3,
show.legend = TRUE
+
) geom_node_text(
aes(filter = degree >= 5, label = name),
color = "black",
repel = TRUE
+
) scale_color_brewer("Сообщество", palette = "Set1") +
theme_void()
[1] 0.2982867
Очевидно, что в группу 1 входит семья Ростовых и их окружение. Например, можно выделить Соню, которая не является дочерью Ростовых, однако с детства воспитывалась в их доме. Сюда же вошла Марья Дмитриевна Ахросимова – подруга семьи Ростовых.
Группа 2 достаточно большая. Однако все ее члены связаны или с Лысыми горами, или непосредственно с Андреем Болконским. Туда по понятным причинам входят Лиза, Николай, м-ль Бурьен (компаньонка княжны Марьи), а также несколько персонажей из военного кластера.
Группа 3 интерпретируется легче – это группа персонажей, участвующих в военных действиях. Здесь есть как Багратион и Кутузов, так и Тушин(вспоминаем эпизод с батареей Тушина), Петя Ростов, Долохов(которого разжаловали в солдаты).
В отдельную группу 4 выделяется Наполеон. Действительно, эпизоды с ним достаточно изолированные. Чего нельзя сказать про Анатоля Курагина, которому также досталась отдельная группа 5. В первом томе у него всего два больших появления: 1) в главе с медведем и квартальным 2) эпизод с м-ль Бурьен. В групповых эпизодах он действительно больше не появлялся, что, возможно, повлияло на его группу.
Группа 6 – группа участников светских вечеров. Сюда входят как А. П. Шерер, у которой проходит большой прием в самом начале романа, так и Курагины.
Группа 7 скорее всего должна была быть включена в группу 3, однако алгоритм выделил для трех персонажей отдельное место.
В последнюю группу, группу 8, входит Александр 1, что логично. Однако туда же попадает и Жерков. Предлполагаю, что в эту же группу вошел бы и Несвицкий, который не отражен на этом графе из-за недостаточного веса. Но что бы эти двое делали вместе с Александром 1 – загадка…
Таким образом, большинство персонажей группируются в соответствии с определенной логикой сюжета романа.