library(emln)
library(igraph)
library(tidyverse)
library(bipartite)
library(magrittr)

Unipartite multilayer network

Toy example

We start with a toy example using Fig. 1A from the paper shown below.

pond_1 <- matrix(c(0,1,1,0,0,1,0,0,0), byrow = T, nrow = 3, ncol = 3, dimnames = list(c('pelican','fish','crab'),c('pelican','fish','crab')))

pond_2 <- matrix(c(0,1,0,0,0,1,0,0,0), byrow = T, nrow = 3, ncol = 3, dimnames = list(c('pelican','fish','crab'),c('pelican','fish','crab')))

pond_3 <- matrix(c(0,1,1,0,0,1,0,0,0), byrow = T, nrow = 3, ncol = 3, dimnames = list(c('pelican','fish','tadpole'),c('pelican','fish','tadpole')))

layer_attrib <- tibble(layer_id=1:3,
                     layer_name=c('pond_1','pond_2','pond_3'),
                     location=c('valley','mid-elevation','uphill'))


# Create the ELL tibble with interlayer links.
interlayer <- tibble(layer_from=c('pond_1','pond_1','pond_1'),
                     node_from=c('pelican','crab','pelican'),
                     layer_to=c('pond_2','pond_2','pond_3'), 
                     node_to=c('pelican','crab','pelican'),
                     weight=1)

# This is a directed network so the links should go both ways, even though they are symmetric.
interlayer_2 <- interlayer[,c(3,4,1,2,5)]
names(interlayer_2) <- names(interlayer)
interlayer <- rbind(interlayer, interlayer_2)


# This example is commented out because it cannot run. It fails because we provide interlayer links, which include layer names, but not the layer attributes that contain the layer names
# multilayer <- create_multilayer_network(list_of_layers = list(pond_1, pond_2, pond_3),
#                           interlayer_links = interlayer,
#                           bipartite = F,
#                           directed = T)

# When layer attributes are not included they are auto-generated
multilayer <- create_multilayer_network(list_of_layers = list(pond_1, pond_2, pond_3), 
                                        bipartite = F, 
                                        directed = T)
## [1] "Layer attributes not provided, I added them (see layer_attributes in the final object)"
## [1] "Layer #1 processing."
## [1] "Done."
## [1] "Layer #2 processing."
## [1] "Done."
## [1] "Layer #3 processing."
## [1] "Done."
## [1] "Creating extended link list with node IDs"
## [1] "Organizing state nodes"
multilayer$layer_attributes
## NULL
# An example with layer attributes and interlayer links
multilayer_unip_toy <- create_multilayer_network(list_of_layers = list(pond_1, pond_2, pond_3),
                          interlayer_links = interlayer,
                          layer_attributes = layer_attrib,
                          bipartite = F,
                          directed = T)
## [1] "Layer #1 processing."
## [1] "Done."
## [1] "Layer #2 processing."
## [1] "Done."
## [1] "Layer #3 processing."
## [1] "Done."
## [1] "Creating extended link list with node IDs"
## [1] "Organizing state nodes"

The multilayer class

The resulting multilayer class includes the following tables:

  • nodes: Physical nodes. First column is a unique node id. Node attributes are included if provided as input.
  • layers: Information on layers.
  • extended: An extended link list of the format layer_from node_from layer_to node_to weight. All nodes and layers are identified by names.
  • extended_ids: An extended link list of the format layer_from node_from layer_to node_to weight. All nodes and layers are identified by unique IDs automatically generated.
  • state_nodes: List of state nodes of the format layer_id node_id layer_name node_name. Also includes state node attributes if provided as input.
multilayer_unip_toy
## $nodes
## # A tibble: 4 × 2
##   node_id node_name
##     <int> <chr>    
## 1       1 crab     
## 2       2 fish     
## 3       3 pelican  
## 4       4 tadpole  
## 
## $layers
## # A tibble: 3 × 3
##   layer_id layer_name location     
##      <int> <chr>      <chr>        
## 1        1 pond_1     valley       
## 2        2 pond_2     mid-elevation
## 3        3 pond_3     uphill       
## 
## $extended
## # A tibble: 14 × 5
##    layer_from node_from layer_to node_to weight
##    <chr>      <chr>     <chr>    <chr>    <dbl>
##  1 pond_1     fish      pond_1   pelican      1
##  2 pond_1     crab      pond_1   pelican      1
##  3 pond_1     crab      pond_1   fish         1
##  4 pond_2     fish      pond_2   pelican      1
##  5 pond_2     crab      pond_2   fish         1
##  6 pond_3     fish      pond_3   pelican      1
##  7 pond_3     tadpole   pond_3   pelican      1
##  8 pond_3     tadpole   pond_3   fish         1
##  9 pond_1     pelican   pond_2   pelican      1
## 10 pond_1     crab      pond_2   crab         1
## 11 pond_1     pelican   pond_3   pelican      1
## 12 pond_2     pelican   pond_1   pelican      1
## 13 pond_2     crab      pond_1   crab         1
## 14 pond_3     pelican   pond_1   pelican      1
## 
## $extended_ids
## # A tibble: 14 × 5
##    layer_from node_from layer_to node_to weight
##         <int>     <int>    <int>   <int>  <dbl>
##  1          1         2        1       3      1
##  2          1         1        1       3      1
##  3          1         1        1       2      1
##  4          2         2        2       3      1
##  5          2         1        2       2      1
##  6          3         2        3       3      1
##  7          3         4        3       3      1
##  8          3         4        3       2      1
##  9          1         3        2       3      1
## 10          1         1        2       1      1
## 11          1         3        3       3      1
## 12          2         3        1       3      1
## 13          2         1        1       1      1
## 14          3         3        1       3      1
## 
## $state_nodes
## # A tibble: 9 × 4
##   layer_id node_id layer_name node_name
##      <int>   <int> <chr>      <chr>    
## 1        1       3 pond_1     pelican  
## 2        1       2 pond_1     fish     
## 3        1       1 pond_1     crab     
## 4        2       3 pond_2     pelican  
## 5        2       2 pond_2     fish     
## 6        2       1 pond_2     crab     
## 7        3       3 pond_3     pelican  
## 8        3       2 pond_3     fish     
## 9        3       4 pond_3     tadpole  
## 
## $description
## NULL
## 
## $references
## NULL
## 
## attr(,"class")
## [1] "multilayer"

Example with real data from Kefi 2016

This is a node-aligned multiplex network with three layers: trophic interactions, non-trophic positive interactions and non-trophic negative interactions.

First, we will see how we can use matrices as input. Along the way we perform sanity checks such as making sure the number and identity of of species is the same in the 3 layers.

# Load and organize the trophic interactions matrix
chilean_TI <- read_delim('chilean_TI.txt', delim = '\t')
nodes <- chilean_TI[,2]
names(nodes) <- 'Species'
chilean_TI <- data.matrix(chilean_TI[,3:ncol(chilean_TI)])
dim(chilean_TI)
## [1] 106 106
dimnames(chilean_TI) <- list(nodes$Species,nodes$Species)

# Load and organize the negative non-trophic interactions matrix
chilean_NTIneg <- read_delim('chilean_NTIneg.txt', delim = '\t')
nodes <- chilean_NTIneg[,2]
names(nodes) <- 'Species'
chilean_NTIneg <- data.matrix(chilean_NTIneg[,3:ncol(chilean_NTIneg)])
dim(chilean_NTIneg)
## [1] 106 106
dimnames(chilean_NTIneg) <- list(nodes$Species,nodes$Species)

# Are all row and column names the same?
setequal(rownames(chilean_TI), rownames(chilean_NTIneg))
## [1] TRUE
setequal(colnames(chilean_TI), colnames(chilean_NTIneg))
## [1] TRUE
chilean_NTIneg <- chilean_NTIneg[rownames(chilean_TI),colnames(chilean_TI)]
all(rownames(chilean_TI)==rownames(chilean_NTIneg))
## [1] TRUE
# Load and organize the negative non-trophic positive matrix
chilean_NTIpos <- read_delim('chilean_NTIpos.txt', delim = '\t')
nodes <- chilean_NTIpos[,2]
names(nodes) <- 'Species'
chilean_NTIpos <- data.matrix(chilean_NTIpos[,3:ncol(chilean_NTIpos)])
dim(chilean_NTIpos)
## [1] 106 106
dimnames(chilean_NTIpos) <- list(nodes$Species,nodes$Species)

# Are all row and column names the same?
setequal(rownames(chilean_TI), rownames(chilean_NTIpos))
## [1] TRUE
setequal(colnames(chilean_TI), colnames(chilean_NTIpos))
## [1] TRUE
chilean_NTIpos <- chilean_NTIpos[rownames(chilean_TI),colnames(chilean_TI)]
all(rownames(chilean_TI)==rownames(chilean_NTIpos))
## [1] TRUE
# Total number of links
sum(chilean_TI, chilean_NTIpos, chilean_NTIneg)
## [1] 4623
layer_attributes <- tibble(layer_id=1:3, layer_name=c('TI','NTIneg','NTIpos'))

multilayer_kefi <- create_multilayer_network(list_of_layers = list(chilean_TI,chilean_NTIneg,chilean_NTIpos),
                                             interlayer_links = NULL,
                                             layer_attributes = layer_attributes,
                                             bipartite = F,
                                             directed = T)
## [1] "Layer #1 processing."
## [1] "Done."
## [1] "Layer #2 processing."
## [1] "Some nodes have no interactions. They will appear in the node table but not in the edge list"
## [1] "Done."
## [1] "Layer #3 processing."
## [1] "Some nodes have no interactions. They will appear in the node table but not in the edge list"
## [1] "Done."
## [1] "Creating extended link list with node IDs"
## [1] "Organizing state nodes"

Now, we can try and use link lists as input. We will convert the matrices to link lists first.

# This is for the Kefi data set
chilean_TI_ll <- matrix_to_list_unipartite(x = chilean_TI, directed = TRUE)$edge_list
chilean_NTIneg_ll <- matrix_to_list_unipartite(x = chilean_NTIneg, directed = TRUE)$edge_list
## [1] "Some nodes have no interactions. They will appear in the node table but not in the edge list"
chilean_NTIpos_ll <- matrix_to_list_unipartite(x = chilean_NTIpos, directed = TRUE)$edge_list
## [1] "Some nodes have no interactions. They will appear in the node table but not in the edge list"
multilayer_kefi_ll <- create_multilayer_network(list_of_layers = list(chilean_TI_ll,chilean_NTIneg_ll,chilean_NTIpos_ll), layer_attributes = layer_attributes, bipartite = F, directed = T)
## [1] "Layer #1 processing."
## [1] "Input: an unipartite edge list"
## [1] "Done."
## [1] "Layer #2 processing."
## [1] "Input: an unipartite edge list"
## [1] "Done."
## [1] "Layer #3 processing."
## [1] "Input: an unipartite edge list"
## [1] "Done."
## [1] "Creating extended link list with node IDs"
## [1] "Organizing state nodes"
# Check that the set of links is the same in both
links_mat <- multilayer_kefi$extended_ids %>% unite(layer_from, node_from, layer_to, node_to)
links_ll <- multilayer_kefi_ll$extended_ids %>% unite(layer_from, node_from, layer_to, node_to)

any(duplicated(links_mat$layer_from))
## [1] FALSE
any(duplicated(links_ll$layer_from))
## [1] FALSE
setequal(links_mat$layer_from, links_ll$layer_from)
## [1] TRUE

Bipartite multilayer network

Toy example

We will work with the toy example from Fig. 3 in the paper.

pollination_layer <- matrix(c(5,3,5,3,1,1,
                              3,3,3,3,0,0,
                              3,0,0,0,1,0,
                              0,0,3,1,0,0), byrow = T, nrow = 4, ncol = 6,
                            dimnames = list(paste('P',1:4,sep=''),paste('A',1:6,sep='')))

herbivory_layer <- matrix(c(6,4,6,2,2,
                            0,4,0,2,0,
                            6,0,4,0,0,
                            6,0,6,0,0), byrow = T, nrow = 4, ncol = 5,
                            dimnames = list(paste('P',1:4,sep=''),paste('H',1:5,sep='')))


layer_attrib <- tibble(layer_id=1:2,
                     layer_name=c('Pollination','Herbivory'),
                     Method=c('Observation','Collection'))


# Create the ELL tibble with interlayer links.
interlayer <- tibble(layer_from=c('Pollination','Pollination','Pollination','Pollination'),
                     node_from=c('P1','P2','P3','P4'),
                     layer_to=c('Herbivory','Herbivory','Herbivory','Herbivory'), 
                     node_to=c('P1','P2','P3','P4'),
                     weight=1)

multilayer_bip_toy <- create_multilayer_network(list_of_layers = list(pollination_layer, herbivory_layer), bipartite = T, directed = F, interlayer_links = interlayer, layer_attributes = layer_attrib)
## [1] "Layer #1 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #2 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Creating extended link list with node IDs"
## [1] "Organizing state nodes"
# Now add information on state nodes for pollinators
abundance_sn <- data.frame(layer_name=c('Pollination','Pollination','Pollination'), node_name=c('A1','A2','A3','A4','A5','A6'), abund=c(15,20,7,8,12,3))

multilayer <- create_multilayer_network(list_of_layers = list(pollination_layer, herbivory_layer), bipartite = T, directed = F, interlayer_links = interlayer, layer_attributes = layer_attrib, state_node_attributes = abundance_sn)
## [1] "Layer #1 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #2 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Creating extended link list with node IDs"
## [1] "Organizing state nodes"
## [1] "Joining with user-provided state nodes"

Real data example

We will work with data from the paper Hot spots of mutualistic networks (Gilarranz et al 2014. J Anim Ecol). The data contains 12 layers (patches) of plant-pollinator interactions. We use the original data, provided in Excel to show how working with real data feels like. This illustrates the workflow discussed in the paper.

library(readxl)
# Import layers
Sierras_matrices <- NULL
for (layer in 1:12){
  d <- suppressMessages(read_excel('Gilarranz2014_Datos Sierras.xlsx', sheet = layer+2))
  web <- data.matrix(d[,2:ncol(d)])
  rownames(web) <- as.data.frame(d)[,1]
  web[is.na(web)] <- 0
  Sierras_matrices[[layer]] <- web
}

# Layer dimensions
sapply(Sierras_matrices, dim)
##      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12]
## [1,]   39   36   33   38   25   17   24   33   28    22    34    34
## [2,]   67   58   63   79   69   67   58   61   48    66    74    68
# Layer attribute table
layer_attrib <- tibble(layer_id=1:12,
                     layer_name=excel_sheets('Gilarranz2014_Datos Sierras.xlsx')[3:14])

multilayer_sierras <- create_multilayer_network(list_of_layers = Sierras_matrices, bipartite = T, directed = F, layer_attributes = layer_attrib)
## [1] "Layer #1 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #2 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #3 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #4 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #5 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #6 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #7 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #8 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #9 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #10 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #11 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #12 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Creating extended link list with node IDs"
## [1] "Organizing state nodes"

To illustrate working with interlayer links, we will connect a pollinator to itself between layers, and weigh these links by the distance between the layers. We emphasize that this is for illustration purposes only; there is no hypothesis behind this exercise. We already calculated the distances between the 12 patches.

dist <- data.matrix(read_csv('Gilarranz2014_distances.csv'))
dimnames(dist) <- list(layer_attrib$layer_name, layer_attrib$layer_name)

# Get the dispersal matrix, for pollinators only. This matrix shows which pollinator is in which layer
dispersal <- multilayer_sierras$extended %>% group_by(node_from) %>% dplyr::select(layer_from) %>% table()
dispersal <- 1*(dispersal>0)

Sierras_interlayer <- NULL
for (s in rownames(dispersal)){ # For each pollinator
  x <- dispersal[s,]
  locations <- names(which(x!=0)) # locations where pollinator occurs
  if (length(locations)<2){next}
  pairwise <- combn(locations, 2)
  # Create interlayer edges between pairwise combinations of locations
  for (i in 1:ncol(pairwise)){
    a <- pairwise[1,i]
    b <- pairwise[2,i]
    weight <- dist[a,b] # Get the interlayer edge weight
    Sierras_interlayer %<>% bind_rows(tibble(layer_from=a, node_from=s, layer_to=b, node_to=s, weight=weight))
  }
}

# Re-create the multilayer object with interlayer links
multilayer_sierras_interlayer <- create_multilayer_network(list_of_layers = Sierras_matrices, bipartite = T, directed = F, layer_attributes = layer_attrib, interlayer_links = Sierras_interlayer)
## [1] "Layer #1 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #2 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #3 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #4 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #5 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #6 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #7 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #8 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #9 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #10 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #11 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #12 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Creating extended link list with node IDs"
## [1] "Organizing state nodes"
# See the interlayer links
multilayer_sierras_interlayer$extended %>% filter(layer_from!=layer_to)
## # A tibble: 2,657 × 5
##    layer_from node_from         layer_to   node_to           weight
##    <chr>      <chr>             <chr>      <chr>              <dbl>
##  1 La Brava   Achyra similalis  Vigilancia Achyra similalis  0.0652
##  2 Amarante   Agraulis vanillae Barrosa    Agraulis vanillae 0.166 
##  3 Amarante   Agraulis vanillae Difuntito  Agraulis vanillae 0.183 
##  4 Amarante   Agraulis vanillae Difuntos   Agraulis vanillae 0.871 
##  5 Amarante   Agraulis vanillae La Brava   Agraulis vanillae 0.617 
##  6 Amarante   Agraulis vanillae Vigilancia Agraulis vanillae 0.552 
##  7 Barrosa    Agraulis vanillae Difuntito  Agraulis vanillae 0.0173
##  8 Barrosa    Agraulis vanillae Difuntos   Agraulis vanillae 0.705 
##  9 Barrosa    Agraulis vanillae La Brava   Agraulis vanillae 0.452 
## 10 Barrosa    Agraulis vanillae Vigilancia Agraulis vanillae 0.387 
## # ℹ 2,647 more rows
multilayer_sierras_interlayer$extended_ids %>% filter(layer_from!=layer_to)
## # A tibble: 2,657 × 5
##    layer_from node_from layer_to node_to weight
##         <int>     <int>    <int>   <int>  <dbl>
##  1          7         2       11       2 0.0652
##  2          1         6        2       6 0.166 
##  3          1         6        4       6 0.183 
##  4          1         6        5       6 0.871 
##  5          1         6        7       6 0.617 
##  6          1         6       11       6 0.552 
##  7          2         6        4       6 0.0173
##  8          2         6        5       6 0.705 
##  9          2         6        7       6 0.452 
## 10          2         6       11       6 0.387 
## # ℹ 2,647 more rows

Converting a multilayer class

To supra-adjacency matrices

A SAM is convenient for matrix operations. See a visual guide on the idea of tensor flattening and producing SAM. Use the function get_sam to obtain a SAM. See ?get_sam for an explanation on what the function returns.

Unipartite example

Let’s take take the toy unipartite network first. This is a directed network.

# Order the physical nodes as in the image for convenience
multilayer_unip_toy$nodes <- multilayer_unip_toy$nodes[c(3,2,1,4),]

# get the SAM
sam_unipartite <- get_sam(multilayer = multilayer_unip_toy, bipartite = F, directed = T, sparse = F, remove_zero_rows_cols = F)

The SAM contains 3 layers by 4 physical nodes = 12 state nodes.

sam_unipartite$M
##    1 2 3 4 5 6 7 8 9 10 11 12
## 1  0 1 1 0 1 0 0 0 1  0  0  0
## 2  0 0 1 0 0 0 0 0 0  0  0  0
## 3  0 0 0 0 0 0 1 0 0  0  0  0
## 4  0 0 0 0 0 0 0 0 0  0  0  0
## 5  1 0 0 0 0 1 0 0 0  0  0  0
## 6  0 0 0 0 0 0 1 0 0  0  0  0
## 7  0 0 1 0 0 0 0 0 0  0  0  0
## 8  0 0 0 0 0 0 0 0 0  0  0  0
## 9  1 0 0 0 0 0 0 0 0  1  0  1
## 10 0 0 0 0 0 0 0 0 0  0  0  1
## 11 0 0 0 0 0 0 0 0 0  0  0  0
## 12 0 0 0 0 0 0 0 0 0  0  0  0

The row and column names correspond to the sn_id column in sam_unipartite$state_nodes_map. However, not all nodes always occur in all layers. This will be reflected in the state_nodes_map: When layer and node ids are NA that means that the node did not occur in the layer.

sam_unipartite$state_nodes_map
##    sn_id layer_name node_name layer_id node_id          tuple
## 1      1     pond_1   pelican        1       3 pond_1_pelican
## 2      2     pond_1      fish        1       2    pond_1_fish
## 3      3     pond_1      crab        1       1    pond_1_crab
## 4      4     pond_1   tadpole       NA      NA pond_1_tadpole
## 5      5     pond_2   pelican        2       3 pond_2_pelican
## 6      6     pond_2      fish        2       2    pond_2_fish
## 7      7     pond_2      crab        2       1    pond_2_crab
## 8      8     pond_2   tadpole       NA      NA pond_2_tadpole
## 9      9     pond_3   pelican        3       3 pond_3_pelican
## 10    10     pond_3      fish        3       2    pond_3_fish
## 11    11     pond_3      crab       NA      NA    pond_3_crab
## 12    12     pond_3   tadpole        3       4 pond_3_tadpole

A node may not have links across all the layers. In that case, its row or column sum will be zero. This can happen in a directed network like this one; for example, the tadpole does not have incoming links. The user can choose to remove rows and columns that sum to zero by setting `remove_zero_rows_cols to TRUE.

sam_unipartite <- get_sam(multilayer = multilayer_unip_toy, bipartite = F, directed = T, sparse = F, remove_zero_rows_cols = T)
# That matrix has less than 12 rows/columns
dim(sam_unipartite$M)
## [1] 8 9
sam_unipartite$M
##    1 2 3 5 6 7 9 10 12
## 1  0 1 1 1 0 0 1  0  0
## 2  0 0 1 0 0 0 0  0  0
## 3  0 0 0 0 0 1 0  0  0
## 5  1 0 0 0 1 0 0  0  0
## 6  0 0 0 0 0 1 0  0  0
## 7  0 0 1 0 0 0 0  0  0
## 9  1 0 0 0 0 0 0  1  1
## 10 0 0 0 0 0 0 0  0  1

When matrices are very large and sparse, excess of zeroes takes a lot of memory. It is possible to use a sparse matrix.

sam_unipartite <- get_sam(multilayer = multilayer_unip_toy, bipartite = F, directed = T, sparse = T, remove_zero_rows_cols = T)
sam_unipartite$M
## 8 x 9 sparse Matrix of class "dgCMatrix"
##    1 2 3 5 6 7 9 10 12
## 1  . 1 1 1 . . 1  .  .
## 2  . . 1 . . . .  .  .
## 3  . . . . . 1 .  .  .
## 5  1 . . . 1 . .  .  .
## 6  . . . . . 1 .  .  .
## 7  . . 1 . . . .  .  .
## 9  1 . . . . . .  1  1
## 10 . . . . . . .  .  1

Bipartite example

Bipartite networks are rectangular matrices. Therefore, it is necessary first to transform the incidence matrix to a square adjacency matrix. A visual example is in the figure below, in panels A–>B. Each layer is independently transformed. Then the layers are put together to a SAM as for unipartite network (panel C).

Now that the network is effectively a unipartite network, we proceed as with the previous example (e.g., state nodes and rows/columns that sum to zero).

Toy example

We will start with the example in this figure. We already created this network. Note the bipartite and directed arguments.

# get the SAM
sam_bipartite <- get_sam(multilayer = multilayer_bip_toy, bipartite = T, directed = F, sparse = F, remove_zero_rows_cols = F)

Because this is a diagnoally-copouled network and only plants are common between layers, there will be many NAs in the state node table.

sam_bipartite$state_nodes_map
##    sn_id  layer_name node_name layer_id node_id          tuple
## 1      1 Pollination        A1        1       1 Pollination_A1
## 2      2 Pollination        A2        1       2 Pollination_A2
## 3      3 Pollination        A3        1       3 Pollination_A3
## 4      4 Pollination        A4        1       4 Pollination_A4
## 5      5 Pollination        A5        1       5 Pollination_A5
## 6      6 Pollination        A6        1       6 Pollination_A6
## 7      7 Pollination        H1       NA      NA Pollination_H1
## 8      8 Pollination        H2       NA      NA Pollination_H2
## 9      9 Pollination        H3       NA      NA Pollination_H3
## 10    10 Pollination        H4       NA      NA Pollination_H4
## 11    11 Pollination        H5       NA      NA Pollination_H5
## 12    12 Pollination        P1        1      12 Pollination_P1
## 13    13 Pollination        P2        1      13 Pollination_P2
## 14    14 Pollination        P3        1      14 Pollination_P3
## 15    15 Pollination        P4        1      15 Pollination_P4
## 16    16   Herbivory        A1       NA      NA   Herbivory_A1
## 17    17   Herbivory        A2       NA      NA   Herbivory_A2
## 18    18   Herbivory        A3       NA      NA   Herbivory_A3
## 19    19   Herbivory        A4       NA      NA   Herbivory_A4
## 20    20   Herbivory        A5       NA      NA   Herbivory_A5
## 21    21   Herbivory        A6       NA      NA   Herbivory_A6
## 22    22   Herbivory        H1        2       7   Herbivory_H1
## 23    23   Herbivory        H2        2       8   Herbivory_H2
## 24    24   Herbivory        H3        2       9   Herbivory_H3
## 25    25   Herbivory        H4        2      10   Herbivory_H4
## 26    26   Herbivory        H5        2      11   Herbivory_H5
## 27    27   Herbivory        P1        2      12   Herbivory_P1
## 28    28   Herbivory        P2        2      13   Herbivory_P2
## 29    29   Herbivory        P3        2      14   Herbivory_P3
## 30    30   Herbivory        P4        2      15   Herbivory_P4

Also possible to remove zero rows/cols and get a sparse matrix

sam_bipartite <- get_sam(multilayer = multilayer_bip_toy, bipartite = T, directed = F, sparse = F, remove_zero_rows_cols = T)
# That matrix has less than 12 rows/columns
dim(sam_bipartite$M)
## [1] 19 19
sam_bipartite$M
##    1 2 3 4 5 6 12 13 14 15 22 23 24 25 26 27 28 29 30
## 1  0 0 0 0 0 0  5  3  3  0  0  0  0  0  0  0  0  0  0
## 2  0 0 0 0 0 0  3  3  0  0  0  0  0  0  0  0  0  0  0
## 3  0 0 0 0 0 0  5  3  0  3  0  0  0  0  0  0  0  0  0
## 4  0 0 0 0 0 0  3  3  0  1  0  0  0  0  0  0  0  0  0
## 5  0 0 0 0 0 0  1  0  1  0  0  0  0  0  0  0  0  0  0
## 6  0 0 0 0 0 0  1  0  0  0  0  0  0  0  0  0  0  0  0
## 12 5 3 5 3 1 1  0  0  0  0  0  0  0  0  0  1  0  0  0
## 13 3 3 3 3 0 0  0  0  0  0  0  0  0  0  0  0  1  0  0
## 14 3 0 0 0 1 0  0  0  0  0  0  0  0  0  0  0  0  1  0
## 15 0 0 3 1 0 0  0  0  0  0  0  0  0  0  0  0  0  0  1
## 22 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0  6  0  6  6
## 23 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0  4  4  0  0
## 24 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0  6  0  4  6
## 25 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0  2  2  0  0
## 26 0 0 0 0 0 0  0  0  0  0  0  0  0  0  0  2  0  0  0
## 27 0 0 0 0 0 0  1  0  0  0  6  4  6  2  2  0  0  0  0
## 28 0 0 0 0 0 0  0  1  0  0  0  4  0  2  0  0  0  0  0
## 29 0 0 0 0 0 0  0  0  1  0  6  0  4  0  0  0  0  0  0
## 30 0 0 0 0 0 0  0  0  0  1  6  0  6  0  0  0  0  0  0
# Sparse
sam_bipartite <- get_sam(multilayer = multilayer_bip_toy, bipartite = T, directed = F, sparse = T, remove_zero_rows_cols = T)
sam_bipartite$M
## 19 x 19 sparse Matrix of class "dsCMatrix"
##                                         
## 1  . . . . . . 5 3 3 . . . . . . . . . .
## 2  . . . . . . 3 3 . . . . . . . . . . .
## 3  . . . . . . 5 3 . 3 . . . . . . . . .
## 4  . . . . . . 3 3 . 1 . . . . . . . . .
## 5  . . . . . . 1 . 1 . . . . . . . . . .
## 6  . . . . . . 1 . . . . . . . . . . . .
## 12 5 3 5 3 1 1 . . . . . . . . . 1 . . .
## 13 3 3 3 3 . . . . . . . . . . . . 1 . .
## 14 3 . . . 1 . . . . . . . . . . . . 1 .
## 15 . . 3 1 . . . . . . . . . . . . . . 1
## 22 . . . . . . . . . . . . . . . 6 . 6 6
## 23 . . . . . . . . . . . . . . . 4 4 . .
## 24 . . . . . . . . . . . . . . . 6 . 4 6
## 25 . . . . . . . . . . . . . . . 2 2 . .
## 26 . . . . . . . . . . . . . . . 2 . . .
## 27 . . . . . . 1 . . . 6 4 6 2 2 . . . .
## 28 . . . . . . . 1 . . . 4 . 2 . . . . .
## 29 . . . . . . . . 1 . 6 . 4 . . . . . .
## 30 . . . . . . . . . 1 6 . 6 . . . . . .

Note that in this undirected network, each layer and the final SAM are symmetric.

Matrix::isSymmetric(sam_bipartite$M) # Does not work on sparse matrices
## [1] TRUE

Temporal bipartite

The past example was a Now let’s try an example with a directed, temporal bipartite network. We will use the pollination layer from the previous example to start with, and we will work with the same pollinators and plants.

time_1 <- matrix(c(5,3,5,3,1,1,
                   3,3,3,3,0,0,
                   3,0,0,0,1,0,
                   0,0,3,1,0,0), byrow = T, nrow = 4, ncol = 6,
                   dimnames = list(paste('P',1:4,sep=''),paste('A',1:6,sep='')))

time_2 <- matrix(c(7,0,5,3,0,1,
                   5,2,3,2,0,1,
                   1,0,0,0,1,0,
                   0,2,2,1,0,0), byrow = T, nrow = 4, ncol = 6,
                   dimnames = list(paste('P',1:4,sep=''),paste('A',1:6,sep='')))

time_3 <- matrix(c(5,1,1,1,0,1,
                   3,0,1,1,0,1,
                   2,0,0,0,0,0,
                   1,0,0,1,0,0), byrow = T, nrow = 4, ncol = 6,
                   dimnames = list(paste('P',1:4,sep=''),paste('A',1:6,sep='')))

# Plot the layers
visweb(time_1, type = 'none')

visweb(time_2, type = 'none')

visweb(time_3, type = 'none') # A5 has no links here

layer_attrib <- tibble(layer_id=1:3, layer_name=c('Time_1','Time_2', 'Time_3'))
                    
# Create the ELL tibble with interlayer links.
# We will link a some plant species for the example. The value is 100 so they will be easy to spot
interlayer <- tibble(layer_from=c('Time_1', 'Time_2', 'Time_2', 'Time_2'),
                     node_from= c('P1',     'P1',     'P2',     'P4'),
                     layer_to=  c('Time_2', 'Time_3', 'Time_3', 'Time_3'), 
                     node_to=  c('P1',     'P1',     'P2',     'P4'),
                     weight=c(100,100,100,100))

multilayer_bip_temporal <- create_multilayer_network(list_of_layers = list(time_1, time_2,time_3), bipartite = T, directed = F, interlayer_links = interlayer, layer_attributes = layer_attrib)
## [1] "Layer #1 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #2 processing."
## [1] "Input: a bipartite matrix"
## [1] "Done."
## [1] "Layer #3 processing."
## [1] "Input: a bipartite matrix"
## [1] "Some node have no interactions. They will appear in the node table but not in the edge list"
## [1] "Done."
## [1] "Creating extended link list with node IDs"
## [1] "Organizing state nodes"

Now get the SAM. Notice that interlayer links are only on one matrix diagonal off-blocks.

temporal_SAM <- get_sam(multilayer_bip_temporal, bipartite = T, directed = T, sparse = T)
temporal_SAM$M
## 30 x 30 sparse Matrix of class "dgCMatrix"
##                                                                       
## 1  . . . . . .   5 3 3 . . . . . . .   .   . .   . . . . . . . . . . .
## 2  . . . . . .   3 3 . . . . . . . .   .   . .   . . . . . . . . . . .
## 3  . . . . . .   5 3 . 3 . . . . . .   .   . .   . . . . . . . . . . .
## 4  . . . . . .   3 3 . 1 . . . . . .   .   . .   . . . . . . . . . . .
## 5  . . . . . .   1 . 1 . . . . . . .   .   . .   . . . . . . . . . . .
## 6  . . . . . .   1 . . . . . . . . .   .   . .   . . . . . . . . . . .
## 7  5 3 5 3 1 1   . . . . . . . . . .   .   . .   . . . . . . . . . . .
## 8  3 3 3 3 . .   . . . . . . . . . .   .   . .   . . . . . . . . . . .
## 9  3 . . . 1 .   . . . . . . . . . .   .   . .   . . . . . . . . . . .
## 10 . . 3 1 . .   . . . . . . . . . .   .   . .   . . . . . . . . . . .
## 11 . . . . . .   . . . . . . . . . .   7   5 1   . . . . . . . . . . .
## 12 . . . . . .   . . . . . . . . . .   .   2 .   2 . . . . . . . . . .
## 13 . . . . . .   . . . . . . . . . .   5   3 .   2 . . . . . . . . . .
## 14 . . . . . .   . . . . . . . . . .   3   2 .   1 . . . . . . . . . .
## 15 . . . . . .   . . . . . . . . . .   .   . 1   . . . . . . . . . . .
## 16 . . . . . .   . . . . . . . . . .   1   1 .   . . . . . . . . . . .
## 17 . . . . . . 100 . . . 7 . 5 3 . 1   .   . .   . . . . . . . . . . .
## 18 . . . . . .   . . . . 5 2 3 2 . 1   .   . .   . . . . . . . . . . .
## 19 . . . . . .   . . . . 1 . . . 1 .   .   . .   . . . . . . . . . . .
## 20 . . . . . .   . . . . . 2 2 1 . .   .   . .   . . . . . . . . . . .
## 21 . . . . . .   . . . . . . . . . .   .   . .   . . . . . . . 5 3 2 1
## 22 . . . . . .   . . . . . . . . . .   .   . .   . . . . . . . 1 . . .
## 23 . . . . . .   . . . . . . . . . .   .   . .   . . . . . . . 1 1 . .
## 24 . . . . . .   . . . . . . . . . .   .   . .   . . . . . . . 1 1 . 1
## 25 . . . . . .   . . . . . . . . . .   .   . .   . . . . . . . . . . .
## 26 . . . . . .   . . . . . . . . . .   .   . .   . . . . . . . 1 1 . .
## 27 . . . . . .   . . . . . . . . . . 100   . .   . 5 1 1 1 . 1 . . . .
## 28 . . . . . .   . . . . . . . . . .   . 100 .   . 3 . 1 1 . 1 . . . .
## 29 . . . . . .   . . . . . . . . . .   .   . .   . 2 . . . . . . . . .
## 30 . . . . . .   . . . . . . . . . .   .   . . 100 1 . . 1 . . . . . .
Matrix::isSymmetric(temporal_SAM$M) # Should be FALSE
## [1] FALSE

Real data example

This example shows that the function can handle large networks. This is the spatial network we used before.

# Without interlayer links
sierras_SAM <- get_sam(multilayer = multilayer_sierras, bipartite = T, directed = F, sparse = T, remove_zero_rows_cols = F)
sum(sierras_SAM$M)==2*nrow(multilayer_sierras$extended) # Number of interactions in the SAM should be links the number of links because we created the square matrices
## [1] TRUE
# With interlayer links
sierras_SAM_interlayer <- get_sam(multilayer = multilayer_sierras_interlayer, bipartite = T, directed = F, sparse = T, remove_zero_rows_cols = F)

# Look at a subset of the data
sierras_SAM_interlayer$M[1:20,1:20]
## 20 x 20 sparse Matrix of class "dsCMatrix"
##                                           
## 1  . . . . . . . . . . . . . . . . . . . .
## 2  . . . . . . . . . . . . . . . . . . . .
## 3  . . . . . . . . . . 1 . 1 . . . . . 1 .
## 4  . . . . . . . . . . . . . . . . . . . .
## 5  . . . . . . . . . . . . . . . . . . . .
## 6  . . . . . . . . . . . . . . . . . . . .
## 7  . . . . . . . . . . . . . . . . . . . .
## 8  . . . . . . . . . . . . . . . . . . . .
## 9  . . . . . . . . . . . . . . . . . 1 . .
## 10 . . . . . . . . . . . . . . . . . . . .
## 11 . . 1 . . . . . . . . . . . . . . . . .
## 12 . . . . . . . . . . . . . . . . . . . .
## 13 . . 1 . . . . . . . . . . . . . . . . .
## 14 . . . . . . . . . . . . . . . . . . . .
## 15 . . . . . . . . . . . . . . . . . . . .
## 16 . . . . . . . . . . . . . . . . . . . .
## 17 . . . . . . . . . . . . . . . . . . . .
## 18 . . . . . . . . 1 . . . . . . . . . . .
## 19 . . 1 . . . . . . . . . . . . . . . . .
## 20 . . . . . . . . . . . . . . . . . . . .

To igraph objects

igraph is used frequently for analysis. Hence, it is convenient to obtain a list of igraph objects as layers. This can be done with the get_igraph function. The attributes of the links, state nodes and the physical nodes are all included in the igraph objects. We will show this using the ponds example with atteibutes we used before. The sn_id corresponds to that in the state_nodes_map.

g_layers <- get_igraph(multilayer_unip_attributes, bipartite = F, directed = T)

Let’s look at one layer as an example.

g_layers$layers_igraph[[2]]
## IGRAPH 6b4c9c2 DNW- 4 2 -- 
## + attr: name (v/c), sn_id (v/n), node_id (v/n), order (v/c), weight (e/n), method (e/c)
## + edges from 6b4c9c2 (vertex names):
## [1] fish->pelican crab->fish

The state nodes are listed below. Note that they include the attributes of the physical nodes (order in this example), and that all attributes are included in the igraph objects.

g_layers$state_nodes_map
##    sn_id layer_name node_name layer_id node_id          tuple          order
## 1      1     pond_1      crab        1       1    pond_1_crab       Decapoda
## 2      2     pond_1      fish        1       2    pond_1_fish Chondrichthyes
## 3      3     pond_1   pelican        1       3 pond_1_pelican Pelecaniformes
## 4      4     pond_1   tadpole       NA      NA pond_1_tadpole           <NA>
## 5      5     pond_2      crab        2       1    pond_2_crab       Decapoda
## 6      6     pond_2      fish        2       2    pond_2_fish Chondrichthyes
## 7      7     pond_2   pelican        2       3 pond_2_pelican Pelecaniformes
## 8      8     pond_2   tadpole       NA      NA pond_2_tadpole           <NA>
## 9      9     pond_3      crab       NA      NA    pond_3_crab           <NA>
## 10    10     pond_3      fish        3       2    pond_3_fish Chondrichthyes
## 11    11     pond_3   pelican        3       3 pond_3_pelican Pelecaniformes
## 12    12     pond_3   tadpole        3       4 pond_3_tadpole          Anura
# Attributes in pond 1
vertex.attributes(g_layers$layers_igraph[[1]])
## $name
## [1] "crab"    "fish"    "pelican" "tadpole"
## 
## $sn_id
## [1] 1 2 3 4
## 
## $node_id
## [1]  1  2  3 NA
## 
## $order
## [1] "Decapoda"       "Chondrichthyes" "Pelecaniformes" NA
edge.attributes(g_layers$layers_igraph[[1]])
## $weight
## [1] 1 1 1
## 
## $method
## [1] "observation"  "observation"  "gut analysis"
#Attributes in pond 2
vertex.attributes(g_layers$layers_igraph[[2]])
## $name
## [1] "crab"    "fish"    "pelican" "tadpole"
## 
## $sn_id
## [1] 5 6 7 8
## 
## $node_id
## [1]  1  2  3 NA
## 
## $order
## [1] "Decapoda"       "Chondrichthyes" "Pelecaniformes" NA
edge.attributes(g_layers$layers_igraph[[2]])
## $weight
## [1] 1 1
## 
## $method
## [1] "observation"  "gut analysis"
#Attributes in pond 3
vertex.attributes(g_layers$layers_igraph[[3]])
## $name
## [1] "crab"    "fish"    "pelican" "tadpole"
## 
## $sn_id
## [1]  9 10 11 12
## 
## $node_id
## [1] NA  2  3  4
## 
## $order
## [1] NA               "Chondrichthyes" "Pelecaniformes" "Anura"
edge.attributes(g_layers$layers_igraph[[3]])
## $weight
## [1] 1 1 1
## 
## $method
## [1] "observation"  "observation"  "gut analysis"

Here is an example with a bipartite network.

g_layers_bip <- get_igraph(multilayer = multilayer_bip_toy, bipartite = T, directed = F)
l2 <- g_layers_bip$layers_igraph[[2]]
l2
## IGRAPH 3780019 UNWB 15 11 -- 
## + attr: name (v/c), sn_id (v/n), node_id (v/n), type (v/l), weight (e/n)
## + edges from 3780019 (vertex names):
##  [1] H1--P1 H1--P3 H1--P4 H2--P1 H2--P2 H3--P1 H3--P3 H3--P4 H4--P1 H4--P2 H5--P1
vertex.attributes(l2)
## $name
##  [1] "A1" "A2" "A3" "A4" "A5" "A6" "H1" "H2" "H3" "H4" "H5" "P1" "P2" "P3" "P4"
## 
## $sn_id
##  [1] 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
## 
## $node_id
##  [1] NA NA NA NA NA NA  7  8  9 10 11 12 13 14 15
## 
## $type
##  [1] FALSE FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE

```