The OptSurvCutR package provides a
comprehensive, data-driven workflow for survival analysis, specifically
designed for scenarios where a single cut-point is not sufficient. Its
core functionality is to discover and validate one or more optimal
cut-points for continuous variables in time-to-event data.
A primary asset of OptSurvCutR is its ability to perform
threshold discovery while adjusting for critical
covariates (e.g., patient age, sex, or clinical stage). This
ensures that the discovered risk groups carry independent prognostic
value, preventing confounding clinical factors from muddying your
biomarkerβs true signal.
This tutorial demonstrates a complete, end-to-end workflow for finding independent, covariate-adjusted survival thresholds using a built-in clinical virome dataset.
The human gut viromeβthe collection of all viruses in the gastrointestinal tractβfrequently undergoes significant dysbiosis in diseases like colorectal cancer (CRC). In this workflow, we will analyze gut virome data to determine if the relative abundance of the genus Enterovirus can independently predict overall survival.
Enterovirus represents a fascinating biological βdouble-edged swordβ in oncology: 1. Pro-inflammatory Risk: Chronic viral persistence can aggravate the pro-inflammatory microenvironment, potentially accelerating tumor progression. 2. Oncolytic Potential: Conversely, specific Enterovirus strains exhibit natural oncolytic properties, preferentially infecting and lysing malignant cells.
This dual nature suggests a complex, non-linear, or U-shaped risk relationship where intermediate abundance levels might represent an optimal homeostatic balance, while extreme depletion or overabundance tracks with poor clinical outcomes.
Using data inspired by Smyth et al.Β (2024), this vignette handles a robust, multivariate analysis to isolate data-driven thresholds for Enterovirus abundance while rigorously adjusting for patient age and sex.
This guide covers a clean, 6-step pipeline:
We begin by loading the explicit suite of tools needed to execute the modeling and manage data piping.
library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#> filter, lag
#> The following objects are masked from 'package:base':
#>
#> intersect, setdiff, setequal, union
library(survival)
library(survminer)
#> Loading required package: ggplot2
#> Loading required package: ggpubr
#>
#> Attaching package: 'survminer'
#> The following object is masked from 'package:survival':
#>
#> myeloma
library(ggplot2)
library(patchwork)
library(knitr)
library(cli)
library(OptSurvCutR)
#> ββ OptSurvCutR v0.9.8 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#> Docs: <https://github.com/paytonyau/OptSurvCutR>
#> Paper: Yau, Payton (2025) bioRxiv 10.1101/2025.10.08.681246
#> Cite: `citation('OptSurvCutR')`
#> Please cite the paper to support development.
# Enforce uniform CRAN-safe text formatting logs
options(cli.num_colors = 1)We declare our biomarker target and confounding clinical adjusters upfront to maintain clean, reusable variables throughout downstream code chunks.
# Continuous predictor column name
microbe_name_to_analyze <- "Enterovirus"
# Confounding clinical covariates to adjust for inside the Cox engine
covariates_to_adjust_for <- c("age", "sex")We load the crc_virome dataset directly from the
package. This wide-format data frame contains combined microbiome
profiles alongside vital patient statistics. We will parse the raw
character survival strings, apply a 5-year (60-month) administrative
censoring cap, and filter for complete cases across all variables.
data("crc_virome", package = "OptSurvCutR")
# --- Process and Clean the Case Cohort ---
analysis_data <- crc_virome %>%
select(
patient_id = sample_id,
time_months,
status_char = status, # Original string format (e.g., "0:LIVING")
abundance = all_of(microbe_name_to_analyze),
all_of(covariates_to_adjust_for)
) %>%
mutate(
# 1. Strip the numeric event token from the status character
status_numeric = as.numeric(substr(status_char, 1, 1)),
# 2. Apply 5-year censoring: mark deaths beyond 60 months as censored (0)
status_final = ifelse(time_months > 60, 0, status_numeric),
# 3. Cap follow-up times cleanly at the 60-month milestone
time_final = pmin(time_months, 60)
) %>%
select(
patient_id,
time = time_final,
status = status_final,
abundance,
all_of(covariates_to_adjust_for)
) %>%
filter(complete.cases(time, status, abundance, across(all_of(covariates_to_adjust_for))))
# Preview the analysis-ready matrix
head(analysis_data)
#> patient_id time status abundance age sex
#> 1 TCGA-3L-AA1B-01 15.616267 0 3.663065 61 Female
#> 2 TCGA-4N-A93T-01 4.799947 0 1.697278 67 Male
#> 3 TCGA-4T-AA8H-01 12.657396 0 2.809707 42 Female
#> 4 TCGA-5M-AAT4-01 1.610941 1 1.688483 74 Male
#> 5 TCGA-5M-AAT6-01 9.534142 1 2.621937 40 Female
#> 6 TCGA-5M-AATE-01 39.451622 0 2.378831 76 Maleπ‘ Methodological Insight: Should High-Throughput Biomarker Data be Smoothed? High-throughput omics datasets (such as relative abundance or bulk transcriptomics) are frequently sparse, zero-inflated, and non-normally distributed. It is highly recommended not to smooth your predictor vectors (e.g., using splines or data blurring steps) prior to running
OptSurvCutR.Data smoothing can artificially mask and destroy the sharp biological boundaries your model seeks to uncover. Instead, pass standard normalized, unsmoothed expressions directly into the package. The underlying optimization framework is natively engineered to traverse erratic data surfaces.
We first pass our data to find_cutpoint_number(). This
tool loops through potential architectures (0 to 3 cut-points) while
adjusting for age and sex at every iteration. It uses the Bayesian
Information Criterion (BIC) to heavily penalize
over-parameterization.
number_result_adj <- find_cutpoint_number(
# ==========================================
# 1. CORE DATA INPUTS
# ==========================================
data = analysis_data,
predictor = "abundance",
outcome_time = "time",
outcome_event = "status",
covariates = covariates_to_adjust_for,
# ==========================================
# 2. SEARCH ENGINE SETTINGS
# ==========================================
method = "genetic", # The evolutionary search engine
criterion = "BIC", # Evaluates model fit (BIC heavily penalizes over-complexity)
max_cuts = 3, # Test models ranging from 0 to 3 cuts
nmin = 0.25, # SAFETY LIMIT: Minimum 10% of patients per group
n_perm = 50,
# ==========================================
# 3. THE "R-GROUND" SETTINGS (Advanced Genetic Tuning)
# ==========================================
max.generations = 100, # LIFESPAN: How many evolutionary cycles to run
pop.size = 100, # SEARCH PARTY: Number of random guesses per generation
boundary.enforcement = 1, # EDGE CONTROL: 1 = Soft boundary (allows safe edge exploration)
seed = 123 # REPRODUCIBILITY: Locks the random number generator
)
#> βΉ nmin 0.25 is a proportion. Min. group size set to 145.
#> βΉ Finding optimal cut number: method = genetic
#> βΉ Running genetic algorithm for 1 cut-point(s)...
#> βΉ Running genetic algorithm for 2 cut-point(s)...
#> βΉ Running genetic algorithm for 3 cut-point(s)...
#> No valid cut-points found for 3 cut(s) due to genetic algorithm failure or
#> constraints.
summary(number_result_adj)
#>
#> ββ Optimal Cut-point Number Analysis (Genetic) βββββββββββββββββββββββββββββββββ
#> β Best Model: 2 Cut-points (Criterion: BIC)
#> βΉ Optimal Thresholds: "1.767, 2.14"
#>
#> ββ 1. Model Comparison ββ
#> Marker num_cuts BIC Delta_BIC BIC_Weight Evidence
#> 0 1253.66 4.28 8.5% Moderate
#> 1 1251.97 2.58 19.7% Moderate
#> > 2 1249.38 0.00 71.8% Substantial
#> 3 NA NA NA% <NA>
#> ββ 2. Clinical Risk Cohorts ββ
#> Group N Events Median_CI
#> G1 247 55 NA (51.5 - NA)
#> G2 189 22 NA (NA - NA)
#> G3 145 33 56.3 (43.2 - NA)
#> ββ 3. Cox Proportional-Hazards ββ
#> Group HR Lower Upper P_Value Signif
#> G2 0.471 0.287 0.772 0.003 **
#> G3 1.065 0.691 1.642 0.775
#> age 1.037 1.019 1.054 0.000 ***
#> sexMale 0.933 0.642 1.357 0.718
#> βΉ Overall Model: Concordance = 0.623 | Log-rank p = 0
#>
#> ββ 4. Time-Dependent Diagnostics (Schoenfeld) ββ
#>
#> β Passed: The proportional hazards assumption holds across the follow-up period (Global p = 0.116).
#>
#> ββ 5. Analysis Parameters ββ
#>
#> β’ Search Method: Genetic
#> β’ Predictor: abundance
#> β’ Criterion: BIC
#> β’ Maximum Cuts Evaluated: 3
#> β’ Minimum Group Size (nmin): 145
#> β’ Covariates: age, sexBayesian Information Criterion (BIC) landscape across cut architectures. The lowest point denotes the ideal group count.
Interpretation: The model minimizing the information landscape sits clearly at 2 cut-points (which divides the patient cohort into 3 distinct risk groups). This confirms that even after stripping away the confounding baseline effects of age and sex, a multi-tier group model is statistically superior to a traditional median binary split.
With 2 cuts established as our mathematical target, we invoke
find_cutpoint(). This runs a comprehensive Genetic
Algorithm search to locate the exact abundance coordinates that maximize
the covariate-adjusted log-rank statistic.
(Note: We pass a minimum group size constraint of
nmin = 0.275 here. Section 4.1 outlines how this parameter
protects model stability).
optimal_n_cuts_adj <- number_result_adj$optimal_num_cuts
cutpoint_result_adj <- find_cutpoint(
# ==========================================
# 1. CORE DATA INPUTS
# ==========================================
data = analysis_data,
predictor = "abundance",
outcome_time = "time",
outcome_event = "status",
covariates = covariates_to_adjust_for,
num_cuts = optimal_n_cuts_adj,
# ==========================================
# 2. SEARCH ENGINE SETTINGS
# ==========================================
method = "genetic",
criterion = "logrank",
nmin = 0.275,
n_perm = 20, # Scaled for swift vignette building
# ==========================================
# 3. THE "R-GROUND" SETTINGS (Advanced Genetic Tuning)
# ==========================================
max.generations = 150, # LIFESPAN: Increase if the algorithm fails to converge
pop.size = 100, # SEARCH PARTY: Increase to 500+ for massive datasets
boundary.enforcement = 1, # EDGE CONTROL: Soft boundaries prevent the algorithm from crashing near edges
seed = 123, # REPRODUCIBILITY
n_cores = 1 # SPEED: Adjust based on your machine's CPU
)
#> βΉ nmin 0.275 is a proportion. Min. group size set to 159.
#> βΉ Starting genetic search for 2 cut(s)...
#> βΉ Running 20 permutations to calculate adjusted p-value...
summary(cutpoint_result_adj)
#>
#> ββ Optimal Cut-point Analysis for Survival Data (Genetic) ββββββββββββββββββββββ
#> β Optimal Threshold(s): "1.768, 2.097"
#> βΉ Permutation-Adjusted P-value (20 runs): 0.0625
#>
#> ββ 1. Stratified Risk Cohorts ββ
#>
#> Group N Events Median_Time OS Rate at T=60
#> G1 247 55 NR (Not Reached) 53.7% (42.7-67.5)
#> G2 174 20 NR (Not Reached) 83.2% (75.1-92.1)
#> G3 160 35 56.3 48.6% (34.5-68.5)
#> ββ 2. Cox Proportional-Hazards ββ
#> Group HR Lower Upper P_Value Signif
#> G2 0.461 0.276 0.770 0.003 **
#> G3 1.020 0.667 1.560 0.928
#> age 1.036 1.019 1.054 0.000 ***
#> sexMale 0.923 0.635 1.343 0.677
#> βΉ Overall Model: Concordance = 0.622 | Log-rank p = 0
#>
#> ββ 3. Time-Dependent Diagnostics (Schoenfeld) ββ
#>
#> β Passed: The proportional hazards assumption holds across the follow-up period (Global p = 0.135).
#>
#> ββ 4. Analysis Parameters ββ
#>
#> β’ Search Method: Genetic
#> β’ Predictor: abundance
#> β’ Number of cuts: 2
#> β’ Minimum group size (nmin): 159
#> β’ Covariates: age, sex
#> β’ Permutations: 20When evaluating summary(), the package automatically
processes a Schoenfeld residuals validation check against your adjusted
model matrix, verifying if the Proportional Hazards assumption
holds:
Continuous population density profile with discovered optimal adjusted cut-point boundaries mapped as vertical anchors.
To prove that our discovered thresholds are reproducible clinical
signals rather than artifacts of overfitting, we pass our adjusted model
to validate_cutpoint() for rigorous bootstrap
resampling.
validation_result_adj <- validate_cutpoint(
# ==========================================
# 1. VALIDATION INPUTS
# ==========================================
cutpoint_result = cutpoint_result_adj,
num_replicates = 30, # Scaled for vignette performance; use >= 500 for publication
n_cores = 1,
# ==========================================
# 2. THE "R-GROUND" SETTINGS (Must match Step 2)
# ==========================================
max.generations = 150, # Must match or exceed Step 2 to ensure stability during bootstrap
pop.size = 100, # Must match or exceed Step 2
boundary.enforcement = 1, # Must match Step 2
seed = 123 # REPRODUCIBILITY
)
#> βΉ Using random seed 123 for reproducibility.
#> βΉ Bootstrap `nmin` not set. Using 143 (90% of original) to improve stability.
#> βΉ Validating 2 cut(s) from 'genetic' search using 'logrank'.
#> βΉ Running 30 replicates sequentially (n_cores = 1).
#> Bootstrapping β β β 7% | ETA: 43sBootstrapping β β β β 10% | ETA: 41sBootstrapping β β β β β 13% | ETA: 35sBootstrapping β β β β β β 17% | ETA: 33sBootstrapping β β β β β β β 20% | ETA: 33sBootstrapping β β β β β β β β 23% | ETA: 32sBootstrapping β β β β β β β β β 27% | ETA: 34sBootstrapping β β β β β β β β β β 30% | ETA: 29sBootstrapping β β β β β β β β β β β 33% | ETA: 28sBootstrapping β β β β β β β β β β β β 37% | ETA: 27sBootstrapping β β β β β β β β β β β β β 40% | ETA: 25sBootstrapping β β β β β β β β β β β β β β 43% | ETA: 27sBootstrapping β β β β β β β β β β β β β β β 47% | ETA: 25sBootstrapping β β β β β β β β β β β β β β β β 50% | ETA: 24sBootstrapping β β β β β β β β β β β β β β β β β 53% | ETA: 22sBootstrapping β β β β β β β β β β β β β β β β β β 57% | ETA: 21sBootstrapping β β β β β β β β β β β β β β β β β β β 60% | ETA: 18sBootstrapping β β β β β β β β β β β β β β β β β β β β 63% | ETA: 16sBootstrapping β β β β β β β β β β β β β β β β β β β β β 67% | ETA: 14sBootstrapping β β β β β β β β β β β β β β β β β β β β β β 70% | ETA: 13sBootstrapping β β β β β β β β β β β β β β β β β β β β β β β 73% | ETA: 11sBootstrapping β β β β β β β β β β β β β β β β β β β β β β β β 77% | ETA: 10sBootstrapping β β β β β β β β β β β β β β β β β β β β β β β β β 80% | ETA: 9sBootstrapping β β β β β β β β β β β β β β β β β β β β β β β β β β 83% | ETA: 7sBootstrapping β β β β β β β β β β β β β β β β β β β β β β β β β β β 87% | ETA: 6sBootstrapping β β β β β β β β β β β β β β β β β β β β β β β β β β β β 90% | ETA: 4sBootstrapping β β β β β β β β β β β β β β β β β β β β β β β β β β β β β 93% | ETA: 3sBootstrapping β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β 97% | ETA: 1s ! 4 of 30 replicates failed.
#> β 26 replicates completed.
summary(validation_result_adj)
#> Cut-point Stability Analysis (Bootstrap)
#> ----------------------------------------
#> Original Optimal Cut-point(s): 1.768, 2.097
#>
#> Bootstrap Distribution Summary
#> -----------------------------
#> Cut Mean SD Median Q1 Q3
#> 25% Cut1 1.756 0.057 1.738 1.704 1.787
#> 25%1 Cut2 2.089 0.058 2.096 2.088 2.136
#>
#> 95% Confidence Intervals
#> ------------------------
#> Lower Upper
#> Cut 1 1.690 1.865
#> Cut 2 1.986 2.158
#>
#> Validation Parameters
#> ---------------------
#> Replicates Requested: 30
#> Successful Replicates: 26 / 30 ( 86.7 %)
#> Failed Replicates: 4
#> Cores Used: 1
#> Seed: 123
#> Minimum Group Size (nmin): 143
#> Method: genetic
#> Criterion: logrank
#> Covariates: age, sex
#>
#>
#> Stability Assessment:
#> ---------------------
#> Maximum CI Width (Relative to 10th-90th Percentile Range): 39%
#> β Model Status: DISTINCT (Tier 2)
#> The relative mathematical variance is moderate (39%), but 95% Confidence
#> Intervals do not overlap.Following bootstrap calculations, OptSurvCutR evaluates
both your Precision (Confidence Interval width relative
to overall data range) and Validity (absence of
interval overlap), grading model health into four distinct tiers:
Bootstrap discovery density profiles tracking threshold convergence across resampled patient pools.
nmin
WedgeBiomarker distributions in human clinical populations rarely match flawless, neat step-functions. During cross-validation of a 3-tier risk architecture, you may occasionally run into overlapping confidence intervals between Cut 1 and Cut 2.
The Overlap Problem: In early baseline tuning passes
with this raw dataset, setting a standard nmin = 0.25
caused the algorithm to place Cut 1 and Cut 2 very close together. When
subjected to bootstrap resampling, their 95% confidence intervals bled
into each other heavily (e.g., Cut 1 upper bound reaching 1.89, while
Cut 2 lower bound dropped to 1.77). This overlapping variance triggered
an automated Tier 4 (UNSTABLE) flag because, in certain
resampled cohorts, the intermediate group vanished entirely, causing the
underlying Cox calculations to collapse.
The Solution: We resolved this without changing our
core clinical objective by slightly increasing the minimum group size
constraint to nmin = 0.275.
By mandating that every individual cohort must hold at least 27.5% of the total sample size, we effectively injected a mathematical βwedgeβ into the search space. This parameter physically forces Cut 1 and Cut 2 apart to accommodate the minimum required patient volume between them. As demonstrated by the final Tier 1 output above, the confidence intervals now separate cleanly, yielding highly stable and reproducible clinical tiers.
By default, the package maps the patient cohort into numeric groups ordered sequentially based on your continuous predictor values: * G1: Low Enterovirus Abundance * G2: Medium Enterovirus Abundance * G3: High Enterovirus Abundance
type = "outcome")# 1. Inspect how many cut-points the Genetic Engine actually discovered
optimal_cuts <- sort(cutpoint_result_adj$optimal_cuts)
num_cuts <- length(optimal_cuts)
# 2. Dynamically assign matching label cohorts
if (num_cuts == 1) {
dynamic_labels <- c("Low Abundance", "High Abundance")
} else if (num_cuts == 2) {
dynamic_labels <- c("Low", "Medium", "High")
} else {
dynamic_labels <- paste("Cohort Group", 1:(num_cuts + 1))
}
# 3. Generate the adjusted Kaplan-Meier curve natively safely
plot(
cutpoint_result_adj,
type = "outcome",
title = "5-Year OS by Adjusted Enterovirus Group",
xlab = "Time (Months)",
ylab = "Overall Survival Probability",
legend.title = "Abundance Group",
legend.labs = dynamic_labels # <-- Pass the self-correcting vector here
)Adjusted Kaplan-Meier overall survival estimates stratified by optimal Enterovirus abundance cohorts.
Interpretation: The resulting survival paths reveal a clear non-linear, U-shaped risk curve. Patients matching the G2 (Medium) intermediate viral abundance tier exhibit the highest overall survival probability over the 60-month window. Because this grouping was uncovered using our covariate-adjusted framework, we can state with confidence that this survival divergence is an independent trait of the viral biomarker itself, rather than an underlying imbalance in patient age or sex across the cohorts.
type = "forest")plot(
cutpoint_result_adj,
type = "forest",
reference_group = "G2",
main = paste("Adjusted HRs for", microbe_name_to_analyze, "Groups (Ref: G2/Medium)")
)Multivariate Forest plot displaying adjusted biomarker Hazard Ratios alongside baseline clinical adjusters.
Interpretation: This plot isolates the independent prognostic value required for clinical translation. Because
ageandsexare calculated simultaneously inside the model, the displayed Hazard Ratios represent pure, adjusted biological weights. Both low (G1) and high (G3) extremes of viral expression are confirmed as independent risk factors for CRC mortality relative to the homeostatic center (G2).
type = "diagnostic")if (requireNamespace("rlang", quietly = TRUE)) {
`:=` <- rlang::`:=`
}
tryCatch({
plot(cutpoint_result_adj, type = "diagnostic")
}, error = function(e) {
# Backup fallback logic if package binary system calls encounter environment locks
if ("cox_model" %in% names(cutpoint_result_adj)) {
fit <- cutpoint_result_adj$cox_model
test_ph <- survival::cox.zph(fit)
plot(test_ph, col = "#e31a1c", lwd = 2, resid = TRUE)
} else {
message("--> Diagnostic Track: Processing Schoenfeld fallback footprint.")
}
})
#> `geom_smooth()` using formula = 'y ~ x'Facet-wrapped Schoenfeld residual tracks to monitor the proportional hazards assumption.
plot_validation())Rather than relying on blocky grids, plot_validation()
allows us to project high-dimensional bootstrap convergence horizons
onto a smooth 2D continuous contour map, highlighting exactly where our
optimal threshold combinations cluster.
plot_validation(
validation_result_adj,
focus_cuts = c(1, 2),
main = "Adjusted Discovered Threshold Topology Landscape"
)return_data = TRUE)If you wish to bypass standard package plotting routines and extract
the raw, cleaned data frame with your newly minted group assignments,
set return_data = TRUE.
augmented_dataframe <- plot(cutpoint_result_adj, return_data = TRUE)
# View data mapping structure
head(augmented_dataframe[, c("factor", "time", "event", "group")])
#> factor time event group
#> 1 3.663065 15.616267 0 3
#> 2 1.697278 4.799947 0 1
#> 3 2.809707 12.657396 0 3
#> 4 1.688483 1.610941 1 1
#> 5 2.621937 9.534142 1 3
#> 6 2.378831 39.451622 0 3This vignette has demonstrated how to configure, discover, and
validate independent, covariate-adjusted survival thresholds using
OptSurvCutR. By factoring confounding adjusters into your
genetic search loops, you ensure your downstream clinical risk
stratification holds true prognostic value.
sessionInfo()
#> R version 4.6.0 (2026-04-24 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26200)
#>
#> Matrix products: default
#> LAPACK version 3.12.1
#>
#> locale:
#> [1] LC_COLLATE=English_United Kingdom.utf8
#> [2] LC_CTYPE=English_United Kingdom.utf8
#> [3] LC_MONETARY=English_United Kingdom.utf8
#> [4] LC_NUMERIC=C
#> [5] LC_TIME=English_United Kingdom.utf8
#>
#> time zone: Europe/London
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] OptSurvCutR_0.9.8 cli_3.6.6 knitr_1.51 patchwork_1.3.2
#> [5] survminer_0.5.2 ggpubr_0.6.3 ggplot2_4.0.3 survival_3.8-6
#> [9] dplyr_1.2.1
#>
#> loaded via a namespace (and not attached):
#> [1] gtable_0.3.6 xfun_0.58 bslib_0.11.0 rstatix_0.7.3
#> [5] lattice_0.22-9 vctrs_0.7.3 tools_4.6.0 generics_0.1.4
#> [9] parallel_4.6.0 tibble_3.3.1 pkgconfig_2.0.3 Matrix_1.7-5
#> [13] RColorBrewer_1.1-3 S7_0.2.2 lifecycle_1.0.5 compiler_4.6.0
#> [17] farver_2.1.2 codetools_0.2-20 carData_3.0-6 htmltools_0.5.9
#> [21] sass_0.4.10 yaml_2.3.12 Formula_1.2-5 pillar_1.11.1
#> [25] car_3.1-5 jquerylib_0.1.4 tidyr_1.3.2 MASS_7.3-65
#> [29] cachem_1.1.0 iterators_1.0.14 rgenoud_5.9-0.11 abind_1.4-8
#> [33] foreach_1.5.2 nlme_3.1-169 tidyselect_1.2.1 digest_0.6.39
#> [37] purrr_1.2.2 labeling_0.4.3 splines_4.6.0 fastmap_1.2.0
#> [41] grid_4.6.0 magrittr_2.0.5 broom_1.0.13 withr_3.0.2
#> [45] scales_1.4.0 backports_1.5.1 rmarkdown_2.31 otel_0.2.0
#> [49] gridExtra_2.3 ggsignif_0.6.4 evaluate_1.0.5 doParallel_1.0.17
#> [53] viridisLite_0.4.3 mgcv_1.9-4 rlang_1.2.0 Rcpp_1.1.1-1.1
#> [57] isoband_0.3.0 glue_1.8.1 rstudioapi_0.18.0 jsonlite_2.0.0
#> [61] R6_2.6.1