Aggregates an sf point dataset into one row per cell. By default computes counts and means, but the aggregation function is configurable.
Arguments
- assigned_points_sf
An sf object with a cell identifier column.
- response_var
Optional response column name for per-cell aggregation: a single character string. A name that is not a column, or a column that is not numeric, is skipped with a warning.
- predictor_vars
Optional predictor column names for per-cell aggregation. Names that are not columns, and columns that are not numeric, are skipped with a warning.
- id_col
Preferred name of the polygon/cell ID column.
- agg_funs
Named list of aggregation functions. Default
list(mean = \(x) mean(x, na.rm = TRUE)). Additional common options:median,sum,sd. A single function (agg_funs = median) or a character vector of function names (c("median", "sum")) is also accepted. A single function is named after the expression passed: a name gives that name (median, orffor a variablefholding a function),stats::mediangivesmedian, and any other expression givesagg1; pass a named list to choose the name. Anything else falls back to the default mean with a warning.- cells_sf
Optional polygon sf layer to join cell geometries onto the output. When supplied, the return value is an sf object with the polygon geometry from cells_sf, with one row per cell in
cells_sf. Cells that no feature fell in are kept, withNAsummaries. Duplicate ID values incells_sfwould multiply those rows, so they are reported with a warning. Its ID column is the first ofid_col,"poly_id","polygon_id","id","cell_id"and"grid_id"it carries, the list and orderassign_features_to_polygons()reads the polygons' IDs from. A summarised ID that matches no cell is reported with a warning, since the join drops it with its points; acells_sfwith none of those columns, or one that is not an sf object, gives a warning and a plain data frame (an error witharea = TRUE). When NULL (default), a plain data.frame/tibble is returned (previous behaviour).- deff
Design-effect adjustment for standard errors. One of:
1(default)No adjustment; the classic IID standard error
sd / sqrt(n), which assumes the observations within a cell are independent. No"deff_applied"attribute is attached, so a result carrying none was computed this way."variogram"Compute a per-cell design effect from a fitted variogram: for
npoints in a cell with correlation matrixR, the effective sample size of the mean isn^2 / sum(R), sodeff = sum(R) / n. This generalises Kish (substituting a constant off-diagonal correlation recovers1 + (n - 1) * rhoexactly) but lets correlation decay with distance, which matters increasingly as cells get larger and Kish's single-rhoassumption degrades. Supply the fit viasac, or it is estimated whenresponse_varis given and 'gstat' is available. Exponential, spherical and Gaussian models are supported, with a nugget and with several structured components (each weighted by its partial sill) and with gstat's 2-D geometric anisotropy (vgm(..., anis = c(angle, ratio))), applied as gstat applies it. A model of any other family falls back todeff = 1with a warning naming it, and so does a request with no usable model (none supplied and none could be estimated, or a rejected fit that could not be replaced by an estimate); see "Value" for how to detect a fallback. A model with no structured component (a pure nugget) implies that distinct observations are uncorrelated, so it is applied as a design effect of 1 in every cell, not treated as a fallback. Points with empty geometry count towards their cells' values but not towards the correlation, with a warning."kish"Estimate per-variable-type intra-class correlations (ICCs) from the grouped data using a one-way random-effects ANOVA decomposition (one ICC for the response variable and a separate ICC for the predictor variables), then apply Kish's formula per cell:
deff_i = 1 + (n_i - 1) * rho. The ICC is the ANOVA (method-of-moments) estimator with Donner'sn0for unequal cell sizes, not the REML estimate a mixed model returns: on a single unbalanced draw the two can differ by 0.1–0.2 (one check with cell sizes 3 to 77 and a true ICC of 0.5 gave 0.33 against REML's 0.51), while balanced designs agree to about 0.01. Neither is wrong, so do not read the difference as a defect. When multiple columns are pooled for a single ICC estimate (e.g. several predictor variables), each column is z-scored before pooling so that variables with different scales contribute equally to the variance decomposition. The response-specific ICC is used for response SEs and the predictor-specific ICC for predictor SEs. The response's ICC is never applied to predictor columns or vice versa, and it is the response's ICC (not the predictors') that setscell_weightwhenever a response was given. Requires at least 2 cells with 2+ observations and at least 2 residual degrees of freedom (N - k >= 2); the ICC is taken as 0 (no correction for that variable type) otherwise, and likewise when the estimate itself comes out at or below 0. The"deff_applied"attribute is attached when either ICC is positive, so when both are 0 there is none.- A positive number
Applied as a uniform design effect to every cell, as
sd * sqrt(deff / n), exactlysqrt(deff)times the naive SE. Use when you have an external estimate of the design effect. Anything that is not a single finite number>= 1(including a value below 1, which would shrink the standard errors, andNAorInf) is refused with a warning and replaced by 1.
- sac
Optional
sac_rangeobject fromestimate_sac_range(), used whendeff = "variogram". Supplying one avoids re-fitting the variogram and lets you inspect the fit the design effect is based on. Asac_rangewhose fit was rejected (its value isNAand it carries arejected_reasonattribute) carries no usable correlation function, so it is set aside rather than correcting by a shape that was not trusted enough to report a range: the variogram is then estimated as if nosachad been given, with a plain warning saying so, or, where that is not possible,defffalls back to 1 with the fallback warning, which names the rejection. Asacwith novariogram_modelattribute – a plain number or aunitsobject, say – is a range without a correlation function, and is set aside the same way. Asacfitted to residuals (attr(sac, "detrended")TRUE) is used as given, with a warning when it corrects response columns (see "Design effects and variable types").- deff_max_n
Cells with more than this many points are subsampled before forming the
n x ncorrelation matrix used bydeff = "variogram". Default 500. It must be a single number of at least 2 whendeff = "variogram"; anything else is an error.- quiet
Logical; suppress this function's progress
message()s. It does not silence R warnings, nor the package's console log echo (seespatialkit_quietfor that). DefaultTRUE, unlike the tessellation functions, whose default isFALSE.- conf_level
Optional confidence level in (0, 1), such as
0.95. When given, every numeric response and predictor column also gets..neff_*,..df_*,..ci_lo_*and..ci_hi_*(see "Confidence intervals"). DefaultNULL: no interval columns, and the frame is exactly what it was before this argument existed.- area
Logical, default
FALSE. WithTRUE, andcells_sfsupplied, the result gainscell_area(each cell's planar area in the squared units ofcells_sf's CRS) andn_per_area(the count of rows in the cell over that area, a point density; a rate of anything else is that thing'sagg_funssum overcell_area). The request is refused with an error, not answered with a number, when the cells' CRS distorts areas across them by more than 1 percent, measured as the spread of planar-to-geodesic area ratios over the cells: a density is a comparison between cells and is meaningless where the map scale differs from one cell to the next. Inside a UTM zone the spread is under 0.3 percent and the request goes through; the conterminous United States forced into one zone (14 percent), or a few degrees of latitude in Web Mercator (4 percent at 48N), does not.ensure_projected()withpurpose = "area"chooses an equal-area CRS for lon/lat input; build the cells in it. Lon/lat cells are measured geodesically instead: theircell_areaissf::st_area()'s area in square metres (on the sphere with s2, sf's default), not a planar area in squared degrees, and the distortion check passes by construction; with s2 switched off, sf needs the lwgeom package for that area and the request is refused without it. The measured spread is attached asattr(, "area_error").
Value
A tibble/data.frame (or sf if cells_sf given) with per-cell
summaries: the ID column, n (rows in the cell), one column per
agg_funs entry per variable, ..sd_* / ..se_* for every numeric
response and predictor, ..neff_* / ..df_* / ..ci_lo_* /
..ci_hi_* for the same columns when conf_level is given,
cell_weight, deff_applied when a design effect was requested (any
deff other than 1), and cell_area / n_per_area when
area = TRUE. An input column also called n is not allowed to shadow
the count.
deff_applied is TRUE on every row when the requested correction was
applied (exactly when the "deff_applied" attribute below is attached;
under "kish", when any standard-error column was corrected)
and FALSE when it fell back to the uncorrected standard errors: a
refused deff, a "variogram" request with no usable model, or
"kish" ICCs of 0 for every variable type (and NA on a cells_sf
row no point fell in).
Unlike the attribute it survives rbind() and
dplyr::bind_rows() of many results. A fallback is also signalled by a
warning of class "spatialkit_deff_fallback" (a Kish ICC of 0 is
reported on attr(, "icc") instead), which
tryCatch(spatialkit_deff_fallback = ) catches without matching the
message; it is raised only when the standard errors really are the
uncorrected ones.
When a correction was actually applied, an attribute "deff_applied" is
attached recording it: method plus icc_resp/icc_pred and deff
for "kish" (deff is the primary variable's per-cell design effect,
all 1 when only the predictor ICC was positive),
deff/deff_rows/rbar/crs/max_n for "variogram" (deff is
the design effect at the primary variable's non-missing count per cell,
deff_rows at the cell's row count, which is the vector the log line
summarises as a median and a max), and deff alone for a fixed number.
When cells_sf is supplied, every per-cell vector in that attribute
(deff, deff_rows and rbar alike) is realigned to the joined row
order, so deff[i] and rbar[i] still describe row i; cells with no
observations carry NA. No attribute is attached when no correction was
applied: deff = 1, a deff = "kish" request whose ICCs are all 0, or
a "variogram" request that could not be fitted. A deff = "kish" request always
records the ICCs it estimated on an attribute "icc" (resp and
pred, NA for a variable type with no numeric column), whether or not
they were positive enough to apply, so a result with no "deff_applied"
still says what the ICC came out as.
The ID column keeps its input type when cells_sf's ID column and the
summarised IDs already have the same class. When the classes differ, both
are coerced to character in order to join (logged as a warning), and the
returned ID column is therefore character. Whole numbers are written out
in full for that ("100000", never "1e+05"), so an integer and a
double ID of the same cell still match.
Details
This is the third step of the package's pipeline, taking the labelled layer
from assign_features_to_polygons() down to cell level. What distinguishes
it from a plain dplyr::group_by() + summarise() is that it carries the
uncertainty of each aggregate with it: alongside every mean it returns a
within-cell standard deviation, a standard error and an observation count,
and it can correct that standard error for within-cell spatial
autocorrelation via deff. Reach for it whenever the cell-level values will
be modelled or mapped, because a cell mean over 2 observations and one over
200 are not the same measurement and nothing downstream can tell them apart
otherwise.
In addition to user-specified aggregation functions, this function always
computes within-cell standard deviation (..sd_<var>) and standard error
(..se_<var>) for every numeric response/predictor column, plus an n
column (rows falling in the cell) and a cell_weight column.
These columns let downstream models account for the fact that a cell with
2 observations carries more aggregation uncertainty than one with 200.
cell_weight is the effective sample size of the primary variable: the
response when one was supplied, otherwise the first predictor. It counts
that variable's non-missing rows, not all rows (a cell of 10 rows with 3
finite responses carries 3 observations' worth of information about the
response, not 10), and it is divided by that cell's design effect when
deff applied one. With deff = 1 and no missing values it equals n.
Pass it as the weights argument of a downstream regression.
Spatial autocorrelation and standard-error bias
By default (deff = 1), the ..se_* columns are computed as
sd / sqrt(n), which treats the observations within each cell as
independent. When data are spatially autocorrelated (the common case for the
spatial workflows this package supports), within-cell observations are
typically positively correlated: they share the cell's departure from the
population mean, so as an estimate of the population (grand) mean a cell
mean has an effective sample size smaller than n. For that estimand the
naive SE is anticonservative (too small), and a downstream weighted
regression that uses cell_weight or the ..se_* columns for population-level
inference will produce overconfident standard errors for cells with strong
intra-cell correlation. For the cell's own mean the naive SE is the right
one when the cell's points are spread through it, and the corrected SE is too
wide; see "What the standard error estimates" before setting deff.
Setting deff = "kish" applies an approximate correction using Kish's
design effect. Separate intra-class correlations (ICCs) are estimated for
response and predictor variables via a one-way random-effects decomposition
across all cells. Each variable type's ICC is used for its own SE
adjustment, and each cell's effective sample size is reduced to
n_i / (1 + (n_i - 1) * rho). This is a first-order correction that
does not require a full spatial covariance model but does require enough
cells and observations for a stable ICC estimate.
A design effect estimated from the data ("kish" or "variogram") comes
with a second, small-sample correction. The within-cell correlation that
inflates the variance of the mean to sigma^2 * deff / n also biases the
within-cell sample variance downward: under exchangeable correlation rho
(Kish's own assumption), with deff = 1 + (n - 1) * rho,
E[s^2] = sigma^2 * (n - deff) / (n - 1), so s^2 understates sigma^2
by very nearly the factor by which deff inflates the mean's variance, and
the two errors compound rather than cancel. The standard error is therefore
s * sqrt(deff / n) * sqrt((n - 1) / (n - deff)), and NA where
deff >= n (the cell then holds one observation's worth of information
and s carries none about sigma). This is the package's own derivation,
not taken from a reference. Measured 95% interval coverage at n = 30
over 20,000 replicates: 0.921, 0.844 and 0.628 at rho = 0.2, 0.5 and 0.8
with s * sqrt(deff / n) alone, against 0.948, 0.950 and 0.949 with the
rescaling.
You may also pass a fixed numeric design effect (e.g. deff = 2) to
uniformly inflate standard errors: an externally supplied constant is
applied as sd * sqrt(deff / n), exactly sqrt(deff) times the naive SE in
every cell. (The E[s^2] correction that the estimated design effects also
apply is derived from within-cell correlation and would not be justified for
a number the caller chose.)
Even with the Kish correction, the adjusted SE is an approximation.
For rigorous inference under spatial dependence, consider fitting an
explicit spatial covariance model (e.g. via fit_bayesian_spatial_model).
What the standard error estimates
The ..se_* columns are the standard error of the cell mean as an
estimate of the population (grand) mean: the unconditional quantity, in
which the cell's own realised deviation is part of the error. That is the
right quantity when cells are treated as samples from a common population,
and the design-effect correction is calibrated for it: measured 95% interval
coverage of the grand mean is 0.95 with deff = "kish" (and 0.29 with the
naive SE) on exchangeable within-cell correlation, and 0.93 with
deff = "variogram" on a simulated Gaussian field.
It is not the standard error of the cell's own mean (the block average
over that cell), which is what a cell-level map or a regression on cell
values usually wants. For that quantity the naive sd / sqrt(n) is the
better of the two on offer: measured coverage 0.95 under exchangeable
within-cell correlation, against very nearly 1.00 for the
design-effect-corrected SE, which is too wide by the factor
sqrt(deff / (1 - rho)) (4.6 at 20 points a cell and rho = 0.5). That 0.95
is exact under the exchangeable model and holds under a spatial covariance
model only when the cell's points are spread through the cell; with
clustered sampling inside a cell it is anticonservative for the block
average too (measured 0.58), and the right quantity there is a
block-kriging variance, which this function does not compute. Use deff
when the cell means feed a population-level inference; leave it at 1 when
they are measurements of the cells themselves and the sampling within cells
is reasonably uniform.
Design effects and variable types
deff = "kish" estimates a separate ICC for response and predictor
variables and applies each to its own columns. deff = "variogram" fits or
accepts one correlation function and applies it to every numeric column,
because a variogram is a property of the field being modelled rather than of
a variable type; a predictor whose spatial structure differs markedly from
the response's will have its SE corrected by the response's correlation.
The internally estimated variogram is fitted to the response itself,
never to OLS residuals, whatever predictor_vars holds: the ..se_resp_*
columns estimate the grand mean of the response, so the correlation to
correct for is the response's own. (A residual variogram, whose correlation
is that of the part the predictors do not explain, is weaker; using it here
dropped grand-mean coverage from 0.93 to 0.51 the moment a predictor was
listed.) Pass sac explicitly when you want a different variogram, such as
a residual one from estimate_sac_range(..., predictor_vars = ). A sac
whose attr(sac, "detrended") is TRUE is used as given, but when it
corrects response columns a warning says that their standard errors are
understated, as kriging_adequacy() warns about the same mismatch.
Confidence intervals
With conf_level set, every numeric response and predictor column gains
four more columns: ..neff_*, that column's effective sample size in the
cell (its non-missing count over its design effect, the per-column version
of cell_weight); ..df_*, the degrees of freedom the interval uses; and
..ci_lo_* / ..ci_hi_*, a t interval for the cell mean as an estimate
of the grand mean. The estimand is the same as ..se_*'s, so everything
in "What the standard error estimates" applies to it, including that it is
not an interval for the cell's own block average. The interval is
mean +/- qt((1 + conf_level) / 2, df) * se, centred on the plain mean of
the column's non-missing values whatever agg_funs computes, and is NA
wherever the standard error is (a single observation; complete redundancy
under deff). ..neff_* and ..df_* are NA where the column has one
non-missing value or none, like the standard error; cell_weight still
counts such a cell (1, or 1 / deff for a numeric deff), so the two
differ there.
The degrees of freedom are not neff - 1. The interval's spread comes
from the within-cell sample variance, and under exchangeable correlation
(deff = "kish") that variance keeps its n - 1 degrees of freedom
whatever the design effect. The design effect inflates the mean's
variance and biases s^2, both of which the standard error already
corrects, and the resulting pivot is exactly t on n - 1 df. Measured 95%
coverage of the grand mean on the Kish path, 20 cells of 20 at an ICC of
0.2 / 0.6 / 0.9: 0.954 / 0.953 / 0.952 with n - 1, against
0.992 / 1.000 / 1.000 with neff - 1, which is not an interval so much as
a refusal to say anything. The effective-sample-size degrees of freedom of
Faes et al. (2009) belong to a mean estimated across many correlated units
whose variance is estimated from the spread between them; they do not
transfer to a single cell's mean with a variance estimated from inside it.
..df_* is therefore n - 1 at deff = 1, for a numeric deff and for
"kish". Under deff = "variogram" correlation decays with distance and
the within-cell variance loses degrees of freedom to it; ..df_* is then
the Satterthwaite (1946) moment-matched df of s^2 under the fitted
correlation matrix, a fraction of n - 1 that shrinks as the range grows.
Measured on a Gaussian field with an exponential range of 150 and 400 on a
1000-unit domain, 16 cells of about 25 points: 0.960 and 0.958 with that
df, against 0.931 and 0.918 with n - 1, and 0.34 and 0.19 for the naive
deff = 1 interval. The interval is only as good as the design effect
under it: a mis-specified variogram, or an ICC estimated from too few
cells, moves the coverage with it.
References
Faes, C., Molenberghs, G., Aerts, M., Verbeke, G. and Kenward, M. G. (2009). The effective sample size and an alternative small-sample degrees-of-freedom method. The American Statistician, 63(4), 389–399. doi:10.1198/tast.2009.08196
Satterthwaite, F. E. (1946). An approximate distribution of estimates of variance components. Biometrics Bulletin, 2(6), 110–114. doi:10.2307/3002019
See also
assign_features_to_polygons(), which produces the input layer;
build_tessellation() for the cells themselves.
Other aggregation:
assign_features_to_polygons(),
determine_optimal_levels(),
kriging_adequacy(),
resolution_profile(),
select_resolution(),
summary.resolution_profile()
Examples
library(sf)
set.seed(1)
n <- 200
east <- runif(n, 0, 100)
north <- runif(n, 0, 100)
# A response with spatial structure, so the within-cell ICC is not zero.
pts <- st_as_sf(
data.frame(x = 5e5 + east, y = 5e6 + north,
val = 5 + 0.02 * east + 0.02 * north + rnorm(n, sd = 0.5)),
coords = c("x", "y"), crs = 32632
)
bnd <- st_sf(geometry = st_sfc(st_polygon(list(rbind(
c(5e5, 5e6), c(5e5 + 100, 5e6), c(5e5 + 100, 5e6 + 100),
c(5e5, 5e6 + 100), c(5e5, 5e6)
))), crs = 32632))
grid <- create_grid_polygons(bnd, target_cells = 9, type = "square")
assigned <- assign_features_to_polygons(pts, grid)
# IID standard errors (default) vs Kish design-effect adjustment. The
# correction is large here, and meant to be: about 22 points per cell that
# all share the cell's part of the trend carry far fewer than 22
# independent pieces of information about the cell mean.
naive <- summarize_by_cell(assigned, response_var = "val")
kish <- summarize_by_cell(assigned, response_var = "val", deff = "kish")
data.frame(n = naive$n,
se_naive = naive$..se_resp_val,
se_kish = kish$..se_resp_val)
#> n se_naive se_kish
#> 1 25 0.09706775 0.7210227
#> 2 28 0.11815220 0.9279034
#> 3 25 0.11573069 0.8596517
#> 4 21 0.11556510 0.7881134
#> 5 23 0.13192931 0.9407000
#> 6 21 0.12707224 0.8665880
#> 7 10 0.18215988 0.8673238
#> 8 27 0.09534639 0.7355266
#> 9 20 0.14402822 0.9590654
attr(kish, "deff_applied") # method, icc_resp, icc_pred, per-cell deff
#> $method
#> [1] "kish"
#>
#> $icc_resp
#> [1] 0.6842466
#>
#> $icc_pred
#> [1] NA
#>
#> $deff
#> [1] 17.42192 19.47466 17.42192 14.68493 16.05343 14.68493 7.15822 18.79041
#> [9] 14.00069
#>
# A 95% interval for each cell mean as an estimate of the grand mean, on
# the Kish-corrected standard error and n - 1 degrees of freedom.
ci <- summarize_by_cell(assigned, response_var = "val", deff = "kish",
conf_level = 0.95)
ci[, c("poly_id", "n", "resp_mean_val", "..neff_resp_val", "..df_resp_val",
"..ci_lo_resp_val", "..ci_hi_resp_val")]
#> # A tibble: 9 × 7
#> poly_id n resp_mean_val ..neff_resp_val ..df_resp_val ..ci_lo_resp_val
#> <int> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 1 25 5.63 1.43 24 4.14
#> 2 2 28 6.30 1.44 27 4.40
#> 3 3 25 6.93 1.43 24 5.16
#> 4 4 21 6.42 1.43 20 4.77
#> 5 5 23 7.11 1.43 22 5.15
#> 6 6 21 7.89 1.43 20 6.09
#> 7 7 10 7.16 1.40 9 5.19
#> 8 8 27 7.77 1.44 26 6.25
#> 9 9 20 8.16 1.43 19 6.15
#> # ℹ 1 more variable: ..ci_hi_resp_val <dbl>