--- title: "A Complete Example" output: rmarkdown::html_vignette: toc: true description: > Step through a complex subtyping workflow. vignette: > %\VignetteIndexEntry{A Complete Example} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` Download a copy of the vignette to follow along here: [a_complete_example.Rmd](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/a_complete_example.Rmd) We recommend you go through the [simple example](https://branchlab.github.io/metasnf/articles/a_simple_example.html) before working through this one. This vignette walks through how the core functionality of this package, including: 1. Setting up the data 2. Building a space of settings to cluster over 3. Running SNF 4. Identifying and visualizing meta clusters 5. Characterizing and selecting top meta clusters 6. Selecting a representative cluster solution from a meta cluster 7. Cluster solution characterization 9. Cluster solution validation ## 1. Setting up the data ### Load the library and data into the R environment Your data should be loaded into the R environment in the following format: 1. The data is in one or more *data.frame* objects 2. The data is in wide form (one row per patient to cluster) 3. All dataframes should have exactly one column that uniquely identifies which patient the row has data for 4. All data should be complete (no missing values) A future vignette will outline how metaSNF can be used to simultaneously cluster over distinct imputations of a dataset. The package comes with a few mock dataframes based on real data from the Adolescent Brain Cognitive Development study: - `abcd_anxiety` (anxiety scores from the CBCL) - `abcd_depress` (depression scores from the CBCL) - `abcd_cort_t` (cortical thicknesses) - `abcd_cort_sa` (cortical surface areas in mm^2) - `abcd_subc_v` (subcortical volumes in mm^3) - `abcd_h_income` (household income on a 1-3 scale) - `abcd_pubertal` (pubertal status on a 1-5 scale) Here's what the cortical thickness data looks like: ```{r} library(metasnf) class(abcd_cort_t) dim(abcd_cort_t) str(abcd_cort_t[1:5, 1:5]) abcd_cort_t[1:5, 1:5] ``` The first column "patient" is the unique identifier (UID) for all subjects in the data. Here's the household income data: ```{r} dim(abcd_h_income) str(abcd_h_income[1:5, ]) abcd_h_income[1:5, ] ``` Putting everything in a list will help us get quicker summaries of all the data. ```{r} abcd_data <- list( abcd_anxiety, abcd_depress, abcd_cort_t, abcd_cort_sa, abcd_subc_v, abcd_h_income, abcd_pubertal ) # The number of rows in each dataframe: lapply(abcd_data, dim) # Whether or not each dataframe has missing values: lapply(abcd_data, function(x) { any(is.na(x)) } ) ``` Some of our data has missing values and not all of our dataframes have the same number of participants. SNF can only be run with complete data, so you'll need to either use complete case analysis (removal of observations with any missing values) or impute the missing values to proceed with the clustering. As mentioned above, `metaSNF` can be used to visualize changes in clustering results across different imputations of the data. For now, we'll just examine the simpler complete-case analysis approach by reducing our dataframes to only common and complete patients. ```{r} abcd_merged_df <- merge_df_list(abcd_data, uid = "patient", no_na = TRUE) common_patients <- abcd_merged_df$"patient" print(length(common_patients)) # Reducing dataframes to only common subjects with no missing data abcd_anxiety <- abcd_anxiety[abcd_anxiety$"patient" %in% common_patients, ] abcd_depress <- abcd_depress[abcd_depress$"patient" %in% common_patients, ] abcd_cort_t <- abcd_cort_t[abcd_cort_t$"patient" %in% common_patients, ] abcd_cort_sa <- abcd_cort_sa[abcd_cort_sa$"patient" %in% common_patients, ] abcd_subc_v <- abcd_subc_v[abcd_subc_v$"patient" %in% common_patients, ] abcd_h_income <- abcd_h_income[abcd_h_income$"patient" %in% common_patients, ] abcd_pubertal <- abcd_pubertal[abcd_pubertal$"patient" %in% common_patients, ] ``` ### Generating the data list The `data_list` structure is a structured list of dataframes (like the one already created), but with some additional metadata about each dataframe. It should only contain the input dataframes we want to directly use as inputs for the clustering. Out of all the data we have available to us, we may be working in a context where the anxiety and depression data are especially important patient outcomes, and we want to know if we can find subtypes using the rest of the data which still do a good job of separating out patients by their anxiety and depression scores. We'll start by creating a data list that stores our input variables. ```{r} # Note that you do not need to explicitly name every single named element # (data = ..., name = ..., etc.) data_list <- generate_data_list( list( data = abcd_cort_t, name = "cortical_thickness", domain = "neuroimaging", type = "continuous" ), list( data = abcd_cort_sa, name = "cortical_surface_area", domain = "neuroimaging", type = "continuous" ), list( data = abcd_subc_v, name = "subcortical_volume", domain = "neuroimaging", type = "continuous" ), list( data = abcd_h_income, name = "household_income", domain = "demographics", type = "continuous" ), list( data = abcd_pubertal, name = "pubertal_status", domain = "demographics", type = "continuous" ), uid = "patient" ) ``` This process removes any patients who did not have complete data across all provided input dataframes. If you'd like to keep track of that information, you can set the "return_missing" parameter to `TRUE` and receive a list containing the data_list as well as the removed patients. The structure of the data list is a nested list tracking the data, the name of the dataframe, what domain (broader source of information) the data belongs to, and the type of variable stored in the dataframe. Options for variable type include "continuous", "discrete", "ordinal", and "categorical". The "uid" parameter is the name of the column in the dataframes that uniquely identifies each patient. Upon data list creation, the UID will be converted to "subjectkey" for consistency across other functions in the package. We can get a summary of our constructed `data_list` with the `summarize_dl` function: ```{r} summarize_dl(data_list) ``` Each input dataframe now has the same 87 subjects with complete data. The width refers to the number of columns in each dataframe, which equals 1 for the UID (subjectkey) column + the number of features in the dataframe. `data_list` now stores all the variables we intend on using for the clustering. We're interested in knowing if any of the clustering solutions we generate can distinguish children apart based on their anxiety and depression scores. To do this, we'll also create a data list storing only variables that we'll use for evaluating our cluster solutions and not for the clustering itself. We'll refer to this as the "target_list". ```{r} target_list <- generate_data_list( list(abcd_anxiety, "anxiety", "behaviour", "ordinal"), list(abcd_depress, "depressed", "behaviour", "ordinal"), uid = "patient" ) summarize_dl(target_list) ``` Note that it is not necessary to make use of a partition of input and out-of-model measures in this way. If you'd like to have no target list and instead use every single variable of interest for the clustering, you can stick to just using one data list. ## 2. Building a space of settings to cluster over The `settings_matrix` stores all the information about the settings we'd like to use for each of our SNF runs. Calling the `generate_settings_matrix` function with a specified number of rows will automatically build a randomly populated `settings_matrix`. ```{r} settings_matrix <- generate_settings_matrix( data_list, nrow = 20, min_k = 20, max_k = 50, seed = 42 ) settings_matrix[1:5, ] ``` The columns are: - `row_id`: Integer to keep track of which row is which - `alpha` - A hyperparameter for SNF (variable that influences the subtyping process) - `k` - A hyperparameter for SNF - `t` - A hyperparameter for SNF - `snf_scheme` - the specific way in which input data gets collapsed into a final fused network (discussed further in the [SNF schemes vignette](https://branchlab.github.io/metasnf/articles/snf_schemes.html)) - `clust_alg` - Which clustering algorithm will be applied to the final fused network produced by SNF - `*_dist` - Which distance metric will be used for the different types of data (discussed further in the [distance metrics vignette](https://branchlab.github.io/metasnf/articles/distance_metrics.html)) - `inc_*` - binary columns indicating whether or not an input dataframe is included (1) or excluded (0) from the corresponding SNF run (discussed further in the [settings matrix vignette](https://branchlab.github.io/metasnf/articles/settings_matrix.html)) Without specifying any additional parameters, `generate_settings_matrix` randomly populates these columns and ensures that no generated rows are identical. What's important for now is that the matrix (technically a dataframe in the R environment) contains several rows which each outline a different but reasonable way in which the raw data could be converted into patient subtypes. Further customization of the `settings_matrix` will enable you to access the broadest possible space of reasonable cluster solutions that your data can produce using SNF and ideally get you closer to a generalizable and useful solution for your context. More on `settings_matrix` customization can be found in the [settings matrix vignette](https://branchlab.github.io/metasnf/articles/settings_matrix.html). Setting the optional `seed` parameter (which will affect the seed of your entire R session) ensures that the same settings matrix is generated each time we run the code. While we end up with a random set of settings here, there is nothing wrong with manually altering the settings matrix to suit your needs. For example, if you wanted to know how much of a difference one input dataframe made, you could ensure that half of the rows included this input dataframe and the other half didn't. You can also add random rows to an already existing dataframe using the `add_settings_matrix_rows` function (further discussed in the vignette). ## 3. Running SNF The `batch_snf` function integrates the data in the `data_list` using each of the sets of settings contained in the `settings_matrix`. The resulting structure is an `solutions_matrix` which is an extension of the `settings_matrix` that contains columns specifying which cluster each subject was assigned for the corresponding `settings_matrix` row. ```{r} solutions_matrix <- batch_snf(data_list, settings_matrix) colnames(solutions_matrix)[1:30] ``` It goes on like this for some time. Just like that, 20 different cluster solutions have been generated! In practice, you may end up wanting to create hundreds or thousands of cluster solutions at a time. If you have access to a multi-core system, `batch_snf` can be run with parallel processing enabled. See `?batch_snf` or the [parallel processing vignette](https://branchlab.github.io/metasnf/articles/parallel_processing.html) for more information. You can pull the clustering results out of each row using the `get_cluster_solutions` function: ```{r} cluster_solutions <- get_cluster_solutions(solutions_matrix) head(cluster_solutions) ``` ## 4. Identifying and visualizing meta clusters Now that we have access to 20 different clustering solutions, we'll need to find some way to pick an optimal one to move forward with for additional characterization. In this case, plotting or running stats manually on each of the solutions might be a reasonable way to determine which ones we like the most. But when the number of solutions generated goes up into the hundreds (or thousands), we're going to need some more automated approaches. The main approach we recommend using is the meta clustering approach described in Caruana et al., 2016. Meta clustering is a good approach to use when the criterion for what makes one cluster solution better than another is hard to formalize quantitatively (and hence, hard to fully automate). The idea is to cluster the *clustering solutions themselves* to arrive at a manageable number of qualitatively similar meta clusters. From there, characterization of representative solutions from each meta cluster can be done to efficiently identify the top choice. The first step is to calculate the [adjusted Rand index](https://en.wikipedia.org/wiki/Rand_index) (ARI) between each pair of cluster solutions. This metric tells us how similar the solutions are to each other, thereby allowing us to find clusters of cluster solutions. ```{r} solutions_matrix_aris <- calc_aris(solutions_matrix) ``` We can visualize the resulting inter-cluster similarities with a heatmap. First, call `get_matrix_order` to get a hierarchical clustering-based ordering of the rows in the adjusted rand indices. ```{r} meta_cluster_order <- get_matrix_order(solutions_matrix_aris) # Just a vector of numbers meta_cluster_order ``` This order can be passed into the `adjusted_rand_index_heatmap` function to get a clearer view of existing meta clusters. ```{r eval = FALSE} ari_hm <- adjusted_rand_index_heatmap( solutions_matrix_aris, order = meta_cluster_order ) save_heatmap( heatmap = ari_hm, path = "./adjusted_rand_index_heatmap.png", width = 550, height = 500, res = 100 ) ```
![](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/adjusted_rand_index_heatmap.png)
The clustering solutions are along the rows and columns of the above figure, and the cells at the intersection between two solutions show how similar (big ARI) those solutions are to each other. The diagonals should always be red, representing the maximum value of 1, as they show the similarity between any clustering solution and itself. Complete-linkage, Euclidean-distance based hierarchical clustering is being applied to these solutions to obtain the row ordering. This is also the default approach used by the `ComplexHeatmap` package, the backbone of all heatmap functions in `metaSNF`. This heatmap is integral to understanding what meta clusters exist in your data (given the space of parameters you explored in the settings matrix). We will later see how to further customize this heatmap to add in rich information about how each plotted cluster solution differs on various measures of quality, different clustering settings, and different levels of separation on input and out-of-model measures. First, we'll divide the heatmap into meta clusters by visual inspection. The indices of our meta cluster boundaries can be passed into the `adjusted_rand_index_heatmap` function as the `split_vector` parameter. You can determine this vector through trial and error or by using the `shiny_annotator` function (a wrapper around functionality from the `InteractiveComplexHeatmap` package). ```{r eval = FALSE} shiny_annotator(ari_hm) ```
![](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/meta_cluster_shiny.png)
A demonstration of the shiny app can be seen here: [Meta Cluster Identification](https://pvelayudhan.shinyapps.io/shiny/) Note that while the shiny app is running, your R console will be unresponsive. Clicking on cell boundaries and tracking the row/column indices (printed to the R console as well as displayed in the app) can get us results looking something like this: ```{r eval = FALSE} split_vec <- c(2, 5, 12, 17) ari_mc_hm <- adjusted_rand_index_heatmap( solutions_matrix_aris, order = meta_cluster_order, split_vector = split_vec ) save_heatmap( heatmap = ari_mc_hm, path = "./ari_mc_hm.png", width = 550, height = 500, res = 100 ) ```
![](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/ari_mc_hm.png)
At this point, we have our meta clusters but are not yet sure of how they differ in terms of their structure across our input and out-of-model variables. We start by running the `extend_solutions` function, which will calculate p-values representing the strength of the association between cluster membership (treated as a categorical variable) and any feature present in a provided data list and/or target_list. `extend_solutions` also adds summary p-value measures (min, mean, and max) of any features present in the target list. ```{r} # Only looking at our out-of-model p-values extended_solutions_matrix <- extend_solutions( solutions_matrix, target_list = target_list ) # What columns have been added? old_cols <- colnames(extended_solutions_matrix) %in% colnames(solutions_matrix) new_cols <- !old_cols head(extended_solutions_matrix[, new_cols]) # Re-running to calculate the p-value for every single input and out-of-model # feature: extended_solutions_matrix <- extend_solutions( solutions_matrix, data_list = data_list, target_list = target_list ) ``` Functionally, the only difference between the `data_list` and `target_list` arguments are that the `target_list` features will also be a part of those summary measures. ```{r eval = FALSE} # Also would work, but without any summary p-values extended_solutions_matrix <- extend_solutions( solutions_matrix, data_list = c(data_list, target_list) ) # Also would work, but now every feature would be part of the summaries extended_solutions_matrix <- extend_solutions( solutions_matrix, target_list = c(data_list, target_list) ) ``` The summary p-value measures can be suppressed by setting `calculate_summaries = FALSE`. This `extended_solutions_matrix` we created can be passed into the `adjusted_rand_index_heatmap` function to easily visualize the level of separation on each of our variables for each of our cluster solutions. ```{r eval = FALSE} annotated_ari_hm <- adjusted_rand_index_heatmap( solutions_matrix_aris, order = meta_cluster_order, split_vector = split_vec, data = extended_solutions_matrix, top_hm = list( "Depression p-value" = "cbcl_depress_r_pval", "Anxiety p-value" = "cbcl_anxiety_r_pval", "Overall outcomes p-value" = "mean_pval" ), bottom_bar = list( "Number of Clusters" = "nclust" ), annotation_colours = list( "Depression p-value" = colour_scale( extended_solutions_matrix$"cbcl_depress_r_pval", min_colour = "purple", max_colour = "black" ), "Anxiety p-value" = colour_scale( extended_solutions_matrix$"cbcl_anxiety_r_pval", min_colour = "green", max_colour = "black" ), "Overall outcomes p-value" = colour_scale( extended_solutions_matrix$"mean_pval", min_colour = "lightblue", max_colour = "black" ) ) ) save_heatmap( heatmap = annotated_ari_hm, path = "./annotated_ari_hm.png", width = 700, height = 500, res = 100 ) ```
![](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/annotated_ari_hm.png)
The `adjusted_rand_index_heatmap` function wraps around a more generic `similarity_matrix_heatmap` function in this package, which itself is a wrapper around `ComplexHeatmap::Heatmap()`. Consequently, anything that can be done in the `ComplexHeatmap` package can be done here. This also makes the documentation for the `ComplexHeatmap` package one of the best places to learn more about what can be done for these heatmaps. The data for the annotations don't necessarily need to come from functions in metaSNF either. If you wanted to, for example, highlight the solutions where two very specific patients happened to cluster together, you could easily add that in as another annotation: ```{r eval = FALSE} extended_solutions_matrix2 <- extended_solutions_matrix |> dplyr::mutate( key_patients_cluster_together = dplyr::case_when( subject_NDAR_INVLF3TNDUZ == subject_NDAR_INVLDQH8ATK ~ TRUE, TRUE ~ FALSE ) ) annotated_ari_hm2 <- adjusted_rand_index_heatmap( solutions_matrix_aris, order = meta_cluster_order, split_vector = split_vec, data = extended_solutions_matrix2, top_hm = list( "Depression p-value" = "cbcl_depress_r_pval", "Anxiety p-value" = "cbcl_anxiety_r_pval", "Key Patients Clustered Together" = "key_patients_cluster_together" ), bottom_bar = list( "Number of Clusters" = "nclust" ), annotation_colours = list( "Depression p-value" = colour_scale( extended_solutions_matrix$"cbcl_depress_r_pval", min_colour = "purple", max_colour = "black" ), "Anxiety p-value" = colour_scale( extended_solutions_matrix$"cbcl_anxiety_r_pval", min_colour = "purple", max_colour = "black" ), "Key Patients Clustered Together" = c( "TRUE" = "blue", "FALSE" = "black" ) ) ) save_heatmap( heatmap = annotated_ari_hm2, path = "./annotated_ari_hm2.png", width = 700, height = 500, res = 100 ) ```
![](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/annotated_ari_hm2.png)
Now that we've visually delineated our meta clusters, we can get a quick summary of what sort of separation exists across our input and held out variables by taking a closer look at one representative cluster solution from each meta cluster. This can be achieved by the `get_representative_solutions` function, which extracts one cluster solution per meta cluster based on having the highest average ARI with the other solutions in that meta cluster. ```{r eval = FALSE} rep_solutions <- get_representative_solutions( solutions_matrix_aris, split_vector = split_vec, order = meta_cluster_order, extended_solutions_matrix ) mc_manhattan <- mc_manhattan_plot( rep_solutions, data_list = data_list, target_list = target_list, hide_x_labels = TRUE, point_size = 2, text_size = 12, threshold = 0.05, neg_log_pval_thresh = 5 ) ggsave( "mc_manhattan.png", mc_manhattan, height = 6, width = 12 ) ```
![](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/mc_manhattan.png)
`neg_log_pval_thresh` sets a threshold for the negative log of the p-values to be displayed. At a value of 5, any p-value that is smaller than 1e-5 will be truncated to 1e-5. Note that the Manhattan plot automatically uses a vertical line to separate features in the data_list argument from those in the target_list. Vertical boundaries can be controlled with the `xints` parameter. The plot as it is is a bit unwieldy plot given how many neuroimaging ROIs are present. Let's take out the cortical thickness and surface area measures to make the plot a little clearer. We'll also be able to read some variable measures more clearly when we dial the number of features plotted back a bit, as well. ```{r eval = FALSE} rep_solutions_no_cort <- dplyr::select( rep_solutions, -dplyr::contains("mrisdp") ) mc_manhattan2 <- mc_manhattan_plot( rep_solutions_no_cort, data_list = data_list, target_list = target_list, point_size = 4, threshold = 0.01, text_size = 12, domain_colours = c( "neuroimaging" = "cadetblue", "demographics" = "purple", "behaviour" = "firebrick" ) ) mc_manhattan2 ggsave( "mc_manhattan2.png", mc_manhattan2, height = 8, width = 12 ) ```
![](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/mc_manhattan2.png)
![](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/annotated_ari_hm2.png)
If you see something interesting in your heatmap, you may be curious to know how that corresponds to the settings that were in your settings matrix. You could certainly stack on each setting to the ARI heatmap as annotations, but that may be a bit cumbersome given how many settings there are. Another option is to start by taking a first look at entire `settings_matrix`, sorted by the meta cluster results, through the `settings_matrix_heatmap` function. ```{r eval = FALSE} sm_hm <- settings_matrix_heatmap( settings_matrix, order = meta_cluster_order ) save_heatmap( heatmap = sm_hm, path = "./settings_matrix_heatmap_ordered.png", width = 400, height = 500, res = 75 ) ```
![](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/settings_matrix_heatmap_ordered.png)
This heatmap rescales all the columns in the settings_matrix to have a maximum value of 1. The purpose of re-ordering the settings matrix in this way is to see if any associations exist between certain settings values and pairwise cluster solution similarities. This heatmap can show you if any settings values were associated with a particular type of clustering result. If there are any particular important settings, you can simply add them into your adjusted rand index heatmap annotations. Recall that the `solutions_matrix` (and, by extension, the `extended_solutions_matrix`) is an extension of the settings matrix, so no further data manipulation is needed to add a setting variable as a heatmap annotation. Give it a try with the code below: ```{r eval = FALSE} annotated_ari_hm3 <- adjusted_rand_index_heatmap( solutions_matrix_aris, order = meta_cluster_order, split_vector = c(11, 14), data = extended_solutions_matrix, top_hm = list( "Depression p-value" = "cbcl_depress_r_pval", "Anxiety p-value" = "cbcl_anxiety_r_pval", "Key Patients Clustered Together" = "key_patients_cluster_together" ), left_hm = list( "Clustering Algorithm" = "clust_alg" # from the original settings ), bottom_bar = list( "Number of Clusters" = "nclust" # also from the original settings ), annotation_colours = list( "Depression p-value" = colour_scale( extended_solutions_matrix$"cbcl_depress_r_pval", min_colour = "purple", max_colour = "black" ), "Anxiety p-value" = colour_scale( extended_solutions_matrix$"cbcl_anxiety_r_pval", min_colour = "purple", max_colour = "black" ), "Key Patients Clustered Together" = c( "TRUE" = "blue", "FALSE" = "black" ) ) ) ``` ### 2. Quality measures Quality metrics are another useful heuristic for the goodness of a cluster that don't require any contextualization of results in the domain they may be used in. metaSNF enables measures of silhouette scores, Dunn indices, and Davies-Bouldin indices. To calculate these values, we'll need not only the cluster results but also the final fused network (the similarity matrices produced by SNF) that the clusters came from. These similarity matrices can be collected from the `batch_snf` using the `return_similarity_matrices` parameter: ```{r} batch_snf_results <- batch_snf( data_list, settings_matrix, return_similarity_matrices = TRUE ) solutions_matrix <- batch_snf_results$"solutions_matrix" similarity_matrices <- batch_snf_results$"similarity_matrices" ``` This time, the output of `batch_snf` is a list. The first element of the list is a single solutions_matrix, like what we usually get. The second element is *yet another list* containing one final fused network (AKA similarity matrix / similarity matrix) per SNF run. Using those two lists, we can calculate the above mentioned quality metrics: ```{r eval = FALSE} silhouette_scores <- calculate_silhouettes( solutions_matrix, similarity_matrices ) dunn_indices <- calculate_dunn_indices( solutions_matrix, similarity_matrices ) db_indices <- calculate_db_indices( solutions_matrix, similarity_matrices ) ``` The first function is a wrapper around `cluster::silhouette` while the second and third come from the *clv* package. *clv* isn't set as a mandatory part of the installation, so you'll ned to install it yourself to calculate these two metrics. The original documentation on these functions can be helpful for interpreting and working with them: 1. [`cluster::silhouette` documentation](https://www.rdocumentation.org/packages/cluster/versions/2.1.4/topics/silhouette) 2. [`clv::clv.Dunn` documentation](https://www.rdocumentation.org/packages/clv/versions/0.3-2.1/topics/clv.Dunn) 3. [`clv::clv.Davies.Bouldin` documentation](https://www.rdocumentation.org/packages/clv/versions/0.3-2.1/topics/clv.Davies.Bouldin) ### 3. Stability measures metaSNF offers tools to evaluate two different measures of stability: 1. Pairwise adjusted Rand indices (across resamplings of the clustering, on average, how similar was every pair of solutions according to the adjusted Rand index?) 2. Fraction clustered together (what is the average fraction of times that patients who clustered together in the full results clustered together in resampled results?) To calculate either of these, you'll need to first generate subsamples of the data_list. ```{r eval = FALSE} data_list_subsamples <- subsample_data_list( data_list, n_subsamples = 30, # calculate 30 subsamples subsample_fraction = 0.8 # for each subsample, use random 80% of patients ) ``` `data_list_subsamples` is a list that now contains 30 smaller subsamples of the original data_list. Then the stability calculations: ```{r eval = FALSE} pairwise_aris <- subsample_pairwise_aris( data_list_subsamples, settings_matrix ) fraction_together <- fraction_clustered_together( data_list_subsamples, settings_matrix, solutions_matrix ) ``` Be warned, that second function is especially extremely slow. As the number of patients and number of solutions you're evaluating grows, these functions can get pretty slow. Consider only using them after eliminating solutions that you are certainly not interested in further characterizing. ### 4. Evaluating separation across "target variables" of importance If you can specify a metric or objective function that may tell you how useful a clustering solution will be for your purposes in advance, that makes the cluster selection process much less arbitrary. There are many ways to go about doing this, but this package offers one way through a `target_list`. The `target_list` contains dataframes what we can examine our clustering results over through linear regression (continuous data), ordinal regression (ordinal data), or the Chi-squared test (categorical data). Just like when generating the initial `data_list`, we need to specify the name of the column in the provided dataframes that is originally being used to uniquely identify the different observations from each other with the `uid` parameter. We will next extend our `solutions_matrix` with p-values from regressing the `target_list` features onto our generated clusters. ```{r} extended_solutions_matrix <- extend_solutions(solutions_matrix, target_list) colnames(extended_solutions_matrix)[1:25] # Looking at the newly added columns head(no_subs(extended_solutions_matrix)) ``` If you just want the p-values: ```{r} target_pvals <- get_pvals(extended_solutions_matrix) head(target_pvals) ``` There is a heatmap for visualizing this too: ```{r eval = FALSE} pval_hm <- pval_heatmap(target_pvals, order = meta_cluster_order) save_heatmap( heatmap = pval_hm, path = "./pval_heatmap_ordered.png", width = 400, height = 500, res = 100 ) ```
![](https://raw.githubusercontent.com/BRANCHlab/metasnf/main/vignettes/pval_heatmap_ordered.png)
These p-values hold no real meaning for the traditional hypothesis-testing context, but they are reasonable proxies of the magnitude of the effect size / separation of the clusters across the variables in question. Here, they are just a tool to find clustering solutions that are well-separated according to the outcome measures you've specified. Finding a cluster solution like this is similar to a supervised learning approach, but where the optimization method is just random sampling. The risk for overfitting your data with this approach is considerable, so make sure you have some rigorous external validation before reporting your findings. We recommend using *label propagation* (provided by the SNFtool package in the `groupPredict` function) for validation: take the top clustering solutions found in some training data, assign predicted clusters to some held out test subjects, and then characterize those test subjects to see how well the clustering solution seemed to have worked. ## Validating results with label propagation Here's a quick step through of the complete procedure, from the beginning, with label propagation to validate our findings. The `metasnf` package comes equipped with a function to do the training/testing split for you :) ```{r} # All the subjects present in all dataframes with no NAs all_subjects <- data_list[[1]]$"data"$"subjectkey" # Remove the "subject_" prefix to allow merges with the original data all_subjects <- gsub("subject_", "", all_subjects) # Dataframe assigning 80% of subjects to train and 20% to test assigned_splits <- train_test_assign(train_frac = 0.8, subjects = all_subjects) # Pulling the training and testing subjects specifically train_subs <- assigned_splits$"train" test_subs <- assigned_splits$"test" # Partition a training set train_abcd_cort_t <- abcd_cort_t[abcd_cort_t$"patient" %in% train_subs, ] train_abcd_cort_sa <- abcd_cort_sa[abcd_cort_sa$"patient" %in% train_subs, ] train_abcd_subc_v <- abcd_subc_v[abcd_subc_v$"patient" %in% train_subs, ] train_abcd_h_income <- abcd_h_income[abcd_h_income$"patient" %in% train_subs, ] train_abcd_pubertal <- abcd_pubertal[abcd_pubertal$"patient" %in% train_subs, ] train_abcd_anxiety <- abcd_anxiety[abcd_anxiety$"patient" %in% train_subs, ] train_abcd_depress <- abcd_depress[abcd_depress$"patient" %in% train_subs, ] # Partition a test set test_abcd_cort_t <- abcd_cort_t[abcd_cort_t$"patient" %in% test_subs, ] test_abcd_cort_sa <- abcd_cort_sa[abcd_cort_sa$"patient" %in% test_subs, ] test_abcd_subc_v <- abcd_subc_v[abcd_subc_v$"patient" %in% test_subs, ] test_abcd_h_income <- abcd_h_income[abcd_h_income$"patient" %in% test_subs, ] test_abcd_pubertal <- abcd_pubertal[abcd_pubertal$"patient" %in% test_subs, ] test_abcd_anxiety <- abcd_anxiety[abcd_anxiety$"patient" %in% test_subs, ] test_abcd_depress <- abcd_depress[abcd_depress$"patient" %in% test_subs, ] # A data list with just training subjects train_data_list <- generate_data_list( list(train_abcd_cort_t, "cortical_thickness", "neuroimaging", "continuous"), list(train_abcd_cort_sa, "cortical_sa", "neuroimaging", "continuous"), list(train_abcd_subc_v, "subcortical_volume", "neuroimaging", "continuous"), list(train_abcd_h_income, "household_income", "demographics", "continuous"), list(train_abcd_pubertal, "pubertal_status", "demographics", "continuous"), uid = "patient" ) # A data list with training and testing subjects full_data_list <- generate_data_list( list(abcd_cort_t, "cortical_thickness", "neuroimaging", "continuous"), list(abcd_cort_sa, "cortical_surface_area", "neuroimaging", "continuous"), list(abcd_subc_v, "subcortical_volume", "neuroimaging", "continuous"), list(abcd_h_income, "household_income", "demographics", "continuous"), list(abcd_pubertal, "pubertal_status", "demographics", "continuous"), uid = "patient" ) # Construct the target lists train_target_list <- generate_data_list( list(train_abcd_anxiety, "anxiety", "behaviour", "ordinal"), list(train_abcd_depress, "depressed", "behaviour", "ordinal"), uid = "patient" ) # Find a clustering solution in your training data settings_matrix <- generate_settings_matrix( train_data_list, nrow = 5, seed = 42, min_k = 10, max_k = 30 ) train_solutions_matrix <- batch_snf( train_data_list, settings_matrix ) extended_solutions_matrix <- extend_solutions( train_solutions_matrix, train_target_list ) extended_solutions_matrix |> colnames() # The fifth row had the lowest minimum p-value across our outcomes lowest_min_pval <- min(extended_solutions_matrix$"min_pval") which(extended_solutions_matrix$"min_pval" == lowest_min_pval) # Keep track of your top solution top_row <- extended_solutions_matrix[4, ] # Use the solutions matrix from the training subjects and the data list from # the training and testing subjects to propagate labels to the test subjects propagated_labels <- lp_solutions_matrix(top_row, full_data_list) head(propagated_labels) tail(propagated_labels) ``` You could, if you wanted, see how *all* of your clustering solutions propagate to the test set, but that would mean reusing your test set and removing the protection against overfitting conferred by this procedure. ```{r} propagated_labels_all <- lp_solutions_matrix( extended_solutions_matrix, full_data_list ) head(propagated_labels_all) tail(propagated_labels_all) ``` That's all! If you have any questions, comments, suggestions, bugs, etc. feel free to post an issue at [https://github.com/BRANCHlab/metasnf](https://github.com/BRANCHlab/metasnf). ## References Caruana, Rich, Mohamed Elhawary, Nam Nguyen, and Casey Smith. 2006. “Meta Clustering.” In Sixth International Conference on Data Mining (ICDM’06), 107–18. https://doi.org/10.1109/ICDM.2006.103. Wang, Bo, Aziz M. Mezlini, Feyyaz Demir, Marc Fiume, Zhuowen Tu, Michael Brudno, Benjamin Haibe-Kains, and Anna Goldenberg. 2014. “Similarity Network Fusion for Aggregating Data Types on a Genomic Scale.” Nature Methods 11 (3): 333–37. https://doi.org/10.1038/nmeth.2810.