Lecture 7
September 16, 2026
Euclidean distance between observations \(i\) and \(j\) is:
\[ d(i,j)=\sqrt{\sum_{k=1}^{p}(x_{ik}-x_{jk})^2} \]
The formula is easy. Choosing variables and scales is the hard part.
There is no universal distance for “similar states.”
1 2 3 4 5 6
1 0.00 1.80 3.05 1.03 2.73 1.53
2 1.80 0.00 3.49 1.96 4.19 1.77
3 3.05 3.49 0.00 2.55 2.76 3.05
4 1.03 1.96 2.55 0.00 2.35 1.67
5 2.73 4.19 2.76 2.35 0.00 3.10
6 1.53 1.77 3.05 1.67 3.10 0.00
The matrix is symmetric, has zero on the diagonal, and contains one distance for every pair of observations.
If we combine raw event counts, deaths, hours, and billions of dollars, damage controls almost every distance.
Standardization makes one standard deviation count equally in each variable—but that is also a substantive choice.
Two states have similar total damage but very different deaths and event counts. Are they similar? Choose the variables and scaling that match one plausible client question.
MDS starts from pairwise distances and finds a low-dimensional arrangement that preserves those distances as closely as possible.
Nearby points are similar under the chosen distance. Axis directions usually have no standalone meaning.
mds <- cmdscale(dist(x), k = 2)
mds_df <- tibble(state = metrics$state, dim1 = mds[, 1], dim2 = mds[, 2])
ggplot(mds_df, aes(dim1, dim2, label = state)) +
geom_hline(yintercept = 0, color = "gray85") +
geom_vline(xintercept = 0, color = "gray85") +
geom_text(color = deep_gold, check_overlap = TRUE) +
labs(title = "Nearby states have similar standardized NOAA profiles", x = "MDS dimension 1", y = "MDS dimension 2")raw_x <- metrics |> select(-state)
raw_mds <- cmdscale(dist(raw_x), k = 2)
ggplot(tibble(state = metrics$state, x = raw_mds[, 1], y = raw_mds[, 2]), aes(x, y, label = state)) +
geom_text(color = blue_gray, check_overlap = TRUE) +
labs(title = "Raw units", x = NULL, y = NULL)
ggplot(mds_df, aes(dim1, dim2, label = state)) +
geom_text(color = deep_gold, check_overlap = TRUE) +
labs(title = "Standardized variables", x = NULL, y = NULL)

embedded_distance <- as.vector(dist(mds))
original_distance <- as.vector(dist(x))
tibble(original_distance, embedded_distance) |>
ggplot(aes(original_distance, embedded_distance)) +
geom_point(alpha = 0.5, color = deep_gold) +
geom_abline(slope = 1, intercept = 0, color = blue_gray) +
coord_equal() +
labs(title = "Departures from the diagonal show distortion", x = "Original standardized distance", y = "Distance in the 2D map")Principal component analysis finds directions that:
PCA rotates the coordinate system; it does not discover truth.
For centered variables, the first component has the form
\[ PC_1=a_{11}X_1+a_{21}X_2+\cdots+a_{p1}X_p. \]
The weights are chosen so that the scores on \(PC_1\) have maximum variance, subject to the loading vector having length one. Later components maximize remaining variance while remaining orthogonal to earlier components.
With counts, deaths, hours, and dollars together, scaling is essential unless the units are intentionally weighted.
PC1 PC2 PC3
event_records 0.50 0.17 -0.58
injuries 0.55 -0.03 -0.29
deaths 0.43 0.23 0.69
damage 0.49 -0.07 0.30
duration 0.15 -0.95 0.05
A loading describes how strongly an original variable contributes to a component. Component signs can flip; relative patterns matter.
loading_scale <- 4
loading_df <- as_tibble(pca$rotation[, 1:2], rownames = "variable") |>
mutate(PC1 = PC1 * loading_scale, PC2 = PC2 * loading_scale)
scores <- as_tibble(pca$x[, 1:2]) |>
mutate(state = metrics$state)
ggplot(scores, aes(PC1, PC2)) +
geom_text(aes(label = state), color = deep_gold, check_overlap = TRUE) +
geom_segment(data = loading_df, aes(x = 0, y = 0, xend = PC1, yend = PC2),
inherit.aes = FALSE, arrow = arrow(length = unit(0.15, "in")), color = blue_gray) +
geom_text(data = loading_df, aes(PC1, PC2, label = variable), inherit.aes = FALSE,
color = blue_gray, vjust = -0.5) +
labs(title = "Scores locate states; arrows show loading directions")Arrows pointing together indicate variables that contribute similarly to these two components. Their displayed length depends on the biplot scaling convention.
Read the PC1 loadings. Propose a plain-language name for the component, then list one reason your name is incomplete or potentially misleading.
scores <- as_tibble(pca$x[, 1:2]) |>
mutate(state = metrics$state)
ggplot(scores, aes(PC1, PC2, label = state)) +
geom_hline(yintercept = 0, color = "gray85") +
geom_vline(xintercept = 0, color = "gray85") +
geom_text(color = deep_gold, check_overlap = TRUE) +
labs(title = "PCA summarizes state profiles on new axes")variance <- pca$sdev^2 / sum(pca$sdev^2)
tibble(component = seq_along(variance), variance) |>
ggplot(aes(component, variance)) +
geom_col(fill = gold) +
geom_line() +
geom_point() +
scale_y_continuous(labels = percent) +
labs(title = "A scree plot makes information loss visible", x = "Principal component", y = "Variance explained")tibble(component = seq_along(variance), cumulative = cumsum(variance)) |>
ggplot(aes(component, cumulative)) +
geom_line(color = deep_gold, linewidth = 1) +
geom_point(color = deep_gold, size = 3) +
geom_hline(yintercept = 0.8, linetype = 2, color = blue_gray) +
scale_y_continuous(labels = percent, limits = c(0, 1)) +
labs(title = "Retaining components trades simplicity for information", x = "Number of components", y = "Cumulative variance explained")For a centered data matrix \(X\),
\[ X=UDV^T. \]
This connection makes PCA computationally stable and links it to many other matrix methods.
Using the scree plot, choose how many components you would retain for exploration. Report the cumulative variance and one important reason variance explained is not the only criterion.
Next: nonlinear embeddings and the transition to time.
MaDS Data Visualization · Fall 2026