github.com/gopherd/gonum@v0.0.4/stat/spatial/spatial_test.go (about) 1 // Copyright ©2017 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 spatial 6 7 import ( 8 "math" 9 "testing" 10 11 "math/rand" 12 13 "github.com/gopherd/gonum/floats/scalar" 14 "github.com/gopherd/gonum/mat" 15 ) 16 17 func simpleAdjacency(n, wide int, diag bool) mat.Matrix { 18 m := mat.NewDense(n, n, nil) 19 for i := 0; i < n; i++ { 20 for j := 1; j <= wide; j++ { 21 if j > i { 22 continue 23 } 24 m.Set(i-j, i, 1) 25 m.Set(i, i-j, 1) 26 } 27 if diag { 28 m.Set(i, i, 1) 29 } 30 } 31 return m 32 } 33 34 func simpleAdjacencyBand(n, wide int, diag bool) mat.Matrix { 35 m := mat.NewBandDense(n, n, wide, wide, nil) 36 for i := 0; i < n; i++ { 37 for j := 1; j <= wide; j++ { 38 if j > i { 39 continue 40 } 41 m.SetBand(i-j, i, 1) 42 m.SetBand(i, i-j, 1) 43 } 44 if diag { 45 m.SetBand(i, i, 1) 46 } 47 } 48 return m 49 } 50 51 var spatialTests = []struct { 52 from, to float64 53 n, wide int 54 fn func(float64, int, *rand.Rand) float64 55 locality func(n, wide int, diag bool) mat.Matrix 56 57 // Values for MoranI and z-score are obtained from 58 // an R reference implementation. 59 wantMoranI float64 60 wantZ float64 61 62 // The value for expected number of significant 63 // segments is obtained from visual inspection 64 // of the plotted data. 65 wantSegs int 66 }{ 67 // Dense matrix locality. 68 { 69 from: 0, to: 1, n: 1000, wide: 1, 70 fn: func(_ float64, _ int, rnd *rand.Rand) float64 { 71 return rnd.Float64() 72 }, 73 locality: simpleAdjacency, 74 75 wantMoranI: -0.04387221370785312, 76 wantZ: -1.3543515772206267, 77 wantSegs: 0, 78 }, 79 { 80 from: -math.Pi / 2, to: 3 * math.Pi / 2, n: 1000, wide: 1, 81 fn: func(x float64, _ int, _ *rand.Rand) float64 { 82 y := math.Sin(x) 83 if math.Abs(y) > 0.5 { 84 y *= 1/math.Abs(y) - 1 85 } 86 return y * math.Sin(x*2) 87 }, 88 locality: simpleAdjacency, 89 90 wantMoranI: 1.0008149537991464, 91 wantZ: 31.648547078779092, 92 wantSegs: 4, 93 }, 94 { 95 from: 0, to: 1, n: 1000, wide: 1, 96 fn: func(_ float64, _ int, rnd *rand.Rand) float64 { 97 return rnd.NormFloat64() 98 }, 99 locality: simpleAdjacency, 100 101 wantMoranI: 0.0259414094549987, 102 wantZ: 0.8511426395944303, 103 wantSegs: 0, 104 }, 105 { 106 from: 0, to: 1, n: 1000, wide: 1, 107 fn: func(x float64, _ int, rnd *rand.Rand) float64 { 108 if rnd.Float64() < 0.5 { 109 return rnd.NormFloat64() + 5 110 } 111 return rnd.NormFloat64() 112 }, 113 locality: simpleAdjacency, 114 115 wantMoranI: -0.0003533345592575677, 116 wantZ: 0.0204605353504713, 117 wantSegs: 0, 118 }, 119 { 120 from: 0, to: 1, n: 1000, wide: 1, 121 fn: func(x float64, i int, rnd *rand.Rand) float64 { 122 if i%2 == 0 { 123 return rnd.NormFloat64() + 5 124 } 125 return rnd.NormFloat64() 126 }, 127 locality: simpleAdjacency, 128 129 wantMoranI: -0.8587138204405251, 130 wantZ: -27.09614459007475, 131 wantSegs: 0, 132 }, 133 { 134 from: 0, to: 1, n: 1000, wide: 1, 135 fn: func(_ float64, i int, _ *rand.Rand) float64 { 136 return float64(i % 2) 137 }, 138 locality: simpleAdjacency, 139 140 wantMoranI: -1, 141 wantZ: -31.559531064275987, 142 wantSegs: 0, 143 }, 144 145 // Band matrix locality. 146 { 147 from: 0, to: 1, n: 1000, wide: 1, 148 fn: func(_ float64, _ int, rnd *rand.Rand) float64 { 149 return rnd.Float64() 150 }, 151 locality: simpleAdjacencyBand, 152 153 wantMoranI: -0.04387221370785312, 154 wantZ: -1.3543515772206267, 155 wantSegs: 0, 156 }, 157 { 158 from: -math.Pi / 2, to: 3 * math.Pi / 2, n: 1000, wide: 1, 159 fn: func(x float64, _ int, _ *rand.Rand) float64 { 160 y := math.Sin(x) 161 if math.Abs(y) > 0.5 { 162 y *= 1/math.Abs(y) - 1 163 } 164 return y * math.Sin(x*2) 165 }, 166 locality: simpleAdjacencyBand, 167 168 wantMoranI: 1.0008149537991464, 169 wantZ: 31.648547078779092, 170 wantSegs: 4, 171 }, 172 { 173 from: 0, to: 1, n: 1000, wide: 1, 174 fn: func(_ float64, _ int, rnd *rand.Rand) float64 { 175 return rnd.NormFloat64() 176 }, 177 locality: simpleAdjacencyBand, 178 179 wantMoranI: 0.0259414094549987, 180 wantZ: 0.8511426395944303, 181 wantSegs: 0, 182 }, 183 { 184 from: 0, to: 1, n: 1000, wide: 1, 185 fn: func(x float64, _ int, rnd *rand.Rand) float64 { 186 if rnd.Float64() < 0.5 { 187 return rnd.NormFloat64() + 5 188 } 189 return rnd.NormFloat64() 190 }, 191 locality: simpleAdjacencyBand, 192 193 wantMoranI: -0.0003533345592575677, 194 wantZ: 0.0204605353504713, 195 wantSegs: 0, 196 }, 197 { 198 from: 0, to: 1, n: 1000, wide: 1, 199 fn: func(x float64, i int, rnd *rand.Rand) float64 { 200 if i%2 == 0 { 201 return rnd.NormFloat64() + 5 202 } 203 return rnd.NormFloat64() 204 }, 205 locality: simpleAdjacencyBand, 206 207 wantMoranI: -0.8587138204405251, 208 wantZ: -27.09614459007475, 209 wantSegs: 0, 210 }, 211 { 212 from: 0, to: 1, n: 1000, wide: 1, 213 fn: func(_ float64, i int, _ *rand.Rand) float64 { 214 return float64(i % 2) 215 }, 216 locality: simpleAdjacencyBand, 217 218 wantMoranI: -1, 219 wantZ: -31.559531064275987, 220 wantSegs: 0, 221 }, 222 } 223 224 func TestGetisOrd(t *testing.T) { 225 for ti, test := range spatialTests { 226 rnd := rand.New(rand.NewSource(1)) 227 data := make([]float64, test.n) 228 step := (test.to - test.from) / float64(test.n) 229 for i := range data { 230 data[i] = test.fn(test.from+step*float64(i), i, rnd) 231 } 232 locality := test.locality(test.n, test.wide, true) 233 234 nseg := getisOrdSegments(data, nil, locality) 235 if nseg != test.wantSegs { 236 t.Errorf("unexpected number of significant segments for test %d: got:%d want:%d", 237 ti, nseg, test.wantSegs) 238 } 239 } 240 } 241 242 // getisOrdSegments returns the number of contiguously significant G*i segemtns in 243 // data. This allows an intuitive validation of the function in lieu of a reference 244 // implementation. 245 func getisOrdSegments(data, weight []float64, locality mat.Matrix) int { 246 const thresh = 2 247 var nseg int 248 segstart := -1 249 for i := range data { 250 gi := GetisOrdGStar(i, data, weight, locality) 251 if segstart != -1 { 252 if math.Abs(gi) < thresh { 253 // Filter short segments. 254 if i-segstart < 5 { 255 segstart = -1 256 continue 257 } 258 259 segstart = -1 260 nseg++ 261 } 262 continue 263 } 264 if math.Abs(gi) >= thresh { 265 segstart = i 266 } 267 } 268 if segstart != -1 && len(data)-segstart >= 5 { 269 nseg++ 270 } 271 return nseg 272 } 273 274 func TestGlobalMoransI(t *testing.T) { 275 const tol = 1e-14 276 for ti, test := range spatialTests { 277 rnd := rand.New(rand.NewSource(1)) 278 data := make([]float64, test.n) 279 step := (test.to - test.from) / float64(test.n) 280 for i := range data { 281 data[i] = test.fn(test.from+step*float64(i), i, rnd) 282 } 283 locality := test.locality(test.n, test.wide, false) 284 285 gotI, _, gotZ := GlobalMoransI(data, nil, locality) 286 287 if !scalar.EqualWithinAbsOrRel(gotI, test.wantMoranI, tol, tol) { 288 t.Errorf("unexpected Moran's I value for test %d: got:%v want:%v", ti, gotI, test.wantMoranI) 289 } 290 if !scalar.EqualWithinAbsOrRel(gotZ, test.wantZ, tol, tol) { 291 t.Errorf("unexpected Moran's I z-score for test %d: got:%v want:%v", ti, gotZ, test.wantZ) 292 } 293 } 294 }