Augmenting a Randomized Trial with External Controls: A LaLonde A-TMLE Tutorial

Overview

In Chapter 22, we discussed the Adaptive TMLE (A-TMLE) as a general approach to regularize the targeting step of a TMLE. In this tutorial, we look at a particular application of that general theoretical framework. Specifically, we discuss an estimator constructed based on A-TMLE that augments the randomized trial data with external arms to improve efficiency.

Randomized controlled trials (RCTs) provide internally valid treatment comparisons, but their precision is limited by the number of randomized participants. One possible way to improve precision is to supplement the randomized control arm with external controls. The difficulty is that external controls were not randomized and may differ from trial participants in both measured and unmeasured ways. Blindly pooling them with the randomized controls can therefore trade variance for substantial bias.

In this tutorial, we consider the National Supported Work (NSW) experiment analyzed by Lalonde (1986) and Dehejia & Wahba (1999). We retain both randomized NSW arms and consider augmenting the trial with CPS3, a nonexperimental control sample from the Current Population Survey (CPS). CPS3 contains unemployed men with low pre-program earnings, making it a plausible but still imperfect external comparison group for NSW participants.

We compare:

  • the unadjusted randomized difference in mean earnings;
  • a covariate-adjusted TMLE using only randomized participants;
  • naive pooling of randomized and CPS3 controls;
  • an A-TMLE data integration estimator that augments the randomized trial with CPS3 and corrects for any biases from pooling of the external controls.

Our goal is to explore and see whether an estimator based on A-TMLE can extract useful information from external controls to perform a favorable bias-variance trade-off and improve efficiency over trial-only estimators without inheriting the severe bias seen under naive pooling.

Why A-TMLE can help to improve efficiency

Let \(\mathcal{M}\) denote the prespecified statistical model and let \(\Psi(P_0)\) be the target parameter. A-TMLE learns a working model \(\mathcal{M}_n\) that approximates an oracle model \(\mathcal{M}_0\subseteq\mathcal{M}\) containing the true distribution \(P_0\). Rather than immediately targeting \(\Psi\) over the whole model, it targets the data-adaptive projection parameter

\[\Psi_{\mathcal{M}_n}(P)=\Psi\{\Pi_{\mathcal{M}_n}(P)\}.\]

Under the oracle-approximation, remainder, and stability conditions developed in the A-TMLE chapter and van der Laan et al. (2024), the resulting estimator has the expansion

\[\widehat\psi_{\mathrm{A\text{-}TMLE}}-\Psi(P_0) = (P_n-P_0)D_{\mathcal{M}_0,P_0}+o_P(n^{-1/2}),\]

because \(P_0\in\mathcal{M}_0\) implies \(\Psi_{\mathcal{M}_0}(P_0)=\Psi(P_0)\). Here, \(D_{\mathcal{M}_0,P_0}\) is the efficient influence function for the oracle projection parameter; with a likelihood or likelihood-behaving loss, it is also the efficient influence function obtained by acting as if the oracle model had been known in advance.

This leads to two useful regimes:

Limiting oracle model Asymptotic implication Finite-sample interpretation
\(\mathcal{M}_0=\mathcal{M}\) The influence function becomes the full-model efficient influence function, so the estimator is asymptotically regular and efficient in \(\mathcal{M}\). A smaller working model \(\mathcal{M}_n\) may still regularize an unstable targeting step and trade a small projection or model-selection bias for lower variance. This can be favorable when the full-model influence function is highly variable or heavy-tailed.
\(\mathcal{M}_0\subsetneq\mathcal{M}\) If the oracle-model efficiency bound is smaller, the A-TMLE can have asymptotic variance at \(P_0\) below the full-model efficiency bound. This super-efficiency is pointwise, as the estimator is regular for perturbations within the oracle model, but may have local asymptotic bias along nearby perturbations in the full model that leave the oracle model. The estimator adapts to a simpler structure and can deliver a real precision gain. However, such super-efficiency necessarily gives up regularity for the original target along local perturbations that leave \(\mathcal{M}_0\).

For many generic data-generating distributions, the learned models are expected to grow toward the full model, so that \(\mathcal{M}_0=\mathcal{M}\) is the ordinary limiting regime. The benefit therefore need not depend on a smaller oracle model, as finite-sample regularization may still improve the bias-variance trade-off. When \(\mathcal{M}_0\) is truly smaller, a real oracle efficiency gain is possible, but it exhibits pointwise super-efficiency rather than full-model regularity.

In some settings, it may be reasonable to trade a degree of regularity for improved efficiency. Regularity provides valuable robustness to local perturbations of the data-generating distribution, but that robustness may have limited practical value if every estimator that is regular over the full model is so variable that it has little power at the available sample size. A-TMLE provides a principled way to navigate this trade-off. By adapting to a data-driven working model, it can improve precision and power while retaining consistency, asymptotic linearity, and asymptotically valid pointwise inference under appropriate conditions.

Why this motivates RCT augmentation

Data integration is a natural application of A-TMLE because external outcome data create an explicit bias–variance trade-off. Incorporating these data may reduce variance by increasing the available information, but differences between the trial and external populations may also introduce bias. As discussed in Section 11 of Chapter 22 and in van der Laan et al. (2026), we may express the trial-population ATE as

\[\Psi_{\mathrm{RCT}}(P_0)=\widetilde\Psi(P_0)-\Psi^{\#}(P_0),\]

where \(\widetilde\Psi\) is a pooled-data treatment-effect component and \(\Psi^{\#}\) corrects the bias induced by differences between trial and external outcomes. In particular, the parameter \(\Psi^{\#}\) has an analytic form involving the conditional effect of trial enrollment on the outcome, give treatment and baseline covariates. We could then define an A-TMLE estimator that learns a working model for this conditional trial-enrollment effect.

If the trial-external discrepancy is absent or captured by a parsimonious function, the external outcomes can contribute substantial precision. As that discrepancy becomes more complex, the working model must become richer and the potential efficiency gain shrinks. In the nonparametric limit, the procedure approaches the protection offered by an efficient trial-only analysis. This adaptivity makes external-data augmentation interesting, as the method attempts to adaptively borrow according to learned compatibility rather than assuming exchangeability of the data sources.

Now, let’s look at an example of applying this A-TMLE estimator in practice.

Setup

library(atmle)
library(ggplot2)
library(sl3)
library(tmle)

set.seed(20260716)

The trial and external data

The Dehejia-Wahba data files are available from the NBER data archive. We use the NSW subset with 1974 earnings available, consisting of 185 randomized treated participants and 260 randomized controls. CPS3 contributes 429 additional untreated individuals.

The following code chunk downloads the randomized NSW arms and CPS3.

Show the data-download code
base_url <- "https://users.nber.org/~rdehejia/data/"

variable_names <- c(
  "treat", "age", "educ", "black", "hispan",
  "married", "nodegree", "re74", "re75", "re78"
)

W_nodes <- c("age", "educ", "black", "hispan",
             "married", "nodegree", "re74", "re75")

A_node <- "treat"
Y_node <- "re78"
S_node <- "S"

read_lalonde_file <- function(file) {
  data <- read.table(paste0(base_url, file))
  names(data) <- variable_names
  data
}

rct <- rbind(
  read_lalonde_file("nswre74_treated.txt"),
  read_lalonde_file("nswre74_control.txt")
)
rct$source <- "NSW randomized trial"
rct$S <- 1L

external <- read_lalonde_file("cps3_controls.txt")
external$source <- "CPS3"
external$S <- 0L

Variable definitions

Variable Role Definition
S Data source 1 for the randomized NSW trial; 0 for the external CPS data
treat Treatment 1 for assignment to NSW job training; 0 for no NSW training
re78 Outcome Real earnings recorded in 1978
age Baseline covariate Age in years
educ Baseline covariate Years of education
black, hispan Baseline covariates Race/ethnicity indicators
married Baseline covariate Indicator for being married
nodegree Baseline covariate Indicator for not having a high-school degree
re74, re75 Baseline covariates Real earnings in 1974 and 1975

An earnings value of zero is a valid observed value, not a missing value: the individual had no recorded earnings in that year. The distributions of re74, re75, and re78 consequently have a point mass at zero and a long right tail.

The CPS3 external cohort

CPS3 contains 429 untreated, unemployed men whose 1975 income was below the poverty level. Its restriction to people with low recent earnings makes it more relevant to the NSW trial than an unrestricted population sample, but it does not make CPS3 exchangeable with the randomized controls. The NBER archive notes that the downloadable CPS3 file is a reconstruction that is similar, but not identical, to LaLonde’s original comparison sample.

Descriptive comparison

We first compare the randomized arms and CPS3 before fitting any estimator.

Table 1: Trial and external-cohort summaries. Earnings are in the units supplied by the LaLonde files.
summarize_source <- function(data, label) {
  data.frame(
    Sample = label,
    N = nrow(data),
    `Age, mean` = mean(data$age),
    `Black, %` = 100 * mean(data$black),
    `Married, %` = 100 * mean(data$married),
    `RE74, mean` = mean(data$re74),
    `RE75, mean` = mean(data$re75),
    `RE78, mean` = mean(data$re78),
    `Zero RE78, %` = 100 * mean(data$re78 == 0),
    check.names = FALSE
  )
}

source_summary <- do.call(
  rbind,
  list(
    summarize_source(rct[rct$treat == 1, ], "NSW randomized treatment"),
    summarize_source(rct[rct$treat == 0, ], "NSW randomized control"),
    summarize_source(external, "CPS3")
  )
)

source_summary
                    Sample   N Age, mean Black, % Married, % RE74, mean
1 NSW randomized treatment 185  25.81622 84.32432   18.91892   2095.574
2   NSW randomized control 260  25.05385 82.69231   15.38462   2107.027
3                     CPS3 429  28.03030 20.27972   51.28205   5619.237
  RE75, mean RE78, mean Zero RE78, %
1   1532.055   6349.144     24.32432
2   1266.909   4554.801     35.38462
3   2466.484   6984.170     22.84382

The CPS3 restriction produces a sample with low pre-program earnings, but it does not make the sources exchangeable. For example, the proportion Black and the marriage rate remain very different. This is the setting in which a data-integration estimator must distinguish useful outcome information from source-related bias.

We can summarize the measured source differences with standardized mean differences (SMDs), comparing CPS3 with all randomized participants.

source_smd <- function(external, source_name) {
  smd <- vapply(W_nodes, function(variable) {
    pooled_sd <- sqrt(
      (stats::var(rct[[variable]]) + stats::var(external[[variable]])) / 2
    )
    if (pooled_sd == 0) return(0)
    (mean(external[[variable]]) - mean(rct[[variable]])) / pooled_sd
  }, numeric(1))

  data.frame(
    Source = source_name,
    Variable = factor(names(smd), levels = rev(W_nodes)),
    Absolute_SMD = abs(smd)
  )
}

smd_data <- source_smd(external, "CPS3")

ggplot(smd_data, aes(x = Absolute_SMD, y = Variable)) +
  geom_vline(xintercept = 0.1, linetype = 2, color = "grey55") +
  geom_point(size = 2.5, color = "#1F77B4") +
  labs(x = "Absolute standardized mean difference", y = NULL) +
  theme_minimal(base_size = 12)
Figure 1: Absolute standardized mean differences between CPS3 and the randomized NSW sample.

The conventional 0.1 SMD line is only a descriptive reference. Small measured differences would not establish conditional exchangeability. Here, the remaining imbalances reinforce that CPS3 should not simply be treated as an extension of the randomized control arm.

Target estimand

Let \(S=1\) identify randomized NSW participants, \(A\) denote randomized assignment, \(W\) denote the baseline covariates, and \(Y\) denote 1978 earnings. Our target is the average treatment effect in the randomized-trial population:

\[\psi_{\mathrm{RCT}} = E\{Y(1)-Y(0)\mid S=1\}.\]

This choice matters. The atmle implementation also reports an estimand averaged over the pooled trial-plus-external covariate distribution (the same estimand considered in van der Laan et al. (2026)). That is a different target. Throughout this tutorial, we retain only the output row labeled "Avg. over S=1", because it is directly comparable with the randomized benchmark.

Randomized-trial benchmarks

Unadjusted randomized estimator

Randomization identifies the trial-population effect without outcome modeling. The unadjusted estimator is the difference in mean 1978 earnings between the two randomized arms.

rct_treated <- rct[rct[[A_node]] == 1, ]
rct_control <- rct[rct[[A_node]] == 0, ]

unadjusted_psi <- mean(rct_treated[[Y_node]]) - mean(rct_control[[Y_node]])
unadjusted_se <- sqrt(
  var(rct_treated[[Y_node]]) / nrow(rct_treated) +
    var(rct_control[[Y_node]]) / nrow(rct_control)
)
unadjusted_ci <- unadjusted_psi + qnorm(c(0.025, 0.975)) * unadjusted_se

unadjusted_result <- data.frame(
  Estimator = "Unadjusted RCT",
  Estimate = unadjusted_psi,
  SE = unadjusted_se,
  Lower = unadjusted_ci[1],
  Upper = unadjusted_ci[2]
)

unadjusted_result
       Estimator Estimate       SE    Lower    Upper
1 Unadjusted RCT 1794.342 670.9965 479.2133 3109.471

The unadjusted estimate is 1794. It can be used as a benchmark for evaluating whether external-data integration is pulling the analysis away from the randomized evidence.

Covariate-adjusted TMLE using only the RCT

Next, we fit a conventional TMLE using only the 445 randomized participants. Baseline adjustment can improve precision without relying on any external outcomes.

set.seed(20260716)

rct_tmle_fit <- tmle::tmle(
  Y = rct[[Y_node]],
  A = rct[[A_node]],
  W = rct[W_nodes],
  family = "gaussian",
  Q.SL.library = c("SL.mean", "SL.glm", "SL.glmnet"),
  g.SL.library = c("SL.mean", "SL.glm", "SL.glmnet"),
  V.Q = 5,
  V.g = 5
)

rct_tmle_result <- data.frame(
  Estimator = "TMLE: RCT only",
  Estimate = rct_tmle_fit$estimates$ATE$psi,
  SE = sqrt(rct_tmle_fit$estimates$ATE$var.psi),
  Lower = rct_tmle_fit$estimates$ATE$CI[1],
  Upper = rct_tmle_fit$estimates$ATE$CI[2]
)

rbind(unadjusted_result, rct_tmle_result)
       Estimator Estimate       SE    Lower    Upper
1 Unadjusted RCT 1794.342 670.9965 479.2133 3109.471
2 TMLE: RCT only 1705.750 660.4374 411.3168 3000.184

The RCT-only TMLE remains close to the unadjusted randomized estimate but is more efficient due to covariate-adjustment. This gives us two trial-based reference points before any external outcomes enter the analysis.

What happens under naive pooling?

A tempting shortcut is to append the CPS controls to the randomized controls and then compare the randomized treated arm with this enlarged control group. This procedure does not account for source differences.

Table 2: Naive estimates obtained by pooling randomized and external controls. These are diagnostic comparisons, not recommended causal estimators.
pooled_controls <- rbind(rct_control, external)
naive_pooling <- data.frame(
  `External cohort` = "CPS3",
  `External N` = nrow(external),
  `External mean RE78` = mean(external$re78),
  `Naively pooled estimate` =
    mean(rct_treated$re78) - mean(pooled_controls$re78),
  check.names = FALSE
)

naive_pooling
  External cohort External N External mean RE78 Naively pooled estimate
1            CPS3        429            6984.17                281.7167

Even with a deliberately restricted external cohort, naive pooling pulls the estimate sharply away from the randomized benchmark. Additional controls are useful only if the analysis accounts adequately for differences between their outcome distribution and that of randomized controls.

A-TMLE data integration

Super Learner specification

For the nuisance regressions, we use a convex Super Learner combining penalized generalized linear models, multivariate adaptive regression splines, and Bayesian additive regression trees. The non-negative least-squares metalearner assigns non-negative weights that sum to one.

sl_library <- list(
  learners = list(
    glmnet = sl3::Lrnr_glmnet$new(),
    earth = sl3::Lrnr_earth$new(),
    dbarts = sl3::Lrnr_dbarts$new()
  ),
  metalearner = sl3::Lrnr_nnls$new(convex = TRUE)
)

Reusable fitting function

fit_atmle_source <- function(n_folds = 5L, seed = 20260716) {
  data <- rbind(rct, external)

  set.seed(seed)
  fit <- atmle::atmle_ate_fusion$new(
    data = data,
    S_node = S_node,
    W_nodes = W_nodes,
    A_node = A_node,
    Y_node = Y_node,
    family = "gaussian",
    n_folds = n_folds
  )

  fit$run(
    g_bar_method = sl_library,
    theta_method = sl_library,
    Pi_method = sl_library,
    Q_bar_method = sl_library,
    A_cate_args = list(
      max_degree = 3L,
      smoothness_orders = 1L,
      num_knots = 10L
    ),
    S_cate_args = list(
      max_degree = 3L,
      smoothness_orders = 1L,
      num_knots = 10L,
      force_A = TRUE
    ),
    verbose = FALSE
  )

  result <- fit$results[fit$results$param == "Avg. over S=1", ]
  result$external_n <- sum(data[[S_node]] == 0)
  result
}

Fit the CPS3 analysis

The CPS3 sample is small enough that we fit the estimator directly when rendering the tutorial.

atmle_results <- fit_atmle_source()

stopifnot(nrow(atmle_results) == 1L)

For the trial-population estimand, the implementation reports

\[\widehat\psi_{\text{A-TMLE}} = \widehat\psi_{\text{tilde}} - \widehat\psi_{\text{pound}},\]

where the second component adapts to estimated trial–external discrepancies. Examining both components helps distinguish a stable final estimate from accidental agreement caused by two unstable pieces.

Table 3: A-TMLE components for the trial-population estimand
atmle_components <- atmle_results[
  ,
  c("external_n", "psi_tilde", "psi_pound", "psi")
]
names(atmle_components) <- c(
  "External N", "Tilde component",
  "Pound component", "A-TMLE estimate"
)

atmle_components
  External N Tilde component Pound component A-TMLE estimate
2        429        1537.649       -214.2002         1751.85

Comparing all estimators

Table 4: RCT-only and A-TMLE estimates of the ATE in the randomized-trial population
atmle_comparison <- data.frame(
  Estimator = "A-TMLE + external controls",
  Estimate = atmle_results$psi,
  SE = atmle_results$se,
  Lower = atmle_results$lower,
  Upper = atmle_results$upper
)

comparison_results <- rbind(
  unadjusted_result,
  rct_tmle_result,
  atmle_comparison
)

comparison_order <- c(
  "Unadjusted RCT", "TMLE: RCT only",
  "A-TMLE + external controls"
)
comparison_results <- comparison_results[
  match(comparison_order, comparison_results$Estimator),
]
comparison_results$`SE ratio vs RCT TMLE` <-
  comparison_results$SE / rct_tmle_result$SE

comparison_results
                   Estimator Estimate       SE    Lower    Upper
1             Unadjusted RCT 1794.342 670.9965 479.2133 3109.471
2             TMLE: RCT only 1705.750 660.4374 411.3168 3000.184
3 A-TMLE + external controls 1751.850 632.8323 511.5210 2992.178
  SE ratio vs RCT TMLE
1            1.0159882
2            1.0000000
3            0.9582019
plot_results <- comparison_results
plot_results$Estimator <- factor(
  plot_results$Estimator,
  levels = rev(comparison_order)
)

ggplot(plot_results, aes(x = Estimator, y = Estimate)) +
  geom_hline(
    yintercept = unadjusted_psi,
    linetype = 2,
    color = "grey45"
  ) +
  geom_errorbar(aes(ymin = Lower, ymax = Upper), width = 0.15) +
  geom_point(size = 2.8, color = "#1F77B4") +
  coord_flip() +
  scale_y_continuous(
    expand = expansion(mult = c(0.05, 0.12))
  ) +
  labs(x = NULL, y = "Estimated effect on 1978 earnings") +
  theme_minimal(base_size = 12)
Figure 2: Point estimates and 95% Wald confidence intervals. The dashed line is the unadjusted randomized estimate.

The CPS3 A-TMLE estimate is 1752. It differs from the unadjusted randomized estimate by only 42 and from the RCT-only TMLE by 46. Its 95% confidence interval contains the unadjusted randomized point estimate. In contrast, naive pooling with the same CPS3 controls gives 282, illustrating why agreement cannot be achieved simply by appending external observations.

The estimated standard error decreases from 660 for the RCT-only TMLE to 633 for A-TMLE with CPS3, a reduction of about 4.2%. In this analysis, A-TMLE therefore preserves the randomized-trial point estimate while producing a modest estimated precision gain. Although this gain seems small, it may be consequential when a scientific conclusion depends on whether a confidence interval crosses a decision threshold. For example, after establishing noninferiority in a randomized trial, even a modest precision gain from external-control augmentation could help determine whether the evidence also supports treatment superiority.

Scope of the demonstration

The LaLonde data are useful because they provide a randomized benchmark together with a nonexperimental comparison sample measured on common variables. This tutorial focuses on augmentation with external controls because that is the setting we aim to illustrate and is common in, for example, pharmaceutical applications; it is not a limitation of the A-TMLE framework, which can also incorporate external treatment arms. Differences between NSW and CPS—including differences in eligibility, study setting, and outcome-measurement procedures—are precisely the types of source discrepancies that the overall bias function \(\Psi^{\#}\) is intended to capture. Rather than assuming that the trial and external outcomes are exchangeable, A-TMLE learns a working model for their conditional differences and estimates the corresponding bias correction. Finally, as in any real-data analysis, the true treatment effect is unknown. Nevertheless, under the randomized design, the unadjusted difference in means is an unbiased estimator of the trial treatment effect and therefore provides the best available empirical benchmark outside a simulation setting. For a full evaluation of A-TMLE, this real-data tutorial should be paired with simulations in which compatibility, overlap, and the true treatment effect can be controlled directly.

Reproducibility information

Show package versions
sessionInfo()
R version 4.5.2 (2025-10-31)
Platform: aarch64-apple-darwin20
Running under: macOS Tahoe 26.2

Matrix products: default
BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] C.UTF-8/C.UTF-8/C.UTF-8/C/C.UTF-8/C.UTF-8

time zone: America/Los_Angeles
tzcode source: internal

attached base packages:
[1] splines   stats     graphics  grDevices utils     datasets  methods  
[8] base     

other attached packages:
 [1] tmle_2.1.1          SuperLearner_2.0-40 gam_1.22-7         
 [4] foreach_1.5.2       nnls_1.6            glmnet_5.0         
 [7] Matrix_1.7-4        sl3_1.4.5           ggplot2_4.0.3      
[10] atmle_0.1.1        

loaded via a namespace (and not attached):
 [1] Rdpack_2.6.4          delayed_0.5.0         pROC_1.19.0.1        
 [4] rlang_1.3.0           magrittr_2.0.4        otel_0.2.0           
 [7] compiler_4.5.2        vctrs_0.7.3           reshape2_1.4.5       
[10] stringr_1.6.0         pkgconfig_2.0.3       shape_1.4.6.1        
[13] crayon_1.5.3          fastmap_1.2.0         backports_1.5.0      
[16] labeling_0.4.3        rmarkdown_2.30        prodlim_2025.04.28   
[19] purrr_1.2.0           xfun_0.55             WeightedROC_2020.1.31
[22] jsonlite_2.0.0        progress_1.2.3        recipes_1.3.1        
[25] uuid_1.2-1            parallel_4.5.2        prettyunits_1.2.0    
[28] R6_2.6.1              stringi_1.8.7         RColorBrewer_1.1-3   
[31] parallelly_1.46.0     rpart_4.1.24          lubridate_1.9.4      
[34] Rcpp_1.1.1            assertthat_0.2.1      iterators_1.0.14     
[37] hal9001_0.4.6         knitr_1.51            future.apply_1.20.1  
[40] R.utils_2.13.0        nnet_7.3-20           igraph_2.2.1         
[43] timechange_0.3.0      tidyselect_1.2.1      abind_1.4-8          
[46] yaml_2.3.12           timeDate_4051.111     codetools_0.2-20     
[49] listenv_0.10.0        lattice_0.22-7        tibble_3.3.0         
[52] plyr_1.8.9            withr_3.0.2           S7_0.2.1             
[55] ROCR_1.0-11           evaluate_1.0.5        future_1.68.0        
[58] survival_3.8-3        pillar_1.11.1         checkmate_2.3.3      
[61] stats4_4.5.2          generics_0.1.4        hms_1.1.4            
[64] scales_1.4.0          origami_1.0.7         plotmo_3.7.0         
[67] globals_0.18.0        class_7.3-23          glue_1.8.0           
[70] tools_4.5.2           data.table_1.18.0     ModelMetrics_1.2.2.2 
[73] rstackdeque_1.1.1     gower_1.0.2           dbarts_0.9-33        
[76] visNetwork_2.1.4      grid_4.5.2            plotrix_3.8-13       
[79] rbibutils_2.4         ipred_0.9-15          nlme_3.1-168         
[82] BBmisc_1.13           earth_5.3.5           Formula_1.2-5        
[85] cli_3.6.5             lava_1.8.2            dplyr_1.2.1          
[88] gtable_0.3.6          R.methodsS3_1.8.2     digest_0.6.39        
[91] caret_7.0-1           htmlwidgets_1.6.4     farver_2.1.2         
[94] htmltools_0.5.9       R.oo_1.27.1           lifecycle_1.0.5      
[97] hardhat_1.4.2         MASS_7.3-65