Skip to contents

Tutorial: driver mutations and hot spots

In the previous tutorial we assembled a binary feature matrix using assemble_genetic_features(). That approach treats each gene as a single binary feature: a sample is either mutated or not. For many analyses that is exactly what you want — but for some genes the type of mutation matters as much as its presence and you will want to separately handle mutations of different varieties or with certain functional annotations.

Consider TP53: a truncating mutation, a DNA-contact hotspot substitution (e.g. R248W), and a structural-domain mutation (e.g. Loop L3) all hit the same gene but have different biological consequences and could different frequencies across lymphoma subtypes. Similarly, CREBBP KAT domain missense mutations have a function distinct from truncating mutations. The canonical MYD88 hotspot is enriched in a different LymphGen class than other MYD88 missense mutations. What other nuances could we be missing?

This tutorial shows how to use annotate_curated_drivers() from GAMBLR.utils to add sub-gene categories to your MAF, and how to use those categories for visualisation and statistical comparison with the companion functions prettyForestPlot() (GAMBLR.viz), test_feature_associations() (GAMBLR.utils), and plot_feature_associations() (GAMBLR.viz).

library(GAMBLR.open)
library(GAMBLR.utils)
library(GAMBLR.viz)
library(tidyverse)

Obtain metadata and mutations

We will compare FL and DLBCL samples across a set of recurrently mutated genes.

metadata <- get_gambl_metadata() %>%
    filter(pathology %in% c("FL", "DLBCL")) %>%
    GAMBLR.helpers::check_and_clean_metadata(duplicate_action = "keep_first")
maf <- get_all_coding_ssm(metadata)

Annotate with driver categories

annotate_curated_drivers() appends four new columns to every row of the MAF for the genes you specify:

Column What it contains
hot_spot logical; TRUE for curated hotspot mutations
hotspot_alias the specific hotspot name (e.g. EZH2_Y641), NA otherwise
mutation_alias finest-resolution label: named hotspot, structural region, or broad class (e.g. TP53_trunc, TP53_R248)
driver_alias two-category label per gene: <GENE>_driver or <GENE>_other
genes <- c("EZH2", "CREBBP", "TP53", "MYD88", "NOTCH1", "NOTCH2")

maf <- annotate_curated_drivers(
    maf_data        = maf,
    genes_of_interest = genes
)

Let’s look at what was added for TP53:

maf %>%
    filter(Hugo_Symbol == "TP53") %>%
    select(Tumor_Sample_Barcode, Variant_Classification,
           HGVSp_Short, hot_spot, mutation_alias, hotspot_alias, driver_alias) %>%
    head(20)
genomic_data Object
Genome Build: grch37
Showing first 10 rows:
   Tumor_Sample_Barcode Variant_Classification HGVSp_Short hot_spot
1           DLBCL10456T      Missense_Mutation     p.R282W     TRUE
2           DLBCL10460T      Missense_Mutation     p.Y126D    FALSE
3           DLBCL10462T      Nonsense_Mutation     p.R196*    FALSE
4           DLBCL10466T      Missense_Mutation     p.V216M    FALSE
5           DLBCL10475T      Missense_Mutation     p.E258D    FALSE
6           DLBCL10477T      Nonsense_Mutation     p.R209*    FALSE
7           DLBCL10478T      Missense_Mutation     p.Y220C    FALSE
8           DLBCL10479T      Nonsense_Mutation     p.Q136*    FALSE
9           DLBCL10488T                 Intron        <NA>    FALSE
10          DLBCL10490T      Missense_Mutation     p.P151H    FALSE
   mutation_alias hotspot_alias driver_alias
1       TP53_R282  TP53_hotspot  TP53_driver
2       TP53_LSH1          <NA>  TP53_driver
3      TP53_trunc          <NA>  TP53_driver
4      TP53_other          <NA>   TP53_other
5      TP53_other          <NA>   TP53_other
6      TP53_trunc          <NA>  TP53_driver
7      TP53_other          <NA>   TP53_other
8      TP53_trunc          <NA>  TP53_driver
9      TP53_other          <NA>   TP53_other
10     TP53_other          <NA>   TP53_other

Notice that hotspot_alias is populated only for the strictest recurrent hotspot codons and is NA for all other rows — including truncating mutations. mutation_alias is more permissive: it labels truncations (TP53_trunc), structural-domain categories, and hotspot codons, leaving very few rows unlabelled. driver_alias collapses everything to just TP53_driver or TP53_other.

And a quick frequency count of the alias categories:

maf %>%
    filter(Hugo_Symbol == "TP53") %>%
    count(mutation_alias, sort = TRUE)
genomic_data Object
Genome Build: grch37
Showing first 10 rows:
   mutation_alias   n
1      TP53_other 351
2      TP53_trunc  98
3       TP53_R248  43
4     TP53_LoopL2  39
5     TP53_LoopL3  34
6       TP53_R175  33
7       TP53_LSH1  31
8       TP53_R273  30
9       TP53_LSH2  24
10      TP53_G245  16

TP53 driver mutations are dominated by truncations (TP53_trunc) alongside a handful of specific structural-domain and hotspot categories. Because hotspot_alias is NA for truncating mutations, genes like TP53 — whose driver biology is dominated by truncations — will have few or no hotspot_alias entries. This distinction matters when choosing how to group features for analysis (see below).

Visualising associations with prettyForestPlot

prettyForestPlot() now accepts a maf_column argument that controls the granularity of the features shown. Compare the three levels:

Hugo_Symbol — one bar per gene (baseline)

prettyForestPlot(
    maf              = maf,
    metadata         = metadata,
    comparison_column = "pathology",
    comparison_values = c("FL", "DLBCL"),
    genes            = genes,
    maf_column       = "Hugo_Symbol"
)$arranged

mutation_alias — finest resolution

Each curated region or hotspot codon becomes its own feature, revealing heterogeneity within a gene. Here TP53 splits into truncation, hotspot codon, and structural-loop categories; EZH2_Y641 stands apart from other EZH2 mutations.

prettyForestPlot(
    maf              = maf,
    metadata         = metadata,
    comparison_column = "pathology",
    comparison_values = c("FL", "DLBCL"),
    genes            = genes,
    maf_column       = "mutation_alias"
)$arranged

This level of granularity is useful for discovery, but it comes at a cost: splitting mutations into many fine-grained categories means each category is supported by fewer samples. Rare categories may not be testable at all, and the multiple-testing burden grows with the number of features. If your goal is to ask whether a gene is differentially mutated between groups — rather than which specific mutation class drives the difference — a coarser grouping is usually more powerful and easier to interpret. The next two levels of maf_column offer that tradeoff.

driver_alias — binary driver classification

Collapses to just two categories per gene (<GENE>_driver vs <GENE>_other), useful when you only want to distinguish curated driver events from background mutations without further sub-classification.

prettyForestPlot(
    maf              = maf,
    metadata         = metadata,
    comparison_column = "pathology",
    comparison_values = c("FL", "DLBCL"),
    genes            = genes,
    maf_column       = "driver_alias"
)$arranged

hotspot_alias — strictly recurrent hotspot codons only

Only the most recurrent named hotspot codons appear here. Truncating mutations and broad LOF regions do not generate a hotspot_alias entry. For TP53, whose driver biology is dominated by truncations, very few rows appear — the per-gene fallback mechanism in test_feature_associations() below handles this case automatically.

prettyForestPlot(
    maf              = maf,
    metadata         = metadata,
    comparison_column = "pathology",
    comparison_values = c("FL", "DLBCL"),
    genes            = genes,
    maf_column       = "hotspot_alias"
)$arranged

Statistical testing with test_feature_associations

prettyForestPlot() computes statistics internally but does not expose them. test_feature_associations() performs the same per-feature Fisher’s exact test and returns a tidy tibble you can inspect, filter, or feed directly into plot_feature_associations().

results <- test_feature_associations(
    maf              = maf,
    metadata         = metadata,
    comparison_column = "pathology",
    comparison_values = c("FL", "DLBCL"),
    genes            = genes,
    maf_column       = "mutation_alias"
)

results %>% arrange(q_value)
# A tibble: 22 × 13
   gene   feature    used_fallback      OR conf_low conf_high  p_value n_mutated
   <chr>  <chr>      <lgl>           <dbl>    <dbl>     <dbl>    <dbl>     <int>
 1 CREBBP CREBBP_KAT FALSE         12.8    9.03        18.4   1.15e-48       263
 2 MYD88  MYD88_L26… FALSE          0.0290 0.000730     0.165 5.87e-12       253
 3 MYD88  MYD88_oth… FALSE          0.114  0.0361       0.274 4.25e-11       318
 4 EZH2   EZH2_SET   FALSE          3.03   2.09         4.35  4.57e- 9       253
 5 TP53   TP53_other FALSE          0.179  0.0640       0.403 2.14e- 7       258
 6 CREBBP CREBBP_tr… FALSE          2.66   1.74         3.99  4.91e- 6       186
 7 NOTCH2 NOTCH2_ot… FALSE          0.331  0.182        0.563 4.81e- 6       371
 8 NOTCH2 NOTCH2_PE… FALSE          0.159  0.0188       0.598 1.36e- 3       105
 9 NOTCH1 NOTCH1_ot… FALSE          0.536  0.288        0.932 2.24e- 2       241
10 TP53   TP53_trunc FALSE          0.298  0.0595       0.916 2.86e- 2        87
# ℹ 12 more rows
# ℹ 5 more variables: n_mutated_FL <int>, n_total_FL <int>,
#   n_mutated_DLBCL <int>, n_total_DLBCL <int>, q_value <dbl>

The output contains odds ratios, confidence intervals, p-values, and Benjamini–Hochberg-adjusted q-values. The n_mutated_<group> and n_total_<group> columns carry per-group counts and effective denominators (only samples present in both the MAF and the metadata by default; set restrict_to_maf = FALSE to include all metadata samples).

Plotting test results with plot_feature_associations

The results tibble feeds directly into plot_feature_associations(). Group sizes are carried in the n_total_* columns so no additional arguments are needed for the bar plot.

plots <- plot_feature_associations(
    results         = results,
    max_q           = 0.1,
    comparison_name = "FL vs DLBCL"
)

plots$arranged

You can filter to the most significant features and inspect the underlying data before plotting:

plots$results %>%
    select(gene, feature, OR, conf_low, conf_high, q_value,
           starts_with("n_mutated_"), starts_with("n_total_"))
# A tibble: 10 × 10
   gene   feature           OR conf_low conf_high  q_value n_mutated_FL
   <chr>  <fct>          <dbl>    <dbl>     <dbl>    <dbl>        <int>
 1 EZH2   EZH2_SET      3.03   2.09         4.35  2.51e- 8           55
 2 CREBBP CREBBP_KAT   12.8    9.03        18.4   2.52e-47          106
 3 CREBBP CREBBP_trunc  2.66   1.74         3.99  1.54e- 5           39
 4 TP53   TP53_trunc    0.298  0.0595       0.916 6.30e- 2            3
 5 TP53   TP53_other    0.179  0.0640       0.403 9.40e- 7            6
 6 MYD88  MYD88_L265P   0.0290 0.000730     0.165 6.46e-11            1
 7 MYD88  MYD88_other   0.114  0.0361       0.274 3.12e-10            5
 8 NOTCH1 NOTCH1_other  0.536  0.288        0.932 5.46e- 2           15
 9 NOTCH2 NOTCH2_PEST   0.159  0.0188       0.598 3.74e- 3            2
10 NOTCH2 NOTCH2_other  0.331  0.182        0.563 1.54e- 5           16
# ℹ 3 more variables: n_mutated_DLBCL <int>, n_total_FL <int>,
#   n_total_DLBCL <int>

Advanced: controlling per-gene feature resolution

The examples above apply the same maf_column uniformly across all genes. The two subsections below show how to handle genes that don’t fit neatly into a single resolution level.

Per-gene fallback for sparse features

Some genes have no testable hotspot_alias entries (e.g. TP53, which is mutated mainly through truncations that don’t receive a hotspot alias). The fallback_column parameter handles this gracefully: when a gene has no features meeting min_samples under maf_column, it retests that gene using fallback_column instead.

results_fb <- test_feature_associations(
    maf              = maf,
    metadata         = metadata,
    comparison_column = "pathology",
    comparison_values = c("FL", "DLBCL"),
    genes            = genes,
    maf_column       = "hotspot_alias",
    fallback_column  = "mutation_alias",
    verbose          = TRUE
)

# which genes used the fallback?
results_fb %>% filter(used_fallback)
# A tibble: 9 × 13
  gene   feature      used_fallback     OR conf_low conf_high  p_value n_mutated
  <chr>  <chr>        <lgl>          <dbl>    <dbl>     <dbl>    <dbl>     <int>
1 EZH2   EZH2_other   TRUE           0.633   0.369      1.03  6.71e- 2       277
2 EZH2   EZH2_SET     TRUE           3.03    2.09       4.35  4.57e- 9       253
3 CREBBP CREBBP_KAT   TRUE          12.8     9.03      18.4   1.15e-48       263
4 CREBBP CREBBP_other TRUE           0.798   0.547      1.15  2.25e- 1       504
5 CREBBP CREBBP_trunc TRUE           2.66    1.74       3.99  4.91e- 6       186
6 NOTCH1 NOTCH1_other TRUE           0.536   0.288      0.932 2.24e- 2       241
7 NOTCH1 NOTCH1_PEST  TRUE           0       0          1.26  1.04e- 1        27
8 NOTCH2 NOTCH2_PEST  TRUE           0.159   0.0188     0.598 1.36e- 3       105
9 NOTCH2 NOTCH2_other TRUE           0.331   0.182      0.563 4.81e- 6       371
# ℹ 5 more variables: n_mutated_FL <int>, n_total_FL <int>,
#   n_mutated_DLBCL <int>, n_total_DLBCL <int>, q_value <dbl>

Manually controlling which genes use fine-grained features

The automatic fallback triggers when a gene has no testable entries under maf_column. But you may want to force the fallback for a specific gene regardless — for example, you may decide that TP53’s truncation-dominated mutation spectrum is not meaningfully split by mutation_alias in your cohort and you’d prefer it to be represented by driver_alias categories instead, while keeping the fine-grained mutation_alias resolution for all other genes.

The simplest way to achieve this is to set mutation_alias to NA for the genes you want to exempt before calling test_feature_associations(). The fallback mechanism will then take over for those genes automatically.

maf_custom <- maf %>%
    mutate(mutation_alias = ifelse(Hugo_Symbol == "TP53", NA, mutation_alias))

results_custom <- test_feature_associations(
    maf              = maf_custom,
    metadata         = metadata,
    comparison_column = "pathology",
    comparison_values = c("FL", "DLBCL"),
    genes            = genes,
    maf_column       = "mutation_alias",
    fallback_column  = "driver_alias",
    verbose          = TRUE
)

# confirm TP53 fell back and others did not
results_custom %>%
    distinct(gene, used_fallback) %>%
    arrange(gene)
# A tibble: 6 × 2
  gene   used_fallback
  <chr>  <lgl>
1 CREBBP FALSE
2 EZH2   FALSE
3 MYD88  FALSE
4 NOTCH1 FALSE
5 NOTCH2 FALSE
6 TP53   TRUE         
plot_feature_associations(
    results         = results_custom,
    max_q           = 0.1,
    comparison_name = "FL vs DLBCL"
)$arranged

This pattern generalises to any set of genes: just NA out mutation_alias (or whichever maf_column you are using) for the genes you want treated more coarsely, and let fallback_column handle them.

Happy GAMBLing!

  /$$$$$$     /$$$$$$    /$$      /$$   /$$$$$$$    /$$        .:::::::
 /$$__  $$   /$$__  $$  | $$$    /$$$  | $$__  $$  | $$        .::    .::
| $$  \__/  | $$  \ $$  | $$$$  /$$$$  | $$  \ $$  | $$        .::    .::
| $$ /$$$$  | $$$$$$$$  | $$ $$/$$ $$  | $$$$$$$   | $$   <-   .: .::
| $$|_  $$  | $$__  $$  | $$  $$$| $$  | $$__  $$  | $$        .::  .::
| $$  \ $$  | $$  | $$  | $$\  $ | $$  | $$  \ $$  | $$        .::    .::
|  $$$$$$/  | $$  | $$  | $$ \/  | $$  | $$$$$$$/  | $$$$$$$$  .::      .::
 \______/   |__/  |__/  |__/     |__/  |_______/   |________/
 ~GENOMIC~~~~~~~~~~~~~OF~~~~~~~~~~~~~~~~~B-CELL~~~~~~~~~~~~~~~~~~IN~~~~~~
 ~~~~~~~~~~~~ANALYSIS~~~~~~MATURE~~~~~~~~~~~~~~~~~~~LYMPHOMAS~~~~~~~~~~R~