Чтобы жить честно, надо рваться, путаться, биться, ошибаться, начинать и опять бросить, и опять начинать, и опять бросать, и вечно бороться и лишаться. А спокойствие — душевная подлость.
– из романа «Война и мир» Л. Н. Толстой
В данной работе исследуются взаимодействия персонажей романа-эпопеи «Война и мир» Л.Н. Толстого и визуализируются с помощью сети графов. Для анализа выбраны первые два тома — они охватывают ключевые события и основной круг персонажей, при этом сохраняя управляемый размер сети.
Выявить основных персонажей первого и второго томов романа с целью определить, какие из них имеют наибольшее влияние в сети взаимодействий, какие социальные группы они образуют и кто выступает связующим звеном между разными социальными кругами.
library(xml2)
library(tidyverse)
library(igraph)
library(ggraph)
library(visNetwork)
Первым делом нужно импортировать файл в формате .xml и
определить пространство имен. Затем создадим функцию, которая принимает
узел тома и возвращает таблицу всех реплик внутри него.
xml_find_all ищет все теги xml_attr извлекает атрибуты who
(говорящий), corresp (адресат) и speech_text
(текст).
doc <- read_xml("/Users/az/Documents/R scripts/War_and_Peace.xml")
ns <- xml_ns_rename(xml_ns(doc), d1 = "tei")
# определяем функцию
extract_speeches <- function(vol_node) {
saids <- xml_find_all(vol_node, ".//*[local-name()='said']")
tibble(
speaker = xml_attr(saids, "who"),
addressee = xml_attr(saids, "corresp"),
text = xml_attr(saids, "speech_text")
)
}
# узлы томов
text_node <- xml_find_first(doc, "//*[local-name()='text']")
vol1 <- xml_children(text_node)[[1]]
vol2 <- xml_children(text_node)[[2]]
# вызываем функцию через map
speeches <- map(list(vol1, vol2), extract_speeches) |>
bind_rows() |>
separate_rows(speaker, sep = "[,;]") |>
separate_rows(addressee, sep = "[,;]") |>
mutate(
speaker = str_squish(speaker),
addressee = str_squish(addressee)
) |>
filter(!is.na(speaker), !is.na(addressee), speaker != "", addressee != "")
# Агрегирование ребер
edges <- speeches |>
count(speaker, addressee, name = "weight") |>
filter(speaker != addressee, weight >= 2)
nrow(speeches)
nrow(edges)
## [1] 3644
## [1] 380
В результате парсинга получено 3644 реплики из первых двух томов. После агрегирования и фильтрации незначимых связей (вес < 2) осталось 380 уникальных пар персонажей, которые и составят ребра графа.
# Персонажи из XML
persons <- xml_find_all(doc, "//*[local-name()='person']")
characters <- tibble(
id = xml_attr(persons, "xml:id"),
name = xml_text(xml_find_first(persons, ".//*[local-name()='persName']"), trim = TRUE)
) |>
filter(!is.na(id))
# Узлы графа
all_nodes <- union(edges$speaker, edges$addressee)
nodes <- tibble(id = all_nodes) |>
left_join(characters, by = "id") |>
mutate(name = coalesce(name, id))
# Граф
g <- graph_from_data_frame(
d = edges,
vertices = nodes,
directed = FALSE
)
cat("Тип:", ifelse(is_directed(g), "ориентированный", "неориентированный"), "\n")
cat("Узлов:", vcount(g), "\n")
cat("Рёбер:", ecount(g), "\n")
cat("Компонент:", components(g)$no, "\n")
cat("Плотность:", round(edge_density(g), 4), "\n")
cat("Средний путь:", round(mean_distance(g), 2), "\n")
cat("Диаметр:", diameter(g), "\n")
## Тип: неориентированный
## Узлов: 128
## Рёбер: 380
## Компонент: 2
## Плотность: 0.0468
## Средний путь: 10.01
## Диаметр: 33
Выводы
Граф построен с помощью функции graph_from_data_frame из
пакета igraph. Узлами являются персонажи, ребрами — факты
диалогического взаимодействия между ними. Граф неориентированный:
направление реплики (кто говорил первым) не учитывается, важен сам факт
разговора. Атрибут ребер weight отражает суммарное
количество реплик между парой персонажей.
# Проверяем атрибуты ребер
edge_attr_names(g)
## [1] "weight"
# Топ-10 самых сильных связей
edges |>
arrange(desc(weight)) |>
head(10)
## # A tibble: 10 × 3
## speaker addressee weight
## <chr> <chr> <int>
## 1 NatashaRostova Nikolai_Rostov 123
## 2 AndreyBolkonsky Pierre_Bezukhov 92
## 3 NatashaRostova Sonya_Rostova 91
## 4 Nikolai_Rostov NatashaRostova 91
## 5 Telyanin Nikolai_Rostov 89
## 6 NatashaRostova Countess_Natalya_Rostova 83
## 7 Boris_Drubetskoy Nikolai_Rostov 77
## 8 Pierre_Bezukhov AndreyBolkonsky 72
## 9 Sonya_Rostova NatashaRostova 57
## 10 Nikolai_Rostov Vasily__Vasska__Denisov 51
Выводы
Единственным атрибутом ребер является weight — вес
связи, равный количеству реплик между двумя персонажами в первых двух
томах. Чем больше диалогов между персонажами, тем сильнее связь. Этот
показатель отражает интенсивность взаимодействия: редкие случайные
разговоры отличаются от устойчивых отношений между героями.
Связи с весом меньше 2 были исключены как незначимые — единственная реплика может быть случайным упоминанием, а не реальным взаимодействием. Наиболее сильные связи ожидаемо приходятся на главных героев и членов одних семей: Николай Ростов и Наташа Ростова, Наташи и Соня, Андрей Болконский и Пьер Безухов. Это подтверждает, что вес ребер осмысленно отражает близость персонажей в тексте.
Кто из персонажей самый важный и активный в первых двух томах?
V(g)$degree <- degree(g)
V(g)$strength <- strength(g, weights = E(g)$weight)
V(g)$betweenness <- betweenness(g, weights = E(g)$weight, normalized = TRUE)
V(g)$closeness <- closeness(g, weights = E(g)$weight, normalized = TRUE)
V(g)$pagerank <- page_rank(g, weights = E(g)$weight)$vector
tibble(
name = V(g)$name,
degree = V(g)$degree,
strength = V(g)$strength,
betweenness = round(V(g)$betweenness, 4)
) |>
arrange(desc(strength)) |>
head(10)
## # A tibble: 10 × 4
## name degree strength betweenness
## <chr> <dbl> <dbl> <dbl>
## 1 Nikolai_Rostov 66 873 0.314
## 2 NatashaRostova 38 712 0.193
## 3 AndreyBolkonsky 62 677 0.337
## 4 Pierre_Bezukhov 43 539 0.158
## 5 Countess_Natalya_Rostova 18 224 0.0023
## 6 Princess_Mariya_Bolkonskaya 15 202 0.018
## 7 Boris_Drubetskoy 15 191 0.0292
## 8 Sonya_Rostova 5 190 0
## 9 Vasili_Kuragin 18 179 0.0345
## 10 Prince_Nikolay_Bolkonsky 20 172 0.0684
Выводы
Каждому узлу графа присвоены несколько показателей важности. Все метрики вычисляются с учетом весов ребер — то есть не просто факт связи, а ее интенсивность.
В топе-10 персонажей лидируют Николай Ростов, Наташа Ростова и Андрей
Болконский. При этом по метрике betweenness
(посредничество) Андрей Болконский опережает Николая (0.337 против
0.313), то есть Болконский говорит реже, но занимает более
стратегическую позицию в сети, соединяя светское общество, военную среду
и семью Болконских.
ego_node <- which(V(g)$name == "AndreyBolkonsky")
ego_g <- make_ego_graph(g, order = 1, nodes = ego_node)[[1]]
ggraph(ego_g, layout = "star", center = which(V(ego_g)$name == "AndreyBolkonsky")) +
geom_edge_link(aes(width = weight), alpha = 0.4, colour = "steelblue") +
geom_node_point(aes(size = strength, colour = name == "AndreyBolkonsky")) +
geom_node_text(aes(label = name), repel = TRUE, size = 3) +
scale_edge_width(range = c(0.3, 4)) +
scale_colour_manual(values = c("TRUE" = "#d62728", "FALSE" = "#1f77b4"), guide = "none") +
scale_size(range = c(3, 12)) +
labs(
title = "Ego-граф Андрея Болконского"
) +
theme_graph(base_family = "sans")
Выводы
Для построения подграфа выбран метод ego-графа 1 порядка (только прямые соседи) с центром в узле Андрея Болконского. Был выбран именно Андрей Болконский, так как его показатель betweenness (0.337) наиболее высокий среди остальных персонажей. Именно он чаще других оказывается на кратчайших путях между разными группами сети. Из этого можно сделать вывод, что он связывает светское общество Петербурга (Шерер, Курагины), свою семью и военную среду. Ego-граф позволяет наглядно увидеть это окружение.
Метод ego-графа предпочтителен в данном случае перед k-core или фильтрацией, поскольку позволяет сосредоточиться на конкретном персонаже и его непосредственном социальном круге, не теряя информацию о структуре связей внутри этого круга.
# Готовим узлы
vis_nodes <- tibble(
id = V(g)$name,
label = V(g)$name,
value = V(g)$strength, # размер узла
color.background = scales::col_numeric(
palette = c("#d4e8f7", "#08306b"),
domain = range(V(g)$betweenness)
)(V(g)$betweenness),
title = paste0( # всплывающая подсказка
"<b>", V(g)$name, "</b><br>",
"Degree: ", V(g)$degree, "<br>",
"Strength: ", V(g)$strength, "<br>",
"Betweenness: ", round(V(g)$betweenness, 3)
)
)
# Готовим ребра
vis_edges <- tibble(
from = edges$speaker,
to = edges$addressee,
value = edges$weight, # толщина ребра
title = paste0("Реплик: ", edges$weight)
)
# Визуализация
visNetwork(vis_nodes, vis_edges,
main = "Сеть персонажей «Войны и мира» (1 и 2 том)") |>
visNodes(
scaling = list(min = 10, max = 50),
font = list(size = 14)
) |>
visEdges(
scaling = list(min = 1, max = 8),
smooth = list(type = "continuous")
) |>
visOptions(
highlightNearest = list(enabled = TRUE, degree = 1, hover = TRUE),
nodesIdSelection = TRUE # выпадающий список для поиска персонажа
) |>
visLayout(randomSeed = 42) |>
visPhysics(
solver = "forceAtlas2Based",
forceAtlas2Based = list(gravitationalConstant = -50),
timestep = 0.4,
stabilization = list(
enabled = TRUE,
iterations = 200
)
)
Выводы
Основной граф визуализирован интерактивно с помощью пакета
visNetwork. Он показывает несколько персонажей с высоким
betweenness — Николай Ростов, Андрей Болконский, Наташа Ростова и Пьер
Безухов. Размер узла отражает взвешенную степень персонажа (strength) —
чем больше реплик, тем крупнее узел. Цвет узла кодирует показатель
посредничества (betweenness) по градиенту от светло-голубого (низкое) до
темно-синего (высокое). Толщина ребра соответствует весу связи. При
наведении на узел отображается всплывающая подсказка с основными
метриками персонажа, при клике подсвечиваются только его прямые связи.
Выпадающий список позволяет найти любого персонажа по имени.
set.seed(42)
communities <- cluster_louvain(g, weights = E(g)$weight)
V(g)$community <- membership(communities)
cat("Число сообществ:", length(communities), "\n")
cat("Модулярность:", round(modularity(communities), 4), "\n")
sizes(communities)
## Число сообществ: 8
## Модулярность: 0.4577
## Community sizes
## 1 2 3 4 5 6 7 8
## 9 15 44 43 3 10 2 2
Выводы
Модулярность графа составляет 0.458. Это умеренно высокий показатель, который свидетельствует о том, что разбиение на классы хорошее. Алгоритм Лувена выделил 8 сообществ, два крупнейших из которых насчитывают 43 и 44 персонажа соответственно. Полученное значение модулярности подтверждает, что персонажи «Войны и мира» действительно общаются преимущественно внутри своих социальных групп, а не равномерно по всей сети.
# Цвета для 8 сообществ
comm_colors <- c("#2c7bb6","#d7191c","#1a9641","#fdae61",
"#abd9e9","#a6d96a","#762a83","#e66101")
vis_nodes <- tibble(
id = V(g)$name,
label = V(g)$name,
value = V(g)$strength,
group = V(g)$community,
color.background = comm_colors[V(g)$community],
color.border = "white",
title = paste0(
"<b>", V(g)$name, "</b><br>",
"Сообщество: ", V(g)$community, "<br>",
"Degree: ", V(g)$degree, "<br>",
"Strength: ", V(g)$strength, "<br>",
"Betweenness: ", round(V(g)$betweenness, 3)
)
)
vis_edges <- tibble(
from = edges$speaker,
to = edges$addressee,
value = edges$weight,
title = paste0("Реплик: ", edges$weight),
color = "rgba(150,150,150,0.3)"
)
visNetwork(vis_nodes, vis_edges,
main = "Сообщества персонажей «Войны и мира» (1 и 2 том)") |>
visNodes(
scaling = list(min = 10, max = 50),
font = list(size = 14, color = "black"),
borderWidth = 2
) |>
visEdges(
scaling = list(min = 1, max = 6),
smooth = list(type = "continuous")
) |>
visOptions(
highlightNearest = list(enabled = TRUE, degree = 1, hover = TRUE),
nodesIdSelection = TRUE,
selectedBy = list(variable = "group", main = "Выбрать сообщество")
) |>
visLegend() |>
visLayout(randomSeed = 42) |>
visPhysics(
solver = "forceAtlas2Based",
forceAtlas2Based = list(gravitationalConstant = -80),
timestep = 0.2,
stabilization = list(
enabled = TRUE,
iterations = 200
)
)
Выводы
Для выявления сообществ применен алгоритм Лувена. Алгоритм объединяет узлы в группы таким образом, чтобы связи внутри групп были плотнее, чем между ними. Louvain хорошо масштабируется и учитывает веса ребер, что важно в данном случае — интенсивность диалогов различается существенно.
Алгоритм выделил 8 сообществ. Два крупнейших насчитывают по 43 персонажа каждое. Интерактивная визуализация показывает, что Ростовы и их ближайшее окружение образуют отдельный кластер (оранжевый), поскольку их диалоги сосредоточены внутри семьи. Болконский и Безухов попадают в одно сообщество (зеленый) — они много общаются друг с другом и с одними и теми же людьми из военной и светской среды Петербурга. Курагины образуют отдельную группу вместе с персонажами светского общества.
ap <- articulation_points(g)
cat("Число точек сочленения:", length(ap), "\n")
cat("Точки сочленения:", V(g)$name[ap], "\n")
cliques_all <- cliques(g, min = 3)
cat("Клик размером >= 3:", length(cliques_all), "\n")
largest <- largest_cliques(g)
cat("Размер крупнейшей клики:", length(largest[[1]]), "\n")
cat("Крупнейшая клика:", V(g)$name[largest[[1]]], "\n")
## Число точек сочленения: 20
## Точки сочленения: Fedor_Ivanovich_Dolokhov Count_Ilya_Rostov NatashaRostova old priest Princess_Anna_Mikhaylovna_Drubetskaya Pierre_Bezukhov Uncle regimental commander адъютант Zherkov Princess_Mariya_Bolkonskaya Prince_Nikolay_Bolkonsky Prince_Nesvitsky Nikolai_Rostov AndreyBolkonsky Prince_Kozlovsky Napoleon_Bonaparte Mikhail_Ilarionovich_Kutuzov Anatole_Kuragin Vasili_Kuragin
## Клик размером >= 3: 225
## Размер крупнейшей клики: 5
## Крупнейшая клика: Marya_Lvovna_Karagina Count_Ilya_Rostov Princess_Anna_Mikhaylovna_Drubetskaya NatashaRostova Countess_Natalya_Rostova
Выводы
В графе найдено 20 точек сочленения — узлов, удаление которых разрывает граф на отдельные компоненты. Среди них все главные герои: Андрей Болконский, Николай Ростов, Наташа Ростова, Пьер Безухов, а также Кутузов и Наполеон. Это подтверждает их роль структурных мостов между разными социальными группами романа.
Найдено 225 клик размером не менее 3 узлов. Крупнейшая клика включает 5 персонажей: Наташу Ростову, графиню Ростову, графа Ростова, княжну Друбецкую и Марью Львовну Карагину — это круг московского дворянства, связанный семейными и светскими отношениями.
Сетевой анализ первых двух томов «Войны и мира» позволил выявить структуру взаимодействий между персонажами и определить ключевых акторов романа. Граф включает 128 персонажей и 380 связей. Плотность сети 0.047 характерна для литературных текстов, так как чаще персонажи общаются внутри своего социального круга, а не равномерно по всей сети.
Показатель посредничества betweenness оказался одним из
наиболее информативных в данном анализе. Он позволил выявить персонажей,
которые не просто много говорят, но и соединяют разные социальные
группы.