
Advanced Classification Workflows
Adam Mattsson
2026-04-28
advanced_classification_workflows.RmdIntroduction
This vignette demonstrates advanced parameter usage for the LundTax classifier, building upon the concepts introduced in the “Getting Started” guide. If you’re new to LundTaxR, we recommend starting with the basic tutorial first:
vignette("getting_started_with_LundTaxR", package = "LundTaxR")Who This Guide Is For
This advanced guide is designed for researchers and bioinformaticians who want to:
- Fine-tune classification parameters for optimal performance on their datasets
- Understand the impact of preprocessing and imputation settings
- Explore confidence scoring and progression risk thresholding
- Build reproducible analytical pipelines for improved consistency in research
What You’ll Learn
- Parameter optimization: Fine-tuning classification thresholds and confidence scores
- Custom classifier settings: Adapting the algorithm for specific data characteristics
- Performance evaluation: Assessing and improving classification accuracy
- Handling edge cases: Dealing with ambiguous or low-quality samples
1. Advanced Classifier Configuration
The classify_samples() function offers numerous
parameters that allow you to fine-tune the classification process for
optimal performance on your specific datasets. Understanding these
parameters and their interactions is crucial for achieving reliable and
reproducible results.
1.1 Classification Thresholds and Confidence Scoring
The LundTaxR classifier uses a two-stage approach: first classifying samples into 5 main classes (Uro, GU, BaSq, Mes, ScNE), then further classifying Uro samples into the following subgroups: UroA, UroB, and UroC. For each sample, the output shows the normalized subtype scores for each subtype, that sums to 1 for the main subtypes and within the Uro classes.
# Load example data
data("sjodahl_2017")
# Basic classification with default parameters
basic_results <- classify_samples(
this_data = sjodahl_2017,
verbose = TRUE
)
#>
#> ============================================================
#> LUND TAXONOMY CLASSIFIER
#> ============================================================
#> Data Check: The data is likely log2-transformed. The range
#> and distribution are consistent with log2-transformed data.
#> Input samples: 267
#> Input genes: 15697
#> Gene ID format: hgnc_symbol
#> ============================================================
#> SUBTYPE CLASSIFICATION
#> ============================================================
#> 1. Predicting 5-class subtypes
#> ------------------------------------------------------------
#> 2. Subclassifying Uro samples
#> ============================================================
#> SIGNATURE CALCULATION
#> ============================================================
#> 1. Calculating Proliferation Score
#> ------------------------------------------------------------
#> 2. Calculating Progression Score
#> ------------------------------------------------------------
#> 3. Applying Grade Predictor (WHO1999):
#> Total rules in classifier: 283
#> Rules with both genes present: 246
#> Rules removed: 37
#> Samples to score: 267
#> Grade threshold: 0.5
#> Grade labels: G1_2 / G3
#> ------------------------------------------------------------
#> 4. Applying Grade Predictor (WHO2022):
#> Total rules in classifier: 140
#> Rules with both genes present: 119
#> Rules removed: 21
#> Samples to score: 267
#> Grade threshold: 0.5
#> Grade labels: LG / HG
#> ------------------------------------------------------------
#> 5. Calculating Immune Signature Scores
#> ------------------------------------------------------------
#> 6. Calculating Stromal and Immune Composite Scores
#> ------------------------------------------------------------
#> 7. Merging Scores
#> ============================================================
#> CLASSIFICATION COMPLETE
#> ============================================================
#> UroA: 43 samples
#> UroB: 27 samples
#> UroC: 51 samples
#> GU: 54 samples
#> BaSq: 56 samples
#> Mes: 16 samples
#> ScNE: 20 samples
#> ------------------------------------------------------------
# Examine the prediction confidence scores
head(basic_results$subtype_scores)
#> Uro UroA UroB UroC GU BaSq Mes ScNE
#> 1.CEL 0.9924 0.9350 0.0048 0.0602 0.0072 0.0004 0.0000 0.0000
#> 2.CEL 0.0016 NA NA NA 0.0052 0.0054 0.9804 0.0074
#> 3.CEL 0.0682 NA NA NA 0.7754 0.0834 0.0390 0.0340
#> 4.CEL 0.9930 0.9334 0.0028 0.0638 0.0066 0.0004 0.0000 0.0000
#> 5.CEL 0.9808 0.0058 0.9878 0.0064 0.0076 0.0090 0.0006 0.0020
#> 6.CEL 0.0082 NA NA NA 0.9890 0.0002 0.0004 0.0022
#> prediction_delta_5_class prediction_delta_7_class
#> 1.CEL 0.9852 0.8748
#> 2.CEL 0.9730 NA
#> 3.CEL 0.6920 NA
#> 4.CEL 0.9864 0.8696
#> 5.CEL 0.9718 0.9814
#> 6.CEL 0.9808 NA
#> prediction_delta_collapsed
#> 1.CEL 0.8748
#> 2.CEL 0.9730
#> 3.CEL 0.6920
#> 4.CEL 0.8696
#> 5.CEL 0.9814
#> 6.CEL 0.9808The prediction_delta column shows the confidence of each
classification - the difference between the highest and second-highest
prediction scores. Higher delta values indicate more confident
predictions.
# Analyze prediction confidence
confidence_summary <- data.frame(
sample = names(basic_results$predictions_7classes),
subtype_7c = basic_results$predictions_7classes,
subtype_5c = basic_results$predictions_5classes,
confidence = basic_results$subtype_scores[, "prediction_delta_5_class"]
)
# Identify low-confidence predictions
low_confidence <- confidence_summary[confidence_summary$confidence < 0.05, ]
print(paste("Samples with low confidence (delta < 0.05):", nrow(low_confidence)))
#> [1] "Samples with low confidence (delta < 0.05): 0"
# Distribution of confidence scores by subtype
library(dplyr)
confidence_by_subtype <- confidence_summary %>%
group_by(subtype_7c) %>%
summarise(
mean_confidence = mean(confidence, na.rm = TRUE),
median_confidence = median(confidence, na.rm = TRUE),
min_confidence = min(confidence, na.rm = TRUE),
.groups = 'drop'
)
print(confidence_by_subtype)
#> # A tibble: 7 × 4
#> subtype_7c mean_confidence median_confidence min_confidence
#> <chr> <dbl> <dbl> <dbl>
#> 1 BaSq 0.942 0.984 0.650
#> 2 GU 0.878 0.926 0.573
#> 3 Mes 0.901 0.956 0.647
#> 4 ScNE 0.897 0.894 0.685
#> 5 UroA 0.983 0.998 0.717
#> 6 UroB 0.926 0.98 0.695
#> 7 UroC 0.939 0.969 0.6351.2 Data Preprocessing Parameters
Log Transformation Control
LundTaxR automatically detects whether your data is log2-transformed, but you can override this behavior. Note: Log2 transformation primarily affects signature score calculations, not the classifier predictions themselves.
⚠️ CRITICAL: Signature score calculations require
log2-transformed values. If your input data is already
log2-transformed and you set log_transform = TRUE, the
signature scores will be calculated on double-logged data, leading to
incorrect results. Conversely, if your data is raw (non-logged) and you
set log_transform = FALSE, signature scores will be
calculated on untransformed data, which is also incorrect.
# Example with already log-transformed data (like sjodahl_2017)
# Setting log_transform = TRUE would cause double-logging
double_logged_results <- classify_samples(
this_data = sjodahl_2017,
log_transform = TRUE, # INCORRECT: causes double log2 transformation
verbose = FALSE
)
# Correct approach for already log-transformed data
correct_results <- classify_samples(
this_data = sjodahl_2017,
log_transform = FALSE, # CORRECT: data is already log2-transformed
verbose = FALSE
)
# Compare classifier predictions (should be identical)
identical(basic_results$predictions_7classes, correct_results$predictions_7classes)
#> [1] TRUE
identical(basic_results$predictions_7classes, double_logged_results$predictions_7classes)
#> [1] TRUE
# Compare signature scores - double logging severely distorts scores
score_comparison <- data.frame(
sample = rownames(basic_results$scores)[1:5], # Show first 5 samples
correct_immune = correct_results$scores$immune141_up[1:5],
double_logged_immune = double_logged_results$scores$immune141_up[1:5],
ratio = correct_results$scores$immune141_up[1:5] / double_logged_results$scores$immune141_up[1:5]
)
print(score_comparison)
#> sample correct_immune double_logged_immune ratio
#> 1 1.CEL 4.274890 4.722811 0.9051578
#> 2 2.CEL 5.356312 5.208732 1.0283332
#> 3 3.CEL 5.500902 5.273202 1.0431805
#> 4 4.CEL 4.366632 4.772984 0.9148641
#> 5 5.CEL 4.527595 4.850548 0.9334192
# The impact is substantial
cat("Mean immune score - Correct:", round(mean(correct_results$scores$immune141_up), 3), "\n")
#> Mean immune score - Correct: 4.875
cat("Mean immune score - Double-logged:", round(mean(double_logged_results$scores$immune141_up), 3), "\n")
#> Mean immune score - Double-logged: 5.002Best Practice Guidelines: - For raw count
data (RNA-seq, microarray): Set
log_transform = TRUE - For already log-transformed
data: Set log_transform = FALSE - When
uncertain: Let LundTaxR auto-detect (recommended default
behavior) - Always validate: Check that signature score
ranges are reasonable for your data type
Score Adjustment and Normalization
The adjustment parameters are crucial for signature score calculations but do not affect the classifier subtype predictions. These parameters control how signature scores are normalized based on stable reference genes:
# Classification without score adjustment
no_adjustment <- classify_samples(
this_data = sjodahl_2017,
adjust = FALSE, # Skip normalization step for signatures
verbose = FALSE
)
# Custom adjustment factor (default is 5.1431)
custom_adjustment <- classify_samples(
this_data = sjodahl_2017,
adjust = TRUE,
adj_factor = 3.0, # Lower factor = more conservative signature scores
verbose = FALSE
)
# Classifier predictions remain identical
identical(basic_results$predictions_7classes, no_adjustment$predictions_7classes)
#> [1] TRUE
identical(basic_results$predictions_7classes, custom_adjustment$predictions_7classes)
#> [1] TRUE
# But signature scores differ significantly
signature_comparison <- data.frame(
sample = rownames(basic_results$scores),
default_luminal = basic_results$scores$b_cells,
no_adj_luminal = no_adjustment$scores$b_cells,
custom_adj_luminal = custom_adjustment$scores$b_cells
)
# Summary statistics show the impact on signature scores
summary(signature_comparison[, -1])
#> default_luminal no_adj_luminal custom_adj_luminal
#> Min. :3.069 Min. :4.336 Min. :1.790
#> 1st Qu.:3.431 1st Qu.:4.678 1st Qu.:2.001
#> Median :3.616 Median :4.836 Median :2.109
#> Mean :3.655 Mean :4.897 Mean :2.132
#> 3rd Qu.:3.829 3rd Qu.:5.033 3rd Qu.:2.233
#> Max. :4.683 Max. :6.147 Max. :2.7321.3 Missing Data Handling and Sample Rejection
The LundTaxR classifier always assigns a subtype to every sample, but the imputation parameters control how missing gene expression data is handled during the classification process:
# Strict imputation settings (conservative approach)
strict_imputation <- classify_samples(
this_data = sjodahl_2017,
impute = TRUE,
impute_kNN = 3, # Use only 3 nearest neighbors
impute_reject = 0.4, # Reject samples with >40% missing classification rules
verbose = FALSE
)
# Lenient imputation settings
lenient_imputation <- classify_samples(
this_data = sjodahl_2017,
impute = TRUE,
impute_kNN = 10, # Use 10 nearest neighbors
impute_reject = 0.8, # Accept samples with up to 80% missing classification rules
verbose = FALSE
)
# All approaches classify the same number of samples
comparison <- data.frame(
method = c("Default", "Strict", "Lenient"),
samples_classified = c(
length(basic_results$predictions_7classes),
length(strict_imputation$predictions_7classes),
length(lenient_imputation$predictions_7classes)
),
samples_input = rep(ncol(sjodahl_2017), 3)
)
print(comparison)
#> method samples_classified samples_input
#> 1 Default 267 267
#> 2 Strict 267 267
#> 3 Lenient 267 267Key Point: The impute_reject threshold
determines when a sample has too much missing data for reliable
classification, but in practice, the classifier will still assign a
subtype.
1.4 Performance Optimization
For large datasets or when you only need classification results, you can optimize performance:
# Minimal output - only subtypes (fastest)
minimal_results <- classify_samples(
this_data = sjodahl_2017,
subtype_only = TRUE, # Skip signature score calculations
include_data = FALSE, # Don't include input data in output
include_pred_scores = FALSE, # Skip prediction score matrix
verbose = FALSE
)
# Check what's returned
str(minimal_results)
#> List of 2
#> $ predictions_7classes: Named chr [1:267] "UroA" "Mes" "GU" "UroA" ...
#> ..- attr(*, "names")= chr [1:267] "1.CEL" "2.CEL" "3.CEL" "4.CEL" ...
#> $ predictions_5classes: Named chr [1:267] "Uro" "Mes" "GU" "Uro" ...
#> ..- attr(*, "names")= chr [1:267] "1.CEL" "2.CEL" "3.CEL" "4.CEL" ...
# Timing comparison for large datasets
system.time({
full_analysis <- classify_samples(sjodahl_2017, verbose = FALSE)
})
#> user system elapsed
#> 5.582 0.046 5.379
system.time({
minimal_analysis <- classify_samples(
sjodahl_2017,
subtype_only = TRUE,
include_pred_scores = FALSE,
verbose = FALSE
)
})
#> user system elapsed
#> 0.895 0.023 0.6611.5 Custom Progression Risk Thresholds
The classifier includes a progression risk score, a continuous score ranging from 0-1, that can be customized based on your clinical context. The default cutoff 0.56 classifies samples above the set threshold into High Risk (HR) and below the threshold to Low Risk (LR). You can adjust this threshold to be more conservative or sensitive. Note: The example dataset primarily consists of muscle-invasive bladder cancer (MIBC) cases, which represent tumors that have already progressed beyond the muscle layer. The progression risk score displayed here was originally developed and validated for non-muscle-invasive bladder cancer (NMIBC) patients to predict their probability of future progression to muscle-invasive disease.
# Conservative progression threshold
conservative_prog <- classify_samples(
this_data = sjodahl_2017,
threshold_progression = 0.7, # Higher threshold (more specific)
verbose = FALSE
)
# Sensitive progression threshold
sensitive_prog <- classify_samples(
this_data = sjodahl_2017,
threshold_progression = 0.5, # Lower threshold (more sensitive)
verbose = FALSE
)
# Compare progression classifications
prog_comparison <- data.frame(
sample = names(basic_results$predictions_7classes),
default_prog = basic_results$scores$progression_risk,
conservative_prog = conservative_prog$scores$progression_risk,
sensitive_prog = sensitive_prog$scores$progression_risk
)
# Convert to matrix for heatmap
prog_mat <- as.matrix(prog_comparison[, c("default_prog", "conservative_prog", "sensitive_prog")])
rownames(prog_mat) <- prog_comparison$sample
# Map HR/LR to colors
prog_colors <- c("HR" = "black", "LR" = "grey")
Heatmap(
t(prog_mat),
name = "Progression",
col = prog_colors,
cluster_rows = FALSE,
cluster_columns = FALSE,
show_row_names = TRUE,
show_column_names = FALSE,
height = unit(2, "cm"),
column_title = "Progression Risk Calls - Threshold Comparison\n(Black = High Risk, Grey = Low Risk)",
show_heatmap_legend = FALSE,
row_names_side = "left"
)
1.6 Quality Control and Validation
Always examine the missing genes information and prediction quality:
# Examine missing genes
missing_info <- basic_results$na_genes
print(paste("Genes missing from input data:", nrow(missing_info)))
#> [1] "Genes missing from input data: 67"
if(nrow(missing_info) > 0) {
head(missing_info)
}
#> genes signature process
#> 1 NUSAP1 proliferation up
#> 2 PRC1 proliferation up
#> 3 FAM72D proliferation up
#> 4 FAM72A proliferation up
#> 5 RNASEH2A proliferation up
#> 6 POLA2 proliferation up
# Identify samples with potential quality issues
quality_flags <- data.frame(
sample = names(basic_results$predictions_7classes),
confidence = basic_results$subtype_scores[, "prediction_delta_5_class"],
quality_flag = basic_results$subtype_scores[, "prediction_delta_5_class"] < 0.05
)
flagged_samples <- quality_flags[quality_flags$quality_flag, ]
if(nrow(flagged_samples) > 0) {
print("Samples flagged for low confidence:")
head(flagged_samples)
}Key Takeaways
- Start with default parameters and adjust based on your data characteristics
-
Monitor prediction confidence using the
prediction_deltascores - Validate imputation settings with your dataset’s missing data patterns
- Consider performance optimization for large-scale analyses
- Always examine quality control metrics before downstream analysis