Author: Hannes Becher

# Pedigree PCA

#### Hannes Becher

    library(randPedPCA)
    #> Loading required package: spam
    #> Spam version 2.11-1 (2025-01-20) is loaded.
    #> Type 'help( Spam)' or 'demo( spam)' for a short introduction
    #> and overview of this package.
    #> Help for individual functions is also obtained by adding the
    #> suffix '.spam' to the function name, e.g. 'help( chol.spam)'.
    #>
    #> Attaching package: 'spam'
    #> The following objects are masked from 'package:base':
    #>
    #>     backsolve, forwardsolve
    #> Loading required package: Matrix
    #>
    #> Attaching package: 'Matrix'
    #> The following object is masked from 'package:spam':
    #>
    #>     det
    #> Loading required package: pedigreeTools
    #> Loading required package: RSpectra

# Intro

Principal component analysis (PCA) is a method for summarising the information in the columns of a matrix. Because pedigrees can be represented as matrices, they can, in principle, be subjected to PCA.

When thinking about PCA, one usually begins with a data matrix, such as the iris dataset, where rows represent individuals (or observations) and columns contain traits (or features or markers). However, when dealing with a pedigree-derived matrix, there is no data matrix of this kind. For instance, a pedigree’s additive relationship matrix \(A\) is a covariance matrix. Fortunately, PCA can also be achieved by starting from a covariance matrix. That is what the `randPedPCA` package does.

In this vignette, we demonstrate how to use the package `randPedPCA` and how to carry out PCA on your own pedigree or pedigree matrix.

## Pedigree input

Using an example dataset that ships with `randPedPCA`, called `pedMeta2`, we first generate a pedigree object. This is done using the function `pedigree` from the dependence `pedigreeTools`. Then we use `rppca` to perform PCA. Ignore the deprecation warning about matrix conversion if one comes up. It is generated by a dependency.

    # generate a pedigree object from the example dataset provided
    ped <- pedigree(pedMeta2$fid,
                    pedMeta2$mid,
                    pedMeta2$id)
    pc01 <- rppca(ped, center=TRUE)
    #> 'as(<dtTMatrix>, "dtCMatrix")' is deprecated.
    #> Use 'as(., "CsparseMatrix")' instead.
    #> See help("Deprecated") and help("Matrix-deprecated").
    plot(pc01, col = factor(pedMeta2$population))
    #> Subsetting colours using the individual index.

## Matrix input

Instead of starting from a pedigree, we can also use the pedigree’s \(L^{-1}\) matrix as the input to `rppca`. This matrix can be generated using the `getLInv` function from `pedigreeTools`. Note that `rppca` expects a sparse matrix in `spam` format. This is why we wrap `sparse2spam` around `getLInv`:

    li <- sparse2spam(getLInv(ped))
    pc02 <- rppca(li, center = TRUE)
    plot(pc02, col = factor(pedMeta2$population))
    #> Subsetting colours using the individual index.

For simplicity, we decided to ship this \(L^{-1}\) matrix as an example dataset. The PCA result is identical when using this object `pedLInv2`:

    pc03 <- rppca(pedLInv2, center = TRUE)
    plot(pc03, col = factor(pedMeta2$population))
    #> Subsetting colours using the individual index.

## Centring

It is usually recommended that PCA is run on centred data, which is what we did in the examples above. If the data are not centred, PC1 tends to capture the deviation from mean = 0 of each data column. In the context of pedigree PCA, this usually means that PC1 accounts for a high proportion of the total variance and it usually aligns with time or generation number.

The plots above show an un-centred and a cented PCA of the same simulated pedigree. Two breeds (red and black) are generated from an initial population and are propagated individually for 10 generations. Then, a hybrid population (green) is made, which keeps receiving geneflow from both red and black for another 10 generations. Without centring (left), PC1 aligns well with the generation number, a pattern that we often observe. With centring (right), PC1 reflects the differentiation between the breeds.

## Variance components

When running PCA, users often care about how much variance and what proportion of the total variance is accounted for by individual principal components. This can be queried using the `summary` function on a PCA object. For objects generated by `rppca`, `summary` will return the standard deviation of each PC. There will be two additional, sowing the proportion of total variance and their cumulative sum, unless if the input to `rppca` was an \(L^{-1}\) matrix. R users may be used to this behaviour from the built-in functions `prcomp` and `princomp`.

The reason that the variance proportions and cumulative sums are not shown by default is that these numbers are costly to estimate from a large \(L^{-1}\) matrix, compared to running the actual PCA. If the total variance is known, it can be specified when running `rppca` using the argument `totVar`. Below, we describe how the total variance can be estimated.

### Estimating the total variance from an \(L^{-1}\) matrix

Again, this is only required if the pedigree is unknown and the user has access to only the \(L^{-1}\) matrix. In the context of pedigrees, the total variance of the data corresponds to the variance of the pedigree’s \(L\) matrix, which is equivalent to the trace of the corresponding \(A\) matrix. Both matrices are expensive to compute. If the PCA is ran without centring, then the total variance also equals the sum of (the inbreeding values + 1), which can be computed directly from the pedigree. With centring, this does not work and the total variance will be lower. It has to be estimated. Here, we implemented the Hutch++ trace estimation algorithm for this purpose.

    # True total variance computed via the pedigree's inbreeding values
    sum(inbreeding(ped) + 1) # "ped" was defined at the top
    #> [1] 3545.965

    # Now estimate the total variance (the trace of A) from the corresponding
    #  L inverse matrix using Hutch++
    li <- sparse2spam(getLInv(ped)) # generate L inverse and convert to spam format
    set.seed(123345) # set random seed as Hutch++ uses random numbers
    hutchpp(li) # Hutch++ with default settings
    #> [1] 3508.627
    #> attr(,"center")
    #> [1] FALSE

    # for higher accuracy increase num_queries (increases running time)
    hutchpp(li,num_queries = 100)
    #> [1] 3538.996
    #> attr(,"center")
    #> [1] FALSE

We recommend that users experiment with the value of `num_queries` to optimise accuracy. Using the the default value of 10 should work within reasonable time even for a large pedigree of 1M individuals.

Hutch++ can also be used to compute the total variance of a centred dataset. We demonstrate this here using a small simulated example, where we can perform naive centring for comparison.

    # Get L, the "data matrix" of a pedigree
    ll <- getL(ped)
    # centre L (because L is upper triangular, we centre the rows)
    llc <- apply(ll, 1, function(x) x - mean(x))
    # compute additive relationship matrix of the centred data
    ac <- llc %*% t(llc)
    sum(diag(ac)) # exact value (would be too expensive to compute for a large pedigree)
    #> [1] 2705.148

    # Obtain centred estimate from L inverse using Hutch++
    li <- sparse2spam(getLInv(ped)) # generate L inverse and convert to spam format
    set.seed(123345) # set random seed as Hutch++ uses random numbers
    hutchpp(li,num_queries = 100, center = TRUE) # estimate
    #> [1] 2702.809
    #> attr(,"center")
    #> [1] TRUE

If you want to use a `hutchpp` estimate when running `rppca`, make sure that the `center` values are identical. E.g., when running a PCA with centring, make sure that you have run `hutchpp` with centring as well!

### Variance components - Examples

The summary of a PCA generated from an \(L^{-1}\) matrix by default only shows the standard deviation accounted for by each PC. It also throws a warning saying that there is no information about the total variance:

    summary(pc02)
    #> Warning in summary.rppca(pc02): Input does not contain information on variance components. Consider
    #> running rppca on a pedigree object or supplying an estimate of the total
    #> variance of the data.
    #> Importance of components:
    #>                       PC1    PC2    PC3    PC4    PC5    PC6     PC7     PC8
    #> Standard deviation 0.5383 0.2549 0.1592 0.1283 0.1158 0.1003 0.09116 0.07829
    #>                        PC9    PC10
    #> Standard deviation 0.07113 0.06784

When starting from a pedigree and with no centring, then variance proportions and cumulative sums are returned as well:

    pc04 <- rppca(ped, center=FALSE)
    summary(pc04)
    #> Importance of components:
    #>                           PC1    PC2     PC3     PC4     PC5      PC6      PC7
    #> Standard deviation     0.6054 0.5383 0.16651 0.15783 0.11610 0.107647 0.096077
    #> Proportion of Variance 0.2738 0.2165 0.02071 0.01861 0.01007 0.008657 0.006896
    #> Cumulative proportion  0.2738 0.4902 0.51095 0.52956 0.53963 0.548290 0.555180
    #>                             PC8      PC9     PC10
    #> Standard deviation     0.077346 0.073546 0.065972
    #> Proportion of Variance 0.004469 0.004041 0.003251
    #> Cumulative proportion  0.559650 0.563690 0.566940

If the user has an estimate of the total variance, this can be supplied when calling `rppca` on a matrix object and the variance proportions and cumulative sums are returned. In practice, this should rarely be needed as the user will probably have access to the pedigree, from which the total variance is automatically computed (example above).

    pc05 <- rppca(pedLInv2, center=FALSE, totVar=3521.534)
    summary(pc05)
    #> Importance of components:
    #>                           PC1    PC2     PC3     PC4     PC5      PC6      PC7
    #> Standard deviation     0.6054 0.5383 0.16652 0.15782 0.11610 0.107938 0.095575
    #> Proportion of Variance 0.2757 0.2180 0.02086 0.01874 0.01014 0.008764 0.006871
    #> Cumulative proportion  0.2757 0.4936 0.51450 0.53324 0.54338 0.552140 0.559010
    #>                            PC8      PC9     PC10
    #> Standard deviation     0.07374 0.071785 0.069139
    #> Proportion of Variance 0.00409 0.003876 0.003596
    #> Cumulative proportion  0.56310 0.566980 0.570570

When running `rppca` on a pedigree object without centring, totVar can be used to override the value computed from the pedigree. This triggers a warning. In this case here, it also causes variance proportions > 1 which do not make sense:

    pc07 <- rppca(ped, center=FALSE, totVar=123)
    #> Warning in rppca.pedigree(ped, center = FALSE, totVar = 123): Using user-specified value of 123 for the total variance
    #>       instead of the value computed from the pedigree, which was 3545.96461401437
    summary(pc07)
    #> Importance of components:
    #>                           PC1     PC2     PC3     PC4     PC5     PC6      PC7
    #> Standard deviation     0.6054  0.5383  0.1665  0.1578  0.1158  0.1080  0.09609
    #> Proportion of Variance 7.8926  6.2405  0.5970  0.5365  0.2886  0.2513  0.19884
    #> Cumulative proportion  7.8926 14.1332 14.7301 15.2666 15.5552 15.8065 16.00534
    #>                             PC8      PC9     PC10
    #> Standard deviation      0.07325  0.07209  0.06733
    #> Proportion of Variance  0.11556  0.11193  0.09763
    #> Cumulative proportion  16.12089 16.23282 16.33045

Centred PCA
Variance proportions are automatically computed if the input is a pedigree.

    pc06 <- rppca(ped, center=TRUE)
    summary(pc06)
    #> Importance of components:
    #>                           PC1     PC2    PC3     PC4     PC5      PC6      PC7
    #> Standard deviation     0.5383 0.25490 0.1592 0.12819 0.11589 0.099049 0.090324
    #> Proportion of Variance 0.2838 0.06363 0.0248 0.01609 0.01315 0.009607 0.007989
    #> Cumulative proportion  0.2838 0.34740 0.3722 0.38830 0.40145 0.411050 0.419040
    #>                             PC8      PC9     PC10
    #> Standard deviation     0.079083 0.073734 0.060644
    #> Proportion of Variance 0.006124 0.005324 0.003601
    #> Cumulative proportion  0.425170 0.430490 0.434090

If the input is an \(L^{-1}\) matrix, variance proportions are only computed if an estimate of the total variance is supplied.

    # No proportions shown by default
    pc08 <- rppca(pedLInv2, center=TRUE)
    summary(pc08)
    #> Warning in summary.rppca(pc08): Input does not contain information on variance components. Consider
    #> running rppca on a pedigree object or supplying an estimate of the total
    #> variance of the data.
    #> Importance of components:
    #>                       PC1    PC2    PC3    PC4    PC5    PC6     PC7     PC8
    #> Standard deviation 0.5383 0.2549 0.1592 0.1284 0.1159 0.1006 0.09108 0.07477
    #>                        PC9    PC10
    #> Standard deviation 0.07179 0.06723

    # Only when estimate is supplied
    pc09 <- rppca(pedLInv2, center=TRUE, totVar=2673.6)
    summary(pc09)
    #> Importance of components:
    #>                           PC1     PC2    PC3     PC4     PC5      PC6      PC7
    #> Standard deviation     0.5383 0.25490 0.1592 0.12830 0.11558 0.099849 0.087249
    #> Proportion of Variance 0.2871 0.06438 0.0251 0.01631 0.01323 0.009878 0.007542
    #> Cumulative proportion  0.2871 0.35150 0.3766 0.39291 0.40614 0.416020 0.423560
    #>                             PC8      PC9     PC10
    #> Standard deviation     0.073368 0.069188 0.066494
    #> Proportion of Variance 0.005333 0.004743 0.004381
    #> Cumulative proportion  0.428900 0.433640 0.438020

# Plotting

We generated a `plot` method for `rppca` objects. If the variance proportions are known, they will be displayed in the axis labels by default. Note that if the total variance was estimated as in this example, where it was supplied when calling `rppca`, then the proportions displayed come with some uncertainty.

    # PC1 and PC2 are plotted by default
    plot(pc06, col=factor(pedMeta2$population), main="My pedigree PCA")
    #> Subsetting colours using the individual index.

    # plot PC1 and PC3 instead
    plot(pc06, dims=c(1,3), col=factor(pedMeta2$population), main="My pedigree PCA,\ncustom PCs shown")
    #> Subsetting colours using the individual index.

## Down-sampling

Because `plot.rppca` relies on R’s relatively slow base plotting function, it is useful to down-sample the individuals to be plotted. Thus, `plot.rppca` will automatically down-sample to 10,000 individuals if there are more in the dataset. Technically there is no down-sampling and all the data are retained. Instead, an integer vector is added in the `ds` slot of the `rppca` object. That vector is automatically used by `plot.rppca` for individuals and any colours specified by the `col` parameter. The value of `col` should thus have the same length as there were individuals in the full dataset. To adjust the down-sampling, one can use the parameter `to` of `plot.rppca`. The value of `to` is passed to the function `dspc`, which carries out the down-sampling/vector generation. See `?dspc` for details. To plot wihthout any down-sampling, do `plot(myPc, to=NA)`.

One potential issue is that every time `plot.rppca` is run, a new set of individuals is sampled, so that repeated plots of the same dataset will look different. To retain a specific down-sampling, one can run `dspc` outside of `plot.rppca`, like `pcDown <- dspc(pc)`. Calling `plot(pcDown)` repeatedly will generate the same plot every time.

# Performance

We have implemented two PCA methods in `rppca`, accessible through the `method` argument. The user can choose between `randSVS` (the default) and `rspec`. The latter does not use randomised SVD but rather the Spectra C++ library (https://spectralib.org/) via the `RSPpectra` package. This method tends to be faster than the default if a greater number of principal components are requested. The exact threshold depends on the dataset and the computing platform used. As a rule of thumb, if the number of PCs requested is \(\leq\) 6, then the default method will be faster. This addition was inspired by reviewer 2 of our manuscript submitted to GSE.

# Using one’s own data

Users will mainly want to analyse their own empirical or simulated datasets. This should be straightforward if the data are in pedigree format, which we recommend. In this case, `rppca` can be called directly in such an object as demonstrated above.

If the user has an \(L^{-1}\) matrix, this needs to be converted into a `spam` object, a specific sparse matrix format. We provide a wrapper called `importLinv` for reading matrices in Matrix Market format from disk and converting these into `spam` format. If a sparse \(L^{-1}\) matrix is already available in in the workspace, `sparse2spam` should work to convert it to `spam` format. Let us know if this does not work for you (and please share a reproducible example).