Skip to content

Commit f792cde

Browse files
authored
Update log Gaussian Cox process notebook (#830)
* Update LGCP notebook * Include Upton reference in .bib * fix typos * Fix typos in myst * Fix grammar
1 parent 1e9b421 commit f792cde

File tree

3 files changed

+144
-143
lines changed

3 files changed

+144
-143
lines changed

examples/gaussian_processes/log-gaussian-cox-process.ipynb

Lines changed: 100 additions & 118 deletions
Large diffs are not rendered by default.

examples/gaussian_processes/log-gaussian-cox-process.myst.md

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ jupytext:
55
format_name: myst
66
format_version: 0.13
77
kernelspec:
8-
display_name: Python 3 (ipykernel)
8+
display_name: PyMC Examples
99
language: python
10-
name: python3
10+
name: pymc-examples
1111
---
1212

1313
(log-gaussian-cox-process)=
1414
# Modeling spatial point patterns with a marked log-Gaussian Cox process
1515

16-
:::{post} May 31, 2022
16+
:::{post} December 31, 2025
1717
:tags: cox process, latent gaussian process, nonparametric, spatial, count data
1818
:category: intermediate
19-
:author: Chrisopher Krapu, Chris Fonnesbeck
19+
:author: Christopher Krapu, Chris Fonnesbeck
2020
:::
2121

2222
+++
@@ -31,22 +31,23 @@ In more formal terms, if we have a space $X$ and $A\subseteq X$, the distributio
3131
$$Y_A \sim Poisson\left(\int_A \lambda(s) ds\right)$$
3232
and the intensity field is defined as
3333
$$\log \lambda(s) \sim GP(\mu(s), K(s,s'))$$
34-
where $GP(\mu(s), K(s,s'))$ denotes a Gaussian process with mean function $\mu(s)$ and covariance kernel $K(s,s')$ for a location $s \in X$. This is one of the simplest models of point patterns of $n$ events recorded as locations $s_1,...,s_n$ in an arbitrary metric space. In conjunction with a Bayesian analysis, this model can be used to answering questions of interest such as:
34+
where $GP(\mu(s), K(s,s'))$ denotes a Gaussian process with mean function $\mu(s)$ and covariance kernel $K(s,s')$ for a location $s \in X$. This is one of the simplest models of point patterns of $n$ events recorded as locations $s_1,...,s_n$ in an arbitrary metric space. In conjunction with a Bayesian analysis, this model can be used to answer questions of interest such as:
3535
* Does an observed point pattern imply a statistically significant shift in spatial intensities?
3636
* What would randomly sampled patterns with the same statistical properties look like?
3737
* Is there a statistical correlation between the *frequency* and *magnitude* of point events?
3838

39-
In this notebook, we'll use a grid-based approximation to the full LGCP with PyMC to fit a model and analyze its posterior summaries. We will also explore the usage of a marked Poisson process, an extension of this model to account for the distribution of *marks* associated with each data point.
39+
In this notebook, we'll use a grid-based approximation to the full LGCP with PyMC to fit a model and analyze its posterior summaries. We will also explore the use of a marked Poisson process, an extension of this model to account for the distribution of *marks* associated with each data point.
4040

4141
+++
4242

4343
## Data
4444

4545
+++
4646

47-
Our observational data concerns 231 sea anemones whose sizes and locations on the French coast were recorded. This data was taken from the [`spatstat` spatial modeling package in R](https://github.com/spatstat/spatstat) which is designed to address models like the LGCP and its subsequent refinements. The original source of this data is the textbook *Spatial data analysis by example* by Upton and Fingleton (1985) and a longer description of the data can be found there.
47+
Our observational data concerns 231 sea anemones whose sizes and locations on the French coast were recorded. These data were taken from the [`spatstat` spatial modeling package in R](https://github.com/spatstat/spatstat), which is designed to address models like the LGCP and its subsequent refinements. The original source of this data is the textbook *Spatial data analysis by example* by Upton and Fingleton {cite}`upton1985spatial` and a longer description of the data can be found there.
4848

4949
```{code-cell} ipython3
50+
import os
5051
import warnings
5152
5253
from itertools import product
@@ -57,19 +58,19 @@ import numpy as np
5758
import pandas as pd
5859
import pymc as pm
5960
60-
from matplotlib import MatplotlibDeprecationWarning
6161
from numpy.random import default_rng
62-
63-
warnings.filterwarnings(action="ignore", category=MatplotlibDeprecationWarning)
6462
```
6563

6664
```{code-cell} ipython3
6765
%config InlineBackend.figure_format = 'retina'
66+
RANDOM_SEED = 827
67+
rng = default_rng(RANDOM_SEED)
6868
az.style.use("arviz-darkgrid")
6969
```
7070

7171
```{code-cell} ipython3
7272
data = pd.read_csv(pm.get_data("anemones.csv"))
73+
7374
n = data.shape[0]
7475
```
7576

@@ -91,7 +92,7 @@ The 'marks' column indicates the size of each anemone. If we were to model both
9192

9293
+++
9394

94-
While there are multiple ways to conduct inference, perhaps the simplest way is to slice up our domain $X$ into many small pieces $A_1, A_2,...,A_M$ and fix the intensity field to be constant within each subset. Then, we will treat the number of points within each $A_j$ as a Poisson random variable such that $Y_j \sim Poisson(\lambda_j)$. and we also consider the $\log{\lambda_1}...,\log{\lambda_M}$ variables as a single draw from a Gaussian process.
95+
While there are multiple ways to conduct inference, perhaps the simplest way is to slice up our domain $X$ into many small pieces $A_1, A_2,...,A_M$ and fix the intensity field to be constant within each subset. Then, we will treat the number of points within each $A_j$ as a Poisson random variable such that $Y_j \sim Poisson(\lambda_j)$, and we also consider the $\log{\lambda_1}...,\log{\lambda_M}$ variables as a single draw from a Gaussian process.
9596

9697
+++
9798

@@ -103,7 +104,6 @@ xy = data[["x", "y"]].values
103104
# Jitter the data slightly so that none of the points fall exactly
104105
# on cell boundaries
105106
eps = 1e-3
106-
rng = default_rng()
107107
xy = xy.astype("float") + rng.standard_normal(xy.shape) * eps
108108
109109
resolution = 20
@@ -154,7 +154,9 @@ We can see that all of the counts are fairly low and range from zero to five. Wi
154154
Our first step is to place prior distributions over the high-level parameters for the Gaussian process. This includes the length scale $\rho$ for the covariance function and a constant mean $\mu$ for the GP.
155155

156156
```{code-cell} ipython3
157-
with pm.Model() as lgcp_model:
157+
coords = {"cell": np.arange(cell_counts.size)}
158+
159+
with pm.Model(coords=coords) as lgcp_model:
158160
mu = pm.Normal("mu", sigma=3)
159161
rho = pm.Uniform("rho", lower=25, upper=300)
160162
variance = pm.InverseGamma("variance", alpha=1, beta=1)
@@ -168,18 +170,18 @@ Next, we transform the Gaussian process into a positive-valued process via `pm.m
168170
with lgcp_model:
169171
gp = pm.gp.Latent(mean_func=mean_func, cov_func=cov_func)
170172
171-
log_intensity = gp.prior("log_intensity", X=centroids)
173+
log_intensity = gp.prior("log_intensity", X=centroids, dims="cell")
172174
intensity = pm.math.exp(log_intensity)
173175
174176
rates = intensity * area_per_cell
175-
counts = pm.Poisson("counts", mu=rates, observed=cell_counts)
177+
counts = pm.Poisson("counts", mu=rates, observed=cell_counts, dims="cell")
176178
```
177179

178180
With the model fully specified, we can start sampling from the posterior using the default NUTS sampler. I'll also tweak the target acceptance rate to reduce the number of divergences.
179181

180182
```{code-cell} ipython3
181183
with lgcp_model:
182-
trace = pm.sample(1000, tune=2000, target_accept=0.95)
184+
idata = pm.sample(return_inferencedata=True, mp_ctx="spawn")
183185
```
184186

185187
# Interpreting the results
@@ -189,7 +191,7 @@ with lgcp_model:
189191
Posterior inference on the length_scale parameter is useful for understanding whether or not there are long-range correlations in the data. We can also examine the mean of the log-intensity field, but since it is on the log scale it is hard to directly interpret.
190192

191193
```{code-cell} ipython3
192-
az.summary(trace, var_names=["mu", "rho"])
194+
az.summary(idata, var_names=["mu", "rho"])
193195
```
194196

195197
We are also interested in looking at the value of the intensity field at a large number of new points in space. We can accommodate this within our model by including a new random variable for the latent Gaussian process evaluated at a denser set of points. Using `sample_posterior_predictive`, we generate posterior predictions on new data points contained in the variable `intensity_new`.
@@ -201,14 +203,18 @@ xs, ys = np.asarray(np.meshgrid(x_new, y_new))
201203
xy_new = np.asarray([xs.ravel(), ys.ravel()]).T
202204
203205
with lgcp_model:
204-
intensity_new = gp.conditional("log_intensity_new", Xnew=xy_new)
205-
206-
spp_trace = pm.sample_posterior_predictive(
207-
trace, var_names=["log_intensity_new"], keep_size=True
206+
lgcp_model.add_coord("cell_new", np.arange(xy_new.shape[0]))
207+
intensity_new = gp.conditional("log_intensity_new", Xnew=xy_new, dims="cell_new")
208+
209+
idata.extend(
210+
pm.sample_posterior_predictive(
211+
idata,
212+
var_names=["log_intensity_new"],
213+
random_seed=RANDOM_SEED,
214+
)
208215
)
209216
210-
trace.extend(spp_trace)
211-
intensity_samples = np.exp(trace.posterior_predictive["log_intensity_new"])
217+
intensity_samples = np.exp(idata.posterior_predictive["log_intensity_new"])
212218
```
213219

214220
Let's take a look at a few realizations of $\lambda(s)$. Since the samples are on the log scale, we'll need to exponentiate them to obtain the spatial intensity field of our 2D Poisson process. In the plot below, the observed point pattern is overlaid.
@@ -219,9 +225,10 @@ axes = axes.ravel()
219225
220226
field_kwargs = {"marker": "o", "edgecolor": "None", "alpha": 0.5, "s": 80}
221227
222-
for i in range(6):
228+
num_draws = min(6, intensity_samples.sizes.get("draw", 0))
229+
for i in range(num_draws):
223230
field_handle = axes[i].scatter(
224-
xy_new[:, 0], xy_new[:, 1], c=intensity_samples.sel(chain=0, draw=i), **field_kwargs
231+
xy_new[:, 0], xy_new[:, 1], c=intensity_samples.isel(chain=0, draw=i), **field_kwargs
225232
)
226233
227234
obs_handle = axes[i].scatter(data["x"], data["y"], s=10, color="k")
@@ -282,6 +289,12 @@ The posterior variance is lowest in the middle of the domain and largest in the
282289

283290
- This notebook was written by [Christopher Krapu](https://github.com/ckrapu) on September 6, 2020 and updated on April 1, 2021.
284291
- Updated by Chris Fonnesbeck on May 31, 2022 for v4 compatibility.
292+
- Updated by Christopher Krapu on December 31, 2025 for v5 compatibility.
293+
294+
## References
295+
:::{bibliography}
296+
:filter: docname in docnames
297+
:::
285298

286299
## Watermark
287300

examples/references.bib

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,12 @@ @book{train2009
890890
year = {2009},
891891
publisher = {Cambridge}
892892
}
893+
@book{upton1985spatial,
894+
title = {Spatial Data Analysis by Example},
895+
author = {Upton, Graham J. G. and Fingleton, Bernard},
896+
year = {1985},
897+
publisher = {Wiley}
898+
}
893899
@online{vandenbergSPSS,
894900
author = {van den Berg, R. G},
895901
title = {SPSS Moderation Regression Tutorial},

0 commit comments

Comments
 (0)