1 Purpose of the Analysis

This report explores an ecological food web from the transitional shoreline-forest region of Lake Malawi. The aim is not only to draw a network, but to interpret the ecological structure of the system: which organisms form the basal resource base, which species transfer energy through the aquatic food web, which consumers connect the lake shoreline to the forest margin, and which species act as highly connected hubs or bridge species.

The network is treated as a directed food web. In this analysis, each edge follows this convention:

consumer -> resource

This means that an arrow or link begins at the organism doing the consuming and points toward the resource, prey, or food item being used. Under this convention, a high out_degree means a species uses many resources, while a high in_degree means a species is used by many consumers.

2 Load Required Libraries

The analysis uses tidyverse for data cleaning, igraph and tidygraph for network analysis, and plotly for the interactive 3D figures. The DT package is used to make tables searchable and scrollable in the HTML output.

library(tidyverse)
library(igraph)
library(tidygraph)
library(plotly)
library(DT)

3 Load and Clean the Food-Web Data

The food-web data are read from a CSV file. The code is written to be flexible: it looks for common column names such as consumer, resource, predator, prey, from, and to. The interaction strength column is also detected automatically if it is named interaction_strength, strength, or weight.

Species names are cleaned by replacing spaces with underscores. This makes them easier to use as graph node names.

file_path <- "C:/Users/Hp User/Downloads/Ecological networks/lake_malawi_shoreline_foodweb_clean.csv"

fw_raw <- readr::read_csv(file_path, show_col_types = FALSE)

consumer_col <- intersect(
  names(fw_raw),
  c("consumer", "Consumer", "predator", "Predator", "from", "From")
)[1]

resource_col <- intersect(
  names(fw_raw),
  c("resource", "Resource", "prey", "Prey", "to", "To")
)[1]

strength_col <- intersect(
  names(fw_raw),
  c("interaction_strength", "Interaction_strength", "strength", "weight", "Weight")
)[1]

if (is.na(consumer_col) || is.na(resource_col)) {
  stop("Could not find consumer/resource columns. Rename them to consumer and resource.")
}

fw <- fw_raw %>%
  transmute(
    consumer = str_replace_all(as.character(.data[[consumer_col]]), "\\s+", "_"),
    resource = str_replace_all(as.character(.data[[resource_col]]), "\\s+", "_"),
    interaction_strength = if (is.na(strength_col)) 1 else as.numeric(.data[[strength_col]])
  ) %>%
  filter(!is.na(consumer), !is.na(resource), consumer != "", resource != "") %>%
  mutate(
    interaction_strength = replace_na(interaction_strength, 1),
    interaction_strength = if_else(interaction_strength <= 0, 0.001, interaction_strength)
  )

glimpse(fw)
## Rows: 215
## Columns: 3
## $ consumer             <chr> "Tropodiaptomus_cunningtoni", "Diaphanosoma_excis…
## $ resource             <chr> "Phytoplankton", "Phytoplankton", "Phytoplankton"…
## $ interaction_strength <dbl> 0.85, 0.70, 0.65, 0.80, 0.30, 0.15, 0.40, 0.70, 0…

The cleaned table contains one row per trophic interaction. The consumer column gives the organism using the resource, the resource column gives the prey or food resource, and interaction_strength gives the relative strength of that feeding relationship.

4 Assign Ecological Guilds and Habitat Zones

To make the network biologically informative, species are grouped into trophic guilds. These guilds are based on broad ecological roles: basal resources, zooplankton, benthic invertebrates, cichlid fish, other fish, waterbirds, reptiles, semi-aquatic mammals, and terrestrial mammals.

The same species are also grouped into three habitat zones:

  • Aquatic/littoral: organisms mainly connected to the lake and nearshore littoral zone.
  • Shoreline interface: organisms that move between water and land, such as waterbirds, crocodiles, reptiles, otters, and hippos.
  • Forest/terrestrial: land-based mammals and forest-edge species.
species <- tibble(name = sort(unique(c(fw$consumer, fw$resource)))) %>%
  mutate(
    guild = case_when(
      str_detect(name, regex("Phytoplankton|algae|macrophytes|detritus|vegetation|periphyton", ignore_case = TRUE)) ~ "Basal resources",
      str_detect(name, regex("Chaoborus|Mesocyclops|Tropodiaptomus|Diaphanosoma|Bosmina|Rotifera|zooplankton", ignore_case = TRUE)) ~ "Zooplankton",
      str_detect(name, regex("Chironomus|Oligochaeta|Caridina|Ephemer|Trichoptera|Melanoides|Bulinus|Coelatura|Dragonfly|Potamonautes|benthos|larvae", ignore_case = TRUE)) ~ "Benthos/invertebrates",
      str_detect(name, regex("Pseudo|Labeot|Melanoc|Metria|Labidoc|Copadich|Rhamph|Sciaeno|Nimboch|Oreochromis|Tilapia|cichlid", ignore_case = TRUE)) ~ "Cichlid fish",
      str_detect(name, regex("Engraulicy|Synod|Clarias|Bagrus|fish", ignore_case = TRUE)) ~ "Other fish",
      str_detect(name, regex("eagle|kingfisher|heron|cormorant|Hamerkop|stork|jacana|openbill|harrier|bird", ignore_case = TRUE)) ~ "Waterbirds",
      str_detect(name, regex("crocodile|Monitor|python|reptile", ignore_case = TRUE)) ~ "Reptiles",
      str_detect(name, regex("otter|Hippop|hippo", ignore_case = TRUE)) ~ "Semi-aquatic mammals",
      TRUE ~ "Terrestrial mammals"
    ),
    habitat = case_when(
      guild %in% c("Basal resources", "Zooplankton", "Benthos/invertebrates", "Cichlid fish", "Other fish") ~ "Aquatic/littoral",
      guild %in% c("Waterbirds", "Reptiles", "Semi-aquatic mammals") ~ "Shoreline interface",
      TRUE ~ "Forest/terrestrial"
    ),
    trophic_band = case_when(
      guild == "Basal resources" ~ 1,
      guild %in% c("Zooplankton", "Benthos/invertebrates") ~ 2,
      guild %in% c("Cichlid fish", "Other fish") ~ 3,
      guild %in% c("Waterbirds", "Reptiles", "Semi-aquatic mammals") ~ 4,
      TRUE ~ 5
    )
  )

guild_pal <- c(
  "Basal resources" = "#5E9F3D",
  "Zooplankton" = "#1B9E77",
  "Benthos/invertebrates" = "#B87923",
  "Cichlid fish" = "#3A8DDE",
  "Other fish" = "#1D5F99",
  "Waterbirds" = "#7B70D6",
  "Reptiles" = "#D35C37",
  "Semi-aquatic mammals" = "#C84A7A",
  "Terrestrial mammals" = "#5F5F5A"
)

habitat_pal <- c(
  "Aquatic/littoral" = "#2F80B7",
  "Shoreline interface" = "#C25D3B",
  "Forest/terrestrial" = "#4F7E3A"
)

DT::datatable(
  species,
  rownames = FALSE,
  options = list(pageLength = 10, scrollX = TRUE)
)

This grouping step is important because the network should show ecological meaning, not only mathematical structure. The guild and habitat categories allow the figures to show how energy moves from basal resources through aquatic consumers and then into shoreline and terrestrial consumers.

5 Build the Directed Food-Web Network

The graph is built with igraph. Each species is a node, and each trophic interaction is a directed link from consumer to resource.

g_ig <- graph_from_data_frame(
  fw %>% select(from = consumer, to = resource, interaction_strength),
  directed = TRUE,
  vertices = species %>% select(name)
)

E(g_ig)$weight <- E(g_ig)$interaction_strength

g_ig
## IGRAPH 867cae7 DNW- 65 215 -- 
## + attr: name (v/c), interaction_strength (e/n), weight (e/n)
## + edges from 867cae7 (vertex names):
##  [1] Tropodiaptomus_cunningtoni->Phytoplankton  
##  [2] Diaphanosoma_excisum      ->Phytoplankton  
##  [3] Bosmina_longirostris      ->Phytoplankton  
##  [4] Rotifera_spp              ->Phytoplankton  
##  [5] Oreochromis_karongae      ->Phytoplankton  
##  [6] Engraulicypris_sardella   ->Phytoplankton  
##  [7] Caridina_nilotica         ->Phytoplankton  
##  [8] Coelatura_mussel          ->Phytoplankton  
## + ... omitted several edges

6 Network-Level Summary

The first summary table describes the whole food web. These values help describe the overall complexity of the network.

network_summary <- tibble(
  species_count = gorder(g_ig),
  interaction_count = gsize(g_ig),
  connectance = edge_density(g_ig, loops = FALSE),
  mean_degree = mean(degree(g_ig, mode = "all")),
  mean_interaction_strength = mean(E(g_ig)$interaction_strength),
  reciprocity = reciprocity(g_ig, ignore.loops = TRUE),
  weak_components = components(g_ig, mode = "weak")$no
)

DT::datatable(
  network_summary,
  rownames = FALSE,
  options = list(dom = "t")
)

Connectance describes the proportion of possible links that are actually present. Food webs are usually sparse rather than fully connected, because not every species interacts with every other species. Mean degree describes the average number of links per species. Weak components indicates whether the network is one connected system or split into separate pieces when edge direction is ignored.

7 Species-Level Network Metrics

The next step calculates centrality metrics for each species. These values help identify likely hubs, bridge species, basal resources, and top consumers.

node_metrics <- tibble(
  name = V(g_ig)$name,
  out_degree = degree(g_ig, mode = "out"),
  in_degree = degree(g_ig, mode = "in"),
  degree_all = degree(g_ig, mode = "all"),
  strength_all = strength(g_ig, mode = "all", weights = E(g_ig)$weight),
  betweenness = betweenness(
    g_ig,
    directed = TRUE,
    normalized = TRUE,
    weights = 1 / E(g_ig)$weight
  ),
  pagerank = page_rank(
    g_ig,
    directed = TRUE,
    weights = E(g_ig)$weight
  )$vector
) %>%
  left_join(species, by = "name") %>%
  mutate(
    node_size = scales::rescale(degree_all, to = c(5, 22)),
    role = case_when(
      out_degree == 0 & in_degree > 0 ~ "Basal / prey-only resource",
      out_degree > 0 & in_degree == 0 ~ "Top consumer",
      betweenness >= quantile(betweenness, 0.90, na.rm = TRUE) ~ "Bridge / connector",
      degree_all >= quantile(degree_all, 0.90, na.rm = TRUE) ~ "Hub",
      TRUE ~ "Intermediate"
    )
  )

top_hubs <- node_metrics %>%
  arrange(desc(degree_all), desc(strength_all)) %>%
  select(name, guild, habitat, role, out_degree, in_degree, degree_all, strength_all, betweenness) %>%
  slice_head(n = 15)

top_bridges <- node_metrics %>%
  arrange(desc(betweenness), desc(degree_all)) %>%
  select(name, guild, habitat, role, out_degree, in_degree, degree_all, strength_all, betweenness) %>%
  slice_head(n = 15)

7.1 Most Connected Hub Species

Hub species have many direct links. In this food web, a hub may be a resource used by many consumers, a generalist consumer that uses many prey, or a species that plays both roles.

DT::datatable(
  top_hubs,
  rownames = FALSE,
  options = list(pageLength = 15, scrollX = TRUE)
)

7.2 Strongest Bridge Species

Bridge species have high betweenness centrality. These species sit on many shortest paths through the network, meaning they may connect different ecological compartments. In this shoreline-forest system, bridge species are especially important because they may link aquatic production to shoreline and terrestrial consumers.

DT::datatable(
  top_bridges,
  rownames = FALSE,
  options = list(pageLength = 15, scrollX = TRUE)
)

Important caution: centrality does not prove that a species is a true ecological keystone. It identifies structural importance in the network. A species should be called a candidate keystone, hub, or bridge species unless there is additional evidence from abundance, biomass, removal experiments, or long-term ecological observation.

8 Guild-Level Feeding Pathways

This table summarizes interactions by guild. It shows which types of consumers are linked to which types of resources and gives the total interaction strength for each guild-to-guild pathway.

guild_summary <- fw %>%
  left_join(
    species %>% select(consumer = name, consumer_guild = guild, consumer_habitat = habitat),
    by = "consumer"
  ) %>%
  left_join(
    species %>% select(resource = name, resource_guild = guild, resource_habitat = habitat),
    by = "resource"
  ) %>%
  group_by(consumer_guild, resource_guild) %>%
  summarise(
    interaction_count = n(),
    total_strength = sum(interaction_strength, na.rm = TRUE),
    mean_strength = mean(interaction_strength, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(desc(total_strength))

DT::datatable(
  guild_summary,
  rownames = FALSE,
  options = list(pageLength = 12, scrollX = TRUE)
)

This table is useful for writing the ecological interpretation. For example, strong pathways from basal resources to zooplankton or benthic invertebrates indicate the lower food-web foundation. Strong pathways from fish to waterbirds, reptiles, or semi-aquatic mammals show the transfer of aquatic production into the shoreline-interface community.

9 Prepare the 3D Network Layout

The 3D layout uses two ideas:

  • The x and y positions come from a force-directed network layout, so species with similar network connections tend to appear nearer to one another.
  • The z position is based on trophic band, so basal resources appear lower and higher consumers appear higher.

This makes the 3D plot ecological rather than only decorative. The vertical axis represents trophic organization.

set.seed(42)

z_spacing <- 2.2

xy_layout <- layout_with_fr(g_ig, weights = E(g_ig)$weight)

nodes_3d <- node_metrics %>%
  mutate(
    x = xy_layout[, 1],
    y = xy_layout[, 2],
    z = trophic_band * z_spacing + runif(n(), -0.16, 0.16),
    hover_text = paste0(
      "<b>", name, "</b>",
      "<br>Guild: ", guild,
      "<br>Habitat: ", habitat,
      "<br>Role: ", role,
      "<br>Diet breadth/out-degree: ", out_degree,
      "<br>Vulnerability/in-degree: ", in_degree,
      "<br>Total degree: ", degree_all,
      "<br>Betweenness: ", round(betweenness, 4)
    )
  )

edge_tbl <- as_data_frame(g_ig, what = "edges") %>%
  rename(consumer = from, resource = to) %>%
  left_join(
    nodes_3d %>% select(consumer = name, x_from = x, y_from = y, z_from = z),
    by = "consumer"
  ) %>%
  left_join(
    nodes_3d %>% select(resource = name, x_to = x, y_to = y, z_to = z),
    by = "resource"
  )

make_edge_trace <- function(edge_data, colour = "rgba(0,0,0,0.45)", width = 3) {
  list(
    x = as.vector(rbind(edge_data$x_from, edge_data$x_to, NA)),
    y = as.vector(rbind(edge_data$y_from, edge_data$y_to, NA)),
    z = as.vector(rbind(edge_data$z_from, edge_data$z_to, NA)),
    line = list(color = colour, width = width)
  )
}

edge_trace <- make_edge_trace(edge_tbl)

10 Figure 1: Interactive 3D Trophic Food Web

This figure shows the full food web in 3D. The height of each node represents its trophic band. Colour represents guild, and node size represents total connectedness. Larger nodes are more connected and may be important hubs in the food-web structure.

p_3d_trophic <- plot_ly() %>%
  add_trace(
    x = edge_trace$x,
    y = edge_trace$y,
    z = edge_trace$z,
    type = "scatter3d",
    mode = "lines",
    line = edge_trace$line,
    hoverinfo = "none",
    showlegend = FALSE
  ) %>%
  add_trace(
    data = nodes_3d,
    x = ~x,
    y = ~y,
    z = ~z,
    type = "scatter3d",
    mode = "markers+text",
    text = ~ifelse(degree_all >= quantile(degree_all, 0.80, na.rm = TRUE), name, ""),
    textposition = "top center",
    hovertext = ~hover_text,
    hoverinfo = "text",
    color = ~guild,
    colors = guild_pal,
    marker = list(
      size = ~node_size,
      opacity = 0.92,
      line = list(color = "black", width = 0.7)
    )
  ) %>%
  layout(
    title = list(
      text = "3D Lake Malawi shoreline food web<br><sup>Height = trophic band; colour = guild; size = connectedness</sup>"
    ),
    scene = list(
      xaxis = list(title = "Network position X", showgrid = FALSE, zeroline = FALSE),
      yaxis = list(title = "Network position Y", showgrid = FALSE, zeroline = FALSE),
      zaxis = list(
        title = "Trophic band",
        tickvals = (1:5) * z_spacing,
        ticktext = c("Basal", "Invertebrates", "Fish", "Interface predators", "Terrestrial"),
        range = c(0.5 * z_spacing, 5.6 * z_spacing)
      ),
      camera = list(eye = list(x = 1.7, y = 1.5, z = 1.35))
    ),
    legend = list(title = list(text = "Guild"))
  )

p_3d_trophic

Interpretation. This figure emphasizes the vertical trophic structure of the web. Basal resources occur in the lower band, invertebrate consumers occupy the next level, fish form a major middle layer, and shoreline-interface predators occur higher in the plot. Highly connected nodes are visually larger, allowing likely hubs to be identified quickly. Because the graph is interactive, hovering over a species reveals its guild, habitat zone, degree values, and betweenness score.

11 Figure 2: Interactive 3D Shoreline-Forest Transition Network

This figure uses the same coordinates as Figure 1, but it changes the ecological question. Instead of colouring nodes by guild, nodes are coloured by habitat zone. Node size is based on betweenness centrality, which highlights bridge species.

p_3d_habitat <- plot_ly() %>%
  add_trace(
    x = edge_trace$x,
    y = edge_trace$y,
    z = edge_trace$z,
    type = "scatter3d",
    mode = "lines",
    line = list(color = "rgba(0,0,0,0.42)", width = 3),
    hoverinfo = "none",
    showlegend = FALSE
  ) %>%
  add_trace(
    data = nodes_3d,
    x = ~x,
    y = ~y,
    z = ~z,
    type = "scatter3d",
    mode = "markers+text",
    text = ~ifelse(betweenness >= quantile(betweenness, 0.85, na.rm = TRUE), name, ""),
    textposition = "top center",
    hovertext = ~hover_text,
    hoverinfo = "text",
    color = ~habitat,
    colors = habitat_pal,
    marker = list(
      size = ~scales::rescale(betweenness + 0.001, to = c(5, 24)),
      opacity = 0.94,
      line = list(color = "black", width = 0.7)
    )
  ) %>%
  layout(
    title = list(
      text = "3D shoreline-forest transition network<br><sup>Colour = habitat; size = betweenness bridge score</sup>"
    ),
    scene = list(
      xaxis = list(title = "Network position X", showgrid = FALSE, zeroline = FALSE),
      yaxis = list(title = "Network position Y", showgrid = FALSE, zeroline = FALSE),
      zaxis = list(
        title = "Trophic band",
        tickvals = (1:5) * z_spacing,
        ticktext = c("Basal", "Invertebrates", "Fish", "Interface predators", "Terrestrial"),
        range = c(0.5 * z_spacing, 5.6 * z_spacing)
      ),
      camera = list(eye = list(x = 1.7, y = 1.5, z = 1.35))
    ),
    legend = list(title = list(text = "Habitat zone"))
  )

p_3d_habitat

Interpretation. This figure is the strongest plot for the shoreline-forest question. Aquatic/littoral species show the lake-based part of the network, forest/terrestrial species show the land-based side, and shoreline-interface species show the transition zone. Large nodes in this figure are not simply the most connected species; they are bridge species with high betweenness. These species may connect aquatic production to birds, reptiles, mammals, and forest-edge consumers.

12 Figure 3: Focal 3D Network Around a Key Species

Sometimes the whole food web is too dense to explain a single species clearly. A focal ego network solves that problem by extracting the local network around one species. The example below uses Nile_crocodile if it is present in the data.

plot_3d_focal_species <- function(focal_species, order = 2, mode = "all") {
  if (!focal_species %in% V(g_ig)$name) {
    stop(paste("Species not found:", focal_species))
  }

  ego <- make_ego_graph(g_ig, order = order, nodes = focal_species, mode = mode)[[1]]

  ego_nodes <- nodes_3d %>%
    filter(name %in% V(ego)$name)

  ego_edges <- as_data_frame(ego, what = "edges") %>%
    rename(consumer = from, resource = to) %>%
    left_join(
      ego_nodes %>% select(consumer = name, x_from = x, y_from = y, z_from = z),
      by = "consumer"
    ) %>%
    left_join(
      ego_nodes %>% select(resource = name, x_to = x, y_to = y, z_to = z),
      by = "resource"
    )

  ego_edge_trace <- make_edge_trace(
    ego_edges,
    colour = "rgba(0,0,0,0.55)",
    width = 4
  )

  plot_ly() %>%
    add_trace(
      x = ego_edge_trace$x,
      y = ego_edge_trace$y,
      z = ego_edge_trace$z,
      type = "scatter3d",
      mode = "lines",
      line = list(color = "rgba(0,0,0,0.55)", width = 4),
      hoverinfo = "none",
      showlegend = FALSE
    ) %>%
    add_trace(
      data = ego_nodes,
      x = ~x,
      y = ~y,
      z = ~z,
      type = "scatter3d",
      mode = "markers+text",
      text = ~name,
      textposition = "top center",
      hovertext = ~hover_text,
      hoverinfo = "text",
      color = ~guild,
      colors = guild_pal,
      marker = list(
        size = ~ifelse(name == focal_species, 26, node_size),
        opacity = 0.95,
        line = list(
          color = ~ifelse(name == focal_species, "black", "grey30"),
          width = 1.2
        )
      )
    ) %>%
    layout(
      title = list(
        text = paste0(
          "3D focal network: ", focal_species,
          "<br><sup>Order ", order, " ego network; mode = ", mode, "</sup>"
        )
      ),
      scene = list(
        xaxis = list(title = "Network position X", showgrid = FALSE, zeroline = FALSE),
        yaxis = list(title = "Network position Y", showgrid = FALSE, zeroline = FALSE),
        zaxis = list(
          title = "Trophic band",
          tickvals = (1:5) * z_spacing,
          ticktext = c("Basal", "Invertebrates", "Fish", "Interface predators", "Terrestrial"),
          range = c(0.5 * z_spacing, 5.6 * z_spacing)
        ),
        camera = list(eye = list(x = 1.7, y = 1.5, z = 1.35))
      )
    )
}
if ("Nile_crocodile" %in% V(g_ig)$name) {
  plot_3d_focal_species("Nile_crocodile", order = 3, mode = "out")
} else {
  print("Nile_crocodile was not found in this dataset.")
}

Interpretation. The focal network is useful for explaining the feeding neighbourhood of one species. With the edge convention used here, mode = "out" shows the resources or prey connected outward from the focal consumer. For an apex or shoreline predator, this can show how aquatic prey and shoreline resources contribute to its diet network.

13 Overall Ecological Interpretation

The Lake Malawi shoreline-forest food web can be interpreted as a connected ecotonal system. Basal aquatic resources such as phytoplankton, algae, macrophytes, and detritus form the lower trophic base. These resources support zooplankton and benthic invertebrates, which then transfer energy upward to cichlid fish and other fish. Fish and invertebrates become important pathways linking the littoral zone to waterbirds, reptiles, and semi-aquatic mammals.

The shoreline-interface species are especially important because they connect the aquatic and terrestrial sides of the food web. In the habitat-coloured 3D plot, species with high betweenness centrality should be read as structural bridges. Their position suggests that changes to these species could affect multiple parts of the system, although this should be interpreted as a network-based hypothesis rather than direct proof of keystone status.

The most informative way to discuss this network is therefore in three layers:

  1. Basal support: Which primary producers and detrital resources support the lower food web?
  2. Aquatic transfer: Which invertebrates and fish move energy upward through the littoral food web?
  3. Shoreline bridging: Which birds, reptiles, mammals, or generalist consumers connect lake-based production to the forest edge?