mirror of
https://github.com/gonum/gonum.git
synced 2025-10-05 15:16:59 +08:00
294 lines
6.6 KiB
Go
294 lines
6.6 KiB
Go
// 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"
|
|
"math/rand"
|
|
"testing"
|
|
|
|
"gonum.org/v1/gonum/floats"
|
|
"gonum.org/v1/gonum/mat"
|
|
)
|
|
|
|
func simpleAdjacency(n, wide int, diag bool) mat.Matrix {
|
|
m := mat.NewDense(n, n, nil)
|
|
for i := 0; i < n; i++ {
|
|
for j := 1; j <= wide; j++ {
|
|
if j > i {
|
|
continue
|
|
}
|
|
m.Set(i-j, i, 1)
|
|
m.Set(i, i-j, 1)
|
|
}
|
|
if diag {
|
|
m.Set(i, i, 1)
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func simpleAdjacencyBand(n, wide int, diag bool) mat.Matrix {
|
|
m := mat.NewBandDense(n, n, wide, wide, nil)
|
|
for i := 0; i < n; i++ {
|
|
for j := 1; j <= wide; j++ {
|
|
if j > i {
|
|
continue
|
|
}
|
|
m.SetBand(i-j, i, 1)
|
|
m.SetBand(i, i-j, 1)
|
|
}
|
|
if diag {
|
|
m.SetBand(i, i, 1)
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
var spatialTests = []struct {
|
|
from, to float64
|
|
n, wide int
|
|
fn func(float64, int, *rand.Rand) float64
|
|
locality func(n, wide int, diag bool) mat.Matrix
|
|
|
|
// Values for MoranI and z-score are obtained from
|
|
// an R reference implementation.
|
|
wantMoranI float64
|
|
wantZ float64
|
|
|
|
// The value for expected number of significant
|
|
// segments is obtained from visual inspection
|
|
// of the plotted data.
|
|
wantSegs int
|
|
}{
|
|
// Dense matrix locality.
|
|
{
|
|
from: 0, to: 1, n: 1000, wide: 1,
|
|
fn: func(_ float64, _ int, rnd *rand.Rand) float64 {
|
|
return rnd.Float64()
|
|
},
|
|
locality: simpleAdjacency,
|
|
|
|
wantMoranI: -0.0019631298955953233,
|
|
wantZ: -0.03039477405151108,
|
|
wantSegs: 0,
|
|
},
|
|
{
|
|
from: -math.Pi / 2, to: 3 * math.Pi / 2, n: 1000, wide: 1,
|
|
fn: func(x float64, _ int, _ *rand.Rand) float64 {
|
|
y := math.Sin(x)
|
|
if math.Abs(y) > 0.5 {
|
|
y *= 1/math.Abs(y) - 1
|
|
}
|
|
return y * math.Sin(x*2)
|
|
},
|
|
locality: simpleAdjacency,
|
|
|
|
wantMoranI: 1.0008149537991464,
|
|
wantZ: 31.648547078779092,
|
|
wantSegs: 4,
|
|
},
|
|
{
|
|
from: 0, to: 1, n: 1000, wide: 1,
|
|
fn: func(_ float64, _ int, rnd *rand.Rand) float64 {
|
|
return rnd.NormFloat64()
|
|
},
|
|
locality: simpleAdjacency,
|
|
|
|
wantMoranI: 0.031195199553564902,
|
|
wantZ: 1.0171161514080056,
|
|
wantSegs: 0,
|
|
},
|
|
{
|
|
from: 0, to: 1, n: 1000, wide: 1,
|
|
fn: func(x float64, _ int, rnd *rand.Rand) float64 {
|
|
if rnd.Float64() < 0.5 {
|
|
return rnd.NormFloat64() + 5
|
|
}
|
|
return rnd.NormFloat64()
|
|
},
|
|
locality: simpleAdjacency,
|
|
|
|
wantMoranI: -0.016245135637562223,
|
|
wantZ: -0.48157993864993476,
|
|
wantSegs: 0,
|
|
},
|
|
{
|
|
from: 0, to: 1, n: 1000, wide: 1,
|
|
fn: func(x float64, i int, rnd *rand.Rand) float64 {
|
|
if i%2 == 0 {
|
|
return rnd.NormFloat64() + 5
|
|
}
|
|
return rnd.NormFloat64()
|
|
},
|
|
locality: simpleAdjacency,
|
|
|
|
wantMoranI: -0.8565268969272998,
|
|
wantZ: -27.027057520918113,
|
|
wantSegs: 0,
|
|
},
|
|
{
|
|
from: 0, to: 1, n: 1000, wide: 1,
|
|
fn: func(_ float64, i int, _ *rand.Rand) float64 {
|
|
return float64(i % 2)
|
|
},
|
|
locality: simpleAdjacency,
|
|
|
|
wantMoranI: -1,
|
|
wantZ: -31.559531064275987,
|
|
wantSegs: 0,
|
|
},
|
|
|
|
// Band matrix locality.
|
|
{
|
|
from: 0, to: 1, n: 1000, wide: 1,
|
|
fn: func(_ float64, _ int, rnd *rand.Rand) float64 {
|
|
return rnd.Float64()
|
|
},
|
|
locality: simpleAdjacencyBand,
|
|
|
|
wantMoranI: -0.0019631298955953233,
|
|
wantZ: -0.03039477405151108,
|
|
wantSegs: 0,
|
|
},
|
|
{
|
|
from: -math.Pi / 2, to: 3 * math.Pi / 2, n: 1000, wide: 1,
|
|
fn: func(x float64, _ int, _ *rand.Rand) float64 {
|
|
y := math.Sin(x)
|
|
if math.Abs(y) > 0.5 {
|
|
y *= 1/math.Abs(y) - 1
|
|
}
|
|
return y * math.Sin(x*2)
|
|
},
|
|
locality: simpleAdjacencyBand,
|
|
|
|
wantMoranI: 1.0008149537991464,
|
|
wantZ: 31.648547078779092,
|
|
wantSegs: 4,
|
|
},
|
|
{
|
|
from: 0, to: 1, n: 1000, wide: 1,
|
|
fn: func(_ float64, _ int, rnd *rand.Rand) float64 {
|
|
return rnd.NormFloat64()
|
|
},
|
|
locality: simpleAdjacencyBand,
|
|
|
|
wantMoranI: 0.031195199553564902,
|
|
wantZ: 1.0171161514080056,
|
|
wantSegs: 0,
|
|
},
|
|
{
|
|
from: 0, to: 1, n: 1000, wide: 1,
|
|
fn: func(x float64, _ int, rnd *rand.Rand) float64 {
|
|
if rnd.Float64() < 0.5 {
|
|
return rnd.NormFloat64() + 5
|
|
}
|
|
return rnd.NormFloat64()
|
|
},
|
|
locality: simpleAdjacencyBand,
|
|
|
|
wantMoranI: -0.016245135637562223,
|
|
wantZ: -0.48157993864993476,
|
|
wantSegs: 0,
|
|
},
|
|
{
|
|
from: 0, to: 1, n: 1000, wide: 1,
|
|
fn: func(x float64, i int, rnd *rand.Rand) float64 {
|
|
if i%2 == 0 {
|
|
return rnd.NormFloat64() + 5
|
|
}
|
|
return rnd.NormFloat64()
|
|
},
|
|
locality: simpleAdjacencyBand,
|
|
|
|
wantMoranI: -0.8565268969272998,
|
|
wantZ: -27.027057520918113,
|
|
wantSegs: 0,
|
|
},
|
|
{
|
|
from: 0, to: 1, n: 1000, wide: 1,
|
|
fn: func(_ float64, i int, _ *rand.Rand) float64 {
|
|
return float64(i % 2)
|
|
},
|
|
locality: simpleAdjacencyBand,
|
|
|
|
wantMoranI: -1,
|
|
wantZ: -31.559531064275987,
|
|
wantSegs: 0,
|
|
},
|
|
}
|
|
|
|
func TestGetisOrd(t *testing.T) {
|
|
for ti, test := range spatialTests {
|
|
rnd := rand.New(rand.NewSource(1))
|
|
data := make([]float64, test.n)
|
|
step := (test.to - test.from) / float64(test.n)
|
|
for i := range data {
|
|
data[i] = test.fn(test.from+step*float64(i), i, rnd)
|
|
}
|
|
locality := test.locality(test.n, test.wide, true)
|
|
|
|
nseg := getisOrdSegments(data, nil, locality)
|
|
if nseg != test.wantSegs {
|
|
t.Errorf("unexpected number of significant segments for test %d: got:%d want:%d",
|
|
ti, nseg, test.wantSegs)
|
|
}
|
|
}
|
|
}
|
|
|
|
// getisOrdSegments returns the number of contiguously significant G*i segemtns in
|
|
// data. This allows an intuitive validation of the function in lieu of a reference
|
|
// implementation.
|
|
func getisOrdSegments(data, weight []float64, locality mat.Matrix) int {
|
|
const thresh = 2
|
|
var nseg int
|
|
segstart := -1
|
|
for i := range data {
|
|
gi := GetisOrdGStar(i, data, weight, locality)
|
|
if segstart != -1 {
|
|
if math.Abs(gi) < thresh {
|
|
// Filter short segments.
|
|
if i-segstart < 5 {
|
|
segstart = -1
|
|
continue
|
|
}
|
|
|
|
segstart = -1
|
|
nseg++
|
|
}
|
|
continue
|
|
}
|
|
if math.Abs(gi) >= thresh {
|
|
segstart = i
|
|
}
|
|
}
|
|
if segstart != -1 && len(data)-segstart >= 5 {
|
|
nseg++
|
|
}
|
|
return nseg
|
|
}
|
|
|
|
func TestGlobalMoransI(t *testing.T) {
|
|
const tol = 1e-14
|
|
for ti, test := range spatialTests {
|
|
rnd := rand.New(rand.NewSource(1))
|
|
data := make([]float64, test.n)
|
|
step := (test.to - test.from) / float64(test.n)
|
|
for i := range data {
|
|
data[i] = test.fn(test.from+step*float64(i), i, rnd)
|
|
}
|
|
locality := test.locality(test.n, test.wide, false)
|
|
|
|
gotI, _, gotZ := GlobalMoransI(data, nil, locality)
|
|
|
|
if !floats.EqualWithinAbsOrRel(gotI, test.wantMoranI, tol, tol) {
|
|
t.Errorf("unexpected Moran's I value for test %d: got:%v want:%v", ti, gotI, test.wantMoranI)
|
|
}
|
|
if !floats.EqualWithinAbsOrRel(gotZ, test.wantZ, tol, tol) {
|
|
t.Errorf("unexpected Moran's I z-score for test %d: got:%v want:%v", ti, gotZ, test.wantZ)
|
|
}
|
|
}
|
|
}
|