Principal Component Analysis for Distributional Data

Matthias Templ

2026-10-08

library(DistributionalDDA)

Introduction

Principal Component Analysis (PCA) is a fundamental technique for exploring multivariate data. In distributional data analysis, each observation is not a single number but an entire probability distribution. The DistributionalDDA package extends R’s classical princomp() function to work seamlessly with “BData” objects - the package’s representation of distributional data. This means you can use the familiar princomp(), plot(), summary(), predict(), biplot(), and screeplot() functions on your distributional data, with specialized methods that understand the functional nature of distributions.

This vignette demonstrates PCA for distributional data using the age dataset included in the package. This dataset contains age distributions from 57 communes in Upper Austria, separated by sex - a real-world example where we want to understand the main modes of variation in how populations are distributed across age groups.

What PCA for Distributions Reveals

In general, PCA for distributional data identifies the main “modes of variation” - the principal ways in which distributions in a dataset differ from each other. It answers questions like:

Questions for the Age Data

Applied to the age dataset, we can specifically ask:

Loading and Exploring the Data

Let’s start by loading the age distribution dataset:

data("age")
str(age)
## List of 3
##  $ data         :'data.frame':   114 obs. of  22 variables:
##   ..$ commune     : chr [1:114] "4010" "4020" "4030" "4040" ...
##   ..$ municipality: chr [1:114] "Linz" "Steyr" "Wels" "Altheim" ...
##   ..$ sex         : Factor w/ 2 levels "male","female": 1 1 1 1 1 1 1 1 1 1 ...
##   ..$ AGE_2       : num [1:114] 0.0517 0.0515 0.0532 0.0479 0.0512 ...
##   ..$ AGE_7       : num [1:114] 0.0446 0.0476 0.0539 0.0464 0.0532 ...
##   ..$ AGE_12      : num [1:114] 0.0452 0.0478 0.0548 0.0543 0.0552 ...
##   ..$ AGE_17      : num [1:114] 0.0507 0.0612 0.0642 0.0651 0.0649 ...
##   ..$ AGE_22      : num [1:114] 0.0689 0.0706 0.0661 0.0654 0.0638 ...
##   ..$ AGE_27      : num [1:114] 0.0824 0.0696 0.0726 0.0624 0.061 ...
##   ..$ AGE_32      : num [1:114] 0.0822 0.0644 0.0713 0.0639 0.0666 ...
##   ..$ AGE_37      : num [1:114] 0.0676 0.0607 0.0616 0.059 0.0704 ...
##   ..$ AGE_42      : num [1:114] 0.0729 0.0693 0.0796 0.0762 0.0799 ...
##   ..$ AGE_47      : num [1:114] 0.0816 0.0794 0.0825 0.0911 0.0934 ...
##   ..$ AGE_52      : num [1:114] 0.0756 0.0811 0.0774 0.0824 0.0771 ...
##   ..$ AGE_57      : num [1:114] 0.0615 0.0662 0.0623 0.0698 0.0716 ...
##   ..$ AGE_62      : num [1:114] 0.0541 0.0577 0.0549 0.0546 0.0552 ...
##   ..$ AGE_67      : num [1:114] 0.0481 0.0529 0.0456 0.0459 0.0404 ...
##   ..$ AGE_72      : num [1:114] 0.0473 0.0506 0.045 0.0451 0.0404 ...
##   ..$ AGE_77      : num [1:114] 0.0265 0.0283 0.0234 0.0319 0.026 ...
##   ..$ AGE_82      : num [1:114] 0.0222 0.0222 0.0181 0.0244 0.0204 ...
##   ..$ AGE_87      : num [1:114] 0.01236 0.01352 0.00991 0.01099 0.00687 ...
##   ..$ AGE_92      : num [1:114] 0.00448 0.00543 0.00362 0.00341 0.00242 ...
##  $ ages         : num [1:19] 2 7 12 17 22 27 32 37 42 47 ...
##  $ distributions: num [1:114, 1:19] 0.0517 0.0515 0.0532 0.0479 0.0512 ...
##   ..- attr(*, "dimnames")=List of 2
##   .. ..$ : chr [1:114] "Linz_male" "Steyr_male" "Wels_male" "Altheim_male" ...
##   .. ..$ : chr [1:19] "AGE_2" "AGE_7" "AGE_12" "AGE_17" ...

The dataset contains:

# Number of observations
nrow(age$data)
## [1] 114

# The age midpoints
age$ages
##  [1]  2  7 12 17 22 27 32 37 42 47 52 57 62 67 72 77 82 87 92

# First few rows of the data
head(age$data[, 1:7])
##   commune           municipality  sex      AGE_2      AGE_7     AGE_12
## 1    4010                   Linz male 0.05167582 0.04464853 0.04522866
## 2    4020                  Steyr male 0.05147977 0.04762422 0.04778713
## 3    4030                   Wels male 0.05318663 0.05388969 0.05480367
## 4    4040                Altheim male 0.04785847 0.04643079 0.05425202
## 5    4041 Gilgenberg am Weilhart male 0.05120178 0.05322157 0.05524137
## 6    4042         Maria Schmolln male 0.04995852 0.05198636 0.05281593
##       AGE_17
## 1 0.05071258
## 2 0.06120011
## 3 0.06422470
## 4 0.06511484
## 5 0.06493638
## 6 0.06728731

# Check that distributions sum to 1
summary(rowSums(age$distributions))
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##       1       1       1       1       1       1

Visualizing the Raw Data

Before performing PCA, let’s visualize the age distributions:

# Plot all distributions with transparency, plus the mean
matplot(age$ages, t(age$distributions), type = "l", lty = 1,
        col = adjustcolor("gray", alpha.f = 0.4),
        xlab = "Age", ylab = "Proportion",
        main = "All Age Distributions with Mean")

# Add the mean distribution
meanDist <- colMeans(age$distributions)
lines(age$ages, meanDist, col = "black", lwd = 3)
legend("topright", c("Individual", "Mean"),
       col = c("gray", "black"), lwd = c(1, 3))

We can also visualize the distributions separately by sex:

# Separate by sex
maleIdx <- which(age$data$sex == "male")
femaleIdx <- which(age$data$sex == "female")

# Plot all distributions colored by sex
matplot(age$ages, t(age$distributions[maleIdx, ]), type = "l",
        lty = 1, col = adjustcolor("blue", alpha.f = 0.3),
        ylim = c(0, 0.1), xlab = "Age", ylab = "Proportion",
        main = "Age Distributions by Sex")
matlines(age$ages, t(age$distributions[femaleIdx, ]),
         lty = 1, col = adjustcolor("red", alpha.f = 0.3))

# Add means for each sex
lines(age$ages, colMeans(age$distributions[maleIdx, ]), col = "blue", lwd = 3)
lines(age$ages, colMeans(age$distributions[femaleIdx, ]), col = "red", lwd = 3)
legend("topright", c("Male", "Female"), col = c("blue", "red"), lwd = 3)

Converting to BData Format

To perform PCA, we need to convert our data into the “BData” format used by the package. This involves:

  1. Creating a suitable Bayes space with a reference measure
  2. Defining a basis for finite-dimensional approximation
  3. Converting the histogram data to “BDataSplines”

Step 1: Create the Bayes Space

Age distributions have support on [0, 100] (approximately), so we use a uniform reference measure on this interval:

# Reference measure: Uniform on [0, 100]
rmAge <- ReferenceMeasureUniform(0, 100)

# Create the Bayes space
B2Age <- BayesSpace(rmAge)
B2Age
## BayesSpace with reference measure:ReferenceMeasureUniform(a = 0, b = 100)

# Create a basis with 7 dimensions
# (this provides a good balance between flexibility and parsimony)
basisAge <- BayesSpaceBasis(B2Age, nbasis = 7)
basisAge
## BayesSpaceBasis.ReferenceMeasureUniform(basis = basis$referenceMeasure, 
##     bayesSpace = basis, nbasis = 7)

Step 2: Convert Histogram Data to BData

The age distributions are given as proportions in 5-year age bins. We treat these as density histograms and convert them through the pipeline: histograms -> densities -> splines

# Define the histogram breaks
# Note: The last bin (90-100) is 10 years wide, others are 5 years
breaks <- c(0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50,
            55, 60, 65, 70, 75, 80, 85, 90, 100)

# Create density histograms from the proportions
ageHist <- BDataDensityHistograms(age$distributions,
                                   breaks = breaks,
                                   basis = basisAge)
class(ageHist)
## [1] "BDataDensityHistograms" "BDataHistograms"        "BData"
dim(ageHist)
## [1] 114  19

# Convert to continuous densities
ageDensities <- as.BDataDensities(ageHist)

# Convert to spline representation (needed for PCA)
ageSplines <- as.BDataSplines(ageDensities)
class(ageSplines)
## [1] "BDataSplines" "BData"
dim(ageSplines)
## [1] 114   7

Step 3: Visualize the BData

Let’s check that our conversion worked correctly:

# Plot all distributions as PDFs with transparency
plot(ageSplines, as = "PDF", col = adjustcolor("gray", alpha.f = 0.4),
     main = "All Age Distributions (as PDFs) with Mean")

# Add the mean distribution
meanAge <- mean(ageSplines)
plot(meanAge, as = "PDF", add = TRUE, col = "black", lwd = 3)
legend("topright", c("Individual", "Mean"), col = c("gray", "black"), lwd = c(1, 3))

# Plot by sex with transparency
maleSplines <- BDataSplines(unclass(ageSplines)[maleIdx, ], basis = basisAge)
femaleSplines <- BDataSplines(unclass(ageSplines)[femaleIdx, ], basis = basisAge)

plot(maleSplines, as = "PDF", col = adjustcolor("blue", alpha.f = 0.3),
     main = "Age Distributions by Sex (as PDFs)")
plot(femaleSplines, as = "PDF", col = adjustcolor("red", alpha.f = 0.3), add = TRUE)

# Add sex-specific means
plot(mean(maleSplines), as = "PDF", add = TRUE, col = "blue", lwd = 3)
plot(mean(femaleSplines), as = "PDF", add = TRUE, col = "red", lwd = 3)
legend("topright", c("Male", "Female"), col = c("blue", "red"), lwd = 3)

Performing PCA

Now we can perform PCA on our distributional data. The syntax is identical to R’s classical princomp() function - simply pass the “BData” object:

pcaAge <- princomp(ageSplines)
pcaAge
## Call:
## princomp(x = ageSplines)
## 
## Standard deviations:
##     Comp.1     Comp.2     Comp.3     Comp.4     Comp.5     Comp.6     Comp.7 
## 0.23002255 0.03460215 0.02770435 0.02165770 0.01681350 0.01309618 0.01272954 
## 
## PC1 explains 94.5 % of the variance, PC1-2 explain 96.7 %
## Basis dimension: 7 
## Number of observations: 114 
## Basis: non-orthonormal (Gram matrix correction applied)

Behind the scenes, R dispatches to the princomp.BData() method, which:

  1. Extracts the spline coefficients from the “BDataSplines” object

  2. Centers the coefficient matrix

  3. Computes the covariance matrix and its eigendecomposition

  4. Returns a “princomp.BData” object that extends the standard “princomp” class

The output shows familiar PCA information (standard deviations, variance explained) plus additional details specific to distributional data:

Summary Statistics

The summary() method works just like for standard PCA:

summary(pcaAge)
## Importance of components:
##                        Comp.1 Comp.2 Comp.3 Comp.4 Comp.5 Comp.6 Comp.7
## Standard deviation      0.230  0.035  0.028  0.022  0.017  0.013  0.013
## Proportion of Variance  0.945  0.021  0.014  0.008  0.005  0.003  0.003
## Cumulative Proportion   0.945  0.967  0.981  0.989  0.994  0.997  1.000

The summary shows:

Visualizing PCA Results

The plot() method for “princomp.BData” objects provides several plot types for exploring and interpreting PCA results. The following table summarizes all available options:

Type Description
"screeplot" Bar chart of variance explained by each PC; helps decide how many components to retain
"variance" Cumulative variance plot with reference lines at 80%, 90%, 95%
"scores" Scatter plot of observations in PC space; reveals clusters and outliers
"loadings" Shows how distribution shape changes along each PC direction
"biplot" Combines scores and loadings in one plot
"covariance" Heatmap of functional covariance surface across the domain
"reconstruction" Distributions reconstructed from a reduced number of PCs
"boxplot" Boxplots of score distributions for each PC

Screeplot: How Many Components?

The screeplot helps decide how many principal components are meaningful:

plot(pcaAge, type = "screeplot")

Interpretation: Look for an “elbow” where the variance drops off. Components before the elbow capture the main structure in the data. Here, PC1 captures most of the variance, with PC2 adding some additional information.

Cumulative Variance Plot

plot(pcaAge, type = "variance")

Interpretation: The dashed lines show common thresholds (80%, 90%, 95%). This helps decide how many components to retain based on a target variance explained.

Score Plot: Exploring Structure

The score plot shows each distribution as a point in PC space:

# Basic score plot
plot(pcaAge, type = "scores")

Coloring by Sex

A key question is whether male and female distributions differ systematically:

# Create color vector
sexColors <- ifelse(age$data$sex == "male", "blue", "red")

# Score plot colored by sex
plot(pcaAge, type = "scores", col = sexColors)
legend("topright", c("Male", "Female"), col = c("blue", "red"), pch = 19)

Interpretation: If males and females separate along PC1, this suggests that the primary mode of variation in age distributions captures sex differences. This makes sense because women tend to live longer, creating different proportions in older age groups.

Using Labels

To identify specific distributions, we can display labels instead of points. With labels = TRUE, the row names are used - in this case, the commune codes (e.g., “4101”, “4102”, etc.) combined with sex:

# Use row names as labels (commune codes)
plot(pcaAge, type = "scores", labels = TRUE, col = sexColors, cex = 0.7)

Interpretation: The labels allow us to identify which communes have unusual age structures. Communes appearing far from the center or separated from the main cluster may have distinctive demographic characteristics - for example, urban centers with younger populations or rural areas with aging communities. Pairs of labels (same commune code, different colors) that are far apart indicate communes where male and female age distributions differ substantially.

Loadings Plot: The Key to Interpretation

This is the most important plot for understanding what PCA reveals.

The loadings plot shows how the distribution shape changes along each principal component direction. For each PC, two perturbations of the mean distribution are shown:

where sdev is the standard deviation (square root of the eigenvalue) for that PC, and loading is the unit-length eigenvector. By default, scale.sdev = 1, so the perturbations represent distributions that are ±1 standard deviation away from the mean along each PC direction.

# Use colorblind-friendly colors: black (mean), blue (PC1), orange (PC2)
cbPalette <- c("black", "blue", "orange")
plot(pcaAge, type = "loadings", npcs = 2, col = cbPalette)

How to read this plot:

Detailed interpretation for the age data:

PC1 (blue lines) captures the dominant mode of variation:

Since males and females separate along PC1 (as seen in the score plot), PC1 essentially captures the sex difference in age structure - primarily driven by women’s longer life expectancy creating higher proportions in older age groups.

PC2 (orange lines) captures the second mode of variation:

PC2 likely captures demographic differences between communes - for example, urban areas attracting young adults (negative PC2) versus suburban/rural areas with established families and middle-aged residents (positive PC2).

Adjusting Scale

The scale.sdev parameter controls how far from the mean the perturbed distributions are shown. By default (scale.sdev = 1), the plot displays distributions at ±1 standard deviation along each PC direction - this represents a “typical” deviation from the mean.

To see more extreme perturbations, increase scale.sdev:

# Show +/- 2 standard deviations from mean
plot(pcaAge, type = "loadings", npcs = 2, scale.sdev = 2, col = cbPalette)

When to adjust the scale:

  • Use scale.sdev = 2 or higher when the differences between solid and dashed lines are subtle and hard to see at the default scale
  • Use scale.sdev = 0.5 if the perturbations look unrealistically extreme
  • The appropriate scale depends on your data - choose a value that shows meaningful differences without producing implausible distributions

Alternative Representations

The loadings can be displayed in different forms using the as parameter. Each representation emphasizes different aspects of the distributions:

as Description
"PDF" Probability density function (default) - shows proper densities
"logPDF" Log of PDF - emphasizes differences in the tails
"clr" Centered log-ratio - the natural Bayes space representation
"density" Density with respect to the reference measure
"logDensity" Log density with respect to reference measure

The CLR (centered log-ratio) representation is particularly useful because it is the representation in the subspace \(L^2_0\) of zero-integral square integrable log-density functions, a subspace isometric to the Bayes space - and hence the set where PCA is actually performed:

# Centered log-ratio (CLR) representation
plot(pcaAge, type = "loadings", npcs = 2, as = "clr", col = cbPalette)

Interpretation: In the CLR representation, values are centered around zero. Positive values indicate age groups with higher-than-geometric-mean proportions; negative values indicate lower-than-geometric-mean proportions. This makes it easy to see which parts of the age range increase or decrease along each PC direction.

Covariance Surface

The covariance surface shows which parts of the age range co-vary:

plot(pcaAge, type = "covariance")

Interpretation:

Reconstruction: Checking Approximation Quality

How well do the first few PCs approximate the original data?

# Reconstruct using only 2 PCs with transparency
sexColorsTransp <- ifelse(age$data$sex == "male",
                          adjustcolor("blue", alpha.f = 0.3),
                          adjustcolor("red", alpha.f = 0.3))
plot(pcaAge, type = "reconstruction", npcs = 2, col = sexColorsTransp)

Compare this to the original distributions. If the reconstructed curves look similar to the originals, 2 PCs capture most of the structure. With only 2 PCs, some detail is lost, but the main patterns (especially the male/female separation) are preserved.

Boxplot of Scores

To see how the PC scores vary across all age distributions:

plot(pcaAge, type = "boxplot", npcs = 4)

This helps identify potential outliers (points beyond the whiskers) and compare the spread along each PC direction.

Biplot

The biplot combines scores and loadings in one visualization:

biplot(pcaAge)

Interpretation:

For functional interpretation, the loadings plot (type = "loadings") is more informative.

Grouped PCA: Comparing Subgroups

When comparing groups (e.g., males vs females), we can perform PCA separately for each group:

pcaGrouped <- princomp(ageSplines, group = age$data$sex)
pcaGrouped
## Grouped PCA on distributional data
## Groups: male, female 
## 
## --- Group: male ---
##   Observations: 57 
##   PC1 explains: 79.8 % of variance
## --- Group: female ---
##   Observations: 57 
##   PC1 explains: 80.2 % of variance

Comparing Screeplots

Do males and females have similar dimensionality of variation?

plot(pcaGrouped, type = "screeplot")

Comparing Loadings

Are the modes of variation the same for both groups?

plot(pcaGrouped, type = "loadings", npcs = 2)

Interpretation: If the loadings look similar across groups, both sexes vary in similar ways. If they differ, each group has distinct patterns of age distribution variation.

Comparing Score Distributions

plot(pcaGrouped, type = "scores")

Multivariate SFPCA: Joint Analysis Across Groups

The grouped PCA above performs separate PCA for each group. But what if we want to analyze how male and female age distributions jointly vary across communes? For example, communes where males have older age structures might also have females with older age structures - this covariation between groups is not captured by separate PCAs.

Multivariate SFPCA (also called 2D-SFPCA, following Hron et al. 2016) solves this by:

  1. Stacking the clr-representations from all groups into a single vector per observation
  2. Performing a single PCA on this combined representation
  3. Finding common principal directions that span all groups

This requires paired observations: the same index must correspond to the same subject (commune) across groups. In our data, each commune has both a male and female age distribution - these are paired.

Running Multivariate SFPCA

Simply add multivariate = TRUE to the princomp() call:

pcaMulti <- princomp(ageSplines, group = age$data$sex, multivariate = TRUE)
pcaMulti
## Multivariate SFPCA (2D-SFPCA) on distributional data
## Groups: male, female 
## Observations per group: 57 
## Coefficients per group: 7 
## Total dimensions: 14 
## Note: Non-orthonormal basis, Gram matrix correction applied
## 
## Importance of components:
##                PC1       PC2       PC3       PC4       PC5        
##   Std. Dev.    0.1352    0.0642    0.0419    0.0303    0.0256     
##   Prop. Var.   0.6703    0.1510    0.0644    0.0337    0.0240     
##   Cum. Prop.   0.6703    0.8213    0.8857    0.9194    0.9434

The output shows:

Screeplot: Variance Across All Groups

plot(pcaMulti, type = "screeplot")

Interpretation: This shows the total variance explained across both male and female distributions. Compare this to the separate grouped PCA - the joint analysis typically explains variance differently because it captures cross-group covariation.

Score Plot: Communes in Joint PC Space

plot(pcaMulti, type = "scores")

Interpretation: Each point represents a commune (not a single distribution). Communes close together have similar age structures for both males and females. Outliers are communes with unusual joint age patterns.

Eigenfunctions: How Groups Vary Together

This is the key plot for interpretation. It shows how distributions change along each PC, with separate panels for each group:

plot(pcaMulti, type = "eigenfunctions", npcs = 2)

How to read this plot:

Interpretation for age data:

When to Use Multivariate vs Separate Grouped PCA

Approach Use when…
Separate (multivariate = FALSE) You want to compare modes of variation between groups
Joint (multivariate = TRUE) You have paired data and want to analyze cross-group covariation

Example questions:

Predicting Scores for New Data

If you have new age distributions, you can project them onto the existing PC space without re-running PCA:

# Suppose we have the first 5 observations as "training" data
# and want to project observations 6-10 as "new" data

# Create a subset as new BDataSplines (subsetting keeps the class)
newAgeSplines <- BDataSplines(unclass(ageSplines)[6:10, ], basis = basisAge)

# The pcaAge model was fit on all data, but let's demonstrate predict():
newScores <- predict(pcaAge, newAgeSplines)
print(newScores[, 1:3])  # Show first 3 PCs
##                                           Comp.1       Comp.2       Comp.3
## Maria Schmolln_male                   -0.1564309 -0.043924789  0.003360579
## Perwang am Grabensee_male             -0.2156627  0.001655020 -0.029175939
## St. Veit im Innkreis_male             -0.2338532  0.019797560 -0.023275240
## Alkoven_male                          -0.1688755 -0.018438557 -0.008804513
## St. Marienkirchen an der Polsenz_male -0.3137905  0.003724351 -0.038584233

# Compare with the actual scores
actualScores <- pcaAge$scores[6:10, 1:3]
print(actualScores)
##                                           Comp.1       Comp.2       Comp.3
## Maria Schmolln_male                   -0.1564309 -0.043924789  0.003360579
## Perwang am Grabensee_male             -0.2156627  0.001655020 -0.029175939
## St. Veit im Innkreis_male             -0.2338532  0.019797560 -0.023275240
## Alkoven_male                          -0.1688755 -0.018438557 -0.008804513
## St. Marienkirchen an der Polsenz_male -0.3137905  0.003724351 -0.038584233

# They should be identical (since we used the same data)

In practice, predict() is useful when:

Practical Workflow Summary

Here’s a concise workflow for PCA on distributional data:

# 1. Load data
data("age")

# 2. Create Bayes space and basis
rmAge <- ReferenceMeasureUniform(0, 100)
basisAge <- BayesSpaceBasis(BayesSpace(rmAge), nbasis = 7)

# 3. Convert to BData (adapt based on your data format)
breaks <- c(0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50,
            55, 60, 65, 70, 75, 80, 85, 90, 100)
ageSplines <- 
    BDataDensityHistograms(age$distributions, 
                           breaks = breaks, 
                           basis = basisAge) |>
  as.BataDensities() |> 
  as.BDataSplines() # pipe |> instead of nesting 

# 4. Run PCA
pcaResult <- princomp(ageSplines)

# 5. Explore results
summary(pcaResult)                              # Variance explained
plot(pcaResult, type = "screeplot")             # How many components?
plot(pcaResult, type = "loadings", npcs = 2)    # What do components mean?
plot(pcaResult, type = "scores", col = myColors) # Where are my observations?

# 6. Optional: Separate grouped analysis (compare groups)
pcaGrouped <- princomp(ageSplines, group = age$data$sex)
plot(pcaGrouped, type = "loadings", npcs = 2)   # Compare groups

# 7. Optional: Multivariate SFPCA (joint analysis, requires paired data)
pcaMulti <- princomp(ageSplines, group = age$data$sex, multivariate = TRUE)
plot(pcaMulti, type = "eigenfunctions", npcs = 2)  # Cross-group covariation

Tips and Best Practices

  1. Choose the right basis dimension: Too few dimensions may lose important variation; too many may introduce noise. Start with 5-7 and adjust based on how well reconstructions match originals.

  2. Use the loadings plot: The type = "loadings" plot is the key to interpreting what each PC means in terms of distribution shape.

  3. Consider the reference measure: The reference measure should match your data’s support. Use ReferenceMeasureUniform(a, b) for bounded data on [a, b].

  4. Check variance explained: If PC1 explains nearly 100% of variance, your data may vary primarily in one characteristic. This is not a bug but a property of your data.

  5. Use grouped PCA for comparison: When comparing subgroups, grouped PCA reveals whether they have the same or different modes of variation.

Session Info

sessionInfo()
## R version 4.3.3 (2024-02-29)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 24.04.4 LTS
## 
## Matrix products: default
## BLAS:   /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.12.0 
## LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0
## 
## locale:
##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_ZA.UTF-8        LC_COLLATE=C              
##  [5] LC_MONETARY=en_ZA.UTF-8    LC_MESSAGES=en_US.UTF-8   
##  [7] LC_PAPER=en_ZA.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_ZA.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: Europe/Berlin
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] splines   stats     graphics  grDevices utils     datasets  methods  
## [8] base     
## 
## other attached packages:
## [1] igraph_2.3.3            ggraph_2.2.2            ggplot2_4.0.2          
## [4] DistributionalDDA_0.1.1 compositions_2.0-9     
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6       tensorA_0.36.2.1   xfun_0.56          bslib_0.10.0      
##  [5] ks_1.14.2          ggrepel_0.9.5      lattice_0.22-5     vctrs_0.7.1       
##  [9] tools_4.3.3        bitops_1.0-7       generics_0.1.3     tibble_3.3.1      
## [13] DEoptimR_1.1-3     cluster_2.1.6      pkgconfig_2.0.3    fds_1.8           
## [17] Matrix_1.6-5       KernSmooth_2.23-22 RColorBrewer_1.1-3 S7_0.2.1          
## [21] lifecycle_1.0.5    compiler_4.3.3     farver_2.1.1       ggforce_0.4.1     
## [25] graphlayouts_1.2.4 htmltools_0.5.9    rainbow_3.8        sass_0.4.10       
## [29] RCurl_1.98-1.14    yaml_2.3.12        pracma_2.4.4       pillar_1.11.1     
## [33] jquerylib_0.1.4    tidyr_1.3.1        MASS_7.3-60.0.1    cachem_1.1.0      
## [37] viridis_0.6.5      mclust_6.0.1       robustbase_0.99-2  tidyselect_1.2.1  
## [41] digest_0.6.39      mvtnorm_1.2-4      dplyr_1.1.4        purrr_1.2.1       
## [45] labeling_0.4.3     polyclip_1.10-6    pcaPP_2.0-4        fastmap_1.2.0     
## [49] grid_4.3.3         colorspace_2.1-0   cli_3.6.5          magrittr_2.0.4    
## [53] dichromat_2.0-0.1  tidygraph_1.3.1    withr_3.0.2        scales_1.4.0      
## [57] rmarkdown_2.25     otel_0.2.0         gridExtra_2.3      deSolve_1.40      
## [61] memoise_2.0.1      evaluate_1.0.5     knitr_1.51         viridisLite_0.4.2 
## [65] rlang_1.1.7        Rcpp_1.1.1         glue_1.8.0         fda_6.3.0         
## [69] bayesm_3.1-6       hdrcde_3.4         tweenr_2.0.2       rstudioapi_0.18.0 
## [73] jsonlite_2.0.0     R6_2.6.1