knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 5 )
This vignette demonstrates NMF-RE (Non-negative Matrix Factorization with Random Effects), a mixed-effects extension of NMF implemented in the nmfkc package.
Standard NMF with covariates models the data as: $$Y \approx X \Theta A$$
where $\Theta A$ captures systematic (fixed) effects of covariates $A$ on latent scores. However, in many applications --- such as longitudinal studies, panel data, or clustered observations --- individuals exhibit unit-specific deviations that cannot be explained by covariates alone.
NMF-RE addresses this by adding random effects $U$: $$Y = X(\Theta A + U) + \mathcal{E}$$
C in the package output).The random effects follow $\mathrm{Var}(\mathrm{vec}(U)) = \tau^2 I$, and the penalty $\lambda = \sigma^2 / \tau^2$ determines the degree of shrinkage. The marginal model is $$\boldsymbol y_n \sim N!\big(X\Theta\boldsymbol a_n,\; \tau^2 X X^\top + \sigma^2 I\big).$$
C.signed (see below).C.signedThe single argument C.signed selects the whole estimation scheme:
C.signed = TRUE (default, recommended): the coefficients $\Theta$ are treated as real-valued (sign-free), updated by exact least squares. Inference uses a two-sided test (interior null $H_0: \Theta_{qk} = 0$). This is the natural choice when covariate effects can be positive or negative.C.signed = FALSE: $\Theta$ is constrained to be non-negative, updated by a multiplicative update, with a one-sided test (boundary null $H_0: \Theta_{qk} = 0$ vs $H_1: \Theta_{qk} > 0$). Use this for compositional or intensity-type scores.In both cases the basis $X$ is always non-negative.
We use the Orthodont dataset from the nlme package: orthodontic distance measurements for 27 children at ages 8, 10, 12, and 14. The covariate of interest is sex (Male/Female).
library(nmfkc) library(nlme) data(Orthodont) head(Orthodont)
Each column of $Y$ is a subject, each row is a time point (age). Since there are 4 ages and 27 subjects, $Y$ is $4 \times 27$.
Y <- matrix(Orthodont$distance, nrow = 4, ncol = 27) colnames(Y) <- paste0("S", 1:27) rownames(Y) <- paste("Age", c(8, 10, 12, 14)) Y[, 1:6]
We create a $2 \times 27$ covariate matrix with an intercept row and a binary male indicator.
male <- ifelse(Orthodont$Sex[seq(1, 108, 4)] == "Male", 1, 0) A <- rbind(intercept = 1, male = male) A[, 1:6]
We fit the model with rank = 1 (a single latent growth trend). No tuning parameter is required: the variance components $(\sigma^2, \tau^2)$, and hence the penalty $\lambda$, are estimated by the EM/ECM algorithm. We keep the default C.signed = TRUE, since the male effect on growth could in principle have either sign. The prefix argument labels the basis.
res <- nmfre(Y, A, rank = 1, prefix = "Trend")
nmfre() performs optimization only. summary() on a freshly fitted
object shows the variance components, fit statistics, and the $\mathrm{df}_U$
diagnostic; standard errors and p-values for $\Theta$ are added in a separate
inference step (Section 2.2), mirroring the nmfkc() / nmfkc.inference()
split.
summary(res)
Variance components:
res$dfU.frac). No cap is imposed.cat("sigma^2 =", round(res$sigma2, 4), "\n") cat("tau^2 =", round(res$tau2, 4), "\n") cat("lambda =", round(res$lambda, 5), "\n") cat("dfU =", round(res$dfU, 2), " dfU/(NQ) =", round(res$dfU.frac, 4), "\n")
R-squared:
nmfre.inference()To obtain standard errors, z-values, p-values, and confidence intervals for
$\Theta$, pass the fit to nmfre.inference() (sandwich SE + wild bootstrap,
conditional on the estimated $\hat X, \hat U$). It returns the same object with
the inference fields added, so summary() now prints the coefficient table.
res <- nmfre.inference(res, Y, A, wild.B = 1000) summary(res)
Coefficient table ($\Theta$):
C.signed = TRUE, this is a two-sided p-value ($H_0: \Theta_{qk} = 0$). Both the intercept and the male effect are detected as statistically significant.The sidedness follows the fit: sign-free $\Theta$ (C.signed = TRUE) uses a
two-sided test, while the non-negative variant (C.signed = FALSE) uses a
one-sided (boundary) test.
We plot the growth curves to see how the model captures both population-level trends (fixed effects) and individual deviations (random effects).
age <- c(8, 10, 12, 14) plot(age, res$XB[, 1], type = "n", ylim = range(Y), xlab = "Age (years)", ylab = "Distance (mm)", main = "Orthodont: NMF-RE Growth Curves") # Plot observed data points for (j in 1:27) { pch_j <- ifelse(male[j] == 1, 4, 1) points(age, Y[, j], pch = pch_j, col = "gray60") } # Plot individual predictions (fixed + random effects) for (j in 1:27) { lines(age, res$XB.blup[, j], col = "steelblue", lty = 3, lwd = 0.8) } # Plot population-level fixed effects (two lines: male and female) for (j in 1:27) { lines(age, res$XB[, j], col = "red", lwd = 2) } legend("topleft", legend = c("Fixed effect (male/female)", "Fixed + Random (BLUP)", "Male (observed)", "Female (observed)"), lwd = c(2, 1, NA, NA), lty = c(1, 3, NA, NA), pch = c(NA, NA, 4, 1), col = c("red", "steelblue", "gray60", "gray60"), cex = 0.85)
The basis $X$ represents the latent temporal pattern shared across all subjects.
cat("Basis X (temporal pattern):\n") print(round(res$X, 4))
Since rank = 1, there is one basis vector. Its shape shows how the latent factor manifests across the four ages. The column is normalized to sum to 1.
$\Theta$ (the C element) maps covariates to latent scores:
cat("Coefficient matrix (Theta):\n") print(round(res$C, 4))
intercept: Baseline level of the latent trend for females.male: Additional contribution for males. A significant value indicates that males have higher orthodontic distances on average.$U$ captures individual deviations. Subjects with positive $U$ values grow faster than the population average; negative values indicate slower growth.
barplot(res$U[1, ], names.arg = colnames(Y), las = 2, cex.names = 0.7, col = ifelse(male == 1, "steelblue", "salmon"), main = "Random Effects (U) by Subject", ylab = "Random effect value") legend("topright", fill = c("steelblue", "salmon"), legend = c("Male", "Female"), cex = 0.85)
Check that the algorithm converged properly:
cat("Converged:", res$converged, "\n") cat("Iterations:", res$iter, "\n") cat("Stop reason:", res$stop.reason, "\n")
plot() shows the marginal negative log-likelihood $\ell(X,\Theta,\sigma^2,\tau^2)$
(random effects integrated out), which the ECM algorithm decreases
monotonically. Note that the fixed-$\lambda$ penalized objective
(res$objfunc.iter) is not monotone across outer iterations --- it jumps
each time $\lambda = \sigma^2/\tau^2$ is updated --- so it is unsuitable for
illustrating convergence.
plot(res, main = "Convergence (marginal NLL)")
Compare the fitted values against the original data:
residuals <- Y - res$XB.blup cat("Mean absolute residual (BLUP):", round(mean(abs(residuals)), 4), "\n") cat("Mean absolute residual (fixed):", round(mean(abs(Y - res$XB)), 4), "\n") # Fitted vs Observed plot(as.vector(Y), as.vector(res$XB.blup), xlab = "Observed", ylab = "Fitted (BLUP)", main = "Observed vs Fitted", pch = 16, col = "steelblue") abline(0, 1, col = "red", lwd = 2)
To appreciate the value of random effects, compare NMF-RE with a standard NMF covariate model (no random effects).
# Standard NMF with covariates (no random effects) res_fixed <- nmfkc(Y, A = A, rank = 1) cat("=== Standard NMF (fixed effects only) ===\n") cat("R-squared:", round(1 - sum((Y - res_fixed$XB)^2) / sum((Y - mean(Y))^2), 4), "\n\n") cat("=== NMF-RE (fixed + random effects) ===\n") cat("R-squared (XB): ", round(res$r.squared.fixed, 4), "\n") cat("R-squared (XB+blup): ", round(res$r.squared, 4), "\n") cat("ICC: ", round(res$ICC, 4), "\n")
The improvement from fixed-only $R^2$ to BLUP $R^2$ quantifies the contribution of individual random effects.
Because nmfre() does not run the bootstrap, fitting is cheap --- useful when
the model is fitted many times (rank selection, cross-validation). Run
nmfre.inference() only on the final model, and re-run it (without re-fitting)
to try different settings such as the number of bootstrap replicates,
confidence level, or p-value sidedness:
# coefficient table from the inference object built in Section 2.2 res$coefficients[, c("Basis", "Covariate", "Estimate", "SE", "p_value")]
nmfkc.DOT()The inference result is compatible with nmfkc.DOT() for graph visualization. Edges are filtered by significance level and decorated with stars:
dot <- nmfkc.DOT(res, type = "YXA", sig.level = 0.05) plot(dot)
C.signed = FALSE)When the latent scores are compositional or intensity-type and the covariate effects must be non-negative, set C.signed = FALSE. The basis is then updated by a positive-part multiplicative rule and inference switches to a one-sided (boundary) test.
res_nn <- nmfre(Y, A, rank = 1, prefix = "Trend", C.signed = FALSE) res_nn <- nmfre.inference(res_nn, Y, A, wild.B = 500) cat("All Theta >= 0 :", all(res_nn$C >= 0), "\n") cat("P-value side :", res_nn$C.p.side, "\n") res_nn$coefficients[, c("Basis", "Covariate", "Estimate", "SE", "p_value")]
For the Orthodont data the male effect is positive, so the sign-free and non-negative fits agree on its direction; the difference matters when an effect is genuinely negative (the non-negative variant clips it to zero).
NMF-RE provides a principled way to model individual heterogeneity in NMF:
| Step | Function | Purpose |
|:---|:---|:---|
| 1 | nmfre() | Fit the mixed-effects model (optimization only; variances estimated by EM/ECM) |
| 2 | nmfre.inference() | Standard errors, p-values, and CIs for $\Theta$ |
| 3 | summary() | Examine variance components and the coefficient table |
| 4 | nmfkc.DOT() | Visualize with significance stars |
When to use NMF-RE:
Choosing C.signed: keep the default TRUE (sign-free, two-sided) when covariate effects can be positive or negative; use FALSE (non-negative, one-sided) for compositional or intensity scores.
For more details on the underlying methodology, see:
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.