4  UMAP exercises

4.1 Data and setup

The dataset contains RNA-seq measurements for 801 tumor samples from five cancer types: breast (BRCA), kidney (KIRC), colon (COAD), lung (LUAD) and prostate (PRAD). It is derived from the TCGA Pan-Cancer dataset distributed through the UCI Machine Learning Repository.

The supplied file contains log2-transformed expression values for the 1,000 genes with the largest variance across samples.

NoteDownload the data

data/tcga_pancan_top1000.csv contains 801 samples, 1,000 gene columns and the label column cancer_type.

library(uwot)
library(ggplot2)
library(patchwork)

df <- read.csv("data/tcga_pancan_top1000.csv", row.names = 1)
X <- as.matrix(df[, setdiff(colnames(df), "cancer_type")])
y <- factor(df$cancer_type)

dim(X)
[1]  801 1000
import matplotlib.pyplot as plt
import pandas as pd
from umap import UMAP

df = pd.read_csv("data/tcga_pancan_top1000.csv", index_col=0)
X = df.drop(columns="cancer_type").to_numpy()
y = df["cancer_type"].to_numpy()

X.shape
(801, 1000)

The labels are not passed to UMAP. We use them only after fitting to assess how the embedding relates to the known cancer types.

This helper keeps the plotting code short:

cancer_colors <- c(
  BRCA = "#0173b2", COAD = "#de8f05", KIRC = "#029e73",
  LUAD = "#d55e00", PRAD = "#cc78bc"
)

embedding_plot <- function(embedding, y, title = "") {
  df <- data.frame(
    UMAP1 = embedding[, 1],
    UMAP2 = embedding[, 2],
    cancer_type = y
  )
  ggplot(df, aes(UMAP1, UMAP2, color = cancer_type)) +
    geom_point(size = 0.8, alpha = 0.8) +
    scale_color_manual(values = cancer_colors) +
    labs(
      x = "UMAP 1", y = "UMAP 2",
      title = title, color = "Cancer type"
    )
}
cancer_colors = {
    "BRCA": "#0173b2",
    "COAD": "#de8f05",
    "KIRC": "#029e73",
    "LUAD": "#d55e00",
    "PRAD": "#cc78bc",
}

def plot_embedding(ax, embedding, labels, title=None):
    for cancer_type in sorted(set(labels)):
        mask = labels == cancer_type
        ax.scatter(
            embedding[mask, 0], embedding[mask, 1],
            c=cancer_colors[cancer_type], s=8, label=cancer_type
        )
    ax.set_xlabel("UMAP 1")
    ax.set_ylabel("UMAP 2")
    if title:
        ax.set_title(title)

4.2 Exercise 1: Fit and inspect a baseline embedding

Fit UMAP with n_neighbors = 15, min_dist = 0.1, Euclidean distance and a fixed random seed (random_state = 42 in Python, set.seed(42) in R). Plot the result and color it by cancer_type.

Before opening the solution, note which conclusions come from the embedding itself and which require the known labels.

set.seed(42)
embedding <- umap(
  X, n_neighbors = 15, min_dist = 0.1,
  metric = "euclidean"
)
embedding_plot(embedding, y)
A scatter plot in which most samples form five compact, label-homogeneous regions. A secondary, mostly breast-cancer island appears away from the main breast-cancer region.
Figure 4.1: Baseline UMAP embedding of the TCGA data, colored by the known cancer-type labels.
reducer = UMAP(
    n_neighbors=15,
    min_dist=0.1,
    metric="euclidean",
    random_state=42,
)
embedding = reducer.fit_transform(X)

fig, ax = plt.subplots(figsize=(5.5, 5))
plot_embedding(ax, embedding, y)
ax.legend()
A scatter plot in which most samples form five compact, label-homogeneous regions. A secondary, mostly breast-cancer island appears away from the main breast-cancer region.
Figure 4.2: Baseline UMAP embedding of the TCGA data, colored by the known cancer-type labels.

Most samples occupy five label-homogeneous regions, so cancer type is strongly associated with the neighborhood structure in this filtered dataset. In this run, a detached island contains 48 BRCA-labeled samples and one LUAD-labeled sample. The plot alone cannot tell us whether those samples represent a biological subtype, a technical effect, unusual samples or a labeling issue. That would require checking sample metadata, quality-control measures and the original expression profiles.

4.3 Exercise 2: Test parameter sensitivity

Fit all six combinations of n_neighbors in {5, 15} and min_dist in {0.01, 0.1, 0.5}. Use the same seed and Euclidean distance for every fit.

Compare the label-level pattern, the compactness of each region and the secondary, mostly BRCA island. Which observations persist across the grid, and which depend on a particular setting?

nn_values <- c(5, 15)
md_values <- c(0.01, 0.1, 0.5)

plots <- list()
for (nn in nn_values) {
  for (mind in md_values) {
    set.seed(42)
    embedding <- umap(
      X, n_neighbors = nn, min_dist = mind,
      metric = "euclidean"
    )
    plots[[paste(nn, mind)]] <- embedding_plot(
      embedding, y,
      title = paste0("n_neighbors=", nn, ", min_dist=", mind)
    )
  }
}
wrap_plots(plots, nrow = 2, ncol = 3)
A two-by-three grid of embeddings. The five main label groups persist while their shapes, positions and point density change. A secondary, mostly breast-cancer island remains visible with varying separation.
Figure 4.3: A grid of UMAP embeddings for six combinations of n_neighbors and min_dist.
nn_values = [5, 15]
min_dist_values = [0.01, 0.1, 0.5]

fig, axes = plt.subplots(2, 3, figsize=(13, 8))
for row, nn in enumerate(nn_values):
    for col, mind in enumerate(min_dist_values):
        embedding = UMAP(
            n_neighbors=nn,
            min_dist=mind,
            metric="euclidean",
            random_state=42,
        ).fit_transform(X)
        plot_embedding(
            axes[row, col], embedding, y,
            title=f"n_neighbors={nn}, min_dist={mind}",
        )

axes[0, -1].legend()
fig.tight_layout()
A two-by-three grid of embeddings. The five main label groups persist while their shapes, positions and point density change. A secondary, mostly breast-cancer island remains visible with varying separation.
Figure 4.4: A grid of UMAP embeddings for six combinations of n_neighbors and min_dist.

The five main label groups are visible throughout the grid. Their shapes, density, orientation and relative positions change, and the gap around the secondary island varies. The stable label-level separation is better supported than any claim based on the exact layout of one panel. The grid does not identify an optimal setting; it shows which visual features are sensitive to the settings.

4.4 Exercise 3: Compare distance metrics

Using the baseline values for n_neighbors, min_dist and the random seed, compare Euclidean and cosine distance. Explain what each metric treats as similar before interpreting the plots.

metrics <- c("euclidean", "cosine")
plots <- lapply(metrics, function(m) {
  set.seed(42)
  embedding <- umap(X, n_neighbors = 15, min_dist = 0.1, metric = m)
  embedding_plot(embedding, y, title = paste0("metric = ", m))
})
wrap_plots(plots, nrow = 1)
Two embeddings. Both show five mostly label-homogeneous main regions and a secondary, mostly breast-cancer island, but the regions have different layouts and shapes.
Figure 4.5: UMAP embeddings of the TCGA data using Euclidean and cosine distance.
fig, axes = plt.subplots(1, 2, figsize=(10, 4.5))
for ax, metric in zip(axes, ["euclidean", "cosine"]):
    embedding = UMAP(
        n_neighbors=15,
        min_dist=0.1,
        metric=metric,
        random_state=42,
    ).fit_transform(X)
    plot_embedding(ax, embedding, y, title=f"metric = {metric}")

axes[-1].legend()
fig.tight_layout()
Two embeddings. Both show five mostly label-homogeneous main regions and a secondary, mostly breast-cancer island, but the regions have different layouts and shapes.
Figure 4.6: UMAP embeddings of the TCGA data using Euclidean and cosine distance.

Euclidean distance measures absolute coordinate differences between expression vectors. Cosine distance compares their direction and is unchanged if a whole vector is multiplied by a positive constant. Both metrics produce five mostly label-homogeneous regions for this dataset, although the layouts differ. Agreement on that broad pattern is useful as a sensitivity check, but it does not prove that either metric is biologically correct or rule out technical effects. Metric choice should still follow the preprocessing and the scientific meaning of the measurements.

4.5 Exercise 4: Check stability across seeds

Repeat the baseline fit with two different random seeds (random_state/set.seed values of 42 and 7). Decide what remains stable and what should not be compared across the two runs.

seeds <- c(42, 7)
plots <- lapply(seeds, function(s) {
  set.seed(s)
  embedding <- umap(
    X, n_neighbors = 15, min_dist = 0.1,
    metric = "euclidean"
  )
  embedding_plot(embedding, y, title = paste0("seed = ", s))
})
wrap_plots(plots, nrow = 1)
Two embeddings with different layouts. Both contain five main label groups and a similar secondary, mostly breast-cancer island.
Figure 4.7: Two UMAP fits of the same data and settings with different random seeds.
fig, axes = plt.subplots(1, 2, figsize=(10, 4.5))
for ax, seed in zip(axes, [42, 7]):
    embedding = UMAP(
        n_neighbors=15,
        min_dist=0.1,
        metric="euclidean",
        random_state=seed,
    ).fit_transform(X)
    plot_embedding(ax, embedding, y, title=f"random_state = {seed}")

axes[-1].legend()
fig.tight_layout()
Two embeddings with different layouts. Both contain five main label groups and a similar secondary, mostly breast-cancer island.
Figure 4.8: Two UMAP fits of the same data and settings with different random seeds.

The label-level grouping and the secondary island appear in both runs, while the orientation and placement of the regions change. UMAP1 and UMAP2 have no fixed meaning, so coordinates from separate fits should not be compared directly. Repeating the fit can reveal whether a visual feature is stable, but stability alone does not establish its biological interpretation.