Introduction to TwoCutoff

2026-06-20

Overview

This vignette introduces the TwoCutoff package, a software tool used in Alzheimer’s disease research to determine biomarker cutoff values. The package uses a two-stage process: first, it identifies biomarker threshold values that separate diseased and non-diseased individuals; second, it evaluates their diagnostic performance. Instead of using a single cutoff, the method defines two cutoffs, creating three groups: non-diseased individuals, diseased individuals, and an intermediate ‘gray zone’ for cases that cannot be classified with enough certainty. The package includes functions for:

Workflow Overview

The diagram below shows the overall pipeline implemented in our package:

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

We demonstrate the workflow using simulated data representing an Alzheimer’s disease biomarker study. The binary outcome variable (DISEASE) indicates disease status, while the phosphorylated tau (PTAU) biomarke is generated with higher mean values in diseased subjects than in non-diseased subjects. This setup mimics real clinical biomarker behavior, where elevated biomarker concentrations are associated with increased probability of disease. Additional variables including AGE, SEX, body mass index (BMI), hypertension (HTN), and diabetes mellitus (DM) are included as potential confounders in the risk-adjustment model.


 n <- 1000

 # Set seed for reproducibility
 set.seed(123)

 # Outcome
 DISEASE <- rbinom(n, 1, 0.35)

 # Biomarker depends on disease
 PTAU <- rnorm(n, mean = 25 + 10 * DISEASE, sd = 8)

 # Confounders (optional)
 AGE <- round(rnorm(n, 35, 7))
 SEX <- sample(c(0, 1), n, replace = TRUE)
 BMI <- rnorm(n, 25, 4)
 HTN <- rbinom(n, 1, 0.3)
 DM <- rbinom(n, 1, 0.2)

 df <- data.frame(PTAU, AGE, SEX, BMI, HTN, DM, DISEASE)

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

Adjusted Risk Model

The adjust_score() function implements a two-stage statistical and machine learning framework for estimating disease risk from biomarker data while accounting for clinical confounders.

The method is designed for situations where:

Stage 1 — Finite Mixture Model (FMM)

In the first stage, the biomarker distribution is modeled using a two-component Gaussian mixture model.

The assumption is that the observed biomarker values arise from two latent populations:

The model estimates:

The Expectation-Maximization (EM) algorithm is used to iteratively estimate these parameters.

After estimating the mixture model parameters for every subject, the model calculates a posterior probability of disease:

\[P(\text{Disease} \mid x)=\hat{p}(x)=\dfrac{\hat{\pi}\cdot N(x\mid \hat{\mu_2}, \hat{\sigma_2})}{\hat{\pi}\cdot N(x\mid \hat{\mu_2}, \hat{\sigma_2}) + (1-\hat{\pi})\cdot N(x\mid \hat{\mu_1}, \hat{\sigma_1})},\] where:

Output:

This probability is stored as p_disease.

Interpretation:

Thus, the first stage transforms the raw biomarker into a probabilistic disease score.

Stage 2 — Confounder-Adjusted Risk Model

In the second stage, the function builds a supervised binary classification model using XGBoost to estimate a confounder-adjusted probability of disease. The binary outcome variable (0 = non-diseased, 1 = diseased) is modeled using the biomarker-derived posterior probability (p_disease) from Stage 1 together with user-specified confounders such as AGE, SEX, BMI, HTN, or DM.

This stage considers other clinical factors besides the biomarker when estimating disease risk.

The function supports two training strategies:

When validation is enabled:

Model complexity is optimized through cross-validation with early stopping.

By default, the function uses predefined XGBoost hyperparameters including:

However, users may also provide their own hyperparameter settings through the xgb_params argument to customize the learning process and tune model performance.

The final output of this stage is the adjusted_risk score, representing the confounder-adjusted probability of disease for each subject, with values ranging from 0 to 1.

Function Usage

adjust_score(
  data_set,
  biomarker,
  outcome,
  confounders = NULL,
  k = 2,
  maxit = 1000,
  epsilon = 1e-08,
  CompCase = FALSE,
  xgb_params = NULL,
  validate = FALSE,
  test_size = 0.2,
  seed = 123
)

Key arguments include:

We now apply the adjust_score() function to estimate biomarker-based disease probabilities and confounder-adjusted risk scores.

res <- adjust_score(
  df,
  biomarker = "PTAU", outcome = "DISEASE",
  confounders = c("AGE","SEX","BMI","HTN","DM"))
#> WARNING! NOT CONVERGENT! 
#> number of iterations= 1000
#> Setting levels: control = 0, case = 1
#> Setting direction: controls < cases
head(res$data[,c("PTAU","p_disease","adjusted_risk")])
#>       PTAU p_disease adjusted_risk
#> 1 20.18486 0.1915690     0.2355698
#> 2 27.05041 0.4768597     0.3722855
#> 3 33.21428 0.6628730     0.4702801
#> 4 41.00849 0.9143770     0.5774739
#> 5 22.92667 0.2647666     0.3495702
#> 6 24.23882 0.3673505     0.2907061

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

Percentile-Based Cutoff Derivation

The derive_cutoffs_percentile() function derives lower and upper biomarker cutoffs using percentile-based thresholds from the confounder-adjusted disease risk scores generated by adjust_score().

The method creates a three-zone diagnostic framework:

Method Overview

The function first separates subjects into low-risk and high-risk groups using the adjusted_risk scores.

Low-Risk Group and Rule-Out Cutoff

Subjects with adjusted_risk < low_thresh are assigned to the low-risk group. By default low_thresh = 0.05, meaning subjects with estimated disease probability below 5% are considered unlikely to have disease. The lower biomarker cutoff (c_L) is then defined as the 80th percentile of biomarker values within this low-risk group.

High-Risk Group and Rule-In Cutoff

Subjects with adjusted_risk >= high_thresh are assigned to the high-risk group. By default high_thresh = 0.70, meaning subjects with estimated disease probability of at least 70% are considered likely to have disease. The upper biomarker cutoff (c_H) is defined as the 20th percentile of biomarker values within this high-risk group. This cutoff is intended to maximize specificity for ruling in disease.

Fallback Behavior for Small Datasets

When fewer than five subjects are available in either group, the function automatically uses fallback cutoffs based on the overall biomarker distribution:

5th percentile for the lower cutoff (c_L), 95th percentile for the upper cutoff (c_H).

In small datasets, the number of subjects below low_thresh = 0.05 or above high_thresh = 0.70 may be too small to estimate reliable cutoff values. In such cases, users may consider selecting different low_thresh or high_thresh values depending on dataset size.

Classification Rules

After estimating the two cutoffs:

Function Usage

derive_cutoffs_percentile(
  adjust_result,
  biomarker,
  low_thresh = 0.05,
  high_thresh = 0.70
)

Key arguments include:

We now derive percentile-based biomarker cutoffs using the adjusted disease risk scores.

cutoffs_p <- derive_cutoffs_percentile(
  res,
  biomarker = "PTAU"
)
#> Number of cases below low_thresh = 0.05 is too small, so fallback to 5th percentile of biomarker was used. Consider using another low_thresh value if your dataset is small.
#> Number of cases above high_thresh = 0.7 is too small, so fallback to 95th percentile of biomarker was used. Consider using another high_thresh value if your dataset is small.

cutoffs_p$c_L
#> [1] 13.43517
cutoffs_p$c_H
#> [1] 43.82378

table(cutoffs_p$class)
#> 
#> Indeterminate      Negative      Positive 
#>           900            50            50

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

ROC-Based Cutoff Derivation

The derive_cutoffs_sensspec() function derives biomarker cutoffs using ROC analysis of the confounder-adjusted generated by adjust_score().

Unlike the percentile-based method, this approach selects cutoffs according to target diagnostic performance measures:

The method again creates three diagnostic regions:

ROC Curve Construction

The function first computes a ROC curve using:

The ROC curve evaluates the ability of these risk scores to discriminate between diseased and non-diseased subjects across a range of possible classification thresholds.

For each possible risk threshold:

At every threshold, the function calculates:

The ROC curve summarizes the trade-off between sensitivity and specificity across all thresholds.

Selection of Rule-Out and Rule-In Thresholds

After constructing the ROC curve, the function identifies two clinically meaningful thresholds on the adjusted_risk scale:

Rule-Out Threshold (T_L)

The lower threshold is selected to achieve a target sensitivity (sens_target).

By default:

meaning the threshold is chosen so that at least 80% of diseased subjects are correctly identified. This threshold prioritizes minimizing false negatives and is therefore used for ruling out disease.

Rule-In Threshold (T_H)

The upper threshold is selected to achieve a target specificity (spec_target). By default:

meaning the threshold is chosen so that at least 80% of non-diseased subjects are correctly identified. This threshold prioritizes minimizing false positives and is therefore used for ruling in disease.

Mapping Risk Thresholds Back to Biomarker Values

The ROC analysis produces thresholds on the adjusted_risk probability scale rather than the original biomarker scale. To obtain clinically interpretable biomarker cutoffs, the function maps these thresholds back to biomarker values. When the relationship between biomarker values and adjusted risk is approximately monotone, the function fits a LOESS regression model:

adjusted_risk ~ biomarker

and uses interpolation to estimate biomarker values corresponding to the selected ROC thresholds. If a non-monotone relationship is detected between biomarker values and adjusted risk scores, the function automatically switches to isotonic regression to enforce a monotone relationship and improve stability of the cutoff estimation process.

Classification Rules

After estimating the lower and upper cutoffs:

Function Usage

derive_cutoffs_sensspec(
  adjust_result,
  biomarker,
  outcome,
  sens_target = 0.8,
  spec_target = 0.8
)

Key arguments include:

We now derive ROC-based biomarker cutoffs using the adjusted disease risk scores.

cutoffs_s <- derive_cutoffs_sensspec(
  res,
  biomarker = "PTAU",
  outcome = "DISEASE",
  sens_target = 0.80,
  spec_target = 0.80
)
#> Warning in derive_cutoffs_sensspec(res, biomarker = "PTAU", outcome =
#> "DISEASE", : Sensitivity target not achievable -> using 5th percentile of
#> adjusted risk as fallback threshold.
#> Warning in derive_cutoffs_sensspec(res, biomarker = "PTAU", outcome =
#> "DISEASE", : Specificity target not achievable -> using 95th percentile of
#> adjusted risk as fallback threshold.

cutoffs_s$c_L
#> [1] 13.05203
cutoffs_s$c_H
#> [1] 45.36341

table(cutoffs_s$class)
#> 
#> Indeterminate      Negative      Positive 
#>           921            46            33

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

Performance Evaluation

The evaluate_performance() function summarizes the diagnostic performance of the derived two-cutoff classification system.

The function compares the predicted classification results against the true disease labels and computes commonly used diagnostic accuracy measures.

Performance Metrics

The following metrics are calculated:

Interpretation

The two-cutoff approach aims to classify subjects only when the prediction is more certain, because subjects with biomarker values between the lower and upper cutoffs are placed in the indeterminate group instead of being forced into positive or negative classifications. As a result:

This means the method prefers more reliable classifications, even if some subjects remain unclassified.

Function Usage

evaluate_performance(
  cutoff_result,
  outcome
)

Key arguments include:

We now evaluate the diagnostic performance of the percentile-based cutoff method.

perf_p <- evaluate_performance(
  cutoffs_p,
  outcome = "DISEASE"
)

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

#> Warning: 'xfun::attr()' is deprecated.
#> Use 'xfun::attr2()' instead.
#> See help("Deprecated")

#> Warning: 'xfun::attr()' is deprecated.
#> Use 'xfun::attr2()' instead.
#> See help("Deprecated")
Sensitivity Specificity PPV NPV Percent_Indeterminate
1 0.806 0.76 1 90

C:9hmIP5cac4c4e460b_to_TwoCutoff.R We can similarly evaluate the ROC-based cutoff method.

perf_s <- evaluate_performance(
  cutoffs_s,
  outcome = "DISEASE"
)

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

#> Warning: 'xfun::attr()' is deprecated.
#> Use 'xfun::attr2()' instead.
#> See help("Deprecated")

#> Warning: 'xfun::attr()' is deprecated.
#> Use 'xfun::attr2()' instead.
#> See help("Deprecated")
Sensitivity Specificity PPV NPV Percent_Indeterminate
1 0.885 0.818 1 92.1

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

Comparison of Cutoff Methods

The compare_performance() function summarizes and compares the diagnostic performance of the percentile-based and ROC-based cutoff approaches.

The function combines:

into a single summary table for easier comparison between methods.

Interpretation

Different cutoff derivation methods may produce different trade-offs between diagnostic accuracy and the proportion of indeterminate subjects.

For example:

Function Usage

compare_performance(
  percentile_result,
  sensspec_result,
  outcome
)

Key arguments include:

We now compare the percentile-based and ROC-based cutoff methods.

comparison <- compare_performance(
  cutoffs_p,
  cutoffs_s,
  outcome = "DISEASE"
)

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

#> Warning: 'xfun::attr()' is deprecated.
#> Use 'xfun::attr2()' instead.
#> See help("Deprecated")

#> Warning: 'xfun::attr()' is deprecated.
#> Use 'xfun::attr2()' instead.
#> See help("Deprecated")
Method c_L c_H Sensitivity Specificity PPV NPV Percent_Indeterminate
Percentile-based 13.435 43.824 1 0.806 0.760 1 90.0
ROC-based 13.052 45.363 1 0.885 0.818 1 92.1

C:9hmIP5cac4c4e460b_to_TwoCutoff.R ## Visualization of Two-Cutoff Classification

The plot_two_cutoff() function provides a graphical visualization of the two-cutoff biomarker classification framework.

The function displays:

The visualization helps assess how effectively the derived biomarker cutoffs separate diseased and non-diseased subjects.

Classification Regions

Subjects are classified into three diagnostic groups according to their biomarker values:

This framework avoids forcing uncertain subjects into positive or negative categories when biomarker values fall within overlapping regions.

Plot Structure

The function creates a scatter-style visualization using jittered biomarker values:

By default:

Confusion Matrix Visualization

When add_table = TRUE, the function additionally displays a confusion matrix table below the plot.

The confusion table summarizes agreement between:

This allows quick visual assessment of:

Confusion tables are currently supported only for single-panel visualizations.

Multi-Panel Visualization

The function also supports side-by-side comparison of multiple cutoff strategies by allowing multiple pairs of lower and upper cutoffs.

This is useful for comparing:

Each panel displays a separate cutoff configuration while sharing the same biomarker dataset.

Function Usage

plot_two_cutoff(
  cutoff_result,
  biomarker,
  outcome,
  panel_title = NULL,
  negative_label = "Control",
  positive_label = "Disease",
  add_table = NULL
)

Key arguments include:

Single-Method Visualization

We first visualize the percentile-based cutoff classification.

plot_two_cutoff(
  cutoffs_p,
  biomarker = "PTAU",
  outcome = "DISEASE"
)

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

Comparison of Multiple Cutoff Methods

The function can also compare multiple cutoff strategies side-by-side.

Below, we compare percentile-based and ROC-based cutoff methods using the same biomarker dataset.

# Create combined plotting object
cut_plot <- cutoffs_p

cut_plot$c_L <- c(cutoffs_p$c_L, cutoffs_s$c_L)
cut_plot$c_H <- c(cutoffs_p$c_H, cutoffs_s$c_H)

plot_two_cutoff(
  cut_plot,
  biomarker = "PTAU",
  outcome = "DISEASE",
  panel_title = c("Percentile-based", "ROC-based")
)

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

Interpretation

The visualization allows direct comparison of how different cutoff derivation methods partition subjects into:

groups.

Differences between panels may reflect alternative trade-offs between sensitivity, specificity, and gray-zone size.

Decision Curve Analysis

The dca_analysis() function evaluates the clinical usefulness of the confounder-adjusted risk model using Decision Curve Analysis (DCA).

Unlike traditional performance metrics such as sensitivity or specificity, DCA evaluates whether using a prediction model improves clinical decision-making across different risk thresholds.

Net Benefit

Decision Curve Analysis is based on the concept of net benefit (NB), which balances:

The net benefit at threshold \(t\) is defined as:

\[ NB(t)=\frac{TP(t)}{N}-\frac{FP(t)}{N}\cdot\frac{t}{1-t} \]

where:

Higher net benefit values indicate better clinical utility.

Strategies Compared

The function compares three clinical decision strategies:

The prediction model is considered clinically useful when its net benefit curve lies above both the "Treat All" and "Treat None" strategies.

Function Usage

dca_analysis(
  adjust_result,
  outcome,
  thresholds = seq(0.01, 0.60, by = 0.01)
)

Key arguments include:

Decision Curve Analysis Example

dca_out <- dca_analysis(
  res,
  outcome = "DISEASE"
)

head(dca_out$data)
#>   Threshold  NB_model    NB_all NB_none
#> 1      0.01 0.3414141 0.3414141       0
#> 2      0.02 0.3346939 0.3346939       0
#> 3      0.03 0.3278351 0.3278351       0
#> 4      0.04 0.3208333 0.3208333       0
#> 5      0.05 0.3136842 0.3136842       0
#> 6      0.06 0.3063830 0.3063830       0

print(dca_out$plot)

C:9hmIP5cac4c4e460b_to_TwoCutoff.R

Interpretation

The blue "Model" curve represents the net benefit obtained when clinical decisions are based on the confounder-adjusted risk model.

The green "Treat All" curve represents the strategy of treating all subjects as diseased, while the red "Treat None" curve represents the strategy of treating no subjects.

The model is clinically useful at thresholds where the "Model" curve lies above both "Treat All" and "Treat None".

In this example, the model maintains a consistently higher net benefit across a broad range of risk thresholds, indicating improved clinical decision-making compared with the default strategies.

At lower thresholds, the "Treat All" strategy performs similarly to the model because most subjects would be classified as positive regardless of predicted risk. However, as the threshold increases, the net benefit of "Treat All" decreases substantially due to increasing false positive classifications.

In contrast, the prediction model maintains positive net benefit across a wide range of risk thresholds, indicating better identification of diseased and non-diseased subjects compared with default clinical strategies.