library(xml2)
library(dplyr)
library(purrr)
library(tidyr)
library(igraph)
library(ggraph)
library(ggplot2)Анализ сетевых сообществ персонажей романа ‘Война и мир’
Cоциальная сеть персонажей романа Л. Н. Толстого «Война и мир».
Цель сегодняшнего исследования - построить и проанализировать сеть совместных появлений персонажей по главам, где узлы представляют персонажей, а рёбра отражают факт их совместного присутствия. Постараемся выявить ключевых персонажей, их ближайшее окружение и структуры некоторых сообществ.
Сначала импортируем библиотеки
Парсим XML
doc <- read_xml("War_and_Peace.xml")
doc <- xml_ns_strip(doc) #избавляемся от проблем с namespace
volumes <- xml_find_all(doc, ".//div[@type='volume'][@n='1' or @n='2']")
chapters <- xml_find_all(volumes, ".//div[@type='chapter']")Теперь надо извлечь всех персонажей и их связи из романа
get_characters <- function(chapter) {
# персонажи из rs
rs_nodes <- xml_find_all(chapter, ".//rs[@ref]")
rs_chars <- xml_attr(rs_nodes, "ref")
# персонажи из said
said_nodes <- xml_find_all(chapter, ".//said[@who]")
said_chars <- xml_attr(said_nodes, "who")
# объединяем и чистим
chars <- unique(c(rs_chars, said_chars))
chars <- chars[!is.na(chars)]
return(chars)
}
chapter_characters <- map(chapters, get_characters)Далее, с помощью следующей функции получим непосредственно ПАРЫ
get_edges <- function(characters) {
if (length(characters) < 2) return(NULL)
combn(characters, 2, simplify = FALSE) |>
map(~ tibble(
source = .x[1],
target = .x[2]
)) |>
bind_rows()
}Наконец, посчитаем, сколько раз встречается каждая пара в тексте
edges_list <- map(chapter_characters, get_edges)
edges <- bind_rows(edges_list)
edges_weighted <- edges_list |>
bind_rows() |>
group_by(source, target) |>
summarise(weight = n(), .groups = "drop")Теперь займемся построением графов и посчитаем основные характеристики
g <- graph_from_data_frame(
edges_weighted,
directed = FALSE
)
num_nodes <- vcount(g)
num_edges <- ecount(g)
density <- edge_density(g)
components <- components(g)
num_components <- components$noИтак, на основе извлечённых данных мы построили неориентированный граф с помощью пакета igraph. Узлы графа представляют персонажей романа, а рёбра — факт их совместного появления в одной главе текста. Вес ребра отражает количество глав, в которых персонажи встречаются вместе.
Построенный граф включает 256 узлов и 2340 рёбер, отражающих совместные появления персонажей в главах. Плотность графа составляет 0.072, что указывает на достаточно плотную сеть взаимодействий. Граф состоит из 2 компонент связности, что, видимо, свидетельствует о высокой связности структуры и наличии одной доминирующей компоненты.
На следующем этапе мы рассчитаем показатели центральности. Предполагаем, что персонажи с наибольшей степенью являются наиболее социально активными, тогда как персонажи с высоким посредничеством выступают в роли «мостов» между различными группами.
# степень
V(g)$degree <- degree(g)
V(g)$degree_scaled <- scale(V(g)$degree)
# посредничество
V(g)$betweenness <- betweenness(g, normalized = TRUE)
# близость
V(g)$closeness <- closeness(g, normalized = TRUE)
as_data_frame(g, what = "vertices") |>
arrange(desc(degree)) |>
head(10) name degree degree_scaled
AndreyBolkonsky AndreyBolkonsky 210 5.796512
Tsar_Alexander_I_of_Russia Tsar_Alexander_I_of_Russia 193 5.264107
Nikolai_Rostov Nikolai_Rostov 160 4.230615
Pierre_Bezukhov Pierre_Bezukhov 135 3.447666
NatashaRostova NatashaRostova 133 3.385030
Mikhail_Ilarionovich_Kutuzov Mikhail_Ilarionovich_Kutuzov 131 3.322394
Fedor_Ivanovich_Dolokhov Fedor_Ivanovich_Dolokhov 130 3.291076
Napoleon_Bonaparte Napoleon_Bonaparte 130 3.291076
Sonya_Rostova Sonya_Rostova 114 2.789989
Count_Ilya_Rostov Count_Ilya_Rostov 111 2.696035
betweenness closeness
AndreyBolkonsky 0.13827724 0.6000000
Tsar_Alexander_I_of_Russia 0.11526402 0.6129808
Nikolai_Rostov 0.09494513 0.5916473
Pierre_Bezukhov 0.04515313 0.5172414
NatashaRostova 0.05060278 0.5579869
Mikhail_Ilarionovich_Kutuzov 0.04069718 0.5616740
Fedor_Ivanovich_Dolokhov 0.05804491 0.5717489
Napoleon_Bonaparte 0.04730317 0.5666667
Sonya_Rostova 0.02978550 0.5460385
Count_Ilya_Rostov 0.01703175 0.4990215
Теперь давайте подграф эго-сети одного персонажа, при этом применив фильтр по весу ребер, чтобы исключить самые нерепрезентативные связи!
g_filtered <- g |>
delete_edges(E(g)[weight < 3])
ego_graph <- make_ego_graph(
g_filtered,
order = 1,
nodes = "Tsar_Alexander_I_of_Russia"
)[[1]]
V(ego_graph)$is_main <- V(ego_graph)$name == "Tsar_Alexander_I_of_Russia"
ggraph(ego_graph, layout = "fr") +
geom_edge_link(aes(width = weight), alpha = 0.5) +
geom_node_point(aes(size = degree, color = is_main)) +
geom_node_text(aes(label = name), repel = TRUE, size = 3) +
scale_color_manual(values = c("steelblue", "red")) +
theme_void()Warning: The `trans` argument of `continuous_scale()` is deprecated as of ggplot2 3.5.0.
ℹ Please use the `transform` argument instead.
Warning: ggrepel: 5 unlabeled data points (too many overlaps). Consider
increasing max.overlaps
Да, мы действительно получили эго-сеть Александра I. Данный выбор обусловлен высокой степенью центральности персонажа (или просто потому что автору так захотелось). Построенный эго-граф первого порядка отражает ближайшее окружение персонажа (Андрей Болконский тут как тут) и демонстрирует его взаимодействие с ключевыми фигурами романа.
Больше визуализации!
# поиск сообществ
communities <- cluster_louvain(g)
# добавляем в граф
V(g)$community <- membership(communities)
sizes(communities)Community sizes
1 2 3 4 5
44 41 64 101 6
ggraph(g, layout = "fr") +
geom_edge_link(alpha = 0.2) +
geom_node_point(aes(color = factor(community), size = degree)) +
theme_void() +
guides(color = "none")modularity_value <- modularity(communities)Для выявления структуры сети мы применяем алгоритм Louvain, чтобы обнаружить сообщества персонажей. В результате выделено 19 сообществ различного размера.
Наиболее крупные сообщества включают 73, 43 и 27 узлов, что говорит о наличии крупных кластеров взаимодействий в соответствии с сюжетными линиями произведения (кластер для военной линии, кластер для семьи Ростовых и тд). При этом присутствуют также малые сообщества и одиночные узлы, отражающие периферийных или эпизодических персонажей.
Наконец, попробуем посчитать точки сочленения
art_points <- articulation_points(g)
length(art_points)[1] 0
V(g)$name[art_points]character(0)
В результате анализа удалось выявить целую одну точку сочленения — Андрея Болконского. Значит, удаление князя приводит к распаду сети на несколько компонент, что подчеркивает его ключевую роль в поддержании связности графа.
Таким образом, проанализиров роман-эпопею с точки зрения связей между персонажами и сетевых сообществ, мы получили достаточно явно определнные сообщества и даже смогли выделить ключевого персонажа, который выступает в качестве посредника между различными группами.
Наш построенный граф включает 256 узлов и 2340 рёбер, с умеренной плотностью (0.072) и двумя компонентами связности, поэтому можем сделать вывод о высокой интеграции персонажей в повествовании.