Protection of multilevel gridded data using a quadtree approach.
gridy builds variable-resolution grids from geolocated data. The idea is to
overlay several nested square grids, from the coarsest to the finest, then
decide for each cell whether it can be disseminated on its own or has to be
merged with its neighbours in order to meet a confidentiality threshold.
The result is a grid whose resolution adapts to density: fine wherever counts allow it, coarse elsewhere.
The method has been developed at Insee and originally presented in a paper for the conference Journées de Méthodologie Statistique in 2018. The paper (in French) is available here. The method is also presented at the chapter 5 of the European Guidelines for statistical disclosure control methods applied on geo-referenced data (Möhler et al. 2025, data.europa.eu/doi/10.2785/8238461).
# install.packages("remotes")
remotes::install_github("InseeFrLab/gridy")The package ships C++ code (Rcpp). Installing from source requires a working
toolchain: Rtools on Windows, r-base-dev on Debian/Ubuntu.
Dependencies: data.table, Rcpp, rlang, purrr, sf, fasterize,
raster.
Cells form a tree. A cell at level i (the parent) is subdivided into cells
at level i+1 (its children). Level 1 is the coarsest resolution, the last
level the finest.
The algorithm walks down the tree one level at a time. For each parent cell, it examines the children in increasing order of count and assigns three attributes:
| Attribute | Meaning |
|---|---|
etat |
TRUE if the cell meets the threshold and can be disseminated on its own, FALSE if it has to be merged. |
force |
Count still needed for the group being formed to cross the threshold. 0 once the threshold is met. |
groupe |
Group identifier. Cells sharing a groupe are disseminated merged together. |
A parent cell that cannot be disseminated passes its state down to its entire descent: the whole branch stays aggregated. A parent cell that can be disseminated lets its children be assessed individually, the smallest ones being absorbed into a group until the threshold is reached.
Cell identifiers follow the Inspire specification:
FR_CRS3035RES1000mN2000000E3000000, where the stated position is that of the
lower left corner.
library(gridy)
library(data.table)
# Geolocated individual data: x, y and the coordinate reference system
n <- 1e4
tab <- as.data.table(
data.frame(
id_obs = 1:n,
x = rnorm(n, 3e6, 2e4),
y = rnorm(n, 2e6, 3e4),
crs = 3035
)
)
# 1. Build the variable-resolution grid
# mesh sizes in DECREASING order, threshold of 5 observations
tab_GS <- create_GS_CPP(tab, seuil = 5, mailles = c(32e3, 16e3, 8e3, 4e3, 2e3, 1e3))
# 2. Extract the "natural" grid: on each branch of the tree,
# the finest cell that can be disseminated
tab_car_nat <- determiner_car_naturel(tab_GS)
# 3. Lookup table between natural cells and finest-level cells
arb_nat <- determiner_arb_naturel(tab_car_nat, tab_GS)
# 4. Estimate sensitive variables on protected cells, by distributing
# the group total in proportion to a non-sensitive variable
tab_car <- tab_GS$tab_car
tab_car[, `:=`(poph = nb_obs * 0.48, popf = nb_obs * 0.52)]
tab_diff <- imputer_cle_repartition(
tab_car,
list_var_imput = c("poph", "popf"),
var_cle = "nb_obs"
)
# 5. Turn cells into geometries for mapping
contours <- lapply(tab_car_nat$id_carreau_nat, make_contour_car)If observations are already aggregated on the centroids of the finest grid
cells, pass agreg = TRUE. The table must then hold the columns nb_obs, x,
y and crs.
tab_GS <- create_GS_CPP(t_car, seuil = 11, mailles = c(8e3, 4e3, 2e3, 1e3), agreg = TRUE)The function returns a list of two data.table objects.
tab_arb — the tree table. One row per cell of the finest grid, with one
identifier column per level (id_carreau_niv1 through id_carreau_niv{n-1},
then id_carreau_petit). This is what carries the parent-child links.
tab_car — the dissemination table. One row per cell of each level:
| Column | Contents |
|---|---|
id_carreau |
Inspire identifier of the cell |
p |
Identifier of the parent cell (NA at level 1) |
niveau |
Grid level, 1 being the coarsest resolution |
nb_obs |
Number of observations in the cell |
etat, force, groupe |
See the table in the "How it works" section |
| Function | Purpose |
|---|---|
create_GS_CPP() |
Entry point. Builds the variable-resolution grid and the dissemination table. |
create_grid_niv() |
Creates a square grid of a given mesh size and adds the Inspire identifier to the table. |
create_grids() |
Creates several nested grids in one pass. |
determiner_car_naturel() |
Extracts the natural grid: on each branch, the finest disseminable cell. |
determiner_arb_naturel() |
Lookup table between natural cells and finest-level cells. |
imputer_cle_repartition() |
Distributes a group total across its cells in proportion to a key variable. |
make_contour_car() |
Converts an Inspire identifier into an sf polygon. |
The algorithmic core (det_etat() and det_etat_tot(), written in C++) is
internal and reachable through gridy:::.
Mesh order is not validated. create_GS_CPP() assumes mailles is sorted
in decreasing order: mailles[1] becomes level 1, the last element the finest
grid. An increasing vector yields an inconsistent tree without raising an error.
This differs from create_grids(), which sorts its input itself.
Mesh sizes must be nested. Each mesh size has to be a multiple of the next
one (32-16-8-4-2-1 km works). Otherwise a fine cell may straddle several
parent cells and the tree becomes arbitrary.
© Institut national de la statistique et des études économiques (Insee).