diff --git a/.Rbuildignore b/.Rbuildignore index 6a62c1e6..e07e9471 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -40,3 +40,4 @@ CRAN-SUBMISSION ^graphify-out$ ^raw$ ^revdep$ +^dev$ diff --git a/.gitignore b/.gitignore index 88ddc2a4..eeb38be5 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ revdep/library/ revdep/checks.noindex/ revdep/library.noindex/ revdep/data.sqlite +dev/ diff --git a/NEWS b/NEWS index ce1b018d..98cdaa43 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,10 @@ bibliometrix V.5.5.0.9000 (development version) Bug Fixes and Improvements +- networkPlot() is four to sixteen times faster on dense networks: two steps worked one edge at a time. Colouring the edges (the colour of the community inside a community, grey between communities) ran an apply() over the edge list that read the vertex attribute again for every edge, and the weights set by community.repulsion were computed in a loop that looked up both end points with which() over the vertex names. On the co-citation network of the 1,000 most cited references of management (120,836 edges) they took about three quarters of the 23 seconds the function needed. Both are now computed on the whole edge list at once. Measured on management and scientometrics over co-citation of references (1,000 nodes) and of authors (500), bibliographic coupling (400 documents), keyword co-occurrence (300) and collaboration (200), each with Louvain, Walktrap, Leiden and no clustering, with and without normalization and repulsion: 23-31 seconds become 5-6 for the co-citation of references, 13-16 become 1-2 for co-citation of authors and coupling, and about 1 second becomes 0.15 for keywords. The graph with every vertex and edge attribute, the layout, the cluster table, the modularity and the Pajek graph compare identical() before and after in all twenty cases. What remains is igraph's own work: the layout, the betweenness centrality and the ten runs of Louvain or Leiden. Every network in Biblioshiny is drawn through networkPlot(), and so are couplingMap(), thematicMap() and thematicEvolution(). +- Biblioshiny: the Co-citation network can be normalized: the Method Parameters of the Co-citation network gain the Normalization Method selector that the Co-occurrence and Collaboration networks already have (None, Association, Jaccard, Salton, Inclusion, Equivalence; default Association, as there). The option had been left out because of the cost of normalizeSimilarity(), which is now computed on the non-zero cells only. As in the other networks, the normalized similarity drives the layout of the nodes; clusters and edge widths are still computed on the co-citation counts. On the co-citation networks of 1,000 references, cited authors and cited sources of management, normalizing adds one to three seconds. +- cocMatrix() keeps the short OpenAlex work ids in CR: in an OpenAlex collection the cited references are work ids (W followed by digits), and cocMatrix(Field = "CR") drops every reference of 10 characters or fewer as not congruent, a filter written for reference strings. Ids of older works are 7 to 10 characters long, so they were discarded: on a collection of 585 documents downloaded through the OpenAlex API, 911 of the 53,078 references (1.7%), i.e. 784 distinct cited works, disappeared from every analysis built on CR (bibliographic coupling, co-citation). The filter now spares a reference that is an OpenAlex work id. Collections from other databases are unaffected: the matrices over management, scientometrics and two Scopus collections (old and new reference format), with and without short = TRUE, compare identical() before and after. +- normalizeSimilarity() no longer builds dense matrices: every index divides the co-occurrence c_ij by a function of c_ii and c_jj, but it was computed with outer() on the diagonal, which allocates dense n x n matrices, and the inclusion index called min() once per cell through mapply(). On the co-citation network of the 9,999 most cited references of management the association strength took 2.9 seconds and a peak of 4.85 GB; the inclusion index took 3.6 seconds on 1,918 keywords. The indices are now computed on the non-zero cells of the sparse matrix only: 0.03 seconds and a peak of 335 MB (the collection included) on the same network. Values and returned class are unchanged: for all five indices, over coupling, co-citation, co-occurrence and collaboration networks from WoS, Scopus and OpenAlex collections, a base matrix input and a matrix with an item that never occurs, the result compares identical() with the previous one. An unknown type, which failed with "object 'S' not found", is now refused by name, and so is a non-square matrix. - No more igraph deprecation warnings: the network functions still called igraph functions deprecated since igraph 2.0/2.1 ("was deprecated in igraph 2.0.0", seen in thematicEvolution() through networkPlot()). They are replaced by their current names with identical results: layout.auto/circle/star/sphere/mds/kamada.kawai/fruchterman.reingold() by layout_nicely(), layout_in_circle(), layout_as_star(), layout_on_sphere(), layout_with_mds(), layout_with_kk(), layout_with_fr(); page.rank() by page_rank(); get.edgelist() by as_edgelist(); decompose.graph() by decompose(); delete.vertices() by delete_vertices(); write.graph() by write_graph(); hub_score()/authority_score() by hits_scores(); cluster_leiden(resolution_parameter = ) by resolution = ; eigen_centrality() no longer passes the deprecated scale and options = arpack_defaults. Each replacement was checked identical() against the old call with the same seed; unused deprecated imports are dropped and igraph (>= 2.1.0) is now required. Biblioshiny still passes layout = "layout.norm" to visNetwork::visIgraphLayout(), which calls that igraph function itself: a soft deprecation inside visNetwork, never shown to users. - applyReferenceMatching() joins each citation string once: normalize_citations() returns one row per occurrence of a reference, and joining it back on the citation string multiplied a reference cited k times into k x k rows, with dplyr's "Detected an unexpected many-to-many relationship" warning. The duplicates were collapsed afterwards, so the results were right, but on 2000 Web of Science records the intermediate table held about 2.55 million rows instead of 130,264, and a single reference cited 1160 times accounted for about 1.35 million of them. Occurrences of the same string share their match (only the ids of the filtered references differ, and those rows are dropped right after), so the matches are now reduced to one row per string before the join. On the Scopus fixture, a 530-document Scopus collection and the 2000-document WoS collection, all four elements of the result compare identical() before and after. - dominance() retains productive co-authors with zero first-authorships and bounds k (#694): in dominance(), the author pool was initialized solely from names(table(results$FirstAuthors[results$nAUperPaper > 1])), omitting prolific co-authors who have multi-authored publications but zero first-authored papers (FAA = 0). Under Kumar & Kumar (2008), their Dominance Factor is strictly DF = FAA / MAA = 0 / MAA = 0.0. Slicing with D[1:k, ] without clamping k to nrow(D) also produced spurious NA rows with false ranks when k exceeded the available author count. dominance() now evaluates all authors with multi-authored papers (MAA > 0), assigns DF = 0.0 when FAA = 0, clamps k to the number of eligible rows, and returns an empty data frame when no multi-authored paper exists. diff --git a/NEWS.md b/NEWS.md index ddb42d99..b8f02946 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,10 @@ # bibliometrix (development version) ## Bug Fixes and Improvements +- **`networkPlot()` is four to sixteen times faster on dense networks**: two steps worked one edge at a time. Colouring the edges (the colour of the community inside a community, grey between communities) ran an `apply()` over the edge list that read the vertex attribute again for every edge, and the weights set by `community.repulsion` were computed in a loop that looked up both end points with `which()` over the vertex names. On the co-citation network of the 1,000 most cited references of `management` (120,836 edges) they took about three quarters of the 23 seconds the function needed. Both are now computed on the whole edge list at once. Measured on `management` and `scientometrics` over co-citation of references (1,000 nodes) and of authors (500), bibliographic coupling (400 documents), keyword co-occurrence (300) and collaboration (200), each with Louvain, Walktrap, Leiden and no clustering, with and without normalization and repulsion: 23-31 seconds become 5-6 for the co-citation of references, 13-16 become 1-2 for co-citation of authors and coupling, and about 1 second becomes 0.15 for keywords. The graph with every vertex and edge attribute, the layout, the cluster table, the modularity and the Pajek graph compare `identical()` before and after in all twenty cases. What remains is igraph's own work: the layout, the betweenness centrality and the ten runs of Louvain or Leiden. Every network in Biblioshiny is drawn through `networkPlot()`, and so are `couplingMap()`, `thematicMap()` and `thematicEvolution()`. +- **Biblioshiny: the Co-citation network can be normalized**: the Method Parameters of the Co-citation network gain the *Normalization Method* selector that the Co-occurrence and Collaboration networks already have (None, Association, Jaccard, Salton, Inclusion, Equivalence; default Association, as there). The option had been left out because of the cost of `normalizeSimilarity()`, which is now computed on the non-zero cells only. As in the other networks, the normalized similarity drives the layout of the nodes; clusters and edge widths are still computed on the co-citation counts. On the co-citation networks of 1,000 references, cited authors and cited sources of `management`, normalizing adds one to three seconds. +- **`cocMatrix()` keeps the short OpenAlex work ids in `CR`**: in an OpenAlex collection the cited references are work ids (`W` followed by digits), and `cocMatrix(Field = "CR")` drops every reference of 10 characters or fewer as not congruent, a filter written for reference strings. Ids of older works are 7 to 10 characters long, so they were discarded: on a collection of 585 documents downloaded through the OpenAlex API, 911 of the 53,078 references (1.7%), i.e. 784 distinct cited works, disappeared from every analysis built on `CR` (bibliographic coupling, co-citation). The filter now spares a reference that is an OpenAlex work id. Collections from other databases are unaffected: the matrices over `management`, `scientometrics` and two Scopus collections (old and new reference format), with and without `short = TRUE`, compare `identical()` before and after. +- **`normalizeSimilarity()` no longer builds dense matrices**: every index divides the co-occurrence `c_ij` by a function of `c_ii` and `c_jj`, but it was computed with `outer()` on the diagonal, which allocates dense *n* × *n* matrices, and the `inclusion` index called `min()` once per cell through `mapply()`. On the co-citation network of the 9,999 most cited references of `management` the association strength took 2.9 seconds and a peak of 4.85 GB; the inclusion index took 3.6 seconds on 1,918 keywords. The indices are now computed on the non-zero cells of the sparse matrix only: 0.03 seconds and a peak of 335 MB (the collection included) on the same network. Values and returned class are unchanged: for all five indices, over coupling, co-citation, co-occurrence and collaboration networks from WoS, Scopus and OpenAlex collections, a base `matrix` input and a matrix with an item that never occurs, the result compares `identical()` with the previous one. An unknown `type`, which failed with *"object 'S' not found"*, is now refused by name, and so is a non-square matrix. - **No more igraph deprecation warnings**: the network functions still called igraph functions deprecated since igraph 2.0/2.1, which R reports as *"was deprecated in igraph 2.0.0"* warnings (seen in `thematicEvolution()` through `networkPlot()`). They are replaced by their current names with identical results: `layout.auto/circle/star/sphere/mds/kamada.kawai/fruchterman.reingold()` by `layout_nicely()`, `layout_in_circle()`, `layout_as_star()`, `layout_on_sphere()`, `layout_with_mds()`, `layout_with_kk()`, `layout_with_fr()`; `page.rank()` by `page_rank()`; `get.edgelist()` by `as_edgelist()`; `decompose.graph()` by `decompose()`; `delete.vertices()` by `delete_vertices()`; `write.graph()` by `write_graph()`; `hub_score()`/`authority_score()` by `hits_scores()`; `cluster_leiden(resolution_parameter = )` by `resolution = `; and `eigen_centrality()` no longer passes the deprecated `scale` and `options = arpack_defaults`. Each replacement was checked `identical()` against the old call with the same seed; unused deprecated imports are dropped and `igraph (>= 2.1.0)` is now required. Biblioshiny still passes `layout = "layout.norm"` to `visNetwork::visIgraphLayout()`, which calls that igraph function itself: a soft deprecation inside visNetwork, never shown to users. - **`applyReferenceMatching()` joins each citation string once**: `normalize_citations()` returns one row per occurrence of a reference, and joining it back on the citation string multiplied a reference cited *k* times into *k* × *k* rows, with dplyr's *"Detected an unexpected many-to-many relationship"* warning. The duplicates were collapsed afterwards, so the results were right, but on 2000 Web of Science records the intermediate table held about 2.55 million rows instead of 130,264, and a single reference cited 1160 times accounted for about 1.35 million of them. Occurrences of the same string share their match (only the ids of the filtered references differ, and those rows are dropped right after), so the matches are now reduced to one row per string before the join. On the Scopus fixture, a 530-document Scopus collection and the 2000-document WoS collection, all four elements of the result compare `identical()` before and after. - **Local citation analyses of a Scopus collection work after reference matching** (#696): applying `applyReferenceMatching()` in Biblioshiny rebuilds `CR` with a join on the normalized references, so a document none of whose references survives the matching gets `CR = NA`. `CR_SO()` passes every reference to `convert_scopus_new_to_classic()`, where `str_detect(NA, ...)` is `NA` and the `if` stopped *Most Local Cited Sources* and the *Co-citation* network with *"missing value where TRUE/FALSE needed"*. Converting the references a second time was not the cause: a reference already in the classic layout is returned unchanged. `convert_scopus_new_to_classic()` now returns a missing or empty reference as it is, so such a document gets `CR_SO = NA`, and a Scopus collection that carries a missing `CR` for any other reason is covered too. Documents with references are unaffected. Reported by @BehradSa1999; the cause was traced by @GabrielBaiano. diff --git a/R/cocMatrix.R b/R/cocMatrix.R index d35033e0..f4614ba3 100644 --- a/R/cocMatrix.R +++ b/R/cocMatrix.R @@ -132,8 +132,10 @@ cocMatrix <- function(M, Field = "AU", type = "sparse", n = NULL, sep = ";", bin } Fi <- lapply(Fi, trim.leading) if (Field == "CR") { - Fi <- lapply(Fi, function(l) l <- l[nchar(l) > 10]) - } ## delete not congruent references + # delete not congruent references, but keep OpenAlex work ids (W + digits), + # which can be as short as 7 characters + Fi <- lapply(Fi, function(l) l <- l[nchar(l) > 10 | grepl("^W[0-9]+$", l)]) + } ## Scelta dell'informazione contenuta in CR da utilizzare (Reference, Autore, Affiliation, ecc.) diff --git a/R/networkPlot.R b/R/networkPlot.R index e218a9ba..3436855b 100644 --- a/R/networkPlot.R +++ b/R/networkPlot.R @@ -552,18 +552,15 @@ clusteringNetwork <- function(bsk.network, cluster, seed = NULL, n_runs = 10) { El <- as.data.frame(as_edgelist(bsk.network, names = F)) colorlist <- colorlist() - # apply() over a zero-row edge list still calls the function once, on an - # empty row, to learn the type of its result -- and the comparison inside - # stops with "argument is of length zero". A graph without edges has no edge - # to colour. - if (nrow(El) > 0) E(bsk.network)$color <- apply(El, 1, function(x) { - if (V(bsk.network)$community[x[1]] == V(bsk.network)$community[x[2]]) { - C <- colorlist[V(bsk.network)$community[x[1]]] - } else { - C <- "gray70" - } - return(C) - }) + # An edge inside a community takes the colour of the community, an edge + # between communities is grey. Computed on the whole edge list at once: an + # apply() over the edges read the vertex attribute again for every edge. A + # graph without edges has no edge to colour. + if (nrow(El) > 0) { + community <- V(bsk.network)$community + c_from <- community[El[, 1]] + E(bsk.network)$color <- ifelse(c_from == community[El[, 2]], colorlist[c_from], "gray70") + } E(bsk.network)$lty <- 1 E(bsk.network)$lty[E(bsk.network)$color == "gray70"] <- 5 @@ -606,28 +603,15 @@ switchLayout <- function(bsk.network, type, community.repulsion) { original_weights <- E(bsk.network)$weight } - # Apply new weighting scheme with gradual growth - new_weights <- numeric(nrow(row)) - - for (i in 1:nrow(row)) { - node1 <- row[i, 1] - node2 <- row[i, 2] - - comm1 <- membership[which(names(membership) == node1)] - comm2 <- membership[which(names(membership) == node2)] - - if (comm1 == comm2) { - # INTRA-COMMUNITY Edge - # Moderate increase with sub-linear growth - multiplier <- 1 + (repulsion_strength^0.7) * 1.5 - new_weights[i] <- original_weights[i] * multiplier - } else { - # INTER-COMMUNITY Edge - # Gradual reduction with attenuated exponential function - divisor <- 1 + exp(repulsion_strength * 1.2) - 1 - new_weights[i] <- original_weights[i] / divisor - } - } + # Apply new weighting scheme with gradual growth: intra-community edges are + # strengthened, inter-community edges weakened + comm1 <- unname(membership)[match(row[, 1], names(membership))] + comm2 <- unname(membership)[match(row[, 2], names(membership))] + # INTRA-COMMUNITY Edge: moderate increase with sub-linear growth + multiplier <- 1 + (repulsion_strength^0.7) * 1.5 + # INTER-COMMUNITY Edge: gradual reduction with attenuated exponential function + divisor <- 1 + exp(repulsion_strength * 1.2) - 1 + new_weights <- ifelse(comm1 == comm2, original_weights * multiplier, original_weights / divisor) # Apply new weights E(bsk.network)$weight <- new_weights diff --git a/R/normalizeSimilarity.R b/R/normalizeSimilarity.R index b5ad242d..3f7d958a 100644 --- a/R/normalizeSimilarity.R +++ b/R/normalizeSimilarity.R @@ -52,33 +52,40 @@ #' @export normalizeSimilarity <- function(NetMatrix, type = "association") { - diag <- Matrix::diag - D <- diag(NetMatrix) - # S=NetMatrix - switch(type, - association = { - S <- NetMatrix / ((outer(D, D, "*"))) - }, - inclusion = { - S <- NetMatrix / outer(D, D, function(a, b) { - mapply(min, a, b) - }) - }, - jaccard = { - S <- NetMatrix / (outer(D, D, "+") - NetMatrix) - }, - salton = { - S <- NetMatrix / (sqrt(outer(D, D, "*"))) - }, - equivalence = { - S <- (NetMatrix / sqrt(outer(D, D, "*")))^2 - } + types <- c("association", "inclusion", "jaccard", "salton", "equivalence") + if (!(type %in% types)) { + stop("normalizeSimilarity(): type must be one of ", paste0('"', types, '"', collapse = ", "), call. = FALSE) + } + if (nrow(NetMatrix) != ncol(NetMatrix)) { + stop("normalizeSimilarity(): NetMatrix must be a square co-occurrence matrix", call. = FALSE) + } + + # Each index divides c_ij by a function of the diagonal counts c_ii and c_jj, + # so it is computed only on the non-zero entries (i, j, x) of the sparse + # matrix: a zero co-occurrence stays zero, and no dense n x n matrix is built. + S <- methods::as(methods::as(methods::as(NetMatrix, "CsparseMatrix"), "generalMatrix"), "dMatrix") + D <- Matrix::diag(S) + i <- S@i + 1L + j <- rep.int(seq_len(ncol(S)), diff(S@p)) + x <- S@x + + S@x <- switch(type, + association = x / (D[i] * D[j]), + inclusion = x / pmin(D[i], D[j]), + jaccard = x / (D[i] + D[j] - x), + salton = x / sqrt(D[i] * D[j]), + equivalence = (x / sqrt(D[i] * D[j]))^2 ) + # 0/0 (an item that never occurs) is set to zero, as before + S@x[is.nan(S@x)] <- 0 + S <- Matrix::drop0(S) - S <- as.matrix(S) - S[is.nan(S)] <- 0 - S <- Matrix(S, sparse = TRUE) - # if (class(S)!="dgCMatrix"){S=as.matrix(S)} + # Return the storage class Matrix() chose for the dense result (dsCMatrix when + # symmetric). Every index is symmetric in i and j, so symmetry is tested + # exactly: the tolerant test of Matrix compares n^2-long sparse vectors and + # misses it on large matrices. + if (Matrix::isSymmetric(S, tol = 0)) S <- Matrix::forceSymmetric(S, uplo = "U") + S <- Matrix(S, sparse = TRUE, forceCheck = TRUE) return(S) } diff --git a/inst/biblioshiny/server.R b/inst/biblioshiny/server.R index aaee93fb..b99308ac 100644 --- a/inst/biblioshiny/server.R +++ b/inst/biblioshiny/server.R @@ -13228,6 +13228,7 @@ To ensure the functionality of Biblioshiny, cit_edgesize <- input$citedgesize cit_cluster <- input$cocitCluster cit_repulsion <- input$cocit.repulsion / 2 + cit_normalize <- if (input$citnormalize == "none") NULL else input$citnormalize shortlabel <- (cit_shortlabel == "Yes") if (label.n > n) { @@ -13309,7 +13310,7 @@ To ensure the functionality of Biblioshiny, withProgress(message = 'Co-citation Network: computing...', value = 0, { bibliometrix::networkPlot( NetRefs, - normalize = NULL, + normalize = cit_normalize, Title = net_title, type = cit_layout, size.cex = TRUE, diff --git a/inst/biblioshiny/ui.R b/inst/biblioshiny/ui.R index 5ac5b746..b0c10f8d 100644 --- a/inst/biblioshiny/ui.R +++ b/inst/biblioshiny/ui.R @@ -9765,6 +9765,24 @@ body <- dashboardBody( ) ) ), + fluidRow( + column( + 12, + selectInput( + "citnormalize", + label = strong("Normalization Method"), + choices = c( + "None" = "none", + "Association" = "association", + "Jaccard" = "jaccard", + "Salton" = "salton", + "Inclusion" = "inclusion", + "Equivalence" = "equivalence" + ), + selected = "association" + ) + ) + ), hr(style = "margin: 15px 0; border-top: 1px solid #ddd;"), div( style = "background-color: #f0f4f8; padding: 12px; border-radius: 5px; margin-bottom: 10px;", diff --git a/inst/biblioshiny/utils.R b/inst/biblioshiny/utils.R index cf028842..db6cd5e8 100644 --- a/inst/biblioshiny/utils.R +++ b/inst/biblioshiny/utils.R @@ -4318,7 +4318,7 @@ intellectualStructure <- function(input, values) { values$cocitnet <- networkPlot( values$NetRefs, - normalize = NULL, + normalize = if (input$citnormalize == "none") NULL else input$citnormalize, Title = values$Title, type = input$citlayout, size.cex = TRUE, diff --git a/tests/testthat/test-network.R b/tests/testthat/test-network.R index 4f56fa19..df115acf 100644 --- a/tests/testthat/test-network.R +++ b/tests/testthat/test-network.R @@ -58,6 +58,34 @@ test_that("cocMatrix mantiene due dimensioni con un solo termine distinto", { expect_equal(dim(cocMatrix(mk(c("X", "Y", "X")), Field = "DE", type = "matrix", sep = ";")), c(3L, 2L)) }) +# Con OpenAlex CR contiene gli id dei work citati (W + cifre). Il filtro che +# scarta le stringhe di riferimento con 10 caratteri o meno eliminava gli id +# piu' corti (7-10 caratteri): l'1,7% dei riferimenti di una raccolta reale, +# in tutte le analisi basate su CR. + +test_that("cocMatrix mantiene gli id OpenAlex corti in CR", { + M <- data.frame( + SR = c("D1", "D2", "D3"), DB = "OPENALEX", + CR = c("W1234567; W2741809807; W99887766", "W1234567; W99887766", "W2741809807"), + stringsAsFactors = FALSE + ) + row.names(M) <- M$SR + WR <- cocMatrix(M, Field = "CR", sep = ";") + expect_setequal(colnames(WR), c("W1234567", "W2741809807", "W99887766")) + expect_equal(as.numeric(Matrix::rowSums(WR)), c(3, 2, 1)) +}) + +test_that("cocMatrix scarta ancora le stringhe CR troppo corte che non sono id", { + M <- data.frame( + SR = c("D1", "D2"), DB = "ISI", + CR = c("ANONYMOUS;SMITH J, 2001, J INFORMETR, V1, P1", "SMITH J, 2001, J INFORMETR, V1, P1;NO TITLE"), + stringsAsFactors = FALSE + ) + row.names(M) <- M$SR + WR <- cocMatrix(M, Field = "CR", sep = ";") + expect_equal(ncol(WR), 1L) +}) + test_that("networkStat calcola statistiche di rete", { M <- load_wos_fixture() NetMatrix <- biblioNetwork(M, analysis = "co-citation", network = "references", sep = ";") @@ -73,6 +101,54 @@ test_that("normalizeSimilarity calcola indici di similarita", { expect_true(inherits(S, "Matrix") || is.matrix(S)) }) +# normalizeSimilarity() costruiva matrici dense n x n con outer(): su una +# co-citazione di 10.000 riferimenti erano quasi 5 GB. Ora lavora solo sulle +# celle non nulle; i valori e la classe restituita devono restare quelli di +# prima, cioe' della formula applicata alla matrice densa. + +test_that("normalizeSimilarity coincide con la formula densa per ogni indice", { + C <- Matrix::Matrix(c( + 4, 2, 0, 1, 0, + 2, 3, 1, 0, 0, + 0, 1, 2, 0, 0, + 1, 0, 0, 1, 0, + 0, 0, 0, 0, 0 + ), 5, sparse = TRUE) + dimnames(C) <- list(letters[1:5], letters[1:5]) + A <- as.matrix(C) + D <- diag(A) + ref <- list( + association = A / outer(D, D), + inclusion = A / outer(D, D, pmin), + jaccard = A / (outer(D, D, "+") - A), + salton = A / sqrt(outer(D, D)), + equivalence = (A / sqrt(outer(D, D)))^2 + ) + for (ty in names(ref)) { + r <- ref[[ty]] + r[is.nan(r)] <- 0 + S <- normalizeSimilarity(C, type = ty) + expect_s4_class(S, "dsCMatrix") + expect_equal(as.matrix(S), r, info = ty) + # la stessa matrice passata come matrix di base + expect_equal(as.matrix(normalizeSimilarity(A, type = ty)), r, info = ty) + } +}) + +test_that("normalizeSimilarity resta sparsa e simmetrica su una rete reale", { + M <- load_wos_fixture() + NetMatrix <- biblioNetwork(M, analysis = "co-citation", network = "references", sep = ";") + S <- normalizeSimilarity(NetMatrix, type = "association") + expect_s4_class(S, "dsCMatrix") + expect_equal(Matrix::nnzero(S), Matrix::nnzero(NetMatrix)) + expect_equal(dimnames(S), dimnames(NetMatrix)) +}) + +test_that("normalizeSimilarity rifiuta un tipo sconosciuto", { + C <- Matrix::Matrix(diag(2), sparse = TRUE) + expect_error(normalizeSimilarity(C, type = "cosine"), "type must be one of") +}) + test_that("networkPlot genera output senza errori", { skip_on_cran() M <- load_wos_fixture() @@ -121,3 +197,34 @@ test_that("clusteringNetwork con seed resta invariato su un grafo con archi", { expect_equal(max(a$net_groups$membership), 2) expect_equal(igraph::modularity(g, a$net_groups$membership), 0.3571429, tolerance = 1e-6) }) + +# La colorazione degli archi e i pesi di community.repulsion erano calcolati +# arco per arco (apply() e un ciclo con which() sui nomi): su una rete di +# 1000 riferimenti co-citati, 120.000 archi, erano i tre quarti del tempo di +# networkPlot(). Ora sono vettoriali; il risultato deve restare lo stesso. + +test_that("clusteringNetwork colora gli archi interni e grigi quelli tra comunita'", { + g <- igraph::make_graph(c(1, 2, 2, 3, 3, 1, 4, 5, 5, 6, 6, 4, 3, 4), directed = FALSE) + igraph::V(g)$name <- letters[1:6] + res <- suppressWarnings(clusteringNetwork(g, "louvain", seed = 7)) + comm <- igraph::V(res$bsk.network)$community + el <- igraph::as_edgelist(res$bsk.network, names = FALSE) + inside <- comm[el[, 1]] == comm[el[, 2]] + expect_equal(igraph::E(res$bsk.network)$color[inside], colorlist()[comm[el[inside, 1]]]) + expect_equal(igraph::E(res$bsk.network)$color[!inside], "gray70") + expect_equal(igraph::E(res$bsk.network)$lty, ifelse(inside, 1, 5)) +}) + +test_that("switchLayout rafforza gli archi interni e indebolisce quelli tra comunita'", { + g <- igraph::make_graph(c(1, 2, 2, 3, 3, 1, 4, 5, 5, 6, 6, 4, 3, 4), directed = FALSE) + igraph::V(g)$name <- letters[1:6] + igraph::V(g)$community <- c(1, 1, 1, 2, 2, 2) + igraph::E(g)$weight <- 2 + w <- igraph::E(switchLayout(g, "circle", 0.5)$bsk.network)$weight + inside <- c(rep(TRUE, 6), FALSE) + expect_true(all(w[inside] > 2)) + expect_true(all(w[!inside] < 2)) + expect_equal(length(unique(w[inside])), 1L) + # senza repulsione i pesi restano quelli di partenza + expect_equal(igraph::E(switchLayout(g, "circle", 0)$bsk.network)$weight, rep(2, 7)) +})