Social Network Analysis

The Copenhagen Networks Study Interaction represents a network of connections between more than 700 university students over a period of 4 weeks (Sapiezynski et al., 2019). The network consists of several layers, like physical proximity, phone calls, text messages, and facebook friends. For this social network analysis we have chosen to focus on the phone calls layer, because it is a more selective and exclusive social connection. Unlike Facebook friends or physical proximity, you do not call just anyone. A phone call requires a certain level of closeness, and it is therefore a stronger indicator of a genuine social relationship. This makes it particularly interesting to study which students actually call each other and how this shapes the community structure in ways that weaker ties like Facebook friendships may not reveal. The phone calls layer consists of a timestamp, caller, callee, and duration for each recorded call.

Loading Libraries

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

Setting seed for reproducibility.

set.seed(123)

Loading Data and Building Network

Data is loaded from Figshare as one csv-file and transformed into an igraph object for modelling.

# Read the edge list CSV
call_edges <- read.csv("calls.csv", header = TRUE)

# Inspect the raw data
head(call_edges)
##   timestamp caller callee duration
## 1       184    300    301      121
## 2      3920    512    299      670
## 3      5623    301    300      504
## 4      9252    401    457       -1
## 5     15466    512      0        5
## 6     15497    512      0       28
# Remove failed calls
call_edges <- call_edges[call_edges$duration > 0, ]

# Build the directed igraph object

call_g <- graph_from_data_frame(
  call_edges[, c("caller", "callee", "duration")],
  directed = TRUE
)

call_g
## IGRAPH 83d6ba1 DN-- 504 2654 -- 
## + attr: name (v/c), duration (e/n)
## + edges from 83d6ba1 (vertex names):
##  [1] 300->301 512->299 301->300 512->0   512->0   19 ->47  687->310 300->301
##  [9] 634->681 466->183 225->574 584->582 466->183 584->582 466->42  269->267
## [17] 678->470 222->471 222->471 222->471 152->405 471->222 289->578 289->578
## [25] 289->578 90 ->91  269->267 267->269 669->283 289->578 42 ->466 139->546
## [33] 139->546 474->639 289->578 19 ->47  341->89  341->89  278->31  218->382
## [41] 514->278 278->514 172->640 19 ->595 382->218 256->845 76 ->31  72 ->73 
## [49] 561->631 297->374 650->136 24 ->82  121->288 289->752 640->172 81 ->374
## [57] 419->8   622->39  8  ->592 39 ->622 622->39  252->668 121->288 651->623
## + ... omitted several edges

1. Description of the Network

Nodes represent individual students and edges represent a phone call made between two students. The network is directed, meaning you have one caller that call someone, and it is weighted by call duration. There is 504 nodes and 2654 edges in the network.

Sparsity measures how far a network is from being fully connected, where a value of 1 means no connections exist and 0 means every possible pair of nodes is connected. The sparsity of this network is 0.989, confirming it is highly sparse and most students are not connected to each other through phone calls. For this network that means that most students only call a small number of the total of students. This is natural as phone calls are seen as more “intimate” and we assume people mostly only call their closest ones. If we had looked at text messages on the other side the sparsity might have been lower because of the lower threshold of sending someone a text compared to calling them.

n <- vcount(call_g)
m <- ecount(call_g)
sparsity <- 1 - (m / (n * (n - 1)))  # directed formula

cat("Nodes:   ", n,
    "\nEdges:   ", m,
    "\nSparsity:", round(sparsity, 4))
## Nodes:    504 
## Edges:    2654 
## Sparsity: 0.9895
# Basic network visualization 
ggraph(call_g, layout = "lgl") + 
  geom_edge_link(color = "gray80", alpha = 0.4) + 
  geom_node_point(color = "steelblue", size = 1.5) + 
  theme_void() +
  labs(title = "Copenhagen Calls Network")

The network visualization shows a core of highly connected students on the left side, with a spread of students further out right with fewer connections. This structure is consistent with the high sparsity of 0.989 computed in Q1, and will be explored further in the analysis below.


2. Average Degree and Standard Deviation

The average degree in the network is 10.53, which means that each node on average has 10.53 direct connections. In other words on average each student calls or receive a call from around 10-11 people. The standard deviation of 14.74 is larger than the mean, which means a few students made or received many more calls than the average student. In this case the mean degree is influenced by highly connected outliers, which weakens its use as a good descriptor of the degree distribution. As one can see from the network visualization in Q1, there are several nodes with fewer connections than the average, which the mean degree alone fails to capture. Average degree can be a useful descriptor in a homogeneous network where most nodes have similar degrees, but given the large differences in calling behaviour between students, the distribution is too spread out for a single average to be meaningful.

deg <- degree(call_g, mode = "all")

cat("Mean degree:", round(mean(deg), 3),
    "\nSD degree:  ", round(sd(deg), 3))
## Mean degree: 10.532 
## SD degree:   14.744

3. Degree Distribution

In order to look at the degree distribution between the nodes we plot them on both a linear scale and a log-log scale for comparison.

# Linear scale
ggplot() + 
  geom_histogram(aes(x = degree(call_g, mode = "all")), 
                 bins = 30, fill = "steelblue", color = "white") + 
  labs(x = "Degree", y = "Count", 
       title = "Degree Distribution (linear scale)") +
  theme_minimal()

# Log-log scale
ggplot() +
  geom_histogram(aes(x = degree(call_g, mode = "all")), 
                 bins = 30, fill = "steelblue", color = "white") +
  scale_x_log10() +
  scale_y_log10() +
  labs(x = "Degree (log)", y = "Count (log)",
       title = "Degree Distribution (log-log scale)") +
  theme_minimal()

Considering the original linear scale is heavily right-skewed, with many nodes with low degrees and a few with much higher degrees giving it a long tail which nearly disappears on a linear scale. The log-log scale is therefore a better scale for the network’s distribution, because it pulls the large values more to the center making the whole distribution and pattern of degrees more visible. This reveals the shape of the tail more clearly, where most students have a low to moderate number of connections, while there is a small number of highly connected students out toward the right.


4. Degree Assortativity

The degree assortativity of 0.75 is high and positive, close to the maximum value of 1. This means that highly connected students tend to call and receive calls from other highly connected students. This can be interpreted as popular students with big networks, tend to hang out and talk with other popular students with good network and social skills. On the other side students with less connections tends to call their friends that are also more reserved and have smaller networks. It is interpreted as this considering how people usually hang out with other people similar to them. This structure can be looked at as a few highly connected hubs where high number of people connects with each other and social influence spreads fast. On the other side we then have the small peripheral group of more closed of students where this spread is more limited.

assortativity_degree(call_g, directed = TRUE)
## [1] 0.7464856

5. Community Detection

Two community detection methods were tested. First, considering this is a directed and weighted network cluster_edge_betweenness is used for community detection. However, it produced 192 communities, where the community sizes mostly were around 1-3, with a few outliers of 40-55. The range is wide and the community sizes are mostly on the lower side. This suggests over-fragmentation of the network.

Louvain was therefore preferred as the final method. Although it requires converting the graph to undirected, this is a justified simplification for phone call data, the direction of a call does not necessarily define a social relationship. A call between two students indicates a social connection regardless of who initiated it. It should be noted that the Louvain algorithm is stochastic, meaning results can vary slightly between runs. A fixed seed was therefore set before each run to ensure reproducibility.

# Edge betweenness — works on directed + weighted graphs
comm_eb <- cluster_edge_betweenness(call_g, directed = TRUE, 
                                     weights = E(call_g)$duration)

# Number of communities
cat("Number of communities:", length(comm_eb), "\n")
## Number of communities: 192
# Size of each community
sizes(comm_eb)
## Community sizes
##   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20 
##   9   1   2   4  40  55   6   4   2   3  18   8   1   1   2   2   2   2   1   3 
##  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40 
##   2   2   1   2  16   5   2   1   2   2   2   3   2   6   1   1   2   1   3   3 
##  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60 
##   2   4   7   2   3   3   3   2   2   1   3   3   2   1   1   2   2   6   1   2 
##  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80 
##   2   1   2   2   2   3   1   2   2   2   3   3   2   9   1   1   3   2   2   2 
##  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 
##   3   2   2   2   1   2   1   2   2   3   2   1   2   1   2   1   1   1   2   1 
## 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 
##   1   2   2   2   2   1   3   2   6   3   2   2   1   1   2   2   2   1   2   1 
## 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 
##   2   2   2   2   2   2   1   2   1   2   2   2   2   2   1   1   1   2   2   1 
## 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 
##   2   1   2   2   2   3   2   2   1   1   2   1   2   2   1   1   1   2   2   1 
## 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 
##   2   2   2   2   1   2   2   2   1   1   1   1   1   1   1   1   1   1   1   1 
## 181 182 183 184 185 186 187 188 189 190 191 192 
##   1   1   1   1   1   1   1   1   1   1   1   1
# Modularity
modularity(comm_eb)
## [1] 0.8063527
# Convert to undirected —
g_un <- as.undirected(call_g, mode = "collapse")

# Extract the largest connected component 
g_lcc <- decompose(g_un)[[1]]

set.seed(321)
# Louvain community detection
communities <- cluster_louvain(g_lcc)

cat("Number of communities:", length(communities), "\n")
## Number of communities: 17
# Size of each community
sizes(communities)
## Community sizes
##  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 
## 33 16 15 14 19 28 15 18 20 22 20 22 25 15 14 14 12
# Check quality of partition with modularity 
modularity(communities)
## [1] 0.8239373

With the Louvain method we got 17 communities with sizes ranging from 12 to 33 students and a modularity of 0.824.

Modularity measures how well a network divides into communities by comparing the density of connections within communities to what would be expected in a random network. A modularity of 0.824 is very high and means students call almost exclusively within their social group and very rarely across groups. In other words, the 17 communities are well separated from each other with few bridging calls between them.

Yes, the 17 detected communities make sociological sense for a university setting. Most students find their study partners and make study groups of maybe 12 people. In some cases you have whole classes with good dynamics and relationships, and a whole class of say 20 can be one community. In universities you also have organisations for sports, arts, music etc. this is another arena where students have the possibility to make many connections and get highly connected. Since phone calls are personal and you usually call people you’re close to, it is naturally that we have a few close groups. The even size of the groups ranging from 12-33 suggests that there is no big dominating social circle, but rather several ones that can arise from the study groups, sports teams, organisations we’ve mentioned earlier.

# Add community membership as node attribute
V(g_lcc)$community <- membership(communities)

set.seed(321)
# Static plot with ggraph 
ggraph(g_lcc, layout = "lgl") +
  geom_edge_link(color = "gray50", alpha = 0.3) +
  geom_node_point(aes(color = as.factor(community)), size = 2) +
  theme_void() +
  labs(title = "Copenhagen Calls Network — Community Structure",
       color = "Community")

The network visualization confirms this by showing the 17 clusters in different colors. In the core of the plot you can see closely connected communities, where you have socially active students calling people from other circles as well. The nodes more on the outside represent the communities where students call mostly within their own group, with a few nodes connecting them to the bigger network. Students in these circles are very socially active within their own circle, but not across the student population when it comes to calling. You also have some communities like community 9 that is only connected to the rest of the network through a narrow chain of nodes, if one of these nodes is disconnected the whole community is isolated. This layout is consistent with the high modularity of 0.824, confirming that most students call predominantly within their own social circle, with some bridging individuals maintaining ties across communities.


6. Degree and Betweenness Centrality

The nodes with highest degree centrality and is therefore the students who call or receive the most calls are 578, 289, 49, 90, 405, with calls ranging from 74-141. On the other side, the 5 top nodes with the highest betweenness centrality, meaning they act as bridges between two other nodes, are different ones with it being 263, 69, 221, 268, and 401. This means that the students that call or receives the most calls are not the same ones that connect the different groups of students.

This suggests that the nodes with the highest degrees are active with phone calls, but it is limited to a tight cluster of contacts. It can be interpreted as popular students that keep within their own social circle. High-betweenness nodes on the other side, make fewer calls but as they sit on the shortest path between students they are highly connected and can connect otherwise disconnected clusters. Most students keep within their circle, especially when it comes to making phone calls. But then you also have the students with connections outside the main friend group, these students might also have a sports team, or several study groups, or in general are more connected. This pattern is consistent with a network that has strong community structure, where most communication is within clusters, with only a few individuals maintaining ties across clusters.

This makes for an interesting discussion about importance in a social network. While degree centrality represent our conventional understanding of popularity, students who call or gets called frequently, Betweenness centrality captures popularity in another way. A student with high betweenness centrality is highly connected and have the ability to connect across groups, whether it is through participation in multiple study groups, sports teams, or other overlapping social contexts. The two metrics together suggest that the most active communicators and the most socially bridging individuals occupy fundamentally different roles within the network.

deg_cent <- degree(call_g, mode = "all")
btw_cent <- betweenness(call_g, directed = TRUE)

cat("Top 5 nodes by DEGREE centrality:\n")
## Top 5 nodes by DEGREE centrality:
print(sort(deg_cent, decreasing = TRUE)[1:5])
## 578 289  49  90 405 
## 141 122  76  75  74
cat("\nTop 5 nodes by BETWEENNESS centrality:\n")
## 
## Top 5 nodes by BETWEENNESS centrality:
print(sort(btw_cent, decreasing = TRUE)[1:5])
##       263        69       221       268       401 
## 11830.952  9928.870  9318.974  7794.992  7055.307
V(g_lcc)$degree     <- degree(g_lcc)
V(g_lcc)$betweenness <- betweenness(g_lcc)

ggraph(g_lcc, layout = "lgl") +
  geom_edge_link(color = "gray80", alpha = 0.3) +
  geom_node_point(aes(size = betweenness, color = degree)) +
  scale_color_gradient(low = "steelblue", high = "red") +
  theme_void() +
  labs(title = "Node Degree (color) and Betweenness (size)",
       color = "Degree", size = "Betweenness")

Looking at the visualization, it also becomes clear that the separation between degree and betweenness is not absolute. There are some nodes that are both large and red, meaning they score high on both measures at the same time. These nodes sit in the dense core of the network, and they represent students who are both very active callers and also connect different groups. So in the core of the network, the two roles seem to overlap more.

What is interesting is that when you move to the edges of the network, you start seeing large but more blue-colored nodes, which is students with high betweenness but low degree. These are students who do not call that much but happen to connect different clusters together. This adds another layer to the interpretation, because it shows that the network is not uniform. In the center you have students that are both popular and well-connected across groups, while on the outside you have students that are structurally important without being particularly active.

7. Clustering Coefficient vs. Configuration Model

Lastly, we will test whether the clustering coefficient of the network can be statistically explained by a configuration model. The clustering coefficient measures how likely it is that two friends of a node is also friends with each other. The configuration model connects the nodes randomly while keeping the degree distribution, testing if students called each other purely based on how popular they are. We can analyse this by looking at if the clustering in the phone calls network is higher than what the degree distribution can predict.

# Largest Connected Component
comp  <- components(g_un)
g_lcc <- induced_subgraph(g_un, which(comp$membership == which.max(comp$csize)))
cat("LCC covers", round(vcount(g_lcc) / vcount(g_un) * 100, 1), "% of nodes\n")
## LCC covers 63.9 % of nodes
# Observed clustering coefficient
real_cc <- transitivity(g_lcc, type = "average")
cat("Observed clustering coefficient:", round(real_cc, 4), "\n")
## Observed clustering coefficient: 0.2387
# Null distribution via configuration model (1000 iterations)
set.seed(42)
null_cc <- replicate(1000, {
  g_null <- sample_degseq(degree(g_lcc), method = "vl")
  transitivity(g_null, type = "average")
})

# p-value: proportion of nulls >= observed
p_val <- mean(null_cc >= real_cc)
cat("p-value:", p_val, "\n")
## p-value: 0
cat("Mean null CC:", round(mean(null_cc), 4), "\n")
## Mean null CC: 0.0108
# Plot
ggplot() +
  geom_histogram(aes(x = null_cc), bins = 30,
                 fill = "steelblue", color = "white", alpha = 0.7) +
  geom_vline(xintercept = real_cc, color = "red",
             linewidth = 1.2, linetype = "dashed") +
  xlim(0, max(max(null_cc), real_cc) + 0.05) +   # <-- fixed
  annotate("text", x = real_cc, y = Inf,
           label = paste("Observed CC =", round(real_cc, 3)),
           color = "red", hjust = 1.1, vjust = 2, size = 4) +
  labs(title = "Null Model vs Observed Clustering Coefficient (Phone Calls)",
       x = "Clustering Coefficient", y = "Count") +
  theme_minimal()

The clustering coefficient cannot be explained by the configuration model. This is shown by p < 0.001 and the red dashed line marks in the plot above, showing the observed clustering coefficient of 0.239 in the real network, which sits far to the right with no overlap with the null distribution whatsoever. This means that none of the randomized models were anywhere similar to the to the real clustering in the network. Clustering in the network is not just a consequence of some students calling more than others, it means that students rather call inside their social circles. A clustering coefficent of 0.24 is much higher than the predicted coefficient of 0.0108 from the randomized model. This also tells us something about the nature of phone calls being a more selective type of communication and happens within tight social groups, and rarely randomly across clusters.

Moreover, the percentage of nodes in the Largest Connected Component is 63.9%, meaning that over half of the students are connected through chains of phone calls. The remaining 36% exist in smaller disconnected groups or as isolated nodes, where they either only call within their own tiny separate clusters, or do not appear in any recorded call between study participants at all. This could also be due to the fact that the dataset only captures calls between participants, so some students may simply have called people outside the study. This fragmentation can also tell us something about the social network of phone calls between students. As mentioned before, the threshold for calling someone is higher than for example sending them a message or a friend request. It is more natural to call someone you are closer with, and it is therefore expected that the network consists of smaller tight clusters. This is consistent with the high clustering coefficient relative to the null model which tells us that the students who do call each other tend to do so within the same close social circles.

Sources

Use of Artificial Intelligence: Claude Sonnet 4.6 (Anthropic) was used for assistance with the following parts of the code: fixing column name errors, selecting and implementing community detection methods, building the configuration model null distribution for Q7, and formatting visualizations using ggraph. Claude was also consulted to flag methodological issues such as the over-fragmentation of edge betweenness on a sparse network. All written interpretations, discussions, and conclusions are our own.

Sapiezynski, P., Stopczynski, A., Lassen, D. D., & Lehmann, S. (2019). Interaction data from the Copenhagen Networks Study [Data set]. figshare. https://doi.org/10.6084/m9.figshare.7267433

Sapiezynski, P., Stopczynski, A., Lassen, D. D., & Lehmann, S. (2019). Interaction data from the Copenhagen Networks Study. Scientific Data, 6, 315. https://doi.org/10.1038/s41597-019-0325-x