Visualization

Visualization in NCPLS happens at three different levels:

  • scoreplot shows how samples are arranged in the latent space.
  • weightlandscape and coefficientlandscape map fitted objects back onto the original predictor surface.
  • weightprofiles keeps the multilinear mode weights separated, one profile per predictor axis.

This page uses Makie for static figures embedded in the manual and PlotlyJS for the interactive plot wrappers. scoreplot supports both backends. The dedicated landscape and profile plot wrappers currently use PlotlyJS only.

Example Data

The examples below reuse the synthetic multilinear dataset introduced on the Fit page. We fit one discriminant-analysis model for score plots and one multilinear regression model for the predictor-surface and weight-profile views.

using NCPLS
using CairoMakie
using PlotlyJS
using Statistics

data = synthetic_multilinear_hybrid_data(
    nmajor=48,
    nminor=32,
    mode_dims=(28, 20),
    orthogonal_truth=true,
    integer_counts=false,
)

model_da = NCPLSModel(
    ncomponents=2,
    multilinear=true,
    orthogonalize_mode_weights=false,
)

model_reg = NCPLSModel(
    ncomponents=2,
    multilinear=true,
    orthogonalize_mode_weights=false,
)

mf_da = fit(
    model_da,
    data.X,
    data.sampleclasses;
    Yadd=data.Yadd,
    obs_weights=data.obs_weights,
    samplelabels=data.samplelabels,
    predictoraxes=data.predictoraxes,
)

mf_reg = fit(
    model_reg,
    data.X,
    data.Yprim_reg;
    Yadd=data.Yadd,
    obs_weights=data.obs_weights,
    responselabels=data.responselabels_reg,
    samplelabels=data.samplelabels,
    sampleclasses=data.sampleclasses,
    predictoraxes=data.predictoraxes,
)

blue, orange, green = Makie.wong_colors()[[1, 2, 3]]

trait1 = data.Yprim_reg[:, 1]
q1, q2 = quantile(trait1, [1 / 3, 2 / 3])
trait_bins = ifelse.(trait1 .<= q1, "low",
             ifelse.(trait1 .<= q2, "mid", "high"))

rt_axis, mz_axis = predictoraxes(mf_reg)

Score Plots

scoreplot has two main entry points:

  • scoreplot(mf) uses the stored sample labels, sample classes, and first two latent variables from a fitted model.
  • scoreplot(samples, groups, scores) is the lower-level form for custom grouping or for plotting scores that were assembled outside the fitted-model convenience path.
NCPLS.scoreplotFunction
scoreplot(samples, groups, scores; backend=:plotly, kwargs...)
scoreplot(mf::NCPLSFit; backend=:plotly, kwargs...)

Backend dispatcher for NCPLS score plots. Use backend=:plotly (default) for the PlotlyJS extension or backend=:makie for the Makie extension.

The dispatcher accepts backend-agnostic keywords and passes any remaining keywords to the selected backend. To avoid confusion, think of the keywords as belonging to three groups:

General (backend-agnostic)

  • backend::Symbol = :plotly Select the backend. Supported values: :plotly, :makie.

PlotlyJS backend keywords (PlotlyJSExtension)

  • group_order::Union{Nothing,AbstractVector} = nothing Order of groups (also draw order; later is on top). If nothing, uses levels(groups) for CategoricalArray, else unique(groups).
  • default_trace = (;) PlotlyJS scatter kwargs applied to every group (except marker).
  • group_trace::AbstractDict = Dict() Per-group PlotlyJS scatter kwargs.
  • default_marker = (;) PlotlyJS marker kwargs for every group (keys must be Symbols).
  • group_marker::AbstractDict = Dict() Per-group marker kwargs (keys must be Symbols).
  • hovertemplate::AbstractString = "Sample: %{text}<br>Group: %{fullData.name}<br>LV1: %{x}<br>LV2: %{y}<extra></extra>" Hover text template. The default shows sample, group, LV1, LV2.
  • layout::Union{Nothing,PlotlyJS.Layout} = nothing Layout object; if nothing, a default layout is created using title, xlabel, and ylabel.
  • plot_kwargs = (;) Extra kwargs passed to PlotlyJS.plot (e.g., config).
  • show_legend::Union{Nothing,Bool} = nothing If false, sets showlegend=false for all traces.
  • title::AbstractString = "Scores"
  • xlabel::AbstractString = "Latent Variable 1"
  • ylabel::AbstractString = "Latent Variable 2"

Makie backend keywords (MakieExtension)

  • group_order::Union{Nothing,AbstractVector} = nothing Order of groups (also draw order).
  • default_scatter = (;) Makie scatter kwargs applied to every group.
  • group_scatter::AbstractDict = Dict() Per-group scatter kwargs.
  • default_trace = (;) Additional scatter kwargs applied to every group (legacy convenience).
  • group_trace::AbstractDict = Dict() Per-group scatter kwargs (legacy convenience).
  • default_marker = (;) Marker-related kwargs applied to every group.
  • group_marker::AbstractDict = Dict() Per-group marker kwargs.
  • title::AbstractString = "Scores"
  • xlabel::AbstractString = "Latent Variable 1"
  • ylabel::AbstractString = "Latent Variable 2"
  • figure = nothing Provide an existing Figure to draw into.
  • axis = nothing Provide an existing Axis to draw into.
  • figure_kwargs = (;) Extra kwargs passed to Figure when it is created.
  • axis_kwargs = (;) Extra kwargs passed to Axis when it is created.
  • show_legend::Bool = true If true, calls axislegend.
  • legend_kwargs = (;) Extra kwargs passed to axislegend.
  • show_inspector::Bool = true If true, enables DataInspector on GLMakie/WGLMakie.
  • inspector_kwargs = (;) Extra kwargs passed to DataInspector.

Notes

  • The dispatcher checks that the requested backend is loaded and errors with "Backend <pkg> not loaded" if not.
  • Unknown backend values throw error("Unknown backend").
  • scores must have at least two columns (LV1 and LV2).
source

The next figure shows both patterns. On the left, scoreplot(mf_da) uses the fitted DA model directly. On the right, the same plotting function is given explicit sample labels, custom groups, and the first two regression scores so that the samples are colored by trait bins rather than by class.

fig_scores = Figure(size=(1200, 500))

scoreplot(
    mf_da;
    backend=:makie,
    figure=fig_scores,
    axis=Axis(fig_scores[1, 1]),
    title="DA scores by class",
    group_order=["minor", "major"],
    default_marker=(; markersize=12),
    group_marker=Dict(
        "minor" => (; color=orange),
        "major" => (; color=blue),
    ),
    show_inspector=false,
)

scoreplot(
    data.samplelabels,
    trait_bins,
    xscores(mf_reg, 1:2);
    backend=:makie,
    figure=fig_scores,
    axis=Axis(fig_scores[1, 2]),
    title="Regression scores grouped by trait bins",
    group_order=["low", "mid", "high"],
    default_marker=(; markersize=12),
    group_marker=Dict(
        "low" => (; color=blue),
        "mid" => (; color=green),
        "high" => (; color=orange),
    ),
    show_inspector=false,
)

save("visualization_scoreplots.svg", fig_scores)

The Plotly backend uses the same high-level call but produces an interactive score plot.

scoreplot_da_plotly = scoreplot(
    mf_da;
    backend=:plotly,
    title="NCPLS DA scores",
    group_order=["minor", "major"],
    default_marker=(; size=10),
    group_marker=Dict(
        "minor" => (; color="rgb(230,159,0)"),
        "major" => (; color="rgb(0,114,178)"),
    ),
)

PlotlyJS.savefig(scoreplot_da_plotly, "visualization_scoreplot.html")

Open the interactive score plot

Predictor Surfaces

NCPLS stores fitted objects that still live on the predictor axes themselves. For a genuinely two-mode predictor, two views are especially useful:

  • weightlandscape(mf; lv=k) shows which regions of the predictor surface define one latent variable.
  • coefficientlandscape(mf; response=j) or coefficientlandscape(mf; response_contrast=(pos, neg)) shows how the fitted model maps the predictor surface to one response or to a signed response contrast.

These are different objects. Weight landscapes are component objects on the predictor side. Coefficient landscapes are response objects derived from the cumulative fitted model. For fits with several responses, it is often clearer to request the response or contrast explicitly rather than relying on defaults.

NCPLS.weightlandscapeFunction
weightlandscape(
    mf::NCPLSFit;
    lv::Union{Symbol, Integer}=1,
    combine::Symbol=:sum,
)

Return a 2D loading-weight landscape from a fitted NCPLS model.

  • lv = k::Integer returns the component-specific loading-weight surface W[:, :, k].
  • lv = :combined or :all combines all component surfaces according to combine.

Supported combination rules are:

  • :sum for a signed sum of the component surfaces.
  • :mean for a signed mean of the component surfaces.
  • :sumabs for the sum of absolute component weights.
  • :meanabs for the mean absolute component weight.

Because latent-variable signs are arbitrary, :sumabs or :meanabs are often more stable than signed combinations when inspecting all components together.

source
NCPLS.weightlandscapeplotFunction
weightlandscapeplot(mf; backend=:plotly, kwargs...)

Backend dispatcher for NCPLS loading-weight landscape plots. The fit must describe exactly two predictor axes, either through stored predictoraxes metadata or implicitly through the returned weight surface.

General keywords

  • backend::Symbol = :plotly Select the plotting backend. Supported values: :plotly.
  • lv::Union{Symbol,Integer} = 1 Use an integer for one component or :combined / :all for an aggregate across all stored components.
  • combine::Symbol = :sum Combination rule for lv = :combined. Supported values: :sum, :mean, :sumabs, and :meanabs.

PlotlyJS backend keywords

  • sample_size::Integer = 50_000
  • iqr_multiplier::Real = 8.0
  • clip_quantile::Real = 0.995
  • colorscale::Union{Nothing,AbstractString} = nothing
  • colorbar_title::AbstractString = "Weight"
  • hovertemplate::Union{Nothing,AbstractString} = nothing
  • title::Union{Nothing,AbstractString} = nothing
  • layout = nothing
  • plot_kwargs = (;)
source
NCPLS.coefficientlandscapeFunction
coefficientlandscape(
    mf::NCPLSFit;
    lv::Union{Symbol, Integer}=:final,
    response::Union{Nothing, Integer}=nothing,
    response_contrast::Union{Nothing, Tuple{<:Integer, <:Integer}}=nothing,
)

Return a 2D coefficient landscape from a fitted NCPLS model. The returned matrix is the requested predictor-surface view of the regression coefficients:

  • lv = :final returns the cumulative final model coefficients.
  • lv = k::Integer returns the isolated contribution of latent variable k, i.e. coef(mf, k) - coef(mf, k - 1) for k > 1.

When the fit has one response, that response is used automatically. When the fit has two responses and neither response nor response_contrast is provided, the default contrast is response 2 - response 1. For more than two responses, response or response_contrast = (positive, negative) must be supplied explicitly.

source
NCPLS.coefflandscapeplotFunction
coefflandscapeplot(mf; backend=:plotly, kwargs...)
landscapeplot(mf; backend=:plotly, kwargs...)

Backend dispatcher for NCPLS coefficient-landscape plots. The fit must describe exactly two predictor axes, either through stored predictoraxes metadata or implicitly through the matrix returned by coefficientlandscape.

General keywords

  • backend::Symbol = :plotly Select the plotting backend. Supported values: :plotly.
  • lv::Union{Symbol,Integer} = :final Use :final for the cumulative fitted model or an integer to plot the isolated contribution of one latent variable.
  • response::Union{Nothing,Integer} = nothing Plot one response surface directly.
  • response_contrast::Union{Nothing,Tuple{Int,Int}} = nothing Plot a signed contrast positive - negative between two responses.

PlotlyJS backend keywords

  • sample_size::Integer = 50_000 Maximum number of coefficient values used when estimating the robust color range.
  • iqr_multiplier::Real = 8.0 Multiplier for the IQR fence used in the robust color range.
  • clip_quantile::Real = 0.995 High quantile of abs.(coef) used as a lower bound on the clipping limit.
  • colorscale::AbstractString = "RdBu"
  • colorbar_title::AbstractString = "Coefficient"
  • hovertemplate::Union{Nothing,AbstractString} = nothing
  • title::Union{Nothing,AbstractString} = nothing
  • layout = nothing
  • plot_kwargs = (;)
source

The figure below compares two latent-variable weight surfaces with two coefficient surfaces from the regression fit.

W_lv1 = weightlandscape(mf_reg; lv=1)
W_lv2 = weightlandscape(mf_reg; lv=2)
B_trait1 = coefficientlandscape(mf_reg; response=1)
B_contrast = coefficientlandscape(mf_reg; response_contrast=(2, 1))

weight_lim = maximum(abs.(vcat(vec(W_lv1), vec(W_lv2))))
coef_lim = maximum(abs.(vcat(vec(B_trait1), vec(B_contrast))))

fig_surfaces = Figure(size=(1300, 850))

ax_w1 = Axis(
    fig_surfaces[1, 1],
    title="LV1 weight landscape",
    xlabel=rt_axis.name,
    ylabel=mz_axis.name,
)
heatmap!(
    ax_w1,
    rt_axis.values,
    mz_axis.values,
    W_lv1;
    colormap=:RdBu,
    colorrange=(-weight_lim, weight_lim),
)

ax_w2 = Axis(
    fig_surfaces[1, 2],
    title="LV2 weight landscape",
    xlabel=rt_axis.name,
    ylabel=mz_axis.name,
)
heatmap!(
    ax_w2,
    rt_axis.values,
    mz_axis.values,
    W_lv2;
    colormap=:RdBu,
    colorrange=(-weight_lim, weight_lim),
)

ax_b1 = Axis(
    fig_surfaces[2, 1],
    title="$(data.responselabels_reg[1]) coefficient landscape",
    xlabel=rt_axis.name,
    ylabel=mz_axis.name,
)
heatmap!(
    ax_b1,
    rt_axis.values,
    mz_axis.values,
    B_trait1;
    colormap=:RdBu,
    colorrange=(-coef_lim, coef_lim),
)

ax_bc = Axis(
    fig_surfaces[2, 2],
    title="$(data.responselabels_reg[2]) - $(data.responselabels_reg[1])",
    xlabel=rt_axis.name,
    ylabel=mz_axis.name,
)
heatmap!(
    ax_bc,
    rt_axis.values,
    mz_axis.values,
    B_contrast;
    colormap=:RdBu,
    colorrange=(-coef_lim, coef_lim),
)

Colorbar(
    fig_surfaces[1, 3],
    limits=(-weight_lim, weight_lim),
    colormap=:RdBu,
    label="Weight",
)

Colorbar(
    fig_surfaces[2, 3],
    limits=(-coef_lim, coef_lim),
    colormap=:RdBu,
    label="Coefficient",
)

save("visualization_surfaces.svg", fig_surfaces)

The interactive plot wrappers build these views directly from the fitted model. For combined weight summaries across all components, absolute combinations such as combine=:meanabs are often easier to interpret than signed sums because latent-variable signs are arbitrary.

weight_plotly = weightlandscapeplot(
    mf_reg;
    lv=:combined,
    combine=:meanabs,
    title="Combined weight landscape (meanabs)",
)

coef_plotly = coefflandscapeplot(
    mf_reg;
    response_contrast=(2, 1),
    title="Coefficient landscape: trait2 - trait1",
)

PlotlyJS.savefig(weight_plotly, "visualization_weight_landscape.html")
PlotlyJS.savefig(coef_plotly, "visualization_coefficient_landscape.html")

Open the interactive combined weight landscape

Open the interactive coefficient landscape

Multilinear Weight Profiles

When multilinear=true, NCPLS also stores one loading-weight vector per predictor mode and per component. weightprofiles returns those vectors directly, whereas weightprofilesplot stacks them into one Plotly figure with one subplot per predictor axis.

This view is often easier to interpret than a 2D surface when the main question is how each individual mode contributes across its own coordinate system.

NCPLS.weightprofilesFunction
weightprofiles(
    mf::NCPLSFit;
    lv::Union{Symbol, Integer}=1,
    combine::Symbol=:sum,
)

Return the multilinear loading-weight vectors stored in W_modes.

  • lv = k::Integer returns the per-axis vectors for latent variable k.
  • lv = :combined or :all combines the per-axis vectors across all components using combine.

Supported combination rules are :sum, :mean, :sumabs, and :meanabs.

source
NCPLS.weightprofilesplotFunction
weightprofilesplot(mf; backend=:plotly, kwargs...)

Backend dispatcher for NCPLS multilinear weight-profile plots. These plots require a multilinear fit with stored W_modes.

General keywords

  • backend::Symbol = :plotly Select the plotting backend. Supported values: :plotly.
  • lv::Union{Symbol,Integer} = 1 Use an integer for one component or :combined / :all for an aggregate across all stored components.
  • combine::Symbol = :sum Combination rule for lv = :combined. Supported values: :sum, :mean, :sumabs, and :meanabs.

PlotlyJS backend keywords

  • title::Union{Nothing,AbstractString} = nothing
  • layout = nothing
  • line_kwargs = (;)
  • zero_line::Bool = true
  • plot_kwargs = (;)
source

Here we compare the mode-specific profiles for LV1 and LV2 and add a combined absolute summary across all stored components.

profiles_lv1 = weightprofiles(mf_reg; lv=1)
profiles_lv2 = weightprofiles(mf_reg; lv=2)
profiles_all = weightprofiles(mf_reg; lv=:combined, combine=:meanabs)

fig_profiles = Figure(size=(1200, 450))

for (j, axis_meta) in enumerate(data.predictoraxes)
    ax = Axis(
        fig_profiles[1, j],
        title="$(axis_meta.name) weight profiles",
        xlabel=axis_meta.name,
        ylabel="Weight",
    )
    lines!(
        ax,
        axis_meta.values,
        zeros(length(axis_meta.values));
        color=:gray60,
        linestyle=:dash,
    )
    lines!(ax, axis_meta.values, profiles_lv1[j], color=blue, label="LV1")
    lines!(ax, axis_meta.values, profiles_lv2[j], color=orange, label="LV2")
    lines!(
        ax,
        axis_meta.values,
        profiles_all[j];
        color=:black,
        linestyle=:dot,
        label="meanabs all",
    )
    axislegend(ax, position=:rt)
end

save("visualization_weightprofiles.svg", fig_profiles)

profiles_plotly = weightprofilesplot(
    mf_reg;
    lv=:combined,
    combine=:meanabs,
    title="Combined weight profiles (meanabs)",
)

PlotlyJS.savefig(profiles_plotly, "visualization_weightprofiles.html")

Open the interactive weight profiles

API