# tocID <- "FND-MAT-Graphs_and_networks.R" # # Purpose: A Bioinformatics Course: # R code accompanying the FND-MAT-Graphs_and_networks unit. # # Version: 1.2 # # Date: 2017 10 - 2019 01 # Author: Boris Steipe (boris.steipe@utoronto.ca) # # Versions: # 1.2 Change from require() to requireNamespace(), # use ::() idiom throughout # 1.1 Update set.seed() usage # 1.0 First final version for learning units. # 0.1 First code copied from 2016 material. # # # TODO: Add some explicit tasks and sample solutions. # # # == DO NOT SIMPLY source() THIS FILE! ======================================= # # If there are portions you don't understand, use R's help system, Google for an # answer, or ask your instructor. Don't continue if you don't understand what's # going on. That's not how it works ... # # ============================================================================== #TOC> ========================================================================== #TOC> #TOC> Section Title Line #TOC> -------------------------------------------------------------- #TOC> 1 REVIEW 50 #TOC> 2 DEGREE DISTRIBUTIONS 206 #TOC> 2.1 Random graph 212 #TOC> 2.2 scale-free graph (Barabasi-Albert) 260 #TOC> 2.3 Random geometric graph 326 #TOC> 3 A CLOSER LOOK AT THE igraph:: PACKAGE 448 #TOC> 3.1 Basics 451 #TOC> 3.2 Components 523 #TOC> 4 RANDOM GRAPHS AND GRAPH METRICS 542 #TOC> 4.1 Diameter 579 #TOC> 5 GRAPH CLUSTERING 648 #TOC> #TOC> ========================================================================== # = 1 REVIEW ============================================================== # This tutorial covers basic concepts of graph theory and analysis in R. Make # sure you have pulled the latest version of the project from the GitHub # repository. # Let's explore some of the basic ideas of graph theory by starting with a small # random graph. # To begin let's write a little function that will create random "gene" names; # there's no particular purpose to this other than to make our graphs look a # little more "biological" ... makeRandomGenenames <- function(N) { nam <- character() while (length(nam) < N) { a <- paste0(c(sample(LETTERS, 1), sample(letters, 2)), collapse="") # one uppercase, two lowercase letters n <- sample(1:9, 1) # one number nam[length(nam) + 1] <- paste(a, n, sep="") # store in vector nam <- unique(nam) # delete if this was a duplicate } return(nam) } N <- 20 set.seed(112358) # set RNG seed for repeatable randomness ( Nnames <- makeRandomGenenames(N) ) set.seed(NULL) # reset the RNG # One way to represent graphs in a computer is as an "adjacency matrix". In this # matrix, each row and each column represents a node, and the cell at the # intersection of a row and column contains a value/TRUE if there is an edge, # 0/FALSE otherwise. # Let's create an adjacency matrix for our random graph: let's say a pair # of nodes has probability p <- 0.09 to have an edge, and our graph is # symmetric , i.e. it is an undirected graph, and it has neither self-edges, # i.e. loops, nor multiple edges between the same nodes, i.e. it # is a "simple" graph. We use the Nnames vector as node labels. makeRandomAM <- function(nam, p = 0.1) { # Make a random adjacency matrix for a set of nodes with edge probability p # Parameters: # nam: a character vector of unique node names. # p: probability that a random pair of nodes will have an edge. # # Value: an adjacency matrix for a simple, undirected graph # N <- length(nam) AM <- matrix(numeric(N * N), ncol = N) # Adjacency matrix, pre-filled with 0 rownames(AM) <- nam colnames(AM) <- nam for (iRow in 1:(N-1)) { # Note how we make sure iRow != iCol - this prevents # loops (self-edges) for (iCol in (iRow+1):N) { if (runif(1) < p) { # runif() creates uniform random numbers # between 0 and 1. The expression is TRUE with # probability p. if it is TRUE ... AM[iRow, iCol] <- 1 # ... record an edge for the pair (iRow, iCol) } } } return(AM) } set.seed(112358) # set RNG seed for repeatable randomness ( myRandAM <- makeRandomAM(Nnames, p = 0.09) ) set.seed(NULL) # reset the RNG # Listing the matrix is not very informative - we should plot this graph. The # standard package for work with graphs in r is "igraph::". We'll go into more # details of the igraph:: package a bit later, for now we just use it to plot: if (! requireNamespace("igraph", quietly = TRUE)) { install.packages("igraph") } # Package information: # library(help = igraph) # basic information # browseVignettes("igraph") # available vignettes # data(package = "igraph") # available datasets myG <- igraph::graph_from_adjacency_matrix(myRandAM, mode = "undirected") set.seed(112358) # set RNG seed for repeatable randomness # calculate layout coordinates myGxy <- igraph::layout_with_graphopt(myG, charge=0.0012) set.seed(NULL) # reset the RNG # The igraph:: package adds its own function to the collection of plot() # functions; R makes the selection which plot function to use based on the class # of the object that we request to plot. This plot function has parameters # layout - the x,y coordinates of the nodes; # vertex.color - which I define to color by node-degree # vertex size - which I define to increase with node-degree # vertex.label - which I set to combine the names of the vertices of the # graph - names(V(iG)) - with the node degree - degree(iG). # See ?igraph.plotting for the complete list of parameters oPar <- par(mar= c(0, 0, 0, 0)) # Turn margins off plot(myG, layout = myGxy, rescale = FALSE, xlim = c(min(myGxy[,1]) * 0.99, max(myGxy[,1]) * 1.01), ylim = c(min(myGxy[,2]) * 0.99, max(myGxy[,2]) * 1.01), vertex.color=heat.colors(max(igraph::degree(myG)+1))[igraph::degree(myG)+1], vertex.size = 1000 + (900 * igraph::degree(myG)), vertex.label = sprintf("%s(%i)", names(igraph::V(myG)), igraph::degree(myG)), vertex.label.family = "sans", vertex.label.cex = 0.7) par(oPar) # reset plot window # The simplest descriptor of a graph are the number of nodes, edges, and the # degree-distribution. In our example, the number of nodes was given: N; the # number of edges can easily be calculated from the adjacency matrix. In our # matrix, we have entered 1 for every edge. Thus we simply sum over the matrix: sum(myRandAM) # Is that what you expect? # What about the degree distribution? We can get that simply by summing over the # rows and summing over the columns and adding the two vectors. rowSums(myRandAM) + colSums(myRandAM) # check this against the plot! # The function degree() gives the same values igraph::degree(myG) # Let's plot the degree distribution in a histogram: degG <- igraph::degree(myG) brk <- seq(min(degG)-0.5, max(degG)+0.5, by=1) # define histogram breaks hist(degG, breaks=brk, col="#A5CCF5", xlim = c(-1,8), xaxt = "n", main = "Node degrees", xlab = "Degree", ylab = "Number") # plot histogram axis(side = 1, at = 0:7) # Note: I don't _have_ to define breaks, the hist() function usually does so # quite well, automatically. But for this purpose I want the columns of the # histogram to represent exactly one node-degree difference. # A degree distribution is actually a very informative descriptor of graphs, # since it is very sensitive to the generating mechanism. For biological # networks, that is one of the key questions we are interested in: how was the # network formed? Asking about the mechanism, the way the interactions # support function and shape the fitness landscape in which the cell evolves, # connects our mathematical abstraction with biological insight. # = 2 DEGREE DISTRIBUTIONS ================================================ # Let's simulate a few graphs that are a bit bigger to get a better sense of # their degree distributions: # # == 2.1 Random graph ====================================================== N <- 200 set.seed(31415927) # set RNG seed for repeatable randomness my200AM <- makeRandomAM(as.character(1:N), p = 0.015) set.seed(NULL) # reset the RNG myG200 <- igraph::graph_from_adjacency_matrix(my200AM, mode = "undirected") myGxy <- igraph::layout_with_graphopt(myG200, charge=0.0001) # calculate layout # coordinates oPar <- par(mar= c(0, 0, 0, 0)) # Turn margins off, save graphics state plot(myG200, layout = myGxy, rescale = FALSE, xlim = c(min(myGxy[,1]) * 0.99, max(myGxy[,1]) * 1.01), ylim = c(min(myGxy[,2]) * 0.99, max(myGxy[,2]) * 1.01), vertex.color=heat.colors(max(igraph::degree(myG200)+1))[igraph::degree(myG200)+1], vertex.size = 150 + (70 * igraph::degree(myG200)), vertex.label = NA) par(oPar) # restore graphics state # This graph has thirteen singletons and one large, connected component. Many # biological graphs look approximately like this. # Calculate degree distributions dg <- igraph::degree(myG200) brk <- seq(min(dg)-0.5, max(dg)+0.5, by=1) hist(dg, breaks=brk, col="#A5F5CC", xlim = c(-1,11), xaxt = "n", main = "Node degrees", xlab = "Degree", ylab = "Number") # plot histogram axis(side = 1, at = 0:10) # Note the pronounced peak of this distribution: this is not "scale-free". # Here is the log-log plot of frequency vs. degree-rank ... ( freqRank <- table(dg) ) plot(log10(as.numeric(names(freqRank)) + 1), log10(as.numeric(freqRank)), type = "b", pch = 21, bg = "#A5F5CC", xlab = "log(Rank)", ylab = "log(frequency)", main = "200 nodes in a random network") # ... which shows us that this does NOT correspond to the single-slope linear # relationship that we expect for a "scale-free" graph. # == 2.2 scale-free graph (Barabasi-Albert) ================================ # What does one of those intriguing "scale-free" distributions look like? The # iGraph:: package has a function to make random graphs according to the # Barabasi-Albert model of scale-free graphs. It is: sample_pa(), where pa # stands for "preferential attachment". Preferential attachment is _one_ type of # process that will yield scale-free distributions. N <- 200 set.seed(31415927) # set RNG seed for repeatable randomness GBA <- igraph::sample_pa(N, power = 0.8, directed = FALSE) set.seed(NULL) # reset the RNG GBAxy <- igraph::layout_with_graphopt(GBA, charge=0.0001) oPar <- par(mar= c(0, 0, 0, 0)) # Turn margins off, save graphics state plot(GBA, layout = GBAxy, rescale = FALSE, xlim = c(min(GBAxy[,1]) * 0.99, max(GBAxy[,1]) * 1.01), ylim = c(min(GBAxy[,2]) * 0.99, max(GBAxy[,2]) * 1.01), vertex.color=heat.colors(max(igraph::degree(GBA)+1))[igraph::degree(GBA)+1], vertex.size = 200 + (30 * igraph::degree(GBA)), vertex.label = NA) par(oPar) # restore graphics state # This is a very obviously different graph! Some biological networks have # features that look like that - but in my experience the hub nodes are usually # not that distinct. But then again, that really depends on the parameter # "power". Play with the "power" parameter and get a sense for what difference # this makes. Also: note that the graph has only a single component - no # singletons. # What's the degree distribution of this graph? ( dg <- igraph::degree(GBA) ) brk <- seq(min(dg)-0.5, max(dg)+0.5, by=1) hist(dg, breaks=brk, col="#DCF5B5", xlim = c(0,max(dg)+1), xaxt = "n", main = "Node degrees 200 nodes PA graph", xlab = "Degree", ylab = "Number") axis(side = 1, at = seq(0, max(dg)+1, by=5)) # Most nodes have a degree of 1, but one node has a degree of 19. (freqRank <- table(dg)) plot(log10(as.numeric(names(freqRank)) + 1), log10(as.numeric(freqRank)), type = "b", pch = 21, bg = "#DCF5B5", xlab = "log(Rank)", ylab = "log(frequency)", main = "200 nodes in a preferential-attachment network") # Sort-of linear, but many of the higher ranked nodes have a frequency of only # one. That behaviour smooths out in larger graphs - let's try 100000 nodes: # N <- 10000 X <- igraph::sample_pa(N, power = 0.8, directed = FALSE) # 100,000 nodes freqRank <- table(igraph::degree(X)) plot(log10(as.numeric(names(freqRank)) + 1), log10(as.numeric(freqRank)), type = "b", xlab = "log(Rank)", ylab = "log(frequency)", pch = 21, bg = "#A5F5CC", main = "100,000 nodes in a random, scale-free network") rm(X) # == 2.3 Random geometric graph ============================================ # Finally, let's simulate a random geometric graph and look at the degree # distribution. Remember: these graphs have a high probability to have edges # between nodes that are "close" together - an entirely biological notion. # We'll randomly place our nodes in a box. Then we'll define the # probability for two nodes to have an edge to be a function of their Euclidian # distance in the box. # Here is a function that makes an adjacency matrix for such graphs. iGraph:: # has a similar function, sample_grg(), which connects nodes that are closer # than a cutoff, the function I give you below is a bit more interesting since # it creates edges according to a probability that is determined by a # generalized logistic function of the distance. This sigmoidal function gives # a smooth cutoff and creates more "natural" graphs. Otherwise, the function is # very similar to the random graph function, except that we output the # "coordinates" of the nodes together with the adjacency matrix which we then # use for the layout. list() FTW. makeRandomGeometricAM <- function(nam, B = 25, Q = 0.001, t = 0.6) { # Make an adjacency matrix for an undirected random geometric graph from # edges connected with probabilities according to a generalized logistic # function. # Parameters: # nam: a character vector of unique names # B, Q, t: probability that a random pair (i, j) of nodes gets an # edge determined by a generalized logistic function # p <- 1 - 1/((1 + (Q * (exp(-B * (x-t)))))^(1 / 0.9))) # # Value: a list with the following components: # AM$mat : an adjacency matrix # AM$nam : labels for the nodes # AM$x : x-coordinates for the nodes # AM$y : y-coordinates for the nodes # nu <- 1 # probably not useful to change AM <- list() AM$nam <- nam N <- length(AM$nam) AM$mat <- matrix(numeric(N * N), ncol = N) # The adjacency matrix rownames(AM$mat) <- AM$nam colnames(AM$mat) <- AM$nam AM$x <- runif(N) # Randomly place nodes into the unit square AM$y <- runif(N) for (iRow in 1:(N-1)) { # Same principles as in makeRandomGraph() for (iCol in (iRow+1):N) { # geometric distance ... d <- sqrt((AM$x[iRow] - AM$x[iCol])^2 + (AM$y[iRow] - AM$y[iCol])^2) # Pythagoras # distance dependent probability p <- 1 - 1/((1 + (Q * (exp(-B * (d-t)))))^(1 / nu)) if (runif(1) < p) { AM$mat[iRow, iCol] <- 1 } } } return(AM) } # Getting the parameters of a generalized logistic right takes a bit of # experimenting. If you are interested, you can try a few variations. Or you can # look up the function at # https://en.wikipedia.org/wiki/Generalised_logistic_function # This function computes generalized logistics ... # genLog <- function(x, B = 25, Q = 0.001, t = 0.5) { # # generalized logistic (sigmoid) # nu <- 1 # return(1 - 1/((1 + (Q * (exp(-B * (x-t)))))^(1 / nu))) # } # # ... and this code plots p-values over the distances we could encouter between # our nodes: from 0 to sqrt(2) i.e. the diagonal of the unit square in which we # will place our nodes. # x <- seq(0, sqrt(2), length.out = 50) # plot(x, genLog(x), type="l", col="#AA0000", ylim = c(0, 1), # xlab = "d", ylab = "p(edge)") # 200 node random geomteric graph N <- 200 set.seed(112358) # set RNG seed for repeatable randomness rGAM <- makeRandomGeometricAM(as.character(1:N), t = 0.4) set.seed(NULL) # reset the RNG myGRG <- igraph::graph_from_adjacency_matrix(rGAM$mat, mode = "undirected") oPar <- par(mar= c(0, 0, 0, 0)) # Turn margins off plot(myGRG, layout = cbind(rGAM$x, rGAM$y), # use our node coordinates for layout, rescale = FALSE, xlim = c(min(rGAM$x) * 0.9, max(rGAM$x) * 1.1), ylim = c(min(rGAM$y) * 0.9, max(rGAM$y) * 1.1), vertex.color=heat.colors(max(igraph::degree(myGRG)+1))[igraph::degree(myGRG)+1], vertex.size = 0.1 + (0.2 * igraph::degree(myGRG)), vertex.label = NA) par(oPar) # degree distribution: (dg <- igraph::degree(myGRG)) brk <- seq(min(dg) - 0.5, max(dg) + 0.5, by = 1) hist(dg, breaks = brk, col = "#FCC6D2", xlim = c(0, 25), xaxt = "n", main = "Node degrees: 200 nodes RG graph", xlab = "Degree", ylab = "Number") axis(side = 1, at = c(0, min(dg):max(dg))) # You'll find that this is kind of in-between the random, and the scale-free # graph. We do have hubs, but they are not as extreme as in the scale-free case; # and we have have no singletons, in contrast to the random graph. (freqRank <- table(dg)) plot(log10(as.numeric(names(freqRank)) + 1), log10(as.numeric(freqRank)), type = "b", pch = 21, bg = "#FCC6D2", xlab = "log(Rank)", ylab = "log(frequency)", main = "200 nodes in a random geometric network") # = 3 A CLOSER LOOK AT THE igraph:: PACKAGE =============================== # == 3.1 Basics ============================================================ # The basic object of the igraph:: package is a graph object. Let's explore the # first graph some more, the one we built with our random gene names: summary(myG) # This output means: this is an IGRAPH graph, with U = UN-directed edges # and N = named nodes, that has 20 nodes and 20 edges. For details, see ?igraph::print.igraph mode(myG) class(myG) # This means an igraph:: graph object is a special list object; it is opaque in # the sense that a user is never expected to modify its components directly, but # through a variety of helper functions which the package provides. There are # many ways to construct graphs - from adjacency matrices, as we have just done, # from edge lists, or by producing random graphs according to a variety of # recipes, called _games_ in this package. # Two basic functions retrieve nodes "Vertices", and "Edges": igraph::V(myG) igraph::E(myG) # additional properties can be retrieved from the Vertices ... igraph::V(myG)$name # As with many R objects, loading the package provides special functions that # can be accessed via the same name as the basic R functions, for example: print(myG) plot(myG) # this is the result of default plot parameters # ... where plot() allows the usual flexibility of fine-tuning the plot. We # first layout the node coordinates with the Fruchtermann-Reingold algorithm - a # force-directed layout that applies an ettractive potential along edges (which # pulls nodes together) and a repulsive potential to nodes (so they don't # overlap). Note the use of the degree() function to color and scale nodes and # labels by degree and the use of the V() function to retrieve the vertex names. # See ?plot.igraph for details." # Plot with some customizing parameters oPar <- par(mar= c(0, 0, 0, 0)) # Turn margins off plot(myG, layout = igraph::layout_with_fr(myG), vertex.color=heat.colors(max(igraph::degree(myG)+1))[igraph::degree(myG)+1], vertex.size = 12 + (5 * igraph::degree(myG)), vertex.label.cex = 0.4 + (0.15 * igraph::degree(myG)), edge.width = 2, vertex.label = igraph::V(myG)$name, vertex.label.family = "sans", vertex.label.cex = 0.9) par(oPar) # ... or with a different layout: oPar <- par(mar= c(0, 0, 0, 0)) # Turn margins off plot(myG, layout = igraph::layout_in_circle(myG), vertex.color=heat.colors(max(igraph::degree(myG)+1))[igraph::degree(myG)+1], vertex.size = 12 + (5 * igraph::degree(myG)), vertex.label.cex = 0.4 + (0.15 * igraph::degree(myG)), edge.width = 2, vertex.label = igraph::V(myG)$name, vertex.label.family = "sans", vertex.label.cex = 0.9) par(oPar) # igraph:: has a large number of graph-layout functions: see # ?layout_ and try them all. # == 3.2 Components ======================================================== # The function igrap::components() tells us whether there are components of the # graph in which there is no path to other components. igraph::components(myG) # In the _membership_ vector, nodes are annotated with the index of the # component they are part of. Sui7 is the only node of component 2, Cyj1 is in # the third component etc. This is perhaps more clear if we sort by component # index sort(igraph::components(myG)$membership, decreasing = TRUE) # Retrieving e.g. the members of the first component from the list can be done by subsetting: (sel <- igraph::components(myG)$membership == 1) # boolean vector .. (c1 <- igraph::components(myG)$membership[sel]) names(c1) # = 4 RANDOM GRAPHS AND GRAPH METRICS ===================================== # Let's explore some of the more interesting, topological graph measures. We # start by building a somewhat bigger graph. We aren't quite sure whether # biological graphs are small-world, or random-geometric, or # preferential-attachment ... but igraph:: has ways to simulate the basic ones # (and we could easily simulate our own). Look at the following help pages: ?igraph::sample_gnm # see also sample_gnp for the Erdös-Rényi models ?igraph::sample_smallworld # for the Watts & Strogatz model ?igraph::sample_pa # for the Barabasi-Albert model # But note that there are many more sample_ functions. Check out the docs! # Let's look at betweenness measures for our first graph. Here: the nodes again # colored by degree. Degree centrality states: nodes of higher degree are # considered to be more central. And that's also the way the force-directed # layout drawas them, obviously. set.seed(112358) # set RNG seed for repeatable randomness myGxy <- igraph::layout_with_fr(myG) # calculate layout coordinates set.seed(NULL) # reset the RNG oPar <- par(mar = rep(0, 4)) # turn margins off, save graphics state plot(myG, layout = myGxy, rescale = FALSE, xlim = c(min(myGxy[,1]) * 0.99, max(myGxy[,1]) * 1.01), ylim = c(min(myGxy[,2]) * 0.99, max(myGxy[,2]) * 1.01), vertex.color=heat.colors(max(igraph::degree(myG)+1))[igraph::degree(myG)+1], vertex.size = 40 + (15 * igraph::degree(myG)), vertex.label.cex = 0.4 + (0.15 * igraph::degree(myG)), vertex.label = igraph::V(myG)$name, vertex.label.family = "sans") par(oPar) # restore graphics state # == 4.1 Diameter ========================================================== igraph::diameter(myG) # The diameter of a graph is its maximum length # shortest path. # let's plot this path: here are the nodes ... igraph::get_diameter(myG) # ... and we can get the x, y coordinates from iGxy by subsetting with the node # names. The we draw the diameter-path with a transparent, thick pink line: lines(myGxy[igraph::get_diameter(myG),], lwd=10, col="#ff63a788") # == Centralization scores ?igraph::centralize # replot our graph, and color by log_betweenness: bC <- igraph::centr_betw(myG) # calculate betweenness centrality nodeBetw <- bC$res nodeBetw <- round(log(nodeBetw +1)) + 1 oPar <- par(mar= c(0, 0, 0, 0)) # Turn margins off plot(myG, layout = myGxy, rescale = FALSE, xlim = c(min(myGxy[,1]) * 0.99, max(myGxy[,1]) * 1.01), ylim = c(min(myGxy[,2]) * 0.99, max(myGxy[,2]) * 1.01), vertex.color=heat.colors(max(igraph::degree(myG)+1))[igraph::degree(myG)+1], vertex.size = 40 + (15 * igraph::degree(myG)), vertex.label.cex = 0.4 + (0.15 * igraph::degree(myG)), vertex.label = igraph::V(myG)$name, vertex.label.family = "sans") par(oPar) # Note that the betweenness - the number of shortest paths that pass through a # node, is in general higher for high-degree nodes - but not always: Eqr2 has # higher betweenness than Itv7: this measure really depends on the detailed # local topology of the graph." # Can you use centr_eigen() and centr_degree() to calculate the respective # values? That's something I would expect you to be able to do. # # Lets plot betweenness centrality for our random geometric graph: bCmyGRG <- igraph::centr_betw(myGRG) # calculate betweenness centrality nodeBetw <- bCmyGRG$res nodeBetw <- round((log(nodeBetw +1))^2.5) + 1 # colours and size proportional to betweenness oPar <- par(mar= c(0, 0, 0, 0)) # Turn margins off plot(myGRG, layout = cbind(rGAM$x, rGAM$y), # use our node coordinates for layout, rescale = FALSE, xlim = c(min(rGAM$x) * 0.9, max(rGAM$x) * 1.1), ylim = c(min(rGAM$y) * 0.9, max(rGAM$y) * 1.1), vertex.color=heat.colors(max(nodeBetw))[nodeBetw], vertex.size = 0.1 + (0.03 * nodeBetw), vertex.label = NA) par(oPar) igraph::diameter(myGRG) lines(rGAM$x[igraph::get_diameter(myGRG)], rGAM$y[igraph::get_diameter(myGRG)], lwd = 10, col = "#ff335533") # = 5 GRAPH CLUSTERING ==================================================== # Clustering finds "communities" in graphs - and depending what the edges # represent, these could be complexes, pathways, biological systems or similar. # There are many graph-clustering algorithms. One approach with many attractive # properties is the Map Equation, developed by Martin Rosvall. See: # http://www.ncbi.nlm.nih.gov/pubmed/18216267 and htttp://www.mapequation.org myGRGclusters <- igraph::cluster_infomap(myGRG) igraph::modularity(myGRGclusters) # ... measures how separated the different # membership types are from each other igraph::membership(myGRGclusters) # which nodes are in what cluster? table(igraph::membership(myGRGclusters)) # how large are the clusters? # The largest cluster has 48 members, the second largest has 25, etc. # Lets plot our graph again, coloring the nodes of the first five communities by # their cluster membership: # first, make a vector with as many grey colors as we have communities ... commColors <- rep("#f1eef6", max(igraph::membership(myGRGclusters))) # ... then overwrite the first five with "real colors" - something like rust, # lilac, pink, and mauve or so. commColors[1:5] <- c("#980043", "#dd1c77", "#df65b0", "#c994c7", "#d4b9da") oPar <- par(mar= c(0, 0, 0, 0)) # Turn margins off plot(myGRG, layout = cbind(rGAM$x, rGAM$y), rescale = FALSE, xlim = c(min(rGAM$x) * 0.9, max(rGAM$x) * 1.1), ylim = c(min(rGAM$y) * 0.9, max(rGAM$y) * 1.1), vertex.color=commColors[igraph::membership(myGRGclusters)], vertex.size = 0.3 + (0.4 * igraph::degree(myGRG)), vertex.label = NA) par(oPar) # That's all. # [END]