26

I'm coming to Python from R and trying to reproduce a number of things that I'm used to doing in R using Python. The Matrix library for R has a very nifty function called nearPD() which finds the closest positive semi-definite (PSD) matrix to a given matrix. While I could code something up, being new to Python/Numpy I don't feel too excited about reinventing the wheel if something is already out there. Any tips on an existing implementation in Python?

Jeremy
  • 22,188
  • 4
  • 68
  • 81
JD Long
  • 59,675
  • 58
  • 202
  • 294
  • Did you look into the question at: http://stackoverflow.com/questions/619335/a-simple-algorithm-for-generating-positive-semidefinite-matrices The answer shows PSD math for numpy. – jakebird451 Jun 07 '12 at 20:26
  • ack... I was searching on the python tag which that didn't have... I'll look and if redundant I'll pull my question and add Python tag to that one – JD Long Jun 07 '12 at 20:28
  • 1
    I looked at that question and they are just forcing the matrix into PSD with no consideration to "nearest". For contrast, here's the Higham paper on which nearPD() was based: http://www.maths.manchester.ac.uk/~nareports/narep369.pdf – JD Long Jun 07 '12 at 20:38
  • 3
    doesn't scipy function [`cov_nearest`](http://www.statsmodels.org/dev/generated/statsmodels.stats.correlation_tools.cov_nearest.html) do neccessary convertion? – diralik Dec 16 '17 at 15:02
  • 1
    @diraria that is an excellent answer to the question and you should add that as an answer, not a comment. The reason nobody initially answered with `cov_nearest` or `corr_nearest` is those functions were not added until August of 2012 and I asked the question in June of 2012. – JD Long Dec 18 '17 at 16:06

5 Answers5

36

I don't think there is a library which returns the matrix you want, but here is a "just for fun" coding of neareast positive semi-definite matrix algorithm from Higham (2000)

import numpy as np,numpy.linalg

def _getAplus(A):
    eigval, eigvec = np.linalg.eig(A)
    Q = np.matrix(eigvec)
    xdiag = np.matrix(np.diag(np.maximum(eigval, 0)))
    return Q*xdiag*Q.T

def _getPs(A, W=None):
    W05 = np.matrix(W**.5)
    return  W05.I * _getAplus(W05 * A * W05) * W05.I

def _getPu(A, W=None):
    Aret = np.array(A.copy())
    Aret[W > 0] = np.array(W)[W > 0]
    return np.matrix(Aret)

def nearPD(A, nit=10):
    n = A.shape[0]
    W = np.identity(n) 
# W is the matrix used for the norm (assumed to be Identity matrix here)
# the algorithm should work for any diagonal W
    deltaS = 0
    Yk = A.copy()
    for k in range(nit):
        Rk = Yk - deltaS
        Xk = _getPs(Rk, W=W)
        deltaS = Xk - Rk
        Yk = _getPu(Xk, W=W)
    return Yk

When tested on the example from the paper, it returns the correct answer

print nearPD(np.matrix([[2,-1,0,0],[-1,2,-1,0],[0,-1,2,-1],[0,0,-1,2]]),nit=10)
[[ 1.         -0.80842467  0.19157533  0.10677227]
 [-0.80842467  1.         -0.65626745  0.19157533]
 [ 0.19157533 -0.65626745  1.         -0.80842467]
 [ 0.10677227  0.19157533 -0.80842467  1.        ]]
sega_sai
  • 8,328
  • 1
  • 29
  • 38
  • 1
    Hey that's pretty fantastic! Thank you. – JD Long Jun 08 '12 at 13:21
  • 3
    Man I hope more people view this sort of stuff as "just for fun". But seriously, this is cool stuff. Awesome. Perhaps consider submitting it to scipy or something? – lightalchemist Apr 27 '13 at 03:14
  • 1
    Your input matrix (`[[2,-1,0,0]...]`) is already positive-definite. And your output matrix has a Frobenius norm of 2.35 from that input. I’m not really sure what’s going on but I ported a Matlab implementation of Higham’s 1988 paper to Python and I’ll just leave the reference here: http://stackoverflow.com/a/43244194/500207 – Ahmed Fasih Apr 06 '17 at 01:37
  • 1
    It seems that the code in the answer above is based on Higham 2000 https://www.maths.manchester.ac.uk/~higham/narep/narep369.pdf which has the goal of computing the nearest _correlation_ matrix (i.e., enforcing unit diagonal). That is much more restrictive than the nearest symmetric positive semidefinite matrix, which is subject of Higham 1988., which the code from @AhmedFasih is based on. https://www.sciencedirect.com/science/article/pii/0024379588902236 – David Bau Oct 28 '20 at 16:05
17

I would submit a non-iterative approach. This is slightly modified from Rebonato and Jackel (1999) (page 7-9). Iterative approaches can take a long time to process on matrices of more than a few hundred variables.

import numpy as np

def nearPSD(A,epsilon=0):
   n = A.shape[0]
   eigval, eigvec = np.linalg.eig(A)
   val = np.matrix(np.maximum(eigval,epsilon))
   vec = np.matrix(eigvec)
   T = 1/(np.multiply(vec,vec) * val.T)
   T = np.matrix(np.sqrt(np.diag(np.array(T).reshape((n)) )))
   B = T * vec * np.diag(np.array(np.sqrt(val)).reshape((n)))
   out = B*B.T
   return(out)

Code is modified from a discussion of this topic here around nonPD/PSD matrices in R.

DomPazz
  • 12,415
  • 17
  • 23
  • This might be a stupid question but can you convert back, i.e. from PSD to the original matrix? – IssamLaradji Mar 05 '14 at 11:21
  • No, you have fundamentally changed the matrix. If you need the original, just make sure you keep a copy (memory permitting, of course). – DomPazz Mar 05 '14 at 15:20
  • This code appears to be wrong. Just testing with a simple positive definite diagonal matrix A = np.diag([1,2,3]) outputs a different one, the identity. – pancho Jul 10 '20 at 08:55
  • 1
    @pancho The linked paper appears to be primarily concerned with correlation matrices, such as those that may be expressed as I + XX' - diag(XX'). Hence, the diagonal terms are limited to 1. – Lorry Mar 05 '23 at 21:33
  • correct, calculate the standard deviations, calculate and correct the correlation matrix, and then use the stdev values to make that a covariance. – DomPazz Jun 26 '23 at 23:09
4

This is perhaps a silly extension to DomPazz answer to consider both correlation and covariance matrices. It also has an early termination if you are dealing with a large number of matrices.

def near_psd(x, epsilon=0):
    '''
    Calculates the nearest postive semi-definite matrix for a correlation/covariance matrix

    Parameters
    ----------
    x : array_like
      Covariance/correlation matrix
    epsilon : float
      Eigenvalue limit (usually set to zero to ensure positive definiteness)

    Returns
    -------
    near_cov : array_like
      closest positive definite covariance/correlation matrix

    Notes
    -----
    Document source
    http://www.quarchome.org/correlationmatrix.pdf

    '''

    if min(np.linalg.eigvals(x)) > epsilon:
        return x

    # Removing scaling factor of covariance matrix
    n = x.shape[0]
    var_list = np.array([np.sqrt(x[i,i]) for i in xrange(n)])
    y = np.array([[x[i, j]/(var_list[i]*var_list[j]) for i in xrange(n)] for j in xrange(n)])

    # getting the nearest correlation matrix
    eigval, eigvec = np.linalg.eig(y)
    val = np.matrix(np.maximum(eigval, epsilon))
    vec = np.matrix(eigvec)
    T = 1/(np.multiply(vec, vec) * val.T)
    T = np.matrix(np.sqrt(np.diag(np.array(T).reshape((n)) )))
    B = T * vec * np.diag(np.array(np.sqrt(val)).reshape((n)))
    near_corr = B*B.T    

    # returning the scaling factors
    near_cov = np.array([[near_corr[i, j]*(var_list[i]*var_list[j]) for i in xrange(n)] for j in xrange(n)])
    return near_cov
Juan Chacon
  • 134
  • 6
3

For those still ending up here, you can now use statsmodels.stats.correlation_tools.cov_nearest

sprague44
  • 121
  • 5
  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/late-answers/31530917) – Kent Kostelac Apr 20 '22 at 08:59
  • when I asked the question 10 years ago, there was no simple canned answer. A few months after I asked the question statsmodels added cov_nearest() and corr_nearest(). I have selected this as the accepted answer because for the last 9+ years this has been the best approach. – JD Long May 12 '22 at 20:54
1

I know this thread is old, but the solutions provided here were not satisfactory for my covariance matrices: the transformed matrices always looked quite different from the original ones (for the cases I tested at least). So, I'm leaving here a very straightforward answer, based on the solution provided in this answer:

import numpy as np

def get_near_psd(A):
    C = (A + A.T)/2
    eigval, eigvec = np.linalg.eig(C)
    eigval[eigval < 0] = 0

    return eigvec.dot(np.diag(eigval)).dot(eigvec.T)

The idea is simple: I compute the symmetric matrix, then do an eigen decomposition to get the eigenvalues and eigenvectors. I zero out all negative eigenvalues and construct back the matrix, which will now be positive semi-definite.

For the sake of completness, I leave a simple code to check whether a matrix is positive semi-definite using numpy (basically checking whether all eigenvalues are non-negative):

def is_pos_semidef(x):
    return np.all(np.linalg.eigvals(x) >= 0)
tjiagoM
  • 448
  • 2
  • 10
  • 23