rm( list = ls() )
Working with Networks in R
In the Network Data Structures chapter of the Social Network Analysis for Crime Analysts textbook, we reviewed how networks can be represented as sociomatrices. These matrices form the basic unit of data analysis in SNA. In this tutorial, we will review how we can build networks in R. Understanding how to work with networks in R is essential for crime analysts because networks provide a powerful lens for analyzing relationships and interactions that drive criminal behavior. Whether you’re mapping connections between individuals in a gang, examining the flow of illicit goods, or identifying key actors in a criminal enterprise, networks reveal critical structures and patterns that traditional data analysis might overlook.
As a reminder, in the tutorials for this textbook, R code appears like this
and can be copied directly into R or RStudio. Let’s start getting our hands dirty (figuratively of course) by building some networks in R!
Working with matrices
First, clear the workspace. To do so, we use the following statement:
Let’s start by working with an example of an undirected, binary network. We will create an object that is the adjacency matrix.
One way to create an adjacency matrix is to use the matrix()
function with the concatenate()
or c()
function.
We can look at what these functions do by asking for help using the help("
function name here ")
or ?("
function name here ")
functions.
The help window describes the arguments that the function takes and also provides examples.
# help for the matrix() function
?matrix
# help for the c() function
?c
Now, let’s create the data object:
<- matrix(
mat c( 0,1,0,0,0,1,0,1,0,0,0,1,0,1,1,0,0,1,0,1,0,0,1,1,0 ),
nrow=5,
byrow=TRUE
)
This command reads as follows:
Combine the following numbers
From these combined numbers, create a matrix by reading across the list
Create an object called mat. This object will be a matrix with 5 rows.
We can see the object by just typing the object name: mat
. Note that if the number of elements does not correctly match the dimensions of the matrix, R gives you an error.
For example:
<- matrix( c( 1,2,3,4,5,6,7 ), nrow=2, byrow=TRUE )
junk_1
# Because there are 7 elements here,
# the 8th element needed for a matrix
# is replaced with the first value in the vector
# print out the object by just typing the name of the object
junk_1
[,1] [,2] [,3] [,4]
[1,] 1 2 3 4
[2,] 5 6 7 1
After we have created our object mat
or junk_1
, we can examine the dimensions with the dim function: dim( mat )
or dim( junk_1 )
.
We can also attach names to the rows and columns of the matrix by using the rownames()
and colnames()
functions.
# attach row names
rownames( mat ) <- c( "Jen","Tom","Bob","Leaf","Jim" )
# attach column names
colnames( mat ) <- c( "Jen","Tom","Bob","Leaf","Jim" )
# print out the object
mat
Jen Tom Bob Leaf Jim
Jen 0 1 0 0 0
Tom 1 0 1 0 0
Bob 0 1 0 1 1
Leaf 0 0 1 0 1
Jim 0 0 1 1 0
We can refer to specific elements, rows, or columns by using the [
and ]
symbols. This reads as: “object[row,column]”. Remember from the Getting Started with R tutorial that I said we would use indexing a lot? Well, this is where that comes true!
For example, let’s look at the relation Jen sends to Tom.
Recall from the Network Data Structures chapter that this is element 1,2 in the matrix (i.e. row one, column two). In R code that is: mat[1,2]
.
This command reads as follows: for the object mat
, return the value at row 1 column 2. The row number is the first dimension and the column is the second dimension. Remember: “rows by columns”.
We can also call the values for an entire row or column:
# this reads: return the first row of data
1,] mat[
Jen Tom Bob Leaf Jim
0 1 0 0 0
# this reads: return the first column of data
1] mat[,
Jen Tom Bob Leaf Jim
0 1 0 0 0
Since we have defined names for the rows and columns, we can use those as well.
# reference the ROW pertaining to Jen
"Jen",] mat[
Jen Tom Bob Leaf Jim
0 1 0 0 0
# reference the COLUMN pertaining to Jen
"Jen"] mat[,
Jen Tom Bob Leaf Jim
0 1 0 0 0
Note: the following does not work because it needs a character, defined by the ""
symbols around the name.
# returns an error because there is no OBJECT called Jen
mat[,Jen]
# compare the difference with the prior line
"Jen"] mat[,
We can also call a series of values:
# return the first three ROWS of the object data
1:3,] mat[
Jen Tom Bob Leaf Jim
Jen 0 1 0 0 0
Tom 1 0 1 0 0
Bob 0 1 0 1 1
# return the first three COLUMNS of the object data
1:3] mat[,
Jen Tom Bob
Jen 0 1 0
Tom 1 0 1
Bob 0 1 0
Leaf 0 0 1
Jim 0 0 1
We can also call a group of values that are non-contiguous using the c()
function:
# return the first and second ROWS of the object data
c(1,3),] mat[
Jen Tom Bob Leaf Jim
Jen 0 1 0 0 0
Bob 0 1 0 1 1
# return the first and second COLUMNS of the object data
c(1,3)] mat[,
Jen Bob
Jen 0 0
Tom 1 1
Bob 0 0
Leaf 0 1
Jim 0 1
We can also call a group of values that do not contain specified values by putting a -
(i.e. a minus sign) in front of the c function:
# return the object data without ROWS 1 and 3
-c(1,3),] mat[
Jen Tom Bob Leaf Jim
Tom 1 0 1 0 0
Leaf 0 0 1 0 1
Jim 0 0 1 1 0
# return the object data without COLUMNS 1 and 3
-c(1,3)] mat[,
Tom Leaf Jim
Jen 1 0 0
Tom 0 0 0
Bob 1 1 1
Leaf 0 0 1
Jim 0 1 0
Got it? If yes, then GREAT! If no, hang in there: you got this! If you are a bit hesitant with working with indexing, the best way to get better is to practice. Feel free to work back through the section above to get better at this basic skill we will use a LOT in subsequent tutorials.
Exploring the network
Package
Now that we have created the mat
object, let’s manipulate it into a network and a graph. To do this, we can use the network
package. network
is a package containing tools to create and modify network objects created by Carter Butts. See the network page for an overview of package functionality.
First, we need to install the package using: install.packages( "network" )
. Note: if you have already installed the package, no do not need to reinstall it.
If it is already installed, we should check to make sure we have the most recent version: update.packages( "network" )
Whenever we start R, we need to load the package because it is not automatically loaded. To do this, use the library()
function. library( "network" )
To get a list of the contents of the package, as for help with respect to the package itself use the help()
function, but tell R we want help on the particular package: help( package="network" )
.
Working with Unidirected, Binary Networks
Now that the package is loaded, let’s create a new object from our matrix that is a network. In R lingo, we will use the network()
function to create an object that is of class network
. To use some of the functions, it has to be a specific class.
Just like you can’t perform calculations on an object that is of class character
(e.g. a list of names), the functions in this page are designed to work with a network
object.
# look at our object
mat
Jen Tom Bob Leaf Jim
Jen 0 1 0 0 0
Tom 1 0 1 0 0
Bob 0 1 0 1 1
Leaf 0 0 1 0 1
Jim 0 0 1 1 0
# what class is the object
class( mat )
[1] "matrix" "array"
# now, coerce the object into
# an object called net.u that
# is of class network
<- as.network( mat )
net_u
net_u
Network attributes:
vertices = 5
directed = TRUE
hyper = FALSE
loops = FALSE
multiple = FALSE
bipartite = FALSE
total edges= 10
missing edges= 0
non-missing edges= 10
Vertex attribute names:
vertex.names
No edge attributes
When we enter the object in the command line, summary info about the object is produced: net_u
. This is because the object is of class network
. We can use the class function to confirm this: class( net_u )
.
Let’s look at the object again: net_u
. What does the summary output of the object tell us?
Note that the network is treated as directed. By default, the function as.network()
sets the argument directed =
to TRUE
. We can see this by looking at the structure of the function in the help page: ?as.network
. What do we need to change in the as.network()
function?
We need to change the input for the directed=
argument because our network is undirected. In other words, directed = FALSE
. This tells the function that the matrix we are entering is an undirected network. This is logical: is the object a directed network? False. Therefore, it is an undirected network.
# create a new object called net_u_correct
<- as.network(
net_u_correct
mat, directed = FALSE )
# compare the difference since telling the function that the network is directed
net_u
Network attributes:
vertices = 5
directed = TRUE
hyper = FALSE
loops = FALSE
multiple = FALSE
bipartite = FALSE
total edges= 10
missing edges= 0
non-missing edges= 10
Vertex attribute names:
vertex.names
No edge attributes
# how is this one different?
net_u_correct
Network attributes:
vertices = 5
directed = FALSE
hyper = FALSE
loops = FALSE
multiple = FALSE
bipartite = FALSE
total edges= 5
missing edges= 0
non-missing edges= 5
Vertex attribute names:
vertex.names
No edge attributes
The summary()
function is a generic function that summarizes objects. We can use it on an object of class network
to provide more information: summary( net_u_correct )
. More information about what can be done with the summary()
function for an object of class network is shown on the ?as.network
page.
We could also enter the data as an edgelist using the matrix.type =
argument. By default, the function as.network()
sets the argument matrix.type =
to adjacency
. For an edgelist, we would need to change the input for the matrix.type =
argument to edgelist
.
# for example, let's make an edgelist called edge
# it will be a matrix of 5 rows and we are reading off by row
<- matrix(
edge c( "Jen","Tom","Tom","Bob","Bob","Leaf","Bob","Jim","Leaf","Jim" ),
nrow = 5,
byrow = TRUE )
# take a look
edge
[,1] [,2]
[1,] "Jen" "Tom"
[2,] "Tom" "Bob"
[3,] "Bob" "Leaf"
[4,] "Bob" "Jim"
[5,] "Leaf" "Jim"
# create an object called edge_net_u
# but change the default to edgelist for the matrix.type argument.
<- as.network(
edge_net_u
edge, directed = FALSE,
matrix.type = "edgelist" )
# now take a look
summary( edge_net_u )
Network attributes:
vertices = 5
directed = FALSE
hyper = FALSE
loops = FALSE
multiple = FALSE
bipartite = FALSE
total edges = 5
missing edges = 0
non-missing edges = 5
density = 0.5
Vertex attributes:
vertex.names:
character valued attribute
5 valid vertex names
No edge attributes
Network adjacency matrix:
Bob Jen Jim Leaf Tom
Bob 0 0 1 1 1
Jen 0 0 0 0 1
Jim 1 0 0 1 0
Leaf 1 0 1 0 0
Tom 1 1 0 0 0
# The as.network() function will often recognize the matrix type being entered
# create the object again, but do not toggle the matrix.type argument
<- as.network(
edge_net_u
edge,directed = FALSE )
# is it different
# no, because the function is programmed to
# read the dimensions of the input object
summary( edge_net_u )
Network attributes:
vertices = 5
directed = FALSE
hyper = FALSE
loops = FALSE
multiple = FALSE
bipartite = FALSE
total edges = 5
missing edges = 0
non-missing edges = 5
density = 0.5
Vertex attributes:
vertex.names:
character valued attribute
5 valid vertex names
No edge attributes
Network adjacency matrix:
Bob Jen Jim Leaf Tom
Bob 0 0 1 1 1
Jen 0 0 0 0 1
Jim 1 0 0 1 0
Leaf 1 0 1 0 0
Tom 1 1 0 0 0
Working with Directed, Binary Networks
Now, let’s work with the example of a directed, binary network. We will create an object that is the adjacency matrix.
# these were written in by row
# so we want it to read by row, byrow=TRUE
<- matrix(
mat_d c( 0,1,0,0,0,0,0,1,0,0,0,0,0,1,1,0,0,1,0,1,0,0,1,1,0 ),
nrow = 5,
byrow = TRUE )
# take a look at the matrix
mat_d
[,1] [,2] [,3] [,4] [,5]
[1,] 0 1 0 0 0
[2,] 0 0 1 0 0
[3,] 0 0 0 1 1
[4,] 0 0 1 0 1
[5,] 0 0 1 1 0
Now, let’s coerce it to be an object of class network.
<- as.network(
net_d
mat_d,directed = TRUE )
# take a look
summary( net_d )
Network attributes:
vertices = 5
directed = TRUE
hyper = FALSE
loops = FALSE
multiple = FALSE
bipartite = FALSE
total edges = 8
missing edges = 0
non-missing edges = 8
density = 0.4
Vertex attributes:
vertex.names:
character valued attribute
5 valid vertex names
No edge attributes
Network adjacency matrix:
1 2 3 4 5
1 0 1 0 0 0
2 0 0 1 0 0
3 0 0 0 1 1
4 0 0 1 0 1
5 0 0 1 1 0
Just as before, we could also enter the data as an edgelist. Since we have directed relations, we have more edges. This is because reciprocated ties count twice. So, we have to tell the matrix()
function that the matrix has 8 rows, instead of 5.
# create the edgelist
<- matrix(
edge_d c( "Jen","Tom","Tom","Bob","Bob","Leaf","Bob","Jim",
"Leaf","Bob","Leaf","Jim","Jim","Bob","Jim","Leaf" ),
nrow = 8,
byrow = TRUE )
# create the network object
<- as.network(
edge_d_net
edge_d,directed = TRUE,
matrix.type = "edgelist" )
# take a look
summary( edge_d_net )
Network attributes:
vertices = 5
directed = TRUE
hyper = FALSE
loops = FALSE
multiple = FALSE
bipartite = FALSE
total edges = 8
missing edges = 0
non-missing edges = 8
density = 0.4
Vertex attributes:
vertex.names:
character valued attribute
5 valid vertex names
No edge attributes
Network adjacency matrix:
Bob Jen Jim Leaf Tom
Bob 0 0 1 1 0
Jen 0 0 0 0 1
Jim 1 0 0 1 0
Leaf 1 0 1 0 0
Tom 1 0 0 0 0
# I have added the argument print.adj=FALSE
# what is different?
summary( edge_d_net,
print.adj = FALSE )
Network attributes:
vertices = 5
directed = TRUE
hyper = FALSE
loops = FALSE
multiple = FALSE
bipartite = FALSE
total edges = 8
missing edges = 0
non-missing edges = 8
density = 0.4
Vertex attributes:
vertex.names:
character valued attribute
5 valid vertex names
No edge attributes
Importing Network Data
If we had a large network, these routines (i.e. using the matrix()
function) would be tedious and most likely result in a few errors. Most of the time, we have a file that is an adjacency matrix or an edgelist that we can import. The read.csv()
function can be used to read in .csv files that are arranged in this way. Let’s take a look at the help for this function: ?read.csv
.
We will use a file called data-undirected-example.csv. To access the file, we can place the url in the read.csv()
function.
Here is the url: https://github.com/jacobtnyoung/snaca-r/raw/main/data/data-undirected-example.csv.
# define the path
<- "https://github.com/jacobtnyoung/snaca-r/raw/main/data/data-undirected-example.csv"
url
# define the data using the url object
<- read.csv( url )
mat_u
# look at the object
mat_u
# note that the read.csv function creates an object of class data.frame.
class( mat_u )
We need to adjust the arguments to read in the file how we want it. Specifically, we want to do the following:
Set the
as.is =
argument equal toTRUE
so that it reads the data as it is.Set the
header =
argument toTRUE
to indicate that there is a header, or column labels.Set the
row.names =
argument equal to 1 to indicate that the name of the rows are in the first column.
# look at the arguments
<- read.csv(
mat_u2
url,as.is = TRUE,
header = TRUE,
row.names = 1
)
mat_u2
# compare them
mat_u mat_u2
Now, make the object into one of class
network:
# we have to first coerce the object to a matrix
<- as.matrix( mat_u2 )
mat_u2
# recall that since this network is undirected
# we set the directed= argument to FALSE
<- as.network(
net_u
mat_u2,directed = FALSE
)
net_u
# we could combine the as.matrix and as.network functions
<- as.network(
net_u as.matrix(
mat_u2 ), directed = FALSE
)
net_u
We could also import the file if it is saved locally (i.e. we are not going to the web to get it). Typically we do not do this because it is a bad practice. That is, creating a version of a file locally. But, sometimes you might be offline or you have files that cannot be put online.
Let’s do this for the directed network. I have saved the file to my desktop. First, look at what directory we are in using the getwd()
function. This function gets the current working directory.
Then, set the directory where the file is located using the setwd()
function. You can get the location of the file by right-clicking and in Windows using Properties or on Mac using Get Info. Note that you have to configure this path to your machine.
Then, use read.csv
as above:
setwd( "PUT THE CORRECT PATH HERE" )
<- read.csv(
mat_d "data-directed-example.csv",
as.is=TRUE,
header=TRUE,
row.names=1
)
# Note: we don't need to tell it that
# the network is directed since
# this is the default,
# but a good habit to get into.
<- as.network(
net_d as.matrix( data_d ),
directed=TRUE
)
# now, print the object.
net_d
Network attributes:
vertices = 5
directed = TRUE
hyper = FALSE
loops = FALSE
multiple = FALSE
bipartite = FALSE
total edges= 8
missing edges= 0
non-missing edges= 8
Vertex attribute names:
vertex.names
No edge attributes
Test Your Knowledge Exercises
- Create a 5x5 adjacency matrix using the
matrix()
function.- What happens if the number of elements doesn’t match the dimensions of the matrix?
- Assign meaningful row and column names to the adjacency matrix using the
rownames()
andcolnames()
functions. - How can you reference a specific row or column by name?
- Use the
[
and]
symbols to extract specific elements, rows, or columns from a matrix.
- Coerce an adjacency matrix into a
network
object using theas.network()
function.- What argument must you modify to create an undirected network?
- What argument must you modify to create an undirected network?
- Create an edgelist for a directed network and convert it to a
network
object. How does this process differ from working with adjacency matrices?
- Summarize a
network
object using thesummary()
function.- What insights can you gain from this output?
- What insights can you gain from this output?
- Explain the difference between directed and undirected networks in the context of the
as.network()
function.- What specific arguments are used to define these properties?
Tutorial Summary
This tutorial introduced the basics of working with network data in R, focusing on creating, manipulating, and importing network structures. It began with constructing adjacency matrices using the matrix()
function, followed by assigning meaningful row and column names for clarity and exploring matrix indexing to extract specific elements, rows, or columns. The tutorial then transitioned to coercing matrices into network
objects using the as.network()
function from the network
package, with special attention to specifying whether a network is directed or undirected. The use of edgelists as an alternative to adjacency matrices was demonstrated, highlighting the importance of specifying the correct matrix type in the as.network()
function. The tutorial also covered importing network data from external sources, including CSV files from URLs or local directories, and converting them into network objects. The emphasis was on building a solid foundation for working with network data structures, which are essential for analyzing relationships and interactions in crime analysis.