Анализ виртуальных сообществ (часть 3 - сетевой анализ топологических свойств и структурных характеристик)

Author

Омельченко Д.А.

Задачи данного этапа анализа

После того, как мы описали основные характеристики пользователей, перейдем к анализу связей внутри сообществ. На данном этапе наших индивидуальных исследовательских проектов, мы:

  • представим сообщества в виде социального графа, состоящего из узлов (vertices) и связей (links, edges);
  • сделаем описание важнейших свойств виртуальных сообществ, характеризующих их структуру и качества связей - связность, плотность, транзитивность, ассортативность;
  • выявим важные характеристики, связанные с расстояниями между узлами;
  • выявим наиболее значимых участников сообществ по различным показателям центральности;
  • посмотрим, как сообщества связаны друг с другом

Создание социальных графов по данным сообществ

Для того, чтобы представить данные по виртуальным группам нам понадобятся списки идентификационных номеров по каждой группе.

С помощью функции getArbitraryNetwork() из библиотеки vkR мы извлечем информацию о том, являются ли члены сообщества друзьями и сохраним результаты в виде новой таблицы, которую мы будем использовать для дальнейшего анализа.

Прежде, чем мы начнем, не забываем подключить пакет vkR, авторизоваться, установить ключ и версию API:

#загрузить библиотеку
library(vkR)
#получить ключ
vkOAuth(11111111)# номер приложения в ВК
#установить ключ
setAccessToken(access_token="vk1.a.....ТУТ ВАШ КЛЮЧ")
# установить версию API
v<-setAPIVersion("5.89")

Теперь выгрузим данные о связях между пользователями в сообществах.

В примере - выгрузка данных по группе кафедры, Вы выгружаете по своим группам.

kaf_friends<-getArbitraryNetwork(users_ids = kaf_info$id, format = "edgelist")

Посмотрим, что представляют собой эти данные:

head(kaf_friends)
      from   to
1 14065837 6559
2 17220459 6559
3 32427481 6559
4 46233132 6559
5 65446060 6559
6 96947178 6559

Мы видим, что это обычная таблица с двумя столбцами - from и to, представляющими дружеские связи между двумя пользователями группы. Хотя from обозначает от, а to - к, на самом деле никакого направления нет, и такие названия в нашем случае - просто условные обозначения, и наш граф (сеть) мы будем считать ненаправленным, то есть нам не важно, является ли узел \(i\) другом узла \(j\) или наоборот, важно просто наличие данной связи.

Дальнейший анализ будет проводиться с помощью одной из ведущих библиотек для сетевого анализа и визуализации сетей в R - igraph.

Установим библиотеку:

install.packages("igraph")

Подключим ее:

library(igraph)

Для того, чтобы провести сетевой анализ в пакете igraph нам нужно создать сетевой объект, импортируя туда наши данные (в примере данные кафедры, вы делаете по всем своим группам).

Мы импортируем связи из таблицы со сведениями о дружеских связях внутри сообщества, но добавим дополнительно информацию о гендерной принадлежности, которая нам понадобится для того, чтобы понять, что такое ассортативность.

kaf_graph <- graph_from_data_frame(d = kaf_friends, vertices=kaf_info[, c("id", "sex")], directed=F)
kaf_graph
IGRAPH ecd5d8a UN-- 206 2248 -- 
+ attr: name (v/c), sex (v/n)
+ edges from ecd5d8a (vertex names):
 [1] 6559   --14065837  6559   --17220459  6559   --32427481  6559   --46233132 
 [5] 6559   --65446060  6559   --96947178  6559   --142307198 6559   --153555180
 [9] 6559   --156349097 6559   --161966937 6559   --166805167 6559   --169605135
[13] 6559   --201184242 6559   --206548101 6559   --228005497 6559   --229791608
[17] 6559   --246371541 6559   --254297036 6559   --294786885 6559   --312228345
[21] 6559   --326445315 6559   --326603147 6559   --364775694 6559   --366258924
[25] 6559   --410272413 6559   --416675940 6559   --500072697 6559   --605395006
[29] 2203516--14065837  2203516--15223162  2203516--65446060  2203516--79085658 
+ ... omitted several edges

Видим, что созданный объект имеет специфический формат, в котором хранятся данные о связях между акторами и их атрибутах.

Вывод отдельных характеристик связей и узлов

Мы можем обратиться к созданному графу и вывести информацию об узлах и связях.

Информация об узлах:

V(kaf_graph)
+ 206/206 vertices, named, from ecd5d8a:
  [1] 6559      2203516   6184975   7619834   9022179   9644203   11873679 
  [8] 13606746  14065837  15223162  15799153  17220459  20004001  22905692 
 [15] 24906192  27613920  28384326  29025168  31278233  31626007  32427481 
 [22] 34601942  37756527  46201518  46233132  53538897  57185914  65446060 
 [29] 66155721  76703324  78747964  79039367  79085658  84570877  85598950 
 [36] 85645509  91018325  91111764  96947178  97704278  98570768  99493052 
 [43] 106513634 118138869 119461058 120373891 122059919 125419254 136722308
 [50] 139262975 141776928 142307198 143222397 146690541 147398712 149189977
 [57] 153555180 155243565 155891726 156349097 159014819 161351271 161966937
 [64] 166805167 169605135 169732853 171634457 171873854 173419769 177967453
+ ... omitted several vertices

В том числе по отдельным атрибутам:

V(kaf_graph)$sex
  [1] 2 1 1 2 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 [38] 1 1 1 1 1 1 1 2 1 1 1 1 1 1 2 1 1 1 2 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 2 1
 [75] 1 1 1 2 2 1 1 1 1 1 1 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
[112] 1 1 1 1 1 1 1 1 1 2 2 1 1 1 1 1 1 1 1 2 1 2 2 1 1 2 1 2 1 1 1 1 1 1 1 1 1
[149] 1 2 1 1 2 1 1 1 1 1 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 1 2
[186] 1 1 1 1 1 1 1 1 1 1 2 2 2 1 1 1 2 1 2 1 1

Поскольку это информация из исходной таблицы, она не перекодирована.

Информация о связях:

E(kaf_graph)
+ 2248/2248 edges from ecd5d8a (vertex names):
 [1] 6559   --14065837  6559   --17220459  6559   --32427481  6559   --46233132 
 [5] 6559   --65446060  6559   --96947178  6559   --142307198 6559   --153555180
 [9] 6559   --156349097 6559   --161966937 6559   --166805167 6559   --169605135
[13] 6559   --201184242 6559   --206548101 6559   --228005497 6559   --229791608
[17] 6559   --246371541 6559   --254297036 6559   --294786885 6559   --312228345
[21] 6559   --326445315 6559   --326603147 6559   --364775694 6559   --366258924
[25] 6559   --410272413 6559   --416675940 6559   --500072697 6559   --605395006
[29] 2203516--14065837  2203516--15223162  2203516--65446060  2203516--79085658 
[33] 2203516--85645509  2203516--358620946 6184975--14065837  6184975--57185914 
[37] 6184975--65446060  6184975--246371541 7619834--14065837  7619834--65446060 
+ ... omitted several edges

Хотя в кафедральной группе 206 участников, связей намного больше - 2248! Такое соотношение уже потенциально указывает на то, что многие участники не просто состоят в группе кафедры, но и состоят в виртуальных дружеских связях.

Создадим базовую визуализацию сообщества, раскрасив узлы в зависимости от пола участников:

plot(kaf_graph, edge.arrow.size=.5, vertex.label=NA, vertex.size=5, vertex.color=c( "pink", "skyblue")[1+(V(kaf_graph)$sex==2)] ) 

Мы видим, что в кафедре есть сообщество пользователей, которые взаимодействуют друг с другом, но есть и те, кто является членом сообщества, но ни с кем не связан.

Анализ структурных характеристик сообщества

Связность сообществ

Одной из важнейших топологических характеристик социальной сети является ее связность. Сеть является связной тогда, когда из любой ее точки (узла) можно добраться к любой другой точке (узлу).

В теории графов связный граф — граф, в котором можно проложить путь между каждой парой его вершин. Разомкнутый граф — граф, в котором хотя бы две вершины не соединены.

Из графика мы видим, что группа кафедры не является связной сетью. Давайте проверим это с помощью специальной функции:

is_connected(kaf_graph)
[1] FALSE

Как и следовало ожидать, результат отрицательный (FALSE).

Если сеть не является связанной, внутри нее все-таки могут существовать отдельные участки, внутри которых все узлы могут быть связаны друг с другом какими-либо путями, такие участки называются компонентами связности.

Компонента связности - часть сети (подсеть), являющаяся связной, то есть такой, где каждый узел может связаться с другим узлом.

Сколько в нашей группе таких компонент?

count_components(kaf_graph)
[1] 25

Почему так много? Если провести подробный анализ, то можно увидеть, что у нас есть одна большая компонента с 181 узлами, одна компонента, состоящая из двух узлов, и 23 компоненты, состоящие всего из одного узла.

components <- igraph::components(kaf_graph, mode="weak")
components$csize
 [1] 181   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1
[20]   1   1   2   1   1   1

Плотность взаимосвязей в сообществе

Плотность сети (density) – это мера, которая определяет, насколько плотно связаны узлы сети друг с другом. Если все связаны со всеми, в такой сети плотность равна 1, если никто не связан друг с другом, плотность равна 0.

Плотность графа (сети) = отношение реального числа связей к максимально возможному (оно вычисляется по формуле \(N * (N – 1) / 2\), где \(N\) – число вершин в сети).

edge_density(kaf_graph)
[1] 0.1064646

Плотность нашего сообщества далеко от максимального значения, скорее можно говорить о том, что реализуются только чуть более 10% всех возможных взаимодействий.

Транзитивность

Понятие транзитивности — это формальное отражение принципа «друг моего друга — мой друг». Если вершины графа — люди, а ребра соединяют друзей, то принцип «друг моего друга — мой друг» выполняется в транзитивных графах.

Транзитивность означает, что в сети присутствует значительное количество связанных троек (три вершины, связанные друг с друг с другом).

Коэффициент транзитивности показывает, каково отношение «треугольников», то есть сочетаний из трех узлов и трех связей между ними, к общему количеству «триад» (совокупность трех узлов, как закрытых - «треугольников», так и открытых, то есть с неполными связями).

\[T = 3\frac{\#треугольников}{\#триад}\]

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

Транзитивность может измеряться на уровне всего сообщества (глобально) и на уровне отдельных узлов. В последнем случае обычно под транзитивностью понимают локальный коэффициент кластеризации.

Посчитаем общую транзитивность для нашего сообщества.

transitivity(kaf_graph, type="global")
[1] 0.3698295

Значение глобального коэффициента транзитивности - 0,36, это скорее посредственное значение, показывающее, что далеко не все тройки узлов закрыты и тенденция образовывать сообщества внутри группы недостаточно распространена.

Ассортативность

Ассортативность, или ассортативное смешивание — предпочтение узлов сети присоединяться к другим узлам, которые каким-либо образом похожи на них.

Добавление этой характеристики в сетевые модели часто позволяет понять поведение акторов и динамику реальных социальных сообществ.

Для измерения ассортативности используется коэффициент ассортативности. Он является позитивным, если узлы (члены сообщества) стремятся взаимодействовать с себе подобными (по каким-либо признакам), и негативным, - если наоборот, этот признак является причиной отталкивания (противоположности сходятся, тогда как похожести - отталкиваются).

Иными словами, коэффициент ассортативности измеряет уровень гомофилии - предпочтения членов своих групп другим.

Есть несколько вариантов коэффициентов ассортативности, отличающихся тем, какой показатель используется для сравнения:

  • номинальная ассортативность (высчитывается по категориальным атрибутам узлов, например, по полу, должности в организации, наличию определенного статуса, например, лица, являющиеся волонтерами охотнее связываются с другими волонтерами).

Согласно работам М. Ньюмана, формула для вычисления категориального коэффициента ассортативности следующая:

\[r=\frac{\sum_ie_{ii}-\sum_i a_i b_i}{1-\sum_ia_i b_i}\]

где \(e(i,j)\) доля связей, объединяющих узлы типа \(i\) и \(j\), \(a_i=\sum_j e_{ij}\) и \(b_j=\sum_i e_{ij}\).

  • ассортативность по числовым характеристикам (например, по возрасту, доходам и пр.). В пакете igraph данный тип вычисляется по формуле:

\[r=\frac{1}{\sigma_q^2}\sum_{jk}jk(e_{jk}-q_jq_k)\]

для ненаправленных сетей \((q_i=\sum_j e_{ij})\) и:

\[r=\frac{1}{\sigma_o\sigma_i}\sum_{jk}jk(e_{jk}-q_j^o q_k^i)\]

для направленных сетей. Здесь \(q_i^o=\sum_j e_{ij}\), \(q_i^i=\sum_j e_{ji}\), и значит,\(\sigma_q\),\(\sigma_o\) и \(\sigma_i\) - это стандартные отклонения \(q\), \(q^o\) и \(q^i\), соответственно.

  • ассортативность по степени узла (количеству соседей). Предполагается, что узлы с похожим поведением в сети (имеющие сходное количество друзей) будут чаще взаимодействовать друг с другом.

Давайте попробуем посчитать коэффициент ассортативности по полу (охотнее ли в нашем сообществе девочки чаще дружат с девочками, а мальчики с мальчиками):

assortativity_nominal(kaf_graph, V(kaf_graph)$sex, directed=F)
[1] 0.1657819

Мы видим, что значение положительное, значит, все же ассортативность присутствует (вероятнее всего, потому что большинство членов группы - женского пола), но она достаточно слабая.

Посмотрим ассортативность по степени узлов:

assortativity_degree(kaf_graph)
[1] -0.1580122

Результаты показывают, что у нас скорее наблюдается обратная ситуация: пользователи с большим количеством связей редко связаны друг с другом и чаще - с теми, у кого связей меньше.

Как свести всю информацию в одну таблицу?

До этого момента мы работали с отдельным графом, но ведь нам нужно сделать анализ по пяти группам, поэтому возникает вопрос, как все соединить в одну таблицу?

Чтобы проиллюстрировать возможности R, выгрузим информацию о двух дополнительных группах - ИГН и и Дом народов Алтайского края создадим под эти группы объекты igraph:

ign_friends<-getArbitraryNetwork(users_ids = ign_info$id, format = "edgelist")
rnd_friends<-getArbitraryNetwork(users_ids = rnd_info$id, format = "edgelist")

Затем создадим сетевые объекты для этих групп:

ign_graph <- graph_from_data_frame(d = ign_friends, vertices=ign_info[, c("id", "sex")], directed=F)
rnd_graph <- graph_from_data_frame(d = rnd_friends, vertices=rnd_info[, c("id", "sex")], directed=F)

Соединим все три графа в один список:

nets<-list(kaf_graph, ign_graph, rnd_graph)

Теперь, с помощью функции sapply, которая позволяет применять какую-то другую функцию ко всем элементам списка и вывести вектор значений, заново посчитаем показатели связности, плотности, транзитивности и ассортативности, но уже не по одной группе, а по всем сразу. Результаты сохраним как отдельные объекты:

connected<-sapply(nets, is_connected)
ncomponents<-sapply(nets, count_components)
density<-sapply(nets, edge_density)
transitivity<-sapply(nets, transitivity)
asort_degree<-sapply(nets, assortativity_degree)

Соединим все метрики в одну таблицу, используя функцию rbind (соединение по строкам) :

table_metr<-rbind(connected, ncomponents, density,  transitivity, asort_degree)
table_metr
                   [,1]         [,2]         [,3]
connected     0.0000000   0.00000000   0.00000000
ncomponents  25.0000000 311.00000000 161.00000000
density       0.1064646   0.01482604   0.01894453
transitivity  0.3698295   0.22308465   0.22022881
asort_degree -0.1580122   0.15185490  -0.13033921

Получилось неплохо, но непонятно, где какая группа. Создадим вектор названий групп и присвоим их в качестве наименований столбцов:

группы<-c("Кафедра", "ИГН", "РНД")
colnames(table_metr)<-группы
table_metr
                Кафедра          ИГН          РНД
connected     0.0000000   0.00000000   0.00000000
ncomponents  25.0000000 311.00000000 161.00000000
density       0.1064646   0.01482604   0.01894453
transitivity  0.3698295   0.22308465   0.22022881
asort_degree -0.1580122   0.15185490  -0.13033921

Уже гораздо лучше. Осталось только добавить наименование метрик:

меры<-c("Является ли граф связным?", "Количество связных компонент", "Плотность", "Транзитивность", "Ассортативность по степени")
table<-cbind(меры,table_metr)
table<-as.data.frame(table)
table
                                     меры           Кафедра                ИГН
connected       Является ли граф связным?                 0                  0
ncomponents  Количество связных компонент                25                311
density                         Плотность 0.106464598626569 0.0148260395454616
transitivity               Транзитивность 0.369829525082304  0.223084654433292
asort_degree   Ассортативность по степени -0.15801218539987  0.151854896006089
                            РНД
connected                     0
ncomponents                 161
density      0.0189445255346857
transitivity  0.220228809152366
asort_degree -0.130339211625452

Расстояния, центр и периферия

Важную роль в оценке социальной сети играют расстояния между отдельными объектами. Чем более тесно связаны узлы сети, тем короче расстояния между ними, и значит, быстрее может передаваться информация между узлами, более тесными могут быть эмоциональные, социальные и другие отношения. Это может быть важно для оценки процессов мобилизации, распространения информации (слухов, например), формирования новых связей.

Как мы можем оценить расстояния в сети в целом? Например, путем расчета среднего расстояния между всеми парами узлов.

mean_dist<-sapply(nets, mean_distance)
mean_dist                  
[1] 2.473145 2.982633 3.026051

Диаметр сети: максимальное расстояние среди всех расстояний между парами узлов сети. Чем меньше диаметр, тем меньше шагов требуется пройти даже между самыми удаленными узлами.

diameter<-sapply(nets, diameter)
diameter
[1] 5 8 7

Видим, что самый маленький диаметр у кафедральной группы, самый большой - у ИГН, целых 8 шагов.

Важно! Когда рассчитываются расстояния, важно помнить, является ли граф связанным, ведь если два узла не имеют связи, между ними нельзя подсчитать расстояние, и диаметр такого графа будет стремиться к бесконечности. Поэтому igraph по умолчанию, если связных компонент несколько, высчитывает диаметр по каждой и возвращает максимальное значение. Об этом стоит помнить, когда мы интерпретируем результаты.

Еще одна важная мера – эксцентриситет узла – максимальное расстояние между узлом n и всеми другими узлами. Самый большой эксцентриситет - это и есть диаметр.

Радиус графа – это его минимальный эксцентриситет.

К сожалению, с радиусом невозможно работать автоматически, если просто задать соответствующую функцию, во всех группах радиус будет 0:

radius<-sapply(nets, radius)
radius
[1] 0 0 0

Поэтому, чтобы найти радиус, нам нужно выделить из нашей сети максимальную компоненту.

Рассмотрим пример с кафедральной группой:

#Сохраним все связные компоненты
kaf_components <- igraph::clusters(kaf_graph, mode="weak")
Warning: `clusters()` was deprecated in igraph 2.0.0.
ℹ Please use `components()` instead.
#Найдем порядковый номер самой большой
biggest_cluster_id <- which.max(kaf_components$csize)
#Выгрузим id узлов, которые входят в эту компоненту
vert_ids <- V(kaf_graph)[kaf_components$membership == biggest_cluster_id]
# Создадим отдельную подсеть (только для узлов, которые входят в самую большую часть сети) 
kaf_max<-igraph::induced_subgraph(kaf_graph, vert_ids)
#Посмотрим на радиус:
radius(kaf_max)
[1] 3

У нас получилось!

Теперь сделаем то же самое для двух других групп:

#ИГН
ign_components <- igraph::clusters(ign_graph, mode="weak")
biggest_cluster_id <- which.max(ign_components$csize)
vert_ids <- V(ign_graph)[ign_components$membership == biggest_cluster_id]
ign_max<-igraph::induced_subgraph(ign_graph, vert_ids)
#РНД
rnd_components <- igraph::clusters(rnd_graph, mode="weak")
biggest_cluster_id <- which.max(rnd_components$csize)
vert_ids <- V(rnd_graph)[rnd_components$membership == biggest_cluster_id]
rnd_max<-igraph::induced_subgraph(rnd_graph, vert_ids)
#Объединим максимальные сети в список
nets_max<-list(kaf_max, ign_max, rnd_max)
radius<-sapply(nets_max, radius)
radius
[1] 3 4 4

Введем еще два важных понятия, связанных с понятиями расстояний в сети - центр и периферия.

Центром графа называется один или несколько узлов, эксцентриситет которых равен радиусу графа. То есть это, по сути, самые важные, главные узлы в сети.

Периферия графа – это набор узлов, эксцентриситет которых равен диаметру. То есть это узлы, максимально далеко отстоящие от всех остальных, те, с которыми у других меньше всего связей.

Чтобы посмотреть, какие пользователи в наших группах являются центральными и периферийными, создадим две функции, которые позволят это определить.

Просто запустите следующий код:

graph_center <- function(g){
ec <- eccentricity(g)
ec[which(ec == min(ec))]
}
periphery <- function(g){ 
ec <- eccentricity(g)
ec[which(ec == max(ec))]
}

Теперь давайте посмотрим, кто входит в центр:

center<-sapply(nets_max, graph_center)
center_num<-sapply(center, length)
center_num
[1]  9  4 39

В группе кафедры это девять пользователей, эксцентриситет которых равен 3. Это методист и преподаватели кафедры. Неудивительно, что они оказались в центре, так как многие студенты и выпускники кафедры состоят у них в друзьях, и они тесно связаны с другими членами группы.

А кто на периферии?

periphery_nets_max<-sapply(nets_max, periphery)
periphery_num<-sapply(periphery_nets_max, length)
periphery_num
[1] 43  7  4

На кафедре таких узлов уже 43, а вот в группах ИГН и ДН их гораздо меньше (всего 7 и 4). Как думаете, почему так?

Сведем всю информацию в расстояниях в группах в одну таблицу:

table2<-rbind(round(mean_dist, 2), diameter, radius, center_num, periphery_num)
colnames(table2)<-группы
table2<-tibble::as_tibble(table2)

Добавим столбец с пояснениями:

меры2<-c("Среднее расстояние","Диаметр", "Радиус", "Количество центральных узлов", "Количество узлов на периферии")
table2<-cbind(меры2,table2)
table2
                          меры2 Кафедра  ИГН   РНД
1            Среднее расстояние    2.47 2.98  3.03
2                       Диаметр    5.00 8.00  7.00
3                        Радиус    3.00 4.00  4.00
4  Количество центральных узлов    9.00 4.00 39.00
5 Количество узлов на периферии   43.00 7.00  4.00

Сохраним в Excel:

library(xlsx)
write.xlsx(table2, "table2.xlsx")

Самостоятельная работа

  1. Определить по своим сообществам, являются ли они связными, сколько компонент связности имеется, сделать базовую визуализацию сообществ с учетом распределения по полу.
  2. Посчитать коэффициенты плотности, транзитивности и ассортативности (по полу и по степени)
  3. Свести всю информацию в отдельную таблицу, сделать текстовое описание.
  4. По каждой группе сохранить информацию о максимальной связной компоненте.
  5. Вычислить среднее расстояние по группам, диаметр и радиус, центр и периферию.
  6. Создать еще одну таблицу с этими показателями.
  7. Проанализировать по поиску ВК, кто является центральными узлами (входят в центр), постараться понять, что это за люди, почему они занимают такую позицию.