# 2 Analyze networks with igraph

In the language of graph theory (or complex networks) species of an ecological network would be the **nodes** (**vertices**) of a graph and **links** (or **edges**) their pairwise interactions.

The reference package for graph/network analysis is igraph.
It is an open source software, optimized by professional developers and can be invoked from R, Python, Mathematica, and C/C++. The recommendation is that, whenever you need to do some operation or calculation with networks, you first look if that functionality is already provided in `igraph`

. If not, you can probably build on some existing functions to develop your own function/package.

In the following we will use the word *graph* meaning an igraph object in the context of the `igraph`

package. But, obviously, graph refers to a more general mathematical entity.
Network data can be represented in several ways, corresponding to different data structures (classes). Below we will consider the following ones:

representation | data structure |
---|---|

edge list | dataframe |

graph | igraph object |

adjacency matrix | matrix/array |

incidence matrix | matrix/array |

## 2.1 From dataframe to igraph object

The network data downloaded from Web of life are stored in a representation (*network_name, species1, species2, connection_strength*). The last three columns correspond to the `edgelist`

representation of a graph. Therefore, the values contained in those three columns can be converted into a graph object using the function `graph_from_data_frame()`

of the igraph package

```
# download the foodweb FW_001
<- paste0(base_url,"get_networks.php?network_name=FW_001")
json_url <- jsonlite::fromJSON(json_url)
FW_001_nw
# check the class
class(FW_001_nw)
```

`## [1] "data.frame"`

```
# import the igraph package (if not done at the beginning)
library(igraph)
# select the 3 relevant columns and create the igraph object
<- FW_001_nw %>% select(species1, species2, connection_strength) %>%
FW_001_graph graph_from_data_frame(directed = TRUE)
# check the class
class(FW_001_graph)
```

`## [1] "igraph"`

As you can see from the last command, we have just created an *igraph object*. Note that, since in foodwebs it is relevant *who eats whom*, the representative graph is **directed**. This is specified setting the corresponding parameter as `TRUE`

in the function `graph_from_data_frame()`

.

## 2.2 Plot a foodweb as a directed graph

Once the network is represented as an `igraph`

object it can easily be visualized

```
plot(FW_001_graph, vertex.size=4,
vertex.label=NA,
edge.arrow.size= 0.3,
layout = layout_on_sphere)
```

Other possible layouts are: layout_nicely, layout_with_kk, layout_randomly, layout_in_circle, layout_on_sphere.

## 2.3 From igraph object to adjacency matrix

Another way to represent an ecological network is the adjacency matrix, that is a square matrix whose rows and columns run over all the species name in the network and whose elements indicate the strength of interaction between species pairs (zero means no interaction). This data structure is generally less efficient from the perspective of memory storage w.r.t. an edge list, but it can be useful to facilitate visualization or to perform some calculations (e.g., evaluate the nestedness of a network, perform spectral analysis, etc.).

Using the function `as_adjacency_matrix()`

of the igraph package
the adjacency matrix associated with a given network can be created. Let us focus on the relatively small **FW_002** foodweb. Some care must be put in passing the right arguments to the function `as_adjacency_matrix()`

because the graph is directed. The reader is addressed to the
user manual for more details.

```
# download the foodweb FW_002
<- paste0(base_url,"get_networks.php?network_name=FW_002")
json_url <- jsonlite::fromJSON(json_url)
FW_002_nw
# select the 3 relevant column and pass and create the igraph object
<- FW_002_nw %>% select(species1, species2, connection_strength) %>%
FW_002_graph graph_from_data_frame(directed = TRUE)
# convert the igraph object into adjacency matrix
<- as_adjacency_matrix(FW_002_graph,
adj_matrix type = "lower", # it is directed!!!
attr = "connection_strength",
sparse = FALSE)
```

If you give a quick look at the variable *adj_matrix* running the command

` head(adj_matrix)`

you will notice that it does not produce exactly what we want:

1. elements are strings, instead of numerical values

2. there are some empty strings to indicate that a pair of species do not interact.

We can fix both problems as follows

```
# convert elements into numeric values
class(adj_matrix) <-"numeric"
# remove NA values (previous empty strings)
which(is.na(adj_matrix) == T)]<-0
adj_matrix[
# check dimensions and compare with the number of species on the Web interface
dim(adj_matrix)
```

`## [1] 14 14`

```
# convert the adjacency matrix into a dataframe just for visualization purposes
<- adj_matrix %>% as.data.frame()
df
<- df[order((rownames(df))),] # to order by column names
df
# visualize the adjacency matrix
%>% formattable() df
```

Leporinus friderici | Astyanax altiparanae | Hoplias malabaricus | Detritus | Phytoplankton | Macrophytes | Terrestrial Invertebrates | Astyanax fasciatus | Brycon nattereri | Hypostomus strigaticeps | Detritus Animal | Import | Salminus hilarii | Zooplankton | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|

Astyanax altiparanae | 0.000 | 0.0000 | 0.500 | 0 | 0 | 0 | 0.0 | 0.000 | 0.000 | 0 | 0 | 0 | 0.740 | 0.0 |

Astyanax fasciatus | 0.000 | 0.0000 | 0.100 | 0 | 0 | 0 | 0.0 | 0.000 | 0.000 | 0 | 0 | 0 | 0.060 | 0.0 |

Brycon nattereri | 0.000 | 0.0000 | 0.000 | 0 | 0 | 0 | 0.0 | 0.000 | 0.000 | 0 | 0 | 0 | 0.115 | 0.0 |

Detritus | 0.000 | 0.0000 | 0.001 | 0 | 0 | 0 | 0.3 | 0.000 | 0.003 | 1 | 0 | 0 | 0.000 | 0.6 |

Detritus Animal | 0.010 | 0.0006 | 0.001 | 0 | 0 | 0 | 0.0 | 0.307 | 0.001 | 0 | 0 | 0 | 0.000 | 0.0 |

Hoplias malabaricus | 0.000 | 0.0000 | 0.030 | 0 | 0 | 0 | 0.0 | 0.000 | 0.000 | 0 | 0 | 0 | 0.000 | 0.0 |

Hypostomus strigaticeps | 0.000 | 0.0000 | 0.000 | 0 | 0 | 0 | 0.0 | 0.000 | 0.000 | 0 | 0 | 0 | 0.010 | 0.0 |

Import | 0.252 | 0.0290 | 0.000 | 0 | 0 | 0 | 0.5 | 0.000 | 0.219 | 0 | 0 | 0 | 0.000 | 0.0 |

Leporinus friderici | 0.000 | 0.0000 | 0.000 | 0 | 0 | 0 | 0.0 | 0.000 | 0.000 | 0 | 0 | 0 | 0.050 | 0.0 |

Macrophytes | 0.100 | 0.1000 | 0.007 | 0 | 0 | 0 | 0.2 | 0.500 | 0.036 | 0 | 0 | 0 | 0.000 | 0.0 |

Phytoplankton | 0.004 | 0.0200 | 0.000 | 0 | 0 | 0 | 0.0 | 0.001 | 0.000 | 0 | 0 | 0 | 0.000 | 0.4 |

Salminus hilarii | 0.000 | 0.0000 | 0.000 | 0 | 0 | 0 | 0.0 | 0.000 | 0.000 | 0 | 0 | 0 | 0.000 | 0.0 |

Terrestrial Invertebrates | 0.634 | 0.8500 | 0.361 | 0 | 0 | 0 | 0.0 | 0.192 | 0.742 | 0 | 0 | 0 | 0.025 | 0.0 |

Zooplankton | 0.000 | 0.0000 | 0.000 | 0 | 0 | 0 | 0.0 | 0.000 | 0.000 | 0 | 0 | 0 | 0.000 | 0.0 |

## 2.4 Bipartite networks

Several ecologically relevant networks are **bipartite**, that is

1. interactions are not directed

2. species can be split in two groups and interactions occurs only from one group to the other.

Typical examples are *pollination* or *seed-dispersal* networks. Let us see how these features of bipartite networks affect the way in which their data has to be handled with igraph.

We first download the seed-dispersal network **M_SD_002** and convert it into an igraph object:

```
# download the foodweb FW_002
<- paste0(base_url,"get_networks.php?network_name=M_SD_002")
json_url <- jsonlite::fromJSON(json_url)
M_SD_002_nw
# select the 3 relevant columns and create the igraph object
<- M_SD_002_nw %>% select(species1, species2, connection_strength) %>%
M_SD_002_graph graph_from_data_frame()
```

To turn this object into a **bipartite** graph, one needs to determine which nodes (species) of the graph **M_SD_002_graph** belong to one or the other guild, e.g., plants or animals.

Within each network we label species acting as **resources** and oppose them to the **consumers**. Technically, this information is returned by the boolean `is.resource`

when the endpoint `https://www.web-of-life.es/get_species_info.php`

is hit.
For clearness, let us store this information in the list `isResource`

```
<- read.csv(paste0(base_url,"get_species_info.php?network_name=M_SD_002"))
M_SD_002_info <- M_SD_002_info$is.resource %>% as.logical() # 0/1 converted to FALSE/TRUE isResource
```

This list can be used to split the nodes of the graph **M_SD_002_graph** into two ecologically meaningful guilds; igraph uses the `type`

(TRUE/FALSE) attribute of nodes to identify the two groups of species. In code, this reads

```
# Add the "type" attribute to the vertices of the graph
V(M_SD_002_graph)$type <- !(isResource)
```

When representing a graph – **M_SD_002_graph** in this case – as an **incidence** matrix, we will adopt the convention that resources be associated with rows. These species are label with `is.resource=TRUE`

. Instead, `igraph`

displays as rows of the **incidence** matrix nodes with the attribute `type=FALSE`

. For this reason, we applied the negation operator `!()`

in the previous chunk.

One can also check that the igraph object was successfully converted into a bipartite graph running the following command

```
# check if igraph understands this object as bipartite
is_bipartite(M_SD_002_graph)
```

`## [1] TRUE`

Since interactions are not directed it is appropriate to remove the arrows attribute from the links of our graph

`E(M_SD_002_graph)$arrow.mode = "-"`

Moreover, we would like to visualize our network assigning distinct colors to nodes belonging to different groups

`V(M_SD_002_graph)$color <- ifelse(V(M_SD_002_graph)$type == TRUE, "lightblue", "lightgreen")`

```
plot(M_SD_002_graph,
layout=layout_as_bipartite,
arrow.mode=0,
vertex.label=NA,
vertex.size=4,
asp=0.2)
```

In the example above, the nodes of the top layer in the graph above represent plants while seed dispersers are displayed on the bottom layer. To adjust the distance between nodes one can play with their size (vertex.size) and the attribute `asp`

of `iplot.graph`

, which controls how rectangular a plot is: asp < 1 yields a wide plot, asp > 1 a tall plot.

### From bipartite graph to incidence matrix

It is possible to pass from the representation of a bipartite graph to that of
incidence_matrix.
The latter is a matrix whose rows represent one group of species (e.g. plants) and columns the other group (e.g. animals).
This can be achieved using the function `as_incidence_matrix()`

of the
igraph package.
*Mutatis mutandis* we first repeat the same steps done for the conversion from an igraph object to adjacency matrix

```
# convert the igraph object into incidence matrix
<- as_incidence_matrix(
inc_matrix
M_SD_002_graph,attr = "connection_strength",
names = TRUE,
sparse = FALSE
)
# convert elements into numeric values
class(inc_matrix) <-"numeric"
# remove NA values (previous empty strings)
which(is.na(inc_matrix) == T)]<-0
inc_matrix[
# check dimensions and compare with the number of species on the Web interface
dim(inc_matrix)
```

`## [1] 31 9`

We can now visualize the elements of the incidence matrix with the function `formattable()`

and order alphabetically rows and columns by relative names:

```
# convert the incidence matrix into a dataframe just for visualization purposes
<- inc_matrix %>% as.data.frame()
df
<- df[,order((colnames(df)))] # to order by column names
df <- df[order((rownames(df))),] # to order by row names
df
# visualize the incidence matrix
%>% formattable() df
```

Diphyllodes magnificus | Epimachus albertisii | Lophorina superba | Manucodia chalybatus | Manucodia keraudrenii | Paradisaea raggiana | Paradisaea rudolphi | Parotia lawesii | Ptiloris magnificus | |
---|---|---|---|---|---|---|---|---|---|

Aglaia sp1 M_SD_002 | 0 | 0 | 0 | 0 | 0 | 3 | 0 | 1 | 0 |

Aporusa sp1 M_SD_002 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |

Canthium sp1 M_SD_002 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |

Chisocheton sp1 M_SD_002 | 13 | 5 | 23 | 1 | 7 | 13 | 3 | 10 | 16 |

Cissus aristata | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |

Cissus hypoglauca | 4 | 1 | 1 | 0 | 13 | 5 | 3 | 2 | 1 |

Dysoxylum sp1 M_SD_002 | 12 | 0 | 0 | 0 | 1 | 17 | 0 | 1 | 0 |

Elmerrillia sp1 M_SD_002 | 12 | 1 | 2 | 1 | 8 | 2 | 0 | 2 | 0 |

Endospermum sp1 M_SD_002 | 15 | 0 | 9 | 0 | 2 | 2 | 0 | 2 | 0 |

Ficus 181 | 0 | 0 | 0 | 0 | 7 | 0 | 1 | 2 | 0 |

Ficus 202 | 1 | 0 | 0 | 1 | 2 | 0 | 0 | 0 | 0 |

Ficus 217 | 0 | 0 | 0 | 0 | 9 | 0 | 0 | 0 | 0 |

Ficus 246 | 2 | 0 | 0 | 1 | 4 | 0 | 0 | 0 | 0 |

Ficus 275 | 5 | 0 | 2 | 8 | 40 | 13 | 4 | 1 | 0 |

Ficus 371 | 0 | 1 | 4 | 1 | 6 | 3 | 7 | 4 | 0 |

Ficus drupacea | 0 | 0 | 0 | 0 | 2 | 0 | 0 | 0 | 0 |

Ficus gul | 8 | 0 | 1 | 25 | 155 | 45 | 1 | 12 | 0 |

Ficus odoardi | 8 | 0 | 1 | 19 | 55 | 20 | 0 | 6 | 2 |

Gastonia sp1 M_SD_002 | 58 | 0 | 0 | 0 | 17 | 24 | 3 | 22 | 7 |

Glochidion sp1 M_SD_002 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |

Homalanthus sp1 M_SD_002 | 104 | 0 | 17 | 0 | 4 | 75 | 23 | 10 | 8 |

Myristica sp1 M_SD_002 | 1 | 0 | 0 | 0 | 3 | 1 | 0 | 3 | 0 |

Pandanus sp1 M_SD_002 | 5 | 0 | 2 | 0 | 0 | 2 | 1 | 0 | 0 |

Piper sp1 M_SD_002 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |

Schefflera sp1 M_SD_002 | 2 | 0 | 0 | 0 | 0 | 0 | 10 | 47 | 1 |

Sloanea aberrans | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |

Sloanea sogerensis | 4 | 0 | 2 | 0 | 2 | 1 | 0 | 2 | 0 |

Sterculia sp1 M_SD_002 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |

Syzygium sp1 M_SD_002 | 0 | 0 | 0 | 0 | 8 | 1 | 0 | 1 | 0 |

Uvaria sp1 M_SD_002 | 0 | 0 | 0 | 0 | 2 | 0 | 0 | 0 | 0 |

Zingiberaceae sp1 M_SD_002 | 2 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 |