| Title: | Bayesian Spatial Functional Clustering |
|---|---|
| Description: | Bayesian clustering of spatial regions with similar functional shapes using spanning trees and latent Gaussian models. The method enforces spatial contiguity within clusters and supports a wide range of latent Gaussian models, including non-Gaussian likelihoods, via the R-INLA framework. The algorithm is based on Zhong, R., Chacón-Montalván, E. A., and Moraga, P. (2026) <doi:10.1002/sim.70597>, extending the approach of Zhang, B., Sang, H., Luo, Z. T., and Huang, H. (2023) <doi:10.1214/22-AOAS1643>. The package includes tools for model fitting, convergence diagnostics, visualization, and summarization of clustering results. |
| Authors: | Erick A. Chacón-Montalván [aut, cre] (ORCID: <https://orcid.org/0000-0001-8068-1034>), Ruiman Zhong [aut] (ORCID: <https://orcid.org/0000-0002-4681-9199>), Paula Moraga [aut] (ORCID: <https://orcid.org/0000-0001-5266-0201>) |
| Maintainer: | Erick A. Chacón-Montalván <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 1.1.1 |
| Built: | 2026-09-24 10:20:08 UTC |
| Source: | https://github.com/erickchacon/sfclust |
A stars raster object containing the Normalized Difference Water Index 2 (NDWI2)
derived from Sentinel-2 satellite imagery over El Chaparrillo, an agricultural
research center in Spain, across 86 time points.
data(chapa)data(chapa)
A stars object with:
NDWI2 values (numeric) derived from Sentinel-2 bands. Non-NA values define the study area mask.
Three dimensions: x (26 columns), y (27 rows),
time (86 dates from 2025-05-15 to 2025-12-01). CRS: WGS 84 / UTM zone 30N.
library(sfclust) data(chapa) chapa plot(chapa["ndwi2"])library(sfclust) data(chapa) chapa plot(chapa["ndwi2"])
Convert spatio-temporal data to long format with a flat spatial index (ids) and
ordered observation indices (id_<dimname> for each non-spatial dimension).
This is a pure converter: all rows are returned including cells that are all-NA.
No filtering is applied. Use filter_df() downstream to restrict to valid cells.
data_all(x, spnames = "geometry")data_all(x, spnames = "geometry")
x |
A |
spnames |
Character vector with the names of the spatial dimensions of |
A long-format data frame with columns id (flat array position in the original
stars object, column-major over all dimensions), ids (flat spatial index,
column-major over spnames, spnames[1] varies fastest), id_<dimname> for
each functional dimension, and all variables from x. All rows are
returned; ids is not remapped to 1..n_valid.
library(sfclust) library(stars) dims <- st_dimensions( geometry = st_sfc(lapply(1:5, function(i) st_point(c(i, i)))), time = seq(as.Date("2024-01-01"), by = "1 day", length.out = 3) ) stdata <- st_as_stars(cases = array(1:15, dim = c(5, 3)), dimensions = dims) data_all(stdata)library(sfclust) library(stars) dims <- st_dimensions( geometry = st_sfc(lapply(1:5, function(i) st_point(c(i, i)))), time = seq(as.Date("2024-01-01"), by = "1 day", length.out = 3) ) stdata <- st_as_stars(cases = array(1:15, dim = c(5, 3)), dimensions = dims) data_all(stdata)
Keeps only rows whose spatial index (ids) appears in valid_ids and adds
a sid column (1..n_valid) giving each valid cell's rank among the valid
cells. sid matches the indexing of membership vectors returned by
genclust(). If valid_ids is NULL, all rows are kept and sid is
assigned by rank of ids.
filter_df(df, valid_ids = NULL)filter_df(df, valid_ids = NULL)
df |
A long-format data frame as returned by |
valid_ids |
Integer vector of valid flat spatial positions, as returned
in |
A filtered data frame with rows corresponding to valid spatial cells only,
with an additional sid column (integer, 1..n_valid).
sfclust ObjectsThis function calculates the fitted values for a specific clustering sample in an
sfclust object, based on the estimated models for each cluster. The fitted
values are computed using the membership assignments and model parameters
associated with the selected clustering sample.
## S3 method for class 'sfclust' fitted(object, sample = object$clust$id, sort = FALSE, aggregate = FALSE, ...)## S3 method for class 'sfclust' fitted(object, sample = object$clust$id, sort = FALSE, aggregate = FALSE, ...)
object |
An object of class 'sfclust', containing clustering results and models. |
sample |
An integer specifying the clustering sample number for which
the fitted values should be computed. The default is the |
sort |
Logical value indicating if clusters should be relabel based on number of elements. |
aggregate |
Logical value indicating if fitted values are desired at cluster
level. Only supported for |
... |
Additional arguments, currently not used. |
A data frame with fitted values and cluster assignments, keyed by id.
For sfclust_stars objects, a stars object is returned instead.
if (requireNamespace("INLA", quietly = TRUE)) { library(sfclust) data(stgaus) result <- sfclust(stgaus, formula = y ~ f(id_time, model = "rw1"), niter = 10, nmessage = 1) # Estimated values ordering clusters by size df_est <- fitted(result, sort = TRUE) # Estimated values aggregated by cluster df_est <- fitted(result, aggregate = TRUE) # Estimated values using a particular clustering sample df_est <- fitted(result, sample = 3) }if (requireNamespace("INLA", quietly = TRUE)) { library(sfclust) data(stgaus) result <- sfclust(stgaus, formula = y ~ f(id_time, model = "rw1"), niter = 10, nmessage = 1) # Estimated values ordering clusters by size df_est <- fitted(result, sort = TRUE) # Estimated values aggregated by cluster df_est <- fitted(result, aggregate = TRUE) # Estimated values using a particular clustering sample df_est <- fitted(result, sample = 3) }
Creates an undirected graph from spatial data or a weighted adjacency matrix,
computes its minimum spanning tree (MST), and partitions it into nclust clusters.
Accepts weighted matrix or Matrix objects, and stars objects with either
vector geometry (contiguity via sf::st_touches()) or raster dimensions
(4-connected grid adjacency for non-NA pixels).
genclust(x, ...) ## Default S3 method: genclust(x, ...) ## S3 method for class 'matrix' genclust(x, nclust = 10, weights = NULL, ...) ## S3 method for class 'Matrix' genclust(x, nclust = 10, weights = NULL, ...) ## S3 method for class 'stars' genclust(x, nclust = 10, spnames = NULL, response = NULL, weights = NULL, ...)genclust(x, ...) ## Default S3 method: genclust(x, ...) ## S3 method for class 'matrix' genclust(x, nclust = 10, weights = NULL, ...) ## S3 method for class 'Matrix' genclust(x, nclust = 10, weights = NULL, ...) ## S3 method for class 'stars' genclust(x, nclust = 10, spnames = NULL, response = NULL, weights = NULL, ...)
x |
Spatial data or adjacency matrix. Accepted classes: |
... |
Not used; required for S3 method consistency. |
nclust |
Integer. Number of initial clusters (default |
weights |
Optional numeric vector of edge weights with |
spnames |
Character vector with the names of the spatial dimensions. If
|
response |
Character. Name of the attribute in |
A list with:
graph: undirected igraph object representing spatial contiguity.
mst: minimum spanning tree of graph.
membership: integer vector of cluster assignments (length = number of valid spatial units).
valid_ids: integer vector of flat spatial positions included in the graph
(only present for stars input; NULL for matrix/Matrix input).
library(sfclust) library(sf) library(stars) # stars object with vector geometry geom <- st_make_grid(cellsize = c(1, 1), offset = c(0, 0), n = c(3, 2)) x <- st_as_stars(st_sf(z = 1:6, geometry = geom)) clust <- genclust(x, nclust = 3) plot(st_sf(geom, cluster = factor(clust$membership))) # stars raster input x <- st_as_stars(cluster = matrix(1:35, 5)) clust <- genclust(x, nclust = 4) x$cluster <- clust$membership plot(x, col = rainbow(4)) # matrix input x <- matrix(c(0,1,0,1, 1,0,1,0, 0,1,0,1, 1,0,1,0), nrow = 4) clust <- genclust(x, nclust = 2) clust$membershiplibrary(sfclust) library(sf) library(stars) # stars object with vector geometry geom <- st_make_grid(cellsize = c(1, 1), offset = c(0, 0), n = c(3, 2)) x <- st_as_stars(st_sf(z = 1:6, geometry = geom)) clust <- genclust(x, nclust = 3) plot(st_sf(geom, cluster = factor(clust$membership))) # stars raster input x <- st_as_stars(cluster = matrix(1:35, 5)) clust <- genclust(x, nclust = 4) x$cluster <- clust$membership plot(x, col = rainbow(4)) # matrix input x <- matrix(c(0,1,0,1, 1,0,1,0, 0,1,0,1, 1,0,1,0), nrow = 4) clust <- genclust(x, nclust = 2) clust$membership
Fit the specified INLA model to each cluster and compute the log marginal likelihood for each cluster specified in the membership vector.
log_mlik_all( membership, data, correction = TRUE, detailed = FALSE, inla_args = NULL )log_mlik_all( membership, data, correction = TRUE, detailed = FALSE, inla_args = NULL )
membership |
Integer, character or factor vector indicating the cluster membership for each spatial unit. |
data |
A long-format data frame as returned by |
correction |
Logical value indicating whether a correction for dispersion. |
detailed |
Logical value indicating whether to return the INLA model instead of
the log marginal likelihood. The argument |
inla_args |
A named list or pairlist of arguments passed to |
A numeric vector containing the log marginal likelihood for each cluster or the
the fitted INLA model for each cluster when detailed = TRUE.
Plots the estimated mean functional shape (linear predictor or inverse-link scale) for
each cluster in a given MCMC sample of an sfclust object.
plot_clusters_fitted( x, sample = x$clust$id, clusters = NULL, sort = FALSE, legend = FALSE, inv_link = TRUE, fnames = NULL, ... )plot_clusters_fitted( x, sample = x$clust$id, clusters = NULL, sort = FALSE, legend = FALSE, inv_link = TRUE, fnames = NULL, ... )
x |
An |
sample |
Integer specifying the clustering sample to display. Defaults to the last sample. |
clusters |
Optional vector of cluster IDs to include. If |
sort |
Logical; if |
legend |
Logical; if |
inv_link |
Logical; if |
fnames |
Character. Name of the column to use as the x-axis (functional dimension).
If |
... |
Additional arguments passed to |
A ggplot2 object.
Produces a ggplot2 map of spatial regions colored by their cluster assignment for a
given MCMC sample of an sfclust_stars object.
plot_clusters_map( x, sample = x$clust$id, clusters = NULL, sort = FALSE, legend = FALSE, geom_before = NULL, ... )plot_clusters_map( x, sample = x$clust$id, clusters = NULL, sort = FALSE, legend = FALSE, geom_before = NULL, ... )
x |
An |
sample |
Integer specifying the clustering sample to display. Defaults to the last sample. |
clusters |
Optional vector of cluster IDs to include. If |
sort |
Logical; if |
legend |
Logical; if |
geom_before |
An optional |
... |
Additional arguments passed to |
A ggplot2 object.
Plots individual region time series faceted by cluster, overlaid with the cluster mean,
for a given variable in an sfclust object.
plot_clusters_series(x, var, clusters = NULL, sort = FALSE, fnames = NULL, ...)plot_clusters_series(x, var, clusters = NULL, sort = FALSE, fnames = NULL, ...)
x |
An |
var |
An unquoted variable name to plot on the y-axis. |
clusters |
Optional vector of cluster IDs to include. If |
sort |
Logical; if |
fnames |
Character. Name of the column to use as the x-axis (functional dimension).
If |
... |
Additional arguments passed to |
A ggplot2 object with one facet per cluster.
Plots the log marginal likelihood across MCMC samples for an sfclust object,
highlighting the selected sample.
plot_log_mlik(x, sample = x$clust$id, ...)plot_log_mlik(x, sample = x$clust$id, ...)
x |
An |
sample |
Integer specifying the clustering sample to highlight. Defaults to the last sample. |
... |
Additional arguments passed to |
A ggplot2 object.
sfclust objectsVisualizes fitted cluster functions (plot 1) and a log marginal likelihood traceplot
(plot 2). For sfclust_stars objects, also includes a spatial map of cluster
assignments (plot 1 in that method, shifting the others to 2 and 3).
## S3 method for class 'sfclust' plot( x, sample = x$clust$id, which = 1:2, clusters = NULL, sort = FALSE, legend = FALSE, fnames = NULL, ... )## S3 method for class 'sfclust' plot( x, sample = x$clust$id, which = 1:2, clusters = NULL, sort = FALSE, legend = FALSE, fnames = NULL, ... )
x |
An |
sample |
Integer specifying the clustering sample to display. Defaults to the last sample. |
which |
Integer vector indicating which plots to show. For |
clusters |
Optional vector of cluster IDs to include. If |
sort |
Logical; if |
legend |
Logical; if |
fnames |
Character. Column name for the x-axis of cluster function plots.
If |
... |
Additional arguments passed to underlying plot functions. |
A composed patchwork object.
Prints details of an sfclust object, including the (i) within-cluster formula; (ii) hyperparameters used for the MCMC sample such as the number of clusters penalty (q) and the movement probabilities (move_prob); (iii) the number of movement type counts during the MCMC sampling; and (iv) the log marginal likelihood of the model of the last clustering sample.
## S3 method for class 'sfclust' print(x, ...)## S3 method for class 'sfclust' print(x, ...)
x |
An object of class 'sfclust'. |
... |
Additional arguments passed to |
Invisibly returns the input sfclust object x. The function also
prints a summary of:
the within-cluster model formula,
clustering hyperparameters,
movement counts from the MCMC sampler,
and the log marginal likelihood of the selected sample.
sfclust() is the main user-facing function for Bayesian spatial functional
clustering via reversible-jump MCMC. It dispatches on the class of the first
argument:
sfclust(x, ...) ## Default S3 method: sfclust(x, ...) ## S3 method for class 'data.frame' sfclust( x, adjacency, graphdata = NULL, fnames = NULL, nclust = 10, move_prob = c(0.425, 0.425, 0.1, 0.05), logpen = log(1 - 0.5), correction = TRUE, niter = 100, burnin = 0, thin = 1, nmessage = 10, path_save = NULL, nsave = nmessage, ... ) ## S3 method for class 'stars' sfclust( x, nclust = 10, graphdata = NULL, spnames = NULL, move_prob = c(0.425, 0.425, 0.1, 0.05), logpen = log(1 - 0.5), correction = TRUE, niter = 100, burnin = 0, thin = 1, nmessage = 10, path_save = NULL, nsave = nmessage, ... )sfclust(x, ...) ## Default S3 method: sfclust(x, ...) ## S3 method for class 'data.frame' sfclust( x, adjacency, graphdata = NULL, fnames = NULL, nclust = 10, move_prob = c(0.425, 0.425, 0.1, 0.05), logpen = log(1 - 0.5), correction = TRUE, niter = 100, burnin = 0, thin = 1, nmessage = 10, path_save = NULL, nsave = nmessage, ... ) ## S3 method for class 'stars' sfclust( x, nclust = 10, graphdata = NULL, spnames = NULL, move_prob = c(0.425, 0.425, 0.1, 0.05), logpen = log(1 - 0.5), correction = TRUE, niter = 100, burnin = 0, thin = 1, nmessage = 10, path_save = NULL, nsave = nmessage, ... )
x |
A |
... |
Additional arguments such as |
adjacency |
A square weighted adjacency matrix (ns × ns) encoding spatial
contiguity and edge weights. Can be a dense |
graphdata |
A list with components |
fnames |
Character. Name of the column in |
nclust |
Integer. Initial number of clusters when |
move_prob |
A numeric vector of probabilities for the MCMC move types: birth,
death, change, and hyperparameter (default is |
logpen |
A negative numeric value representing the log-scale penalty for
increasing the number of clusters by one. The number of clusters is assumed to
follow a geometric prior with probability |
correction |
A logical indicating whether correction to compute the marginal
likelihoods should be applied (default is |
niter |
An integer specifying the number of MCMC iterations after burn-in
(default is |
burnin |
An integer specifying the number of burn-in iterations to discard
(default is |
thin |
An integer specifying the thinning interval for recording the results
(default is |
nmessage |
An integer specifying how often progress messages should be printed
(default is |
path_save |
A character string specifying the file path to save the results
(default is |
nsave |
An integer specifying the number of iterations between saved results
(default is |
spnames |
Character vector with the names of the spatial dimensions of |
sfclust.data.frame(): core interface — takes a pre-built long-format data frame
and a weighted adjacency matrix. Use this when working with any data format
after converting it yourself.
sfclust.stars(): stars wrapper — takes a stars spatio-temporal object,
converts it to long format, builds the spatial graph, and calls the core algorithm.
This implementation draws inspiration from the methods described in the paper: "Bayesian Clustering of Spatial Functional Data with Application to a Human Mobility Study During COVID-19" by Bohai Zhang, Huiyan Sang, Zhao Tang Luo, and Hui Huang, published in The Annals of Applied Statistics, 2023. For further details on the methodology, please refer to:
The paper: doi:10.1214/22-AOAS1643
Supplementary material: doi:10.1214/22-AOAS1643SUPPB
The MCMC algorithm in this implementation is largely based on the supplementary material provided in the paper. However, we have generalized the computation of the marginal likelihood ratio by leveraging INLA (Integrated Nested Laplace Approximation). This generalization enables integration over all parameters and hyperparameters, allowing for inference within a broader family of distribution functions and model terms, thereby extending the scope and flexibility of the original approach. Further details of our approach can be found in our paper "Bayesian spatial functional data clustering: applications in disease surveillance" by Ruiman Zhong, Erick A. Chacón-Montalván, Paula Moraga:
The paper: doi:10.1002/sim.70597
An sfclust object (from sfclust.data.frame) or an sfclust_stars object
inheriting from sfclust (from sfclust.stars). Both contain:
samples: MCMC trace with membership, log_mlike, and move_counts.
clust: selected clustering with id, membership, and fitted models.
sfclust_stars additionally carries input_args with stars (structural shell
of the input), spnames, and fnames, used by spatial plot methods.
Ruiman Zhong [email protected], Erick A. Chacón-Montalván [email protected], Paula Moraga [email protected]
if (requireNamespace("INLA", quietly = TRUE)) { library(sfclust) # Stars interface: Gaussian model data(stgaus) result <- sfclust(stgaus, formula = y ~ f(id_time, model = "rw1"), niter = 10, nmessage = 1) print(result) summary(result, sort = TRUE) fitted(result, sort = TRUE) plot(result) plot_clusters_series(result, var = y) result2 <- update(result, niter = 2, nmessage = 1) plot(result2) # Stars interface: Binomial model data(stbinom) result <- sfclust(stbinom, formula = cases ~ poly(id_time, 2) + f(id), family = "binomial", Ntrials = population, niter = 10, nmessage = 1) print(result) summary(result, sort = TRUE) fitted(result, sort = TRUE) plot(result) plot_clusters_series(result, var = cases/population) result2 <- update(result, niter = 2, nmessage = 1) plot(result2) # data.frame interface: Poisson model ns <- 6L; nt <- 4L set.seed(4) df <- data.frame( id = seq_len(ns * nt), ids = rep(seq_len(ns), nt), id_time = rep(seq_len(nt), each = ns), expected = rep(10L, ns * nt) ) df <- transform(df, y = rpois(ns * nt, expected * exp(0.5 * id_time * rep(c(-1, 1), each = ns / 2, nt))) ) adj <- Matrix::sparseMatrix( i = c(1, 2, 4, 5, 1, 2, 3), j = c(2, 3, 5, 6, 4, 5, 6), x = 1L, dims = c(ns, ns), symmetric = TRUE ) result <- sfclust(df, adjacency = adj, nclust = 3, fnames = "id_time", formula = y ~ 1 + id_time, family = "poisson", E = expected, niter = 3, burnin = 0, thin = 1, nmessage = 1) print(result) summary(result, sort = TRUE) fitted(result, sort = TRUE) plot(result) plot_clusters_series(result, var = y) result2 <- update(result, niter = 2, nmessage = 1) plot(result2) }if (requireNamespace("INLA", quietly = TRUE)) { library(sfclust) # Stars interface: Gaussian model data(stgaus) result <- sfclust(stgaus, formula = y ~ f(id_time, model = "rw1"), niter = 10, nmessage = 1) print(result) summary(result, sort = TRUE) fitted(result, sort = TRUE) plot(result) plot_clusters_series(result, var = y) result2 <- update(result, niter = 2, nmessage = 1) plot(result2) # Stars interface: Binomial model data(stbinom) result <- sfclust(stbinom, formula = cases ~ poly(id_time, 2) + f(id), family = "binomial", Ntrials = population, niter = 10, nmessage = 1) print(result) summary(result, sort = TRUE) fitted(result, sort = TRUE) plot(result) plot_clusters_series(result, var = cases/population) result2 <- update(result, niter = 2, nmessage = 1) plot(result2) # data.frame interface: Poisson model ns <- 6L; nt <- 4L set.seed(4) df <- data.frame( id = seq_len(ns * nt), ids = rep(seq_len(ns), nt), id_time = rep(seq_len(nt), each = ns), expected = rep(10L, ns * nt) ) df <- transform(df, y = rpois(ns * nt, expected * exp(0.5 * id_time * rep(c(-1, 1), each = ns / 2, nt))) ) adj <- Matrix::sparseMatrix( i = c(1, 2, 4, 5, 1, 2, 3), j = c(2, 3, 5, 6, 4, 5, 6), x = 1L, dims = c(ns, ns), symmetric = TRUE ) result <- sfclust(df, adjacency = adj, nclust = 3, fnames = "id_time", formula = y ~ 1 + id_time, family = "poisson", E = expected, niter = 3, burnin = 0, thin = 1, nmessage = 1) print(result) summary(result, sort = TRUE) fitted(result, sort = TRUE) plot(result) plot_clusters_series(result, var = y) result2 <- update(result, niter = 2, nmessage = 1) plot(result2) }
A simulated stars object containing binomial response data with a functional clustering
pattern defined by polynomial fixed effects. This dataset includes the variables cases
and population observed across 100 simulated spatial regions over 91 time points.
data(stbinom)data(stbinom)
A stars object with:
Number of observed cases (integer)
Population at risk (integer)
Two dimensions: geometry (spatial features) and time (daily observations)
library(sfclust) data(stbinom) stbinom plot(stbinom["cases"])library(sfclust) data(stbinom) stbinom plot(stbinom["cases"])
A simulated stars object containing Gaussian response data with a functional
clustering pattern uging random walk processes. This dataset includes the response
variable y observed across 100 simulated spatial regions over 91 time points.
data(stgaus)data(stgaus)
A stars object with:
Response variable
library(sfclust) data(stgaus) stgaus plot(stgaus["y"])library(sfclust) data(stgaus) stgaus plot(stgaus["y"])
This function summarizes the cluster assignments from the desired clustering sample.
## S3 method for class 'sfclust' summary(object, sample = object$clust$id, sort = FALSE, ...)## S3 method for class 'sfclust' summary(object, sample = object$clust$id, sort = FALSE, ...)
object |
An object of class 'sfclust'. |
sample |
An integer specifying the clustering sample number to be summarized (default is the last sample). |
sort |
Logical value indicating if clusters should be relabel based on number of elements. |
... |
Additional arguments passed to |
Invisibly returns a table with the number of regions in each cluster for the selected sample. The function also prints a summary that includes:
the within-cluster model formula,
the total number of MCMC clustering samples,
the cluster membership counts for the specified sample (optionally sorted),
and the log marginal likelihood of the selected clustering sample.
This function continues the MCMC sampling of a sfclust object based on previous results or
update the model fitting for a specified sample clustering if the argument sample is
provided.
## S3 method for class 'sfclust' update( object, niter = NULL, burnin = 0, thin = 1, nmessage = 10, sample = NULL, path_save = NULL, nsave = nmessage, ... )## S3 method for class 'sfclust' update( object, niter = NULL, burnin = 0, thin = 1, nmessage = 10, sample = NULL, path_save = NULL, nsave = nmessage, ... )
object |
A |
niter |
An integer specifying the number of additional MCMC iterations to perform.
If |
burnin |
An integer specifying the number of burn-in iterations to discard. |
thin |
An integer specifying the thinning interval for recording results. |
nmessage |
An integer specifying the number of messages to display during the process. |
sample |
An integer specifying the clustering sample number to be executed.
The default is the last sample (i.e., |
path_save |
A character string specifying the file path to save the results. If
|
nsave |
An integer specifying how often to save results. Defaults to |
... |
Additional arguments (currently not used). |
This function takes the last state of the Markov chain from a previous
sfclust_fit / sfclust execution and uses it as the starting point for
additional MCMC iterations. If niter = NULL (the default), no additional
iterations are run; instead, the within-cluster INLA models are fitted for
the clustering given by sample (or the current clust$id if sample is
NULL). This is useful when loading a checkpoint saved during a run, where
the models were not yet stored: result <- update(result).
An updated sfclust object with (i) new clustering samples if sample is not
specified, or (ii) updated within-cluster model results if sample is given.