This is a difficult question as there just have not been many, if any, spatial process statistics developed for line features. Without seriously digging into equations and code, point process statistics are not readily applicable to linear features and thus, statistically invalid. This is because the null, that a given pattern is tested against, is based on point events given a random field and not linear dependencies. I have to say that I do not even know what the null would be as far as intensity and arrangement/orientation would be even more difficult.
I am just spit-balling here but, I am wondering if a multi-scale evaluation of line density coupled with Euclidean distance (or Hausdorff distance if lines are complex) would not indicate a continuous measure of clustering. This data could then be summarized to the line vectors, using variance to account for disparity in lengths (Thomas 2011), and assigned a cluster value using a statistic such as K-means. I know that you are not after assigned clusters but the cluster value could partition degrees of clustering. This would, obviously, require an optimal fit of k so, arbitrary clusters are not assigned. I am thinking that this would be an interesting approach in evaluating edge structure in graph theoretical models.
Here is a worked example in R, sorry, but it is faster and more reproducible than providing a QGIS example, and is more in my comfort zone:)
Add libraries and use copper psp object from spatstat as line example
library(spatstat)
library(spatstat.core)
library(raster)
library(spatialEco)
data(copper)
l <- copper$Lines
l <- rotate.psp(l, pi/2)
Calculate standardized 1st and 2nd order line density and then coerce to raster class objects
d1st <- density(l)
d1st <- d1st / max(d1st)
d1st <- raster(d1st)
d2nd <- density(l, sigma = 2)
d2nd <- d2nd / max(d2nd)
d2nd <- raster(d2nd)
Standardize 1st and 2nd order density in to a scale-integrated density
d <- d1st + d2nd
d <- d / cellStats(d, stat='max')
Calculate standardized inverted euclidean distance and coerce to raster class
euclidean <- distmap(l)
euclidean <- euclidean / max(euclidean)
euclidean <- raster.invert(raster(euclidean))
Coerce spatstat psp to a sp SpatialLinesDataFrame object to use in raster::extract
as.SpatialLines.psp <- local({
ends2line <- function(x) Line(matrix(x, ncol=2, byrow=TRUE))
munch <- function(z) { Lines(ends2line(as.numeric(z[1:4])), ID=z[5]) }
convert <- function(x) {
ends <- as.data.frame(x)[,1:4]
ends[,5] <- row.names(ends)
y <- apply(ends, 1, munch)
SpatialLines(y)
}
convert
})
l <- as.SpatialLines.psp(l)
l <- SpatialLinesDataFrame(l, data.frame(ID=1:length(l)) )
Plot results
par(mfrow=c(2,2))
plot(d1st, main="1st order line density")
plot(l, add=TRUE)
plot(d2nd, main="2nd order line density")
plot(l, add=TRUE)
plot(d, main="integrated line density")
plot(l, add=TRUE)
plot(euclidean, main="euclidean distance")
plot(l, add=TRUE)
Extract raster values and calculate summary statistics associated with each line
l.dist <- extract(euclidean, l)
l.den <- extract(d, l)
l.stats <- data.frame(min.dist = unlist(lapply(l.dist, min)),
med.dist = unlist(lapply(l.dist, median)),
max.dist = unlist(lapply(l.dist, max)),
var.dist = unlist(lapply(l.dist, var)),
min.den = unlist(lapply(l.den, min)),
med.den = unlist(lapply(l.den, median)),
max.den = unlist(lapply(l.den, max)),
var.den = unlist(lapply(l.den, var)))
Use cluster silhouette values to evaluate optimal k (number of clusters), with the optimal.k function, then assign cluster values to lines. We can then assign colors to each cluster and plot on top of the density raster.
clust <- optimal.k(scale(l.stats), nk = 10, plot = TRUE)
l@data <- data.frame(l@data, cluster = clust$clustering)
kcol <- ifelse(clust$clustering == 1, "red", "blue")
plot(d)
plot(l, col=kcol, add=TRUE)
At this point one could perform a randomization of the lines to test if the resulting intensity and distance are significant from random. You could use the "rshift.psp" function to randomly reorient your lines. You could also just randomize the start and stop points and recreate each line.
One also wonders "what if" you just performed a point pattern analysis using a univariate or cross analysis statistic on the start and stop points, invariant of the lines. In a univariate analysis you would compare the results of the start and stop points to see if there consistency in clustering between the two point patterns. This could be done via a f-hat, G-hat or Ripley's-K-hat (for unmarked point processes). Another approach would be a Cross analysis (eg., cross-K) where the two point processes are tested simultaneously by marking them as [start,stop]. This would indicate the distance relationships in the clustering process between the start and stop points. However, spatial dependency (nonstaionarity) on an underlying intensity process can be an issue in these types of models making them inhomogeneous and requiring a different model. Ironically, inhomogeneous process is modeled using an intensity function which, brings us full circle back to density thus, supporting the idea of using a scale-integrated density as a measure of clustering.
Here is a quick worked example of if the Ripleys K (Besags L) statistic for autocorrelation of an unmarked point process using the start, stop locations of a line feature class. The last model is a cross-k using both start and stop locations as a nominal marked process.
library(spatstat)
library(spatstat.core)
library(spatstat.geom)
data(copper)
l <- copper$Lines
l <- rotate.psp(l, pi/2)
Lr <- function (...) {
K <- Kest(...)
nama <- colnames(K)
K <- K[, !(nama %in% c("rip", "ls"))]
L <- eval.fv(sqrt(K/pi)-bw)
L <- rebadge.fv(L, substitute(L(r), NULL), "L")
return(L)
}
### Ripley's K ( Besag L(r) ) for start locations
start <- endpoints.psp(l, which="first")
marks(start) <- factor("start")
W <- start$window
area <- area.owin(W)
lambda <- start$n / area
ripley <- min(diff(W$xrange), diff(W$yrange))/4
rlarge <- sqrt(1000/(pi * lambda))
rmax <- min(rlarge, ripley)
bw=seq(0, rmax, by=1)
( Lenv <- plot( envelope(start, fun="Lr", r=seq(0, rmax, by=1), nsim=199, nrank=5) ) )
### Ripley's K ( Besag L(r) ) for end locations
stop <- endpoints.psp(l, which="second")
marks(stop) <- factor("stop")
W <- stop$window
area <- area.owin(W)
lambda <- stop$n / area
ripley <- min(diff(W$xrange), diff(W$yrange))/4
rlarge <- sqrt(1000/(pi * lambda))
rmax <- min(rlarge, ripley)
bw=seq(0, rmax, by=1)
( Lenv <- plot( envelope(start, fun="Lr", r=seq(0, rmax, by=1), nsim=199, nrank=5) ) )
### Ripley's Cross-K ( Besag L(r) ) for start/stop
sdata.ppp <- superimpose(start, stop)
( Lenv <- plot(envelope(sdata.ppp, fun="Kcross", r=bw, i="start", j="stop", nsim=199,nrank=5,
transform=expression(sqrt(./pi)-bw), global=TRUE) ) )
References
Thomas J.C.R. (2011) A New Clustering Algorithm Based on K-Means Using a Line Segment as Prototype. In: San Martin C., Kim SW. (eds) Progress in Pattern Recognition, Image Analysis, Computer Vision, and Applications. CIARP 2011. Lecture Notes in Computer Science, vol 7042. Springer, Berlin, Heidelberg