Where surCoin() puts
the categories of the variables on the graph, surScat()
puts the cases themselves. Each case becomes a node,
placed on a factorial plane extracted from the variables and coloured by
the group it falls into. The result is a netCoin object
holding the nodes, their coordinates and the options, which can be
plotted as an interactive scattergram.
iris holds 150 flowers, four measurements of each and
the species it belongs to. The species is what makes it useful here: it
is held out of the analysis as an illustrative
variable, so it does not shape the plane, but it travels with
the cases and can be compared afterwards against whatever grouping is
found.
variables are the columns that travel with the cases;
active are the ones that build the plane.
All four active variables are numeric, so surScat() runs
a principal component analysis. Had any of them been a
factor it would have run a multiple correspondence analysis instead;
type states it explicitly. Each axis is labelled with the
share of variance it accounts for:
The node table holds one row per case: its variables, the coordinates
fx and fy on the plane, and the clusterization
surScat() computes on its own, of
nclusters = 2 groups by default.
head(S$nodes[, c("Species", "Groups(2)", "fx", "fy")], 3)
#> Species Groups(2) fx fy
#> 1 setosa Group: 1 -1.321232 -0.5004175
#> 2 setosa Group: 1 -1.214037 0.7027698
#> 3 setosa Group: 1 -1.379296 0.3564318It also holds ntext, the HTML description behind every
node’s tooltip, and name, its identifier. Printing the
object itself (S, or print(S)) does not dump
the node table: it summarizes it instead, leaving out ntext
and the k-means group columns, which are named on their own line.
S
#> <netCoin object>
#> Nodes: 150 (Sepal.Length, Sepal.Width, Petal.Length, Petal.Width, Species, name, fx, fy)
#> Links: 0
#> Clusterizations: Groups(2)
#> Planes: PCATwo groups are enough to tell one species from the other two, which is as far as two groups can go:
table(S$nodes[["Groups(2)"]], S$nodes$Species)
#>
#> setosa versicolor virginica
#> Group: 1 50 0 0
#> Group: 2 0 50 50The interactive scattergram opens in the browser, where the cases can be hovered, filtered and recoloured by any column of the node table:
Drawn statically from those same coordinates, the plane looks like this, with the default grouping on the left and the species on the right:
addClusters() takes a clusterization computed elsewhere
and adds it as one more column, so that several ways of grouping the
same cases can be compared on one plane. A kmeans object is
understood as it comes:
set.seed(1)
km <- kmeans(scale(iris[, 1:4]), 3, nstart = 25)
S <- addClusters(S, km, name = "kmeans(3)")
table(S$nodes[["kmeans(3)"]], S$nodes$Species)
#>
#> setosa versicolor virginica
#> Kmeans: 1 50 0 0
#> Kmeans: 2 0 39 14
#> Kmeans: 3 0 11 36An hclust has to be cut first, since the number of
groups is not part of it:
hc <- cutree(hclust(dist(scale(iris[, 1:4]))), 3)
S <- addClusters(S, hc, name = "hclust(3)")
table(S$nodes[["hclust(3)"]], S$nodes$Species)
#>
#> setosa versicolor virginica
#> Hclust: 1 49 0 0
#> Hclust: 2 1 21 2
#> Hclust: 3 0 29 48Every column is an ordered factor whose groups are numbered by their
mean coordinate on the first axis, so that group 1 always sits on the
left. The labels take the name of the column without its number of
groups, so "kmeans(3)" reads "Kmeans: 1" and
on.
An object holding more than one solution is taken apart into one
column per solution, each named after the method and its number of
groups. A tidyLPA estimated for two, three and four
profiles yields LPA(2), LPA(3) and
LPA(4) in a single call:
mod <- tidyLPA::estimate_profiles(iris[, 1:4], 2:4,
variances = "equal", covariances = "zero")
S <- addClusters(S, mod)
table(S$nodes[["LPA(3)"]], S$nodes$Species)
#>
#> setosa versicolor virginica
#> LPA: 1 50 0 0
#> LPA: 2 0 49 6
#> LPA: 3 0 1 44Of the three ways of cutting the cases in three, the latent profiles are the one that comes closest to the species.
addCommunities() groups the cases by a different route.
It correlates them with one another over the variables,
which is a transposed reading of the data frame: each case becomes a
column and each variable a row, so that a link joins two cases whose
profiles run parallel. The communities of that network are then a
grouping of the cases, and reach the node table as any other one
does.
S <- addCommunities(S, iris, variables = names(iris)[1:4], community = c("lo", "wa"))
#> Gathered into the residual group -- louvain(6): 2 communities (2 of fewer than 2 cases), 2 cases in all; walktrap(10): 2 communities (2 of fewer than 2 cases), 2 cases in all. Set minSize=1 to keep every community apart.
lou <- grep("^louvain", names(S$nodes), value = TRUE)
table(S$nodes[[lou]], S$nodes$Species)
#>
#> setosa versicolor virginica
#> Louvain: 1 49 1 0
#> Louvain: 2 1 9 10
#> Louvain: 3 0 24 26
#> Louvain: 4 0 14 12
#> Louvain: 5 0 0 2
#> Louvain: rest 0 2 0Note what this does and does not recover. One species comes out almost clean, while the other two stay mixed together across the communities. That is the method speaking, not a failure: a correlation between two cases centres each of them on its own mean across the variables, so what it compares is the shape of a profile, not its size. One species has a distinctly shaped flower, with petals small relative to its sepals; the other two share that shape and differ mostly in magnitude, which this reading deliberately sets aside.
Three arguments govern the result:
scale standardizes the variables before transposing. It
is on by default, because without it the variable of widest range writes
the profile on its own.minL is how high a correlation two cases need to be
linked. Raising it makes the network sparser, and the communities more
and smaller.minSize gathers the communities below it into a
residual group, since a network of cases leaves isolated ones behind and
igraph returns each of them as a community of its own. That group is
pinned last and labelled with a word instead of a number, so it reads
apart from the real ones.levels(S$nodes[[lou]])
#> [1] "Louvain: 1" "Louvain: 2" "Louvain: 3" "Louvain: 4"
#> [5] "Louvain: 5" "Louvain: rest"Eight methods are available, by code or by name:
edgeBetweenness (ed), fastGreedy
(fa), labelProp (la),
leadingEigen (le), louvain
(lo), optimal (op),
spinglass (sp) and walktrap
(wa). Their costs do not grow alike, and each carries the
size it can meet: louvain answers on thousands of cases in
seconds, whereas optimal solves the exact maximum by
integer programming and does not reach the 150 of iris.
addCommunities(S, iris, variables = names(iris)[1:4], community = "op")
#> Error in `addCommunities()`:
#> ! optimal does not answer on a network of 150 nodes; it bears 100. Choose one of fastGreedy, labelProp, leadingEigen, louvain, walktrap, cap the nodes with maxN in surScat, or raise maxNodes to wait for it anyway.The scattergram is drawn on one plane, but others can be added to it
and picked from a selector. addAxes() takes two further
coordinates, either factorial or observed, and adds them under a name of
their own:
S <- addAxes(S, iris[, c("Petal.Length", "Sepal.Width")], name = "petals")
names(S$layouts)
#> [1] "PCA" "petals"Each clusterization stays as it is; only the coordinates change under it, which is what makes the two planes comparable.
$clusters holds the (weighted) centroid of every group
of every clusterization the object holds, on every plane it holds: a
flat list with one entry per clusterization-plane pair, its name joining
the two with a dot, and one row per group.
S$clusters
#> <netCoin clusters> 8 clusterizations x 2 planes (16 sets of centroids)
#> Clusterizations: Groups(2), kmeans(3), hclust(3), LPA(2), LPA(3), LPA(4), louvain(6), walktrap(10)
#> Planes: PCA, petals
S$clusters[["Groups(2).PCA"]]
#> PC1 (73.0%) PC2 (22.9%)
#> Group: 1 -1.2979252 -0.3012007
#> Group: 2 0.6489626 0.1506003Printing it, on its own or as part of the object, only says how many clusterizations and planes it covers, rather than dumping every matrix.
addAxes extends every clusterization already there to
the plane it just added, and addClusters computes the
centroids of whatever it adds on every plane already there, so the two
stay in step automatically. kmeans(3) was added earlier,
before petals existed, yet its centroids are there on that
plane too:
Two arguments matter once the cases are many. patterns
collapses the cases that share the same values into a single node, whose
size is how many they are; a clusterization computed case by case is
then reduced to the commonest group of each pattern. And
maxN, 2000 by default, caps the nodes by drawing a random
sample of them, since a scattergram of tens of thousands of points is
neither readable nor quick. The object records which cases were drawn,
so that addCommunities() can build its network on them.