Skip to content

Commit 43e90b9

Browse files
committed
start implementation of conditional and total moments for local i
1 parent 5e7e38d commit 43e90b9

File tree

2 files changed

+152
-1
lines changed

2 files changed

+152
-1
lines changed

esda/moran.py

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def __moments(self):
217217
self.VI_norm = v_num / v_den - (1.0 / (n - 1)) ** 2
218218
self.seI_norm = self.VI_norm ** (1 / 2.0)
219219

220-
# variance under randomization
220+
# variance under total randomization
221221
xd4 = z ** 4
222222
xd2 = z ** 2
223223
k_num = xd4.sum() / n
@@ -1005,6 +1005,7 @@ def __init__(
10051005
quads = [1, 3, 2, 4]
10061006
self.quads = quads
10071007
self.__quads()
1008+
self.__moments()
10081009
if permutations:
10091010
self.p_sim, self.rlisas = _crand_plus(
10101011
z,
@@ -1057,6 +1058,43 @@ def __quads(self):
10571058
+ self.quads[3] * pn
10581059
)
10591060

1061+
def __moments(self):
1062+
W = self.w.sparse
1063+
z = self.z
1064+
warnings.simplefilter('always', sparse.SparseEfficiencyWarning)
1065+
n = self.n
1066+
m2 = (z*z).sum()/n
1067+
wi = numpy.asarray(W.sum(axis=1)).flatten()
1068+
wi2 = numpy.asarray(W.multiply(W).sum(axis=1)).flatten()
1069+
# ---------------------------------------------------------
1070+
# Conditional randomization null, Sokal 1998, Eqs. A7 & A8
1071+
# ---------------------------------------------------------
1072+
expectation = -(z**2 * wi) / ((n-1)*m2)
1073+
variance = ((z/m2)**2 *
1074+
(n/(n-2)) *
1075+
((wi2 - wi**2) / (n-1)) *
1076+
(m2 - z**2) / (n-1))
1077+
1078+
self.EIc = expectation
1079+
self.VIc = variance
1080+
# ---------------------------------------------------------
1081+
# Total randomization null, Sokal 1998, Eqs. A3 & A4*
1082+
# ---------------------------------------------------------
1083+
m4 = z**4/n
1084+
b2 = m4/m2**2
1085+
1086+
expectation = -wi / (n-1)
1087+
variance_sokal = (wi2*(n - b2)/(n-1)
1088+
+ (wi**2 - wi2)*(2*b2 - n)/((n-1)*(n-2))
1089+
- (-wi / (n-1))**2)
1090+
_wikh = wikh(W, sokal_correction=sokal_correction)
1091+
variance_anselin = (wi2 * (n - b2)/(n-1)
1092+
+ 2*_wikh*(2*b2 - n) / ((n-1)*(n-2))
1093+
- wi**2/(n-1)**2)
1094+
self.EI = expectation
1095+
self.VI_sokal = variance_sokal
1096+
self.VI_anselin = variance_anselin
1097+
10601098
@property
10611099
def _statistic(self):
10621100
"""More consistent hidden attribute to access ESDA statistics"""
@@ -1622,6 +1660,95 @@ def by_col(
16221660
for col in rate_df.columns:
16231661
df[col] = rate_df[col]
16241662

1663+
# --------------------------------------------------------------
1664+
# Conditional Randomization Moment Estimators
1665+
# --------------------------------------------------------------
1666+
1667+
def _wikh_fast(W, sokal_correction=False):
1668+
"""
1669+
This computes the outer product of weights for each observation.
1670+
1671+
w_{i(kh)} = \sum_{k \neq i}^n \sum_{h \neq i}^n w_ik * w_hk
1672+
1673+
if the sokal correction is used (default), then we also have h \neq k
1674+
Since the sokal correction introduces a simplification in the expression
1675+
where this is used, the defaults should always return the version in
1676+
the original :cite:`Anselin1995 paper`.
1677+
1678+
Arguments
1679+
---------
1680+
W : scipy sparse matrix
1681+
a sparse matrix describing the spatial relationships
1682+
between observations.
1683+
sokal_correction: bool
1684+
Whether to avoid self-neighbors in the summation of weights.
1685+
If False (default), then the outer product of all weights
1686+
for observation i are used, regardless if they are of the form
1687+
w_hh or w_kk.
1688+
1689+
Returns
1690+
-------
1691+
(n,) length numpy.ndarray containing the result.
1692+
"""
1693+
return _wikh_numba(W.shape[0], *W.nonzero(), W.data,
1694+
sokal_correction=sokal_correction)
1695+
1696+
@_njit(fastmath=True)
1697+
def _wikh_numba(n, row, col, data, sokal_correction=False):
1698+
"""
1699+
This is a fast implementation of the wi(kh) function from
1700+
:cite:`Anselin1995`.
1701+
1702+
This uses numpy to compute the outer product of each observation's
1703+
weights, after removing the w_ii entry. Then, the sum of the outer
1704+
product is taken. If the sokal correction is requested, the trace
1705+
of the outer product matrix is removed from the result.
1706+
"""
1707+
result = numpy.empty((n,), dtype=data.dtype)
1708+
ixs = numpy.arange(n)
1709+
for i in ixs:
1710+
# all weights that are not the self weight
1711+
row_no_i = data[(row == i) & (col != i)]
1712+
# compute the pairwise product
1713+
pairwise_product = numpy.outer(row_no_i, row_no_i)
1714+
# get the sum overall (wik*wih)
1715+
result[i] = pairwise_product.sum()
1716+
if sokal_correction:
1717+
# minus the diagonal (wik*wih when k==h)
1718+
result[i] -= numpy.trace(pairwise_product)
1719+
return result/2
1720+
1721+
def _wikh_slow(W, sokal_correction=False):
1722+
"""
1723+
This is a slow implementation of the wi(kh) function from
1724+
:cite:`Anselin1995`
1725+
1726+
This does three nested for-loops over n, doing the literal operations
1727+
stated by the expression.
1728+
"""
1729+
W = W.toarray()
1730+
(n,n) = W.shape
1731+
result = numpy.empty((n,))
1732+
# for each observation
1733+
for i in range(n):
1734+
acc = 0
1735+
# we need the product wik
1736+
for k in range(n):
1737+
# excluding wii * wih
1738+
if i == k:
1739+
continue
1740+
# and wij
1741+
for h in range(n):
1742+
# excluding wik * wii
1743+
if (i == h):
1744+
continue
1745+
if sokal_correction:
1746+
# excluding wih * wih
1747+
if (h == k):
1748+
continue
1749+
acc += W[i,k] * W[i,h]
1750+
result[i] = acc
1751+
return result/2
16251752

16261753
# --------------------------------------------------------------
16271754
# Conditional Randomization Function Implementations

esda/tests/test_moran.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,30 @@ def test_by_col(self):
136136
self.assertAlmostEqual(lm.z_z_sim[0], -0.6990291160835514)
137137
self.assertAlmostEqual(lm.z_p_z_sim[0], 0.24226691753791396)
138138

139+
def test_local_moments(self):
140+
lm = moran.Moran_Local(
141+
self.y,
142+
self.w,
143+
transformation="r",
144+
permutations=0,
145+
seed=SEED,
146+
)
147+
148+
wikh_fast = moran._wikh_fast(lm.w)
149+
wikh_slow = moran._wikh_slow(lm.w)
150+
wikh_fast_c = moran._wikh_fast(lm.w, sokal_correction=True)
151+
wikh_slow_c = moran._wikh_slow(lm.w, sokal_correction=True)
152+
153+
numpy.testing.assert_allclose(wikh_fast, wikh_slow)
154+
numpy.testing.assert_allclose(wikh_fast, wikh_slow)
155+
156+
assert NotImplementedError()
157+
numpy.testing.assert_allclose(lm.EIc, ...)
158+
numpy.testing.assert_allclose(lm.VIc, ...)
159+
numpy.testing.assert_allclose(lm.EI, ...)
160+
numpy.testing.assert_allclose(lm.VI_sokal, ...)
161+
numpy.testing.assert_allclose(lm.VI_anselin, ...)
162+
139163

140164
class Moran_Local_BV_Tester(unittest.TestCase):
141165
def setUp(self):

0 commit comments

Comments
 (0)