| Title: | Multi-Task Learning with Orthogonal Constraints |
|---|---|
| Description: | Fits regularised multi-task learning models where relationships between tasks are controlled via orthogonality or disjoint-support constraints. Supports regression, binary classification, and censored survival data. In survival mode, time-to-event outcomes are converted into binary labels at user-defined thresholds, enabling the discovery of features with time-varying effects that standard proportional-hazards models cannot detect. Implements the penalty described in Vervier et al. (2014) <https://hal.science/hal-00985654>. |
| Authors: | Kevin Vervier [aut, cre], Novartis Pharma AG [cph, fnd] |
| Maintainer: | Kevin Vervier <[email protected]> |
| License: | GPL-3 |
| Version: | 0.1.0 |
| Built: | 2026-09-22 09:02:33 UTC |
| Source: | https://github.com/cran/orthoMTL |
Pooled fraction of correctly classified non-NA cells, for
orthoMTL fits in classification mode (logistic = TRUE).
A cell is positive when its true label is > 0 and predicted
positive when its score is > 0. This works for both the
encoding the solver optimises and a
encoding, and for raw "link" scores or "response"
probabilities (threshold 0.5 corresponds to a link of 0... see note).
accuracy_mtl(true.label.mat, pred.label.mat, threshold = 0)accuracy_mtl(true.label.mat, pred.label.mat, threshold = 0)
true.label.mat |
A numeric matrix of true responses
( |
pred.label.mat |
A numeric matrix of predictions of the same
dimensions (as produced by |
threshold |
Decision threshold applied to |
A single numeric value in [0, 1] (higher is better).
set.seed(1) Y <- matrix(sample(c(-1, 1), 30, replace = TRUE), 10, 3) P <- Y * abs(matrix(rnorm(30), 10, 3)) # mostly correct signs accuracy_mtl(Y, P)set.seed(1) Y <- matrix(sample(c(-1, 1), 30, replace = TRUE), 10, 3) P <- Y * abs(matrix(rnorm(30), 10, 3)) # mostly correct signs accuracy_mtl(Y, P)
Pooled AUC over every non-NA cell, computed via the
Mann-Whitney U statistic (with 0.5 credit for ties), for
orthoMTL fits in classification mode. The positive class is
defined by true label > 0. AUC is invariant to monotone
transforms, so raw "link" scores and "response"
probabilities give identical results.
auc_mtl(true.label.mat, pred.label.mat)auc_mtl(true.label.mat, pred.label.mat)
true.label.mat |
A numeric matrix of true responses
( |
pred.label.mat |
A numeric matrix of predictions of the same
dimensions (as produced by |
A single numeric value in [0, 1] (higher is better;
0.5 = random).
set.seed(1) Y <- matrix(sample(c(-1, 1), 30, replace = TRUE), 10, 3) P <- Y + matrix(rnorm(30), 10, 3) auc_mtl(Y, P)set.seed(1) Y <- matrix(sample(c(-1, 1), 30, replace = TRUE), 10, 3) P <- Y + matrix(rnorm(30), 10, 3) auc_mtl(Y, P)
Estimates coefficient variability and statistical relevance by comparing bootstrapped models (real signal) against null models (permuted outcomes). This two-pronged approach answers: (1) how stable is each coefficient across resamples, and (2) is each coefficient distinguishable from what would be obtained by chance.
bootstrap_orthoMTL( X, Y, lambda = 1, alpha = 0, step_size = 0.1, K = NULL, disjoint = FALSE, schedule = c("sqrt", "log", "const", "linear"), survival = FALSE, censored.mat = NULL, n_repeats = 100, n_cores = 2, seed = NULL, verbose = TRUE )bootstrap_orthoMTL( X, Y, lambda = 1, alpha = 0, step_size = 0.1, K = NULL, disjoint = FALSE, schedule = c("sqrt", "log", "const", "linear"), survival = FALSE, censored.mat = NULL, n_repeats = 100, n_cores = 2, seed = NULL, verbose = TRUE )
X |
A numeric matrix of predictor variables with dimensions
|
Y |
A numeric matrix of response labels with dimensions
|
lambda |
Regularisation parameter for the orthogonal penalty. |
alpha |
Elastic-net mixing parameter in |
step_size |
Step size for gradient descent. Default: |
K |
Constraint matrix of dimensions
|
disjoint |
Logical. Enforce disjoint supports? Default:
|
schedule |
Character; the gradient-step decay schedule passed to
|
survival |
Logical. Use censored survival loss? Default:
|
censored.mat |
A numeric indicator matrix of dimensions
|
n_repeats |
Number of bootstrap/permutation repeats. Default:
|
n_cores |
Number of cores for parallel execution. Default:
|
seed |
Optional base random seed. Default: |
verbose |
Logical. Print progress information? Default:
|
Real bootstrap: For each repeat, rows of X, Y,
and censored.mat are resampled with replacement. The
model is refit with identical hyperparameters. This produces a
distribution of coefficient values reflecting estimation variability.
Null permutation: For each repeat, rows of Y and
censored.mat are permuted without replacement while
X remains fixed. This breaks the association between features
and outcomes, producing a null distribution of coefficients.
Comparing real vs null distributions for each feature and task
indicates whether observed coefficients are distinguishable from
noise. Visualise with plot_bootstrap.
An object of class "bootstrap_orthoMTL" containing:
A tidy data.frame with columns id
(feature name), time (task/threshold), coeff
(coefficient value), group ("real" or
"null"), and repeat_id (integer).
A list of n_repeats coefficient
matrices (each p x numTasks).
A list of n_repeats coefficient
matrices from permuted outcomes.
Numeric vector of final objective values for real bootstrap models.
Numeric vector of final objective values for null permutation models.
Number of repeats.
Number of features.
Number of tasks.
Character vector of feature names.
Character vector of task names.
The matched function call.
set.seed(42) n <- 30; p <- 5; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) SurvTime <- rexp(n, rate = 0.1) Event <- rbinom(n, 1, 0.7) thresholds <- c(4, 6, 10) Y <- create_longitudinal_labels(SurvTime, Event, thresholds) W <- create_indicator_matrix(Y) K <- create_constraint_matrix(n_tasks) boot_res <- bootstrap_orthoMTL( X = X, Y = Y, lambda = 1e-3, step_size = 0.1, K = K, survival = TRUE, censored.mat = W, n_repeats = 5, n_cores = 1, verbose = FALSE ) print(boot_res) head(boot_res$results)set.seed(42) n <- 30; p <- 5; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) SurvTime <- rexp(n, rate = 0.1) Event <- rbinom(n, 1, 0.7) thresholds <- c(4, 6, 10) Y <- create_longitudinal_labels(SurvTime, Event, thresholds) W <- create_indicator_matrix(Y) K <- create_constraint_matrix(n_tasks) boot_res <- bootstrap_orthoMTL( X = X, Y = Y, lambda = 1e-3, step_size = 0.1, K = K, survival = TRUE, censored.mat = W, n_repeats = 5, n_cores = 1, verbose = FALSE ) print(boot_res) head(boot_res$results)
Computes a concordance index (C-index) adapted for the multi-task survival framework. Predictions across tasks are aggregated by row-sum to produce a single score per patient, then concordance is evaluated over pairs where at least one member has a fully observed outcome.
cindex_mtl(true.label.mat, pred.label.mat)cindex_mtl(true.label.mat, pred.label.mat)
true.label.mat |
A numeric matrix of true response labels with
dimensions |
pred.label.mat |
A numeric matrix of predicted response values
with dimensions |
The effective survival time for each patient is derived as the highest task index where the true label is positive (i.e., the last threshold at which the patient was progression-free).
A patient is considered "uncensored" only if all task labels are
non-NA. Concordant pairs require both a correct ordering
of effective survival times and a matching ordering of predicted
scores.
A numeric value between 0 and 1 (higher is better). A value of 0.5 indicates random concordance.
The following limitations are documented and flagged for future investigation:
No credit for tied predictions or tied survival times
Strict censoring: uncensored requires all tasks observed
Equal weighting of all tasks in the row-sum aggregation
predict.orthoMTL for generating the
prediction matrix.
# Simulate a small multi-task prediction scenario set.seed(42) n <- 30; n_tasks <- 4 SurvTime <- rexp(n, rate = 0.1) Event <- rbinom(n, 1, 0.7) thresholds <- c(4, 6, 10, 15) Y <- create_longitudinal_labels(SurvTime, Event, thresholds) # Simulate imperfect predictions (add noise to true labels) pred <- Y pred[is.na(pred)] <- 0.5 pred <- pred + matrix(rnorm(n * n_tasks, sd = 0.3), n, n_tasks) cindex_mtl(Y, pred)# Simulate a small multi-task prediction scenario set.seed(42) n <- 30; n_tasks <- 4 SurvTime <- rexp(n, rate = 0.1) Event <- rbinom(n, 1, 0.7) thresholds <- c(4, 6, 10, 15) Y <- create_longitudinal_labels(SurvTime, Event, thresholds) # Simulate imperfect predictions (add noise to true labels) pred <- Y pred[is.na(pred)] <- 0.5 pred <- pred + matrix(rnorm(n * n_tasks, sd = 0.3), n, n_tasks) cindex_mtl(Y, pred)
Returns the coefficient matrix from a fitted orthoMTL model.
## S3 method for class 'orthoMTL' coef(object, ...)## S3 method for class 'orthoMTL' coef(object, ...)
object |
A fitted model object of class |
... |
Additional arguments (currently ignored). |
A numeric matrix of regression coefficients with dimensions
n_features x n_tasks. Row names correspond to feature names
and column names to task names, if available.
set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 K <- matrix(1, n_tasks, n_tasks); diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K) coef(fit)set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 K <- matrix(1, n_tasks, n_tasks); diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K) coef(fit)
Builds a square constraint matrix K encoding the prior belief
that temporally distant tasks should have more orthogonal coefficients.
Off-diagonal entries accumulate weight proportional to the distance
between task indices, implementing a diffusion-like pattern.
create_constraint_matrix(numTasks, diag_val = 0.5)create_constraint_matrix(numTasks, diag_val = 0.5)
numTasks |
An integer specifying the number of tasks. |
diag_val |
A numeric value for the diagonal of |
This matrix is passed to orthoMTL via the K
argument to control the orthogonality penalty between tasks.
The construction rule: for each distance level h from
1 to numTasks, add 1 to all entries where
|row - col| > h. This produces a matrix where nearby tasks
(adjacent thresholds) share more support, while distant tasks
are pushed toward orthogonality.
For survival analysis with time thresholds, this encodes the assumption that the set of predictive features changes gradually over time rather than abruptly.
A numeric square matrix of dimensions
numTasks x numTasks. Off-diagonal entry K[i,j]
is larger when tasks i and j are further apart.
Diagonal entries are set to diag_val.
# 5-task constraint matrix K <- create_constraint_matrix(5) K # Override diagonal for a specific penalty balance K <- create_constraint_matrix(7, diag_val = 6) K# 5-task constraint matrix K <- create_constraint_matrix(5) K # Override diagonal for a specific penalty balance K <- create_constraint_matrix(7, diag_val = 6) K
Converts NA entries in a longitudinal label matrix (as produced
by create_longitudinal_labels) into a binary indicator
matrix. This indicator is used by orthoMTL to mask
censored observations in the loss computation.
create_indicator_matrix(Y)create_indicator_matrix(Y)
Y |
A numeric matrix of dimensions |
A numeric matrix of the same dimensions as Y.
1 = label is observed (known), 0 = label is censored
(unknown).
set.seed(42) SurvTime <- rexp(10, rate = 0.1) Event <- rbinom(10, 1, 0.7) Y <- create_longitudinal_labels(SurvTime, Event, c(4, 6, 10)) W <- create_indicator_matrix(Y) W # 1 = observed, 0 = censoredset.seed(42) SurvTime <- rexp(10, rate = 0.1) Event <- rbinom(10, 1, 0.7) Y <- create_longitudinal_labels(SurvTime, Event, c(4, 6, 10)) W <- create_indicator_matrix(Y) W # 1 = observed, 0 = censored
Transforms time-to-event survival data into a matrix of binary labels at user-defined time thresholds. This is the core data transformation that enables survival analysis within the multi-task learning framework.
create_longitudinal_labels(SurvTime, Event, thresholds = c(4, 6))create_longitudinal_labels(SurvTime, Event, thresholds = c(4, 6))
SurvTime |
A numeric vector of length |
Event |
A numeric vector of length |
thresholds |
A numeric vector of length |
The encoding logic for each patient at each threshold is:
1 — patient is progression-free at this threshold
(survival time exceeds threshold, regardless of event status)
0 — patient experienced an event before this threshold
(survival time < threshold AND event observed)
NA — patient was censored before this threshold
(survival time < threshold AND no event observed); label is unknown
A numeric matrix of dimensions n x numTasks. Column
names are set to the threshold values. Contains 1, 0,
and NA values as described above.
create_indicator_matrix to convert NA
values into a binary censoring indicator matrix.
# Simulate 10 patients set.seed(42) SurvTime <- rexp(10, rate = 0.1) Event <- rbinom(10, 1, 0.7) thresholds <- c(4, 6, 10, 15) Y <- create_longitudinal_labels(SurvTime, Event, thresholds) Y # 1 = progression-free, 0 = event, NA = censored# Simulate 10 patients set.seed(42) SurvTime <- rexp(10, rate = 0.1) Event <- rbinom(10, 1, 0.7) thresholds <- c(4, 6, 10, 15) Y <- create_longitudinal_labels(SurvTime, Event, thresholds) Y # 1 = progression-free, 0 = event, NA = censored
Performs a parallelised grid search over hyperparameters for
orthoMTL, evaluating each configuration via
cross-validated concordance index. Returns the best configuration
without retraining a final model (that is the caller's responsibility).
cv_orthoMTL( X.train, Y.train, W.train = NULL, K = NULL, lambdas = c(0.001, 0.01), alphas = 0, stepsizes = c(0.1, 0.5), diag_vals = c(0.5, 1), survival = TRUE, logistic = FALSE, metric = NULL, disjoint = FALSE, schedule = c("sqrt", "log", "const", "linear"), folds = NULL, n_cores = 2, seed = NULL, verbose = TRUE )cv_orthoMTL( X.train, Y.train, W.train = NULL, K = NULL, lambdas = c(0.001, 0.01), alphas = 0, stepsizes = c(0.1, 0.5), diag_vals = c(0.5, 1), survival = TRUE, logistic = FALSE, metric = NULL, disjoint = FALSE, schedule = c("sqrt", "log", "const", "linear"), folds = NULL, n_cores = 2, seed = NULL, verbose = TRUE )
X.train |
A numeric matrix of training features with dimensions
|
Y.train |
A numeric matrix of training labels with dimensions
|
W.train |
A numeric indicator matrix of dimensions
|
K |
A square constraint matrix of dimensions
|
lambdas |
A numeric vector of regularisation parameters to search. |
alphas |
A numeric vector of elastic-net mixing parameters in
|
stepsizes |
A numeric vector of gradient descent step sizes to search. |
diag_vals |
A numeric vector of diagonal values for the
constraint matrix |
survival |
Logical. Use censored survival loss? Default:
|
logistic |
Logical. Fit logistic (classification) models?
Passed through to |
metric |
Character; the scoring metric maximised/minimised over
the grid, or |
disjoint |
Logical. Enforce disjoint supports? Default:
|
schedule |
Character; the gradient-step decay schedule passed to
|
folds |
An integer vector of length |
n_cores |
Integer. Number of cores for parallel execution.
Default: |
seed |
Optional integer random seed for reproducibility. Default:
|
verbose |
Logical. Print progress information? Default:
|
The grid is constructed as the full Cartesian product of
lambdas, alphas, stepsizes, and
diag_vals. Each configuration is evaluated independently
in parallel across cores. Within each configuration, folds are
evaluated sequentially and the per-fold C-indices are averaged.
The best configuration is selected by joint maximisation of the mean CV C-index over the entire flattened grid (not greedy sequential search).
This function does not retrain a final model. Use the
returned hyperparameters to train via orthoMTL.
An object of class "cv_orthoMTL" containing:
A list with the best hyperparameters: lambda,
alpha, stepsize, diag_val, and the
corresponding cv_score.
A data.frame of all configurations with
their mean CV C-index, sorted descending by cv_score.
The fold assignment vector used.
Total number of configurations tested.
Number of unique folds.
The matched function call.
set.seed(42) n <- 50; p <- 5; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) SurvTime <- rexp(n, rate = 0.1) Event <- rbinom(n, 1, 0.7) thresholds <- c(4, 6, 10) Y <- create_longitudinal_labels(SurvTime, Event, thresholds) W <- create_indicator_matrix(Y) K <- create_constraint_matrix(n_tasks) folds <- rep(1:2, length.out = n) cv_res <- cv_orthoMTL( X.train = X, Y.train = Y, W.train = W, K = K, lambdas = c(1e-3, 1e-2), alphas = 0, stepsizes = c(0.1), diag_vals = c(0.5, 1), survival = TRUE, disjoint = FALSE, folds = folds, n_cores = 1, seed = 42, verbose = FALSE ) print(cv_res) cv_res$bestset.seed(42) n <- 50; p <- 5; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) SurvTime <- rexp(n, rate = 0.1) Event <- rbinom(n, 1, 0.7) thresholds <- c(4, 6, 10) Y <- create_longitudinal_labels(SurvTime, Event, thresholds) W <- create_indicator_matrix(Y) K <- create_constraint_matrix(n_tasks) folds <- rep(1:2, length.out = n) cv_res <- cv_orthoMTL( X.train = X, Y.train = Y, W.train = W, K = K, lambdas = c(1e-3, 1e-2), alphas = 0, stepsizes = c(0.1), diag_vals = c(0.5, 1), survival = TRUE, disjoint = FALSE, folds = folds, n_cores = 1, seed = 42, verbose = FALSE ) print(cv_res) cv_res$best
Applies an isotonic regression-style projection to enforce that the
output vector is non-negative and non-increasing. Used internally
by predict.orthoMTL to ensure survival predictions
are monotonically decreasing across time thresholds.
nnmaxheap_C(m)nnmaxheap_C(m)
m |
A numeric vector to project. |
This function implements a pool-adjacent-violators style algorithm.
It replaces the external Iso package dependency used in the
predecessor orthopen package.
A numeric vector of the same length as m, projected
onto the non-negative non-increasing constraint space.
# Project a vector onto the non-negative non-increasing space nnmaxheap_C(c(3, 1, 2, -1)) # Already valid input: returned unchanged nnmaxheap_C(c(5, 3, 3, 1))# Project a vector onto the non-negative non-increasing space nnmaxheap_C(c(3, 1, 2, -1)) # Already valid input: returned unchanged nnmaxheap_C(c(5, 3, 3, 1))
This function solves a multi-task problem where relationships between tasks can be complex
orthoMTL( X, Y, lambda = 1, step_size = 0.1, tol = 1e-05, stop_no_improve = 100, max_iter = 1e+06, W_0 = NULL, seed = NULL, K = NULL, disjoint = FALSE, logistic = FALSE, alpha = 0, schedule = c("sqrt", "log", "const", "linear"), survival = FALSE, censored.mat = NULL, verbose = 0 )orthoMTL( X, Y, lambda = 1, step_size = 0.1, tol = 1e-05, stop_no_improve = 100, max_iter = 1e+06, W_0 = NULL, seed = NULL, K = NULL, disjoint = FALSE, logistic = FALSE, alpha = 0, schedule = c("sqrt", "log", "const", "linear"), survival = FALSE, censored.mat = NULL, verbose = 0 )
X |
a matrix of predictor variables with dimensions n x p |
Y |
a matrix of response variables with dimensions n x numTasks, where numTasks is the number of response variables. NAs can be used for censored data. |
lambda |
the regularization parameter for the OrthoPen penalty, default is 1 |
step_size |
the step size for updating the regression coefficients in gradient descent, default is 0.1 |
tol |
Convergence tolerance. The algorithm stops when the
improvement in the objective function is less than |
stop_no_improve |
the number of iterations without improvement in the objective function to trigger convergence, default is 100 |
max_iter |
the maximum number of iterations, default is 1e+06 |
W_0 |
a matrix of initial values for the regression coefficients, default is NULL, as not provided and will be randomly attributed |
seed |
an optional random seed for reproducibility, default is
|
K |
a constraint matrix of weights to adjust the OrthoPen penalty, default is an identity matrix with dimensions numTasks x numTasks |
disjoint |
a logical value indicating whether the response variables should have disjoint supports, default is FALSE |
logistic |
a logical value indicating whether logistic regression should be used instead of linear regression, default is FALSE |
alpha |
the elastic-net mixing parameter in |
schedule |
Character; the gradient-step decay schedule – how the
per-iteration scale grows with the iteration index
The default |
survival |
a logical value indicating whether survival analysis should be performed, default is FALSE |
censored.mat |
a matrix indicating whether observations are censored, used only if survival=TRUE |
verbose |
the level of verbosity, default is 0 (no messages) |
a list containing the following elements:
B |
a matrix of regression coefficients with dimensions p x numTasks |
obj |
the final objective function when algorithms stops |
imax |
the number of iterations |
Kevin Vervier, Pierre Mahé, Alexandre d’Aspremont, Jean-Baptiste Veyrieras, Jean-Philippe Vert (2014). On learning matrices with orthogonal columns or disjoint supports. https://hal.science/hal-00985654/file/learningDisjointSupports.pdf
# Regression with orthogonal columns set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) W_true <- qr.Q(qr(matrix(rnorm(p * n_tasks), p, n_tasks))) Y <- X %*% W_true + matrix(rnorm(n * n_tasks), n) * 0.1 K <- matrix(1, n_tasks, n_tasks) diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K, disjoint = FALSE) fit$B # coefficient matrix fit$converged # did optimisation converge?# Regression with orthogonal columns set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) W_true <- qr.Q(qr(matrix(rnorm(p * n_tasks), p, n_tasks))) Y <- X %*% W_true + matrix(rnorm(n * n_tasks), n) * 0.1 K <- matrix(1, n_tasks, n_tasks) diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K, disjoint = FALSE) fit$B # coefficient matrix fit$converged # did optimisation converge?
Displays faceted line plots comparing real (bootstrapped) vs null (permuted) coefficient trajectories across tasks for selected or all features. Each panel shows the mean and standard error of the coefficient at each task/threshold.
plot_bootstrap(x, features = NULL, batch_size = 9)plot_bootstrap(x, features = NULL, batch_size = 9)
x |
Either a |
features |
A character vector of feature names to plot. If
|
batch_size |
Number of features per facet page. Default:
|
Blue lines show coefficients from models fitted on bootstrapped (resampled) data — reflecting estimation variability under real signal. Grey lines show coefficients from models fitted on permuted outcomes — reflecting the null distribution.
When the blue (real) band is clearly separated from the grey (null) band, the feature's coefficient is distinguishable from noise at that task/threshold.
A list of ggplot2 objects, one per batch/page.
set.seed(42) n <- 30; p <- 5; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) SurvTime <- rexp(n, rate = 0.1) Event <- rbinom(n, 1, 0.7) thresholds <- c(4, 6, 10) Y <- create_longitudinal_labels(SurvTime, Event, thresholds) W <- create_indicator_matrix(Y) K <- create_constraint_matrix(n_tasks) boot_res <- bootstrap_orthoMTL( X = X, Y = Y, lambda = 1e-3, step_size = 0.1, K = K, survival = TRUE, censored.mat = W, n_repeats = 5, n_cores = 1, verbose = FALSE ) plots <- plot_bootstrap(boot_res) plots[[1]]set.seed(42) n <- 30; p <- 5; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) SurvTime <- rexp(n, rate = 0.1) Event <- rbinom(n, 1, 0.7) thresholds <- c(4, 6, 10) Y <- create_longitudinal_labels(SurvTime, Event, thresholds) W <- create_indicator_matrix(Y) K <- create_constraint_matrix(n_tasks) boot_res <- bootstrap_orthoMTL( X = X, Y = Y, lambda = 1e-3, step_size = 0.1, K = K, survival = TRUE, censored.mat = W, n_repeats = 5, n_cores = 1, verbose = FALSE ) plots <- plot_bootstrap(boot_res) plots[[1]]
Displays pairwise distances between task coefficient vectors as a
heatmap. Distance is computed as 1 - cosine_similarity.
Values near 0 indicate similar coefficient profiles; values near 1
indicate orthogonal profiles.
plot_correlation(x, midpoint = 0.5, limits = c(0, 1))plot_correlation(x, midpoint = 0.5, limits = c(0, 1))
x |
Either a fitted |
midpoint |
Midpoint for the colour scale. Default: |
limits |
Numeric vector of length 2 for the colour scale limits.
Default: |
A ggplot2 object.
set.seed(42) n <- 100; p <- 10; n_tasks <- 4 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 colnames(Y) <- c("T1", "T2", "T3", "T4") K <- matrix(1, n_tasks, n_tasks); diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K) plot_correlation(fit)set.seed(42) n <- 100; p <- 10; n_tasks <- 4 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 colnames(Y) <- c("T1", "T2", "T3", "T4") K <- matrix(1, n_tasks, n_tasks); diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K) plot_correlation(fit)
Displays the coefficient matrix as a heatmap. Features (rows) can optionally be reordered by their mean coefficient across tasks.
plot_heatmap(x, reorder = TRUE)plot_heatmap(x, reorder = TRUE)
x |
Either a fitted |
reorder |
Logical. If |
A ggplot2 object.
coef.orthoMTL, plot_correlation
set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 colnames(Y) <- c("T1", "T2", "T3") K <- matrix(1, n_tasks, n_tasks); diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K) # From fitted object plot_heatmap(fit) # From raw matrix plot_heatmap(coef(fit), reorder = FALSE)set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 colnames(Y) <- c("T1", "T2", "T3") K <- matrix(1, n_tasks, n_tasks); diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K) # From fitted object plot_heatmap(fit) # From raw matrix plot_heatmap(coef(fit), reorder = FALSE)
Displays a prediction matrix as a heatmap (swimmer plot), where rows are patients and columns are tasks/thresholds.
plot_prediction(x)plot_prediction(x)
x |
A numeric prediction matrix with dimensions
|
A ggplot2 object.
set.seed(42) n <- 50; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 colnames(Y) <- c("T1", "T2", "T3") K <- matrix(1, n_tasks, n_tasks); diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K) preds <- predict(fit, newdata = X) plot_prediction(preds)set.seed(42) n <- 50; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 colnames(Y) <- c("T1", "T2", "T3") K <- matrix(1, n_tasks, n_tasks); diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K) preds <- predict(fit, newdata = X) plot_prediction(preds)
Generate predictions from a fitted orthoMTL model for new
observations. In survival mode, predictions are projected onto the
non-negative non-increasing space to ensure monotonicity across tasks.
## S3 method for class 'orthoMTL' predict(object, newdata, type = c("link", "response", "class"), ...)## S3 method for class 'orthoMTL' predict(object, newdata, type = c("link", "response", "class"), ...)
object |
A fitted model object of class |
newdata |
A numeric matrix of new observations with dimensions
|
type |
Character; scale of the returned predictions. One of:
|
... |
Additional arguments (currently ignored). |
If the fitted object contains feature_names (i.e., the training
matrix X had column names), newdata columns are aligned
to match. Missing features cause an error. Extra features trigger a
warning and are dropped.
In survival mode (object$hyperparameters$survival == TRUE),
each row of the raw prediction matrix is projected via
nnmaxheap_C() to enforce non-negative, non-increasing values
across tasks (time thresholds).
A numeric matrix of predictions with dimensions
n_new x n_tasks. Column names correspond to task names
if available.
Vervier, K., Mahe, P., d'Aspremont, A., Veyrieras, J.-B., and Vert, J.-P. (2014). On Learning Matrices with Orthogonal Columns or Disjoint Supports. ECML-PKDD 2014. https://hal.science/hal-00985654
set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) W_true <- qr.Q(qr(matrix(rnorm(p * n_tasks), p, n_tasks))) Y <- X %*% W_true + matrix(rnorm(n * n_tasks), n) * 0.1 K <- matrix(1, n_tasks, n_tasks); diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K, disjoint = FALSE) X_new <- matrix(rnorm(20 * p), 20, p) colnames(X_new) <- paste0("V", seq_len(p)) preds <- predict(fit, newdata = X_new) dim(preds)set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) W_true <- qr.Q(qr(matrix(rnorm(p * n_tasks), p, n_tasks))) Y <- X %*% W_true + matrix(rnorm(n * n_tasks), n) * 0.1 K <- matrix(1, n_tasks, n_tasks); diag(K) <- 0.5 fit <- orthoMTL(X, Y, lambda = 1e-3, K = K, disjoint = FALSE) X_new <- matrix(rnorm(20 * p), 20, p) colnames(X_new) <- paste0("V", seq_len(p)) preds <- predict(fit, newdata = X_new) dim(preds)
Displays a summary of bootstrap inference results from
bootstrap_orthoMTL.
## S3 method for class 'bootstrap_orthoMTL' print(x, ...)## S3 method for class 'bootstrap_orthoMTL' print(x, ...)
x |
An object of class |
... |
Additional arguments (currently ignored). |
Invisibly returns x.
# See ?bootstrap_orthoMTL for a full example# See ?bootstrap_orthoMTL for a full example
Displays a summary of cross-validation results from
cv_orthoMTL.
## S3 method for class 'cv_orthoMTL' print(x, n_top = 5, ...)## S3 method for class 'cv_orthoMTL' print(x, n_top = 5, ...)
x |
An object of class |
n_top |
Number of top configurations to display. Default: 5. |
... |
Additional arguments (currently ignored). |
Invisibly returns x.
# See ?cv_orthoMTL for a full example# See ?cv_orthoMTL for a full example
Displays a compact summary of a fitted orthoMTL model.
## S3 method for class 'orthoMTL' print(x, ...)## S3 method for class 'orthoMTL' print(x, ...)
x |
A fitted model object of class |
... |
Additional arguments (currently ignored). |
Invisibly returns x.
set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 fit <- orthoMTL(X, Y, lambda = 1e-3) print(fit)set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 fit <- orthoMTL(X, Y, lambda = 1e-3) print(fit)
Displays a summary of a simulated dataset from
simulate_mtl.
## S3 method for class 'simulated_mtl' print(x, ...)## S3 method for class 'simulated_mtl' print(x, ...)
x |
An object of class |
... |
Additional arguments (currently ignored). |
Invisibly returns x.
sim <- simulate_mtl(n = 100, p = 20, n_signals = 4) print(sim)sim <- simulate_mtl(n = 100, p = 20, n_signals = 4) print(sim)
Pooled over every non-NA
cell, for orthoMTL fits in regression mode. uses
the global mean of the pooled true values.
r2_mtl(true.label.mat, pred.label.mat)r2_mtl(true.label.mat, pred.label.mat)
true.label.mat |
A numeric matrix of true responses
( |
pred.label.mat |
A numeric matrix of predictions of the same
dimensions (as produced by |
A single numeric value (higher is better; 1 = perfect). Can be negative when predictions are worse than the mean.
set.seed(1) Y <- matrix(rnorm(30), 10, 3) P <- Y + matrix(rnorm(30, sd = 0.1), 10, 3) r2_mtl(Y, P)set.seed(1) Y <- matrix(rnorm(30), 10, 3) P <- Y + matrix(rnorm(30, sd = 0.1), 10, 3) r2_mtl(Y, P)
Pooled RMSE over every non-NA cell of the true/predicted
matrices, for orthoMTL fits in regression mode
(logistic = FALSE, survival = FALSE).
rmse_mtl(true.label.mat, pred.label.mat)rmse_mtl(true.label.mat, pred.label.mat)
true.label.mat |
A numeric matrix of true responses
( |
pred.label.mat |
A numeric matrix of predictions of the same
dimensions (as produced by |
A single non-negative numeric value (lower is better).
set.seed(1) Y <- matrix(rnorm(30), 10, 3) P <- Y + matrix(rnorm(30, sd = 0.1), 10, 3) rmse_mtl(Y, P)set.seed(1) Y <- matrix(rnorm(30), 10, 3) P <- Y + matrix(rnorm(30, sd = 0.1), 10, 3) rmse_mtl(Y, P)
Generates a realistic simulated dataset with binary and continuous
features and a known ground-truth coefficient structure. The
mode argument selects the response type: piecewise-exponential
survival times (default), multi-task regression
targets, or multi-task binary classification labels. Designed
for demonstrating and testing orthoMTL.
simulate_mtl( n = 200, p = 30, n_signals = 5, n_continuous = 1, thresholds = c(4, 6, 10, 15), mode = c("survival", "regression", "classification"), noise_sd = 1, censoring_max = 25, baseline_hazard = 0.05, effect_strength = 0.8, treatment_effect = -0.3, seed = NULL )simulate_mtl( n = 200, p = 30, n_signals = 5, n_continuous = 1, thresholds = c(4, 6, 10, 15), mode = c("survival", "regression", "classification"), noise_sd = 1, censoring_max = 25, baseline_hazard = 0.05, effect_strength = 0.8, treatment_effect = -0.3, seed = NULL )
n |
Number of patients. Default: |
p |
Number of features excluding the treatment column.
Default: |
n_signals |
Number of features with true non-zero effects.
Default: |
n_continuous |
Number of continuous features (placed first
in the feature matrix). Default: |
thresholds |
Numeric vector of time thresholds defining the
task structure. Default: |
mode |
Character; the response type to generate. One of
|
noise_sd |
Standard deviation of the Gaussian noise added in
|
censoring_max |
Maximum censoring time. Censoring times are
drawn from |
baseline_hazard |
Baseline hazard rate per interval. Default:
|
effect_strength |
Multiplier controlling the magnitude of
feature effects. Default: |
treatment_effect |
Effect of treatment on the log-hazard
(negative = protective). Default: |
seed |
Optional random seed for reproducibility. Default:
|
Feature structure:
Continuous features are drawn from N(0, 1).
Binary features have prevalences drawn from Beta(2, 10),
producing a realistic range (~5-30%).
Treatment is balanced 1:1 via random assignment.
Effect templates: Each signal feature is assigned one of five temporal patterns:
"early": strong effect at early thresholds, fading to
zero at late.
"late": zero at early thresholds, emerging at late.
"constant": equal effect across all thresholds
(detectable by standard Cox models).
"increasing": effect grows over time.
"decreasing": effect shrinks over time.
Templates are assigned cyclically across signal features with random sign (risk-increasing or protective).
Survival time generation:
Uses a piecewise-exponential model where the hazard in each
interval is
h_k(i) = baseline_hazard * exp(X[i, ] %*% beta[, k]).
Patients progress through intervals sequentially; an event
occurs when the simulated time within an interval is shorter
than the interval width.
Censoring:
Independent of event times. C ~ Unif(0, censoring_max).
Observed time = min(T, C), event indicator = T <= C.
An object of class "simulated_mtl" containing:
The response type generated.
For mode = "regression" / "classification",
the n x length(thresholds) response matrix. NULL
in survival mode (use SurvTime/Event instead).
Survival mode only (NULL otherwise):
observed times and event indicators.
Numeric matrix of dimensions n x (p + 1).
Columns include n_continuous continuous features
(standard normal), p - n_continuous binary features
(prevalences drawn from Beta(2, 10)), and a
treatment column (balanced 1:1).
Numeric vector of observed survival times.
Binary vector: 1 = event observed,
0 = censored.
Binary vector (also present as last column
of X).
Character vector of column names of
X.
A list containing:
Matrix of true coefficients with
dimensions (p + 1) x length(thresholds).
Names of features with non-zero effects.
Names of features with zero effects.
Named character vector mapping signal features to their temporal effect template.
The thresholds used.
The baseline hazard used.
Input parameters stored for reference.
The matched function call.
create_longitudinal_labels,
orthoMTL
# Generate simulated data sim <- simulate_mtl(n = 100, p = 20, n_signals = 4, seed = 42) sim # Inspect ground truth sim$ground_truth$signal_features sim$ground_truth$effect_types sim$ground_truth$coefficients[sim$ground_truth$signal_features, ] # Use in orthoMTL workflow thresholds <- c(4, 6, 10, 15) Y <- create_longitudinal_labels(sim$SurvTime, sim$Event, thresholds) W <- create_indicator_matrix(Y) K <- create_constraint_matrix(length(thresholds)) fit <- orthoMTL(sim$X, Y, lambda = 1e-3, K = K, survival = TRUE, censored.mat = W) summary(fit)# Generate simulated data sim <- simulate_mtl(n = 100, p = 20, n_signals = 4, seed = 42) sim # Inspect ground truth sim$ground_truth$signal_features sim$ground_truth$effect_types sim$ground_truth$coefficients[sim$ground_truth$signal_features, ] # Use in orthoMTL workflow thresholds <- c(4, 6, 10, 15) Y <- create_longitudinal_labels(sim$SurvTime, sim$Event, thresholds) W <- create_indicator_matrix(Y) K <- create_constraint_matrix(length(thresholds)) fit <- orthoMTL(sim$X, Y, lambda = 1e-3, K = K, survival = TRUE, censored.mat = W) summary(fit)
Displays a detailed summary of a fitted orthoMTL model,
including hyperparameters, coefficient matrix statistics, and
top features by mean absolute coefficient.
## S3 method for class 'orthoMTL' summary(object, n_top = 5, ...)## S3 method for class 'orthoMTL' summary(object, n_top = 5, ...)
object |
A fitted model object of class |
n_top |
Number of top features to display. Default: 5. |
... |
Additional arguments (currently ignored). |
Invisibly returns object.
set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 fit <- orthoMTL(X, Y, lambda = 1e-3) summary(fit)set.seed(42) n <- 100; p <- 10; n_tasks <- 3 X <- matrix(rnorm(n * p), n, p) colnames(X) <- paste0("V", seq_len(p)) Y <- X %*% matrix(rnorm(p * n_tasks), p) + matrix(rnorm(n * n_tasks), n) * 0.1 fit <- orthoMTL(X, Y, lambda = 1e-3) summary(fit)