library(XML)
library(tidyverse)
library(igraph)
library(ggraph)
<- xmlTreeParse("War_and_Peace.xml", useInternalNodes = TRUE)
doc
#добираемся до всех тегов said
<- getNodeSet(doc, "/tei:TEI//tei:text//tei:div//tei:div//tei:div//tei:p//tei:said",
all_saids namespaces = c(tei = "http://www.tei-c.org/ns/1.0"))
#функция, достающая из одного said атрибуты corresp и who
<- function(one_said) {
get_corresps_and_whos <- xmlGetAttr(one_said, "corresp")
corrs <- xmlGetAttr(one_said, "who")
whos
tibble(corresp = corrs,
who = whos)
}
#все связи тут
<- map_df(all_saids, get_corresps_and_whos) corresps_and_whos
Графы из ‘Войны и Мира’ Л.Н. Толстого
и не только графы на самом деле
Война и мир: датасет
“Война и мир” — роман-эпопея писателя Льва Николаевича Толстого. По данному произведению был создан датасет в формате XML с разметкой TEI. Такой формат дает возможность представить датасет в виде графа, где вершинами могут быть персонажи, а ребрами — их взаимодействия. В данной работе мы попробуем построить различные графы по этим данным и прокомментируем полученные результаты.
Подготовка данных
Загрузим нужные пакеты и начнем работу с XML. При рассмотрении документа я обратила внимание на теги said: почти во всех них упоминались персонажи в атрибутах corresp (кто говорит) и who (к кому обращаются). На их основе можно собрать данные о связях между персонажами:
corresp | who |
---|---|
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Anna_Pavlovna_Scherer | Vasili_Kuragin |
Anna_Pavlovna_Scherer | Vasili_Kuragin |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Anna_Pavlovna_Scherer | Vasili_Kuragin |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Anna_Pavlovna_Scherer | Vasili_Kuragin |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Anna_Pavlovna_Scherer | Vasili_Kuragin |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Anna_Pavlovna_Scherer | Vasili_Kuragin |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Anna_Pavlovna_Scherer | |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Anna_Pavlovna_Scherer | Vasili_Kuragin |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Vasili_Kuragin | Anna_Pavlovna_Scherer |
Здесь отображены первые 20 строк из полученной таблицы, по которым видно, что в начале произведения друг с другом разговаривают Анна Павловна Шерер и Василий Курагин. Заметен и пропуск в колонке corresp, который объясняется отсутствием значения данного атрибута в этом месте, т.е. технически можно считать, что здесь нет взаимодействия. Такие пропуски встречаются по всей таблице в обеих колонках, и для дальнейшей работы будет логично смотреть только на полноценные взаимодействия:
<- corresps_and_whos|>
data filter(corresp != "", who != "")
Создание графа
Для графа будет полезно внести дополнительную информацию о вершинах (персонажах) и ребрах (их связях). Прежде всего, мы можем посчитать веса ребер. Для этого можно объеденить corresp и who в одну колонку, чтобы можно было посчитать, сколько раз встречается какое-либо ребро. Это и станет весом ребра:
<- data |>
data unite("edge_", corresp:who, sep = "__", remove = FALSE)
#расчитываем веса ребер
<- data|>
edge_weights count(edge_)
#заносим в таблицу
<- data|>
data left_join(edge_weights) |>
select(-edge_) |>
rename(Weight = n)
Joining with `by = join_by(edge_)`
#получаем объект графа, его можно сделать направленным
<- data|>
war_peace_g graph_from_data_frame(directed=TRUE)
#Проверяем, есть ли веса, высветим 20 значений
E(war_peace_g)$Weight[1:20]
[1] 23 30 30 23 30 23 30 23 30 23 30 23 23 23 23 23 30 23 23 30
Отлично! Можно еще добавить центральность уже к созданному объекту графа:
#добавляем к атрибутам центральность
V(war_peace_g)$degree = degree(war_peace_g)
И вообще посмотрим на сам объект направленного графа war_peace_g:
war_peace_g
IGRAPH 261641d DN-- 451 5418 --
+ attr: name (v/c), degree (v/n), Weight (e/n)
+ edges from 261641d (vertex names):
[1] Vasili_Kuragin ->Anna_Pavlovna_Scherer
[2] Anna_Pavlovna_Scherer->Vasili_Kuragin
[3] Anna_Pavlovna_Scherer->Vasili_Kuragin
[4] Vasili_Kuragin ->Anna_Pavlovna_Scherer
[5] Anna_Pavlovna_Scherer->Vasili_Kuragin
[6] Vasili_Kuragin ->Anna_Pavlovna_Scherer
[7] Anna_Pavlovna_Scherer->Vasili_Kuragin
[8] Vasili_Kuragin ->Anna_Pavlovna_Scherer
+ ... omitted several edges
Вышло много вершин (451) и еще больше ребер (5418), среди атрибутов вершин есть естественно названия и центральность, а среди атрибутов ребер — вес. Дополнительно можно узнать плотность и число компонент:
edge_density(war_peace_g)
[1] 0.02669623
components(war_peace_g)$no
[1] 11
Эксперименты с визуализацией
У нас вышло довольно много и ребер и вершин, поэтому первую визуализацию представим в некотором минималистичном формате:
set.seed(234)
ggraph(war_peace_g, layout = "fr", ) +
geom_edge_link(color = "#41bcd5",
aes(alpha = E(war_peace_g)$Weight),
width = 1,
show.legend = FALSE) +
geom_node_point(color = "#34495e",
aes(size = V(war_peace_g)$degree),
show.legend = FALSE) +
labs(title = "War and Peace",
subtitle = "Full Graph with 451 nodes and 5418 edges") +
theme(text=element_text(family="serif",
color = "#41495e"))
Что-то в этом конечно есть…. Видна разница между ребрами и размером вершин, но в целом увидеть что-либо здесь о центральных персонажах практически невозможно. Облегчим себе жизнь и создадим граф с высоким весом ребер. Я остановилась на значении 35: так граф выходил полностью связанным и на нём достаточно хорошо уже были видны главные герои:
<- data|>
data2 filter(Weight > 35)
<- data2|>
war_peace_g2 graph_from_data_frame(directed=TRUE)
V(war_peace_g2)$degree = degree(war_peace_g2)
set.seed(345)
ggraph(war_peace_g2, layout = "fr") +
geom_edge_link(color = "#68d2e7",
aes(alpha = 0.1),
width = 1,
show.legend = FALSE) +
geom_node_point(color = "#34495e",
aes(size = V(war_peace_g2)$degree),
show.legend = FALSE) +
geom_node_text(aes(label = V(war_peace_g2)$name),
family="serif",
nudge_y = 0.05,
nudge_x = 0.03) +
labs(title = "War and Peace",
subtitle = "Nodes with weight more than 35") +
theme(text=element_text(family="serif",
color = "#41495e"))
Сообщества
Отсмотрев различные алгоритмы обнаружения сообществ, я решила остановиться на Spinglass, так как полученный результат показался мне весьма любопытным. С одной стороны, модулярность вышла очень маленькой, т.е. получающиеся группы не плотные:
<- cluster_spinglass(war_peace_g2)
csg V(war_peace_g2)$membership <- membership(csg)
modularity(csg)
[1] 0.005713214
Тем не менее, запуская данный алгоритм несколько раз, я получала если не одинаковые, то похожие расклады, которые можно спокойно проинтерпретировать:
ggraph(war_peace_g2, layout = "fr", maxiter = 500) +
geom_edge_link(color = "#41bcd5") +
geom_node_point(
aes(color = as.factor(membership)),
size = 4,
show.legend = TRUE
+
) geom_node_text(
aes(label = name),
color = "#34495e",
repel = TRUE,
family="serif"
+
) labs(title = "War and Peace",
subtitle = "Communities with Spinglass") +
scale_color_brewer("Communities", palette = "Paired") +
theme(text=element_text(family="serif",
color = "#41495e"))
Например, при одном из запусков получалась группа, включающая в себя полностью семью Болконских и Билибина. Здесь же есть разделение на Марью с Николаем и Андрея с Билибиным, что в общем тоже нормально. Я плохо помню Билибина, поэтому решила немного поискать информации о нем. Наткнулась на описание из книги: “Билибин был человек лет тридцати пяти, холостой, одного общества с князем Андреем”, что не противоречит получившейся картине.
Другая группа включает в себя женщин из семьи Ростовых, что тоже не лишено смысла.
Возможно не самая показательная группа здесь с Николаем Ростовым, Теляниным, Друбецким и Денисовым. Взаимодействия в книге были, однако нельзя сказать, что эти четверо объединялись в какую-то определенную команду, либо я и правда уже всё забыла :(
Пьер Безухов, Платон Каратаев и Рамбаль — тоже неоднозначная группа. Можно сказать, что это взаимодействия Безухова, связанные с войной, но в целом это не какое-то плотное сообщество.