This page was generated from notebooks/11_distance_decay.ipynb. Interactive online version:
Distance Decay¶
Luc Anselin¶
(revised 09/18/2024)¶
Preliminaries¶
In this notebook, a closer look is taken at the distance decay implied by different kernel functions that are used in the linear SLX model, as well as by the nonlinear distance transformations used in the NSLX specification. Since these functions are derived for the k-nearest neighbors, the spatial patterning of these neighbors will affect how much relevance the values observed at those locations have for the spatial lag. For example, if the nearest neighbors are all rather far away from the origin, e.g., with a relative distance (z) of 0.5 or more, a steep distance decay will essentially eliminate their effect.
Prerequisites¶
Familiarity with numpy, pandas operations and the plot
function for pandas dataframes is assumed. Further customization can be carried out by means of specific features of matplotlib, but that is not considered here.
Modules Needed¶
Since no spreg functions are used, the imports are limited to numpy, pandas and matplotlib.pyplot (the latter is optional).
[85]:
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
np.set_printoptions(legacy = "1.25")
Functions Used¶
from numpy:
linspace
reshape
hstack
from pandas:
DataFrame
concat
plot
from matplotlib.pyplot:
show
Files and Variables¶
In this notebook, no actual data are used.
Distance Metric¶
To obtain comparable graphs, the distance is scaled to a value less than one. This corresponds to the \(z\) value that is computed relative to the bandwidth. This approach avoids problems due to the varying scale of the actual distance metric (e.g., meters vs. km) and makes all distance decay functions comparable.
For the purpose of graphing, 20 equidistance points are used in the interval 0-1, by means of numpy.linspace
.
In practice, the \(z\) values are computed for up to k nearest neighbors, which are not necessarily equally spaced. In fact, the weight of neighbors will vary depending on whether they are close to the origin or closer to the farthest nearest neighbor.
[25]:
z = np.linspace(0.0,1.0,num=21)
z = z.reshape(-1,1)
Kernel Functions¶
Triangular Kernel¶
The triangular kernel is \(1.0 - z\). Note that in the SLX implementation, the value at the origin (diagonal) is set to zero. It is kept at 1.0 here to make the graph clearer.
[26]:
tri = 1.0 - z
tri = tri.reshape(-1,1)
The kernel function is matched with the distance metric to create a simple plot.
[ ]:
p1 = np.hstack((z,tri))
df1 = pd.DataFrame(p1,columns=["distance","triangular"])
df1.plot(x="distance",y="triangular",kind="line",title="Triangular Kernel")
plt.show()
As expected, the result is a linear decline with distance, yielding a weight equal to \(1.0 - z\) at each point. The function terminates with a value of zero for \(z = 1.0\).
Quadratic/Epanechnikov Kernel¶
The quadratric or Epanechnikov kernel takes the value \(1.0 - z^2\). Strictly speaking, this value is scaled by 3/4, but this can be ignored here. The resulting distance decay graph is obtained in the same way as before.
[ ]:
z2 = z**2
z2 = z2.reshape(-1,1)
epa = 1.0 - z2
p2 = np.hstack((z,epa))
df2 = pd.DataFrame(p2,columns=["distance","quadratic"])
df2.plot(x="distance",y="quadratic",kind="line",title="Epanechnikov Kernel")
plt.show()
Note how the curvature is above the line that connects the two endpoints, yielding generally higher weights for the neighbors than the corresponding triangular kernel weights.
Quartic Kernel¶
The quartic kernel is \((1.0 - z^2)^2\). Again, this is customary scaled by 15/16, which is ignored here. The graph is obtained in the same way.
[ ]:
quar = (1.0 - z2) ** 2
p3 = np.hstack((z,quar))
df3 = pd.DataFrame(p3,columns=["distance","quartic"])
df3.plot(x="distance",y="quartic",kind="line",title="Quartic Kernel")
plt.show()
Note the characteristic reverse S-shape with higher than linear weights close to the original, changing to less than linear values in the second half of the distance range.
Gaussian Kernel¶
The Gaussian kernel takes the form \(\sqrt{(2 \pi)}. e^{-z^2/2}\). For the graph, the scaling factor is again ignored. Note that in contrast to the quadratic and quartic kernel functions, this would yield a value at the origin of larger than one, and generally remain above one in the distance range considered. It is therefore applied without the scaling factor. The graph is obtained in the same fashion as before.
[ ]:
zz = -z2 / 2.0
gs = np.exp(zz)
p4 = np.hstack((z,gs))
df4 = pd.DataFrame(p4,columns=["distance","Gaussian"])
df4.plot(x="distance",y="Gaussian",kind="line",title="Gaussian Kernel")
plt.show()
Similar to the pattern for the quadratic kernel, this yields weights above the diagonal for the distance range. Also note that the value at the end point is not zero, but around 0.6 (in fact, for \(z = 1\), the kernel value is \(e^{-1/2} = 0.6\)).
Comparison¶
To facilitate a comparison of the distance decay patterns generated by the different functions, they are next plotted together, anchored to the value of one at the origin.
[ ]:
pall = pd.concat((df1,df2["quadratic"],df3["quartic"],df4["Gaussian"]),axis=1)
pall.plot(x="distance",y=["triangular","quadratic","quartic","Gaussian"],kind="line")
plt.show()
The curves clearly show how the nonlinear functions provide differential weights with distance. Relative to the triangular kernel, only the quartic function penalizes more distant locations more. As pointed out earlier, the Gaussian kernel gives much higher weights than the other curves, implying very little distance decay.
Negative Exponential Distance Function¶
The negative exponential distance function that is implemented for the nonlinear SLX model uses the transformation \(e^{-\alpha z}\), where \(z\) is the same distance fraction as for the kernel functions. This function can be readily graphed for a range of values of the \(\alpha\) parameter. Note that \(\alpha\) is taken to be positive. Negative values for this coefficient are not allowed, since they would yield increasing weights with distance, violating Tobler’s law. For \(\alpha = 0\), the weights are constant and equal to one.
To illustrate the shape of the distance decay function for different parameter values, a range from 0 to 6.0 is considered. To graph the functions, only a simple computation is needed, which is implemented in a for
loop.
[ ]:
a = [0.0, 0.5, 1.0, 2.0, 6.0]
p5 = z
for i in a:
za = - i * z
expon = np.exp(za)
p5 = np.hstack((p5,expon))
cols = ["exp" + str(i) for i in a]
cols.insert(0,"distance")
df5 = pd.DataFrame(p5,columns=cols)
df5.plot(x="distance",y=cols[1:],kind="line",title="Negative Exponential Distance")
plt.show()
Using \(\alpha = 1.0\) as a reference, values less than 1.0 yield a much slower distance decay and vice versa. Note that the value of the weight at the end point is \(e^{-\alpha}\), which is larger than zero for small values of \(\alpha\). Once \(\alpha\) becomes larger, the distance decay becomes very steep. As illustrated in the graph, for a value of 6.0, the weights are essentially zero for \(z > 0.5\), wiping out the effect of any nearest neighbors beyond that distance. It is important to keep this in mind when interpreting the coefficient estimates in a NSLX model.
Inverse Distance Power Function¶
The inverse distance power transformation takes the form \((1.0 - z)^{\alpha}\), with \(\alpha\) taking on a postive value. Using the same procedure as before, the implied distance decay can be graphed for a range of parameter values. For \(\alpha = 0\), the curve is again horizontal.
[ ]:
a = [0.0, 0.5, 1.0, 2.0, 6.0]
p6 = z
zz = 1.0 - z
for i in a:
za = zz**i
p6 = np.hstack((p6,za))
cols = ["pow" + str(i) for i in a]
cols.insert(0,"distance")
df6 = pd.DataFrame(p6,columns=cols)
df6.plot(x="distance",y=cols[1:],kind="line",title="Inverse Distance Power Function")
plt.show()
In contrast to the negative exponential function, the weights end up (by construction) with a value of zero at the bandwidth distance. Coefficient values less than one yield a distance decay with much larger weights for shorter distances, only dropping off more rapidly in the farthest distance range. Values of \(\alpha\) larger than one imply a much steeper (steeper than a linear curve) decay. Even more so than for the negative exponential function, larger values for \(\alpha\) yield essentially zero weights for smaller and smaller distances. In the graph, this is already the case for \(z\) around 0.4, again eliminating the impact of nearest neighbors that are farther away.
Practice¶
It is straightforward to experiment with some other parameter values and possibly other distance transformations. Consider some other kernel functions, currently not supported by PySAL, such as the tricube (\((1.0 - z^3)^3\)) or cosine kernel (\((\pi/4).cos((\pi/2) z)\)).