使用带标记的对数高斯Cox过程建模空间点模式#
介绍#
对数高斯Cox过程(LGCP)是一种点模式的概率模型,通常在空间或时间中观察到。它有两个主要组成部分。首先,在整个域\(X\)上使用指数变换的高斯过程对正实数值的潜在强度场\(\lambda(s)\)进行建模,这使得\(\lambda\)保持为正。然后,使用这个强度场来参数化一个泊松点过程,该过程表示在空间中放置点的随机机制。一些适合这种表示的现象包括一个县内癌症病例的发生率,或城市中犯罪事件的时空位置。尽管本教程仅涉及二维空间数据,但在此框架内可以同等处理空间和时间维度。
更正式地说,如果我们有一个空间 \(X\) 和 \(A\subseteq X\),子集 \(A\) 内发生的点数 \(Y_A\) 的分布由
观察到的点模式是否意味着空间强度发生了统计学上显著的变化?
具有相同统计特性的随机采样模式会是什么样子?
点事件的频率和幅度之间是否存在统计相关性?
在本笔记本中,我们将使用基于网格的近似方法来处理完整的LGCP,并使用PyMC来拟合模型并分析其后验摘要。我们还将探讨标记泊松过程的使用,这是对该模型的一种扩展,用于处理与每个数据点相关的标记的分布。
数据#
我们的观测数据涉及231个海葵,它们的大小和在法国海岸的位置已被记录。这些数据来自R中的spatstat
空间建模包,该包旨在解决LGCP及其后续改进等模型。该数据的原始来源是Upton和Fingleton(1985)的教科书《Spatial data analysis by example》,有关数据的更详细描述可以在那里找到。
import warnings
from itertools import product
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pymc as pm
from matplotlib import MatplotlibDeprecationWarning
from numpy.random import default_rng
warnings.filterwarnings(action="ignore", category=MatplotlibDeprecationWarning)
%config InlineBackend.figure_format = 'retina'
az.style.use("arviz-darkgrid")
data = pd.read_csv(pm.get_data("anemones.csv"))
n = data.shape[0]
此数据集包含每个海葵的坐标和离散标记值。虽然这些标记是整数,但为了简化起见,我们将在后续步骤中将这些值建模为连续值。
data.head(3)
x | y | marks | |
---|---|---|---|
0 | 27 | 7 | 6 |
1 | 197 | 5 | 4 |
2 | 74 | 15 | 4 |
让我们在二维空间中看一下这些数据:
plt.scatter(data["x"], data["y"], c=data["marks"])
plt.colorbar(label="Anemone size")
plt.axis("equal");

‘marks’ 列表示每个海葵的大小。如果我们同时对标记和点的空间分布进行建模,我们就是在对 标记的泊松点过程 进行建模。将基本点模式模型扩展以包含此特征是本笔记本的第二部分。
虽然有多种进行推理的方法,但最简单的方法可能是将我们的域 \(X\) 分割成许多小块 \(A_1, A_2,...,A_M\) 并将强度场固定在每个子集内为常数。然后,我们将每个 \(A_j\) 内的点数视为泊松随机变量,使得 \(Y_j \sim Poisson(\lambda_j)\)。我们还考虑 \(\log{\lambda_1}...,\log{\lambda_M}\) 变量作为从高斯过程的一次抽取。
下面的代码将域分割成网格单元,计算每个单元内的点数,并识别其质心。
xy = data[["x", "y"]].values
# Jitter the data slightly so that none of the points fall exactly
# on cell boundaries
eps = 1e-3
rng = default_rng()
xy = xy.astype("float") + rng.standard_normal(xy.shape) * eps
resolution = 20
# Rescaling the unit of area so that our parameter estimates
# are easier to read
area_per_cell = resolution**2 / 100
cells_x = int(280 / resolution)
cells_y = int(180 / resolution)
# Creating bin edges for a 2D histogram
quadrat_x = np.linspace(0, 280, cells_x + 1)
quadrat_y = np.linspace(0, 180, cells_y + 1)
# Identifying the midpoints of each grid cell
centroids = np.asarray(list(product(quadrat_x[:-1] + 10, quadrat_y[:-1] + 10)))
cell_counts, _, _ = np.histogram2d(xy[:, 0], xy[:, 1], [quadrat_x, quadrat_y])
cell_counts = cell_counts.ravel().astype(int)
将点分割到不同的单元格并计算单元格中心后,我们可以绘制如下所示的新网格化数据集。
line_kwargs = {"color": "k", "linewidth": 1, "alpha": 0.5}
plt.figure(figsize=(6, 4.5))
[plt.axhline(y, **line_kwargs) for y in quadrat_y]
[plt.axvline(x, **line_kwargs) for x in quadrat_x]
plt.scatter(data["x"], data["y"], c=data["marks"], s=6)
for i, row in enumerate(centroids):
shifted_row = row - 2
plt.annotate(cell_counts[i], shifted_row, alpha=0.75)
plt.title("Anemone counts per grid cell"), plt.colorbar(label="Anemone size");

我们可以看到所有的计数都相当低,范围从零到五。在准备好所有数据后,我们可以继续在PyMC中编写我们的概率模型。我们将把上述每个单元格的计数\(Y_1,...Y_M\)视为泊松随机变量。
推理#
我们的第一步是为高斯过程的高级参数设置先验分布。这包括协方差函数的长度尺度 \(\rho\) 和GP的常数均值 \(\mu\)。
with pm.Model() as lgcp_model:
mu = pm.Normal("mu", sigma=3)
rho = pm.Uniform("rho", lower=25, upper=300)
variance = pm.InverseGamma("variance", alpha=1, beta=1)
cov_func = variance * pm.gp.cov.Matern52(2, ls=rho)
mean_func = pm.gp.mean.Constant(mu)
接下来,我们通过pm.math.exp
将高斯过程转换为正值过程,并使用每个细胞的面积将强度函数\(\lambda(s)\)转换为速率\(\lambda_i\),这些速率参数化了泊松似然性,用于细胞\(i\)内的计数。
with lgcp_model:
gp = pm.gp.Latent(mean_func=mean_func, cov_func=cov_func)
log_intensity = gp.prior("log_intensity", X=centroids)
intensity = pm.math.exp(log_intensity)
rates = intensity * area_per_cell
counts = pm.Poisson("counts", mu=rates, observed=cell_counts)
模型完全指定后,我们可以使用默认的NUTS采样器从后验分布中进行采样。我还会调整目标接受率以减少分歧的数量。
with lgcp_model:
trace = pm.sample(1000, tune=2000, target_accept=0.95)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [mu, rho, variance, log_intensity_rotated_]
Sampling 4 chains for 2_000 tune and 1_000 draw iterations (8_000 + 4_000 draws total) took 8125 seconds.
There were 2 divergences after tuning. Increase `target_accept` or reparameterize.
There was 1 divergence after tuning. Increase `target_accept` or reparameterize.
解释结果#
对length_scale参数的后验推断有助于理解数据中是否存在长程相关性。我们还可以检查对数强度场的均值,但由于它是在对数尺度上,因此很难直接解释。
az.summary(trace, var_names=["mu", "rho"])
mean | sd | hdi_3% | hdi_97% | mcse_mean | mcse_sd | ess_bulk | ess_tail | r_hat | |
---|---|---|---|---|---|---|---|---|---|
mu | -0.929 | 0.642 | -2.186 | 0.293 | 0.012 | 0.009 | 2825.0 | 2025.0 | 1.0 |
rho | 243.612 | 44.685 | 161.203 | 299.971 | 0.658 | 0.470 | 4083.0 | 2785.0 | 1.0 |
我们还对在空间中大量新点处查看强度场的值感兴趣。我们可以在模型中通过包含一个新的随机变量来适应这一点,该变量用于在更密集的一组点上评估潜在的高斯过程。使用sample_posterior_predictive
,我们在包含在变量intensity_new
中的新数据点上生成后验预测。
x_new = np.linspace(5, 275, 20)
y_new = np.linspace(5, 175, 20)
xs, ys = np.asarray(np.meshgrid(x_new, y_new))
xy_new = np.asarray([xs.ravel(), ys.ravel()]).T
with lgcp_model:
intensity_new = gp.conditional("log_intensity_new", Xnew=xy_new)
spp_trace = pm.sample_posterior_predictive(
trace, var_names=["log_intensity_new"], keep_size=True
)
trace.extend(spp_trace)
intensity_samples = np.exp(trace.posterior_predictive["log_intensity_new"])
让我们来看几个 \(\lambda(s)\) 的实现。由于样本是在对数尺度上,我们需要对它们进行指数运算以获得我们2D泊松过程的空间强度场。在下图中,观察到的点模式被叠加显示。
fig, axes = plt.subplots(2, 3, figsize=(8, 5), constrained_layout=True)
axes = axes.ravel()
field_kwargs = {"marker": "o", "edgecolor": "None", "alpha": 0.5, "s": 80}
for i in range(6):
field_handle = axes[i].scatter(
xy_new[:, 0], xy_new[:, 1], c=intensity_samples.sel(chain=0, draw=i), **field_kwargs
)
obs_handle = axes[i].scatter(data["x"], data["y"], s=10, color="k")
axes[i].axis("off")
axes[i].set_title(f"Sample {i}")
plt.figlegend(
(obs_handle, field_handle),
("Observed data", r"Posterior draws of $\lambda(s)$"),
ncol=2,
loc=(0.2, -0.01),
fontsize=14,
frameon=False,
);

虽然这些表面显示出一些异质性,但我们获得了一个具有非常清晰定义空间表面的后验平均表面,右上角强度较高,左下角强度较低。
fig = plt.figure(figsize=(5, 4))
plt.scatter(
xy_new[:, 0],
xy_new[:, 1],
c=intensity_samples.mean(("chain", "draw")),
marker="o",
alpha=0.75,
s=100,
edgecolor=None,
)
plt.title("$E[\\lambda(s) \\vert Y]$")
plt.colorbar(label="Posterior mean");

如果存在大量不确定性,我们估计的强度场的空间变化可能没有太大意义。在这种情况下,我们可以绘制后验方差(或标准差)的类似图:
fig = plt.figure(figsize=(5, 4))
plt.scatter(
xy_new[:, 0],
xy_new[:, 1],
c=intensity_samples.var(("chain", "draw")),
marker="o",
alpha=0.75,
s=100,
edgecolor=None,
)
plt.title("$Var[\\lambda(s) \\vert Y]$"), plt.colorbar();

后验方差在域的中间最低,在角落和边缘最大。这是有道理的——在有更多数据的地方,我们对强度场的可能值有更准确的估计。
水印#
%load_ext watermark
%watermark -n -u -v -iv -w
Last updated: Wed Jun 01 2022
Python implementation: CPython
Python version : 3.9.10
IPython version : 8.1.1
matplotlib: 3.5.1
arviz : 0.12.0
numpy : 1.22.2
pymc : 4.0.0b6
pandas : 1.4.1
sys : 3.9.10 | packaged by conda-forge | (main, Feb 1 2022, 21:24:11)
[GCC 9.4.0]
Watermark: 2.3.0