Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for and find nearest PSD LD matrix #9

Open
3 tasks
shz9 opened this issue Jun 14, 2022 · 1 comment
Open
3 tasks

Check for and find nearest PSD LD matrix #9

shz9 opened this issue Jun 14, 2022 · 1 comment

Comments

@shz9
Copy link
Owner

shz9 commented Jun 14, 2022

In some applications, we may require that the LD matrix (SNP-by-SNP correlation matrix) be positive definite (PD) or positive semi-definite (PSD). Very often, this is not the case, which may result in instabilities for some downstream models. Therefore, it would be great to augment our LD functionalities to check for PSD and to also find the nearest PSD matrix to the sample LD matrix.

A good starting point may be the code snippet in this StackOverflow thread. I modified this function to find the nearest PSD while retaining the sparsity structure in the original matrix:

from numpy import linalg as la

def nearestPD(A):
    """Find the nearest positive-definite matrix to input

    A Python/Numpy port of John D'Errico's `nearestSPD` MATLAB code [1], which
    credits [2].
    
    Credit: https://stackoverflow.com/a/43244194
    Modified by Shadi Zabad to retain the sparsity structure in the original matrix.

    [1] https://www.mathworks.com/matlabcentral/fileexchange/42885-nearestspd

    [2] N.J. Higham, "Computing a nearest symmetric positive semidefinite
    matrix" (1988): https://doi.org/10.1016/0024-3795(88)90223-6
    """

    np.fill_diagonal(A, 1.)
    B = (A + A.T) / 2
    _, s, V = la.svd(B)

    H = np.dot(V.T, np.dot(np.diag(s), V))

    A2 = (B + H) / 2

    A3 = (A2 + A2.T) / 2
    A3[A == 0.] = 0.

    if isPD(A3):
        return A3

    spacing = np.spacing(la.norm(A))
    # The above is different from [1]. It appears that MATLAB's `chol` Cholesky
    # decomposition will accept matrixes with exactly 0-eigenvalue, whereas
    # Numpy's will not. So where [1] uses `eps(mineig)` (where `eps` is Matlab
    # for `np.spacing`), we use the above definition. CAVEAT: our `spacing`
    # will be much larger than [1]'s `eps(mineig)`, since `mineig` is usually on
    # the order of 1e-16, and `eps(1e-16)` is on the order of 1e-34, whereas
    # `spacing` will, for Gaussian random matrixes of small dimension, be on
    # othe order of 1e-16. In practice, both ways converge, as the unit test
    # below suggests.
    I = np.eye(A.shape[0])
    k = 1
    while not isPD(A3):
        mineig = np.min(np.real(la.eigvals(A3)))
        A3 += I * (-mineig * k**2 + spacing)
        A3[A == 0.] = 0.
        k += 1

    return A3


def isPD(B):
    """Returns true when input is positive-definite, via Cholesky"""
    try:
        _ = la.cholesky(B)
        return True
    except la.LinAlgError:
        return False

The main bottleneck here is that we need to perform SVD on the LD matrix, which may be computationally expensive, even with sparse arrays. One way to get around this difficulty is to deploy this within LD blocks, which may be more manageable.

To experiment with this, we can try to tackle the following tasks:

  • Extract blocks from LDMatrix corresponding to the LD blocks defined by the block estimator.
  • Find nearest PSD for each block separately.
  • Check if this alleviates computational or numerical instabilities for some downstream tasks (e.g. viprs).
@shz9
Copy link
Owner Author

shz9 commented Jun 25, 2022

Another option that seems to work well in some settings is to use the shrinkage algorithm of Higham et al. (2014). Here's a rough implementation of the idea:

from scipy.sparse import identity
from scipy.sparse.linalg import eigsh, ArpackNoConvergence

def isPSD_sparse(B):
    """
    Check for positive semi-definitness using the ARPACK library by 
    examining the smallest eigenvalues.
    """

    try:
        return eigsh(B, 1, which='SA', return_eigenvectors=False)[0] >= 0.
    except ArpackNoConvergence:
        return eigsh(B, 1, which='SA', sigma=0, return_eigenvectors=False)[0] >= 0.
    
def nearcorr_bisection(m0, tol=1e-5):
    """
    Find the nearest positive semi-definite correlation matrix 
    using the bisection method (Algorithm 3.1) of Higham et al. (2014).
    
    The method finds an intermediate matrix between the original `m0` 
    and the identity matrix of the same shape. The benefit of this is that 
    it retains the sparsity structure in the original matrix, which is 
    important for large covariance matrices in genomics.
    
    One major difference from Higham et al. is that we use the scipy's 
    and ARPACK's sparse matrix operations to find the smallest eigen 
    values efficiently, instead of performing cholesky decomposition to check for 
    positive semi-definitness.
    
    The method specifically returns the `alpha` value from the shrinkage 
    procedure, instead of the updated matrix. This is because under this 
    formulation, we'd only need to multiply the off-diagonal elements by 
    (1 - a_*) to obtain the shrunk correlation matrix.
    
    """
    
    a_l = 0.
    a_r = 1.
    
    m1 = identity(m0.shape[0])
    
    while a_r - a_l > tol:
        print(a_l, a_r)
        a_m = .5*(a_l + a_r)
        if not isPSD_sparse(a_m*m1 + (1. - a_m)*m0):
            a_l = a_m
        else:
            a_r = a_m
            
    return a_r

Needs more testing, but looks like a promising approach that can be very fast.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant