Longitudinal polygon datasets often lack identifiers that remain
meaningful when boundaries or labels change. spatpersist
creates those identifiers from spatial continuity. It does not require
prior IDs and does not use names to determine identity.
This vignette uses a synthetic dataset containing four common cases:
The default rule continues an identity when at least 75 percent of the earlier polygon is preserved. Candidate links are selected one-to-one.
result <- persist_ids(
polygons,
time = "year",
threshold = 0.75,
metric = "share_old"
)
sf::st_drop_geometry(result)[
, c(
"year",
"name",
"spatial_id",
"spatial_version_id",
"lineage_id",
"parent_id",
"transition_type"
)
]
#> year name spatial_id spatial_version_id lineage_id parent_id
#> 1 2000 Alpha SID000001 SID000001-V001 LID000001 <NA>
#> 2 2001 Alpha SID000001 SID000001-V001 LID000001 <NA>
#> 3 2000 Beta SID000003 SID000003-V001 LID000003 <NA>
#> 4 2001 Beta SID000003 SID000003-V002 LID000003 <NA>
#> 5 2000 Gamma SID000002 SID000002-V001 LID000002 <NA>
#> 6 2001 Delta SID000002 SID000002-V001 LID000002 <NA>
#> 7 2000 Omega SID000004 SID000004-V001 LID000004 <NA>
#> 8 2001 Omega East SID000006 SID000006-V001 LID000004 SID000004
#> 9 2001 Omega West SID000005 SID000005-V001 LID000004 SID000004
#> transition_type
#> 1 initial
#> 2 continuation
#> 3 initial
#> 4 continuation
#> 5 initial
#> 6 continuation
#> 7 initial
#> 8 split
#> 9 splitThe output separates four concepts:
spatial_id identifies one continuing unit.spatial_version_id identifies consecutive boundary
spells within that unit.lineage_id connects related units through splits,
mergers, or replacements.parent_id records the strongest predecessor when a
related observation starts a new identity.The renamed polygon retains its identity because its geometry is unchanged. The expanded polygon retains its identity but advances to a new geometry version. Under the default rule, neither half of the split preserves 75 percent of the old polygon, so both receive new identities while remaining in the old polygon’s lineage.
Three spatial metrics are available:
share_old: intersection divided by predecessor
area;share_new: intersection divided by successor area;iou: intersection divided by union area.match_rule = "greedy" ranks all eligible links and
selects them one-to-one. match_rule = "mutual_best"
considers only links that are best for both sides.
Near ties are controlled separately:
conservative <- persist_ids(
polygons,
time = "year",
metric = "iou",
threshold = 0.75,
match_rule = "mutual_best",
ambiguity_tolerance = 0.05,
ambiguity_action = "new"
)The three ambiguity actions are:
"flag": select deterministically and set
match_ambiguous = TRUE;"new": reject near-tied links and start new
identities;"error": stop so the user can inspect the
evidence.match_confidence combines the selected spatial score
with its separation from eligible competitors. A score near zero
indicates a tie or a candidate that lost a stronger competing link.
Automatic identity decisions should remain inspectable. The transition table contains every positive-area overlap, not just selected links.
transitions <- id_transitions(result)
transitions[
, c(
"old_spatial_id",
"new_spatial_id",
"share_old",
"share_new",
"iou",
"eligible",
"mutual_best",
"ambiguous",
"selected",
"lineage_link"
)
]
#> old_spatial_id new_spatial_id share_old share_new iou eligible
#> 1 SID000001 SID000001 1.0 1.0000000 1.0000000 TRUE
#> 2 SID000002 SID000002 1.0 1.0000000 1.0000000 TRUE
#> 3 SID000003 SID000003 1.0 0.8333333 0.8333333 TRUE
#> 4 SID000004 SID000005 0.5 1.0000000 0.5000000 FALSE
#> 5 SID000004 SID000006 0.5 1.0000000 0.5000000 FALSE
#> mutual_best ambiguous selected lineage_link
#> 1 TRUE FALSE TRUE TRUE
#> 2 TRUE FALSE TRUE TRUE
#> 3 TRUE FALSE TRUE TRUE
#> 4 FALSE FALSE FALSE TRUE
#> 5 FALSE FALSE FALSE TRUEAn identity-level summary is also available:
id_lineages(result, time = "year")
#> spatial_id lineage_id parent_id first_time last_time first_transition
#> 1 SID000001 LID000001 <NA> 2000 2001 initial
#> 2 SID000002 LID000002 <NA> 2000 2001 initial
#> 3 SID000003 LID000003 <NA> 2000 2001 initial
#> 4 SID000004 LID000004 <NA> 2000 2000 initial
#> 5 SID000005 LID000004 SID000004 2001 2001 split
#> 6 SID000006 LID000004 SID000004 2001 2001 split
#> n_observations n_versions registry_observations ambiguous_matches
#> 1 2 1 0 0
#> 2 2 1 0 0
#> 3 2 2 0 0
#> 4 1 1 0 0
#> 5 1 1 0 0
#> 6 1 1 0 0
#> min_match_confidence
#> 1 1
#> 2 1
#> 3 1
#> 4 NA
#> 5 NA
#> 6 NASolid edges are selected identity continuations. Dashed edges are broader lineage links. In the example, the two dashed branches show the split without forcing either child to reuse the predecessor’s identity.
issues <- validate_ids(result, time = "year")
issues
#> [1] severity check row message
#> <0 rows> (or 0-length row.names)A zero-row issue table means the result passed all implemented checks. These include within-period uniqueness, lineage consistency, sequential geometry versions, parent availability, transition labels, and match diagnostics.
Validation confirms structural consistency; it does not prove that a chosen spatial continuity rule is substantively correct for a particular research design. Thresholds and diagnostics should still be reviewed.
Matching parameters encode a research decision and should be calibrated before IDs are frozen. A practical calibration workflow is:
match_ambiguous = TRUE.max_time_gap explicitly when continuity should not
cross missing periods.Agreement with an existing identifier system is evidence, not proof.
A reference system may use names, institutional knowledge, or other
attributes that spatpersist intentionally does not use.
Persistent disagreements should therefore be interpreted substantively
rather than resolved by maximizing a single aggregate score.
Passing a prior result as registry protects issued IDs
when rows are reordered or new units are added.
rerun <- persist_ids(
polygons,
time = "year",
registry = result
)
all(rerun$registry_matched)
#> [1] TRUE
identical(rerun$spatial_id, result$spatial_id)
#> [1] TRUERegistry recovery uses same-time geometry and defaults to an IoU of
0.999999. Conflicting identity or geometry-version anchors stop with an
error. New identities are numbered above the registry’s largest existing
spatial ID. If new spatial evidence joins observations anchored to
multiple existing lineages, the joined component deliberately
consolidates to the lexicographically smallest existing
lineage_id.
All generated identifiers are dataset-local. Separate projects
independently start with identifiers such as SID000001 and
LID000001, so combine outputs only with an additional
project or dataset key. Persistence is scoped to one dataset and its
registry chain, not to a global namespace.
By default, invalid geometries stop processing. They can be repaired explicitly:
geometry_precision sets an sf precision
scale before spatial operations. For example,
geometry_precision = 100 rounds coordinates to two decimal
places when geometries are passed to spatial libraries.
After repair and precision are applied, exact coextensive geometries within one time period are rejected. Because attributes are intentionally excluded from matching, no reproducible spatial tie-breaker exists for those observations.
max_time_gap prevents continuity across large gaps in
numeric or date-based time columns. Its units are the time column’s
numeric units, days for Date, and seconds for
POSIXt.
parent_id stores one strongest predecessor; the
transition table contains complete merger evidence.SID000001 and
SID000001-V001.