12  Finding associations between clinical variables and spatial features

When it comes to biological datasets, the end goal is either mechanistic or translational. For example, if we had a mechanistic end goal, we might want to find what genes are differentially expressed between two conditions, and further aim to characterise the pathways which lead to this differential expression. Alternatively, if the end goal is translational, we might want to use a biological dataset that can be relatively cheaply obtained (e.g. IMC) to predict whether a patient’s disease will progress or not progress (e.g. metastasize in cancer).

# set parameters
set.seed(51773)
use_mc <- TRUE
if (use_mc) {
  nCores <- max(parallel::detectCores()/2, 1)
} else {
  nCores <- 1
}
BPPARAM <- simpleSeg:::generateBPParam(nCores)
theme_set(theme_classic())

12.1 Introduction to ClassifyR

ClassifyR provides a structured pipeline for cross-validated classification. Classification is viewed in terms of four stages: data transformation, feature selection, classifier training, and prediction. The driver functions crossValidate and runTests implements varieties of cross-validation. They are:

  • Permutation of the order of samples followed by k-fold cross-validation
  • Repeated \(x\%\) test set cross-validation
  • leave-k-out cross-validation

We will use the Keren 2018 dataset to perform two predictions: 1) predict the patient’s tumour type (compartmentalised or mixed) and 2) predict the patient’s survival outcome.

# load the Keren 2018 dataset
kerenSPE <- SpatialDatasets::spe_Keren_2018()

# remove any missing data in our outcome columns
kerenSPE = kerenSPE[, complete.cases(colData(kerenSPE)[, c("Censored", "Survival_days_capped*",
                                                           "tumour_type")])]
# load pre-computed data for efficiency
kontextMat <- readRDS("data/kontextMat.rds")
stateMat <- readRDS("data/stateMat.rds")

12.2 Classification of patients by condition

We will use the Keren 2018 dataset to classify a patient’s tumour into one of two types: compartmentalised and mixed. First, we will filter out all cases which had cold tumours.

# filter out cold tumours
kerenSPE = kerenSPE[, colData(kerenSPE)$tumour_type != "cold"]
kerenSPE$tumour_type = droplevels(kerenSPE$tumour_type)
levels(kerenSPE$tumour_type) = c("compartmentalised", "mixed")

kontextMat = kontextMat[rownames(kontextMat) %in% unique(kerenSPE$imageID), ]
stateMat = stateMat[rownames(stateMat) %in% unique(kerenSPE$imageID), ]

We will build a list of feature matrices using the features we’ve generated in the previous chapters:

  1. Cell type proportions (FuseSOM)
  2. Co-localisation between pairs of cell types using the L-function (spicyR)
  3. Cell type co-localisation with respect to a parent population using Kontextual (Statial)
  4. Regions of co-localisation, or spatial domains (lisaClust)
  5. Marker means in each cell type (Statial)
  6. Marker means in each cell type in each region (Statial)
  7. Proximity-associated changes in marker expression using SpatioMark (Statial)
data <- list()

# Cell type proportions (FuseSOM)
data[["proportions"]] <- getProp(kerenSPE, "cellType")

# Cell localisation (spicyR)
data[["spicyR"]] <- getPairwise(kerenSPE,
                                BPPARAM = BPPARAM) |> as.data.frame()

# Cell localisation with respect to a parent (Kontextual)
data[["Kontextual"]] <- kontextMat

# Spatial Domains (lisaClust)
data[["lisaClust"]] <- getProp(lisaClust(kerenSPE, k = 5,
                                         BPPARAM = BPPARAM), "region")
Generating local L-curves.
# Marker means in each cell type (Statial)
data[["celltypeMarkerMeans"]] <- getMarkerMeans(kerenSPE, imageID = "imageID",
                                        cellType = "cellType",
                                        region = "cellType")

# Marker means in each cell type in each region (Statial)
data[["regionMarkerMeans"]] <- getMarkerMeans(lisaClust(kerenSPE, k = 5,
                                                        BPPARAM = BPPARAM), 
                                        imageID = "imageID",
                                        cellType = "cellType",
                                        region = "region")
Generating local L-curves.
# Proximity-associated changes in marker expression (SpatioMark)
data[["SpatioMark"]] <- stateMat

Time for this code chunk to run with 5.5 cores: 33.54 seconds

We will then generate a factor vector of our outcome variable.

# outcome vector
outcome = kerenSPE$tumour_type[!duplicated(kerenSPE$imageID)]
names(outcome) = kerenSPE$imageID[!duplicated(kerenSPE$imageID)]

head(outcome, 5)
                1                 2                 3                 4 
            mixed             mixed compartmentalised compartmentalised 
                5 
compartmentalised 
Levels: compartmentalised mixed

ClassifyR provides a convenient function, crossValidate, to build and test models. crossValidate must be supplied with measurements, a simple tabular data container or a list-like structure of such related tabular data on common samples. It can be in the form of a matrix, data.frame, DataFrame, MultiAssayExperiment or a list of data.frames.

crossValidate must also be supplied with outcome, which represents the prediction to be made. outcome can be either a factor containing the class labels for each observation, or a character of length 1 that matches a column name in measurements which holds the classes. If a character is provided, crossValidate will automatically remove the classes before training.

By default, crossValidate will build and train a random forest. Alternative classifiers can be specified using the classifier argument. To view all available feature selection and classification approaches, use the available function.

# perform 50 repeats of 5-fold cross-validation
cv = crossValidate(measurements = data,
                   outcome = outcome,
                   nFolds = 5,
                   nRepeats = 50,
                   nCores = nCores)

Time for this code chunk to run with 5.5 cores: 25.54 seconds

We can use performancePlot to visualise performance metrics for all our features. Here, we visualise the AUC for each of the seven feature matrices we tested. Additional performance metrics can be specified in the metric argument.

performancePlot(
  cv,
  metric = "AUC",
  characteristicsList = list(x = "Assay Name"),
  orderingList = list("Assay Name" = c("proportions", "spicyR", "lisaClust", "Kontextual", "celltypeMarkerMeans", "regionMarkerMeans", "SpatioMark"))
)

From the graph, both lisaClust and proportions appear to capture information which is predictive of the tumour type of patients.

12.3 Classification of patients by survival

crossValidate also has the capacity to test classification performance for a survival outcome. In this case, outcome must be a Surv object of the same length as the number of samples in the feature matrix and should contain information about the time and censoring of the samples. Alternatively, we can specify outcome to be a character of length 2 or 3 that each match a column name in a data frame which holds information about the time and censoring of the samples. The time-to-event columns will automatically be removed before training is done.

We will first add a survival column to the kerenSPE object.

# create a Surv object named "survival"
kerenSPE$event = 1 - kerenSPE$Censored
kerenSPE$survival = Surv(kerenSPE$`Survival_days_capped*`, kerenSPE$event)

# outcome vector
surv_outcome = kerenSPE$survival[!duplicated(kerenSPE$imageID)]
names(surv_outcome) = kerenSPE$imageID[!duplicated(kerenSPE$imageID)]

surv_outcome
    1     2     3     4     5     6     7     8     9    10    11    12    13 
 2612   745 3130+ 2523+ 1683+ 2275+   584   946 3767+ 3822+ 3774+ 4353+  1072 
   14    16    17    18    20    21    23    27    28    29    31    32    33 
4145+   530  2842 5063+ 4761+   635    91  3658 3767+  1319  1009 1568+ 1738+ 
   34    35    36    37    39    40    41 
2832+ 2759+ 3063+ 2853+ 2096+  3573 3355+ 

We can then run crossValidate and specify the outcome to be surv_outcome, and use performancePlot to visualise the performance of the cross-validation. Since we are performing survival analysis, we will specify metric = "C-index".

# perform 50 repeats of 5-fold cross-validation
surv_cv = crossValidate(measurements = data,
                   outcome = surv_outcome,
                   nFolds = 5,
                   nRepeats = 50,
                   nCores = nCores)

performancePlot(surv_cv,
  metric = "C-index",
  characteristicsList = list(x = "Assay Name"),
  orderingList = list("Assay Name" = c("proportions", "spicyR", "lisaClust", "Kontextual", "celltypeMarkerMeans", "regionMarkerMeans", "SpatioMark"))
)

From the graph, we can see that lisaClust appears to capture information that is predictive of survival outcomes comparatively well.

12.4 Easy and hard to classify patients

The samplesMetricMap function allows the visual comparison of sample-wise error rate or accuracy measures from the cross-validation process.

12.4.1 Predicting tumour type

samplesMetricMap(cv,  
                 classColours = c("#3F48CC", "#880015"),
                 metricColours = list(c("#FFFFFF", "#CFD1F2", "#9FA3E5", "#6F75D8", "#3F48CC"),
                                      c("#FFFFFF", "#E1BFC4", "#C37F8A", "#A53F4F", "#880015")))

TableGrob (2 x 1) "arrange": 2 grobs
  z     cells    name                grob
1 1 (2-2,1-1) arrange      gtable[layout]
2 2 (1-1,1-1) arrange text[GRID.text.409]

The benefit of this plot is that it allows the easy identification of samples which are hard to classify and could be explained by considering additional information about them. For example, we can see that patients 36 and 13 in particular were difficult to classify. Overall, both cell type proportions (FuseSOM) and cell type co-localisation (spicyR and Kontextual) performed comparitively well at classifying patients, and compartmentalised tumours were easier to classify compared to mixed tumours.

12.4.2 Predicting survival outcomes

We can also use sampleMetricMap to identify samples that were difficult to classify with respect to a survival outcome.

TableGrob (2 x 1) "arrange": 2 grobs
  z     cells    name                grob
1 1 (2-2,1-1) arrange      gtable[layout]
2 2 (1-1,1-1) arrange text[GRID.text.561]

We can see that there was some difficulty in classying patient 35, and that overall, features showed similar performance across samples. lisaClust shows the overall highest performance, especially for samples 31-20.

DataFrame(), outcome, crossValParams, SVMparams) ``` ::: The index of chosen of the parameters, as well as all combinations of parameters and their associated performance metric, are stored for every validation, and can be accessed with the `tunedParameters` function. ::: {.cell} ```{.r .cell-code} tunedParameters(SVMresults)[1:5] ``` ::: -->

In this way, we can predict clinical outcomes and identify samples that are hard to classify.

In subsequent sections, we will demonstrate how the end-to-end workflow can be applied to a single dataset in the form of case studies.

13 sessionInfox

R version 4.4.1 (2024-06-14)
Platform: aarch64-apple-darwin20
Running under: macOS Sonoma 14.4.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

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

time zone: Australia/Sydney
tzcode source: internal

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

other attached packages:
 [1] SpatialDatasets_1.4.0       SpatialExperiment_1.16.0   
 [3] SingleCellExperiment_1.28.1 ExperimentHub_2.14.0       
 [5] AnnotationHub_3.14.0        BiocFileCache_2.14.0       
 [7] dbplyr_2.5.0                ggplot2_3.5.1              
 [9] spicyR_1.18.0               Statial_1.8.0              
[11] lisaClust_1.14.4            ClassifyR_3.10.5           
[13] survival_3.7-0              BiocParallel_1.40.0        
[15] MultiAssayExperiment_1.32.0 SummarizedExperiment_1.36.0
[17] Biobase_2.66.0              GenomicRanges_1.58.0       
[19] GenomeInfoDb_1.42.0         IRanges_2.40.0             
[21] MatrixGenerics_1.18.0       matrixStats_1.4.1          
[23] S4Vectors_0.44.0            BiocGenerics_0.52.0        
[25] generics_0.1.3             

loaded via a namespace (and not attached):
  [1] splines_4.4.1           later_1.4.1             filelock_1.0.3         
  [4] bitops_1.0-9            svgPanZoom_0.3.4        tibble_3.2.1           
  [7] polyclip_1.10-7         XML_3.99-0.17           lifecycle_1.0.4        
 [10] rstatix_0.7.2           lattice_0.22-6          MASS_7.3-61            
 [13] backports_1.5.0         magrittr_2.0.3          limma_3.62.1           
 [16] plotly_4.10.4           rmarkdown_2.29          yaml_2.3.10            
 [19] httpuv_1.6.15           doRNG_1.8.6.1           sp_2.1-4               
 [22] dcanr_1.22.0            spatstat.sparse_3.1-0   DBI_1.2.3              
 [25] minqa_1.2.8             RColorBrewer_1.1-3      abind_1.4-8            
 [28] zlibbioc_1.52.0         purrr_1.0.2             RCurl_1.98-1.16        
 [31] rappdirs_0.3.3          tweenr_2.0.3            GenomeInfoDbData_1.2.13
 [34] spatstat.utils_3.1-1    terra_1.7-78            genefilter_1.88.0      
 [37] pheatmap_1.0.12         goftest_1.2-3           simpleSeg_1.8.0        
 [40] annotate_1.84.0         spatstat.random_3.3-2   svglite_2.1.3          
 [43] codetools_0.2-20        DelayedArray_0.32.0     ggforce_0.4.2          
 [46] tidyselect_1.2.1        raster_3.6-30           UCSC.utils_1.2.0       
 [49] farver_2.1.2            viridis_0.6.5           lme4_1.1-35.5          
 [52] spatstat.explore_3.3-3  jsonlite_1.8.9          Formula_1.2-5          
 [55] iterators_1.0.14        systemfonts_1.1.0       foreach_1.5.2          
 [58] tools_4.4.1             Rcpp_1.0.13-1           glue_1.8.0             
 [61] gridExtra_2.3           SparseArray_1.6.0       xfun_0.49              
 [64] ranger_0.17.0           mgcv_1.9-1              ggthemes_5.1.0         
 [67] EBImage_4.48.0          dplyr_1.1.4             HDF5Array_1.34.0       
 [70] scam_1.2-17             shinydashboard_0.7.2    withr_3.0.2            
 [73] numDeriv_2016.8-1.1     BiocManager_1.30.25     fastmap_1.2.0          
 [76] ggh4x_0.2.8             boot_1.3-31             rhdf5filters_1.18.0    
 [79] fansi_1.0.6             digest_0.6.37           R6_2.5.1               
 [82] mime_0.12               colorspace_2.1-1        tensor_1.5             
 [85] RSQLite_2.3.8           jpeg_0.1-10             spatstat.data_3.1-4    
 [88] utf8_1.2.4              tidyr_1.3.1             data.table_1.16.2      
 [91] class_7.3-22            httr_1.4.7              htmlwidgets_1.6.4      
 [94] S4Arrays_1.6.0          pkgconfig_2.0.3         gtable_0.3.6           
 [97] blob_1.2.4              XVector_0.46.0          htmltools_0.5.8.1      
[100] carData_3.0-5           fftwtools_0.9-11        scales_1.3.0           
[103] ggupset_0.4.0           png_0.1-8               spatstat.univar_3.1-1  
[106] knitr_1.49              rstudioapi_0.17.1       reshape2_1.4.4         
[109] rjson_0.2.23            curl_6.0.1              nlme_3.1-166           
[112] nloptr_2.1.1            bdsmatrix_1.3-7         cachem_1.1.0           
[115] rhdf5_2.50.0            stringr_1.5.1           BiocVersion_3.20.0     
[118] vipor_0.4.7             parallel_4.4.1          concaveman_1.1.0       
[121] AnnotationDbi_1.68.0    pillar_1.9.0            grid_4.4.1             
[124] vctrs_0.6.5             coxme_2.2-22            promises_1.3.2         
[127] ggpubr_0.6.0            car_3.1-3               xtable_1.8-4           
[130] beeswarm_0.4.0          evaluate_1.0.1          magick_2.8.5           
[133] cli_3.6.3               locfit_1.5-9.10         compiler_4.4.1         
[136] rlang_1.1.4             crayon_1.5.3            rngtools_1.5.2         
[139] ggsignif_0.6.4          labeling_0.4.3          plyr_1.8.9             
[142] ggbeeswarm_0.7.2        stringi_1.8.4           nnls_1.6               
[145] viridisLite_0.4.2       deldir_2.0-4            cytomapper_1.18.0      
[148] Biostrings_2.74.0       lmerTest_3.1-3          munsell_0.5.1          
[151] lazyeval_0.2.2          tiff_0.1-12             spatstat.geom_3.3-4    
[154] Matrix_1.7-1            bit64_4.5.2             Rhdf5lib_1.28.0        
[157] KEGGREST_1.46.0         statmod_1.5.0           shiny_1.9.1            
[160] memoise_2.0.1           igraph_2.1.1            broom_1.0.7            
[163] bit_4.5.0