Skip to content

Commit

Permalink
Spatially weighted quantiles
Browse files Browse the repository at this point in the history
  • Loading branch information
baddstats committed Jan 21, 2024
1 parent bfe1858 commit f2ee266
Show file tree
Hide file tree
Showing 10 changed files with 511 additions and 6 deletions.
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: spatstat.explore
Version: 3.2-5.008
Date: 2024-01-10
Version: 3.2-5.009
Date: 2024-01-21
Title: Exploratory Data Analysis for the 'spatstat' Family
Authors@R: c(person("Adrian", "Baddeley",
role = c("aut", "cre", "cph"),
Expand Down
6 changes: 6 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,10 @@ export("spatialCDFtestCalc")
export("spatialCovariateEvidence")
export("spatialCovariateEvidence.exactppm")
export("spatialCovariateEvidence.ppp")
export("SpatialMedian")
export("SpatialMedian.ppp")
export("SpatialQuantile")
export("SpatialQuantile.ppp")
export("sphere.volume")
export("[.ssf")
export("ssf")
Expand Down Expand Up @@ -713,6 +717,8 @@ S3method("Smooth", "solist")
S3method("Smooth", "ssf")
S3method("spatialCovariateEvidence", "exactppm")
S3method("spatialCovariateEvidence", "ppp")
S3method("SpatialMedian", "ppp")
S3method("SpatialQuantile", "ppp")
S3method("[", "ssf")
S3method("StieltjesCalc", "fv")
S3method("StieltjesCalc", "stepfun")
Expand Down
9 changes: 8 additions & 1 deletion NEWS
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@

CHANGES IN spatstat.explore VERSION 3.2-5.008
CHANGES IN spatstat.explore VERSION 3.2-5.009

OVERVIEW

o We thank Marcelino de la Cruz for contributions.

o Spatially weighted median and quantile of mark values.

o Adaptive estimation of intensity for split point patterns.

o Internal improvements.
Expand All @@ -11,6 +15,9 @@ OVERVIEW

NEW FUNCTIONS

o SpatialMedian.ppp, SpatialQuantile.ppp
Spatially weighted median and quantile of mark values of a point pattern.

o densityAdaptiveKernel.splitppp
A method for 'densityAdaptiveKernel' for split point patterns.

Expand Down
173 changes: 173 additions & 0 deletions R/spatialQuantile.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#'
#' Spatially weighted quantile
#'

SpatialMedian <- function(X, ...) {
UseMethod("SpatialMedian")
}

SpatialQuantile <- function(X, prob=0.5, ...) {
UseMethod("SpatialQuantile")
}

#' methods for 'ppp' class

SpatialMedian.ppp <- function(X, sigma=NULL, ...,
type=1,
at=c("pixels", "points"), leaveoneout=TRUE,
weights=NULL,
edge=TRUE, diggle=FALSE, verbose=FALSE) {
SpatialQuantile.ppp(X, sigma=sigma, prob=0.5,
...,
type=type,
at=at, leaveoneout=leaveoneout,
weights=weights,
edge=edge, diggle=diggle, verbose=verbose)
}

SpatialQuantile.ppp <- function(X, prob=0.5, sigma=NULL, ...,
type=1,
at=c("pixels", "points"), leaveoneout=TRUE,
weights=NULL,
edge=TRUE, diggle=FALSE, verbose=FALSE) {
if(!is.ppp(X)) stop("X should be a point pattern")
if(!is.marked(X)) stop("The point pattern X should have marks")
check.1.real(prob)
stopifnot(prob >= 0)
stopifnot(prob <= 1)
at <- match.arg(at)
atName <- switch(at, pixels="pixels", points="data points")
check.1.integer(type)
type <- as.integer(type)
if(!any(type == c(1L,4L)))
stop(paste("Quantiles of type", type, "are not supported"), call.=FALSE)
## extract marks
X <- coerce.marks.numeric(X)
m <- marks(X)
## multiple columns of marks?
if(!is.null(dim(m)) && ncol(m) > 1) {
## compute separately for each column
Xlist <- unstack(X)
Zlist <- lapply(Xlist, SpatialQuantile, prob=prob, sigma=sigma, ...,
type=type,
at=at, leaveoneout=leaveoneout, weights=weights,
edge=edge, diggle=diggle, verbose=verbose)
ZZ <- switch(at,
pixels = as.imlist(Zlist),
points = do.call(data.frame, Zlist))
return(ZZ)
}
## single column of marks
m <- as.numeric(m)
nX <- npoints(X)
#' unique mark values
um <- sort(unique(m))
Num <- length(um)
#' trivial cases
if(nX == 0 || ((Num == 1) && leaveoneout)) {
Z <- switch(at,
pixels = as.im(NA_real_, W=Window(X), ...),
points = rep(NA_real_, nX))
attr(Z, "sigma") <- sigma
return(Z)
}
if(Num == 1) {
Z <- switch(at,
pixels = as.im(um[1], W=Window(X), ...),
points = rep(um[1], nX))
attr(Z, "sigma") <- sigma
return(Z)
}
#' numerical weights
if(!is.null(weights)) {
check.nvector(weights, nX)
if(any(weights < 0)) stop("Negative weights are not permitted")
if(sum(weights) < .Machine$double.eps)
stop("Weights are numerically zero; quantiles are undefined", call.=FALSE)
}
#' start main calculation
## bandwidth selector
if(is.function(sigma))
sigma <- sigma(X, ...)
#' edge correction has no effect if diggle=FALSE
#' (because uniform edge correction cancels)
edge <- edge && diggle
#' smoothed intensity of entire pattern
UX <- unmark(X)
LX <- density(UX, ...,
at=at, leaveoneout=leaveoneout,
weights=weights,
edge=edge, diggle=diggle, positive=TRUE)
#' extract smoothing bandwidth actually used
sigma <- attr(LX, "sigma")
varcov <- attr(LX, "varcov")
#' initialise result
Z <- LX
Z[] <- NA
#' guard against underflow
tinythresh <- 8 * .Machine$double.eps
if(underflow <- (min(LX) < tinythresh)) {
Bad <- (LX < tinythresh)
warning(paste("Numerical underflow detected at",
percentage(Bad, 1), "of", paste0(atName, ";"),
"sigma is probably too small"),
call.=FALSE)
#' apply l'Hopital's Rule at the problem locations
Z[Bad] <- nnmark(X, at=at, xy=LX)[Bad]
Good <- !Bad
}
#' compute
for(k in 1:Num) {
#' cumulative spatial weight of points with marks <= m_[k]
if(k == Num) {
Acum.k <- 1
} else {
w.k <- (m <= um[k]) * (weights %orifnull% 1)
Lcum.k <- density(UX,
weights=w.k,
sigma=sigma, varcov=varcov,
xy=LX,
at=at, leaveoneout=leaveoneout,
edge=edge, diggle=diggle, positive=TRUE)
Acum.k <- Lcum.k/LX
}
if(k == 1) {
#' region where quantile is um[1]
relevant <- (Acum.k >= prob)
if(underflow) relevant <- relevant & Good
if(any(relevant)) {
Z[relevant] <- um[1]
if(verbose)
splat("value um[1] =", um[1], "assigned to", sum(relevant), atName)
}
} else {
#' region where quantile is between um[k-1] and um[k]
unassigned <- (Acum.kprev < prob)
if(underflow) unassigned <- unassigned & Good
if(!any(unassigned)) break
relevant <- unassigned & (Acum.k >= prob)
if(any(relevant)) {
if(type == 1) {
## left-continuous inverse
left <- (Acum.k > prob)
Z[relevant & left] <- um[k-1]
Z[relevant & !left] <- um[k]
} else if(type == 4) {
## linear interpolation
Z[relevant] <- um[k-1] +
(um[k] - um[k-1]) * ((prob - Acum.kprev)/(Acum.k - Acum.kprev))[relevant]
}
if(verbose)
splat("values between",
paste0("um", paren(k-1, "[")), "=", um[k-1],
"and",
paste0("um", paren(k, "[")), "=", um[k],
"assigned to", sum(relevant), atName)
}
}
Acum.kprev <- Acum.k
}
attr(Z, "sigma") <- sigma
attr(Z, "varcov") <- varcov
return(Z)
}
2 changes: 1 addition & 1 deletion inst/doc/packagesizes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ date version nhelpfiles nobjects ndatasets Rlines srclines
"2023-05-13" "3.2-1" 234 523 0 31561 6365
"2023-09-07" "3.2-3" 235 523 0 31638 6365
"2023-10-22" "3.2-5" 236 524 0 31769 6365
"2024-01-10" "3.2-5.008" 237 530 0 31854 6365
"2024-01-21" "3.2-5.009" 240 534 0 32027 6365
130 changes: 130 additions & 0 deletions man/SpatialMedian.ppp.Rd
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
\name{SpatialMedian.ppp}
\alias{SpatialMedian.ppp}
\title{
Spatially Weighted Median of Values at Points
}
\description{
Given a spatial point pattern with numeric marks,
compute a weighted median of the mark values,
with spatially-varying weights that depend on distance to the data points.
}
\usage{
\method{SpatialMedian}{ppp}(X, sigma = NULL, \dots,
type = 1, at = c("pixels", "points"), leaveoneout = TRUE,
weights = NULL, edge = TRUE, diggle = FALSE, verbose = FALSE)
}
\arguments{
\item{X}{
A spatial point pattern (object of class \code{"ppp"})
with numeric marks.
}
\item{sigma}{
Smoothing bandwidth, passed to \code{\link{density.ppp}}.
}
\item{\dots}{
Further arguments passed to \code{\link{density.ppp}} controlling the
spatial smoothing.
}
\item{type}{
Type of median (see \code{\link[stats]{quantile.default}}.
Only types 1 and 4 are currently implemented.
}
\item{at}{
Character string indicating whether to compute the median
at every pixel of a pixel image (\code{at="pixels"}, the default)
or at every data point of \code{X} (\code{at="points"}).
}
\item{leaveoneout}{
Logical value indicating whether to compute a leave-one-out
estimator. Applicable only when \code{at="points"}.
}
\item{weights}{
Optional vector of numeric weights attached to the points of \code{X}.
}
\item{edge,diggle}{
Arguments passed to \code{\link{density.ppp}} to
determine the edge correction.
}
\item{verbose}{
Logical value specifying whether to print progress reports
during the calculation.
}
}
\details{
The argument \code{X} should be a spatial point pattern
(object of class \code{"ppp"}) with numeric marks.

The algorithm computes the weighted median of the mark values
at each desired spatial location, using spatially-varying weights
which depend on distance to the data points.

Suppose the data points are at spatial locations
\eqn{x_1,\ldots,x_n}{x[1], ..., x[n]}
and have mark values
\eqn{y_1,\ldots,y_n}{y[1], ..., y[n]}.
For a query location \eqn{u}, the smoothed median is defined
as the weighted median of the mark values
\eqn{y_1,\ldots,y_n}{y[1], ..., y[n]} with weights
\eqn{w_1,\ldots,w_n}{w[1], ..., w[n]},
where
\deqn{
w_i = \frac{k(u,x_i)}{\sum_{j=1}^n k(u,x_j)}
}{
w[i] = k(u,x[i])/(k(u, x[1]) + ... + k(u, x[n]))
}
where \eqn{k(u,v)} is the smoothing kernel with bandwidth \code{sigma}

If \code{at="points"} and \code{leaveoneout=TRUE}, then
a leave-one-out calculation is performed, which means that
when the query location is a data point \eqn{x_i}{x[i]},
the value at the data point is ignored, and
the weighted median is computed from the values \eqn{y_j}{y[j]}
for all \eqn{j} not equal to \eqn{i}.
}
\value{
\emph{If \code{X} has a single column of marks:}
\itemize{
\item
If \code{at="pixels"} (the default), the result is
a pixel image (object of class \code{"im"}).
\item
If \code{at="points"}, the result is a numeric vector
of length equal to the number of points in \code{X}.
}
\emph{If \code{X} has a data frame of marks:}
\itemize{
\item
If \code{at="pixels"} (the default), the result is a named list of
pixel images (object of class \code{"im"}). There is one
image for each column of marks. This list also belongs to
the class \code{"solist"}, for which there is a plot method.
\item
If \code{at="points"}, the result is a data frame
with one row for each point of \code{X},
and one column for each column of marks.
Entries are values of the interpolated function at the points of \code{X}.
}
The return value has attributes
\code{"sigma"} and \code{"varcov"} which report the smoothing
bandwidth that was used.
}
\author{
\adrian.
}
\seealso{
\code{\link{SpatialQuantile.ppp}}

\code{\link{Smooth.ppp}}
}
\examples{
X <- longleaf
if(!interactive()) {
## mark values rounded to nearest multiple of 10 to reduce check time
marks(X) <- round(marks(X), -1)
}
Z <- SpatialMedian(X, sigma=30)
ZX <- SpatialMedian(X, sigma=30, at="points")
}
\keyword{spatial}
\keyword{methods}
\keyword{smooth}
Loading

0 comments on commit f2ee266

Please sign in to comment.