This lab works with unipartite/one-mode projections of two-mode networks.


Introduction

As discussed in Chapter 10: Bipartite Graphs/Two-Mode Networks of the SNA Textbook, bipartite graphs are useful for operationalizing contexts where nodes come from two separate classes.

Further, as discussed in Chapter 11: Projection, projection is the process by which we map the connectivity between modes to a single mode. For example, consider a two-mode network of people in groups. By projecting, we get:

  • One-mode network of people connected to people by groups.

  • One-mode network of groups connected by people.

In this tutorial, we will review how a unipartite or one-mode network can be created through projection.


One-Mode Networks by Projection

How do we do this?

Following Breiger (1974), we can build the adjacency matrix for each projected network through matrix algebra. Specifically, multiplying an adjacency matrix by it’s transpose. The transpose of a matrix A simply reverses the columns and rows: \(\sf{A^T_{ij}}\) = \(\sf{A_{ji}}\).

The two-mode, NxM, adjacency matrix, when multiplied by it’s transpose, produces either:

  • An NxN matrix (ties among N nodes via M)

  • An MxM matrix (ties among M nodes via N)

To examine how this works, let’s first set up an example:


# clear the workspace
rm( list = ls() )

# create an example matrix
A <- rbind(
  c( 1,1,0,0,0 ),
  c( 1,0,0,0,0 ),
  c( 1,1,0,0,0 ),
  c( 0,1,1,1,1 ),
  c( 0,0,1,0,0 ),
  c( 0,0,1,0,1 )
  )

# name the rows and columns 
rownames( A ) <- c( "A","B","C","D","E","F" )
colnames( A ) <- c( "1","2","3","4","5" )

# print out the matrix
A
##   1 2 3 4 5
## A 1 1 0 0 0
## B 1 0 0 0 0
## C 1 1 0 0 0
## D 0 1 1 1 1
## E 0 0 1 0 0
## F 0 0 1 0 1


In R, the t() function, or transpose() returns the transposition of a matrix. To see the the help on the transpose() function, just use ?t to pull up the help page.


# print the transpose of our example
t( A )
##   A B C D E F
## 1 1 1 1 0 0 0
## 2 1 0 1 1 0 0
## 3 0 0 0 1 1 1
## 4 0 0 0 1 0 0
## 5 0 0 0 1 0 1

What is different? Compare the difference between A and t( A ).


To create the project, we need to use matrix algebra. To multiply matrices in R, we have to use the following operator: %*%. This is different then * in that %*% tells R to use matrix multiplication. For example, compare the differences:


# create two matrices
a <- matrix( c(1,1,1,1), nrow=2, byrow=TRUE )
b <- matrix( c(2,2,2,2), nrow=2, byrow=TRUE )

# multiply the first element in a by the first element in b
a * b
##      [,1] [,2]
## [1,]    2    2
## [2,]    2    2
# multiply the matrix a by the matrix b
a %*% b
##      [,1] [,2]
## [1,]    4    4
## [2,]    4    4

What is the difference? When we use a * b, it is not using matrix multiplication.


To multiply two matrices, the number of columns in the first matrix must match the number of rows in the second matrix. This is called conformability. Only matrices with conformable dimensions can be multiplied. For example, 5x6 X 6x5 works, but not 5x6 X 5x6. When two matrices are multiplied by each other, this renders the product matrix. The product matrix has the number of rows equal to the first matrix and the number of columns equal to the second matrix.


Recall that from the two-mode, NxM, adjacency matrix, we can produce two different matrices:

  • An NxN matrix (ties among N nodes via M)

    • This is created by using NxM X t(NxM), or NxM X MxN
  • An MxM matrix (ties among M nodes via N)

    • This is created by using t(NxM) X NxM, or MxN X NxM

Let’s create each of these with our example network.


# create the NxN matrix
A %*% t( A )
##   A B C D E F
## A 2 1 2 1 0 0
## B 1 1 1 0 0 0
## C 2 1 2 1 0 0
## D 1 0 1 4 1 2
## E 0 0 0 1 1 1
## F 0 0 0 2 1 2
# create the MxM matrix
t( A ) %*% A
##   1 2 3 4 5
## 1 3 2 0 0 0
## 2 2 3 1 1 1
## 3 0 1 3 1 2
## 4 0 1 1 1 1
## 5 0 1 2 1 2


Ok, so we have created the projections, which are the one-mode networks that represent information in the two-mode networks. Recall from Chapter 11: Projection that Breiger (1974), referred to the person matrix (i.e. the NxN matrix) and the group matrix (i.e. the MxM matrix).


# create the "person" matrix: recall this is A X t(A)
P <- A %*% t( A )

P
##   A B C D E F
## A 2 1 2 1 0 0
## B 1 1 1 0 0 0
## C 2 1 2 1 0 0
## D 1 0 1 4 1 2
## E 0 0 0 1 1 1
## F 0 0 0 2 1 2

What does the diagonal represent in this matrix? What do the off-diagonal elements represent?


The diagonal elements represent the number of nodes in the second mode of the bipartite graph to which a node is connected. Put differently, if we have a two-mode network where one set of nodes are individuals and the other set of nodes are events, then the diagonal of the projection for the “person” matrix represents the number of events that an individual attended.

The off-diagonal elements represent the ties between nodes in the same node set of the bipartite graph. That is, ties between individuals.


To visualize this by plotting each network, then we can better see what is happening. We will use the gplot() function, so be sure to call the sna package library.


# call the library
library( sna )

# set the plot regions to ease with visualization
par( 
  mfrow = c( 1, 2 ),
  mar = c( 0, 1, 4, 1)
  )

# set the seed to reproduce the plot
set.seed( 507 )

# plot the bipartite network
gplot( A,
       gmode = "twomode",
       main = NA,
       usearrows = FALSE,
       label = c( "A","B","C","D","E","F", "1","2","3","4","5" ),
       label.pos = 5,
       vertex.cex = 2,
       vertex.col = c(                   # create a vector of colors
         rep( "#fa6e7a", dim( A )[1] ),  # the first color is the number of nodes in the first mode
         rep( "#00aaff", dim( A )[2] ) ) # the second color is the number of nodes in the second mode
       )
title( "Bipartite Matrix", line = 1 )

# plot the person matrix
gplot( P,
       gmode = "graph",
       label = c( "A","B","C","D","E","F" ),
       main = NA,
       usearrows = FALSE,
       label.pos = 5,
       vertex.cex = 2,
       vertex.col = "#fa6e7a",
       vertex.sides = 99.      # set the shapes to be circles
       )
title( "Unipartite Projection of\n Person Matrix", line = -1 )

From the plots we can see what the projection is doing. It is creating a unipartite graph based on the ties in the bipartite graph. For example, consider nodes A, B, and C. In the bipartite graph, B, is connected to A and C through node 1. We see this in the unipartite graph where A, B, and C are connected. In other words, we have taken the links between A, B, and C in the bipartite graph and reproduced them in a unipartite graph.


Now, let’s take a look at the “group” matrix. The projection for the “group” matrix has a different interpretation. Let’s work through this to see it.


# create the "group" matrix: recall this is t(A) X A
G <- t( A ) %*% A

G
##   1 2 3 4 5
## 1 3 2 0 0 0
## 2 2 3 1 1 1
## 3 0 1 3 1 2
## 4 0 1 1 1 1
## 5 0 1 2 1 2

What does the diagonal represent in this matrix? What do the off-diagonal elements represent?


The diagonal elements represent the number of nodes in the first mode of the bipartite graph to which a node is connected. If we have a two-mode network where one set of nodes are individuals and the other set of nodes are events, then the diagonal of the projection for the “group” matrix represents the number of individuals that attend an event. The off-diagonal elements represent the ties between events. Essentially, how events are connected by people attending them. Let’s plot this to see it.


# call the library
library( sna )

# set the plot regions to ease with visualization
par( 
  mfrow = c( 1, 2 ),
  mar = c( 0, 1, 4, 1)
  )

# set the seed to reproduce the plot
set.seed( 507 )

# plot the bipartite network
gplot( A,
       gmode = "twomode",
       main = NA,
       usearrows = FALSE,
       label = c( "A","B","C","D","E","F", "1","2","3","4","5" ),
       label.pos = 5,
       vertex.cex = 2,
       vertex.col = c( 
         rep( "#fa6e7a", dim( A )[1] ), 
         rep( "#00aaff", dim( A )[2] ) )
       )
title( "Bipartite Matrix", line = 1 )

# plot the person matrix
gplot( G,
       gmode = "graph",
       label = c( "1","2","3","4","5" ),
       main = NA,
       usearrows = FALSE,
       label.pos = 5,
       vertex.cex = 2,
       vertex.col = "#00aaff",
       vertex.sides = 4        # set the shape to be a square
       )
title( "Unipartite Projection of\n Group Matrix", line = -1 )


From the plots we can see what the projection is doing. It is creating a unipartite graph based on the ties in the bipartite graph, but this time it is for the other node set. For example, consider nodes 1, 2, and 3. In the bipartite graph, 1, is connected to 2 through node A and C. Node 2 and 3 are connected through node D. We see this in the unipartite graph where 1 is connected to 2 and 2 is connected to 3. In other words, we have taken the links between 1, 2, and 3 in the bipartite graph and reproduced them in a unipartite graph.


Empirical Example

Now, let’s work with a real example. As discussed in the Introduction to Social Network Analysis chapter of the SNA Textbook, Young & Ready (2015) examined how police officers develop cognitive frames about the usefulness of body-worn cameras. They argued that police officers views of body-worn cameras influence whether they use their cameras in incidents and that these views partly result from sharing incidents with other officers where they exchanged views about the legitimacy of body-worn cameras.

We worked through this example in Tutorial 08 - Bipartite Graphs and Two-Mode Networks in R, so let’s revisit it here.

The adjacency matrix is available in the SNA Textbook data folder. Let’s import the the matrix and then examine the projections.


# set the location for the file
loc <- "https://github.com/jacobtnyoung/sna-textbook/raw/main/data/data-officer-events-adj.csv"

# read in the .csv file
camMat <- as.matrix(
  read.csv( 
    loc,
    as.is = TRUE,
    header = TRUE,
    row.names = 1 
    )
  )

We can check the dimensions of the matrix using the dim() function. The camMat matrix has 81 rows and 153 columns. Recall that this is police officers and incidents, so there are 81 police officers and 153 incidents that connect officers.

Now that we have it put together, let’s create the projections. To do this, we just need to employ matrix multiplication on the camMat matrix.

# create the "person" matrix
camMatP <- camMat %*% t( camMat )

# create the "group" matrix
camMatG <- t( camMat ) %*% camMat


Now, let’s plot the networks!

# set the plot regions to ease with visualization
par( 
  mfrow = c( 2, 2 ),
  mar = c( 2, 1, 4, 1)
  )

# set the seed to reproduce the plot
set.seed( 507 )

# plot the bipartite network
gplot( camMat,
       gmode="twomode",
       usearrows=FALSE,
       edge.col="grey60",
       vertex.col = c( 
         rep( "#34e5eb", dim( camMat )[1] ), 
         rep( "#4f0a1a", dim( camMat )[2] ) ),
       edge.lwd=1.2
       )
title( "Bipartite Matrix of Officers and Incidents", line = 1 )

# plot the person matrix
gplot( camMatP,
       gmode = "graph",
       usearrows = FALSE,
       edge.col="grey60",
       edge.lwd=1.2,
       vertex.col = "#34e5eb"
       )
title( "Unipartite Projection of\n Officers (Person) Matrix", line = 1 )

# plot the group matrix
gplot( camMatG,
       gmode = "graph",
       usearrows = FALSE,
       edge.col="grey60",
       edge.lwd=1.2,
       vertex.col = "#4f0a1a",
       vertex.sides = 4
       )
title( "Unipartite Projection of\n Incidents (Group) Matrix", line = 1 )


Now that we have reduced our bipartite graph to a unipartite graph, we can employ the same descriptive tools we have previously used.

CAUTION: Note that when we create the projection, the matrix is actually a weighted matrix. That is, the off-diagonal elements are not just 0 and 1 (i.e. binary). Rather, the represents the number of nodes to which there is adjacency. As mentioned in Chapter 11: Projection of the SNA Textbook, we dichotomizing the adjacency matrix for ease of analysis.


We can actually use this information in our plot. That is, we can use the weighted matrix to shade the edges (darker are larger weights) and size the edges (where larger are bigger weights). To see how this works, let’s build a few functions.

First, we will create the edge.rescale() function to help us here. This function returns a weighted edgelist that can be used to aid with plotting. Then, we will create the edge.shade() function that shades the edges based on the size of the edge.


edge.rescale <- function( uniMat, low, high ){
  diag( uniMat ) <- 0
  min_w <- min( uniMat[uniMat != 0] )
  max_w <- max( uniMat[uniMat != 0] )
  rscl <- ( ( high-low )  * ( uniMat[uniMat != 0] - min_w ) ) / ( max_w - min_w ) + low
  rscl
}

edge.shade <- function( uniMat ){
  net.edges <- edge.rescale( uniMat, 0.01, 1 )
  vec.to.color <- as.vector( abs( net.edges ) )
  vec.to.color <- 1 - vec.to.color # subtract 1 to flip the grey function scale.
  edge.cols <- grey( vec.to.color )
  return( edge.cols )
}

Now, let’s plot the networks with the edges adjusted.

# set the plot regions to ease with visualization
par( 
  mfrow = c( 2, 2 ),
  mar = c( 2, 1, 4, 1)
  )

# set the seed to reproduce the plot
set.seed( 507 )

# plot the bipartite network
gplot( camMat,
       gmode="twomode",
       usearrows=FALSE,
       edge.col="grey60",
       vertex.col = c( 
         rep( "#34e5eb", dim( camMat )[1] ), 
         rep( "#4f0a1a", dim( camMat )[2] ) ),
       edge.lwd=1.2
       )
title( "Bipartite Matrix of Officers and Incidents", line = 1 )

# plot the person matrix
gplot( camMatP,
       gmode = "graph",
       usearrows = FALSE,
       edge.col = edge.shade( camMatP ),
       edge.lwd = edge.rescale( camMatP, 0.1, 5 ),
       vertex.col = "#34e5eb"
       )
title( "Unipartite Projection of\n Officers (Person) Matrix", line = 1 )

# plot the group matrix
gplot( camMatG,
       gmode = "graph",
       usearrows = FALSE,
       edge.col = edge.shade( camMatG ),
       edge.lwd = edge.rescale( camMatG, 0.1, 5 ),
       vertex.col = "#4f0a1a",
       vertex.sides = 4
       )
title( "Unipartite Projection of\n Incidents (Group) Matrix", line = 1 )



Please report any needed corrections to the Issues page. Thanks!