Sparsity plot

A model with an L1 penalty drops most variables on most components. The sparsity plot counts how many each component keeps nonzero and draws them as bars, so how hard the penalty bit — component by component — reads at a glance.

plot_sparsity(loadings; comps = [...], ncomp = 0, compnames = ..., asfraction = false, kwargs...)

The models with an L1 penalty pass the field their penalty acts on:

ModelCall
spcplot_sparsity(m.loadings)
pmdplot_sparsity(m.v) — the variable side, m.u for the sample side
splsdaplot_sparsity(m.loadings_X)
sccaplot_sparsity(m.u) — the X side, m.v for the Y side

The dense models — pca, plskern, plsda, cca — select every variable on every component, so their sparsity plot would be a flat row of full bars and is not drawn.

Note that pmd and scca penalize both factors, so each has a sparsity on either side. For pmd, m.v counts the variables kept and m.u the samples.

Setup

using BigRiverEssence
using BigRiverPlots
using Plots
using StableRNGs

A simulated example

Forty variables driven by three latent signals, fitted with a sparse PCA. Forty is enough that the penalty leaves the counts well below the total — on twenty the bars would sit close to full and the plot would have nothing to show.

We print the counts alongside, since that column of numbers is exactly what the figure draws.

rng = StableRNG(20240801)

n = 90    # observations
p = 40    # variables

latent = randn(rng, n, 3)
X = latent * randn(rng, 3, p) .+ 0.3 .* randn(rng, n, p)

m = spc(X; k = 4, c = sqrt(p) / 2)

# what the plot will draw, as numbers
[count(!iszero, m.loadings[:, j]) for j in 1:4], p
([16, 14, 17, 19], 40)

The default plot

One bar per component, ticked by index, with the count printed over each bar. The label is what makes this readable without squinting at the axis — the exact number matters more here than the relative height.

plot_sparsity(m.loadings)
Example block output

Modifying the plot

There are two ways to choose components and they differ in an important way. ncomp takes a leading count — ncomp = 2 gives components 1 and 2 — while comps takes an explicit vector, so a non-contiguous set like [1, 3] is possible with comps and not with ncomp.

asfraction changes the quantity from a count to a proportion, compnames names the bars, labelcounts removes the numbers, and sparsitycolor sets the fill.

Naming, and choosing components

compnames is subset along with the counts, so the labels stay correct whatever we select — which means we pass one name per component of the matrix, not one per entry of comps. That is the same convention as varnames on the loadings plot.

Here we take a non-contiguous pair, which only comps can do.

plot_sparsity(m.loadings;
              comps     = [1, 3],
              compnames = ["SPC $(j)" for j in 1:4],
              ylabel    = "variables selected",
              title     = "components 1 and 3")
Example block output

As a fraction

asfraction = true divides by the total number of variables, which reads more easily when there are many — a bar at 0.15 tells us the penalty kept fifteen percent without our needing to remember what p was.

The count labels turn themselves off here, since a fraction over a bar reads worse than the axis does.

plot_sparsity(m.loadings;
              asfraction = true,
              compnames  = ["SPC $(j)" for j in 1:4],
              ylabel     = "fraction of variables selected",
              title      = "the same fit, as a proportion",
              ylims      = (0, 1))
Example block output

Color, and the rest

sparsitycolor fills the bars and labelcounts = false removes the numbers over them — worth doing when the figure is small enough that the labels crowd, or when it sits beside others and the axis carries the reading.

Everything else the plot sets for itself yields to what we pass.

plot_sparsity(m.loadings;
              compnames     = ["SPC $(j)" for j in 1:4],
              sparsitycolor = "#238b45",
              labelcounts   = false,
              ylabel        = "variables selected",
              title         = "sparse PCA",
              size          = (600, 400),
              guidefontsize = 10)
Example block output