Analyzing Sociocentric Data: The `netwrite` Function

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)

ideanet aims to simplify learning and performing network analysis in R, which is currently arduous and time-consuming because necessary tools span multiple packages. Each package has its own data formats and syntax, leading to difficulties in choosing the right function as well as potential conflicts between packages. Packages often assume data order and default settings, which may not be readily apparent to new users, leading to unrecognized data processing errors. ideanet resolves these challenges by integrating them into a cohesive set of functions that enable seamless, high-quality network measurements from initial data, making it more accessible for researchers.

This package, as part of the broader IDEANet project, is supported by the National Science Foundation as part of the Human Networks and Data Science - Infrastructure program (BCS-2024271 and BCS-2140024).

Sociocentric Data Processing and Analysis

Global, or sociocentric, networks capture a full census of actors (typically referred to as nodes or vertices) and the relationships between them (typically referred to as ties or edges) in a given context of interest (such as a classroom, hospital, city, etc.). Users applying ideanet to sociocentric data can use the netwrite function to generate an extensive common set of measures and summaries of their networks, which may be stored in a variety of data structures.

Network data are generally represented as two linked datasets: the edgelist capturing relations and the nodelist capturing attributes of each node. In an edgelist each row represents an edge of a particular type connecting one node, i, to another node, j, both of whom are represented by a unique ID number. In a directed network, one column represents the sender of a tie while another represents the receiver. If the network is undirected, ties between nodes have no direction, and these columns merely represent the two nodes at the ends of a tie. Edgelists can also contain additional columns representing edge attributes, such as the relational type, strength or duration.

Edgelists are often accompanied by a nodelist containing attribute information about nodes. In a nodelist, each row represents a node in the network and each column is a node attribute. One of the columns is an ID that matches the unique ID number in the edgelist. If your network contains isolates – nodes with no relations – a nodelist is needed to retain information about them, as they cannot be represented in the edgelist.

To familiarize ourselves with netwrite and other functions for sociocentric data, we'll work with a nodelist and an edgelist representing a simulated network of friendships in an American high school ("Faux Mesa High") borrowed from the \code{statnet} package. Friendship ties between nodes (students) are stored in the fauxmesa_edges data frame, while attributes of individual nodes are contained in fauxmesa_nodes (both of which are native to ideanet):

library(ideanet)

fauxmesa_edges <- fauxmesa_edges
fauxmesa_nodes <- fauxmesa_nodes

Let's look over these two data frames:

dplyr::glimpse(fauxmesa_edges)

This edgelist represents 203 directed connections between students. Looking at our nodelist, we see that we have information about grade level, race/ethnicity, and sex for 205 students.

dplyr::glimpse(fauxmesa_nodes)

The netwrite function will generate a comprehensive set of node and system-level measures for a network. netwrite asks users to specify several arguments pertaining to node-level input data, edge-level input data, and function outputs. To familiarize ourselves with this function, we list these arguments below, organized by category.

Edge-Level Arguments

Node-Level Arguments

Output Arguments

Now let's use netwrite to get a better understanding of this school's friendship network:

nw_fauxmesa <- netwrite(data_type = "edgelist",
                        nodelist = fauxmesa_nodes,
                        node_id = "id",
                        i_elements = fauxmesa_edges$from,
                        j_elements = fauxmesa_edges$to,
                        directed = TRUE,
                        net_name = "faux_mesa",
                        shiny = TRUE)

Many network measures only apply to networks with particular structures. For example, eigenvector based methods cannot apply to isolates and many measures assume a network with one large connected component. In cases (as here), where the network does not conform to those expectations, we have made choices that seem reasonable to us (such as assigning NA values or running the measure separately by connected component) and send a warning to the output. Users should take care to inspect these warnings to see if they apply to measures they intend to use in analysis and that they agree with our choices. Here we see that certain centrality measures have been adjusted to account for the presence of singular matrices, multiple components, and isolated nodes.

Upon completion, netwrite stores its outputs in a single list object. In the following section, we'll examine each of the outputs within this list and what they contain.

Interpreting netwrite Output

System-Level Measures

netwrite outputs multiple measures aimed at characterizing the network's global structure. One can view a select set of these measures in a summary visualization stored in the system_measure_plot object:

nw_fauxmesa$system_measure_plot

A more comprehensive set of measures is available in traditional table form via the system_level_measures object:

head(nw_fauxmesa$system_level_measures)
knitr::kable(head(nw_fauxmesa$system_level_measures))

igraph Object(s)

igraph is one of the standard network analysis packages in R. netwrite creates an igraph object that contains all of the original data from the input nodelist and edgelist, plus edge-level and node-level metrics computed on the network by netwrite. This igraph object allows for traditional network manipulation, such as plotting. The igraph object will bear the name users specify in netwrite's net_name argument (here faux_mesa); otherwise it will be stored as an object named network.

nw_fauxmesa$faux_mesa

Note that this igraph object has various measures embedded in it as node- and edge- attributes. Having these measures already contained in the igraph object ensures that node attributes are properly linked to the network object, which allows us to use them when customizing network visualizations. Here we plot our network with nodes colored by student grade level, which appeared in our original nodelist:

plot(nw_fauxmesa$faux_mesa,
     vertex.label = NA,
     vertex.size = 4,
     edge.arrow.size = 0.2,
     vertex.color = igraph::V(nw_fauxmesa$faux_mesa)$grade)

In addition to the full network, researchers may be interested in the shape of major sub-components. netwrite outputs two additional graph objects: the largest component in the network, and the largest bi-component of the network.

plot(nw_fauxmesa$largest_component, vertex.label = NA, vertex.size = 2, edge.arrow.size = 0.2, 
     main = "Largest Component")
plot(nw_fauxmesa$largest_bi_component, vertex.label = NA, vertex.size = 2, edge.arrow.size = 0.2, 
     main = "Largest Bicomponent")

In some cases, networks may have 2+ largest components of equal size. When this occurs, netwrite will store each of the largest components as a list so that users may access them all.

Edgelist

netwrite outputs an edgelist dataframe of the same length as the input edgelist. This edgelist object contains unique dyad-level ids, simplified ego and alter ids (i_id and j_id, respectively), and the original id values and weights as they initially appeared in edges (uniformly set to 1 if no weights are defined).

head(nw_fauxmesa$edgelist)
knitr::kable(head(nw_fauxmesa$edgelist))

You may notice that i_id and j_id are zero-indexed. This is done to maximize compatibility with the igraph package.

Node-Level Measures

Finally, netwrite returns several popular node-level measures as a dataframe of values and plots their distributions. These are accessed via the node_measures and node_measure_plot objects, respectively. The metrics set are restricted to those applicable to the type of graph (weighted/unweighted, directed/undirected).

head(nw_fauxmesa$node_measures)
knitr::kable(head(nw_fauxmesa$node_measures))

On first glance, one sees that the node_measures dataframe contains simplified node identifiers matching those appearing in edgelist. One also sees that node_measures contains all original node-level attributes as they appeared in our original nodelist. Depending on how it was initially named, a nodelist's original column of node identifiers may be renamed to original_id.

nw_fauxmesa$node_measure_plot

netwrite makes it simple to compute complex structural metrics on existing relational data. The output of netwrite is designed to facilitate the discovery process by providing key visualizations that help support exploratory analysis.

Adjacency Matrices

In addition to edgelists, netwrite supports processing and analysis of network data stored as an adjacency matrix. An adjacency matrix is a square matrix in which each row and each column corresponds to an individual node in the network. The value of a given cell in this matrix, [i, j], indicates the existence of a tie from node i to node j. Here we provide a quick example of how to use netwrite on an adjacency matrix. The matrix below represents a network of 9 nodes, the ties between which form all possible triads and motifs that can appear in a directed network.

triad

Now we pass this matrix into netwrite.

nw_triad <- netwrite(data_type = "adjacency_matrix",
                     adjacency_matrix = triad,
                     directed = TRUE,
                     net_name = "triad_igraph",
                     shiny = TRUE)

To show that we've successfully processed this matrix, let's plot the igraph object produced by netwrite:

plot(nw_triad$triad_igraph,
     edge.arrow.size = 0.2,
     vertex.label = NA)

Multirelational Networks

In some networks, edges may represent one of several different types of relationships between nodes. These multirelational (or multiplex) networks often demand more detailed processing and analysis— users may want to subset these networks by each edge type and calculate measures based on each subset. netwrite handles such processing and analysis in a streamlined manner while making minimal additional user demands. The function only requires that a multirelational network's edgelist is stored in a long format in which each dyad-relationship type combination is given its own row.

To show how netwrite works with multirelational networks, we'll work with an edgelist of relationships between prominent families in Renaissance-era Florence. Here edges between nodes can represent marriages or business transactions between families:

head(florentine_edges)
knitr::kable(head(florentine_edges, n = 10))

To treat this network as multirelational, we only need to specify which column in this edgelist indicates the type of each edge in the network. We do this using the type argument:

nw_flor <- netwrite(nodelist = florentine_nodes,
                    node_id = "id",
                    i_elements = florentine_edges$source,
                    j_elements = florentine_edges$target,
                    type = florentine_edges$type,
                    directed = FALSE,
                    net_name = "florentine")

When given a multi-relational network, netwrite will return the outputs described previously in slightly different ways. First, we can see that the edgelist object is now a list containing an edgelist subset by each type of tie. Additionally, this list contains a complete edgelist for the summary_graph containing all ties.

head(nw_flor$edgelist$business)
knitr::kable(head(nw_flor$edgelist$business))
head(nw_flor$edgelist$summary_graph)
knitr::kable(head(nw_flor$edgelist$summary_graph))

node_measures remains a single data frame, but now includes each node-level metric calculated for each individual relation type as well as the overall graph. We see here that netwrite has calculated 3 different values for total_degree. However, node_measures_plot is now a list containing summary visualizations for each relation type as well as the overall summary_graph.

knitr::kable(nw_flor$node_measures %>% 
  dplyr::select(id, total_degree, marriage_total_degree, business_total_degree) %>%
  head())

Similarly, system_level_measures remains a single data frame, while system_measure_plot has become a list containing multiple visualizations. Note that system_level_measures now contains additional column detailing measure values for each individual relation type.

head(nw_flor$system_level_measures)
knitr::kable(head(nw_flor$system_level_measures))

netwrite also produces both an igraph object of the overall network, as it does with networks with a single relation type, as well as a list of igraph objects for each subset of the network. Here we access the igraph_list object to compare business and marriage relationships between families side-by-side:

# Create a consistent layout for both plots
flor_layout <- igraph::layout.fruchterman.reingold(nw_flor$igraph_list$marriage)
plot(nw_flor$igraph_list$marriage, vertex.label = NA, vertex.size = 4, edge.arrow.size = 0.2, 
     vertex.color = "gray", main = "Marriage Network", layout = flor_layout)
plot(nw_flor$igraph_list$business, vertex.label = NA, vertex.size = 4, edge.arrow.size = 0.2, 
     vertex.color = "red", main = "Business Network", layout = flor_layout)

Community Detection

When analyzing a network, users are often interested in whether nodes cluster together to form distinct subgroups or communities. Many methods exist for identifying discernible communities in a network, and one might want to know how different methods perform the same task. ideanet's comm_detect function leverages several community detection algorithms found in the igraph package, as well as a couple of others, to find and compare inferred communities across these methods. Where relevant, each method is only run at default values here so, for instance, the edge_betweenness method will warn that mamberships "will be selected based on the highest modularity score" from the dendrogram generated by the method. Similarly cluster_leiden is run here at the default resolution parameter for modularity and at a resolution equal to the average weighted density of the network for the constant Potts model.

Using comm_detect is simple: you only needs to pass an igraph object produced by netwrite into the function. Let's quickly apply several community detection methods to the Florentine families network we just processed.

flor_communities <- comm_detect(nw_flor$florentine)

The comm_detect function returns a list of three data frames, and will automatically generate a set of visualizations showing each node's community membership as determined by each community detection method. Within the list produced, the summaries data frame details the number of communities detected by each method, as well as the modularity score associated with each method. This offers one way of comparing community detection methods— higher modularity scores (within a single network) typically indicate more effective partitioning of the network (though there are many scores that one can use).

flor_communities$summaries
knitr::kable(flor_communities$summaries)

A second data frame in the list, score_comparison, allows for further comparison of community detection methods. score_comparison contains a matrix of adjusted Rand values indicating the level of similarity between two methods in how they assigned nodes to communities. This matrix tells us, for example, that the Fast-Greedy and Leading Eigenvector methods were identical in their community assignment:

flor_communities$score_comparison
knitr::kable(flor_communities$score_comparison)

memberships, the final data frame in the list, shows each node's community membership according to each of the methods used.

flor_communities$memberships
knitr::kable(head(flor_communities$memberships))

memberships is designed to be easily merged with the node_measures data frame produced by netwrite, should users be inclined to combine the two.

node_info <- nw_flor$node_measures %>%
  dplyr::left_join(flor_communities$memberships, by = "id")


Try the ideanet package in your browser

Any scripts or data that you put into this service are public.

ideanet documentation built on April 3, 2025, 11:55 p.m.