Fitting known signatures: exposures, presence tests, and bootstrap intervals
Source:vignettes/signature-fitting.Rmd
signature-fitting.RmdDe-novo extraction (ms_extract()) discovers signatures;
reference fitting answers the mirror-image question. Given a trusted
signature dictionary, ms_fit() estimates each sample’s
exposure to every dictionary signature, ms_test_presence()
asks which signatures the cohort’s evidence supports at all, and
ms_fit_bootstrap() attaches bootstrap confidence intervals
and support stability to the same fit. This guide walks the three faces
on one small simulated cohort, then reads the MsFit result
object and the bootstrap plotting faces. The upstream steps (variants,
tallying) are covered in get-started.html.
A simulated cohort against COSMIC signatures
The dictionary is a channels x signatures matrix, or an
MsSignature whose signatures matrix is used.
The bundled COSMIC reference database ships canonical SBS96 matrices
whose rows already carry the canonical channel labels, so slicing a few
columns gives a dictionary that ms_fit() can align by
label.
refdb <- ms_refdb_bundled(build = "GRCh38")
#> ms_refdb_bundled: no official ID reference set for build GRCh38 -- serving the declared build-independent set (nearest official build(s): GRCh37)
sbs <- refdb[["SBS_GRCh38"]]@matrices[[1]]
dict <- sbs[, c("SBS1", "SBS5", "SBS13", "SBS4")]
dim(dict)
#> [1] 96 4A subgroup of samples plants an APOBEC signature on top of the clock-like pair; another dictionary signature is planted nowhere, so the presence test below has something to reject.
samples <- paste0("T", 1:12)
act <- matrix(0, nrow = ncol(dict), ncol = length(samples),
dimnames = list(colnames(dict), samples))
act["SBS1", ] <- 0.45
act["SBS5", ] <- 0.45
act["SBS13", samples[1:4]] <- 0.10
# share exposures: rebalance the clock pair so every column sums to one
resid <- 1 - colSums(act)
act["SBS1", ] <- act["SBS1", ] + resid / 2
act["SBS5", ] <- act["SBS5", ] + resid / 2
cat <- ms_simulate(dict, act, arm = "multinomial", burden = 600, seed = 11)
cat
#> <MsCatalog> SBS96: 96 channels x 12 samplesms_simulate() returns an MsCatalog, exactly
the input ms_fit() dispatches on.
Fitting exposures with ms_fit()
The default method, "nnls", solves the Lawson-Hanson
non-negative least squares problem per sample: the classical
deconstructSigs-style baseline. The other two methods start from that
solution and refine it on a likelihood scale:
-
"likelihood_bidirectional"(MuSiCal flavour) alternates backward and forward steps on the per-mutation multinomial log-likelihood: a signature leaves when the cheapest removal costs less than the epsilon threshold and enters when the best gain exceeds it, until both steps decline; -
"lrt"(mSigAct flavour) maximizes the negative-binomial likelihood over the exposures through an internal safeguarded-Newton ascent, warm-started from NNLS.
fit <- ms_fit(cat, dict) # method = "nnls" (default)
fit
#> <MsFit> 4 signatures x 12 samples | engine: nnls | refdb: adhoc-1
head(fit@support)
#> signature sample exposure share active
#> 1 SBS1 T1 269.1006 0.4485010 TRUE
#> 2 SBS5 T1 257.2659 0.4287765 TRUE
#> 3 SBS13 T1 73.6335 0.1227225 TRUE
#> 4 SBS4 T1 0.0000 0.0000000 FALSE
#> 5 SBS1 T2 248.8291 0.4154832 TRUE
#> 6 SBS5 T2 282.0921 0.4710242 TRUE
head(fit@tests)
#> signature sample lrt_stat lrt_p se
#> 1 SBS1 T1 384.21030 7.538704e-86 17.775025
#> 2 SBS5 T1 495.11013 5.506652e-110 18.191517
#> 3 SBS13 T1 83.40555 3.341701e-20 9.877928
#> 4 SBS4 T1 0.00000 5.000000e-01 NA
#> 5 SBS1 T2 333.73923 7.368207e-75 17.197792
#> 6 SBS5 T2 282.16723 1.265599e-63 18.883105Two tidy evidence tables ride along. support holds one
row per signature-sample pair with the exposure, its compositional share
and the zeroing decision: exposures whose share falls below
zero_threshold are set to exactly zero and reported
active = FALSE. tests holds the entry-wise
presence LRT statistic and raw p-value per pair, plus a Fisher standard
error that exists only on the interior support – boundary parameters get
tests, not intervals, so boundary rows carry NA.
Two further arguments shape the whole fit.
rescale = TRUE (default) multiplies each sample’s kept
exposures back onto the sample’s original mutation total, the MuSiCal
TMB-rescale semantics: the multiplication leaves zeroed entries at zero,
and the shares (hence the zeroing decision) and the presence evidence
are invariant. connected = NULL by default; passing
distinct dictionary column indices selects a connected-signature group –
for example the clock-like background pair – which is fixed at its
initial full-NNLS solution after the method refit
(FixAndRefit) while the remaining signatures are
re-optimized against the residual. This is a documented approximation of
the upstream joint-refit strategy, which is declared future work and
never silently substituted.
Choosing among the methods
All three methods run through the same kernel, and the
tests evidence is method-independent by construction: the
LRT columns are always computed at the negative-binomial MLE exposures,
so the tables compare fairly across methods. The nb_size
argument is the negative-binomial size of every likelihood-based column;
larger values sit closer to the Poisson limit.
fits <- list(
nnls = fit,
bidirectional = ms_fit(cat, dict, method = "likelihood_bidirectional"),
lrt = ms_fit(cat, dict, method = "lrt")
)
round(vapply(fits, function(f) f@exposures[, "T1"], numeric(ncol(dict))), 1)
#> nnls bidirectional lrt
#> SBS1 269.1 269.1 269.1
#> SBS5 257.3 257.3 257.3
#> SBS13 73.6 73.6 73.6
#> SBS4 0.0 0.0 0.0Testing cohort presence with ms_test_presence()
ms_test_presence() tests, per signature, whether the
signature is present in the cohort. Per-sample entry-wise LRT statistics
are pooled by summation (independent likelihoods add), and the raw
p-value is the exact pooled-null tail: under the null each per-sample
statistic follows the Self-Liang boundary law (an equal mixture of a
point mass at zero and a chi-square), so the pooled statistic over
independent samples is a Binomial-weighted mixture of chi-squares.
Benjamini-Hochberg correction runs over the signature family, and
pass_bh applies the frozen family-wise alpha.
ms_test_presence(cat, dict) # kernel face: catalog + dictionary
#> signature stat p_raw p_bh pass_bh
#> 1 SBS1 4478.368208 0.000000e+00 0.000000e+00 TRUE
#> 2 SBS5 9420.543645 0.000000e+00 0.000000e+00 TRUE
#> 3 SBS13 316.849472 2.266357e-63 3.021809e-63 TRUE
#> 4 SBS4 5.304463 4.961519e-01 4.961519e-01 FALSE
ms_test_presence(fit) # MsFit face: re-aggregates the fit's own evidence
#> signature stat p_raw p_bh pass_bh
#> 1 SBS1 4478.368208 0.000000e+00 0.000000e+00 TRUE
#> 2 SBS5 9420.543645 0.000000e+00 0.000000e+00 TRUE
#> 3 SBS13 316.849472 2.266357e-63 3.021809e-63 TRUE
#> 4 SBS4 5.304463 4.961519e-01 4.961519e-01 FALSEThe result is a plain data.frame with one row per dictionary
signature, so it drops into any downstream filter or report. The two
faces agree by construction – the MsFit face re-derives its
p-values from the fit’s stored evidence grid with the pure-R twin of the
kernel’s arithmetic.
Bootstrap intervals and stability with ms_fit_bootstrap()
The bootstrap resamples each sample’s mutations from its empirical
channel distribution (nonparametric multinomial), refits the selected
method per replicate, and reports the percentile interval over the
replicate exposures plus the support stability – the frequency with
which the zeroing rule keeps each exposure active. The result is an
MsFit like ms_fit() returns, enriched with
attributes on the exposures matrix; the support table gains
a boot_stability column. The point exposures and tests
remain an ordinary point fit: the interval is support-conditional
evidence alongside the LRT, not a replacement for it. With
return_draws = TRUE the raw per-replicate exposure cube is
attached as the boot_draws attribute for the histogram face
below; bca = TRUE adds the bias-corrected-and-accelerated
interval computed from the same replicates. The default replicate count
is higher; this guide keeps n_boot small for rendering
speed.
boot <- ms_fit_bootstrap(cat, dict, method = "nnls", n_boot = 50, seed = 1,
return_draws = TRUE)
boot
#> <MsFit> 4 signatures x 12 samples | engine: nnls | refdb: adhoc-1
head(boot@support)
#> signature sample exposure share active boot_stability
#> 1 SBS1 T1 269.1006 0.4485010 TRUE 1
#> 2 SBS5 T1 257.2659 0.4287765 TRUE 1
#> 3 SBS13 T1 73.6335 0.1227225 TRUE 1
#> 4 SBS4 T1 0.0000 0.0000000 FALSE 0
#> 5 SBS1 T2 248.8291 0.4154832 TRUE 1
#> 6 SBS5 T2 282.0921 0.4710242 TRUE 1
names(attributes(boot@exposures))
#> [1] "dim" "dimnames" "ci_lower"
#> [4] "ci_upper" "support_stability" "n_boot"
#> [7] "seed" "ci_level" "boot_draws"
dim(attr(boot@exposures, "boot_draws"))
#> [1] 4 12 50The CI endpoints are labelled matrices with the same shape as the exposures, so the interval for one sample reads directly as a table.
round(cbind(lower = attr(boot@exposures, "ci_lower")[, "T1"],
estimate = boot@exposures[, "T1"],
upper = attr(boot@exposures, "ci_upper")[, "T1"],
stability = attr(boot@exposures, "support_stability")[, "T1"]), 1)
#> lower estimate upper stability
#> SBS1 241.7 269.1 292.7 1
#> SBS5 231.1 257.3 292.5 1
#> SBS13 52.4 73.6 97.1 1
#> SBS4 0.0 0.0 0.0 0The MsFit object and the bootstrap plots
The MsFit container carries the labelled
exposures matrix (signatures x samples), the
support and tests data.frames, an adhoc
reference_summary snapshot derived from the dictionary’s
signature labels (a sha256 digest over the labels, never an embedded
reference database), and the engine string recording the
method. print shows one line with dimensions, engine and
snapshot version; the plotting faces read the bootstrap attributes
directly.
plot_boot_ci(boot, sample = "T1")
plot_boot_distribution(boot, sample = "T1") # requires return_draws = TRUE
plot_boot_stability(boot, sample = "T1")
plot_boot_ci() draws the point exposures with their
percentile whiskers for one sample, ordered by exposure share;
plot_boot_distribution() shows the raw per-replicate
exposure histograms with the estimate and both bounds marked;
plot_boot_stability() bars the support stability against
the calibration floor. Without the draws cube the histogram face raises
a structured error that says to re-run ms_fit_bootstrap()
with return_draws = TRUE.
Where to go next
Once signatures are fitted and their presence established, the natural next steps are comparing dictionaries and extracted catalogs – Comparing signatures – and the calibration statistics behind the presence and interval machinery – Calibrated statistics. The input path from VCFs to catalogs is in get-started.html.