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  }