Exploration of project memberships

Author

Romain Frelat

Published

August 13, 2026

The objective is to explore the relation among members of FRB-MTE-OFB projects.

Based on project membership, we:

This document is mostly an exploration of network analysis methodology and interactive visualization.

The raw data is not available on Github to safeguard privacy.

General overview

Code
library(igraph) # network analysis
library(visNetwork) # interactive network
library(plotly) # interactive plot

# load membership list
mem <- read.csv(
  here::here("data", "derived-data", "mte_members.csv")
)

# load informations on projects
meta <- readxl::read_xlsx(
  here::here("data", "raw-data", "mte", "Projets_FRB-MTECT-OFB.xlsx")
)

# create a last+first name column
mem$name <- paste(mem$lastname, mem$firstname)

# number of members per group and position
table(mem$group, mem$function_in_group)
             
              Associate Member PI Post-doc
  Acoucene            0     10  2        1
  Beyonds             0     10  2        1
  Carapat             1      4  2        0
  Comepi              0      8  1        1
  Desybel             1      8  1        0
  Discar              0     10  2        1
  Dragon              0      9  2        1
  Ebenn               0      5  1        1
  Elan                0      6  1        0
  Fellow              0     10  2        1
  Fragshifts          0      8  2        1
  Funbiodiv           0     10  2        1
  Funindic            0      5  1        1
  Impacts             0     10  2        1
  Indicators          0     10  2        1
  Interface           0      5  1        0
  Landbio             0      8  1        0
  Landworm            0      7  3        1
  Motiver             0     10  2        1
  Pppirec             1      1  2        0
  Rodexpo             1      6  2        0
  Solaire-pb          0      7  1        1
  Spatman             0      8  2        1
  Tres-pratic         0      8  1        1
Code
# number of members per type of group
n_mem <- data.frame(table(mem$group))
n_mem$Type <- meta$Type[match(n_mem$Var1, meta$Acronyme)]
tapply(n_mem$Freq, n_mem$Type, summary)
$`REVUE SYSTEMATIQUE`
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
    7.0     8.5     9.5     9.0    10.0    10.0 

$SYNERGIE
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   4.00    7.00    8.00    8.30    9.75   13.00 

$SYNTHESE
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  11.00   12.25   13.00   12.50   13.00   13.00 
Code
# list all group per name
infoV <- tapply(mem$group, mem$name, paste, collapse = "<br>")

nproj <- table(table(mem$name))

Among the 24 projects of the programs, there are 223 different researchers. Most of the researcher are member of a single project (N=204), but 17 researchers are attached to 2 projects; and 2 researchers works on 3 projects.

Project network

Based on the number of shared members, we can build a network of projects.

Code
group_mem <- table(mem$group, mem$name)
member_overlap <- tcrossprod(group_mem)
# or Jaccard to account for different group size
# proxy::simil(group_mem, method = "Jaccard", diag = TRUE, upper = TRUE)

diag(member_overlap) <- 0

netMEM <- igraph::graph_from_adjacency_matrix(
  member_overlap,
  mode = "undirected",
  weighted = TRUE
)

# modularity (optimal because the graph is tiny)
modMEM <- cluster_walktrap(netMEM) # same as cluster_optimal

# set color per group
cluMEM <- membership(modMEM)
nclu <- table(cluMEM)
cclu <- colorspace::qualitative_hcl(n = length(nclu), palette = "Dark 3")
cclu[nclu == 1] <- "#808080" # grey
colMEM <- cclu[cluMEM]
simclu <- ifelse(cluMEM <= (sum(nclu > 1)), cluMEM, "isolated")

The network has 24 nodes, 19 edges, a connectance of 0.069 and a modularity of 0.69

Project network with modules

Code
V(netMEM)$type <- as.factor(meta$Type[match(V(netMEM)$name, meta$Acronyme)])
V(netMEM)$year <- meta$Year[match(V(netMEM)$name, meta$Acronyme)]
V(netMEM)$group <- simclu
V(netMEM)$color <- colMEM
E(netMEM)$width <- E(netMEM)$weight
V(netMEM)$title <- paste(
  V(netMEM)$name,
  V(netMEM)$type,
  V(netMEM)$year,
  sep = "<br>"
)
visNetwork::visIgraph(netMEM, randomSeed = 54, layout = "layout_nicely") |>
  visOptions(
    highlightNearest = TRUE,
    selectedBy = list(variable = "group", multiple = T)
  ) |>
  visPhysics(stabilization = FALSE)
Code
# layout_nicely, layout_with_fr, layout_with_kk
Code
# identify cliques
for (i in sort(unique(simclu))) {
  print(paste(
    i,
    paste(V(netMEM)$name[simclu == i], collapse = ", "),
    sep = ": "
  ))
}
[1] "1: Comepi, Fellow, Funbiodiv, Motiver"
[1] "2: Acoucene, Indicators, Interface, Landbio"
[1] "3: Dragon, Funindic, Impacts, Landworm, Pppirec, Tres-pratic"
[1] "4: Desybel, Ebenn, Solaire-pb"
[1] "5: Beyonds, Elan"
[1] "isolated: Carapat, Discar, Fragshifts, Rodexpo, Spatman"

Project network with year and type

Code
# color by typexnod
typshp <- c("triangle", "square", "dot")[V(netMEM)$type]
V(netMEM)$shape <- typshp

# x-axis by year
vyr <- V(netMEM)$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)
# layout_nicely(netMEM)[, 2]
# sqrt(membership(modMEM)) / max(sqrt(membership(modMEM)))
# 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)
laby <- sort(unique(vyr))
seqy <- (laby - min(laby)) / (max(laby) - min(laby)) * 2 - 1

visNetwork::visIgraph(
  netMEM,
  layout = 'layout.norm',
  layoutMatrix = coo,
  randomSeed = 54,
  type = "full"
) |>
  visLegend(
    addNodes = list(
      list(
        label = "Revue",
        shape = "triangle",
        color = "grey"
      ),
      list(
        label = "Synergie",
        shape = "square",
        color = "grey"
      ),
      list(
        label = "Synthese",
        shape = "dot",
        color = "grey"
      )
    ),
    useGroups = FALSE
  ) |>
  visOptions(
    highlightNearest = TRUE,
    selectedBy = list(variable = "type", multiple = T)
  )

Centrality

There are a lot of different measures of centrality. Here we use betweenness (number of shortest paths going through a vertex) and the Google page rank.

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

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
  )
Code
info <- apply(centP[, 1:3], 2, round, 3)
score <- apply(scale(sqrt(info)), 1, mean)
DT::datatable(info[order(score, decreasing = TRUE), ])

Researcher network

Based on project membership, we can build two member networks:

  1. where all group members are linked among them
  2. where group members are linked only to the PI+postdoc
Code
pw <- c()
pw_pi <- c()
for (i in sort(unique(mem$group))) {
  ni <- mem$name[mem$group == i]
  pwi <- expand.grid(ni, ni) #or combn(ni, 2) for undirected
  combn(ni, 2)
  #remove identity / loop
  pwi <- pwi[pwi$Var1 != pwi$Var2, ]
  pw <- rbind(pw, pwi)

  # considering the central role of PI
  pi <- mem$name[
    mem$function_in_group %in% c("PI", "Post-doc") & mem$group == i
  ]
  pwi2 <- expand.grid(ni, pi)
  pwi2 <- pwi2[as.character(pwi2$Var1) != as.character(pwi2$Var2), ]
  pw_pi <- rbind(pw_pi, pwi2)
}

# add weight based on how many times
netRES <- igraph::graph_from_data_frame(pw, directed = FALSE)
modRES <- cluster_walktrap(netRES)
# plot(
#   netRES,
#   layout = layout_with_mds(netRES),
#   vertex.label = NA
# )

netRES_PI <- igraph::graph_from_data_frame(pw_pi, directed = FALSE)
modRES_PI <- cluster_walktrap(netRES_PI)
# plot(
#   netRES_PI,
#   layout = layout_with_mds(netRES_PI),
#   vertex.label = NA
# )

A. all members

The network linking all group members between them has 223 nodes, 2406 edges, a connectance of 0.097 and a modularity of 0.86.

Code
V(netRES)$title <- paste(
  V(netRES)$name,
  infoV[match(V(netRES)$name, names(infoV))],
  sep = "<br>"
)

modcol <- rainbow(max(membership(modRES)))[membership(modRES)]
V(netRES)$color <- modcol
visNetwork::visIgraph(netRES)

B. PI connected

The network linking all group members to the PI+Postdoc has 223 nodes, 559 edges, a connectance of 0.023 and a modularity of 0.89.

Code
V(netRES_PI)$title <- paste(
  V(netRES_PI)$name,
  infoV[match(V(netRES_PI)$name, names(infoV))],
  sep = "<br>"
)

modcol <- rainbow(max(membership(modRES_PI)))[membership(modRES_PI)]
V(netRES_PI)$color <- modcol
visNetwork::visIgraph(netRES_PI)

Centrality

We calculate centrality on the network with all members connected.

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

plot_ly(centR) |>
  add_markers(
    x = ~betweenness,
    y = ~pagerank,
    size = ~degree,
    text = ~lab,
    hoverinfo = "text"
  ) |>
  layout(title = "Researcher centrality") |>
  config(
    modeBarButtons = list(list("toImage")),
    displaylogo = FALSE
  )
Code
info <- apply(centR[, -4], 2, round, 3)
score <- apply(scale(sqrt(info)), 1, mean)
DT::datatable(info[order(score, decreasing = TRUE)[1:20], ])

Bipartite network

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


V(netBI)$color <- ifelse(
  V(netBI)$type,
  V(netMEM)$color[match(V(netBI)$name, V(netMEM)$name)],
  "lightblue"
)
V(netBI)$shape <- ifelse(V(netBI)$type, "square", "dot")
# V(netBI)$size <- ifelse(V(netBI)$type, 3, 1)

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

# V(netBI)$size <- 1
modBI <- cluster_walktrap(netBI)

showBI <- delete_vertices(netBI, degree(netBI) == 1)
# plot(netBI, layout = layout_as_bipartite(netBI))
visNetwork::visIgraph(showBI)

The bipartite network has 247 nodes, 244 edges, and a connectance of 0.004.

Conclusion

We identified 5 sub-groups of projects based on shared members and a handful of researchers working on multiple FRB-MTE-OFB projects.

Yet, the membership is only a rough proxy of research collaboration. To better understand the research being produced, we need to investigate the bibliographic references produced and cited by each of the projects.