PLSDA: PLS Discriminant Analysis

PLS discriminant analysis or PLSDA is a supervised classification method. PLSDA is a modification of partial least square regression that makes it suited for classification, where the components separate the classes. Unlike its sparse counterpart splsda, PLSDA does not do variable selection, so every variable contributes to each component.

In this documentation, we will depomstrate implementation of PLSDA using BigRiverEssence.plsda on a SRBCT dataset.

The method

Let us consider a vector of classes where the $i$th component indicates the class of the $i$th observation. Let $X$ be a matrix of predictors with variables being the columns and observations being the rows. In PLSDA, the class labels are first dummy-encoded such that we get an indicator matrix $Y$ from the vector of classes, where $Y_{ik} = 1$ if sample $i$ belongs to class $k$. PLSDA then performs a PLS regression of the predictors $X$ onto this indicator matrix $Y$.

In PLSDA, we find each component by obtaining loading vectors $u$ (over variables) and $v$ (over classes) such that we maximize the covariance between the variable side score $Xu$ and the class side score $Yv$. Hence the optimization probem can be written as: $\max_{u,v} \; \operatorname{cov}(Xu, Yv) \quad \text{subject to } \|u\|_2 = \|v\|_2 = 1.$ Unlike splsda, no penalty is applied to $u$, so all variables contribute to each loading. We then solve the component by alternating power iteration where we update $u$ from the class scores, then we update $v$ from the variable scores — until the loadings converge. We then deflate both the variable block $X$ and the class block $Y$ by regressing on the variable score (the regression mode). We then extract the next component from the residual.

The result gives, per component, a dense loading over all variables and sample scores that separate the classes.

Implementation note. This implementation adapts and streamlines mixOmics::plsda from the R package mixOmics. It preserves the dummy-encoded response formulation and regression-mode deflation while re-engineering the calculations in Julia to reuse work arrays, reduce intermediate memory allocation, and improve execution efficiency.

The data

We use the SRBCT dataset from the mixOmics package. The SRBCT (Small Round Blue Cell Tumors) dataset contains expression levels of $2308$ genes on $63$ samples across four tumor classes — Ewing sarcoma (EWS), Burkitt lymphoma (BL), neuroblastoma (NB), and rhabdomyosarcoma (RMS) — from Khan et al. (2001) [1], obtained via the mixOmics R package [2]. The goal is to build components that can best discremenate the tumour classes.

using BigRiverEssence, DelimitedFiles, Plots, Statistics
datadir = joinpath(pkgdir(BigRiverEssence), "reference_Data", "srbctdata")
X = readdlm(joinpath(datadir, "gene.csv"), ',', Float64)          # 63 × 2308 gene expression
y = vec(readdlm(joinpath(datadir, "class.csv"), String))          # 63 class labels
size(X), unique(y)
((63, 2308), ["EWS", "BL", "NB", "RMS"])

Fitting the model

Now we fit plsda to the predictor matrix $X$. We also pass the number of components we want to have as ncomp. Unlike splsda, there is no keepX since PLSDA does not select variables. We fit three components.

ncomp = 3
m = plsda(X, y, ncomp)

# unlike splsda, the loadings are dense — every gene contributes to each component
[count(!iszero, m.loadings_X[:, c]) for c in 1:m.ncomp]
3-element Vector{Int64}:
 2308
 2308
 2308

The fitted Plsda holds the sample scores (variates_X), the dense gene loadings (loadings_X), the class labels, and the dummy encoding.

Sample plot

Now we project the samples onto the first two components and color by tumor class. This will show us how the discriminant components separate the types.

V = m.variates_X
scatter(V[:, 1], V[:, 2]; group = y,
    xlabel = "PLSDA component 1", ylabel = "PLSDA component 2",
    title = "SRBCT samples: components 1 and 2", legend = :outerright,
    markersize = 5, markerstrokewidth = 0.5)
Example block output

We note from the above plot that components $1$ and $2$ are able to separate the four tumor types. Component 1 separates BL from EWS and NB from RMS. Hwere as component 1 separates BL and EWS from NB and RMS. Hence we see 4 clusters at four corners.

Top genes by loading

Since PLSDA does not select variables, every gene has a nonzero loading. To see which genes define a component, we look at the ones with the largest-magnitude loadings. We plot the discriminant weights of the $40$ genes with the largest absolute loading on component $1$ as a bar plot.

L1 = m.loadings_X[:, 1]
sel = sortperm(abs.(L1); rev = true)[1:40]     # the 40 largest-magnitude genes on component 1

bar(L1[sel]; xticks = (1:length(sel), string.(sel)), xrotation = 90,
    legend = false, xlabel = "gene (index)", ylabel = "loading",
    title = "Component 1: top $(length(sel)) genes by |loading|")
Example block output

Variable importance (VIP)

VIP (Variable Importance in Projection) summarizes each gene's contribution across all components into a single score. We compute it as in mixOmics: variables with VIP scores to see the proportion above or below $1$.

vip_final = vip(m)[:, end]                    # cross-component VIP (last column)

# --- all genes, sorted by VIP, thin horizontal bars ---
ord  = sortperm(vip_final; rev = true)        # ALL genes, sorted high → low VIP
vals = vip_final[ord]
n    = length(vals)

xhi = ceil(maximum(vals) * 10) / 10           # a bit above the largest VIP

bar(
    1:n, reverse(vals);                       # explicit (category, value) so orientation is unambiguous
    orientation = :horizontal,
    fillcolor = :black, linecolor = :grey,   # thin black bars
    bar_width = 1.0,
    yticks = false,                            # 2308 labels won't fit — hide them
    legend = false,
    xlims = (0, xhi),                          # VIP axis: 0 to ~2.4 (NOT the gene count)
    xlabel = "VIP ($(m.ncomp) axes)",
    ylabel = "genes (sorted by VIP)",
    title = "Variable Importance in Projection",
    left_margin = (10,:mm),
    size = (600, 550)
)
vline!([1.0], color = :red, linestyle = :dash)   # threshold at VIP = 1
Example block output

We see in the above plot, that when sorted by VIP, around half of genes have VIP avove 1 and half below 1.

Summary

In this example, we used a dataset with $63$ samples and $2308$ genes. The plsda model we fitted built three components that separate the four tumor types using all of the genes. plsda can be used in any such dataset where the goal is discriminent analysis and the number of predictor variables exceeds the number of rows. When variable selection is also desired, splsda adds sparsity on top of this same model.

References

[1] Khan, J., Wei, J. S., Ringnér, M., et al. (2001). Classification and diagnostic prediction of cancers using gene expression profiling and artificial neural networks. Nature Medicine, 7(6), 673–679.

[2] Rohart, F., Gautier, B., Singh, A., & Lê Cao, K.-A. (2017). mixOmics: An R package for 'omics feature selection and multiple data integration. PLoS Computational Biology, 13(11), e1005752.