Exploration of CESAB groups

Author

Romain Frelat

Published

September 2, 2026

The objective is this document is to explore the diversity and interconnectedness of FRB CESAB projects using network analysis. The exploration consists of two complementary analysis:

  1. Project membership network based on who is collaborating in the projects
  2. Citation coupling network based on the scientific articles published by projects

1. Project membership

The FRB-Cesab hosted73 projects between 2010 and 2026 with 885 different researchers. On average, a CESAB project includes 13.8 members, with a minimum of 6 for Unicop and a maximum of 43 for Parsec (Table 1). Most researchers participated in a single project (N=801), but 61 researchers collaborated in two projects; and 23 researchers worked on three or more projects.

Code
table(cesab$group, cesab$function_in_group)
Table 1: Number of members, principal investigators (PI), and post-doc per CESAB project.
                    
                     Member PI Post-Doc
  Acoucene               10  2        1
  Actias                 12  1        1
  Afrobiodrivers         14  1        1
  Agri-TE                 6  1        1
  Betsi                  13  1        1
  Beyonds                10  2        1
  Biodis                 10  3        1
  BioForest              12  2        2
  Bioshifts              11  3        1
  Blue Justice           12  3        1
  Bridge                  6  2        1
  Coreids                10  2        2
  CropTraits              7  2        0
  Discar                 10  2        1
  Disco-Weed             11  1        1
  Div4Drought            10  2        0
  DiveRS                 12  2        1
  Divgrass               14  1        2
  Dragon                  9  2        1
  Dynamite                8  2        0
  Eseb                    9  2        1
  Fairwood                8  2        0
  FARsex                  8  2        0
  FaunaServices           8  2        1
  Fellow                 10  2        1
  Fishglob                6  2        0
  FishMIP-OSP            10  2        0
  Food-webs               7  3        1
  Forcis                 11  2        1
  Free                   21  1        2
  Free 2                 19  1        1
  FresH2O Zoops          11  2        0
  FunBioDiv              10  2        1
  FunctionalWebs         12  1        1
  Fundiva                 6  2        0
  Gaspar                  9  1        1
  Geisha                 14  3        2
  Impacts                10  2        1
  InDySEM                 6  1        1
  Intraco                 5  2        0
  InvaHealth              8  2        0
  Irbas                   9  1        1
  Islands                14  1        2
  Islets                  8  2        0
  JustConservation        7  1        1
  KH Appropriation       14  2        0
  KH Archetypes          12  3        1
  KH Evolution            1  2        1
  KH Justice             14  2        1
  KH Protected Areas      0  0        0
  LandWorm                7  3        1
  Lola BMS               15  2        1
  Maestro                15  2        1
  Motiver                10  2        1
  Navidiv                 9  2        1
  Nefineo                 7  2        1
  Netseed                17  1        2
  Parsec                 40  2        1
  Pelagic                23  3        2
  PhenoFish               9  2        0
  PowerBiodiv            11  2        1
  PPR Océans             13  1        1
  RAATD                  12  1        1
  Rainbio                16  1        1
  Red-Bio                 6  2        0
  Rivage                 10  2        1
  Score-Reef             15  2        2
  Silk                    6  2        0
  Spatman                 8  2        1
  SpeciationDB            7  2        0
  SynTreeSys             12  2        1
  Unicop                  4  2        0
  Woodiv                 13  1        3

Bipartite membership network

Code
netBI <- graph_from_data_frame(
  cesab[, c("name", "group")]
)
V(netBI)$type <- V(netBI)$name %in% cesab$group

V(netBI)$color <- ifelse(V(netBI)$type, "blue", "red")
V(netBI)$shape <- ifelse(V(netBI)$type, "square", "dot")
# V(netBI)$size <- ifelse(V(netBI)$type, 3, 1)

extra <- ifelse(
  V(netBI)$type,
  "",
  infoV[match(V(netBI)$name, names(infoV))]
)
V(netBI)$title <- paste(
  V(netBI)$name,
  extra,
  sep = "<br>"
)

# remove members that are not connected to multiple groups
# for visualization purpose only
showBI <- delete_vertices(netBI, degree(netBI) == 1)

visNetwork::visIgraph(
  showBI,
  randomSeed = 25,
  layout = "layout_with_fr", #layout_with_kk, layout_nicely
  smooth = TRUE
) |>
  visOptions(
    highlightNearest = TRUE
  )
Figure 1: Simplified bipartite network containing all projects (nodes in blue square) and the members that participated in multiple projects (nodes in red circle).

The bipartite network offer a complete visualization of the projects (blue square) and their members (red circles) (Figure 1). However, the complexity of handling two types of nodes (project and member) prevent further in-depth analysis. The bipartite network can be projected into (1) a project network, and (2) a member network.

Project network

The project network is made of nodes representing projects and edges representing the number of shared members.

Code
# Jaccard transformation to handle uneven nomber of members
# sim <- similarity(netPR, method = "jaccard")
# diag(sim) <- 0
# netPRj <- igraph::graph_from_adjacency_matrix(
#   sim,
#   mode = "undirected",
#   weighted = TRUE
# )
# V(netPRj)$name <- V(netPR)$name

modPR <- cluster_fast_greedy(netPR)
cluPR <- membership(modPR)
nclu <- table(cluPR)
simclu <- ifelse(cluPR <= (sum(nclu > 1)), cluPR, "isolated")
# add0 <- cluPR < 10 & simclu != "isolated"
# simclu[add0] <- paste0("0", simclu[add0])
simclu <- as.factor(simclu)
cclu <- colorspace::qualitative_hcl(n = nlevels(simclu) - 1, palette = "Dark 3")
cclu <- c(cclu, "#808080") # grey
colPR <- cclu[simclu]

The network has 73 nodes (one node = one project) and 122 edges (one edge = shared member between two projects). The connectance is 0.046 (very low) and the modularity is 0.51 (moderately high). It means that there are few connections among projects but they are organized in highly connected sub-groups (called modules).

Modules

Code
V(netPR)$group <- simclu
V(netPR)$color <- colPR
E(netPR)$width <- E(netPR)$weight
E(netPR)$title <- E(netPR)$weight
V(netPR)$year <- cesab$start_year[match(V(netPR)$name, cesab$group)]
V(netPR)$title <- paste(V(netPR)$name, V(netPR)$year, sep = "<br>")

visNetwork::visIgraph(
  netPR,
  randomSeed = 25,
  layout = "layout_with_fr",
  smooth = TRUE,
  type = "full"
) |>
  visOptions(
    highlightNearest = TRUE,
    selectedBy = list(variable = "group", multiple = T)
  )
Figure 2: Project network of shared membership. The width of the edges corresponds to the number of members shared between projects. The different modules of projects are highlighted in different colors.
Code
# echo: false
# identify cliques
for (i in sort(unique(simclu))) {
  print(paste(
    i,
    paste(V(netPR)$name[simclu == i], collapse = ", "),
    sep = ": "
  ))
}
[1] "1: CropTraits, Disco-Weed, Divgrass, Eseb, Fishglob, Free, Free 2, Impacts, Maestro, Nefineo, Silk"
[1] "2: Actias, Betsi, FaunaServices, FunBioDiv, Islands, LandWorm, Motiver"
[1] "3: Bioshifts, Discar, Fundiva, Gaspar, KH Protected Areas, Pelagic, PhenoFish, RAATD, Rivage, Score-Reef"
[1] "4: Blue Justice, InDySEM, JustConservation, KH Appropriation, KH Archetypes, KH Evolution, KH Justice, Parsec, PowerBiodiv, PPR Océans"
[1] "5: Agri-TE, Beyonds, Dragon, Lola BMS, Navidiv, Woodiv"
[1] "6: BioForest, Bridge, Fellow, Islets, SynTreeSys"
[1] "7: Coreids, Food-webs, FresH2O Zoops, Geisha, Netseed, Red-Bio"
[1] "8: Div4Drought, Intraco"
[1] "9: Dynamite, Forcis"
[1] "isolated: Acoucene, Afrobiodrivers, Biodis, DiveRS, Fairwood, FARsex, FishMIP-OSP, FunctionalWebs, InvaHealth, Irbas, Rainbio, Spatman, SpeciationDB, Unicop"

Temporal dynamic

Code
# x-axis by year
vyr <- V(netPR)$year
xnod <- (vyr - min(vyr)) / (max(vyr) - min(vyr)) * 2 - 1

# take the membership as y-axis
ymod <- as.numeric(as.factor(simclu))
ynod <- (ymod / max(ymod)) * 2 - 1
# or take the first MDS dimension
# ynod <- layout_with_mds(netMEM)[, 1]

# coordinates
coo <- cbind(jitter(xnod, 1.5), jitter(ynod, 3))

# plot(netMEM, layout = coo)

visNetwork::visIgraph(
  netPR,
  layout = 'layout.norm',
  layoutMatrix = coo,
  randomSeed = 54,
  type = "full"
) |>
  visOptions(
    highlightNearest = TRUE,
    selectedBy = list(variable = "year", multiple = T)
  )
Figure 3: Project network of shared membership visualized per starting year (in x-axis) and per module (in y-axis).

Project centrality

To quantify the project centrality, i.e. how project connects different parts of the network, we examine three indices:

  • Degree: number of connections to other projects;
  • Google PageRank: influence based on the number and the importance of connected project;
  • Betweenness: number of shortest paths going through a project (calculated for the largest connected graph)
Code
centP <- data.frame(
  "degree" = degree(netPR),
  "pagerank" = page_rank(netPR)$vector,
  "betweenness" = betweenness(netPR),
  "lab" = V(netPR)$title,
  "color" = colPR
)

plot_ly(centP) |>
  add_markers(
    x = ~betweenness,
    y = ~pagerank,
    size = ~degree,
    marker = list(color = ~color, line = list(color = ~color)),
    text = ~lab,
    hoverinfo = "text"
  ) |>
  layout(title = "Project centrality") |>
  config(
    modeBarButtons = list(list("toImage")),
    displaylogo = FALSE
  )
Figure 4: Project centrality based on shared members. The size of the dot corresponds to the degree (number of connections), its colors corresponds to the modules.
Code
info <- apply(centP[, 1:3], 2, round, 3)
score <- apply(scale(sqrt(info)), 1, mean)
DT::datatable(info[order(score, decreasing = TRUE), ])
Table 2: Project centrality based on shared members. The degree is the number of connections to other projects, the Google PageRank measures the influence based on the number and the importance of connected project and the betweenness is the number of shortest paths going through a project.

Member network

The member network is made of nodes representing members and edges representing project collaboration. In other words, all members of a project are connected to each other. Because of the high number of nodes (885 members), it is not informative to plot the network, but it provides relevant information about members’ centrality.

Member centrality

The member centrality quantifies how individuals connect different parts of the network. As previously, it is quantified by three indicators: degree, Google PageRank, and Betweenness.

Code
# Visualization
E(netMEM)$width <- E(netMEM)$weight
V(netMEM)$size <- log(degree(netMEM))
V(netMEM)$title <- paste(
  V(netMEM)$name,
  infoV[match(V(netMEM)$name, names(infoV))],
  sep = "<br>"
)

# visNetwork::visIgraph(netMEM, randomSeed = 54, layout = "layout_with_fr") |>
#   visOptions(
#     highlightNearest = TRUE,
#   )

# Centrality
centR <- data.frame(
  "betweenness" = betweenness(netMEM),
  "degree" = degree(netMEM),
  "pagerank" = page_rank(netMEM)$vector,
  "lab" = V(netMEM)$title
)

plot_ly(centR) |>
  add_markers(
    x = ~betweenness,
    y = ~pagerank,
    size = ~degree,
    text = ~lab,
    hoverinfo = "text"
  ) |>
  layout(title = "Member centrality") |>
  config(
    modeBarButtons = list(list("toImage")),
    displaylogo = FALSE
  )
Figure 5: Member centrality based on project collaboration. The size of the dot corresponds to the degree (number of connections).
Code
info <- apply(centR[, -4], 2, round, 5)
score <- apply(scale(sqrt(info)), 1, mean)
info <- as.data.frame(info)

infoV <- tapply(cesab$group, cesab$name, paste, collapse = ", ")
info$project <- infoV[match(row.names(info), names(infoV))]
# info$first_aut <- V(netREF)$faut
# info$year <- V(netREF)$year
# info$short_title <- V(netREF)$shorttitle
DT::datatable(info[order(score, decreasing = TRUE), ])
Table 3: Member’s centrality based on project collaboration. The degree is the number of collaborations, the Google PageRank measures the influence based on the number and the importance of collaborations and the betweenness is the number of shortest paths going through a member.

2. Citation coupling

Another way to measure the closeness or distance between scientific projects is to use the scientific articles published from the project activities.

The CESAB projects published 405 scientific articles between 2011 and 2026. We estimate the distance among articles by using the bibliographic coupling, i.e. the pairwize overlap in the reference lists. Documents that share many references are likely working on similar topics or using similar methods.

Coupling network

Code
# get the co-citation network of references
NetMatrix <- bibliometrix::biblioNetwork(
  M,
  analysis = "coupling",
  network = "references",
  n = NULL,
  short = FALSE,
  sep = ";"
)
# dim(NetMatrix) # 406

netCC <- igraph::graph_from_adjacency_matrix(
  NetMatrix,
  mode = "undirected",
  diag = FALSE,
  weighted = TRUE
)
V(netCC)$name <- M$shortname[match(V(netCC)$name, M$SR)]

modCC <- cluster_louvain(netCC)

# delete non connected literature
# not needed anymore
# netCC <- delete_vertices(netCC, degree(netCC) == 0)
netCC2 <- delete_vertices(netCC, components(netCC)$membership != 1)
# table(components(netCC)$membership)

# modularity
modCC2 <- cluster_louvain(netCC2)
cluCC2 <- membership(modCC2)
# group isolated modules
# scluCC <- ifelse(cluCC <= (sum(table(cluCC2) > 1)), cluCC, "isolated")
# add 0 for nicer sorting
add0 <- cluCC2 < 10
cluCC2[add0] <- paste0("0", cluCC2[add0])
cluCC2 <- as.factor(cluCC2)
# attribute colors
palCC2 <- colorspace::qualitative_hcl(n = nlevels(cluCC2), palette = "Dark 3")
colCC2 <- palCC2[cluCC2]

The network has 405 nodes (one node = one article), and 12 546 edges (one edge = shared references between two articles). The connectance is 0.153 and the modularity 0.38.

For visual representation, we only keep the largest connected community of articles (we removed the disconnected articles, N=9).

Code
V(netCC2)$title <- paste(
  V(netCC2)$name,
  M$shortjournal[match(V(netCC2)$name, M$shortname)],
  M$breaktitle[match(V(netCC2)$name, M$shortname)],
  M$linkDOI[match(V(netCC2)$name, M$shortname)],
  M$project[match(V(netCC2)$name, M$shortname)],
  sep = "<br>"
)
V(netCC2)$group <- cluCC2
V(netCC2)$color <- colCC2

# check out other layout
visNetwork::visIgraph(
  netCC2,
  layout = "layout_with_fr",
  idToLabel = FALSE,
  randomSeed = 25,
  # smooth = TRUE,
  type = "full"
) |>
  visOptions(
    highlightNearest = TRUE,
    selectedBy = list(variable = "group", multiple = T)
  )
Figure 6: Largest connected community of the coupling network. Nodes are articles and edges represent the shared cited references. The colors represent the modules of the coupling network.

Temporal dynamic

Code
# x-axis by year
vyr <- M$PY[match(V(netCC2)$name, M$shortname)]
V(netCC2)$year <- vyr
xnod <- (vyr - min(vyr)) / (max(vyr) - min(vyr)) * 2 - 1

# take the membership as y-axis
ymod <- as.numeric(as.factor(cluCC2))
ynod <- (ymod / max(ymod)) * 2 - 1
# or take the first MDS dimension
# ynod <- layout_with_mds(netMEM)[, 1]

# coordinates
coo <- cbind(jitter(xnod, 1.5), jitter(ynod, 3))

# plot(netMEM, layout = coo)

visNetwork::visIgraph(
  netCC2,
  layout = 'layout.norm',
  layoutMatrix = coo,
  randomSeed = 54,
  type = "full"
) |>
  visOptions(
    highlightNearest = TRUE,
    selectedBy = list(variable = "year", multiple = T)
  )
Figure 7: Coupling network of shared references represented per publishing year (in x-axis) and per module (in y-axis and in different colors). Nodes are articles and edges represent the shared cited references.
Code
year_mod <- table(cluCC2, vyr) |> as.data.frame()
names(year_mod) <- c("module", "year", "count")

plot_ly(
  year_mod,
  x = ~year,
  y = ~count,
  color = ~module,
  colors = palCC2,
  type = 'bar'
) |>
  layout(
    barmode = "stack",
    xaxis = list(title = "Year"),
    yaxis = list(title = "Count"),
    legend = list(title = list(text = "Module")),
    hovermode = "x unified"
  )
Figure 8: Number of references per year and per module.

Article centrality

Code
centR2 <- data.frame(
  "betweenness" = betweenness(netCC2),
  "degree" = degree(netCC2),
  "pagerank" = page_rank(netCC2)$vector,
  "color" = V(netCC2)$color,
  "lab" = V(netCC2)$title
)

plot_ly(centR2) |>
  add_markers(
    x = ~betweenness,
    y = ~pagerank,
    size = ~degree,
    marker = list(color = ~color, line = list(color = ~color)),
    text = ~lab,
    hoverinfo = "text"
  ) |>
  layout(title = "Article centrality") |>
  config(
    modeBarButtons = list(list("toImage")),
    displaylogo = FALSE
  )
Figure 9: Article centrality based on citation coupling. The size of the dot corresponds to the degree (number of connections), its colors corresponds to the modules.
Code
# compute centrality on all references
centR <- data.frame(
  "betweenness" = round(betweenness(netCC), 1),
  "degree" = round(degree(netCC), 1),
  "pagerank" = round(page_rank(netCC)$vector, 5)
)

score <- apply(scale(sqrt(centR)), 1, mean)
centR <- as.data.frame(centR)

centR$journal <- M$shortjournal[match(row.names(centR), M$shortname)]
# centR$title <- M$breaktitle[match(row.names(centR), M$shortname)]
centR$project <- M$project[match(row.names(centR), M$shortname)]
centR$DOI <- M$linkDOI[match(row.names(centR), M$shortname)]
DT::datatable(centR[order(score, decreasing = TRUE), ], escape = FALSE)
Table 4: Article centrality based on citation coupling. The degree is the number of connections to other articles, the Google PageRank measures the influence based on the number and the importance of connected references and the betweenness is the number of shortest paths going through a reference.

Centrality per project

Each article is attached to one or multiple projects. We can use this link to calculate the average article centrality per project.

Code
pp <- strsplit(centR$project, ", ")
# data frame per reference
infoR <- data.frame(
  "project" = unlist(pp),
  "betweenness" = rep(centR$betweenness, sapply(pp, length)),
  "degree" = rep(centR$degree, sapply(pp, length)),
  "pagerank" = rep(centR$pagerank, sapply(pp, length))
)

# statistics per project
infoP <- data.frame(
  "project" = sort(unique(infoR$project)),
  "betweenness" = round(tapply(infoR$betweenness, infoR$project, mean), 1),
  "betweenness_sd" = tapply(infoR$betweenness, infoR$project, sd),
  "degree" = round(tapply(infoR$degree, infoR$project, mean), 1),
  "pagerank" = round(tapply(infoR$pagerank, infoR$project, mean), 5),
  "pagerank_sd" = tapply(infoR$pagerank, infoR$project, sd),
  "N" = as.numeric(table(infoR$project))
)
# infoP$project[!infoP$project %in% cesab$group]
infoP$year <- cesab$start_year[match(infoP$project, cesab$group)]
infoP$lab <- paste(
  infoP$project,
  ifelse(is.na(infoP$year), "", infoP$year),
  "<br>N ref =",
  infoP$N
)

plot_ly(
  infoP,
  x = ~betweenness,
  y = ~pagerank,
  # color = ~project,
  size = ~N,
  # marker = list(color = ~project, line = list(color = ~project)),
  text = ~lab,
  hoverinfo = "text",
  type = "scatter",
  mode = "markers",
  # error_x = ~ list(array = betweenness_sd),
  # error_y = ~ list(array = pagerank_sd),
  showlegend = FALSE
) |>
  layout(title = "Average article centrality per project") |>
  config(
    modeBarButtons = list(list("toImage")),
    displaylogo = FALSE
  )
Figure 10: Average article centrality per project. The size of the dot corresponds to the number of articles per project.
Code
info <- infoP[, c("project", "year", "N", "degree", "pagerank", "betweenness")]
score <- apply(
  scale(sqrt(infoP[, c("degree", "pagerank", "betweenness")])),
  1,
  mean
)

DT::datatable(
  info[order(score, decreasing = TRUE), ],
  escape = FALSE,
  rownames = FALSE
)
Table 5: Average reference centrality per project. The degree is the number of shared references, the Google PageRank measures the influence based on the number and the importance of connected references and the betweenness is the number of shortest paths going through a reference.

Conclusion

  • already some cool networks analysis that provide a good overview of CESAB’s projects

Exploration to be completed…