---
title: "Supervised NDRLM: latent-variable regression and optimization"
author: "Zsolt T. Kosztyan, Marcell T. Kurbucz, Attila I. Katona, and Zahid Khan"
bibliography: references.bib
link-citations: true
output:
rmarkdown::html_vignette:
toc: true
number_sections: true
css: vignette.css
vignette: >
%\VignetteIndexEntry{Supervised NDRLM: latent-variable regression and optimization}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.width = 8,
fig.height = 5.5,
warning = FALSE,
message = FALSE
)
library(nda)
set.seed(42)
```
# What `ndrlm()` adds
`ndrlm()` combines network-based dimensionality reduction with one supervised
model per dependent variable. It can reduce the input side, the output side,
both sides, or neither; retain excluded indicators as direct connections; fit
OLS, ridge, elastic-net, or LOESS models; and tune NDA weights and thresholds
with NSGA-II.
1. ReduceObserved indicators form input and/or output latent variables.
2. RegressLatent and direct variables enter the selected regression engine.
3. EvaluateMetrics, predictions, residuals, paths, and optimization history are retained.
NDRLM is predictive and exploratory. A fitted path is not, by itself, evidence
of a causal effect. Its role as a data-driven benchmark for structural models
is developed in @Kosztyan2026NBSEM.
# Published latent-input example
The example follows the article's low-sample-size `mtcars` analysis. The ten
input indicators form two communities. The first groups `cyl`, `hp`, `qsec`,
`vs`, and `carb`; the second groups `disp`, `drat`, `wt`, `am`, and `gear`.
The two resulting input latent variables predict `mpg`.
```{r latent-input-fit}
X_car <- mtcars[, setdiff(names(mtcars), "mpg")]
Y_car <- mtcars["mpg"]
input_membership <- c(
cyl = 1, disp = 2, hp = 1, drat = 2, wt = 2,
qsec = 1, vs = 1, am = 2, gear = 2, carb = 1
)
fit_latent <- ndrlm(
Y_car, X_car,
latents = "in",
optimize = FALSE,
membership.X = input_membership,
regression = "lm",
seed = 42
)
fit_latent
summary(fit_latent)
```
The NDA object remains available inside the supervised model, so the
measurement structure is inspectable rather than hidden.
```{r latent-input-results}
data.frame(
indicator = colnames(fit_latent$NDAin$R),
community = unname(fit_latent$NDAin$membership)
)
round(fit_latent$NDAin$loadings, 3)
coef(fit_latent$fits[[1]])
fit_latent$fit_stats
```
The latent scores are computed only from their own community indicators. The
OLS coefficients then describe the fitted relationship between each input
latent variable and `mpg`.
# Predictions and residual diagnostics
```{r latent-prediction}
prediction <- predict(fit_latent, X_car)
diagnostic <- data.frame(
observed = Y_car$mpg,
fitted = fitted(fit_latent)$mpg,
residual = residuals(fit_latent)$mpg
)
head(prediction)
head(diagnostic)
```
```{r diagnostic-figure, fig.cap="Observed versus fitted fuel efficiency and residual diagnostic for the latent-input model."}
op <- par(mfrow = c(1, 2), mar = c(4, 4, 3, 1))
plot(diagnostic$observed, diagnostic$fitted,
pch = 19, col = "#0072B2",
xlab = "Observed mpg", ylab = "Fitted mpg",
main = "Observed versus fitted")
abline(0, 1, col = "grey45", lty = 2)
plot(diagnostic$fitted, diagnostic$residual,
pch = 19, col = "#009E73",
xlab = "Fitted mpg", ylab = "Residual",
main = "Residual pattern")
abline(h = 0, col = "grey45", lty = 2)
par(op)
```
These plots are descriptive in-sample diagnostics. A predictive study should
evaluate the complete reduction-and-regression pipeline using held-out data or
resampling.
# Input, output, and full latent models
| `latents` | Predictors used by regression | Responses used by regression |
|---|---|---|
| `"none"` | observed `X` | observed `Y` |
| `"in"` | input NDA scores | observed `Y` |
| `"out"` | observed `X` | output NDA scores |
| `"both"` | input NDA scores | output NDA scores |
The next executable example reduces both sides. Two observed response
indicators form one output latent variable. With `dircon = TRUE`, membership
zero would retain an excluded indicator as a direct observed variable.
```{r both-sides-fit}
X_both <- mtcars[, c("cyl", "disp", "hp", "wt", "qsec", "vs", "am", "gear")]
Y_both <- mtcars[, c("mpg", "drat")]
fit_both <- ndrlm(
Y_both, X_both,
latents = "both",
optimize = FALSE,
membership.X = c(1, 2, 1, 2, 1, 1, 2, 2),
membership.Y = c(1, 1),
regression = "lm",
seed = 42
)
fit_both$NDAin$membership
fit_both$NDAout$membership
fit_both$indep_names
fit_both$dep_names
head(fitted(fit_both))
```
# Regression engines with visible results
The enclosing NDRLM interface is the same for every engine. OLS provides
coefficient tests and intervals. Ridge stabilizes correlated predictors.
Elastic net combines shrinkage with coefficient selection. LOESS provides a
small-predictor nonlinear model but no linear-path significance tests.
```{r engine-comparison}
fit_ols <- fit_latent
if (requireNamespace("glmnet", quietly = TRUE)) {
fit_ridge <- ndrlm(
Y_car, X_car,
latents = "in", optimize = FALSE,
membership.X = input_membership,
regression = "ridge", lambda = 0.05,
seed = 42
)
fit_elastic <- ndrlm(
Y_car, X_car,
latents = "in", optimize = FALSE,
membership.X = input_membership,
regression = "elastic_net", alpha = 0.5, lambda = 0.05,
seed = 42
)
engine_results <- rbind(
OLS = fit_ols$fit_stats,
Ridge = fit_ridge$fit_stats,
Elastic_net = fit_elastic$fit_stats
)
engine_results
round(cbind(
ridge = fit_ridge$fits[[1]]$coefficients,
elastic_net = fit_elastic$fits[[1]]$coefficients
), 3)
} else {
cat("Install the suggested package 'glmnet' to run ridge and elastic net.\n")
}
```
```{r loess-fit}
fit_loess <- ndrlm(
Y_car,
X_car["wt"],
latents = "none",
regression = "loess",
loess_span = 0.80
)
head(predict(fit_loess, X_car["wt"]))
fit_loess$fit_stats
```
Raw coefficients from different engines or differently scaled predictors are
not standardized effect-size comparisons. Compare prediction metrics on a
common validation design when prediction is the scientific goal.
# NSGA-II optimization and convergence history
When `optimize = TRUE`, NSGA-II tunes indicator weights and the four NDA
thresholds. This deliberately small run demonstrates the complete interface.
Larger populations, more generations, and repeated runs are appropriate for a
final analysis.
```{r optimization}
fit_optimized <- ndrlm(
Y_car, X_car,
latents = "in",
membership.X = input_membership,
optimize = TRUE,
target = "adj.r.square",
rel_weight = TRUE,
lower.bounds.x = rep(0.5, ncol(X_car)),
upper.bounds.x = rep(1.5, ncol(X_car)),
lower.bounds.latentx = c(0, 0, 0, 0),
upper.bounds.latentx = c(0.01, 0.01, 0.01, 0.01),
popsize = 4,
generations = 3,
restarts = 1,
regression = "lm",
seed = 42
)
fit_optimized$optimized
fit_optimized$fval
round(fit_optimized$NDAin_weight, 3)
tail(fit_optimized$NSGA_trace)
```
```{r convergence-figure, fig.cap="Best adjusted R-squared encountered during the compact NSGA-II run."}
trace <- fit_optimized$NSGA_trace
plot(
trace$evaluation,
trace$best_target,
type = "s", lwd = 2, col = "#0072B2",
xlab = "Objective evaluation",
ylab = "Best adjusted R-squared",
main = "NSGA-II search history"
)
points(trace$evaluation, trace$target_value,
pch = 16, cex = 0.65, col = grDevices::adjustcolor("grey35", 0.45))
lines(trace$evaluation, trace$best_target, lwd = 2, col = "#0072B2")
```
The grey points are evaluated candidates; the blue step line is the best
target value encountered so far. A short flat segment is not proof of global
convergence, but the trace makes search progress transparent and helps compare
longer runs and restarts.
# Latent path network
```{r ndrlm-static-network, fig.cap="Latent-input NDRLM graph. Rectangles are observed indicators, circles are latent variables, and path colour carries sign."}
path_network <- plot(
fit_latent,
sig = 1,
interactive = FALSE,
show_edge_labels = TRUE,
shape = "automatic"
)
path_network
```
Observed variables are rectangles and latent variables are circles, following
standard structural-model notation. Each latent variable and its observed
indicators share one community colour. Measurement loadings are grey. Positive
regression paths are green, negative paths are red, and absolute magnitude is
mapped to width separately within loadings and structural paths. All lines are
solid by default. Colours, line types, shapes,
and the minimum/maximum width can be overridden explicitly when a publication
requires another convention.
# References
The references connect the original and generalized NDA methods to the package
implementation and the supervised network-based structural extension
[@Kosztyan2022; @Kosztyan2023; @Kosztyan2024; @Kosztyan2026Packages;
@Kosztyan2026NBSEM].
::: {#refs}
:::