pycnogrid

{pycnogrid} provides tools for interpolating more aggregate polygon-level extensive data (e.g. population, employment, or event counts) to a range of alternative grid types using pycnophylactic interpolation.

Often administrative and statistical reporting units do not align with the geographic supports needed for analysis. They can also introduce sensitivity to the scale and zoning of spatial units, which are central to the modifiable areal unit problem (MAUP). Waldo Tobler’s pycnophylactic interpolation method can potentially help to address this mismatch by transferring polygon totals to an alternative regular grid while preserving represented source totals and smoothing estimates across neighbouring cells.

Existing implementations of Tobler’s pycnophylactic interpolation include the {pycno} package for R and the {tobler} module within the larger Python Spatial Analysis Library (PySAL). These implementations focus on transferring source data to target raster cells. {pycnogrid} extends Tobler’s pycnophylactic interpolation approach beyond regular raster lattices and supports a range of discrete global grid systems (DGGSs), including H3, A5, S2, and ISEA grids, as well as rasters and other local grids. The flexibility of the underlying interpolation approach makes it possible to create spatially smooth, mass-preserving representations of aggregate data with area or shape preserving geographic supports.

For a full introduction to the interpolation workflow, grid options, and output interpretation, see the getting started vignette.

Installation

You can install the development version of {pycnogrid} from GitHub:

# install.packages("remotes")
remotes::install_github("higgicd/pycnogrid")

Example

This example interpolates census tract population counts for a small area of New York City to an H3 grid at resolution 10:

library(dplyr)
library(pycnogrid)
library(sf)
library(tmap)
out <- nyc_ct_small |>
  pycnogrid::to_grid(
    value_col = populationE,
    grid_type = "h3",
    resolution = 10
  )

The returned object is an {sf} object containing the target-cell geometries, the interpolated count, an estimated density, and the proportion of each cell covered by the source geography. The map below shows the interpolated population counts:

Census tract population counts interpolated to an H3 grid.

Census tract population counts interpolated to an H3 grid.

How it works

For a polygon layer containing source totals, {pycnogrid}:

  1. creates a target grid covering the source geography;
  2. allocates each source total to intersecting target cells;
  3. smooths estimated densities across neighbouring cells; and
  4. repeatedly rescales the estimates so that the original source totals are preserved.

The resulting grid can be used in downstream mapping, accessibility, spatial modelling, and sensitivity analyses.

Key arguments

The main function, to_grid(), provides several options for specifying the target geography and interpolation process:

For most applications, cell_inclusion = "intersect" and cell_allocation = "area" provide the most geographically complete initial representation because all source–target intersections are retained and source totals are allocated according to their area of overlap.

Interpreting the output

Interpolated values are estimates of the amount of the source total associated with each target cell. With cell_allocation = "area", the output for partially covered target cells represents the amount estimated within the portion of the cell that overlaps the source geography. In this sense, the method does not extrapolate counts beyond source boundaries. The output includes:

out |> glimpse()
#> Rows: 336
#> Columns: 7
#> $ h3                <chr> "8a2a100d2db7fff", "8a2a100d2d97fff", "8a2a100d2d87f…
#> $ geometry          <POLYGON [m]> POLYGON ((585062.6 4511955,..., POLYGON ((58…
#> $ .tid              <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1…
#> $ pycno_populationE <dbl> 103.085325, 183.514968, 32.247510, 11.821690, 41.908…
#> $ pycno_density     <dbl> 0.0068098960, 0.0121233098, 0.0021303414, 0.00078095…
#> $ pycno_coverage    <dbl> 1.0000000, 1.0000000, 1.0000000, 1.0000000, 1.000000…
#> $ pycno_iter        <int> 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, …

with:

Additional information about convergence, represented input totals, missing source areas, grid settings, and neighbourhood settings is stored as attributes on the returned object.

Important considerations

The pycnophylactic interpolation algorithm implemented in {pycnogrid} is intended for extensive variables: quantities that can be meaningfully divided and summed across space, such as population, households, jobs, trips, or service counts.

It should not be used to directly interpolate intensive variables such as median income, percentages, rates, or averages. Where appropriate, interpolate the underlying numerator and denominator separately, then calculate the rate or ratio on the resulting grid.

Grid choice remains an analytical decision. Different grids vary in cell area, shape, orientation, hierarchy, adjacency structure, and suitability for tasks such as spatial aggregation, indexing, visualization, or accessibility analysis. Moreover, options related to cell inclusion, allocation, neighbourhood order, etc., can all meaningfully shape the nature of the interpolation and smoothing. {pycnogrid} facilitates comparison across these alternative spatial support systems and modelling choices rather than treating any single grid as universally optimal.