pub mod getis_ord;
pub mod moran;
pub use getis_ord::{GetisOrdGiStar, GetisOrdResult};
pub use moran::{LocalMoransI, LocalMoransIResult, MoransI, MoransIResult};
use crate::error::{AnalyticsError, Result};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
#[derive(Debug, Clone)]
pub struct SpatialWeights {
pub weights: Array2<f64>,
pub row_standardized: bool,
}
impl SpatialWeights {
pub fn from_adjacency(adjacency: Array2<f64>) -> Result<Self> {
let (n, m) = adjacency.dim();
if n != m {
return Err(AnalyticsError::dimension_mismatch(
format!("{}x{}", n, n),
format!("{}x{}", n, m),
));
}
Ok(Self {
weights: adjacency,
row_standardized: false,
})
}
pub fn from_distance_threshold(distances: &ArrayView2<f64>, threshold: f64) -> Result<Self> {
let (n, m) = distances.dim();
if n != m {
return Err(AnalyticsError::dimension_mismatch(
format!("{}x{}", n, n),
format!("{}x{}", n, m),
));
}
let mut weights = Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
if i != j && distances[[i, j]] <= threshold {
weights[[i, j]] = 1.0;
}
}
}
Ok(Self {
weights,
row_standardized: false,
})
}
pub fn from_inverse_distance(distances: &ArrayView2<f64>, power: f64) -> Result<Self> {
let (n, m) = distances.dim();
if n != m {
return Err(AnalyticsError::dimension_mismatch(
format!("{}x{}", n, n),
format!("{}x{}", n, m),
));
}
let mut weights = Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
if i != j {
let dist = distances[[i, j]];
if dist < f64::EPSILON {
return Err(AnalyticsError::invalid_input(
"Distance matrix contains zero distances",
));
}
weights[[i, j]] = 1.0 / dist.powf(power);
}
}
}
Ok(Self {
weights,
row_standardized: false,
})
}
pub fn row_standardize(&mut self) {
let n = self.weights.nrows();
for i in 0..n {
let row_sum: f64 = self.weights.row(i).sum();
if row_sum > f64::EPSILON {
for j in 0..n {
self.weights[[i, j]] /= row_sum;
}
}
}
self.row_standardized = true;
}
#[must_use]
pub fn n_neighbors(&self) -> Array1<usize> {
let n = self.weights.nrows();
let mut neighbors = Array1::zeros(n);
for i in 0..n {
let count = self
.weights
.row(i)
.iter()
.filter(|&&w| w > f64::EPSILON)
.count();
neighbors[i] = count;
}
neighbors
}
pub fn spatial_lag(&self, values: &ArrayView1<f64>) -> Result<Array1<f64>> {
let n = self.weights.nrows();
if values.len() != n {
return Err(AnalyticsError::dimension_mismatch(
format!("{}", n),
format!("{}", values.len()),
));
}
let mut lag = Array1::zeros(n);
for i in 0..n {
let mut weighted_sum = 0.0;
for j in 0..n {
weighted_sum += self.weights[[i, j]] * values[j];
}
lag[i] = weighted_sum;
}
Ok(lag)
}
}
pub fn calculate_spatial_lag(
values: &ArrayView1<f64>,
weights: &ArrayView2<f64>,
) -> Result<Array1<f64>> {
let n = weights.nrows();
if values.len() != n || weights.ncols() != n {
return Err(AnalyticsError::dimension_mismatch(
format!("{}", n),
format!("{}x{}", weights.nrows(), weights.ncols()),
));
}
let mut lag = Array1::zeros(n);
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
sum += weights[[i, j]] * values[j];
}
lag[i] = sum;
}
Ok(lag)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_spatial_weights_from_adjacency() {
let adj = array![[0.0, 1.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
let weights = SpatialWeights::from_adjacency(adj)
.expect("Failed to create spatial weights from adjacency matrix");
assert_eq!(weights.weights.nrows(), 3);
assert!(!weights.row_standardized);
}
#[test]
fn test_spatial_weights_distance_threshold() {
let distances = array![[0.0, 1.0, 5.0], [1.0, 0.0, 2.0], [5.0, 2.0, 0.0]];
let weights = SpatialWeights::from_distance_threshold(&distances.view(), 2.5)
.expect("Failed to create spatial weights from distance threshold");
assert_eq!(weights.weights[[0, 1]], 1.0);
assert_eq!(weights.weights[[1, 2]], 1.0);
assert_eq!(weights.weights[[0, 2]], 0.0);
}
#[test]
fn test_row_standardize() {
let adj = array![[0.0, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]];
let mut weights = SpatialWeights::from_adjacency(adj)
.expect("Failed to create spatial weights for row standardization test");
weights.row_standardize();
assert!(weights.row_standardized);
assert_abs_diff_eq!(weights.weights.row(0).sum(), 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(weights.weights.row(1).sum(), 1.0, epsilon = 1e-10);
}
#[test]
fn test_spatial_lag() {
let adj = array![[0.0, 1.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
let values = array![1.0, 2.0, 3.0];
let weights = SpatialWeights::from_adjacency(adj)
.expect("Failed to create spatial weights for spatial lag test");
let lag = weights
.spatial_lag(&values.view())
.expect("Failed to calculate spatial lag");
assert_abs_diff_eq!(lag[0], 2.0, epsilon = 1e-10); assert_abs_diff_eq!(lag[1], 4.0, epsilon = 1e-10); assert_abs_diff_eq!(lag[2], 2.0, epsilon = 1e-10); }
#[test]
fn test_n_neighbors() {
let adj = array![[0.0, 1.0, 1.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let weights = SpatialWeights::from_adjacency(adj)
.expect("Failed to create spatial weights for n_neighbors test");
let neighbors = weights.n_neighbors();
assert_eq!(neighbors[0], 2);
assert_eq!(neighbors[1], 1);
assert_eq!(neighbors[2], 1);
}
}