oxigdal_analytics/hotspot/
mod.rs1pub mod getis_ord;
9pub mod moran;
10
11pub use getis_ord::{GetisOrdGiStar, GetisOrdResult};
12pub use moran::{LocalMoransI, LocalMoransIResult, MoransI, MoransIResult};
13
14use crate::error::{AnalyticsError, Result};
15use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
16
17#[derive(Debug, Clone)]
21pub struct SpatialWeights {
22 pub weights: Array2<f64>,
24 pub row_standardized: bool,
26}
27
28impl SpatialWeights {
29 pub fn from_adjacency(adjacency: Array2<f64>) -> Result<Self> {
37 let (n, m) = adjacency.dim();
38 if n != m {
39 return Err(AnalyticsError::dimension_mismatch(
40 format!("{}x{}", n, n),
41 format!("{}x{}", n, m),
42 ));
43 }
44
45 Ok(Self {
46 weights: adjacency,
47 row_standardized: false,
48 })
49 }
50
51 pub fn from_distance_threshold(distances: &ArrayView2<f64>, threshold: f64) -> Result<Self> {
60 let (n, m) = distances.dim();
61 if n != m {
62 return Err(AnalyticsError::dimension_mismatch(
63 format!("{}x{}", n, n),
64 format!("{}x{}", n, m),
65 ));
66 }
67
68 let mut weights = Array2::zeros((n, n));
69 for i in 0..n {
70 for j in 0..n {
71 if i != j && distances[[i, j]] <= threshold {
72 weights[[i, j]] = 1.0;
73 }
74 }
75 }
76
77 Ok(Self {
78 weights,
79 row_standardized: false,
80 })
81 }
82
83 pub fn from_inverse_distance(distances: &ArrayView2<f64>, power: f64) -> Result<Self> {
92 let (n, m) = distances.dim();
93 if n != m {
94 return Err(AnalyticsError::dimension_mismatch(
95 format!("{}x{}", n, n),
96 format!("{}x{}", n, m),
97 ));
98 }
99
100 let mut weights = Array2::zeros((n, n));
101 for i in 0..n {
102 for j in 0..n {
103 if i != j {
104 let dist = distances[[i, j]];
105 if dist < f64::EPSILON {
106 return Err(AnalyticsError::invalid_input(
107 "Distance matrix contains zero distances",
108 ));
109 }
110 weights[[i, j]] = 1.0 / dist.powf(power);
111 }
112 }
113 }
114
115 Ok(Self {
116 weights,
117 row_standardized: false,
118 })
119 }
120
121 pub fn row_standardize(&mut self) {
125 let n = self.weights.nrows();
126 for i in 0..n {
127 let row_sum: f64 = self.weights.row(i).sum();
128 if row_sum > f64::EPSILON {
129 for j in 0..n {
130 self.weights[[i, j]] /= row_sum;
131 }
132 }
133 }
134 self.row_standardized = true;
135 }
136
137 #[must_use]
139 pub fn n_neighbors(&self) -> Array1<usize> {
140 let n = self.weights.nrows();
141 let mut neighbors = Array1::zeros(n);
142
143 for i in 0..n {
144 let count = self
145 .weights
146 .row(i)
147 .iter()
148 .filter(|&&w| w > f64::EPSILON)
149 .count();
150 neighbors[i] = count;
151 }
152
153 neighbors
154 }
155
156 pub fn spatial_lag(&self, values: &ArrayView1<f64>) -> Result<Array1<f64>> {
164 let n = self.weights.nrows();
165 if values.len() != n {
166 return Err(AnalyticsError::dimension_mismatch(
167 format!("{}", n),
168 format!("{}", values.len()),
169 ));
170 }
171
172 let mut lag = Array1::zeros(n);
173 for i in 0..n {
174 let mut weighted_sum = 0.0;
175 for j in 0..n {
176 weighted_sum += self.weights[[i, j]] * values[j];
177 }
178 lag[i] = weighted_sum;
179 }
180
181 Ok(lag)
182 }
183}
184
185pub fn calculate_spatial_lag(
194 values: &ArrayView1<f64>,
195 weights: &ArrayView2<f64>,
196) -> Result<Array1<f64>> {
197 let n = weights.nrows();
198 if values.len() != n || weights.ncols() != n {
199 return Err(AnalyticsError::dimension_mismatch(
200 format!("{}", n),
201 format!("{}x{}", weights.nrows(), weights.ncols()),
202 ));
203 }
204
205 let mut lag = Array1::zeros(n);
206 for i in 0..n {
207 let mut sum = 0.0;
208 for j in 0..n {
209 sum += weights[[i, j]] * values[j];
210 }
211 lag[i] = sum;
212 }
213
214 Ok(lag)
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use approx::assert_abs_diff_eq;
221 use scirs2_core::ndarray::array;
222
223 #[test]
224 fn test_spatial_weights_from_adjacency() {
225 let adj = array![[0.0, 1.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
226
227 let weights = SpatialWeights::from_adjacency(adj)
228 .expect("Failed to create spatial weights from adjacency matrix");
229 assert_eq!(weights.weights.nrows(), 3);
230 assert!(!weights.row_standardized);
231 }
232
233 #[test]
234 fn test_spatial_weights_distance_threshold() {
235 let distances = array![[0.0, 1.0, 5.0], [1.0, 0.0, 2.0], [5.0, 2.0, 0.0]];
236
237 let weights = SpatialWeights::from_distance_threshold(&distances.view(), 2.5)
238 .expect("Failed to create spatial weights from distance threshold");
239
240 assert_eq!(weights.weights[[0, 1]], 1.0);
241 assert_eq!(weights.weights[[1, 2]], 1.0);
242 assert_eq!(weights.weights[[0, 2]], 0.0);
243 }
244
245 #[test]
246 fn test_row_standardize() {
247 let adj = array![[0.0, 1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 0.0]];
248
249 let mut weights = SpatialWeights::from_adjacency(adj)
250 .expect("Failed to create spatial weights for row standardization test");
251 weights.row_standardize();
252
253 assert!(weights.row_standardized);
254 assert_abs_diff_eq!(weights.weights.row(0).sum(), 1.0, epsilon = 1e-10);
255 assert_abs_diff_eq!(weights.weights.row(1).sum(), 1.0, epsilon = 1e-10);
256 }
257
258 #[test]
259 fn test_spatial_lag() {
260 let adj = array![[0.0, 1.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
261 let values = array![1.0, 2.0, 3.0];
262
263 let weights = SpatialWeights::from_adjacency(adj)
264 .expect("Failed to create spatial weights for spatial lag test");
265 let lag = weights
266 .spatial_lag(&values.view())
267 .expect("Failed to calculate spatial lag");
268
269 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); }
273
274 #[test]
275 fn test_n_neighbors() {
276 let adj = array![[0.0, 1.0, 1.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
277
278 let weights = SpatialWeights::from_adjacency(adj)
279 .expect("Failed to create spatial weights for n_neighbors test");
280 let neighbors = weights.n_neighbors();
281
282 assert_eq!(neighbors[0], 2);
283 assert_eq!(neighbors[1], 1);
284 assert_eq!(neighbors[2], 1);
285 }
286}