As discussed in the Bipartite Graphs & Two-Mode Networks lecture, bipartite graphs are useful for operationalizing contexts where nodes come from two separate classes. In contrast to one-mode networks, or unipartite graphs, where edges can be incident within a particular node/vertex set, in two-mode or bipartite graphs there are two partitions of nodes (called modes), and edges only occur between these partitions (i.e. not within). In the Projection & Weighted Graphs lecture, we saw that projection is the process by which we map the connectivity between modes to a single mode. And, we saw that we can use a weighted graph or a binarized graph in the analysis of the project.

In this lab, we will review how a unipartite or one-mode network can be created through projection. We will also review how to work with weighted graphs.

Why are you learning this? Bipartite graphs are a common network structure used in many network analysis projects. Being able to work with these objects and summarize their structure is an important tool for your skill set as a social network analyst!


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


Transposition

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 ).


Matrix Multiplication in R

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


Projections

The Person Matrix

Ok, so we have created the projections, which are the one-mode networks that represent information in the two-mode networks. Recall from the Projection & Weighted Graphs lecture 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( 605 )

# 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.


The Group Matrix

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( 605 )

# 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.


Structural Properties of Young & Ready’s (2015) Police Officer Network

Now, let’s work with a real example. As discussed in the Bipartite Graphs & Two-Mode Networks lecture, 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.

The adjacency matrix is available in the SNA Textbook data folder. Let’s import the the matrix, create a network, assign an attribute, and plot it. Then, we will work through the structural properties.

# 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.

# identify the number of police officers
N <- dim( camMat )[1]

# identify the number of incidents
M <- dim( camMat )[2]


Create the Projections

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( 605 )

# 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.


Using the Weights in the Plot

Note that when we create the projection, the matrix is actually a weighted matrix. We saw this in the Projection & Weighted Graphs lecture. We can 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.


Reworking the rescale() function to incorporate edge weights

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. We will plot the bipartite graph, the officer network, and the officer network with the shading.

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

# 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
set.seed( 605 )
gplot( camMatP,
       gmode = "graph",
       usearrows = FALSE,
       vertex.col = "#34e5eb"
       )
title( "Unipartite Projection of\n Officers (Person) Matrix\n (no shading)", line = 1 )

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

Now, we will plot the bipartite graph, the incident network, and the incident network with the shading.

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

# 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 group matrix
set.seed( 605 )
gplot( camMatG,
       gmode = "graph",
       usearrows = FALSE,
       vertex.col = "#4f0a1a",
       vertex.sides = 4
       )
title( "Unipartite Projection of\n Incidents (Group) Matrix\n (no shading)", line = 1 )


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



Wrapping up…

In this lab, we reviewed how a unipartite or one-mode network can be created through projection. We also reviewed how to work with weighted graphs.


Back to SAND main page


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


Back to SAND main page