t-SNE, UMAP, and Visualizing Trends + Time Series Data

Lecture 8

Shannon Gallagher

September 21, 2026

Today

Logistics

  • Homework 4: due Tuesday, September 22 at 11:59 p.m.
  • Final project: keep working on it and bring questions or blockers to class.

Topics

  • distinguish global and local structure in an embedding,
  • compare the objectives and settings of t-SNE and UMAP,
  • explain why repeated runs and settings matter,
  • begin visualizing ordered observations as a time series.

Linear and nonlinear objectives

PCA asks for linear directions that preserve variance.

t-SNE asks for a low-dimensional arrangement that preserves local neighborhoods, using probabilities rather than raw distance.

These are different objectives. Neither is universally “better.”

A spiral exposes the linear limitation

Code
set.seed(36613)
spiral <- tibble(
  t = seq(0, 4 * pi, length.out = 500),
  x = t * cos(t) + rnorm(500, sd = 0.12),
  y = t * sin(t) + rnorm(500, sd = 0.12)
)

spiral |>
  ggplot(aes(x, y, color = t)) +
  geom_point(size = 1.6) +
  scale_color_gradient(low = purple, high = deep_gold) +
  coord_equal() +
  labs(title = "Nearby points follow a curved manifold", color = "Position")
Two-dimensional spiral colored by position along the curve.

The spiral needs both principal components

Code
spiral_pca <- prcomp(spiral |> select(x, y), scale. = TRUE)
spiral_variance <- spiral_pca$sdev^2 / sum(spiral_pca$sdev^2)

set.seed(36613)
pca_scores <- as_tibble(spiral_pca$x[, 1, drop = FALSE]) |>
  mutate(t = spiral$t, y = 0) |>
  slice_sample(prop = 1)

The curve has one intrinsic coordinate, t, but it bends through two linear directions. PC1 explains 56% of the variance and PC2 explains 44%.

Keeping PC1 alone loses the spiral

The first principal-component score for each point on the spiral, displayed along one horizontal dimension. Purple and gold points from different parts of the spiral overlap.

PC1 changes distances on the spiral

Scatterplot comparing original standardized distances among 50 sampled spiral points with their distances after projection onto PC1. Many points fall well below the perfect-preservation diagonal.

Perfect preservation would place every pair on the purple line. The points far below it represent relationships that collapse when we discard PC2.

t-SNE prioritizes local proximity

  • t-SNE gives the closest pairs the greatest weight.
  • Its central goal is to preserve local proximity: neighbors in the original space should remain neighbors in two dimensions.
  • The algorithm represents closeness through probabilities, not exact metric distances.
  • Global separation and cluster area can change substantially.

t-SNE distances on the spiral

Scatterplot comparing original standardized distances among 50 sampled spiral points with their distances in a one-dimensional t-SNE map. Pairs that were nearest neighbors in the original space are highlighted in purple and tend to remain close in the map.

t-SNE does not target the purple diagonal from the PCA slide. Its vertical scale has no original unit, and distant pairs can move substantially.

t-distributed stochastic neighbor embedding

  • exploring possible local groupings,
  • separating nearby neighborhoods,
  • creating hypotheses for later investigation.

It is not a reliable measurement of cluster size, inter-cluster distance, or global geometry.

Perplexity sets the neighborhood scale

Think of perplexity as the effective number of neighbors each point consults.

Lower perplexity

  • narrower neighborhood
  • emphasizes very local detail
  • can produce small, fragmented groups

Higher perplexity

  • broader neighborhood
  • emphasizes larger-scale patterns
  • can smooth over small local groups

For our 50 states, we will compare 5, 10, and 15. The useful scale depends on the number of observations and the structure in the data.

Two places to turn on caching

Every chunk in the document

---
execute:
  cache: true
---

One expensive R chunk

#| cache: true
#| cache.extra: !expr file.mtime("data.csv")

fit <- Rtsne::Rtsne(x)

For this deck, chunk-level caching targets the slow t-SNE fits. cache.extra forces a new result when the input file changes.

Fit t-SNE in R

Code
set.seed(36613)

fit <- Rtsne::Rtsne(
  embedding_x,
  dims = 2,
  perplexity = 10,
  check_duplicates = FALSE
)

fit$Y is a 50 × 2 matrix: one row per state and one column per plotted dimension.

Compare several settings on the NOAA state profiles

t-SNE embeddings of state-level NOAA profiles using several perplexity values.

t-SNE embeddings of state-level NOAA profiles using several perplexity values.

t-SNE embeddings of state-level NOAA profiles using several perplexity values.

Checkpoint 1 · Read with restraint

Someone says, “Cluster A is twice as far from B as from C on the t-SNE plot.” Explain why that claim is unsafe and propose a defensible statement instead.

Random starts change the embedding

t-SNE commonly begins from a random arrangement. Repeated runs can differ.

A pattern that disappears across reasonable seeds or settings is not strong evidence. Stability checks belong beside the visualization.

Compare repeated runs

Three t-SNE embeddings using different random seeds.

Three t-SNE embeddings using different random seeds.

Three t-SNE embeddings using different random seeds.

UMAP also starts with local neighborhoods

  • UMAP builds a weighted graph that connects each observation to nearby observations.
  • It searches for a low-dimensional layout with similar local connections.
  • UMAP often runs faster than t-SNE as the number of observations grows.
  • The axes, orientation, and absolute distances still have no direct interpretation.

UMAP may retain more broad structure than t-SNE, but neither method guarantees a faithful global map.

UMAP settings

Neighborhood size

n_neighbors

  • lower values emphasize smaller neighborhoods
  • higher values consider broader structure
  • similar role to perplexity, but not the same quantity

Visual spacing

min_dist

  • lower values allow tighter groups
  • higher values keep nearby points more spread out
  • changes the visual packing, not the input data

Fit UMAP in R

Code
set.seed(36613)

umap_fit <- uwot::umap(
  embedding_x,
  n_neighbors = 10,
  min_dist = 0.1,
  n_components = 2,
  n_threads = 1
)

umap_fit is a 50 × 2 matrix. Install the package once with install.packages("uwot").

t-SNE and UMAP on the same state profiles

Side-by-side t-SNE and UMAP embeddings of the same 50 standardized NOAA state profiles. States are labeled by two-letter abbreviation.

Side-by-side t-SNE and UMAP embeddings of the same 50 standardized NOAA state profiles. States are labeled by two-letter abbreviation.

Look for neighborhoods that appear in both maps. Do not compare coordinates, axis scales, or the size of empty spaces across the panels.

A responsible nonlinear embedding workflow

  1. Clean and scale variables deliberately.
  2. Try several seeds and neighborhood settings.
  3. Compare with PCA or the original variables.
  4. Investigate apparent groups in the source data.
  5. Label the embedding as exploratory.

Checkpoint 2 · Stability plan

Design a three-run stability check for an embedding of state impact profiles. What would need to remain similar before you trusted an apparent group?

From neighborhoods to temporal order

An embedding uses position to show similarity among unordered observations. Time series already have a meaningful order, and a line makes that order visible.

Before drawing it, identify:

  • time unit,
  • aggregation rule,
  • missing intervals,
  • whether the series measures events, observations, or people.

Example: NOAA event reports by day

Code
daily <- events |>
  mutate(day = as.Date(begin_dt)) |>
  count(day) |>
  complete(day = seq(min(day), max(day), by = "day"), fill = list(n = 0))

daily |>
  ggplot(aes(day, n)) +
  geom_line(color = deep_gold, linewidth = 0.7) +
  scale_x_date(date_breaks = "2 months", date_labels = "%b") +
  labs(title = "Event reports arrive in bursts", subtitle = "Daily NOAA Storm Events records, 2024", x = NULL, y = "Event records")
Line chart of daily NOAA event record counts during 2024.

Add lines to emphasize order

Code
daily |>
  ggplot(aes(day, n)) +
  geom_point(alpha = 0.35, color = blue_gray) +
  labs(title = "Points", x = NULL, y = "Event records")
daily |>
  ggplot(aes(day, n)) +
  geom_line(color = deep_gold) +
  labs(title = "A line emphasizes sequence", x = NULL, y = "Event records")

Daily NOAA counts displayed first as points and then as a line.

Daily NOAA counts displayed first as points and then as a line.

Area charts emphasize volume

Code
daily |>
  ggplot(aes(day, n)) +
  geom_area(fill = pale_gold, color = deep_gold, linewidth = 0.35) +
  scale_x_date(date_breaks = "2 months", date_labels = "%b") +
  labs(
    title = "Area emphasizes total volume",
    subtitle = "Fine day-to-day variation becomes harder to see",
    x = NULL,
    y = "Event records"
  )
Area chart of daily NOAA event counts.

Several time series

Code
events |>
  filter(event_type %in% c("Hail", "Tornado", "Heat", "Winter Weather")) |>
  count(month, event_type) |>
  complete(month = 1:12, event_type, fill = list(n = 0)) |>
  ggplot(aes(month, n, color = event_type)) +
  geom_line(linewidth = 1) +
  geom_point(size = 1.5) +
  scale_x_continuous(breaks = 1:12, labels = month.abb) +
  labs(title = "Different hazards have different seasonal signatures", x = NULL, y = "Event records", color = NULL) +
  theme(legend.position = "bottom")
Monthly lines for four NOAA event types.

Directly label lines

Code
series <- events |>
  filter(event_type %in% c("Hail", "Tornado", "Heat", "Winter Weather")) |>
  count(month, event_type) |>
  complete(month = 1:12, event_type, fill = list(n = 0))

ggplot(series, aes(month, n, color = event_type)) +
  geom_line(linewidth = 1) +
  ggrepel::geom_text_repel(
    data = filter(series, month == 12),
    aes(label = event_type),
    direction = "y",
    hjust = 0,
    nudge_x = 0.35,
    segment.color = "gray70",
    min.segment.length = 0,
    max.overlaps = Inf,
    seed = 36613,
    show.legend = FALSE
  ) +
  scale_x_continuous(breaks = 1:12, labels = month.abb, limits = c(1, 14.5)) +
  labs(title = "Label lines where the reader finishes tracing them", x = NULL, y = "Event records", color = NULL) +
  theme(legend.position = "none")
Monthly NOAA event-type lines with labels at their December endpoints.

Highlighting one series among many

Code
state_series <- events |>
  mutate(state = state.abb[match(str_to_title(state_upper), state.name)]) |>
  filter(!is.na(state)) |>
  count(state, month) |>
  complete(state, month = 1:12, fill = list(n = 0))

ggplot(state_series, aes(month, n, group = state)) +
  geom_line(color = "gray82") +
  geom_line(data = filter(state_series, state == "TX"), color = deep_gold, linewidth = 1.4) +
  scale_x_continuous(breaks = 1:12, labels = month.abb) +
  labs(title = "Texas in context", subtitle = "Other states remain visible in gray", x = NULL, y = "Event records")
Monthly event counts for many states with Texas highlighted.

Time axes and missing intervals

  • Show the full relevant time window.
  • Make missing intervals explicit.
  • Avoid unequal spacing that visually implies equal time.
  • Use zero when magnitude comparison requires it; explain justified truncation.
  • Separate seasonal patterns from changes in coverage or reporting.

Checkpoint 3 · What is the series?

For the multi-series plot, write what one point represents. Then identify one reporting or exposure issue that prevents the line from being interpreted as pure weather frequency.

Recap and next steps

  • Nonlinear embeddings are exploratory neighborhood views.
  • t-SNE and UMAP use different objectives and settings.
  • Settings, seeds, and preprocessing are part of the result.
  • Time adds order and makes missing intervals consequential.
  • A line chart is only as meaningful as its aggregation rule.

Next: time-series structure and spatial foundations.