library(tidyverse)
library(xml2)
library(igraph)
library(ggraph)
В этой работе строится граф персонажей романа «Война и мир». Связь между двумя персонажами определяется по факту прямой речи: персонажи считаются связанными, если они вступают в диалог.
Вес ребра показывает, сколько раз один персонаж обращается к другому (реплика диалога). Граф строится по всем томам, представленным в XML-файле.
Дополнительно вводится фильтрация: в анализ включаются только персонажи, у которых не меньше заданного числа реплик (конкретно в примере 10).
filename <- "War_and_Peace.xml"
doc <- read_xml(filename)
ns <- xml_ns_rename(xml_ns(doc), d1 = "tei")
rootnode <- xml_root(doc)
chapters <- xml_find_all(
rootnode,
"//tei:text//tei:div[@type='chapter']",
ns
)
length(chapters)
[1] 358
Здесь выбираются все главы без ограничения по конкретному тому. Это значит, что далее в анализ попадают все тома, которые есть в файле.
dialogues <- tibble(
volume = character(),
chapter = character(),
speech_id = character(),
speaker = character(),
addressee = character()
)
for (i in seq_along(chapters)) {
chapter_node <- chapters[[i]]
volume_node <- xml_find_first(
chapter_node,
"./ancestor::tei:div[@type='volume'][1]",
ns
)
volume_id <- xml_attr(volume_node, "n")
chapter_id <- paste0("chapter_", i)
said_nodes <- xml_find_all(
chapter_node,
".//tei:said[@who and @corresp]",
ns
)
speech_id <- xml_attr(said_nodes, "speech_id")
speaker <- xml_attr(said_nodes, "who")
addressee <- xml_attr(said_nodes, "corresp")
chapter_tbl <- tibble(
volume = volume_id,
chapter = chapter_id,
speech_id = speech_id,
speaker = speaker,
addressee = addressee
) |>
filter(!is.na(speaker)) |>
filter(!is.na(addressee)) |>
filter(speaker != "") |>
filter(addressee != "") |>
filter(speaker != addressee)
dialogues <- bind_rows(dialogues, chapter_tbl)
}
dialogues |>
head(10)
На этом этапе собираются все речевые акты по всем главам. Каждая строка таблицы соответствует случаю, когда один персонаж обращается к другому.
char_counts <- dialogues |>
pivot_longer(
cols = c(speaker, addressee),
values_to = "character"
) |>
count(character, name = "n_replicas", sort = TRUE)
char_counts |>
head(15)
Считается речевая активность персонажей. Персонаж учитывается и как говорящий, и как адресат, потому что обе роли важны для участия в коммуникационной сети.
threshold <- 10
valid_chars <- char_counts |>
filter(n_replicas >= threshold)
valid_chars |>
head(15)
Порог фильтрации можно менять. Например, можно сравнить графы при
threshold <- 5, 10 или 20.
dialogues_filtered <- dialogues |>
filter(speaker %in% valid_chars$character) |>
filter(addressee %in% valid_chars$character)
dialogues_filtered |>
head(10)
Здесь остаются только те речевые акты, в которых оба участника проходят порог по числу реплик.
Важно: в этой версии решения дубликаты не удаляются. Каждая реплика учитывается как отдельное наблюдение.
edges <- dialogues_filtered |>
mutate(
from = if_else(speaker < addressee, speaker, addressee),
to = if_else(speaker < addressee, addressee, speaker)
) |>
count(from, to, name = "weight", sort = TRUE)
edges |>
head(15)
На этом шаге создаются рёбра графа. Граф строится как неориентированный: если персонажи говорят друг с другом в обе стороны, это считается одной связью между ними.
Вес ребра weight показывает, сколько раз данная пара
персонажей вступала в прямое речевое взаимодействие.
vertices <- tibble(
name = sort(unique(c(edges$from, edges$to)))
)
vertices |>
head(15)
Здесь перечисляются все персонажи, которые вошли в граф после фильтрации.
g <- graph_from_data_frame(
edges,
directed = FALSE,
vertices = vertices
)
g
IGRAPH 8b71d78 UNW- 100 325 --
+ attr: name (v/c), weight (e/n)
+ edges from 8b71d78 (vertex names):
[1] NatashaRostova --Nikolai_Rostov
[2] AndreyBolkonsky --Pierre_Bezukhov
[3] NatashaRostova --Sonya_Rostova
[4] Countess_Natalya_Rostova--NatashaRostova
[5] NatashaRostova --Pierre_Bezukhov
[6] Boris_Drubetskoy --Nikolai_Rostov
[7] Nikolai_Rostov --Telyanin
[8] Nikolai_Rostov --Vasily__Vasska__Denisov
+ ... omitted several edges
Получен объект igraph. Граф является неориентированным и
взвешенным: вершины соответствуют персонажам, а ребра отражают речевые
взаимодействия между ними. Вес ребра показывает, сколько раз один
персонаж обращался к другому. Для графа были рассчитаны базовые
характеристики: число вершин, число ребер, число компонент связности и
плотность. Эти параметры позволяют описать общую структуру сети до более
детального анализа.
vcount(g)
[1] 100
ecount(g)
[1] 325
components(g)
$membership
Anatole_Kuragin AndreyBolkonsky
1 1
Anna_Pavlovna_Scherer Arakcheev
1 1
Balashev Bilibin
1 1
Bolhovitinov Boris_Drubetskoy
1 1
Captain_Ramballe Catiche(eldest princess)
1 1
Count_Ilya_Rostov Count_Rostopchin
1 1
Countess_Natalya_Rostova Danila
1 1
Dron_Zakharych Dunyasha
1 1
Dunyasha (Bolkonsky maid) Emperor_Francis_I_of_Austria
1 1
esaul Lovaysky Fedor_Ivanovich_Dolokhov
1 1
Ferapontov General_Davoust
1 1
General_Mack governor's wife
1 1
HeleneKuragin Hippolyte_Kuragin
1 1
Ilagin Ilyin
1 1
Joseph_Alexeevich_Bazdeev Julie_Karagina
1 1
Kirsten Lavrushka
1 1
Lieutenant_Alphonse_Karlovich_Berg mademoiselle_Bourienne
1 1
Marya_Dmitriyevna_Akhrosimova Marya_Lvovna_Karagina
1 1
Mavra_Kuzminishna Michaud
1 1
Mikhail_Ilarionovich_Kutuzov Napoleon_Bonaparte
1 1
NatashaRostova Nikolai_Rostov
1 1
Petya_Rostov Pierre_Bezukhov
1 1
Platon_Karataev Prince_Bagration
1 1
Prince_Dolgorukov Prince_Kozlovsky
1 1
Prince_Nesvitsky Prince_Nikolay_Bolkonsky
1 1
Princess_Anna_Mikhaylovna_Drubetskaya Princess_Elisabeta__Lisa__Karlovna_Bolkonskaya
1 1
Princess_Mariya_Bolkonskaya Pyotr_Nikolaitch_Shinshin
1 1
regimental commander Rugai
1 1
Shcherbinin Sonya_Rostova
1 1
Staff_Captain_Tushin Telyanin
1 1
Tikhon (servant) Tikhon_Shtcherbatov
1 1
Timohin Tsar_Alexander_I_of_Russia
1 1
Tushin's gunner Uncle
1 1
Vasili_Kuragin Vasily__Vasska__Denisov
1 1
Vera_Rostova Villarsky
1 1
viscount Yakov_Alpatych
1 1
Yermolov young Nikolenka Bolkonsky
1 1
Zherkov адъютант
1 1
Балага берейтор
1 2
Богданыч генерал
1 1
гусар денщик
1 1
Десаль доктор
1 1
лекарша Лихачев
1 1
ополченец офицер
1 1
Пелагеюшка пехотный офицер
1 1
плясун Смольянинов (ритор)
1 1
солдат солдаты
1 1
Сперанский Тит
1 2
фельдфебель фельдшер
1 1
француз штаб-офицер
1 1
$csize
[1] 98 2
$no
[1] 2
edge_density(g)
[1] 0.06565657
Здесь выводятся базовые характеристики графа: число вершин, число рёбер, число компонент связности и плотность.
V(g)$degree <- degree(g)
V(g)$weighted_degree <- strength(g)
V(g)$closeness <- closeness(g)
V(g)$betweenness <- betweenness(g)
V(g)$eigenvector <- eigen_centrality(g)$vector
Здесь каждому узлу добавляются основные показатели важности.
top_nodes <- tibble(
name = V(g)$name,
degree = V(g)$degree,
weighted_degree = V(g)$weighted_degree,
closeness = V(g)$closeness,
betweenness = V(g)$betweenness,
eigenvector = V(g)$eigenvector
) |>
arrange(-weighted_degree)
top_nodes |>
head(15)
Эта таблица показывает наиболее активных персонажей в коммуникационной сети.
top_edges <- edges |>
arrange(-weight)
top_edges |>
head(15)
Здесь видно, какие пары персонажей чаще всего разговаривают друг с другом.
В качестве подграфа выбран k-core, так как этот метод выделяет плотное структурное ядро сети, а не просто удаляет слабосвязанные вершины. Порог core >= 3 позволяет убрать периферийных персонажей и сохранить центральную часть коммуникационной структуры, которая остается достаточно большой для анализа.
V(g)$core <- coreness(g)
table(V(g)$core)
1 2 3 4 5 6
18 19 14 11 3 35
g_core <- induced_subgraph(g, vids = V(g)[core >= 3])
g_core
IGRAPH 8b8baf3 UNW- 63 271 --
+ attr: name (v/c), degree (v/n), weighted_degree (v/n), closeness (v/n), betweenness
| (v/n), eigenvector (v/n), core (v/n), weight (e/n)
+ edges from 8b8baf3 (vertex names):
[1] NatashaRostova --Nikolai_Rostov
[2] AndreyBolkonsky --Pierre_Bezukhov
[3] NatashaRostova --Sonya_Rostova
[4] Countess_Natalya_Rostova--NatashaRostova
[5] NatashaRostova --Pierre_Bezukhov
[6] Boris_Drubetskoy --Nikolai_Rostov
[7] Nikolai_Rostov --Vasily__Vasska__Denisov
+ ... omitted several edges
vcount(g_core)
[1] 63
ecount(g_core)
[1] 271
Здесь создаётся подграф с условием core >= 3. Если
нужно, это значение можно изменить.
cw <- cluster_walktrap(g, steps = 45)
membership(cw) |>
head()
Anatole_Kuragin AndreyBolkonsky Anna_Pavlovna_Scherer Arakcheev
4 5 1 5
Balashev Bilibin
10 5
modularity(cw)
[1] 0.2568506
Для анализа сообществ был использован алгоритм Walktrap. Он позволяет выделить группы персонажей, внутри которых связи плотнее, чем между группами. Значение модулярности показывает, насколько отчетливо сеть делится на такие сообщества: в данном случае модульная структура выражена, но группы не являются полностью изолированными, что хорошо соответствует устройству большого романа с пересекающимися сюжетными линиями.
V(g)$community <- membership(cw)
articulation_points(g)
+ 10/100 vertices, named, from 8b71d78:
[1] Yakov_Alpatych Uncle Petya_Rostov
[4] Tsar_Alexander_I_of_Russia Julie_Karagina Pierre_Bezukhov
[7] Nikolai_Rostov Mikhail_Ilarionovich_Kutuzov Prince_Nikolay_Bolkonsky
[10] AndreyBolkonsky
Точки сочленения — это узлы, удаление которых увеличивает число компонент связности.
largest_cliques(g)
[[1]]
+ 7/100 vertices, named, from 8b71d78:
[1] AndreyBolkonsky Pierre_Bezukhov Nikolai_Rostov
[4] Countess_Natalya_Rostova NatashaRostova Sonya_Rostova
[7] Princess_Mariya_Bolkonskaya
Клики — это полностью связные подмножества узлов.
Среди точек сочленения закономерно выделяются Pierre_Bezukhov, Nikolai_Rostov и AndreyBolkonsky, что подтверждает их роль посредников между разными частями сети. Такие узлы особенно важны для связности графа: их удаление сильнее всего меняет структуру сети. Клики, найденные в графе, показывают наиболее плотные группы персонажей, где каждый участник связан с каждым.
set.seed(42)
ggraph(g, layout = "stress") +
geom_edge_link(aes(alpha = weight), width = 0.5, show.legend = FALSE) +
geom_node_point(
aes(size = weighted_degree, color = as.factor(community)),
show.legend = TRUE
) +
geom_node_text(
aes(
filter = weighted_degree > mean(weighted_degree),
label = name
),
repel = TRUE,
size = 3,
show.legend = FALSE
) +
theme_void()
Визуализация полного графа позволяет одновременно видеть центральных персонажей, силу связей и сообщества. Размер узла отражает взвешенную степень, цвет показывает принадлежность к сообществу, а прозрачность ребра зависит от веса связи.
set.seed(42)
ggraph(g_core, layout = "stress") +
geom_edge_link(aes(alpha = weight), width = 0.6, show.legend = FALSE) +
geom_node_point(
aes(size = weighted_degree, color = as.factor(core)),
show.legend = TRUE
) +
geom_node_text(
aes(label = name),
repel = TRUE,
size = 3,
show.legend = FALSE
) +
theme_void()
Подграф показывает наиболее плотное ядро сети персонажей.
par(mar = rep(0, 4))
plot(cw, g,
vertex.label.cex = 0.5, # размер текста
vertex.label.color = "black",
vertex.size = 3) # размер точек (чтобы не мешали)
В работе был построен граф персонажей романа «Война и мир» на основе
XML-разметки. Данные извлекались из тегов
Полученный граф является неориентированным и взвешенным. После фильтрации по числу реплик он включает около 100 вершин и более 300 ребер, при этом почти все персонажи входят в одну большую компоненту. Это соответствует структуре романа Толстого как единого повествовательного мира с множеством пересекающихся линий. Наиболее центральные позиции занимают Пьер Безухов, Наташа Ростова, Николай Ростов и Андрей Болконский. Это ожидаемо с литературной точки зрения: именно эти персонажи находятся в центре основных сюжетных линий и активно взаимодействуют с разными группами героев. Например, Пьер соединяет светское общество, военную линию и философские эпизоды, а Наташа связана как с семьей Ростовых, так и с линией Болконских.
В качестве подграфа был выбран метод k-core, позволяющий выделить плотное ядро сети. Подграф с core ≥ 3 включает большинство ключевых персонажей и отражает центральный круг общения, в который входят Ростовы, Болконские и их ближайшее окружение. Визуализация показывает, что именно эти группы формируют ядро романа, тогда как второстепенные персонажи образуют периферию.
Анализ сообществ с помощью алгоритма Walktrap выявил несколько групп, которые можно интерпретировать как сюжетные и социальные кластеры: семейный круг Ростовых, круг Болконских, а также персонажи, связанные с военной и придворной линиями. Значение модулярности около 0.35 показывает, что эти группы различимы, но не изолированы, что хорошо соответствует композиции романа, где линии постоянно пересекаются.
Дополнительно были найдены точки сочленения и клики. Среди точек сочленения оказываются Пьер Безухов, Николай Ростов и Андрей Болконский — они играют роль посредников между различными частями сети. Крупнейшая клика объединяет персонажей из центральных семейных линий (Ростовы и Болконские), что отражает их тесную взаимосвязь в тексте. В целом полученный граф хорошо воспроизводит литературную структуру романа: он показывает плотное ядро ключевых героев и более разреженную периферию второстепенных персонажей.