Skip to content

Building a network

Abstract

Before getting started with the package itself, we will see how we can build a network, access its content, and iterate over the interactions. This page is intended to give you some intuitions about how the type system works, before reading more of the manual.

To begin with, we will load the package:

using SpeciesInteractionNetworks

Creating a list of species

We will create a very small network, made of four species and their interactions. The first step is to define a list of species:

species = [:fox, :vole, :hawk, :turnip]
4-element Vector{Symbol}:
 :fox
 :vole
 :hawk
 :turnip

In order to make sure that we are explicit about the type of network we are working with, we will create a representation of this list of species that is unipartite, using the Unipartite constructor:

nodes = Unipartite(species)
Unipartite{Symbol}([:fox, :vole, :hawk, :turnip])

Note that the package is not considering information about the ecological nature of the interaction, only (i) the structure of the community, as captured by its Partiteness, and later on about its Interactions.

Creating interactions

As with species, we want to represent interactions in a way that captures ecological information. In this case, we will use binary interactions (true/0), and work from a matrix, where the rows are the source of the interaction, and the column is its destination. It means that interactions go from predator to preys.

int_matrix = Bool[
    0 1 0 0;
    0 0 0 1;
    0 1 0 0;
    0 0 0 0
]
4×4 Matrix{Bool}:
 0  1  0  0
 0  0  0  1
 0  1  0  0
 0  0  0  0

About interaction as matrices

By specifying interactions as a matrix, it is fundamental that columns and orders are in the correct order. There are alternative ways to specify networks that do not rely on matrices (using tuples or pairs), but because most species interaction network data are represented as matrices, this is supported by the package.

As this network is binary, we will wrap this matrix into a Binary collection of interactions:

edges = Binary(int_matrix)
Binary{Bool}(sparse([1, 3, 2], [2, 2, 4], Bool[1, 1, 1], 4, 4))

Assembling the network

The network itself is a collection of nodes and edges. There are a number of specific checks performed when creating the network, to ensure that we cannot create an object that makes no sense. These checks are done when calling SpeciesInteractionNetwork, which is the main type around which the package is built.

network = SpeciesInteractionNetwork(nodes, edges)
A binary unipartite network
 → 3 interactions
 → 4 species

The networks are iterable, i.e. we can walk through them, specifically one interaction at a time:

for interaction in network
    println(interaction)
end
(:fox, :vole, true)
(:hawk, :vole, true)
(:vole, :turnip, true)

Internally, this is done by only returning the pairs of species that do not have a value of zero. There is a way to capture all of the interactions at a time, using interactions – this solution is faster than direct iteraction, potentially by a few orders of magnitude.

interactions(network)
3-element Vector{Tuple{Symbol, Symbol, Bool}}:
 (:fox, :vole, 1)
 (:vole, :turnip, 1)
 (:hawk, :vole, 1)

Basics of network exploration

We can also get a list of the species that establish an interaction with a given species (in this case, predators):

predecessors(network, :vole)
Set{Symbol} with 2 elements:
  :fox
  :hawk

Or the species witch which a given species establishes interactions (in this case, preys):

successors(network, :fox)
Set{Symbol} with 1 element:
  :vole

Furthermore, we can return a subset (or more accurately a subgraph) of the network, by giving a list of species:

interactions(subgraph(network, [:fox, :vole, :turnip]))
2-element Vector{Tuple{Symbol, Symbol, Bool}}:
 (:fox, :vole, 1)
 (:vole, :turnip, 1)

Networks are editable

The content of networks can be modified. For example, to circumvent the issue of needing to write the interaction matrix in the correct order, we can start with an empty network:

netsize = (richness(nodes,1), richness(nodes, 2))
edges2 = Binary(zeros(Bool, netsize))
network2 = SpeciesInteractionNetwork(nodes, edges2)
interactions(network2)
Tuple{Symbol, Symbol, Bool}[]

We can then add the interactions one by one:

for interaction in [(:fox, :vole), (:hawk, :vole), (:vole, :turnip)]
    network2[interaction...] = true
end
interactions(network2)
3-element Vector{Tuple{Symbol, Symbol, Bool}}:
 (:fox, :vole, 1)
 (:vole, :turnip, 1)
 (:hawk, :vole, 1)

Networks are tables

All of the networks can be converted to a tabular data, for use with e.g. the DataFrames package:

import DataFrames
DataFrames.DataFrame(network)
3×3 DataFrame
Row123
SymbolSymbolBool
1foxvoletrue
2voleturniptrue
3hawkvoletrue

Networks are broadcastable

We can use the broadcast syntax to rapidly apply operations to all positions in a network. Note that this is not only applying operations to the positions that are non-zero. For example, we can flip the sign of all interactions in the network:

interactions(.!network)
13-element Vector{Tuple{Symbol, Symbol, Bool}}:
 (:fox, :fox, 1)
 (:fox, :hawk, 1)
 (:fox, :turnip, 1)
 (:vole, :fox, 1)
 (:vole, :vole, 1)
 (:vole, :hawk, 1)
 (:hawk, :fox, 1)
 (:hawk, :hawk, 1)
 (:hawk, :turnip, 1)
 (:turnip, :fox, 1)
 (:turnip, :vole, 1)
 (:turnip, :hawk, 1)
 (:turnip, :turnip, 1)

In the same way, we can broadcast operations on pairs of networks. In this case, the constraint that will be enforced is that the species must be the same (they do not need to be in the same order, however):

interactions(network .+ network)
3-element Vector{Tuple{Symbol, Symbol, Int64}}:
 (:fox, :vole, 2)
 (:vole, :turnip, 2)
 (:hawk, :vole, 2)

In practice, the application of broadcasting to network is of limited value, but the package offers the ability to do it.