
We are going to see that we can gain intuition for structure in high dimensions through visualisation
It doesn’t mean that it’s easy. It doesn’t mean that visualisation is used alone. It means that (high-dimensional) visualisation is an important part of your toolbox, especially to allow discovery of what we don’t know.
Tours of high-dimensional data are like examining the shadows (projections)
(and slices/sections to see through a shadow)
Increasing dimension adds an additional orthogonal axis.
If you want more high-dimensional shapes there is an R package, geozoo, which will generate cubes, spheres, simplices, mobius strips, torii, boy surface, klein bottles, cones, various polytopes, …
And read or watch Flatland: A Romance of Many Dimensions (1884) Edwin Abbott.
Data
Projection basis
Projected data

Data is 2D:
Projection is 1D:
Notice that the values of change between (-1, 1). All possible values being shown during the tour.

watching the 1D shadows we can see:
What does the 2D data look like? Can you sketch it?

⟵
The 2D data


Data is 3D:
Projection is 2D:
Notice that the values of change between (-1, 1). All possible values being shown during the tour.
See:

Data is 4D:
Projection is 2D:
How many clusters do you see?

1D paths in 3D space

2D paths in 3D space

Grand tour: see from all sides

Guided tour: Steer towards the most interesting features.
Avoid being a blind man inspecting the elephant
Principal component analysis
Data in the model space 1
Model in the data space
library(mulgar)
p_pca_m <- pca_model(p_pca, s=2.2)
p_pca_m_d <- rbind(p_pca_m$points, penguins_sub[,1:4])
animate_xy(p_pca_m_d, edges=p_pca_m$edges,
axes="bottomleft",
edges.col="#E7950F",
edges.width=3)
render_gif(p_pca_m_d,
grand_tour(),
display_xy(half_range=4.2,
edges=p_pca_m$edges,
edges.col="#E7950F",
edges.width=3),
gif_file="gifs/p_pca_model.gif",
frames=500,
width=400,
height=400,
loop=FALSE)
Data in the model space
Model in the data space
???
Stay tuned for new work to appear later this year
library(tidyverse)
library(tourr)
library(GGally)
set.seed(946)
d <- tibble(x1=runif(200, -1, 1),
x2=runif(200, -1, 1),
x3=runif(200, -1, 1))
d <- d %>%
mutate(x4 = x3 + runif(200, -0.1, 0.1))
d <- bind_rows(d, c(x1=0, x2=0, x3=-0.5, x4=0.5))
d_r <- d %>%
mutate(x1 = cos(pi/6)*x1 + sin(pi/6)*x3,
x3 = -sin(pi/6)*x1 + cos(pi/6)*x3,
x2 = cos(pi/6)*x2 + sin(pi/6)*x4,
x4 = -sin(pi/6)*x2 + cos(pi/6)*x4)

library(tidyverse)
library(tourr)
library(GGally)
set.seed(946)
d <- tibble(x1=runif(200, -1, 1),
x2=runif(200, -1, 1),
x3=runif(200, -1, 1))
d <- d %>%
mutate(x4 = x3 + runif(200, -0.1, 0.1))
d <- bind_rows(d, c(x1=0, x2=0, x3=-0.5, x4=0.5))
d_r <- d %>%
mutate(x1 = cos(pi/6)*x1 + sin(pi/6)*x3,
x3 = -sin(pi/6)*x1 + cos(pi/6)*x3,
x2 = cos(pi/6)*x2 + sin(pi/6)*x4,
x4 = -sin(pi/6)*x2 + cos(pi/6)*x4)


How should you plot your projected data?
Best projection provided by the guided tour, separating three species.
Removing flipper length

Removing bill length

Projection

Slice

This is especially useful for exploring classification models, comparing boundaries produced by different models. (The same penguins data used here.)
Linear discriminant analysis

Classification tree

penguins_BIC <- mclustBIC(penguins_sub[,c(1,3)])
ggmc <- ggmcbic(penguins_BIC, cl=2:9, top=4) +
scale_color_discrete_divergingx(palette = "Roma") +
ggtitle("Choose model") +
theme_minimal()
penguins_mc <- Mclust(penguins_sub[,c(1,3)],
G=3,
modelNames = "EVE")
penguins_mce <- mc_ellipse(penguins_mc)
penguins_cl <- penguins_sub[,c(1,3)]
penguins_cl$cl <- factor(penguins_mc$classification)
ggell <- ggplot() +
geom_point(data=penguins_cl, aes(x=bl, y=fl,
colour=cl),
alpha=0.3) +
geom_point(data=penguins_mce$ell, aes(x=bl, y=fl,
colour=cl),
shape=16) +
geom_point(data=penguins_mce$mn, aes(x=bl, y=fl,
colour=cl),
shape=3, size=2) +
scale_color_discrete_divergingx(palette = "Zissou 1") +
theme_minimal() +
theme(aspect.ratio=1, legend.position="none") +
ggtitle("Best model: EVE")
ggmc + ggell + plot_layout(ncol=2)
Best model: four-cluster VEE

Three-cluster EEE

Market segmentation data typically has NO separated clusters. It is a partitioning.
Three different 2D data sets. What is a useful partition?


Here we show the model in the data space so we can see where it is partitioning the “blob”.

This is what the model looks like in only one variable at a time. You can’t see where it is partitioning.
Australian Vacation Activities
Austrian Winter Activities
#library(lionfish)
library(pytourr)
#data("ausActiv")
data(winterActiv)
# Animated gif
activities <- colnames(winterActiv)
wa <- winterActiv
colnames(wa) <- paste0("V", 1:27)
wa <- data.frame(wa)
set.seed(340)
render_gif(wa,
grand_tour(),
display_xy(half_range=2,
axes="off"),
gif_file = "gifs/austrian.gif",
frames = 300,
width = 400,
height = 300,
loop = FALSE)# perform initial k-means clustering
set.seed(1234)
wa_cl = kmeans(wa, 6)
wa_all <- wa |>
mutate(cl = factor(wa_cl$cluster, levels=1:6))
wa_pp <- animate_xy(wa_all[,1:27],
guided_tour(lda_pp(wa_all$cl)),
col=wa_all$cl,
half_range = 2.5,
axes="off")
set.seed(429)
render_gif(wa_all[,1:27],
guided_tour(lda_pp(wa_all$cl)),
display_xy(half_range=2,
axes="off", col=wa_all$cl),
gif_file = "gifs/austrian_cl.gif",
frames = 156,
width = 400,
height = 300,
loop = FALSE)
# Examine just clusters 3 and 6
wa_all_3_6 <- wa_all |>
filter(cl %in% c(3, 6)) |>
mutate(cl = factor(cl, labels = c("3", "6")))
set.seed(419)
wa_3_6_t1 <- save_history(wa_all_3_6[,1:27],
guided_tour(lda_pp(wa_all_3_6$cl)))
animate_xy(wa_all_3_6[,1:27],
planned_tour(wa_3_6_t1),
col=wa_all_3_6$cl,
half_range = 2.5,
axes="off")
render_gif(wa_all_3_6[,1:27],
planned_tour(wa_3_6_t1),
display_xy(half_range=2,
axes="off", col=wa_all_3_6$cl),
gif_file = "gifs/austrian_3_6.gif",
frames = 149,
width = 400,
height = 300,
loop = FALSE)
wa_sep <- tibble(act = activities,
proj = as.vector(wa_3_6_t1[,2,36])) |>
arrange(desc(proj))
save(wa_sep, file="data/wa_sep.rda")Result of -means with Euclidean distance into 6 clusters
Using a guided tour. There is some hint of the partitioning, when looking at all clusters, but there is too much overlap.
Focus on two clusters only.
We wouldn’t cluster the data this way, but the clustering algorithms don’t see gaps like we do. See also flexclust for alternative clustering approaches.
First find the separation, then examine the combination of variables.
27 variables contributing to projection
Cluster 6 consists of tourists who like going to health facilities and drinking wine.
# A tibble: 5 × 2
act proj
<chr> <dbl>
1 using.health.facilities 0.255
2 heurigen 0.119
3 going.to.a.spa 0.0885
4 organized.excursions 0.0841
5 excursions 0.0728
Cluster 3 consists of tourists who very much like going to a disco or bar.
# A tibble: 5 × 2
act proj
<chr> <dbl>
1 snowboarding -0.103
2 ski.touring -0.106
3 theater.opera -0.119
4 alpine.skiing -0.214
5 going.to.discos.bars -0.889
The tourr package provides the algorithm to generate the tour paths of projection bases, and also the ability to create new tours, and draw projections with a variety of different display methods.
With market segmentation, it is used to examine where the partitions are made, to address questions like:
All of these are easier with direct manipulation interactive graphics tools: stay tuned for Dr Laa’s presentation next!
Please use these tools with your high-dimensional analysis methods and data. 😃
Somewhere in Berlin there is now a little R sticker graffiti!
Slides made in Quarto, with code included.

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Utilise distance from the projection plane to make the slice, and shift centre of projection plane.


Increase variables, increase concentration, possibly obscuring important structure.
Transformation expands the centre to make a sage display.


TARGET BASIS (would show dog if we could find)



Givens interpolation ends at requested frame, but geodesic interpolation arrives at the plane, is frame-agnostic, and that is problematic for optimisation using the guided tour.
Convex hulls are often used to summarise clusters in 2D. It is possible to view these in high-d, too.
p_dist <- dist(penguins_sub[,1:4])
p_hcw <- hclust(p_dist, method="ward.D2")
p_cl <- data.frame(cl_w = cutree(p_hcw, 3))
penguins_mc <- Mclust(penguins_sub[,1:4],
G=3,
modelNames = "EEE")
p_cl <- p_cl %>%
mutate(cl_mc = penguins_mc$classification)
p_cl <- p_cl %>%
mutate(cl_w_j = jitter(cl_w),
cl_mc_j = jitter(cl_mc))
If you want to discover and mark the clusters you see, you can use the detourr package to spin and brush points. Here’s a live demo. Hopefully this works.
p_dist <- dist(penguins_sub[,1:4])
p_hcw <- hclust(p_dist, method="ward.D2")
p_cl <- data.frame(cl_w = cutree(p_hcw, 3))
penguins_mc <- Mclust(penguins_sub[,1:4],
G=3,
modelNames = "EEE")
p_cl <- p_cl %>%
mutate(cl_mc = penguins_mc$classification)
p_cl <- p_cl %>%
mutate(cl_w_j = jitter(cl_w),
cl_mc_j = jitter(cl_mc))| cl_w | cl_mc | ||
|---|---|---|---|
| 1 | 2 | 3 | |
| 1 | 149 | 8 | 0 |
| 2 | 0 | 0 | 119 |
| 3 | 0 | 57 | 0 |
DATA 1: projections
DATA 2: cluster labels
library(crosstalk)
library(plotly)
library(viridis)
p_cl_shared <- SharedData$new(penguins_cl)
detour_plot <- detour(p_cl_shared, tour_aes(
projection = bl:bm,
colour = cl_w)) |>
tour_path(grand_tour(2),
max_bases=50, fps = 60) |>
show_scatter(alpha = 0.7, axes = FALSE,
width = "100%", height = "450px")
conf_mat <- plot_ly(p_cl_shared,
x = ~cl_mc_j,
y = ~cl_w_j,
color = ~cl_w,
colors = viridis_pal(option = "D")(3),
height = 450) |>
highlight(on = "plotly_selected",
off = "plotly_doubleclick") %>%
add_trace(type = "scatter",
mode = "markers")
bscols(
detour_plot, conf_mat,
widths = c(5, 6)
) DAGStat 2025 - https://dicook.github.io/DAGStat_2025/