10  Principal Component Analysis

Principal Component Analysis (PCA) is a technique used to replace a set of correlated variables by a smaller set of new, uncorrelated variables — the principal components — that retain as much of the original variability as possible.

Where clustering looks for structure among the rows of a data set (which clients resemble each other?), PCA looks for structure among the columns (which variables carry the same information?). The two questions are complementary, and in practice PCA is very often the step that comes before clustering or classification.

PCA was introduced by Pearson (1901) as a problem of fitting lines and planes to a cloud of points, and developed in its modern statistical form by Hotelling (1933). A comprehensive treatment is given by Jolliffe (2002).

10.1 Why Reduce Dimension?

A retail bank stores dozens — often hundreds — of numeric attributes per client: balances across several account types, transaction counts by channel, credit exposure, tenure, product holdings, contact history. An insurer stores a similar volume per policy. Working with all of them at once creates four practical difficulties.

Redundancy

Many variables measure the same underlying thing. A client’s income, current-account balance, savings balance, investment holdings and credit limit are all partial views of a single unobserved quantity we might call affluence. Keeping five columns to describe one construct adds noise, not information.

Multicollinearity

Strongly correlated predictors destabilise regression models: coefficients become large, sign-flipped and highly sensitive to small changes in the sample. Because principal components are uncorrelated by construction, they offer one way out of this problem.

Visualisation

We can look at a scatterplot of two variables, and with effort at a matrix of pairwise plots for five or six. We cannot look at a thirteen-dimensional cloud. PCA gives us the best possible two-dimensional “photograph” of that cloud, in a precise sense defined below.

The cost of dimension

Distances, densities and neighbourhoods behave badly in high dimension — the phenomenon usually called the curse of dimensionality. Since \(k\)-means, \(k\)-medoids and \(k\)-nearest-neighbours all rest on distances, reducing dimension first often improves both their speed and their results.

Note

PCA does not select a subset of the original variables. It builds new variables, each one a weighted combination of all the originals. If your goal is to discard columns and keep the rest (for example, because measuring each variable is expensive), PCA is the wrong tool — you want variable selection instead.

10.1.1 Applications in Banking and Insurance

Client segmentation

Reducing a wide client table to two or three interpretable axes — affluence, digital engagement, credit strain — and then clustering on those axes rather than on the raw variables.

Credit and behavioural scoring

Compressing correlated bureau and transaction variables into orthogonal components before fitting a scorecard, so that coefficient estimates remain stable.

Yield-curve and market-risk modelling

The classic financial application: applied to daily changes in interest rates across maturities, PCA almost always returns three components interpretable as level, slope and curvature, which together explain over 95% of yield-curve movement. Risk systems hedge these three factors instead of every maturity separately.

Insurance portfolio analysis

Summarising policyholder and claims characteristics (exposure, frequency, severity, coverage limits, bonus-malus level) into a small number of risk dimensions for tariff analysis or reinsurance decisions.

Fraud and anomaly detection

An observation that is poorly reconstructed from the first few components — that is, one lying far from the low-dimensional subspace where ordinary clients live — is a natural candidate for investigation.

Questionnaire and survey analysis

Identifying the latent dimensions behind a battery of customer-satisfaction items, which are typically far more numerous than the concepts they measure.

Packages and commands. No code in this section. The chapter uses stats for the analysis itself (prcomp, later varimax and promax), dplyr for the data preparation, factoextra and corrplot for the standard PCA figures, gridExtra to place figures side by side, and GPArotation for the rotation criteria that stats does not provide. All are installed and loaded by the setup chunk at the top of the chapter.

10.2 The Data: A Retail Bank Client Portfolio

Throughout this chapter we work with a simulated portfolio of \(600\) retail bank clients. The data are generated rather than downloaded so that every result in this chapter is exactly reproducible in your browser, but the variables, their units and their correlation structure are typical of a real retail banking table.

Variable Type Description Unit
client_id id Anonymised client identifier —
age numeric Age of the client years
tenure numeric Time as a client of the bank years
income numeric Declared annual net income €
balance_current numeric Average balance, current account €
balance_savings numeric Average balance, savings products €
investments numeric Value of investment holdings €
credit_limit numeric Total credit-card limit granted €
utilization numeric Share of the credit limit in use %
loan_balance numeric Outstanding loan and mortgage debt €
card_tx numeric Card transactions per month count
mobile_logins numeric Mobile-app logins per month count
branch_visits numeric Branch visits per year count
missed_payments numeric Missed payments in the last 24 months count
segment factor Commercial segment: Mass / Affluent / Private —
region factor Region of residence —
churn factor Client closed the relationship in the last year —

The code below creates the data set. Run it once: every interactive box in this chapter uses the object bank that it leaves behind.

Before any multivariate method, look at the marginal distributions. This is exactly the exploratory work of the EDA chapter, and it determines the choices we make next.

summary(bank[, c("income", "balance_savings", "investments",
                 "utilization", "mobile_logins", "missed_payments")])
     income       balance_savings    investments      utilization   
 Min.   :  4500   Min.   :    180   Min.   :    40   Min.   : 0.00  
 1st Qu.: 18000   1st Qu.:   4368   1st Qu.:  1478   1st Qu.:19.10  
 Median : 24950   Median :  10685   Median :  4930   Median :33.60  
 Mean   : 30208   Mean   :  22762   Mean   : 17393   Mean   :34.41  
 3rd Qu.: 37950   3rd Qu.:  24255   3rd Qu.: 13492   3rd Qu.:47.20  
 Max.   :144500   Max.   :1209700   Max.   :577330   Max.   :99.00  
 mobile_logins    missed_payments  
 Min.   :  1.00   Min.   : 0.0000  
 1st Qu.: 10.00   1st Qu.: 0.0000  
 Median : 16.00   Median : 0.0000  
 Mean   : 18.75   Mean   : 0.8717  
 3rd Qu.: 24.00   3rd Qu.: 1.0000  
 Max.   :184.00   Max.   :12.0000  

10.2.1 Skewness and the Log Transform

The monetary variables are strongly right-skewed, as monetary variables almost always are: most clients hold modest amounts and a small number hold very large ones.

skewness <- function(x) mean((x - mean(x))^3) / sd(x)^3
round(sapply(bank[num_vars], skewness), 2)
            age          tenure          income balance_current balance_savings 
           0.09            0.42            2.02            2.53           15.10 
    investments    credit_limit     utilization    loan_balance         card_tx 
           7.29            3.47            0.34            3.55            2.11 
  mobile_logins   branch_visits missed_payments 
           4.16            1.96            3.07 

The savings balance has a skewness of \(15.1\) and investments of \(7.3\). PCA is a variance-based method, and variance is not robust: a handful of private-banking clients with balances two orders of magnitude above the median would, on the raw scale, define the first component almost by themselves. Taking logarithms of the monetary variables brings their skewness essentially to zero:

library(dplyr)

X <- bank %>%
  transmute(age, tenure,
            log_income  = log(income),
            log_current = log(balance_current),
            log_savings = log(balance_savings),
            log_invest  = log(investments),
            log_limit   = log(credit_limit),
            utilization,
            log_loan    = log(loan_balance),
            card_tx,
            mobile      = mobile_logins,
            branch      = branch_visits,
            missed      = missed_payments)

round(sapply(X, skewness), 2)
        age      tenure  log_income log_current log_savings  log_invest 
       0.09        0.42        0.04        0.07        0.01        0.04 
  log_limit utilization    log_loan     card_tx      mobile      branch 
       0.11        0.34        0.06        2.11        4.16        1.96 
     missed 
       3.07 

The same matrix is built in your browser session by the helper below. Every interactive box from here on begins with X <- make_X(), so you can run them in any order.

Tip

Log transformation of monetary variables before PCA is standard practice in banking and insurance analytics. It has three effects: it removes the dominance of a few very large clients, it turns multiplicative relationships (a client with twice the income tends to hold twice the savings) into additive ones, which is what linear methods can capture, and it makes the resulting components easier to read as differences in order of magnitude rather than in euros.

Beware of zeros: \(\log(0)\) is undefined. Common remedies are \(\log(x+1)\), or treating “holds no investments” as a separate binary variable.

The count variables card_tx, mobile and branch keep some skewness, which is normal for counts and not damaging at this level. We leave them untransformed so that their scale stays interpretable.

10.2.2 The Correlation Matrix

PCA is, in the end, a way of reading a correlation matrix. It is worth looking at that matrix directly first.

library(corrplot)
corrplot(cor(X), method = "color", type = "upper",
         order = "hclust", tl.col = "black", tl.srt = 45,
         addCoef.col = "grey30", number.cex = 0.55)

Correlation matrix of the transformed bank variables. Blocks of mutually correlated variables are exactly what PCA will summarise.

Three blocks stand out:

  • the monetary variables (log_income, log_savings, log_invest, log_limit, log_current) move together;
  • the channel variables (card_tx, mobile) move together and both move against branch, age and tenure;
  • utilization, log_loan and missed form a third, smaller block.

If the correlation matrix were the identity — all variables mutually uncorrelated — PCA would have nothing to do: every component would explain exactly \(1/p\) of the variance and no reduction would be possible. PCA is useful precisely to the extent that the variables are correlated.

Packages and commands. Simulation with set.seed, rnorm, rpois, runif and sample; marginal distributions with summary and sapply; the log transform with dplyr’s transmute; the correlation matrix with cor and corrplot’s corrplot(method = "color", order = "hclust"), whose order argument is what makes the blocks visible.

10.3 Pre-PCA Diagnostic Tests

The previous section ended on a qualitative statement: PCA is useful to the extent that the variables are correlated. Two classical diagnostics turn that statement into numbers, and they are routinely reported before a PCA:

  • Bartlett’s test of sphericity asks whether there is any correlation to summarise;
  • the Kaiser–Meyer–Olkin (KMO) measure asks whether that correlation is shared across many variables, which is what a small number of components can capture, or confined to isolated pairs.

Both come from the factor-analysis tradition. They do not decide whether PCA can be computed — it always can — but whether it is likely to produce a useful reduction.

10.3.1 Bartlett’s Test of Sphericity

Bartlett’s test (Bartlett 1950) tests the hypothesis that the population correlation matrix is the identity: \[ H_0: \mathbf{R} = \mathbf{I}_p \qquad \text{versus} \qquad H_1: \mathbf{R} \neq \mathbf{I}_p . \] Under \(H_0\) the variables are uncorrelated and, in the language of the previous section, PCA has nothing to do. The test statistic is built from the determinant of the sample correlation matrix \(\hat{\mathbf{R}}\): \[ \chi^2 = -\left(n - 1 - \frac{2p + 5}{6}\right) \ln |\hat{\mathbf{R}}| , \] which, under \(H_0\) and multivariate normality, is approximately \(\chi^2\)-distributed with \(p(p-1)/2\) degrees of freedom.

The determinant is the natural quantity to look at. It equals the product of the eigenvalues of \(\hat{\mathbf{R}}\), so \(|\hat{\mathbf{R}}| = 1\) when the variables are uncorrelated and \(|\hat{\mathbf{R}}| \to 0\) as they become redundant — that is, as some eigenvalues approach zero. A small determinant means a large statistic.

bartlett_sphericity <- function(X) {
  R    <- cor(X)
  n    <- nrow(X)
  p    <- ncol(X)
  stat <- -(n - 1 - (2 * p + 5) / 6) * log(det(R))
  df   <- p * (p - 1) / 2
  c(det = det(R), chisq = stat, df = df,
    p.value = pchisq(stat, df, lower.tail = FALSE))
}

signif(bartlett_sphericity(X), 4)
      det     chisq        df   p.value 
6.974e-04 4.316e+03 7.800e+01 0.000e+00 

The determinant is \(0.0007\), the statistic is \(\chi^2 = 4316\) on \(78\) degrees of freedom, and the \(p\)-value is zero to machine precision. The hypothesis of no correlation is rejected overwhelmingly.

That conclusion is less informative than it looks. The test has enormous power at realistic sample sizes, so it rejects for correlations far too weak to be worth summarising. Compare a matrix of pure noise of the same shape with the first \(30\) clients of our portfolio:

set.seed(1)
noise <- matrix(rnorm(600 * 13), nrow = 600, ncol = 13)
signif(bartlett_sphericity(noise), 4)       # 600 x 13, no structure
    det   chisq      df p.value 
 0.8788 76.7400 78.0000  0.5191 
signif(bartlett_sphericity(X[1:30, ]), 4)   # only 30 clients
      det     chisq        df   p.value 
1.703e-05 2.617e+02 7.800e+01 1.090e-21 

On noise the test correctly fails to reject (\(\chi^2 = 76.7\), \(p = 0.52\)); on just \(30\) clients it already rejects with \(p \approx 10^{-21}\). Passing Bartlett’s test is therefore a minimum requirement, not a recommendation: if it does not reject, stop — there is nothing for PCA to find. If it does, move on to KMO.

Warning

stats::bartlett.test() is not this test. It is Bartlett’s test for homogeneity of variances across groups, by the same author, and applying it to a data frame returns a meaningless result without an error. Use the function above or psych::cortest.bartlett().

The \(\chi^2\) approximation also assumes multivariate normality. Our count variables and the bounded utilization are clearly not normal, so treat the \(p\)-value as approximate — which, given how far it sits from any threshold, changes nothing here.

10.3.2 The Kaiser–Meyer–Olkin Measure of Sampling Adequacy

The KMO measure (Kaiser 1970, 1974) compares the ordinary correlations with the partial correlations — the correlation between two variables after the linear effect of all the others has been removed. The partial correlations are read off the inverse of the correlation matrix, \(\mathbf{Q} = \mathbf{R}^{-1}\): \[ a_{ij} = -\frac{q_{ij}}{\sqrt{q_{ii}\, q_{jj}}} . \]

The logic is this. If variables \(i\) and \(j\) are correlated because both reflect a common underlying dimension that other variables also reflect, then controlling for those other variables removes most of the link and \(a_{ij}\) is small. If instead the two are linked only to each other, the partial correlation stays as large as the raw one. Components summarise the first kind of correlation, not the second.

The overall index compares the two sums of squares over all pairs \(i \neq j\): \[ \text{KMO} = \frac{\sum_{i}\sum_{j \neq i} r_{ij}^2}{\sum_{i}\sum_{j \neq i} r_{ij}^2 + \sum_{i}\sum_{j \neq i} a_{ij}^2} , \] and the measure of sampling adequacy of a single variable restricts the sums to its own column: \[ \text{MSA}_j = \frac{\sum_{i \neq j} r_{ij}^2}{\sum_{i \neq j} r_{ij}^2 + \sum_{i \neq j} a_{ij}^2} . \] Both lie between \(0\) and \(1\). Values near \(1\) mean the partial correlations are negligible compared with the raw ones; a value of \(0.5\) is what uncorrelated noise produces. Kaiser’s own labels for the scale are still the ones in use:

KMO Kaiser’s verdict
\(\geq 0.90\) marvellous
\(0.80\) – \(0.89\) meritorious
\(0.70\) – \(0.79\) middling
\(0.60\) – \(0.69\) mediocre
\(0.50\) – \(0.59\) miserable
\(< 0.50\) unacceptable

These labels are Kaiser’s verbal conventions from factor analysis, not inferential cut-offs: no sampling distribution stands behind them, and a middling value does not stop PCA from finding clear components, as the rest of this chapter shows.

kmo <- function(X) {
  R <- cor(X)
  Q <- solve(R)
  A <- -Q / sqrt(diag(Q) %o% diag(Q))   # partial correlations
  diag(R) <- 0
  diag(A) <- 0
  list(overall = sum(R^2) / (sum(R^2) + sum(A^2)),
       msa     = colSums(R^2) / (colSums(R^2) + colSums(A^2)))
}

kmo_X <- kmo(X)
round(kmo_X$overall, 3)
[1] 0.784
round(sort(kmo_X$msa), 3)
utilization      mobile      tenure     card_tx         age      missed 
      0.610       0.644       0.655       0.656       0.674       0.677 
   log_loan      branch  log_income   log_limit  log_invest log_savings 
      0.717       0.888       0.891       0.892       0.903       0.912 
log_current 
      0.933 
round(kmo(noise)$overall, 3)
[1] 0.491

The overall KMO is \(0.784\): middling, close to meritorious, and comfortably adequate for PCA. Pure noise gives \(0.491\), as expected.

The per-variable values are more instructive than the overall one. The five monetary variables all score between \(0.89\) and \(0.93\): their raw correlations with one another lie between \(0.51\) and \(0.71\), but their partial correlations only between \(0.06\) and \(0.31\), because the block explains each pair jointly. The lowest scores — utilization (\(0.61\)), mobile (\(0.64\)), tenure (\(0.66\)), card_tx (\(0.66\)), age (\(0.67\)) and missed (\(0.68\)) — belong to variables whose correlation is concentrated in one or two partners:

Rm <- cor(X)
Qm <- solve(Rm)
Am <- -Qm / sqrt(diag(Qm) %o% diag(Qm))   # partial correlations

pairs <- rbind(c("age", "tenure"), c("card_tx", "mobile"),
               c("utilization", "missed"), c("utilization", "log_loan"),
               c("log_savings", "log_invest"))
data.frame(pair    = paste(pairs[, 1], "-", pairs[, 2]),
           raw     = round(Rm[pairs], 2),
           partial = round(Am[pairs], 2))
                      pair  raw partial
1             age - tenure 0.86    0.81
2         card_tx - mobile 0.82    0.79
3     utilization - missed 0.55    0.47
4   utilization - log_loan 0.37    0.41
5 log_savings - log_invest 0.65    0.20

age and tenure are correlated at \(0.86\), and after removing the other eleven variables they are still correlated at \(0.81\): older clients have simply been clients for longer, and no other variable explains that. The same holds for card_tx and mobile (\(0.82 \to 0.79\)) and for the credit-strain trio. The monetary pair, by contrast, falls from \(0.65\) to \(0.20\): most of what links savings and investments is the affluence that every other monetary variable also measures.

A low MSA does not make a variable useless to PCA — these same variables will define the digital-engagement and credit-strain components later in the chapter. It says that their correlation is local, so that it takes a component to describe a small group of variables rather than a large one. The usual rule is to consider dropping variables with \(\text{MSA}_j < 0.5\), one at a time and starting from the lowest, recomputing after each removal. None of ours comes close, so all thirteen are kept.

ImportantWhen the diagnostics cannot be computed

KMO needs \(\mathbf{R}^{-1}\) and Bartlett needs \(\ln|\mathbf{R}|\), so both fail when \(\mathbf{R}\) is singular: when \(p \geq n\), or when one variable is an exact linear combination of others — a total alongside its parts, or a full set of dummy variables. solve then stops with an error and det returns zero. Remove the redundancy first. PCA itself still runs in these cases, and is often exactly the right tool for them; it is only the diagnostics that break.

Packages and commands. Both diagnostics need only base R: cor, det and pchisq for Bartlett’s statistic, and solve with the outer product %o% for the partial correlations behind KMO. The psych package wraps them as cortest.bartlett(R, n = ) and KMO(X). Do not confuse either with stats::bartlett.test, which tests equality of variances.

10.4 Notation, Centering and Scaling

Let \(\mathbf{X}\) be an \(n \times p\) data matrix: \(n\) clients in rows, \(p\) variables in columns. Write \(\mathbf{x}_i = (x_{i1}, \ldots, x_{ip})^{\top}\) for the record of client \(i\), and \(\bar{x}_j\) and \(s_j\) for the sample mean and standard deviation of variable \(j\).

10.4.1 Centering

PCA always begins by centering each variable: \[ \tilde{x}_{ij} = x_{ij} - \bar{x}_j . \] Centering moves the origin of the coordinate system to the centre of gravity of the cloud of points. It is not optional: the components are directions through the mean, and without centering the first component would mostly point from the origin towards the mean of the data, which tells us nothing about its shape.

10.4.2 Scaling

Scaling is optional, and it is the single most consequential decision in a PCA. If we also divide by the standard deviation, \[ z_{ij} = \frac{x_{ij} - \bar{x}_j}{s_j}, \] then every variable has variance \(1\), and PCA is performed on the correlation matrix. If we do not, PCA is performed on the covariance matrix, and variables with large numerical variance dominate.

Look at the raw variances in our data:

round(apply(bank[, num_vars], 2, var))
            age          tenure          income balance_current balance_savings 
            150              33       336024916        14134909      3343558545 
    investments    credit_limit     utilization    loan_balance         card_tx 
     2227378091        13769324             383       979512350             204 
  mobile_logins   branch_visits missed_payments 
            185              33               2 

The variance of balance_savings is of the order of \(3.3 \times 10^{9}\) (euros squared); the variance of missed_payments is about \(2\) (counts squared). These numbers are not comparable — they are not even in the same units. A PCA on the covariance matrix of these columns is a PCA of the savings balance and nothing else:

pca_unscaled <- prcomp(bank[, num_vars], scale. = FALSE)
round(100 * pca_unscaled$sdev^2 / sum(pca_unscaled$sdev^2), 1)[1:4]
[1] 66.7 18.3 12.0  2.8
round(pca_unscaled$rotation[, 1:2], 3)
                  PC1    PC2
age             0.000  0.000
tenure          0.000  0.000
income          0.167  0.137
balance_current 0.026  0.004
balance_savings 0.794 -0.556
investments     0.575  0.637
credit_limit    0.031  0.034
utilization     0.000  0.000
loan_balance    0.100  0.515
card_tx         0.000  0.000
mobile_logins   0.000  0.000
branch_visits   0.000  0.000
missed_payments 0.000  0.000

The first component absorbs \(66.7\%\) of the variance and loads almost entirely on balance_savings and investments. Age, utilisation and every count variable have loadings indistinguishable from zero — not because they carry no information, but because they are measured in small numbers.

Warning

When variables are measured in different units, always use the correlation matrix (scale. = TRUE in prcomp, or cor = TRUE in princomp). Otherwise the analysis reflects your choice of units rather than the structure of the data: converting income from euros to thousands of euros removes it from the first component altogether (its loading falls from \(0.17\) to \(0\)), although nothing about the clients has changed.

The covariance matrix is appropriate only when all variables share the same unit and their differences in variability are themselves meaningful — for example, daily returns of a set of equities, or changes in yields across maturities.

From here on, every PCA is on the correlation matrix of the transformed matrix X.

Packages and commands. scale centres and standardises a matrix, and apply(X, 2, var) shows why that matters here. prcomp(X, scale. = TRUE) runs the PCA on the correlation matrix and scale. = FALSE on the covariance matrix; the older princomp makes the same choice through cor = TRUE.

10.5 The Algebra of PCA

10.5.1 The First Principal Component

Let \(\mathbf{Z}\) be the centred and scaled data matrix, and consider a direction in variable space given by a unit vector \(\mathbf{a} = (a_1, \ldots, a_p)^{\top}\), with \(\mathbf{a}^{\top}\mathbf{a} = 1\). Projecting the data onto this direction produces a new variable \[ y_i = \mathbf{a}^{\top}\mathbf{z}_i = a_1 z_{i1} + a_2 z_{i2} + \cdots + a_p z_{ip}, \qquad i = 1, \ldots, n , \] a linear combination of the original variables. Its sample variance is \[ \operatorname{Var}(y) = \mathbf{a}^{\top}\mathbf{S}\,\mathbf{a}, \] where \(\mathbf{S}\) is the sample covariance matrix of \(\mathbf{Z}\) — which, because the variables are standardised, is the correlation matrix \(\mathbf{R}\) of the original variables.

The first principal component is the direction that makes this variance as large as possible: \[ \mathbf{a}_1 = \arg\max_{\mathbf{a}^{\top}\mathbf{a} = 1} \; \mathbf{a}^{\top}\mathbf{S}\,\mathbf{a}. \]

The constraint \(\mathbf{a}^{\top}\mathbf{a} = 1\) is essential: without it the variance could be made arbitrarily large simply by multiplying \(\mathbf{a}\) by a large constant, which changes the scale of the new variable but not the direction it points in.

Introducing a Lagrange multiplier \(\lambda\), we maximise \[ \mathbf{a}^{\top}\mathbf{S}\mathbf{a} - \lambda(\mathbf{a}^{\top}\mathbf{a} - 1), \] and setting the derivative with respect to \(\mathbf{a}\) to zero gives \[ 2\mathbf{S}\mathbf{a} - 2\lambda \mathbf{a} = \mathbf{0} \qquad \Longleftrightarrow \qquad \mathbf{S}\mathbf{a} = \lambda \mathbf{a}. \]

This is the eigenvalue equation. The optimal direction \(\mathbf{a}\) must be an eigenvector of \(\mathbf{S}\), and its associated \(\lambda\) is an eigenvalue. Moreover, pre-multiplying by \(\mathbf{a}^{\top}\), \[ \mathbf{a}^{\top}\mathbf{S}\mathbf{a} = \lambda\, \mathbf{a}^{\top}\mathbf{a} = \lambda , \] so the variance captured by a component is its eigenvalue. To maximise variance we take the eigenvector belonging to the largest eigenvalue.

Note

Two readings of the same result:

  • Maximum variance: the first component is the direction along which the clients are most spread out.
  • Best fit: the first component is also the line that minimises the sum of squared perpendicular distances from the points to the line. This is the geometry of Pearson (1901), and it is not the same as the line fitted by ordinary least squares, which minimises vertical distances to a designated response variable.

Both descriptions define the same direction, which is why PCA can be presented either as variance maximisation or as low-dimensional approximation.

10.5.2 Subsequent Components

The second component solves the same problem with one extra requirement: it must be uncorrelated with the first. Since \[ \operatorname{Cov}(\mathbf{a}_1^{\top}\mathbf{z},\; \mathbf{a}_2^{\top}\mathbf{z}) = \mathbf{a}_1^{\top}\mathbf{S}\mathbf{a}_2 = \lambda_1 \mathbf{a}_1^{\top}\mathbf{a}_2, \] requiring zero correlation is equivalent to requiring \(\mathbf{a}_1^{\top}\mathbf{a}_2 = 0\): the directions are orthogonal. Continuing in this way, the \(k\)-th component is the direction of maximum remaining variance among those orthogonal to the first \(k-1\).

Because \(\mathbf{S}\) is symmetric and positive semi-definite, it admits the spectral decomposition \[ \mathbf{S} = \mathbf{A}\,\boldsymbol{\Lambda}\,\mathbf{A}^{\top}, \qquad \mathbf{A} = [\mathbf{a}_1, \ldots, \mathbf{a}_p], \qquad \boldsymbol{\Lambda} = \operatorname{diag}(\lambda_1, \ldots, \lambda_p), \] with \(\lambda_1 \ge \lambda_2 \ge \cdots \ge \lambda_p \ge 0\) and \(\mathbf{A}^{\top}\mathbf{A} = \mathbf{I}\). The columns of \(\mathbf{A}\) are the \(p\) principal directions, sorted by the variance they explain, and they form an orthonormal basis: PCA is a rotation of the coordinate system, nothing more, until we decide to discard some of the new axes.

10.5.3 Total Variance is Preserved

Because the trace of a matrix is invariant under this rotation, \[ \sum_{k=1}^{p} \lambda_k = \operatorname{tr}(\mathbf{S}) = \sum_{j=1}^{p} s_{jj}. \] On the correlation matrix every \(s_{jj} = 1\), so \[ \sum_{k=1}^{p} \lambda_k = p . \]

With \(p = 13\) variables, the eigenvalues of our correlation matrix must sum to \(13\). This gives an immediate benchmark: a component with \(\lambda_k = 1\) explains exactly as much variance as one original standardised variable. This is the basis of the Kaiser criterion in Section 10.7.

The proportion of variance explained by component \(k\) is therefore \[ \text{PVE}_k = \frac{\lambda_k}{\sum_{m=1}^{p}\lambda_m} = \frac{\lambda_k}{p} \quad \text{(correlation matrix)} . \]

10.5.4 Connection with the Singular Value Decomposition

In practice, software does not form \(\mathbf{S}\) and diagonalise it; it applies the singular value decomposition to \(\mathbf{Z}\) directly: \[ \mathbf{Z} = \mathbf{U}\,\mathbf{D}\,\mathbf{V}^{\top}, \] with \(\mathbf{U}^{\top}\mathbf{U} = \mathbf{V}^{\top}\mathbf{V} = \mathbf{I}\) and \(\mathbf{D}\) diagonal with non-negative entries \(d_1 \ge \cdots \ge d_p\). Then \[ \mathbf{S} = \frac{1}{n-1}\mathbf{Z}^{\top}\mathbf{Z} = \frac{1}{n-1}\mathbf{V}\mathbf{D}^{2}\mathbf{V}^{\top}, \] so \(\mathbf{V} = \mathbf{A}\) (the loadings) and \(\lambda_k = d_k^2/(n-1)\). The SVD route is numerically more stable, because squaring the data to form \(\mathbf{S}\) squares the condition number as well. This is why prcomp() (SVD-based) is preferred in R over the older princomp() (eigen-based).

Note

prcomp() divides by \(n-1\); princomp() divides by \(n\). The eigenvectors are identical, the eigenvalues differ by the factor \(n/(n-1)\), and for \(n = 600\) the difference is immaterial. Use prcomp().

10.5.5 Doing It by Hand

Everything above can be verified in a few lines. First the eigen-decomposition of the correlation matrix:

R_mat <- cor(X)
e <- eigen(R_mat)

round(e$values, 3)          # the p eigenvalues
 [1] 4.051 2.935 1.755 1.025 0.649 0.505 0.500 0.372 0.345 0.321 0.280 0.133
[13] 0.130
sum(e$values)               # must equal p = 13
[1] 13

Now compare with prcomp():

pca <- prcomp(X, scale. = TRUE)

max(abs(e$values - pca$sdev^2))                        # eigenvalues agree
[1] 9.325873e-15
max(abs(abs(e$vectors[, 1]) - abs(pca$rotation[, 1]))) # first direction agrees
[1] 7.771561e-16

Both differences are of the order of \(10^{-14}\) — machine precision. And through the SVD:

Z <- scale(X)                 # centred and scaled
s <- svd(Z)
max(abs(s$d / sqrt(nrow(X) - 1) - pca$sdev))
[1] 0

Identical again.

Warning

Signs are arbitrary. If \(\mathbf{a}\) is an eigenvector, so is \(-\mathbf{a}\), with the same eigenvalue. Different software — and different versions of the same software — may return a component with all its signs flipped. This changes nothing about the analysis, but it does reverse the verbal interpretation (“high on PC3” becomes “low on PC3”), so always state the direction explicitly when reporting results. This is why the comparison above uses abs().

10.5.6 A Two-Dimensional Picture of the Rotation

With thirteen variables the rotation cannot be drawn. With two it can, and the picture is worth more than the algebra. Take just log_income and log_limit — a client’s declared income and the card limit the bank granted them, which are related for an obvious reason.

pair  <- X[, c("log_income", "log_limit")]
pca2  <- prcomp(pair, scale. = TRUE)

# both loadings come back negative; flip so that PC1 points up and to the right
pca2$rotation <- -pca2$rotation
pca2$x        <- -pca2$x

cor(pair)[1, 2]
[1] 0.712997
round(pca2$rotation, 4)
              PC1     PC2
log_income 0.7071 -0.7071
log_limit  0.7071  0.7071
round(pca2$sdev^2, 4)
[1] 1.713 0.287

The correlation is \(r = 0.713\). The left panel below shows the two standardised variables with the component directions drawn over the cloud; the right panel shows the same clients after the rotation, plotted against their scores.

Left: two correlated standardised variables, with the component axes drawn over the cloud. Right: the same 600 clients plotted on those axes. Nothing has moved relative to anything else — the page has been turned by 45 degrees. Both panels use a 1:1 aspect ratio, without which a rotation does not look like one.

The elongated, tilted ellipse on the left becomes an ellipse aligned with the axes on the right. Three things are worth checking rather than believing:

# 1. distances between clients are unchanged: a rotation is a rigid motion
d_before <- dist(scale(pair))
d_after  <- dist(pca2$x)
max(abs(d_before - d_after))
[1] 1.776357e-15
# 2. total variance is unchanged, only redistributed
c(before = sum(apply(scale(pair), 2, var)), after = sum(pca2$sdev^2))
before  after 
     2      2 
# 3. the new variables are uncorrelated
round(cor(pca2$x)[1, 2], 12)
[1] 0

The variables started with variance \(1\) each. After the rotation the same total of \(2\) is split \(1.713\) / \(0.287\) — that is, \(85.6\%\) of it now sits on a single axis. This is the whole of PCA in one line: the rotation does not create or destroy variance, it concentrates it.

Packages and commands. cor followed by eigen gives the eigenvalues ($values) and loadings ($vectors) by hand; svd gives the same through singular values ($d); prcomp does it in one call and is the one to use. dist checks that the rotation leaves every distance alone. The two-dimensional figure is ggplot2 plus gridExtra’s grid.arrange, and depends on coord_equal — without a 1:1 aspect ratio a rotation does not look like one.

10.6 Scores, Loadings and Explained Variance

Three objects come out of a PCA, and confusing them is the most common source of error in reading PCA output.

Object Dimension Meaning In prcomp()
Loadings \(p \times p\) Weight of each variable in each component $rotation
Scores \(n \times p\) Coordinate of each observation on each component $x
Eigenvalues \(p\) Variance carried by each component $sdev^2

10.6.1 Loadings

The loading \(a_{jk}\) is the weight of variable \(j\) in component \(k\). The vector \(\mathbf{a}_k\) has unit length, so the loadings of a component satisfy \(\sum_j a_{jk}^2 = 1\); a variable’s contribution to that component is therefore \(100\,a_{jk}^2\) per cent.

10.6.2 Scores

The score of client \(i\) on component \(k\) is \[ y_{ik} = \mathbf{a}_k^{\top}\mathbf{z}_i = \sum_{j=1}^{p} a_{jk}\, z_{ij}. \] Scores are the new data: a client who was described by \(13\) numbers is now described by \(13\) new numbers, of which we intend to keep only the first few. By construction the scores have mean zero, variance \(\lambda_k\), and zero correlation with each other.

10.6.3 Fitting the PCA

pca <- prcomp(X, scale. = TRUE)
summary(pca)
Importance of components:
                          PC1    PC2    PC3     PC4    PC5     PC6     PC7
Standard deviation     2.0126 1.7131 1.3248 1.01252 0.8054 0.71094 0.70720
Proportion of Variance 0.3116 0.2257 0.1350 0.07886 0.0499 0.03888 0.03847
Cumulative Proportion  0.3116 0.5373 0.6723 0.75119 0.8011 0.83997 0.87844
                          PC8     PC9    PC10    PC11    PC12    PC13
Standard deviation     0.6098 0.58713 0.56654 0.52899 0.36522 0.35987
Proportion of Variance 0.0286 0.02652 0.02469 0.02153 0.01026 0.00996
Cumulative Proportion  0.9071 0.93356 0.95825 0.97978 0.99004 1.00000
library(factoextra)
eig <- get_eigenvalue(pca)
round(eig, 3)
       eigenvalue variance.percent cumulative.variance.percent
Dim.1       4.051           31.158                      31.158
Dim.2       2.935           22.574                      53.732
Dim.3       1.755           13.501                      67.233
Dim.4       1.025            7.886                      75.119
Dim.5       0.649            4.990                      80.109
Dim.6       0.505            3.888                      83.997
Dim.7       0.500            3.847                      87.844
Dim.8       0.372            2.860                      90.705
Dim.9       0.345            2.652                      93.356
Dim.10      0.321            2.469                      95.825
Dim.11      0.280            2.153                      97.978
Dim.12      0.133            1.026                      99.004
Dim.13      0.130            0.996                     100.000

The eigenvalues are \(4.05\), \(2.93\), \(1.76\) and \(1.03\), explaining \(31.2\%\), \(22.6\%\), \(13.5\%\) and \(7.9\%\) of the total variance. The first three components together account for \(67.2\%\) of the variability of the thirteen original variables; adding the fourth brings this to \(75.1\%\).

Note

It is normal — and healthy — for social and behavioural data not to be compressible into two components. A first component explaining \(90\%\) of the variance usually means either that the variables are near-duplicates of one another, or that a scaling problem has gone unnoticed.

10.6.4 Reconstruction

Keeping only \(k\) components and rotating back gives an approximation to the standardised data: \[ \hat{\mathbf{Z}}_{(k)} = \mathbf{Y}_{(k)}\,\mathbf{A}_{(k)}^{\top}, \] where \(\mathbf{Y}_{(k)}\) holds the first \(k\) score columns and \(\mathbf{A}_{(k)}\) the first \(k\) loading columns. The Eckart–Young theorem (Eckart and Young 1936) states that this is the best possible rank-\(k\) approximation of \(\mathbf{Z}\) in the least-squares sense: no other set of \(k\) linear combinations reconstructs the data with smaller squared error.

Z <- scale(X)
for (k in c(1, 2, 3, 4, 6, 8)) {
  Zhat <- pca$x[, 1:k, drop = FALSE] %*% t(pca$rotation[, 1:k, drop = FALSE])
  cat(sprintf("k = %d  mean squared error = %.3f   variance retained = %.1f%%\n",
              k, mean((Z - Zhat)^2), 100 * sum(pca$sdev[1:k]^2) / ncol(X)))
}
k = 1  mean squared error = 0.687   variance retained = 31.2%
k = 2  mean squared error = 0.462   variance retained = 53.7%
k = 3  mean squared error = 0.327   variance retained = 67.2%
k = 4  mean squared error = 0.248   variance retained = 75.1%
k = 6  mean squared error = 0.160   variance retained = 84.0%
k = 8  mean squared error = 0.093   variance retained = 90.7%

The error and the retained variance are two views of the same quantity: retaining \(67.2\%\) of the variance with three components means leaving \(32.8\%\) of it — a mean squared error of \(0.327\) on standardised data — in the discarded directions.

Packages and commands. prcomp returns $rotation (loadings), $x (scores) and $sdev (square roots of the eigenvalues). summary prints the variance table; factoextra’s get_eigenvalue returns the same thing as a data frame. Reconstruction is plain matrix algebra: pca$x[, 1:k] %*% t(pca$rotation[, 1:k]).

10.7 How Many Components to Retain?

There is no test that settles this question. The usual practice is to look at several criteria and take a decision that is defensible both statistically and substantively.

10.7.1 The Scree Plot

Plot the eigenvalues against their index and look for an “elbow” — the point after which the curve flattens into a scree slope of small, roughly equal eigenvalues (Cattell 1966). Components before the elbow are taken as signal, those after as noise.

fviz_eig(pca, addlabels = TRUE, choice = "eigenvalue",
         barfill = "steelblue", barcolor = "steelblue") +
  geom_hline(yintercept = 1, linetype = "dashed", colour = "red3") +
  labs(title = "Scree plot", x = "Component", y = "Eigenvalue")

Scree plot of the bank PCA. The curve bends after the third component; the fourth is borderline.

10.7.2 Cumulative Proportion

Retain as many components as needed to reach a target share of the total variance — commonly \(70\%\) or \(80\%\). The threshold is a convention, not a result, and should be chosen with the use in mind: a visualisation may be satisfied with two components, a preprocessing step for modelling usually wants more.

cum <- data.frame(comp = 1:ncol(X), cum = cumsum(100 * pca$sdev^2 / ncol(X)))

ggplot(cum, aes(comp, cum)) +
  geom_line(colour = "steelblue", linewidth = 1) +
  geom_point(colour = "steelblue", size = 2.5) +
  geom_hline(yintercept = c(70, 80), linetype = "dashed", colour = "grey40") +
  scale_x_continuous(breaks = 1:ncol(X)) +
  labs(title = "Cumulative variance explained",
       x = "Number of components", y = "Cumulative %") +
  theme_minimal()

Cumulative percentage of variance explained. Three components pass 67%, four pass 75%.

10.7.3 The Kaiser Criterion

On the correlation matrix, retain components with \(\lambda_k > 1\) (Kaiser 1960). The logic is direct: a component explaining less than one standardised variable’s worth of variance is not a summary of anything.

sum(pca$sdev^2 > 1)
[1] 4
round(pca$sdev^2, 3)
 [1] 4.051 2.935 1.755 1.025 0.649 0.505 0.500 0.372 0.345 0.321 0.280 0.133
[13] 0.130

Four components pass. Note how narrowly: \(\lambda_4 = 1.025\). A criterion that would drop a component if the fourth eigenvalue were \(0.99\) and keep it at \(1.01\) should not be applied mechanically.

10.7.4 The Broken-Stick Model

If a stick of unit length is broken at random into \(p\) pieces, the expected length of the \(k\)-th longest piece is \[ b_k = \frac{1}{p}\sum_{m=k}^{p} \frac{1}{m}. \] Retain component \(k\) if it explains more variance than \(b_k\) — that is, more than would be expected if the total variance were divided among the \(p\) directions purely at random. This is a stricter rule than Kaiser’s, and generally a better-behaved one.

p <- ncol(X)
bstick <- sapply(1:p, function(k) sum(1 / (k:p)) / p) * 100
obs    <- 100 * pca$sdev^2 / p

data.frame(component = 1:p,
           observed  = round(obs, 2),
           broken_stick = round(bstick, 2),
           retain = obs > bstick)[1:6, ]
  component observed broken_stick retain
1         1    31.16        24.46   TRUE
2         2    22.57        16.77   TRUE
3         3    13.50        12.92   TRUE
4         4     7.89        10.36  FALSE
5         5     4.99         8.44  FALSE
6         6     3.89         6.90  FALSE

Broken-stick retains three components; Kaiser retains four. The disagreement is informative rather than annoying: it tells us the fourth component sits on the boundary between structure and noise, and we should decide it on interpretive grounds.

Tip

Summary of retention criteria

Criterion Rule Retains here Comment
Scree / elbow Bend in the eigenvalue curve 3 Visual, subjective, widely used
Cumulative % Reach 70–80% 3–4 Threshold is a convention
Kaiser \(\lambda_k > 1\) 4 Only for the correlation matrix; tends to over-retain
Broken stick \(\lambda_k/p > b_k\) 3 Stricter, generally reliable
Interpretability Can you name it? 3 The criterion that decides ties — but see Section 10.10

When criteria disagree, prefer the solution you can explain to the business. A component nobody can name is rarely worth carrying into the next stage of the analysis.

We retain three components for interpretation. Had the goal been prediction rather than explanation, keeping the fourth would have been defensible: a component that resists naming can still carry usable signal.

That is not quite the end of the question. Section 10.10 reopens it: a component that cannot be named as it stands may become nameable once the axes inside the retained subspace are rotated, and the fourth component of this portfolio is exactly such a case.

Packages and commands. factoextra’s fviz_eig(choice = "eigenvalue") draws the scree plot; cumsum and ggplot2 draw the cumulative curve. The Kaiser count is sum(pca$sdev^2 > 1), and the broken-stick values need no package at all — just sapply over the formula.

10.8 Visualising a PCA

10.8.1 The Variable Correlation Circle

Because the data are standardised, the correlation between variable \(j\) and component \(k\) is \[ r(x_j, y_k) = a_{jk}\sqrt{\lambda_k}, \] and these correlations are the coordinates used to plot variables on the correlation circle. Since a correlation cannot exceed \(1\) in absolute value, every variable lies inside a circle of radius \(1\).

fviz_pca_var(pca, axes = c(1, 2), col.var = "cos2",
             gradient.cols = c("grey70", "steelblue", "red3"),
             repel = TRUE) +
  labs(title = "Variables: PC1 vs PC2")

Variables on the plane of the first two components, coloured by quality of representation (cos2). Arrows close to the circle are well represented; small arrows are not.

How to read this plot:

  • Direction: variables pointing the same way are positively correlated; opposite directions mean negative correlation; a right angle between two well-represented arrows means near-zero correlation.
  • Length: the closer an arrow reaches the circle, the better the variable is represented by these two components. A short arrow means the variable lives mostly in other dimensions and should not be interpreted on this plane.
  • Position relative to the axes: an arrow lying along the horizontal axis defines PC1; one lying along the vertical axis defines PC2.

The monetary variables form a tight bundle along the positive horizontal axis. mobile and card_tx point up; branch, age and tenure point down. utilization, log_loan and missed have short arrows here — they belong to the third component, not to this plane.

fviz_pca_var(pca, axes = c(1, 3), col.var = "cos2",
             gradient.cols = c("grey70", "steelblue", "red3"),
             repel = TRUE) +
  labs(title = "Variables: PC1 vs PC3")

Variables on the plane of components 1 and 3. The credit-strain block, invisible in the previous figure, appears clearly.

10.8.2 Quality of Representation: cos2

The quality with which variable \(j\) is represented by component \(k\) is the squared correlation \[ \cos^2(j, k) = r(x_j, y_k)^2 = a_{jk}^{2}\lambda_k , \] which is exactly the share of variable \(j\)’s variance recovered by component \(k\). Summing over all \(p\) components gives \(1\); summing over the retained ones gives the quality of representation on the chosen plane.

var_info <- get_pca_var(pca)
round(var_info$cos2[, 1:3], 3)
            Dim.1 Dim.2 Dim.3
age         0.207 0.467 0.009
tenure      0.151 0.453 0.017
log_income  0.696 0.024 0.000
log_current 0.511 0.052 0.009
log_savings 0.718 0.000 0.001
log_invest  0.687 0.003 0.000
log_limit   0.709 0.021 0.000
utilization 0.105 0.000 0.672
log_loan    0.107 0.001 0.483
card_tx     0.057 0.687 0.016
mobile      0.001 0.641 0.016
branch      0.012 0.582 0.003
missed      0.089 0.005 0.528

log_savings has \(\cos^2 = 0.718\) on PC1: nearly three quarters of its variability is captured by the first component alone. utilization has \(\cos^2 = 0.105\) on PC1 but \(0.672\) on PC3 — reading it on the first plane would be a mistake.

10.8.3 Communalities

The sum of the squared cosines of a variable over the retained components has a name of its own: it is the communality of that variable, \[ h_j^2 = \sum_{c=1}^{k} \cos^2(j, c) = \sum_{c=1}^{k} a_{jc}^{2}\lambda_c , \] where \(k\) is the number of components kept. It is the proportion of variable \(j\)’s variance that the retained components account for, and \(1 - h_j^2\) — the part they leave behind — is called the variable’s uniqueness or specific variance.

The name records what the quantity measures: the part of a variable that it holds in common with the others, in the sense that the retained components — which are built from all the variables at once — can reproduce it. What is left over, the uniqueness, is the part of the variable that the shared structure does not reach.

Two properties follow directly from the definition:

  • Over all \(p\) components the communality is exactly \(1\), since the components form a complete orthonormal basis. Communalities are only informative once some components have been discarded.
  • The average communality across the \(p\) variables equals the proportion of total variance retained. Communalities are therefore a decomposition, variable by variable, of the single global figure reported by the scree plot.
communality <- rowSums(var_info$cos2[, 1:3])   # three components retained

round(sort(communality), 3)
log_current    log_loan      branch      tenure      missed      mobile 
      0.572       0.592       0.597       0.621       0.622       0.658 
        age  log_invest  log_income log_savings   log_limit     card_tx 
      0.683       0.690       0.719       0.719       0.730       0.760 
utilization 
      0.777 
c(mean_communality = round(mean(communality), 3),
  variance_retained = round(sum(pca$sdev[1:3]^2) / ncol(X), 3))
 mean_communality variance_retained 
            0.672             0.672 

The two figures agree: \(0.672\), the \(67.2\%\) of total variance carried by the first three components.

Communalities are the natural check on whether a chosen number of components serves every variable or only most of them. Here they range from \(0.572\) for log_current to \(0.777\) for utilization — a narrow band, so no variable is being badly misrepresented by the three-component solution. A variable with a communality of, say, \(0.15\) would be a warning: it is nearly absent from the retained subspace, and any interpretation of the solution that mentions it would be unfounded. The usual responses are to retain another component, or to accept that the variable measures something the others do not share and to handle it separately.

corrplot(var_info$cos2[, 1:3], is.corr = FALSE,
         method = "color", tl.col = "black",
         addCoef.col = "grey20", number.cex = 0.7,
         cl.pos = "b")

Quality of representation of each variable on the first three components.

10.8.4 Contributions

Where cos2 asks how well is this variable explained by the component, contribution asks the reverse: how much of this component is built from that variable. It is \[ \text{contrib}(j,k) = 100 \times a_{jk}^{2}, \] and the contributions of all variables to a given component sum to \(100\%\). The dashed reference line marks \(100/p\), the contribution a variable would have if all contributed equally.

library(gridExtra)
g1 <- fviz_contrib(pca, choice = "var", axes = 1, top = 6, fill = "steelblue") +
      labs(title = "PC1")
g2 <- fviz_contrib(pca, choice = "var", axes = 2, top = 6, fill = "steelblue") +
      labs(title = "PC2")
g3 <- fviz_contrib(pca, choice = "var", axes = 3, top = 6, fill = "steelblue") +
      labs(title = "PC3")
grid.arrange(g1, g2, g3, ncol = 3)

Six largest contributions to each of the first three components. The dashed line is the contribution a variable would have under equal weighting (100/13 = 7.7%).
Note

Keep the two summaries apart. Contributions are read down a column — they say how much each variable builds a given component, and sum to \(100\%\) over the variables. Communalities are read along a row — they say how much of a given variable the retained components recover, and sum to \(1\) over all the components.

10.8.5 Individuals

The scores place each client on the new axes. Colouring them by a variable that did not take part in the PCA is one of the most useful checks available: if an external, meaningful grouping lines up with a component, the component is measuring something real.

fviz_pca_ind(pca, axes = c(1, 2),
             geom.ind = "point", pointshape = 19, alpha.ind = 0.6,
             col.ind = bank$segment,
             palette = c("grey55", "steelblue", "red3"),
             addEllipses = TRUE, ellipse.type = "norm", ellipse.alpha = 0.08,
             legend.title = "Segment") +
  labs(title = "Clients: PC1 vs PC2")

Clients on the first plane, coloured by commercial segment. The segments separate along PC1, which the PCA discovered without ever seeing them.

The commercial segment was not among the thirteen variables entered into the PCA, yet the three groups order themselves neatly along PC1:

scores <- as.data.frame(pca$x[, 1:3])
agg <- aggregate(scores, by = list(segment = bank$segment), FUN = mean)
agg[, -1] <- round(agg[, -1], 3)
agg
   segment    PC1    PC2    PC3
1     Mass -1.182 -0.108  0.033
2 Affluent  1.280  0.092  0.002
3  Private  3.248  0.376 -0.206

Mean PC1 rises from \(-1.18\) (Mass) through \(1.28\) (Affluent) to \(3.25\) (Private), while mean PC2 and PC3 barely move. This is strong external evidence that PC1 is an affluence axis.

10.8.6 The Biplot

A biplot superimposes individuals and variables on the same plane. It is the most informative single figure of a PCA and also the easiest to over-read: the two sets of points are on different scales, and only their relative geometry is meaningful.

fviz_pca_biplot(pca, axes = c(1, 2),
                geom.ind = "point", pointshape = 19,
                alpha.ind = 0.35, col.ind = bank$segment,
                palette = c("grey55", "steelblue", "red3"),
                col.var = "black", repel = TRUE,
                legend.title = "Segment") +
  labs(title = "Biplot: bank client portfolio")

Biplot of the bank portfolio. Clients are positioned by their scores, variables by their loadings; a client lying far along an arrow has high values of that variable.
Note

Reading a biplot:

  • A client positioned in the direction of an arrow has an above-average value of that variable.
  • Clients far from the origin are atypical on the plane shown; clients near the origin are either average, or badly represented in these two dimensions. The two cases are distinguished by looking at the individual cos2, not at the plot.
  • The angle between two arrows approximates the correlation between the corresponding variables — but only when both arrows are long.

Packages and commands. All from factoextra: fviz_pca_var (correlation circle), fviz_contrib (contributions), fviz_pca_ind (individuals, with col.ind and addEllipses) and fviz_pca_biplot (biplot). get_pca_var returns the numbers behind those figures in $coord, $cos2 and $contrib. corrplot(is.corr = FALSE) displays a cos2 matrix, and aggregate summarises scores by an external grouping.

10.9 Interpreting the Components

A principal component has no meaning until someone gives it one. Interpretation proceeds by reading the loadings of a component and asking what the highly weighted variables have in common. Loadings below roughly \(0.3\) in absolute value are usually ignored as noise.

round(pca$rotation[, 1:4], 3)
               PC1    PC2    PC3    PC4
age          0.226 -0.399 -0.071 -0.479
tenure       0.193 -0.393 -0.098 -0.537
log_income   0.414  0.090 -0.005  0.191
log_current  0.355  0.133 -0.072  0.117
log_savings  0.421 -0.002 -0.028  0.020
log_invest   0.412  0.030 -0.009  0.076
log_limit    0.418  0.084 -0.012  0.157
utilization -0.161 -0.005 -0.619  0.037
log_loan     0.163  0.020 -0.525  0.178
card_tx      0.119  0.484 -0.095 -0.371
mobile      -0.018  0.467 -0.096 -0.459
branch       0.055 -0.445 -0.041  0.129
missed      -0.148 -0.043 -0.548  0.058

PC1 — Affluence (\(31.2\%\)). Large positive loadings on log_savings (\(0.421\)), log_limit (\(0.418\)), log_income (\(0.414\)), log_invest (\(0.412\)) and log_current (\(0.355\)); everything else is small. This is a size factor: a client high on PC1 has more of everything monetary. Its negative loadings on utilization (\(-0.161\)) and missed (\(-0.148\)) are weak but sensible — wealthier clients use less of their limit and miss fewer payments.

PC2 — Digital engagement versus branch and life stage (\(22.6\%\)). Positive on card_tx (\(0.484\)) and mobile (\(0.467\)); negative on branch (\(-0.445\)), age (\(-0.399\)) and tenure (\(-0.393\)). This is a contrast factor, and it says something the bank would care about: younger, shorter-tenure clients transact digitally, older and long-standing clients come to the branch. Note that PC2 is essentially uncorrelated with wealth — the digital divide in this portfolio runs along age, not income.

PC3 — Credit strain, reversed (\(13.5\%\)). Negative on utilization (\(-0.619\)), missed (\(-0.548\)) and log_loan (\(-0.525\)). A low score on PC3 means high utilisation, missed payments and large outstanding debt. Since it is easier to talk about a factor whose high values mean “more of the thing”, we can flip its sign:

pca_flip <- pca
pca_flip$rotation[, 3] <- -pca$rotation[, 3]
pca_flip$x[, 3]        <- -pca$x[, 3]

round(pca_flip$rotation[, 3], 3)
        age      tenure  log_income log_current log_savings  log_invest 
      0.071       0.098       0.005       0.072       0.028       0.009 
  log_limit utilization    log_loan     card_tx      mobile      branch 
      0.012       0.619       0.525       0.095       0.096       0.041 
     missed 
      0.548 

After flipping, a high PC3 is a client under credit strain. The eigenvalue, the variance explained and every distance between clients are unchanged; only the label changes.

Warning

If you flip a component for readability, flip both the loadings and the scores, and do it once, at the start. Flipping only one of the two silently breaks the relationship \(\mathbf{Y} = \mathbf{Z}\mathbf{A}\) and every downstream result with it.

PC4 — a warning (\(7.9\%\)). The Kaiser criterion retained it (\(\lambda_4 = 1.025\)), but its loadings are a mixture: negative on tenure (\(-0.537\)), age (\(-0.479\)), mobile (\(-0.459\)) and card_tx (\(-0.371\)), with nothing else prominent. It appears to be a residual contrast between age and digital use after PC2 has taken the part of that contrast the two share. That is a statement about the geometry of the solution, not about clients. It is retainable for prediction and not interpretable as a construct — a distinction worth keeping.

Component Variance Name High score means
PC1 31.2% Affluence High income, savings, investments and credit limit
PC2 22.6% Digital engagement Young, short tenure, transacts by card and app, rarely visits a branch
PC3 (flipped) 13.5% Credit strain High credit utilisation, missed payments, large debt
PC4 7.9% — No stable substantive reading
Tip

The interpretation above recovers, almost exactly, the four latent factors used to generate the data (W affluence, A life stage, D digital propensity, R credit risk) — although A and D were deliberately entangled and PCA, which is restricted to orthogonal directions, splits them across PC2 and PC4 rather than isolating each.

PCA recovers latent structure only up to a rotation. That is a real limitation, but it is also an opening: if the retained subspace is right and only the axes inside it are awkwardly placed, the axes can be turned. Section 10.10 does exactly that, and recovers all four factors — A included.

Packages and commands. No new tools: interpretation is reading pca$rotation, and flipping a component for readability is a minus sign applied to both $rotation[, k] and $x[, k].

10.10 Rotating the Components for Interpretation

Three of our four components had a clear reading; the fourth did not. That is a common outcome, and there is a standard response to it: keep the same set of retained components but turn the axes inside the subspace they span until the loadings fall into a pattern that is easier to name.

This is a different operation from the rotation of Section 10.5.1. There, rotating the axes was the PCA — the whole \(p\)-dimensional basis was turned so that the first axis caught the most variance. Here we take the \(k\) retained components as given and rotate only within them, deliberately giving up the maximum-variance ordering in exchange for readability.

10.10.1 Simple Structure

The target is what Thurstone (1947) called simple structure: a loadings matrix in which

  • each variable loads strongly on one component and near zero on the others,
  • each component has a handful of large loadings and many small ones,
  • few variables load on several components at once.

A matrix like that almost reads itself. The unrotated one rarely looks like that, because the first component is built to absorb as much variance as it can from every variable, which guarantees it has many moderate loadings.

Write \(\mathbf{L} = \mathbf{A}_k \boldsymbol{\Lambda}_k^{1/2}\) for the \(p \times k\) matrix of loadings on the retained components — the variable-component correlations of Section 10.8. A rotation replaces it by \[ \mathbf{L}^{*} = \mathbf{L}\,\mathbf{T}, \] with \(\mathbf{T}\) a \(k \times k\) orthogonal matrix chosen to make \(\mathbf{L}^{*}\) as close to simple structure as some criterion can measure. Throughout this section \(k\) is the number of retained components (the k of the code) and components are indexed by \(c\).

Warning

“Loadings” is used for two different matrices. Earlier in the chapter it meant pca$rotation, the eigenvectors, whose columns have unit length. Here it means \(\mathbf{A}_k\boldsymbol{\Lambda}_k^{1/2}\), whose entries are correlations and whose columns have squared sum \(\lambda_c\). Both are standard and both are called loadings; rotation is defined on the second, because simple structure is a statement about correlations. When you compare a printed table with pca$rotation, check which one you are holding.

Note

What a rotation preserves

  • the subspace spanned by the retained components, so the fitted values and the reconstruction error are unchanged;
  • every variable’s communality \(h_j^2\), and therefore the total variance retained.

What it gives up

  • the ordering: rotated components are no longer sorted by variance, and none of them is the maximum-variance direction any more;
  • the eigenvector property: they are no longer principal components, which is why they are conventionally written RC1, RC2, … rather than PC1, PC2, …;
  • uniqueness of the story: \(k\) must be chosen before rotating, and a different \(k\) gives a different rotated solution — not a nested one.

10.10.2 The Orthomax Family

Most orthogonal rotations maximise one criterion with one parameter changed: \[ Q(\mathbf{L}^{*}) \;=\; \sum_{c=1}^{k}\sum_{j=1}^{p} l_{jc}^{*4} \;-\; \frac{\gamma}{p}\sum_{c=1}^{k}\left(\sum_{j=1}^{p} l_{jc}^{*2}\right)^{2}. \] Different \(\gamma\) give the named methods:

Method \(\gamma\) Simplifies Behaviour
Quartimax \(0\) rows (variables) Each variable loads on as few components as possible. Tends to leave one large general component.
Varimax \(1\) columns (components) Each component gets a few large loadings. The default choice, and the one that most reliably produces nameable components.
Equamax \(k/2\) both A compromise; spreads variance more evenly across components. Can be unstable.
Parsimax \(p(k-1)/(p+k-2)\) both A further compromise, weighted by the problem’s dimensions.

Varimax (Kaiser 1958) is the one to reach for unless you have a reason not to.

GPArotation parameterises the same family through the Crawford-Ferguson constant \(\kappa = \gamma/p\), which is why equamax appears below as kappa = k / (2 * ncol(X)) rather than as \(\gamma = k/2\). The two are the same rotation.

10.10.3 Varimax on the Bank Portfolio

We rotate four components rather than three. The reason is the one Section 10.7 left unresolved: Kaiser retained four, broken-stick three, and we kept three because the fourth could not be named. Rotation changes that calculation, as we are about to see.

k <- 4
L <- pca$rotation[, 1:k] %*% diag(pca$sdev[1:k])
dimnames(L) <- list(colnames(X), paste0("PC", 1:k))

round(L, 3)
               PC1    PC2    PC3    PC4
age          0.454 -0.683 -0.094 -0.485
tenure       0.389 -0.673 -0.130 -0.543
log_income   0.834  0.153 -0.006  0.193
log_current  0.715  0.228 -0.095  0.118
log_savings  0.847 -0.004 -0.038  0.020
log_invest   0.829  0.052 -0.012  0.077
log_limit    0.842  0.144 -0.016  0.159
utilization -0.323 -0.009 -0.820  0.037
log_loan     0.328  0.034 -0.695  0.181
card_tx      0.239  0.829 -0.125 -0.375
mobile      -0.036  0.800 -0.128 -0.465
branch       0.112 -0.763 -0.055  0.131
missed      -0.298 -0.074 -0.727  0.059
vm <- varimax(L)
RC <- vm$loadings[, ]
colnames(RC) <- paste0("RC", 1:k)

round(RC, 3)
               RC1    RC2    RC3    RC4
age          0.196 -0.242  0.016 -0.906
tenure       0.123 -0.198 -0.020 -0.928
log_income   0.866  0.028  0.079 -0.019
log_current  0.753  0.138 -0.012 -0.008
log_savings  0.809  0.007  0.070 -0.247
log_invest   0.813  0.015  0.086 -0.163
log_limit    0.863  0.041  0.075 -0.054
utilization -0.222  0.047 -0.852  0.015
log_loan     0.427 -0.003 -0.664 -0.033
card_tx      0.270  0.903 -0.024  0.107
mobile      -0.018  0.927 -0.051  0.107
branch       0.022 -0.680 -0.073 -0.382
missed      -0.211 -0.027 -0.762 -0.002

Loadings on four retained components, before and after a varimax rotation. The rotated panel is what simple structure looks like: one strong cell per row.

The rotated solution is far easier to read, and the change is not cosmetic:

  • RC1 — affluence. Unchanged in substance: income, savings, investments, limit, current balance, all above \(0.75\). Age has dropped out of it (\(0.45 \to 0.20\)).
  • RC2 — digital engagement. Now almost pure: card_tx \(0.90\), mobile \(0.93\), branch \(-0.68\). Age and tenure, which contributed \(-0.68\) and \(-0.67\) to the unrotated PC2, have fallen to \(-0.24\) and \(-0.20\).
  • RC3 — credit strain. Essentially untouched.
  • RC4 — life stage. The component that could not be named is now the cleanest of the four: age \(-0.91\), tenure \(-0.93\), and nothing else above \(0.4\). A high RC4 is a young, recently acquired client.

Rotation has separated the two constructs that the unrotated solution smeared across PC2 and PC4. Now check what it cost:

rbind(unrotated = round(100 * colSums(L^2)  / ncol(X), 1),
      varimax   = round(100 * colSums(RC^2) / ncol(X), 1))
           PC1  PC2  PC3  PC4
unrotated 31.2 22.6 13.5  7.9
varimax   29.1 17.4 13.7 15.0
c(total_unrotated = round(100 * sum(L^2)  / ncol(X), 1),
  total_varimax   = round(100 * sum(RC^2) / ncol(X), 1))
total_unrotated   total_varimax 
           75.1            75.1 
max(abs(rowSums(RC^2) - rowSums(L^2)))     # communalities
[1] 6.661338e-16

The total retained variance is \(75.1\%\) either way and no communality moves by more than \(10^{-15}\). What changed is the split: the first component gave up \(31.2\% \to 29.1\%\) and the second \(22.6\% \to 17.4\%\), while the fourth rose from \(7.9\%\) to \(15.0\%\) — and RC4 now explains more than RC3, so the components are genuinely no longer in order.

Tip

This is the honest argument for keeping the fourth component. Unrotated it was a \(7.9\%\) residual nobody could name; rotated it is a \(15.0\%\) life-stage factor with two loadings above \(0.9\). The number of components to retain and the decision to rotate are not independent questions.

10.10.4 Comparing Methods

library(GPArotation)

crit <- function(M) sum(apply(M^2, 2, function(z) mean(z^2) - mean(z)^2))
cplx <- function(M) mean(rowSums(M^2)^2 / rowSums(M^4))   # Hoffman complexity

sols <- list(Unrotated = L,
             Varimax   = varimax(L, normalize = FALSE)$loadings[, ],
             Quartimax = quartimax(L)$loadings[, ],
             Equamax   = cfT(L, kappa = k / (2 * ncol(X)))$loadings[, ])

data.frame(
  criterion  = round(sapply(sols, crit), 4),
  complexity = round(sapply(sols, cplx), 3),
  var_first  = round(sapply(sols, function(M) 100 * sum(M[, 1]^2) / ncol(X)), 1),
  var_fourth = round(sapply(sols, function(M) 100 * sum(M[, 4]^2) / ncol(X)), 1),
  total      = round(sapply(sols, function(M) 100 * sum(M^2) / ncol(X)), 1))
          criterion complexity var_first var_fourth total
Unrotated    0.2245      1.511      31.2        7.9  75.1
Varimax      0.3434      1.215      28.5       15.8  75.1
Quartimax    0.3427      1.207      29.1       15.4  75.1
Equamax      0.3425      1.229      27.9       16.3  75.1

The varimax row here is the unnormalised variant, so that all four solutions are scored on the same criterion; the rotation used above is the normalised default, which is why its first component carries \(29.1\%\) rather than \(28.5\%\).

Complexity is the average number of components a variable loads on: it falls from \(1.51\) unrotated to about \(1.21\) under any of the three rotations. On this portfolio the three orthogonal methods give practically the same answer — quartimax leaves slightly more variance on the first component, equamax slightly less, exactly as their definitions predict. When the three disagree materially, that is a sign the solution is not stable enough to name.

Warning

stats::varimax() applies Kaiser normalisation by default (normalize = TRUE): rows are scaled to unit length before rotating and rescaled afterwards, so that variables with small communalities still get a say. GPArotation does not normalise by default. The two therefore optimise slightly different things — on this data the raw criterion reaches \(0.3434\) without normalisation and \(0.3420\) with it. Neither is wrong, but a comparison across packages must hold the setting fixed.

10.10.5 Oblique Rotations

Orthogonal rotation keeps the components uncorrelated. That is a convenience, not a fact about the world: real constructs are often related, and forcing right angles can prevent simple structure from being reached at all. Oblique rotations — promax (Hendrickson and White 1964), oblimin, quartimin — drop the constraint.

pm  <- promax(L)
Phi <- solve(t(pm$rotmat) %*% pm$rotmat)      # component correlation matrix
dimnames(Phi) <- list(paste0("RC", 1:k), paste0("RC", 1:k))

round(pm$loadings[, ], 3)
               PC1    PC2    PC3    PC4
age          0.046  0.013  0.020 -0.951
tenure      -0.039  0.076 -0.013 -0.997
log_income   0.889 -0.039  0.057  0.094
log_current  0.762  0.088 -0.030  0.066
log_savings  0.785  0.018  0.052 -0.170
log_invest   0.805 -0.001  0.067 -0.076
log_limit    0.877 -0.012  0.054  0.051
utilization -0.199  0.064 -0.848 -0.006
log_loan     0.465 -0.030 -0.676  0.039
card_tx      0.176  0.932 -0.026 -0.055
mobile      -0.127  0.984 -0.044 -0.102
branch       0.043 -0.623 -0.075 -0.268
missed      -0.185 -0.012 -0.758 -0.008
round(Phi, 3)
       RC1   RC2   RC3    RC4
RC1  1.000 0.116 0.059 -0.250
RC2  0.116 1.000 0.011  0.433
RC3  0.059 0.011 1.000  0.019
RC4 -0.250 0.433 0.019  1.000

Promax sharpens the loadings further — tenure reaches \(-0.997\) on RC4 and mobile \(0.984\) on RC2 — and it reports something an orthogonal rotation cannot: the components are correlated, and one pair substantially so. RC2 (digital) and RC4 (life stage) correlate at \(0.43\).

That number is not noise. The data were generated with D depending on A through the term \(-0.45\,\mathrm{A}\), which implies a correlation of about \(-0.49\) between digital propensity and life stage in the population, and \(-0.45\) in these 600 clients; RC4 loads negatively on age, making it a “youth” axis, which flips the sign. The oblique rotation has recovered a feature of the data-generating process that the orthogonal solutions, PCA and varimax alike, were constrained from showing.

Note

With an oblique rotation, read the pattern matrix (unique contribution of each component, printed above) for interpretation and the structure matrix (plain variable–component correlations, \(\mathbf{L}^{*}\boldsymbol{\Phi}\)) for a description of association. They differ precisely because the components overlap, and for the same reason the per-component variance shares no longer add up to the total — shared variance would be counted twice.

10.10.6 Rotated Scores

The scores rotate with the loadings, and they are what you carry into the next analysis. Since the matrix we rotated was \(\mathbf{L} = \mathbf{A}_k\boldsymbol{\Lambda}_k^{1/2}\) rather than the eigenvectors themselves, its partner is the standardised score matrix, and the rotation multiplies it on the right: \[ \mathbf{Y}^{*} = \mathbf{Z}\,\mathbf{A}_k\,\boldsymbol{\Lambda}_k^{-1/2}, \qquad \mathbf{Y}^{*}_{\text{rot}} = \mathbf{Y}^{*}\mathbf{T}. \] For an oblique rotation the same holds with \((\mathbf{T}^{\top})^{-1}\) in place of \(\mathbf{T}\), and the columns come out correlated — their correlation matrix is exactly \(\boldsymbol{\Phi}\).

Ystar    <- scale(X) %*% pca$rotation[, 1:k] %*% diag(1 / pca$sdev[1:k])
RCscores <- Ystar %*% vm$rotmat              # varimax
PMscores <- Ystar %*% solve(t(pm$rotmat))    # promax
colnames(RCscores) <- colnames(PMscores) <- paste0("RC", 1:k)

round(apply(RCscores, 2, sd), 3)                          # unit variance
RC1 RC2 RC3 RC4 
  1   1   1   1 
signif(max(abs(cor(RCscores)[upper.tri(diag(k))])), 3)    # still uncorrelated
[1] 8.31e-16
signif(max(abs(cor(PMscores) - Phi)), 3)                  # promax: exactly Phi
[1] 6.66e-16

Each column now reads as a score on a named construct: RC4 correlates \(-0.906\) with age, so it is a youth-and-recency index on a standard-deviation scale. The promax scores carry the \(0.43\) between digital engagement and life stage, so anything downstream that assumes uncorrelated inputs has to allow for it.

Warning

Two score conventions. prcomp()$x returns scores with variance \(\lambda_c\); the ones above have variance \(1\) in every column, because they are the partners of correlation-scaled loadings. Neither is wrong, but they are not interchangeable — \(k\)-means on the first weights the components by \(\lambda\), on the second equally. Say which you used.

10.10.7 When to Rotate, and When Not To

Caution

Rotate when the goal is to name and communicate constructs — a segmentation brief, a questionnaire’s underlying dimensions, a set of risk factors for a tariff discussion.

Do not rotate when the goal depends on the properties rotation destroys:

  • compression, where you want the fewest axes carrying the most variance;
  • a single index — a first component used as a wealth or vulnerability score — since after rotation no component is the best single summary;
  • any downstream use of the ordering, such as taking “the first two components”.

And whichever you do, say so. “The first four components explain 75%” and “the first four varimax-rotated components explain 75%, split 29/17/14/15 rather than 31/23/14/8” describe different objects.

Packages and commands. stats provides varimax (with normalize controlling Kaiser normalisation) and promax; both return $loadings and $rotmat. GPArotation supplies the rest of the family — quartimax, and cfT(kappa = ) for equamax — as well as the oblique oblimin and quartimin. The component correlation matrix is solve(t(rotmat) %*% rotmat), and rotated scores need solve and t as shown above.

10.11 Practical Issues

10.11.1 Outliers and Heavy Tails

PCA maximises variance, and variance is not a robust quantity. A single record far from the rest can rotate a component towards itself, producing a “component” that describes one client.

Suppose data-entry errors inflate the savings balance and the investment holdings of client C0001 to €50 million and €30 million; the true values are €22 020 and €77 560. We introduce the same error twice — once in the untransformed variables, once in the log-transformed ones — and compare.

Xr <- bank[, num_vars]                 # untransformed
Xr_bad <- Xr
Xr_bad$balance_savings[1] <- 50e6      # true value 22 020
Xr_bad$investments[1]     <- 30e6      # true value 77 560

X_bad <- X                             # log-transformed
X_bad$log_savings[1] <- log(50e6)
X_bad$log_invest[1]  <- log(30e6)

pca_r  <- prcomp(Xr,     scale. = TRUE)
pca_rb <- prcomp(Xr_bad, scale. = TRUE)
pca_lb <- prcomp(X_bad,  scale. = TRUE)

pv <- function(q) round(100 * q$sdev[1:3]^2 / ncol(X), 1)
rbind(untransformed_clean     = pv(pca_r),
      untransformed_corrupted = pv(pca_rb),
      log_clean               = pv(pca),
      log_corrupted           = pv(pca_lb))
                        [,1] [,2] [,3]
untransformed_clean     26.8 22.7 13.2
untransformed_corrupted 22.8 19.9 15.3
log_clean               31.2 22.6 13.5
log_corrupted           30.8 22.6 13.5

On the log scale the corrupted record barely registers: the variance explained by the first three components moves from \(31.2\%\), \(22.6\%\), \(13.5\%\) to \(30.8\%\), \(22.6\%\), \(13.5\%\). On the untransformed scale the solution falls apart. Compare the first component before and after:

cbind(clean = round(pca_r$rotation[, 1], 3),
      corrupted = round(pca_rb$rotation[, 1], 3))
                 clean corrupted
age              0.206     0.432
tenure           0.177     0.421
income           0.449    -0.012
balance_current  0.358    -0.096
balance_savings  0.369    -0.061
investments      0.429    -0.060
credit_limit     0.441    -0.001
utilization     -0.140    -0.029
loan_balance     0.144     0.010
card_tx          0.151    -0.455
mobile_logins    0.013    -0.462
branch_visits    0.024     0.446
missed_payments -0.133     0.012
cor(pca_r$rotation[, 1], pca_rb$rotation[, 1])
[1] -0.01780224

The correlation between the two versions of PC1 is \(-0.02\): the affluence factor has not been perturbed, it has been displaced entirely, and the first component of the corrupted analysis is now the age-versus-digital contrast. The affluence structure did not merely move down the ordering — the corrupted variables, having almost all their standardised variance concentrated in one point, are now nearly constant across the other \(599\) clients and no longer correlate with anything.

Where did the corrupted client go? Into a component of its own:

round(pca_rb$x[1, ] / apply(pca_rb$x, 2, sd), 1)   # its scores, in standard deviations
 PC1  PC2  PC3  PC4  PC5  PC6  PC7  PC8  PC9 PC10 PC11 PC12 PC13 
-2.6 -1.0 24.3  0.6 -0.1  0.2  0.5 -0.2 -0.2 -0.2  0.0  0.0 -0.1 
round(pca_rb$rotation[, 3], 3)                     # the component it created
            age          tenure          income balance_current balance_savings 
          0.031           0.032           0.000          -0.056           0.703 
    investments    credit_limit     utilization    loan_balance         card_tx 
          0.702          -0.022           0.033          -0.017          -0.048 
  mobile_logins   branch_visits missed_payments 
         -0.030           0.041           0.029 

The third component of the corrupted analysis takes \(15.3\%\) of the total variance, loads \(0.70\) on each of the two corrupted variables and essentially zero on everything else, and the offending client sits \(24\) standard deviations along it. That component is not a description of the portfolio; it is a description of one typing mistake.

par(mfrow = c(1, 2))
plot(pca_r$x[, 1], pca_r$x[, 2], pch = 19, col = "grey60", cex = 0.6,
     xlab = "PC1", ylab = "PC2", main = "Clean data: PC1 vs PC2")
plot(pca_rb$x[, 1], pca_rb$x[, 3], pch = 19, col = "grey60", cex = 0.6,
     xlab = "PC1", ylab = "PC3", main = "Corrupted data: PC1 vs PC3")
points(pca_rb$x[1, 1], pca_rb$x[1, 3], pch = 19, col = "red3", cex = 1.6)

Left: the clean untransformed analysis, where PC1 is affluence. Right: the same data with two corrupted cells, on the plane where the damage is visible. The third component of the corrupted analysis exists only to hold one client (red).
par(mfrow = c(1, 1))

All of this followed from an error in two cells of a table of \(7\,800\) values.

Tip

Notice what the log transform bought us. It did not remove the outlier — client C0001 still has an absurd balance — but it compressed errors of a factor of about \(2300\) (savings) and \(390\) (investments) on the euro scale to additive shifts of \(\log(2271) \approx 7.7\) and \(\log(387) \approx 6.0\) on the log scale, which are large but not catastrophic. Transformation is a cheap and effective first defence against exactly this failure mode. It is not a substitute for looking at the data.

Practical defences:

  • Screen for outliers before the PCA using the methods of the Outlier Detection chapter.
  • Distinguish errors from genuine extremes. A savings balance of €50 million is an error; a private-banking client with €1.2 million is a real client and should probably stay, possibly after a log transform.
  • Use the PCA itself as a diagnostic: an observation with a large score on a late, low-variance component, or a large reconstruction error, is atypical in a way the main structure does not explain.
  • Consider a robust PCA, based on a robust covariance estimator such as the MCD, when contamination is suspected and cannot be cleaned.
# reconstruction error per client using three components
Zhat <- pca$x[, 1:3] %*% t(pca$rotation[, 1:3])
err  <- rowSums((scale(X) - Zhat)^2)

head(bank[order(err, decreasing = TRUE), c("client_id", "segment", "utilization",
                                           "missed_payments", "mobile_logins")], 5)
    client_id  segment utilization missed_payments mobile_logins
272     C0272     Mass        76.5               3           184
308     C0308     Mass        86.3              12            17
231     C0231 Affluent         0.0               1             4
127     C0127     Mass        52.7               2             2
535     C0535     Mass        30.1               0            76

These are the clients least well described by the three-component summary — the natural shortlist for an anomaly review.

10.11.2 Missing Values

prcomp() has no mechanism for missing data: it removes any row with an NA, which can be a substantial part of the sample when many variables are involved. The options are those of the Missing Values chapter — deletion, single imputation, or multiple imputation — with one addition specific to PCA: iterative PCA imputation, which fills the missing cells with the values predicted by a low-rank reconstruction and iterates until convergence (implemented in the missMDA package).

X_na <- X
set.seed(1)
X_na[sample(nrow(X), 60), "log_income"] <- NA

nrow(na.omit(X_na))          # rows surviving listwise deletion
[1] 540

Losing \(10\%\) of the sample to missingness in one variable is already noticeable; with missingness spread over several variables the loss compounds quickly.

10.11.3 Categorical Variables

PCA is defined for numeric variables. segment, region and churn cannot enter the analysis directly. Three options:

  • Leave them out and use them for illustration, as we did above — colouring the individuals plot by segment. This is the safest choice and often the most informative.
  • Dummy-code them. Workable for binary variables, but a dummy has variance \(\pi(1-\pi)\), which is small for rare categories; standardising then inflates rare categories and lets them drive components.
  • Use a method designed for the data type: Multiple Correspondence Analysis (MCA) for a set of categorical variables, or Factor Analysis of Mixed Data (FAMD) when numeric and categorical variables must be analysed together. Both are available in the FactoMineR package.

10.11.4 Multicollinearity and Near-Constant Variables

PCA tolerates perfect collinearity — the corresponding eigenvalue is simply zero — but a variable with zero variance breaks the standardisation, since dividing by \(s_j = 0\) is undefined. Check before scaling:

which(apply(X, 2, sd) == 0)   # integer(0): no constant columns
named integer(0)

Also worth avoiding is including a variable twice in different guises (an amount and its logarithm, a total and its components). PCA will faithfully report that they are the same variable, and they will jointly dominate a component for no substantive reason.

10.11.5 Sample Size

PCA estimates \(p(p+1)/2\) covariances from \(n\) observations. With \(p = 13\) that is \(91\) quantities; \(n = 600\) is comfortable. Rules of thumb suggest at least \(5\)–\(10\) observations per variable, and more when the correlations are weak. With \(n < p\) the correlation matrix is singular and at most \(n-1\) eigenvalues are non-zero — PCA still runs, but the later components are pure artefact.

Packages and commands. na.omit shows the cost of listwise deletion, apply(X, 2, sd) finds constant columns, and the per-client reconstruction error is rowSums((scale(X) - Zhat)^2). Named but not used here: missMDA for iterative PCA imputation, FactoMineR for MCA and FAMD on categorical and mixed data, and a robust covariance estimator such as the MCD for robust PCA.

10.12 Common Pitfalls

Caution
  1. Forgetting to scale when variables have different units — the analysis then reflects the units.
  2. Interpreting a component before checking cos2 — a variable near the origin of the correlation circle is not “average”, it is absent from that plane.
  3. Reading the sign of a component as meaningful — the sign is arbitrary; only the contrast is real.
  4. Assuming the first component is the important one — it is only the largest. PCA orders components by variance in the variables, with no reference to any outcome you might later want to model, so importance depends on the question being asked.
  5. Applying PCA to a set of uncorrelated variables — the eigenvalues will all be near \(1\) and nothing is gained. Look at the correlation matrix first.
  6. Computing the PCA on the full data set before splitting into train and test sets — the test data then influence the transformation, and performance estimates are optimistic. Fit prcomp() on the training set and apply it with predict().
  7. Treating principal components as causes — they are descriptive summaries of covariation, and nothing about the method licenses a causal reading.
  8. Reporting components nobody can name without saying so — an uninterpretable component is a legitimate result, but it should be labelled as such.
# The correct way to use PCA inside a predictive workflow
set.seed(42)
idx        <- sample(nrow(X), 0.7 * nrow(X))
pca_train  <- prcomp(X[idx, ], scale. = TRUE)          # fitted on training data only
scores_tr  <- pca_train$x[, 1:4]
scores_te  <- predict(pca_train, newdata = X[-idx, ])[, 1:4]

Packages and commands. The train/test pattern is the only code here: fit prcomp on the training rows, then apply the same transformation to the test rows with predict(pca_train, newdata = ). Never refit.

10.13 Beyond PCA: Other Routes to Fewer Dimensions

PCA makes three commitments: the new variables are linear combinations of the old ones, the criterion is global variance, and the whole \(n \times p\) matrix has to be decomposed at once. Each one is relaxed by a family of methods, and on large data the third becomes the binding constraint.

Multidimensional scaling (MDS) starts from a matrix of distances rather than from the variables themselves. Classical MDS — also called principal coordinates analysis — applied to Euclidean distances is not a rival to PCA; it is PCA:

mds <- cmdscale(dist(scale(X)), k = 2)

signif(max(abs(abs(mds) - abs(pca$x[, 1:2]))), 3)   # identical, up to the sign of each axis
[1] 4.99e-14

The interesting case is the one PCA cannot reach. Feed MDS a dissimilarity that is not Euclidean — Gower’s coefficient for mixed numeric and categorical data, a correlation distance, an edit distance between strings — and you get a map of the clients even though no variable matrix exists. Non-metric MDS goes further still, preserving only the rank order of the dissimilarities, which is the right choice when the dissimilarities are ordinal.

Nonlinear methods drop the linearity. Kernel PCA runs an ordinary PCA in a feature space reached through a kernel. t-SNE and UMAP take a different view again: rather than preserving variance or global distance, they try to keep each point’s near neighbours near, which is what makes them so effective at showing clusters in high-dimensional data. UMAP is the faster of the two, keeps more of the global structure, and — unlike t-SNE — can place new points on an existing map.

On large data, the pressure is usually computational rather than conceptual, and it comes in two forms. With many rows and columns, a truncated or randomised SVD returns the leading \(k\) components without ever forming the full decomposition, and works on sparse matrices that would not fit in memory as a dense one. With very many columns, random projection is cheaper still: project onto random directions and, by the Johnson–Lindenstrauss lemma, pairwise distances survive to within a controllable error. Autoencoders — neural networks trained to reconstruct their own input through a narrow layer — are the nonlinear version of the same idea, and scale to data far beyond what prcomp will hold.

NIPALS — nonlinear iterative partial least squares (Wold 1966) — is the oldest algorithm that computes only the leading components. It never forms \(\mathbf{S}\). Each component is found by alternating two least-squares regressions on the residual matrix \(\mathbf{E}\), which starts as \(\mathbf{Z}\). Given a guess at the scores \(\mathbf{y}\), regress each column of \(\mathbf{E}\) on \(\mathbf{y}\) and normalise the coefficients to obtain the loadings \(\mathbf{a}\); then regress each row of \(\mathbf{E}\) on \(\mathbf{a}\) to update the scores, \(\mathbf{y} = \mathbf{E}\mathbf{a}\). Repeat until \(\mathbf{y}\) settles, then deflate, \(\mathbf{E} \leftarrow \mathbf{E} - \mathbf{y}\mathbf{a}^{\top}\), and start on the next component. Combining the two regressions gives \(\mathbf{a} \propto \mathbf{E}^{\top}\mathbf{E}\,\mathbf{a}\), so this is the power method, converging to the eigenvector of the largest remaining eigenvalue at a rate set by the ratio of the next eigenvalue to that one. Because each regression is a sum over cells, it can simply skip missing ones, and NIPALS gives a PCA of incomplete data without deleting any client — provided the cells are missing at random.

Method Idea In R
Classical MDS Coordinates from a distance matrix; Euclidean case = PCA cmdscale (stats)
Non-metric MDS Preserve the rank order of dissimilarities isoMDS (MASS), metaMDS (vegan), smacof
Kernel PCA PCA in a feature space defined by a kernel kpca (kernlab)
t-SNE Preserve local neighbourhoods, for visualisation Rtsne (Rtsne)
UMAP As above, faster, and can map new points umap (uwot)
Truncated / randomised SVD The same leading components, computed cheaply prcomp_irlba (irlba), RSpectra
NIPALS Leading components one at a time by alternating regressions; tolerates missing cells nipals (nipals, ade4), pca(method = "nipals") (pcaMethods)
Autoencoder Nonlinear compression learned by a network keras, torch
Warning

t-SNE and UMAP are for looking, not for measuring. They have no loadings, so no component can be named; they are stochastic, so two runs differ; and the distances between clusters, along with the apparent sizes of the clusters, carry no reliable meaning. Both depend strongly on a neighbourhood-size setting (perplexity, n_neighbors) that can manufacture groups that are not there.

For the kind of tabular client data in this chapter, PCA remains the default — it is deterministic, its components have loadings you can interpret, and predict applies the same transformation to new clients. Reach for a neighbour-embedding method when the goal is a picture of a high-dimensional space, and report it as a picture.

Packages and commands. cmdscale and dist are in stats; isoMDS is in MASS, which ships with R. Everything else is an install: vegan or smacof for non-metric MDS, kernlab for kpca, Rtsne and uwot for t-SNE and UMAP, irlba (prcomp_irlba) or RSpectra for truncated SVD on large or sparse matrices, nipals or ade4 (nipals) and the Bioconductor package pcaMethods for NIPALS, keras or torch for autoencoders.

10.14 Summary

  • PCA replaces \(p\) correlated variables by \(p\) uncorrelated components, ordered by the variance they explain, and is useful only to the extent that the original variables are correlated.
  • Check suitability before fitting. Bartlett’s test of sphericity must reject, but at realistic sample sizes it nearly always does; the KMO measure (\(0.78\) here) and the per-variable MSA say whether the correlation is shared widely enough for a few components to summarise it.
  • The components are the eigenvectors of the covariance (or correlation) matrix, and the variance each explains is the corresponding eigenvalue. Equivalently, they come from the SVD of the centred data matrix.
  • Centering is mandatory. Scaling is mandatory whenever the variables are in different units, which for client data is nearly always. Log-transform skewed monetary variables first.
  • The eigenvalues of a correlation matrix sum to \(p\), so a component with \(\lambda > 1\) explains more than one original variable.
  • Choose the number of components with several criteria at once — scree, cumulative variance, Kaiser, broken stick — and let interpretability settle the disagreements.
  • Loadings describe variables, scores describe observations, cos2 measures how well a variable is represented, contributions measure how much a variable builds a component, and the communality \(h_j^2\) — the sum of a variable’s squared cosines over the retained components — measures how much of that variable the reduced solution recovers.
  • In our bank portfolio, three components explaining \(67\%\) of the variance recover an affluence axis, a digital engagement versus branch axis, and a credit strain axis.
  • Rotating the retained components — varimax and its relatives — buys interpretability by giving up the variance ordering. It leaves the subspace, the communalities and the total retained variance untouched, and redistributes variance among the components. Here it turns an unnameable fourth component into the clearest factor of the four.
  • PCA is unsupervised: the largest component need not be the most useful for any particular downstream task.

10.15 Exercises

10.15.1 Exercise 1 — Scaling

Run a PCA on the raw (untransformed, unscaled) bank variables and compare it with the PCA on the correlation matrix of the transformed data.

  1. What percentage of variance does the first component explain in each case?
  2. Which variables have loadings above \(0.3\) on PC1 in each case?
  3. Convert the six monetary variables (income, balance_current, balance_savings, investments, credit_limit, loan_balance) from euros to thousands of euros and re-run both analyses. Which one changes, and why?

10.15.2 Exercise 2 — Reproducing the output by hand

Using only cor(), eigen() and matrix multiplication:

  1. Compute the eigenvalues of the correlation matrix of X and verify that they sum to \(13\).
  2. Compute the scores of the first three components as \(\mathbf{Y} = \mathbf{Z}\mathbf{A}\), where \(\mathbf{Z}\) is the standardised data.
  3. Verify that the score columns have variances equal to the eigenvalues, and that their pairwise correlations are zero.
  4. Verify that \(\cos^2(j,k) = a_{jk}^2\lambda_k\) equals the squared correlation between variable \(j\) and score \(k\).

Hint: Y <- Z %*% e$vectors gives all the scores at once. Compare apply(Y, 2, var) with e$values, and round(cor(Y[, 1:3]), 10) with the identity matrix.

10.15.3 Exercise 3 — How many components?

For the PCA of X:

  1. Produce the scree plot and state where you would place the elbow.
  2. Apply the Kaiser criterion and the broken-stick model. They disagree — explain the disagreement in terms of \(\lambda_4\).
  3. How many components are needed to reach \(80\%\) of the total variance? Is that a reasonable target here?
  4. Examine the loadings of PC5 and PC6. Can you name either of them? Does that change your answer to (1)?

10.15.4 Exercise 4 — An insurance portfolio

Adapt the analysis to an insurance setting. The code below generates a portfolio of \(500\) motor policies.

  1. Which variables should be log-transformed before the PCA? Justify with a skewness calculation.
  2. claim_cost_3y is zero for policies with no claims. What does that do to a log transform, and how will you handle it?
  3. Fit the PCA on the correlation matrix, decide how many components to keep, and name them.
  4. Produce the correlation circle and the biplot, colouring policies by claims_3y > 0.
  5. Compare your components with the two latent variables (V, B) used to generate the data. Did PCA recover them?
  6. Rotate your retained components with varimax. Does the rotated solution separate the vehicle-and-coverage dimension from the driving-behaviour dimension more cleanly than the unrotated one? Compare each solution’s correlations with V and B.

10.15.5 Exercise 5 — Rotation and interpretation

  1. Build the loadings matrix \(\mathbf{L}\) for three retained components and apply a varimax rotation. Compare it with the four-component rotation of Section 10.10. Does a clean life-stage factor appear? What does that tell you about the order in which “how many components?” and “rotate or not?” have to be decided?
  2. Verify on your own solution that the rotation leaves every communality and the total retained variance unchanged, while changing the share carried by each component.
  3. Return to the four-component solution of Section 10.10 (k <- 4) and rotate it with quartimax and with equamax as well. Which of the three leaves the most variance on the first component, and why is that exactly what its criterion is built to do?
  4. On the same four components, run varimax() with normalize = TRUE and with normalize = FALSE. Report both criterion values and explain which variables Kaiser normalisation is protecting.
  5. Apply promax() to the four-component solution. Report the component correlation matrix. Which pair of components is most strongly correlated, and does the code that generated bank account for it? Repeat with three components: what happens to that correlation, and why?
  6. A colleague writes: “the first rotated component explains 29.1% of the variance, so it is the most important dimension in the portfolio.” Give two reasons to be careful with that sentence.

References

Bartlett, Maurice S. 1950. “Tests of Significance in Factor Analysis.” British Journal of Statistical Psychology 3 (2): 77–85.
Bezdek, J. C., and R. J. Hathaway. 2002. “VAT: A Tool for Visual Assessment of (Cluster) Tendency.” Proceedings of the 2002 International Joint Conference on Neural Networks. IJCNN’02 (Cat. No.02CH37290) 3: 2225–30.
Brock, Guy, Vasyl Pihur, Susmita Datta, and Somnath Datta. 2008. “clValid: An r Package for Cluster Validation.” Journal of Statistical Software 25 (4): 1–22. https://doi.org/10.18637/jss.v025.i04.
Cattell, Raymond B. 1966. “The Scree Test for the Number of Factors.” Multivariate Behavioral Research 1 (2): 245–76.
Charrad, Malika, Nadia Ghazzali, Véronique Boiteau, and Azam Niknafs. 2014. “NbClust: An r Package for Determining the Relevant Number of Clusters in a Data Set.” Journal of Statistical Software 61 (6): 1–36. https://doi.org/10.18637/jss.v061.i06.
Eckart, Carl, and Gale Young. 1936. “The Approximation of One Matrix by Another of Lower Rank.” Psychometrika 1 (3): 211–18.
Gower, J. C. 1971. “A General Coefficient of Similarity and Some of Its Properties.” Biometrics 27 (4): 857–71.
Hartigan, John A., and Manchek A. Wong. 1979. “A K-Means Clustering Algorithm.” Applied Statistics 28: 100–108.
Hendrickson, Alan E., and Paul O. White. 1964. “Promax: A Quick Method for Rotation to Oblique Simple Structure.” British Journal of Statistical Psychology 17 (1): 65–70.
Hotelling, Harold. 1933. “Analysis of a Complex of Statistical Variables into Principal Components.” Journal of Educational Psychology 24 (6): 417–41.
Jolliffe, Ian T. 2002. Principal Component Analysis. 2nd ed. Springer.
Kaiser, Henry F. 1958. “The Varimax Criterion for Analytic Rotation in Factor Analysis.” Psychometrika 23 (3): 187–200.
Kaiser, Henry F. 1960. “The Application of Electronic Computers to Factor Analysis.” Educational and Psychological Measurement 20 (1): 141–51.
Kaiser, Henry F. 1970. “A Second Generation Little Jiffy.” Psychometrika 35 (4): 401–15.
Kaiser, Henry F. 1974. “An Index of Factorial Simplicity.” Psychometrika 39 (1): 31–36.
Kaufman, Leonard, and Peter J. Rousseeuw. 1990. Finding Groups in Data: An Introduction to Cluster Analysis. Wiley.
MacQueen, J. 1967. “Some Methods for Classification and Analysis of Multivariate Observations.” In Proceedings of the Fifth Berkeley Symposium on Mathematical Statistics and Probability, edited by L. M. Le Cam and J. Neyman, vol. 1. University of California Press.
Midway, Steve. 2022. Data Analysis in r. https://bookdown.org/steve_midway/DAR/.
Pearson, Karl. 1901. “On Lines and Planes of Closest Fit to Systems of Points in Space.” The London, Edinburgh, and Dublin Philosophical Magazine and Journal of Science 2 (11): 559–72.
Peng, Roger D. 2012. Exploratory Data Analysis with R. https://bookdown.org/rdpeng/exdata/.
Theodoridis, Sergios, and Konstantinos Koutroumbas. 2008. Pattern Recognition. 4th ed. Academic Press.
Thurstone, Louis L. 1947. Multiple-Factor Analysis: A Development and Expansion of the Vectors of Mind. University of Chicago Press.
Tibshirani, Robert, Guenther Walther, and Trevor Hastie. 2001. “Estimating the Number of Data Clusters via the Gap Statistic.” Journal of the Royal Statistical Society: Series B (Statistical Methodology) 63 (2): 411–23.
Tufte, Edward. 2006. Beautiful Evidence. Graphics Press LLC.
Tukey, John Wilder. 1977. Exploratory Data Analysis. Addison-Wesley Publishing Company.
Wickham, Hadley, Mine Cetinkaya-Rundel, and Garrett Grolemund. 2023. R for Data Science: Import, Tidy, Transform, Visualize, and Model Data. 2nd ed. https://r4ds.hadley.nz/.
Wickham, Hadley, Danielle Navarro, and Thomas Lin Pederson. 2019. Ggplot2: Elegant Graphics for Data Analysis. 3rd ed. https://ggplot2-book.org/.
Wold, Herman. 1966. “Estimation of Principal Components and Related Models by Iterative Least Squares.” In Multivariate Analysis, edited by P. R. Krishnaiah. Academic Press.