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!
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.
## 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
## [,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)
An MxM matrix (ties among M nodes via N)
Let’s create each of these with our example network.
## 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
## 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 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).
## 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.
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.
## 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.
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]
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.
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.
rescale()
function to incorporate
edge weightsFirst, 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 )
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.