Графы из ‘Войны и Мира’ Л.Н. Толстого

и не только графы на самом деле

Author

Оксана Цегоева

Война и мир: датасет

“Война и мир” — роман-эпопея писателя Льва Николаевича Толстого. По данному произведению был создан датасет в формате XML с разметкой TEI. Такой формат дает возможность представить датасет в виде графа, где вершинами могут быть персонажи, а ребрами — их взаимодействия. В данной работе мы попробуем построить различные графы по этим данным и прокомментируем полученные результаты.

Подготовка данных

Загрузим нужные пакеты и начнем работу с XML. При рассмотрении документа я обратила внимание на теги said: почти во всех них упоминались персонажи в атрибутах corresp (кто говорит) и who (к кому обращаются). На их основе можно собрать данные о связях между персонажами:

library(XML)
library(tidyverse)
library(igraph)
library(ggraph)

doc <- xmlTreeParse("War_and_Peace.xml", useInternalNodes = TRUE)

#добираемся до всех тегов said
all_saids <- getNodeSet(doc, "/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 атрибуты corresp и who
get_corresps_and_whos <- function(one_said) {
  corrs <- xmlGetAttr(one_said, "corresp")
  whos <- xmlGetAttr(one_said, "who")
  
  tibble(corresp = corrs,
         who = whos)

}

#все связи тут
corresps_and_whos <- map_df(all_saids, get_corresps_and_whos)
Взаимодействия персонажей ‘Война и Мир’
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, который объясняется отсутствием значения данного атрибута в этом месте, т.е. технически можно считать, что здесь нет взаимодействия. Такие пропуски встречаются по всей таблице в обеих колонках, и для дальнейшей работы будет логично смотреть только на полноценные взаимодействия:

data <- corresps_and_whos|>
  filter(corresp != "", who != "")

Создание графа

Для графа будет полезно внести дополнительную информацию о вершинах (персонажах) и ребрах (их связях). Прежде всего, мы можем посчитать веса ребер. Для этого можно объеденить corresp и who в одну колонку, чтобы можно было посчитать, сколько раз встречается какое-либо ребро. Это и станет весом ребра:

data <- data |>
  unite("edge_", corresp:who, sep = "__", remove = FALSE)

#расчитываем веса ребер
edge_weights <- data|>
  count(edge_) 

#заносим в таблицу
data <- data|>
  left_join(edge_weights) |>
  select(-edge_) |>
  rename(Weight = n) 
Joining with `by = join_by(edge_)`
#получаем объект графа, его можно сделать направленным
war_peace_g <- data|>
  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: так граф выходил полностью связанным и на нём достаточно хорошо уже были видны главные герои:

data2 <- data|>
  filter(Weight > 35)
  

war_peace_g2 <- data2|>
  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, так как полученный результат показался мне весьма любопытным. С одной стороны, модулярность вышла очень маленькой, т.е. получающиеся группы не плотные:

csg <- cluster_spinglass(war_peace_g2)
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"))

Например, при одном из запусков получалась группа, включающая в себя полностью семью Болконских и Билибина. Здесь же есть разделение на Марью с Николаем и Андрея с Билибиным, что в общем тоже нормально. Я плохо помню Билибина, поэтому решила немного поискать информации о нем. Наткнулась на описание из книги: “Билибин был человек лет тридцати пяти, холостой, одного общества с князем Андреем”, что не противоречит получившейся картине.

Другая группа включает в себя женщин из семьи Ростовых, что тоже не лишено смысла.

Возможно не самая показательная группа здесь с Николаем Ростовым, Теляниным, Друбецким и Денисовым. Взаимодействия в книге были, однако нельзя сказать, что эти четверо объединялись в какую-то определенную команду, либо я и правда уже всё забыла :(

Пьер Безухов, Платон Каратаев и Рамбаль — тоже неоднозначная группа. Можно сказать, что это взаимодействия Безухова, связанные с войной, но в целом это не какое-то плотное сообщество.