rm(list = ls())
seed <- 1
set.seed(seed)
require(kaiaulu)
require(igraph)
require(visNetwork)
require(data.table)
require(yaml)
require(stringi)
require(knitr)
This notebook demonstrates how to compute radio silence — a social smell that identifies developers who act as sole bridges between otherwise disconnected communication communities. The pipeline uses GitHub issue and pull request reply data.
Radio silence boundary spanners are authors who are the only link between two distinct developer communities. If they stop communicating, the communities lose contact entirely, creating a coordination risk.
The pipeline proceeds as follows:
smell_radio_silencetool <- parse_config("../tools.yml")
conf <- parse_config("../conf/kaiaulu.yml")
oslom_undir_path <- get_tool_project("oslom_undir", tool)
dv8_path <- get_tool_project("dv8", tool)
github_issue_path <- get_github_issue_path(conf, "project_key_1")
github_pull_request_path <- get_github_pull_request_path(conf, "project_key_1")
github_reply_path <- get_github_issue_or_pr_comment_path(conf, "project_key_1")
github_commit_path <- get_github_commit_path(conf, "project_key_1")
github_pr_comments_path <- get_github_pr_comments_path(conf, "project_key_1")
# DV8 parameters
project_path <- get_dv8_folder_path(conf)
project_name <- stringi::stri_split_regex(project_path, pattern = "/")[[1]]
project_name <- project_name[length(project_name)]
Parse all GitHub issues, pull requests, and comments into a single reply table.
project_github_replies <- parse_github_replies(
issues_json_folder_path = github_issue_path,
pull_requests_json_folder_path = github_pull_request_path,
comments_json_folder_path = github_reply_path,
commit_json_folder_path = github_commit_path,
pr_comments_json_folder_path = github_pr_comments_path
)
nrow(project_github_replies)
## [1] 1648
kable(head(project_github_replies[, .(reply_from, reply_subject)]))
| reply_from | reply_subject |
|---|---|
| BenjyNStrauss | 306 |
| BenjyNStrauss | 306 |
| BenjyNStrauss | 306 |
| BenjyNStrauss | 306 |
| BenjyNStrauss | 306 |
| CorneJB | 128 |
Detect communication communities using OSLOM. Each cluster groups authors who communicate densely with one another.
mail_clusters <- community_oslom(oslom_undir_path, author_network, seed = seed, n_runs = 1000)
kable(mail_clusters[["assignment"]])
| node_id | cluster_id |
|---|---|
| BenjyNStrauss | 1 |
| Carlos Paradis carlosviansi@gmail.com | 2 |
| Michelle4929 | 3 |
| codecov[bot] | 4 |
| cohenruport | 5 |
| cfuke1 | 6 |
| john-a-flinn | 7 |
| rnkazman | 8 |
| RavenMarQ 143663502+RavenMarQ@users.noreply.github.com | 9 |
| beydlern | 10 |
| Mark Burgess mkh.burg@gmail.com | 11 |
| daomcgill | 12 |
| Sean Sunoo 97641529+Ssunoo2@users.noreply.github.com | 13 |
| connorn-dev | 14 |
| geraldmjhuff | 15 |
| Anthony Lau 98019016+anthonyjlau@users.noreply.github.com | 16 |
| usradam | 17 |
| Ian Jaymes Iwata 97856957+ian-lastname@users.noreply.github.com | 18 |
| nicolehoess 86601771+nicolehoess@users.noreply.github.com | 19 |
| ryanseng03 | 20 |
| haotian1028 | 21 |
| lh-zhan | 22 |
| splimon | 23 |
| jseto808 | 24 |
| CorneJB | 25 |
| MahsaBazzaz | 26 |
| Ruben Jacobo 96926588+Rubegen@users.noreply.github.com | 27 |
| harrismumtaz | 28 |
| Leilani Reich 89750594+leilani-reich@users.noreply.github.com | 29 |
| malialiu 76604944+malialiu@users.noreply.github.com | 30 |
| massihonda | 31 |
| mumtaz-haris | 32 |
| Nico 63322470+nicoelee123@users.noreply.github.com | 33 |
| tuejari | 34 |
| valentina-lenarduzzi | 35 |
| Waylon Ho 84744515+waylonho@users.noreply.github.com | 36 |
kable(mail_clusters[["info"]])
| cluster_id | cluster_size | cluster_pvalue |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 1 | 1 |
| 3 | 1 | 1 |
| 4 | 1 | 1 |
| 5 | 1 | 1 |
| 6 | 1 | 1 |
| 7 | 1 | 1 |
| 8 | 1 | 1 |
| 9 | 1 | 1 |
| 10 | 1 | 1 |
| 11 | 1 | 1 |
| 12 | 1 | 1 |
| 13 | 1 | 1 |
| 14 | 1 | 1 |
| 15 | 1 | 1 |
| 16 | 1 | 1 |
| 17 | 1 | 1 |
| 18 | 1 | 1 |
| 19 | 1 | 1 |
| 20 | 1 | 1 |
| 21 | 1 | 1 |
| 22 | 1 | 1 |
| 23 | 1 | 1 |
| 24 | 1 | 1 |
| 25 | 1 | NA |
| 26 | 1 | NA |
| 27 | 1 | NA |
| 28 | 1 | NA |
| 29 | 1 | NA |
| 30 | 1 | NA |
| 31 | 1 | NA |
| 32 | 1 | NA |
| 33 | 1 | NA |
| 34 | 1 | NA |
| 35 | 1 | NA |
| 36 | 1 | NA |
Count the number of boundary spanners — authors who are the sole bridge between otherwise disconnected communities. An output of 0 means all communities are either well-connected or isolated with no single bottleneck author.
brokers <- smell_radio_silence(mail.graph = author_network, clusters = mail_clusters)
cat("Number of radio silence boundary spanners:", length(brokers), "\n")
## Number of radio silence boundary spanners: 36
if (length(brokers) > 0) {
kable(data.table(boundary_spanner = brokers))
}
| boundary_spanner |
|---|
| BenjyNStrauss |
| Carlos Paradis carlosviansi@gmail.com |
| Michelle4929 |
| codecov[bot] |
| cohenruport |
| cfuke1 |
| john-a-flinn |
| rnkazman |
| RavenMarQ 143663502+RavenMarQ@users.noreply.github.com |
| beydlern |
| Mark Burgess mkh.burg@gmail.com |
| daomcgill |
| Sean Sunoo 97641529+Ssunoo2@users.noreply.github.com |
| connorn-dev |
| geraldmjhuff |
| Anthony Lau 98019016+anthonyjlau@users.noreply.github.com |
| usradam |
| Ian Jaymes Iwata 97856957+ian-lastname@users.noreply.github.com |
| nicolehoess 86601771+nicolehoess@users.noreply.github.com |
| ryanseng03 |
| haotian1028 |
| lh-zhan |
| splimon |
| jseto808 |
| CorneJB |
| MahsaBazzaz |
| Ruben Jacobo 96926588+Rubegen@users.noreply.github.com |
| harrismumtaz |
| Leilani Reich 89750594+leilani-reich@users.noreply.github.com |
| malialiu 76604944+malialiu@users.noreply.github.com |
| massihonda |
| mumtaz-haris |
| Nico 63322470+nicoelee123@users.noreply.github.com |
| tuejari |
| valentina-lenarduzzi |
| Waylon Ho 84744515+waylonho@users.noreply.github.com |
This section repeats the pipeline above but replaces OSLOM community detection with DV8 DR Space hierarchical clustering. The author-author network is exported as a DSM, clustered by DR Space, and radio silence is computed per layer using module assignments.
dir.create(path.expand(project_path), showWarnings = FALSE, recursive = TRUE)
adsmj_path <- file.path(project_path, paste0(project_name, "-author-hdsm.json"))
graph_to_dsmj(author_network, dsmj_path = adsmj_path,
dsmj_name = paste0(project_name, "-author-hdsm"))
## [1] "../../analysis/junit/dv8//-author-hdsm.json"
adsmb_path <- file.path(project_path, paste0(project_name, "-author-hdsm.dv8-dsm"))
dv8_dsmj_to_dsmb(dv8_path = dv8_path, dsmj_path = adsmj_path, dsmb_path = adsmb_path)
## [1] "../../analysis/junit/dv8//-author-hdsm.dv8-dsm"
clsx_path <- file.path(project_path, paste0(project_name, "-author-clsx.dv8-clsx"))
dv8_mdsmb_to_hierclsxb(dv8_path = dv8_path, mdsmb_path = adsmb_path,
hierclsxb_path = clsx_path, recursive = TRUE)
## [1] "../../analysis/junit/dv8//-author-clsx.dv8-clsx"
clsxj_path <- file.path(project_path, paste0(project_name, "-author-clsx.json"))
dv8_clsxb_to_clsxj(dv8_path = dv8_path, clsxb_path = clsx_path, clsxj_path = clsxj_path)
## [1] "../../analysis/junit/dv8//-author-clsx.json"
cluster_table <- parse_dv8_clusters(clsxj_path)
kable(cluster_table[, .N, by = .(layer, module)][order(layer, module)])
| layer | module | N |
|---|---|---|
| Control | Isolated | 1 |
| Isolated | Isolated | 12 |
| L0 | M0 | 19 |
| L0 | M1 | 1 |
| L0 | M2 | 1 |
| L0 | M3 | 1 |
| L0 | M4 | 1 |
| L0/M0 | Control | 1 |
| L0/M0 | L0 | 18 |
| L0/M0/Control | Isolated | 1 |
| L0/M0/L0 | M0 | 13 |
| L0/M0/L0 | M1 | 5 |
| L0/M0/L0/M0 | Control | 2 |
| L0/M0/L0/M0 | L0 | 11 |
| L0/M0/L0/M0/Control | M1 | 1 |
| L0/M0/L0/M0/Control | M2 | 1 |
| L0/M0/L0/M0/Control/M1 | Isolated | 1 |
| L0/M0/L0/M0/Control/M2 | Isolated | 1 |
| L0/M0/L0/M0/L0 | M0 | 10 |
| L0/M0/L0/M0/L0 | M1 | 1 |
| L0/M0/L0/M0/L0/M0 | Control | 1 |
| L0/M0/L0/M0/L0/M0 | L0 | 9 |
| L0/M0/L0/M0/L0/M0/Control | Isolated | 1 |
| L0/M0/L0/M0/L0/M0/L0 | M0 | 4 |
| L0/M0/L0/M0/L0/M0/L0 | M1 | 5 |
| L0/M0/L0/M0/L0/M0/L0/M0 | Control | 1 |
| L0/M0/L0/M0/L0/M0/L0/M0 | L0 | 3 |
| L0/M0/L0/M0/L0/M0/L0/M0/Control | Isolated | 1 |
| L0/M0/L0/M0/L0/M0/L0/M0/L0 | M0 | 1 |
| L0/M0/L0/M0/L0/M0/L0/M0/L0 | M1 | 2 |
| L0/M0/L0/M0/L0/M0/L0/M0/L0/M0 | Isolated | 1 |
| L0/M0/L0/M0/L0/M0/L0/M0/L0/M1 | Isolated | 2 |
| L0/M0/L0/M0/L0/M0/L0/M1 | Control | 1 |
| L0/M0/L0/M0/L0/M0/L0/M1 | L0 | 4 |
| L0/M0/L0/M0/L0/M0/L0/M1/Control | Isolated | 1 |
| L0/M0/L0/M0/L0/M0/L0/M1/L0 | M0 | 1 |
| L0/M0/L0/M0/L0/M0/L0/M1/L0 | M1 | 3 |
| L0/M0/L0/M0/L0/M0/L0/M1/L0/M0 | Isolated | 1 |
| L0/M0/L0/M0/L0/M0/L0/M1/L0/M1 | Control | 1 |
| L0/M0/L0/M0/L0/M0/L0/M1/L0/M1 | L0 | 2 |
| L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/Control | Isolated | 1 |
| L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0 | M0 | 1 |
| L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0 | M1 | 1 |
| L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0/M0 | Isolated | 1 |
| L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0/M1 | Isolated | 1 |
| L0/M0/L0/M0/L0/M1 | Isolated | 1 |
| L0/M0/L0/M1 | Control | 1 |
| L0/M0/L0/M1 | L0 | 4 |
| L0/M0/L0/M1/Control | Isolated | 1 |
| L0/M0/L0/M1/L0 | M0 | 1 |
| L0/M0/L0/M1/L0 | M1 | 1 |
| L0/M0/L0/M1/L0 | M2 | 2 |
| L0/M0/L0/M1/L0/M0 | Isolated | 1 |
| L0/M0/L0/M1/L0/M1 | Isolated | 1 |
| L0/M0/L0/M1/L0/M2 | Isolated | 2 |
| L0/M1 | Isolated | 1 |
| L0/M2 | Isolated | 1 |
| L0/M3 | Isolated | 1 |
| L0/M4 | Isolated | 1 |
For each layer in the DR Space hierarchy, call radio silence using the module assignments as communities. Layers with fewer than two distinct non-Isolated modules are skipped.
all_brokers <- character(0)
for (lyr in unique(cluster_table$layer)) {
layer_dt <- cluster_table[layer == lyr]
active_modules <- setdiff(unique(layer_dt$module), "Isolated")
if (length(active_modules) < 2) next
cat("\n### Layer:", lyr, "\n")
module_levels <- unique(layer_dt$module)
module_to_id <- seq_along(module_levels)
names(module_to_id) <- module_levels
assignment <- data.table(node_id = layer_dt$file_path,
cluster_id = as.character(module_to_id[layer_dt$module]))
info_dt <- assignment[, .(cluster_size = .N), by = cluster_id]
layer_clusters <- list(assignment = assignment, info = info_dt)
layer_nodes <- layer_dt$file_path
layer_network <- author_network
layer_network[["nodes"]] <- author_network[["nodes"]][author_network[["nodes"]]$name %in% layer_nodes, ]
layer_network[["edgelist"]] <- author_network[["edgelist"]][author_network[["edgelist"]]$from %in% layer_nodes &
author_network[["edgelist"]]$to %in% layer_nodes, ]
brokers <- smell_radio_silence(mail.graph = layer_network, clusters = layer_clusters)
cat("Radio silence boundary spanners:", length(brokers), "\n")
if (length(brokers) > 0) {
print(kable(data.table(layer = lyr, boundary_spanner = brokers)))
all_brokers <- union(all_brokers, brokers)
}
}
##
## ### Layer: L0
## Radio silence boundary spanners: 4
##
##
## |layer |boundary_spanner |
## |:-----|:---------------------------------------------------------|
## |L0 |ryanseng03 |
## |L0 |BenjyNStrauss |
## |L0 |lh-zhan |
## |L0 |nicolehoess 86601771+nicolehoess@users.noreply.github.com |
##
## ### Layer: L0/M0
## Radio silence boundary spanners: 1
##
##
## |layer |boundary_spanner |
## |:-----|:----------------|
## |L0/M0 |codecov[bot] |
##
## ### Layer: L0/M0/L0
## Radio silence boundary spanners: 0
##
## ### Layer: L0/M0/L0/M0
## Radio silence boundary spanners: 0
##
## ### Layer: L0/M0/L0/M0/L0
## Radio silence boundary spanners: 1
##
##
## |layer |boundary_spanner |
## |:--------------|:---------------------------------------------------------------|
## |L0/M0/L0/M0/L0 |Ian Jaymes Iwata 97856957+ian-lastname@users.noreply.github.com |
##
## ### Layer: L0/M0/L0/M0/L0/M0
## Radio silence boundary spanners: 1
##
##
## |layer |boundary_spanner |
## |:-----------------|:----------------|
## |L0/M0/L0/M0/L0/M0 |geraldmjhuff |
##
## ### Layer: L0/M0/L0/M0/L0/M0/L0
## Radio silence boundary spanners: 0
##
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M0
## Radio silence boundary spanners: 1
##
##
## |layer |boundary_spanner |
## |:-----------------------|:----------------|
## |L0/M0/L0/M0/L0/M0/L0/M0 |usradam |
##
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M0/L0
## Radio silence boundary spanners: 1
##
##
## |layer |boundary_spanner |
## |:--------------------------|:----------------|
## |L0/M0/L0/M0/L0/M0/L0/M0/L0 |haotian1028 |
##
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M1
## Radio silence boundary spanners: 1
##
##
## |layer |boundary_spanner |
## |:-----------------------|:----------------|
## |L0/M0/L0/M0/L0/M0/L0/M1 |beydlern |
##
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M1/L0
## Radio silence boundary spanners: 1
##
##
## |layer |boundary_spanner |
## |:--------------------------|:----------------------------------------------------|
## |L0/M0/L0/M0/L0/M0/L0/M1/L0 |Sean Sunoo 97641529+Ssunoo2@users.noreply.github.com |
##
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M1/L0/M1
## Radio silence boundary spanners: 1
##
##
## |layer |boundary_spanner |
## |:-----------------------------|:-------------------------------|
## |L0/M0/L0/M0/L0/M0/L0/M1/L0/M1 |Mark Burgess mkh.burg@gmail.com |
##
## ### Layer: L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0
## Radio silence boundary spanners: 2
##
##
## |layer |boundary_spanner |
## |:--------------------------------|:---------------------------------------------------------|
## |L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0 |RavenMarQ 143663502+RavenMarQ@users.noreply.github.com |
## |L0/M0/L0/M0/L0/M0/L0/M1/L0/M1/L0 |Anthony Lau 98019016+anthonyjlau@users.noreply.github.com |
##
## ### Layer: L0/M0/L0/M0/Control
## Radio silence boundary spanners: 2
##
##
## |layer |boundary_spanner |
## |:-------------------|:----------------|
## |L0/M0/L0/M0/Control |daomcgill |
## |L0/M0/L0/M0/Control |connorn-dev |
##
## ### Layer: L0/M0/L0/M1
## Radio silence boundary spanners: 1
##
##
## |layer |boundary_spanner |
## |:-----------|:----------------|
## |L0/M0/L0/M1 |Michelle4929 |
##
## ### Layer: L0/M0/L0/M1/L0
## Radio silence boundary spanners: 2
##
##
## |layer |boundary_spanner |
## |:--------------|:----------------|
## |L0/M0/L0/M1/L0 |cohenruport |
## |L0/M0/L0/M1/L0 |cfuke1 |
Visualize the 1-hop neighborhood of all boundary spanners in the full author-author network. Each spanner node and its direct connections are shown.
htmltools::tagList(lapply(all_brokers, function(broker) {
broker_edges <- author_network[["edgelist"]][
author_network[["edgelist"]]$from == broker |
author_network[["edgelist"]]$to == broker, ]
broker_nodes <- unique(c(broker_edges$from, broker_edges$to))
broker_node_dt <- author_network[["nodes"]][author_network[["nodes"]]$name %in% broker_nodes, ]
igraph::graph_from_data_frame(
d = broker_edges,
directed = FALSE,
vertices = broker_node_dt
) |> visIgraph(randomSeed = seed)
}))