Introduction to triageR

library(triageR)
library(mlbench)

Overview

This vignette walks through a full clinical prediction modelling workflow using triageR, from raw data to a TRIPOD+AI-aligned report. We will use the PIMA Indians Diabetes dataset, a well-known real-world clinical dataset, to demonstrate each stage of the pipeline.

1. Load and prepare data

The PIMA dataset has a known data quality issue: several clinical measurements use 0 to represent a missing value, which we know is physiologically impossible for variables like blood pressure or BMI. We convert these to proper NAs before proceeding, this is a good example of the kind of cleaning tr_load_clinical() expects to receive data after.

data(PimaIndiansDiabetes)

pima <- PimaIndiansDiabetes
pima$id <- seq_len(nrow(pima))

pima$glucose[pima$glucose == 0]   <- NA
pima$pressure[pima$pressure == 0] <- NA
pima$triceps[pima$triceps == 0]   <- NA
pima$insulin[pima$insulin == 0]   <- NA
pima$mass[pima$mass == 0]         <- NA

pima_loaded <- tr_load_clinical(pima, id_col = "id")
pima_loaded
#> # A tibble: 768 × 10
#>    pregnant glucose pressure triceps insulin  mass pedigree   age diabetes    id
#>       <dbl>   <dbl>    <dbl>   <dbl>   <dbl> <dbl>    <dbl> <dbl> <fct>    <int>
#>  1        6     148       72      35      NA  33.6    0.627    50 pos          1
#>  2        1      85       66      29      NA  26.6    0.351    31 neg          2
#>  3        8     183       64      NA      NA  23.3    0.672    32 pos          3
#>  4        1      89       66      23      94  28.1    0.167    21 neg          4
#>  5        0     137       40      35     168  43.1    2.29     33 pos          5
#>  6        5     116       74      NA      NA  25.6    0.201    30 neg          6
#>  7        3      78       50      32      88  31      0.248    26 pos          7
#>  8       10     115       NA      NA      NA  35.3    0.134    29 neg          8
#>  9        2     197       70      45     543  30.5    0.158    53 pos          9
#> 10        8     125       96      NA      NA  NA      0.232    54 pos         10
#> # ℹ 758 more rows

2. Check and handle missing data

tr_check_missing(pima_loaded)
#> Missing data summary:
#> - 5 of 10 columns have missing values
#> - 768 total rows
#> # A tibble: 10 × 3
#>    column   n_missing pct_missing
#>    <chr>        <int>       <dbl>
#>  1 insulin        374        48.7
#>  2 triceps        227        29.6
#>  3 pressure        35         4.6
#>  4 mass            11         1.4
#>  5 glucose          5         0.7
#>  6 pregnant         0         0  
#>  7 pedigree         0         0  
#>  8 age              0         0  
#>  9 diabetes         0         0  
#> 10 id               0         0

insulin and triceps show substantial missingness — a realistic scenario in clinical datasets. We impute using mice:

pima_for_impute <- pima_loaded[, setdiff(names(pima_loaded), "id")]
pima_imputed <- tr_impute(pima_for_impute, method = "mice", m = 5)
sum(is.na(pima_imputed))
#> [1] 0

3. Fit a model

We split the data into training and test sets, then fit a logistic regression model.

set.seed(42)
n <- nrow(pima_imputed)
train_idx <- sample(seq_len(n), size = floor(0.7 * n))

pima_train <- pima_imputed[train_idx, ]
pima_test  <- pima_imputed[-train_idx, ]

pima_model <- tr_fit(pima_train, outcome = "diabetes", engine = "logistic_reg")
#> Model fitted successfully using engine: logistic_reg

triageR also supports "random_forest" and "boost_tree" engines via the same interface — useful for comparing approaches without rewriting your pipeline:

pima_model_rf  <- tr_fit(pima_train, outcome = "diabetes", engine = "random_forest")
#> Model fitted successfully using engine: random_forest
pima_model_xgb <- tr_fit(pima_train, outcome = "diabetes", engine = "boost_tree")
#> Model fitted successfully using engine: boost_tree

4. Validate the model

Validation on a genuine holdout set produces discrimination metrics, a confusion matrix, and an ROC curve. We validate all three engines on the same holdout set to compare performance.

pima_validation     <- tr_validate(pima_model, newdata = pima_test)
#> Validation metrics (newdata):
#> # A tibble: 4 × 3
#>   .metric  .estimator .estimate
#>   <chr>    <chr>          <dbl>
#> 1 roc_auc  binary         0.835
#> 2 sens     binary         0.587
#> 3 spec     binary         0.865
#> 4 accuracy binary         0.775
#> 
#> Confusion Matrix:
#>           Truth
#> Prediction neg pos
#>        neg 135  31
#>        pos  21  44

pima_validation_rf  <- tr_validate(pima_model_rf, newdata = pima_test)
#> Validation metrics (newdata):
#> # A tibble: 4 × 3
#>   .metric  .estimator .estimate
#>   <chr>    <chr>          <dbl>
#> 1 roc_auc  binary         0.829
#> 2 sens     binary         0.613
#> 3 spec     binary         0.853
#> 4 accuracy binary         0.775
#> 
#> Confusion Matrix:
#>           Truth
#> Prediction neg pos
#>        neg 133  29
#>        pos  23  46

pima_validation_xgb <- tr_validate(pima_model_xgb, newdata = pima_test)
#> Validation metrics (newdata):
#> # A tibble: 4 × 3
#>   .metric  .estimator .estimate
#>   <chr>    <chr>          <dbl>
#> 1 roc_auc  binary         0.828
#> 2 sens     binary         0.587
#> 3 spec     binary         0.846
#> 4 accuracy binary         0.762
#> 
#> Confusion Matrix:
#>           Truth
#> Prediction neg pos
#>        neg 132  31
#>        pos  24  44

pima_validation$roc_plot

Comparing engines

engine_comparison <- dplyr::bind_rows(
  dplyr::mutate(pima_validation$metrics, engine = "logistic_reg"),
  dplyr::mutate(pima_validation_rf$metrics, engine = "random_forest"),
  dplyr::mutate(pima_validation_xgb$metrics, engine = "boost_tree")
)

tidyr::pivot_wider(engine_comparison, names_from = .metric, values_from = .estimate)
#> # A tibble: 3 × 6
#>   .estimator engine        roc_auc  sens  spec accuracy
#>   <chr>      <chr>           <dbl> <dbl> <dbl>    <dbl>
#> 1 binary     logistic_reg    0.835 0.587 0.865    0.775
#> 2 binary     random_forest   0.829 0.613 0.853    0.775
#> 3 binary     boost_tree      0.828 0.587 0.846    0.762
pima_validation_rf$roc_plot

pima_validation_xgb$roc_plot

For the remainder of this vignette, we continue with the logistic regression model for explainability and reporting, as it offers the most directly interpretable coefficients for clinical use.

5. Explain the model

tr_explain(pima_model, method = "permutation")

Glucose consistently emerges as the strongest predictor of diabetes in this dataset, consistent with established clinical literature.

6. Automated pipeline review

tr_agent_review() checks for common clinical modelling pitfalls: class imbalance, low events-per-variable ratio, possible data leakage, and small sample size.

pima_review <- tr_agent_review(pima_train, pima_model, use_agent = FALSE)
#> 
#> --- triageR Pipeline Review ---
#> 
#> No major issues flagged.

7. Sensitivity analysis

pima_sensitivity <- tr_sensitivity(pima_train, pima_model)
#> Model fitted successfully using engine: logistic_reg
#> Validation metrics (training_data):
#> # A tibble: 4 × 3
#>   .metric  .estimator .estimate
#>   <chr>    <chr>          <dbl>
#> 1 roc_auc  binary         0.850
#> 2 sens     binary         0.575
#> 3 spec     binary         0.884
#> 4 accuracy binary         0.773
#> 
#> Confusion Matrix:
#>           Truth
#> Prediction neg pos
#>        neg 304  82
#>        pos  40 111

#> Model fitted successfully using engine: logistic_reg
#> Validation metrics (training_data):
#> # A tibble: 4 × 3
#>   .metric  .estimator .estimate
#>   <chr>    <chr>          <dbl>
#> 1 roc_auc  binary         0.860
#> 2 sens     binary         0.611
#> 3 spec     binary         0.890
#> 4 accuracy binary         0.792
#> 
#> Confusion Matrix:
#>           Truth
#> Prediction neg pos
#>        neg 290  68
#>        pos  36 107

#> 
#> --- Sensitivity Analysis Comparison ---
#> # A tibble: 2 × 6
#>   scenario          .estimator roc_auc  sens  spec accuracy
#>   <chr>             <chr>        <dbl> <dbl> <dbl>    <dbl>
#> 1 complete_case     binary       0.850 0.575 0.884    0.773
#> 2 outliers_excluded binary       0.860 0.611 0.890    0.792
#> 
#> Note: Compare AUC/sensitivity/specificity across scenarios. Large swings suggest the model is not robust to that assumption.

8. Generate a TRIPOD+AI-aligned report

Finally, all of the above can be compiled into a single reproducible report, aligned with TRIPOD+AI reporting guidance.

tr_tripod_report(
  model = pima_model,
  validation = pima_validation,
  review = pima_review,
  sensitivity = pima_sensitivity,
  output_file = file.path(tempdir(), "pima_diabetes_report"),
  format = "html"
)

Optional: AI-assisted method recommendation

If a Gemini API key is configured (see ?tr_recommend_method), triageR can also suggest an appropriate statistical or ML approach based on the structure of your data:

tr_recommend_method(
  pima_train,
  outcome = "diabetes",
  context = "predicting diabetes onset in adult women"
)

Summary

This vignette covered the full triageR workflow: loading and cleaning clinical data, handling missingness, fitting and validating a model, explainability, automated pipeline review, sensitivity analysis, and reproducible reporting, all using a real clinical dataset.