gonum.org/v1/gonum@v0.14.0/stat/pca_cca.go (about)

     1  // Copyright ©2016 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package stat
     6  
     7  import (
     8  	"errors"
     9  	"math"
    10  
    11  	"gonum.org/v1/gonum/floats"
    12  	"gonum.org/v1/gonum/mat"
    13  )
    14  
    15  // PC is a type for computing and extracting the principal components of a
    16  // matrix. The results of the principal components analysis are only valid
    17  // if the call to PrincipalComponents was successful.
    18  type PC struct {
    19  	n, d    int
    20  	weights []float64
    21  	svd     *mat.SVD
    22  	ok      bool
    23  }
    24  
    25  // PrincipalComponents performs a weighted principal components analysis on the
    26  // matrix of the input data which is represented as an n×d matrix a where each
    27  // row is an observation and each column is a variable.
    28  //
    29  // PrincipalComponents centers the variables but does not scale the variance.
    30  //
    31  // The weights slice is used to weight the observations. If weights is nil, each
    32  // weight is considered to have a value of one, otherwise the length of weights
    33  // must match the number of observations or PrincipalComponents will panic.
    34  //
    35  // PrincipalComponents returns whether the analysis was successful.
    36  func (c *PC) PrincipalComponents(a mat.Matrix, weights []float64) (ok bool) {
    37  	c.n, c.d = a.Dims()
    38  	if weights != nil && len(weights) != c.n {
    39  		panic("stat: len(weights) != observations")
    40  	}
    41  
    42  	c.svd, c.ok = svdFactorizeCentered(c.svd, a, weights)
    43  	if c.ok {
    44  		c.weights = append(c.weights[:0], weights...)
    45  	}
    46  	return c.ok
    47  }
    48  
    49  // VectorsTo returns the component direction vectors of a principal components
    50  // analysis. The vectors are returned in the columns of a d×min(n, d) matrix.
    51  //
    52  // If dst is empty, VectorsTo will resize dst to be d×min(n, d). When dst is
    53  // non-empty, VectorsTo will panic if dst is not d×min(n, d). VectorsTo will also
    54  // panic if the receiver does not contain a successful PC.
    55  func (c *PC) VectorsTo(dst *mat.Dense) {
    56  	if !c.ok {
    57  		panic("stat: use of unsuccessful principal components analysis")
    58  	}
    59  
    60  	if dst.IsEmpty() {
    61  		dst.ReuseAs(c.d, min(c.n, c.d))
    62  	} else {
    63  		if d, n := dst.Dims(); d != c.d || n != min(c.n, c.d) {
    64  			panic(mat.ErrShape)
    65  		}
    66  	}
    67  	c.svd.VTo(dst)
    68  }
    69  
    70  // VarsTo returns the column variances of the principal component scores,
    71  // b * vecs, where b is a matrix with centered columns. Variances are returned
    72  // in descending order.
    73  // If dst is not nil it is used to store the variances and returned.
    74  // Vars will panic if the receiver has not successfully performed a principal
    75  // components analysis or dst is not nil and the length of dst is not min(n, d).
    76  func (c *PC) VarsTo(dst []float64) []float64 {
    77  	if !c.ok {
    78  		panic("stat: use of unsuccessful principal components analysis")
    79  	}
    80  	if dst != nil && len(dst) != min(c.n, c.d) {
    81  		panic("stat: length of slice does not match analysis")
    82  	}
    83  
    84  	dst = c.svd.Values(dst)
    85  	var f float64
    86  	if c.weights == nil {
    87  		f = 1 / float64(c.n-1)
    88  	} else {
    89  		f = 1 / (floats.Sum(c.weights) - 1)
    90  	}
    91  	for i, v := range dst {
    92  		dst[i] = f * v * v
    93  	}
    94  	return dst
    95  }
    96  
    97  func min(a, b int) int {
    98  	if a < b {
    99  		return a
   100  	}
   101  	return b
   102  }
   103  
   104  // CC is a type for computing the canonical correlations of a pair of matrices.
   105  // The results of the canonical correlation analysis are only valid
   106  // if the call to CanonicalCorrelations was successful.
   107  type CC struct {
   108  	// n is the number of observations used to
   109  	// construct the canonical correlations.
   110  	n int
   111  
   112  	// xd and yd are used for size checks.
   113  	xd, yd int
   114  
   115  	x, y, c *mat.SVD
   116  	ok      bool
   117  }
   118  
   119  // CanonicalCorrelations performs a canonical correlation analysis of the
   120  // input data x and y, columns of which should be interpretable as two sets
   121  // of measurements on the same observations (rows). These observations are
   122  // optionally weighted by weights. The result of the analysis is stored in
   123  // the receiver if the analysis is successful.
   124  //
   125  // Canonical correlation analysis finds associations between two sets of
   126  // variables on the same observations by finding linear combinations of the two
   127  // sphered datasets that maximize the correlation between them.
   128  //
   129  // Some notation: let Xc and Yc denote the centered input data matrices x
   130  // and y (column means subtracted from each column), let Sx and Sy denote the
   131  // sample covariance matrices within x and y respectively, and let Sxy denote
   132  // the covariance matrix between x and y. The sphered data can then be expressed
   133  // as Xc * Sx^{-1/2} and Yc * Sy^{-1/2} respectively, and the correlation matrix
   134  // between the sphered data is called the canonical correlation matrix,
   135  // Sx^{-1/2} * Sxy * Sy^{-1/2}. In cases where S^{-1/2} is ambiguous for some
   136  // covariance matrix S, S^{-1/2} is taken to be E * D^{-1/2} * Eᵀ where S can
   137  // be eigendecomposed as S = E * D * Eᵀ.
   138  //
   139  // The canonical correlations are the correlations between the corresponding
   140  // pairs of canonical variables and can be obtained with c.Corrs(). Canonical
   141  // variables can be obtained by projecting the sphered data into the left and
   142  // right eigenvectors of the canonical correlation matrix, and these
   143  // eigenvectors can be obtained with c.Left(m, true) and c.Right(m, true)
   144  // respectively. The canonical variables can also be obtained directly from the
   145  // centered raw data by using the back-transformed eigenvectors which can be
   146  // obtained with c.Left(m, false) and c.Right(m, false) respectively.
   147  //
   148  // The first pair of left and right eigenvectors of the canonical correlation
   149  // matrix can be interpreted as directions into which the respective sphered
   150  // data can be projected such that the correlation between the two projections
   151  // is maximized. The second pair and onwards solve the same optimization but
   152  // under the constraint that they are uncorrelated (orthogonal in sphered space)
   153  // to previous projections.
   154  //
   155  // CanonicalCorrelations will panic if the inputs x and y do not have the same
   156  // number of rows.
   157  //
   158  // The slice weights is used to weight the observations. If weights is nil, each
   159  // weight is considered to have a value of one, otherwise the length of weights
   160  // must match the number of observations (rows of both x and y) or
   161  // CanonicalCorrelations will panic.
   162  //
   163  // More details can be found at
   164  // https://en.wikipedia.org/wiki/Canonical_correlation
   165  // or in Chapter 3 of
   166  // Koch, Inge. Analysis of multivariate and high-dimensional data.
   167  // Vol. 32. Cambridge University Press, 2013. ISBN: 9780521887939
   168  func (c *CC) CanonicalCorrelations(x, y mat.Matrix, weights []float64) error {
   169  	var yn int
   170  	c.n, c.xd = x.Dims()
   171  	yn, c.yd = y.Dims()
   172  	if c.n != yn {
   173  		panic("stat: unequal number of observations")
   174  	}
   175  	if weights != nil && len(weights) != c.n {
   176  		panic("stat: len(weights) != observations")
   177  	}
   178  
   179  	// Center and factorize x and y.
   180  	c.x, c.ok = svdFactorizeCentered(c.x, x, weights)
   181  	if !c.ok {
   182  		return errors.New("stat: failed to factorize x")
   183  	}
   184  	c.y, c.ok = svdFactorizeCentered(c.y, y, weights)
   185  	if !c.ok {
   186  		return errors.New("stat: failed to factorize y")
   187  	}
   188  	var xu, xv, yu, yv mat.Dense
   189  	c.x.UTo(&xu)
   190  	c.x.VTo(&xv)
   191  	c.y.UTo(&yu)
   192  	c.y.VTo(&yv)
   193  
   194  	// Calculate and factorise the canonical correlation matrix.
   195  	var ccor mat.Dense
   196  	ccor.Product(&xv, xu.T(), &yu, yv.T())
   197  	if c.c == nil {
   198  		c.c = &mat.SVD{}
   199  	}
   200  	c.ok = c.c.Factorize(&ccor, mat.SVDThin)
   201  	if !c.ok {
   202  		return errors.New("stat: failed to factorize ccor")
   203  	}
   204  	return nil
   205  }
   206  
   207  // CorrsTo returns the canonical correlations, using dst if it is not nil.
   208  // If dst is not nil and len(dst) does not match the number of columns in
   209  // the y input matrix, Corrs will panic.
   210  func (c *CC) CorrsTo(dst []float64) []float64 {
   211  	if !c.ok {
   212  		panic("stat: canonical correlations missing or invalid")
   213  	}
   214  
   215  	if dst != nil && len(dst) != c.yd {
   216  		panic("stat: length of destination does not match input dimension")
   217  	}
   218  	return c.c.Values(dst)
   219  }
   220  
   221  // LeftTo returns the left eigenvectors of the canonical correlation matrix if
   222  // spheredSpace is true. If spheredSpace is false it returns these eigenvectors
   223  // back-transformed to the original data space.
   224  //
   225  // If dst is empty, LeftTo will resize dst to be xd×yd. When dst is
   226  // non-empty, LeftTo will panic if dst is not xd×yd. LeftTo will also
   227  // panic if the receiver does not contain a successful CC.
   228  func (c *CC) LeftTo(dst *mat.Dense, spheredSpace bool) {
   229  	if !c.ok || c.n < 2 {
   230  		panic("stat: canonical correlations missing or invalid")
   231  	}
   232  
   233  	if dst.IsEmpty() {
   234  		dst.ReuseAs(c.xd, c.yd)
   235  	} else {
   236  		if d, n := dst.Dims(); d != c.xd || n != c.yd {
   237  			panic(mat.ErrShape)
   238  		}
   239  	}
   240  	c.c.UTo(dst)
   241  	if spheredSpace {
   242  		return
   243  	}
   244  
   245  	xs := c.x.Values(nil)
   246  	xv := &mat.Dense{}
   247  	c.x.VTo(xv)
   248  
   249  	scaleColsReciSqrt(xv, xs)
   250  
   251  	dst.Product(xv, xv.T(), dst)
   252  	dst.Scale(math.Sqrt(float64(c.n-1)), dst)
   253  }
   254  
   255  // RightTo returns the right eigenvectors of the canonical correlation matrix if
   256  // spheredSpace is true. If spheredSpace is false it returns these eigenvectors
   257  // back-transformed to the original data space.
   258  //
   259  // If dst is empty, RightTo will resize dst to be yd×yd. When dst is
   260  // non-empty, RightTo will panic if dst is not yd×yd. RightTo will also
   261  // panic if the receiver does not contain a successful CC.
   262  func (c *CC) RightTo(dst *mat.Dense, spheredSpace bool) {
   263  	if !c.ok || c.n < 2 {
   264  		panic("stat: canonical correlations missing or invalid")
   265  	}
   266  
   267  	if dst.IsEmpty() {
   268  		dst.ReuseAs(c.yd, c.yd)
   269  	} else {
   270  		if d, n := dst.Dims(); d != c.yd || n != c.yd {
   271  			panic(mat.ErrShape)
   272  		}
   273  	}
   274  	c.c.VTo(dst)
   275  	if spheredSpace {
   276  		return
   277  	}
   278  
   279  	ys := c.y.Values(nil)
   280  	yv := &mat.Dense{}
   281  	c.y.VTo(yv)
   282  
   283  	scaleColsReciSqrt(yv, ys)
   284  
   285  	dst.Product(yv, yv.T(), dst)
   286  	dst.Scale(math.Sqrt(float64(c.n-1)), dst)
   287  }
   288  
   289  func svdFactorizeCentered(work *mat.SVD, m mat.Matrix, weights []float64) (svd *mat.SVD, ok bool) {
   290  	n, d := m.Dims()
   291  	centered := mat.NewDense(n, d, nil)
   292  	col := make([]float64, n)
   293  	for j := 0; j < d; j++ {
   294  		mat.Col(col, j, m)
   295  		floats.AddConst(-Mean(col, weights), col)
   296  		centered.SetCol(j, col)
   297  	}
   298  	for i, w := range weights {
   299  		floats.Scale(math.Sqrt(w), centered.RawRowView(i))
   300  	}
   301  	if work == nil {
   302  		work = &mat.SVD{}
   303  	}
   304  	ok = work.Factorize(centered, mat.SVDThin)
   305  	return work, ok
   306  }
   307  
   308  // scaleColsReciSqrt scales the columns of cols
   309  // by the reciprocal square-root of vals.
   310  func scaleColsReciSqrt(cols *mat.Dense, vals []float64) {
   311  	if cols == nil {
   312  		panic("stat: input nil")
   313  	}
   314  	n, d := cols.Dims()
   315  	if len(vals) != d {
   316  		panic("stat: input length mismatch")
   317  	}
   318  	col := make([]float64, n)
   319  	for j := 0; j < d; j++ {
   320  		mat.Col(col, j, cols)
   321  		floats.Scale(math.Sqrt(1/vals[j]), col)
   322  		cols.SetCol(j, col)
   323  	}
   324  }