// Copyright ©2017 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package spatial import ( "math" "gonum.org/v1/gonum/mat" "gonum.org/v1/gonum/stat" ) // TODO(kortschak): Implement weighted routines. // GetisOrdGStar returns the Local Getis-Ord G*i statistic for element of the // weighted data using the provided locality matrix. The returned value is a z-score. // // G^*_i = num_i / den_i // // num_i = \sum_j (w_{ij} x_j) - \bar X \sum_j w_{ij} // den_i = S \sqrt(((n \sum_j w_{ij}^2 - (\sum_j w_{ij})^2))/(n - 1)) // \bar X = (\sum_j x_j) / n // S = \sqrt((\sum_j x_j^2)/n - (\bar X)^2) // // GetisOrdGStar will panic if locality is not a square matrix with dimensions the // same as the length of data or if i is not a valid index into data. // // See doi.org/10.1111%2Fj.1538-4632.1995.tb00912.x. // // Weighted Getis-Ord G*i is not currently implemented and GetisOrdGStar will // panic if weights is not nil. func GetisOrdGStar(i int, data, weights []float64, locality mat.Matrix) float64 { if weights != nil { panic("spatial: weighted data not yet implemented") } r, c := locality.Dims() if r != len(data) || c != len(data) { panic("spatial: data length mismatch") } n := float64(len(data)) mean, std := stat.MeanStdDev(data, weights) var dwd, dww, sw float64 if doer, ok := locality.(mat.RowNonZeroDoer); ok { doer.DoRowNonZero(i, func(_, j int, w float64) { sw += w dwd += w * data[j] dww += w * w }) } else { for j, v := range data { w := locality.At(i, j) sw += w dwd += w * v dww += w * w } } s := std * math.Sqrt((n-1)/n) return (dwd - mean*sw) / (s * math.Sqrt((n*dww-sw*sw)/(n-1))) } // GlobalMoransI performs Global Moran's I calculation of spatial autocorrelation // for the given data using the provided locality matrix. GlobalMoransI returns // Moran's I, Var(I) and the z-score associated with those values. // GlobalMoransI will panic if locality is not a square matrix with dimensions the // same as the length of data. // // See https://doi.org/10.1111%2Fj.1538-4632.2007.00708.x. // // Weighted Global Moran's I is not currently implemented and GlobalMoransI will // panic if weights is not nil. func GlobalMoransI(data, weights []float64, locality mat.Matrix) (i, v, z float64) { if weights != nil { panic("spatial: weighted data not yet implemented") } if r, c := locality.Dims(); r != len(data) || c != len(data) { panic("spatial: data length mismatch") } mean := stat.Mean(data, nil) doer, isDoer := locality.(mat.RowNonZeroDoer) // Calculate Moran's I for the data. var num, den, sum float64 for i, xi := range data { zi := xi - mean den += zi * zi if isDoer { doer.DoRowNonZero(i, func(_, j int, w float64) { sum += w zj := data[j] - mean num += w * zi * zj }) } else { for j, xj := range data { w := locality.At(i, j) sum += w zj := xj - mean num += w * zi * zj } } } i = (float64(len(data)) / sum) * (num / den) // Calculate Moran's E(I) for the data. e := -1 / float64(len(data)-1) // Calculate Moran's Var(I) for the data. // http://pro.arcgis.com/en/pro-app/tool-reference/spatial-statistics/h-how-spatial-autocorrelation-moran-s-i-spatial-st.htm // http://pro.arcgis.com/en/pro-app/tool-reference/spatial-statistics/h-global-morans-i-additional-math.htm var s0, s1, s2 float64 var var2, var4 float64 for i, v := range data { v -= mean v *= v var2 += v var4 += v * v var p2 float64 if isDoer { doer.DoRowNonZero(i, func(i, j int, wij float64) { wji := locality.At(j, i) s0 += wij v := wij + wji s1 += v * v p2 += v }) } else { for j := range data { wij := locality.At(i, j) wji := locality.At(j, i) s0 += wij v := wij + wji s1 += v * v p2 += v } } s2 += p2 * p2 } s1 *= 0.5 n := float64(len(data)) a := n * ((n*n-3*n+3)*s1 - n*s2 + 3*s0*s0) c := (n - 1) * (n - 2) * (n - 3) * s0 * s0 d := var4 / (var2 * var2) b := d * ((n*n-n)*s1 - 2*n*s2 + 6*s0*s0) v = (a-b)/c - e*e // Calculate z-score associated with Moran's I for the data. z = (i - e) / math.Sqrt(v) return i, v, z }