Exploration of CESAB groups

Author

Romain Frelat

Published

September 10, 2026

The objective is this document is to explore the keywords and natural language processing from FRB CESAB scientific outputs using network analysis. The exploration consists of two complementary analysis:

  1. Keywords analysis with ‘Bibliometrix’ R package based on keywords from OpenAlex

  2. Natural language processing of abstract with tall R package based on the scientific articles published by projects

Aria, M. & Cuccurullo, C. (2017) bibliometrix: An R-tool for comprehensive science mapping analysis, Journal of Informetrics, 11(4), pp 959-975 DOI 10.1016/j.joi.2017.08.007

Aria, M., Spano, M., D’Aniello, L., Cuccurullo, C., & Misuraca, M. (2026). TALL: Text analysis for all — an interactive R-shiny application for exploring, modeling, and visualizing textual data. SoftwareX, 34, 102590. DOI 10.1016/j.softx.2026.102590

1. Keywords analysis

The CESAB projects published 405 scientific articles between 2011 and 2026. The authors listed 938 different keywords (mean = 11.49 keywords per article, sd = 3.72 ), that were cleaned into 590 keywords in OpenAlex (mean = 5.17 keywords per article, sd = 2.46 ).

Code
top1 <- nref1[order(nref1, decreasing = TRUE)[1:10]]
top2 <- nref2[order(nref2, decreasing = TRUE)[1:10]]
info <- data.frame(
  "Author_KW" = names(top1),
  "Author_N" = as.numeric(top1),
  "OpenAlex_KW" = names(top2),
  "OpenAlex_N" = as.numeric(top2)
)

# summary(nkey1)
# summary(nkey2)
# err <- which(refkey1["ECOLOGY", ] + refkey2["ECOLOGY", ] == 1)
# M$ID[err]
# M$DE[err]

info
Table 1: Most popular keywords as listed by authors and by OpenAlex.
                           Author_KW Author_N           OpenAlex_KW OpenAlex_N
1                            ecology      327               ecology        192
2                            biology      320               biology        105
3                          geography      192          biodiversity         90
4                       biodiversity      136             geography         50
5              environmental science      130             ecosystem         48
6                          ecosystem      106      species richness         44
7                            habitat       96                 trait         36
8                   computer science       81            coral reef         33
9                   species richness       66 environmental science         32
10 environmental resource management       62               habitat         32

The authors keywords seems the most complete, so we will use this list in the following analysis. We can build a bipartite network showing the relations between articles and their keywords.

Bipartite membership network

Code
# simplified bipartite network
# remove keywords used by a single ref,
refkeys1 <- refkey1[nref1 > ncol(refkey1) * 0.01, ]
# remove papers with a single keyword
refkeys1 <- refkeys1[, colSums(refkeys1) > 1]

netKO <- igraph::graph_from_biadjacency_matrix(
  refkeys1,
  mode = "all"
)

V(netKO)$type <- V(netKO)$name %in% dimnames(refkeys1)[[1]]
V(netKO)$color <- ifelse(V(netKO)$type, "red", "blue")
V(netKO)$shape <- ifelse(V(netKO)$type, "dot", "square")

extra <- ifelse(
  V(netKO)$type,
  "",
  M$project[match(V(netKO)$name, M$shortname)]
)

V(netKO)$title <- paste(
  V(netKO)$name,
  extra,
  sep = "<br>"
)

visNetwork::visIgraph(
  netKO,
  layout = "layout_with_fr",
  randomSeed = 25,
  smooth = FALSE,
) |>
  visOptions(
    highlightNearest = TRUE
  )
Figure 1: Bipartite network containing the articles (nodes in blue square) and their keywords (nodes in red circle). We kept only keywords that were used by at least 1% of the articles, and articles that used at least 2 of the selected keywords.
Code
splitBI <- bipartite_projection(netKO)
netREF <- splitBI$proj1
netKW <- splitBI$proj2

Multivariate analysis

From the bipartite network (= a matrix with articles in row, and keywords in column), we can run a multivariate analysis. After multiple tries with MCA and CA, with different standadization, the classic PCA was the one showing the most relevant decomposition (even if very low variance explained).

Code
# conceptualStructure
# based on termExtraction() then bibliometrix:::factorial()
#  uses on ca::ca and ca::mjca

# MCA : discarded because kept FALSE and TRUE factors : duplicated keywords
# refkeys1_fac <- data.frame(apply(refkeys1, 1, factor), stringsAsFactors = TRUE)
# mca1 <- ade4::dudi.acm(refkeys1_fac, scannf = FALSE, nf = 3)
# mca1 <- ca::mjca(refkeys1_fac)

# CA: using chi-square distances
# not bad but ugly shaped
# ca1 <- ade4::dudi.coa(refkeys1, scannf = FALSE, nf = 3)
# plot(ca1$co[, 1:2], pch = 16)
# plot(ca1$li[, 1:2], pch = 16)

# t-SNE: discarded because no bivariate projection
# https://www.datanovia.com/learn/machine-learning/dimension-reduction/t-sne
# install.packages("Rtsne")
# tsne1 <- Rtsne::Rtsne(
#   refkeys1,
#   check_duplicates = FALSE,
#   perplexity = mean(nkey1),
#   max_iter = 5000
# )
# plot(tsne1$Y, pch = 16)

# PCA
pca1 <- ade4::dudi.pca(
  t(refkeys1),
  #vegan::decostand(t(refkeys1), "hellinger"),
  center = TRUE,
  scale = TRUE,
  scannf = FALSE,
  nf = 2
)

pca1_p <- round(pca1$eig / sum(pca1$eig) * 100, 1)
# visual checks
# plot(pca1$co[, 1:2], pch = 16)
# plot(pca1$li[, 1:2], pch = 16)
# plot(pca1$co[, 1], rowSums(refkeys1), pch = 16)
# plot(pca1$li[, 1], colSums(refkeys1), pch = 16)
# barplot(pca1$eig) # 5 dimension would be best

# Low variance explained
# (pca1$eig / sum(pca1$eig)) * 100

# cluster the keywords with modularity from KW network
# nclust <- NbClust::NbClust(
#   pca1$co,
#   distance = "euclidean",
#   method = "kmeans"
# )
# clu <- kmeans(pca1$co, centers = 9)
modKW <- cluster_louvain(netKW) # 0.15
cluKW <- membership(modKW)
# palclu <- colorspace::qualitative_hcl(
#   n = length(unique(cluKW)),
#   palette = "Dark 3"
# )

key_df <- data.frame(
  pca1$co,
  "N" = rowSums(refkeys1),
  "clu" = as.factor(cluKW)
  # "color" = palclu[cluKW]
)

p1 <- plot_ly(key_df) |>
  add_markers(
    x = ~Comp1,
    y = ~Comp2,
    size = ~N,
    color = ~clu,
    #marker = list(color = ~color, line = list(color = ~color)),
    text = row.names(key_df),
    hoverinfo = "text"
  ) |>
  layout(
    title = "Keywords loadings",
    showlegend = FALSE,
    xaxis = list(title = paste0("PC1 - ", pca1_p[1], "%")),
    yaxis = list(title = paste0("PC2 - ", pca1_p[2], "%"))
  ) |>
  config(
    modeBarButtons = list(list("toImage")),
    displaylogo = FALSE
  )

p1
Figure 2: Loadings of the keywords in the PCA analysis on bipartite network. The size of the dot represent the number of articles listing the keyword. The clusters shown in colors were identified from the modules of the keyword network.
Code
project <- M$project[match(colnames(refkeys1), M$shortname)]
pp <- strsplit(project, ", ")
npp <- sapply(pp, length)

palproj <- colorspace::qualitative_hcl(
  n = length(unique(unlist(pp))),
  palette = "Dark 3"
)

ref_df <- data.frame(
  "shortname" = rep(colnames(refkeys1), npp),
  "project" = unlist(pp),
  "PC1" = rep(pca1$li[, 1], npp),
  "PC2" = rep(pca1$li[, 2], npp),
  "color" = palproj[as.factor(unlist(pp))],
  "N" = rep(colSums(refkeys1), npp)
)
ref_df$label <- paste(ref_df$shortname, ref_df$project, sep = "<br>")

p2 <- plot_ly(ref_df) |>
  add_markers(
    x = ~PC1,
    y = ~PC2,
    size = ~N,
    marker = list(color = ~color, line = list(color = ~color)),
    # color = ~project,
    text = ~label,
    hoverinfo = "text",
    # legendgroup = ~project
  ) |>
  layout(
    title = "References scores",
    showlegend = FALSE,
    xaxis = list(title = paste0("PC1 - ", pca1_p[1], "%")),
    yaxis = list(title = paste0("PC2 - ", pca1_p[2], "%"))
  ) |>
  config(
    modeBarButtons = list(list("toImage")),
    displaylogo = FALSE
  )

p2
Figure 3: Scores of the references in the PCA analysis on bipartite network. The size of the dot represent the number of kyword per article. The color show the projects who produced the articles.
Code
# subplot(p1, p2) |>
# layout(showlegend = FALSE, title = 'Side By Side Subplots') |>
# config(
#   modeBarButtons = list(list("toImage")),
#   displaylogo = FALSE
# )

Keyword centrality

We can also build a network of keywords.

Code
# visNetwork::visIgraph(
#   netKW,
#   layout = "layout_with_fr",
#   idToLabel = FALSE,
#   randomSeed = 25,
#   # smooth = TRUE,
#   type = "full"
# ) |>
#   visOptions(
#     highlightNearest = TRUE
#   )

centP <- data.frame(
  "degree" = degree(netKW),
  "pagerank" = page_rank(netKW)$vector,
  "closeness" = closeness(netKW),
  "betweenness" = betweenness(netKW),
  "clu" = as.factor(cluKW),
  "lab" = V(netKW)$name
)

plot_ly(centP) |>
  add_markers(
    x = ~betweenness,
    y = ~pagerank,
    size = ~degree,
    color = ~clu,
    #marker = list(color = ~color, line = list(color = ~color)),
    text = ~lab,
    hoverinfo = "text"
  ) |>
  layout(title = "Keyword centrality", showlegend = FALSE) |>
  config(
    modeBarButtons = list(list("toImage")),
    displaylogo = FALSE
  )
Figure 4: Centrality of keywords in the keyword network. The size of the dot represent the degree (number of associated keywords) and its color show the different modules.

Thematic map

Code
thematicMapResults <- thematicMap(
  M, # 405, 62
  field = "DE",
  n = 250,
  minfreq = 13,
  stemming = FALSE,
  size = 0.3,
  n.labels = 3,
  repel = TRUE,
  cluster = "louvain"
)

plot_ly(thematicMapResults$clusters) |>
  add_markers(
    x = ~centrality,
    y = ~density,
    size = ~freq,
    text = ~words,
    hoverinfo = "text"
  ) |>
  layout(
    title = "Thematic map" # ,
    # shapes = list(
    #   hline(median(thematicMapResults$clusters$density)),
    #   vline(median(thematicMapResults$clusters$centrality))
    # )
  ) |>
  config(
    modeBarButtons = list(list("toImage")),
    displaylogo = FALSE
  )
Figure 5: Thematic map made from the keyword network. The size of the dot represent the degree (number of associated keywords) and its color show the different modules.

Centrality (Callon’s centrality): Measures the strength of external ties between a cluster and other clusters. High centrality indicates that a theme is strongly connected to other themes, making it central to the field.

Density (Callon’s density): Measures the strength of internal ties within a cluster. High density indicates that a theme is well-developed and internally coherent.

  • Upper-right (Motor themes): High centrality + High density. These are well-developed themes that are central to the field. They drive the research agenda and are both internally mature and externally relevant.
  • Lower-right (Basic/Transversal themes): High centrality + Low density. These themes are important to the field but not yet well-developed. They represent general, transversal topics that cut across many research areas.
  • Upper-left (Niche themes): Low centrality + High density. These are well-developed but peripheral themes. They represent specialized topics with a strong internal structure but limited connections to the broader field.
  • Lower-left (Emerging/Declining themes): Low centrality + Low density. These themes are both peripheral and underdeveloped. They may represent either newly emerging topics or themes that are fading from the research landscape.

2. Natural language processing of abstract

Can’t really use tall package outside the shiny app (and should be executed from terminal, not Positron) …

https://massimoaria.github.io/tall-app/

tall::tall()