The areaOfEffect package classifies spatial points by
their position relative to a region’s boundary—without requiring sf
expertise.
Dataframe in → dataframe out.
Points are classified as:
Core: inside the original support
Halo: outside the original but inside the area of effect
Pruned: outside the AoE entirely (removed)
By default, halos are defined as equal area to the core—a proportion-based definition that enables consistent cross-region comparisons.
library(areaOfEffect)
library(sf)
#> Linking to GEOS 3.13.1, GDAL 3.11.4, PROJ 9.7.0; sf_use_s2() is TRUEThe simplest usage: pass a dataframe with coordinates and a country name.
# Your occurrence data
observations <- data.frame(
species = c("Oak", "Beech", "Pine", "Spruce"),
lon = c(14.5, 15.2, 16.8, 20.0),
lat = c(47.5, 48.1, 47.2, 48.5)
)
# Classify relative to Austria
result <- aoe(observations, "Austria")
result$aoe_class
#> [1] "core" "core" "halo"The package auto-detects coordinate columns (lon/lat, x/y, longitude/latitude, etc.).
# Get Austria and transform to equal-area projection
austria <- get_country("AT")
austria_ea <- st_transform(austria, "ESRI:54009")
# Create a point inside Austria
dummy_pt <- st_centroid(austria_ea)
#> Warning: st_centroid assumes attributes are constant over geometries
# Run aoe() to get geometries (uses buffer method by default)
result <- aoe(dummy_pt, austria_ea)
geoms <- aoe_geometry(result, "both")
# Extract geometries
austria_geom <- geoms[geoms$type == "original", ]
aoe_geom <- geoms[geoms$type == "aoe", ]
# Plot
par(mar = c(1, 1, 1, 1), bty = "n")
plot(st_geometry(aoe_geom), border = "steelblue", lty = 2, lwd = 1.5)
plot(st_geometry(austria_geom), border = "black", lwd = 2, add = TRUE)
legend("topright",
legend = c("Austria (core)", "Area of Effect"),
col = c("black", "steelblue"),
lty = c(1, 2),
lwd = c(2, 1.5),
inset = 0.02)support <- st_as_sf(
data.frame(id = 1),
geometry = st_sfc(st_polygon(list(
cbind(c(0, 100, 100, 0, 0), c(0, 0, 100, 100, 0))
))),
crs = 32631
)Create observation points:
pts <- st_as_sf(
data.frame(
id = 1:5,
value = c(10, 20, 15, 25, 30)
),
geometry = st_sfc(
st_point(c(50, 50)), # center
st_point(c(10, 10)), # near corner
st_point(c(95, 50)), # near edge
st_point(c(120, 50)), # outside, in halo
st_point(c(250, 250)) # far outside
),
crs = 32631
)Apply the area of effect:
result <- aoe(pts, support)
print(result)
#> Area of Effect Result
#> ─────────────────────
#> Points: 4 (3 core, 1 halo)
#> Supports: 1
#> Scale: 0.414 (multiplier 1.41, theoretical halo:core 1.00)
#>
#> Simple feature collection with 4 features and 5 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: 10 ymin: 10 xmax: 120 ymax: 50
#> Projected CRS: WGS 84 / UTM zone 31N
#> point_id support_id aoe_class id value geometry
#> 1 1 1 core 1 10 POINT (50 50)
#> 2 2 1 core 2 20 POINT (10 10)
#> 3 3 1 core 3 15 POINT (95 50)
#> 4 4 1 halo 4 25 POINT (120 50)The result contains only points inside the AoE, with their classification:
Process multiple regions at once:
# Two adjacent regions
supports <- st_as_sf(
data.frame(region = c("A", "B")),
geometry = st_sfc(
st_polygon(list(cbind(c(0, 50, 50, 0, 0), c(0, 0, 100, 100, 0)))),
st_polygon(list(cbind(c(50, 100, 100, 50, 50), c(0, 0, 100, 100, 0))))
),
crs = 32631
)
# Points that may fall in overlapping AoEs
pts_multi <- st_as_sf(
data.frame(id = 1:3),
geometry = st_sfc(
st_point(c(25, 50)), # inside A
st_point(c(50, 50)), # on boundary
st_point(c(75, 50)) # inside B
),
crs = 32631
)
result_multi <- aoe(pts_multi, supports)
print(result_multi)
#> Area of Effect Result
#> ─────────────────────
#> Points: 4 (4 core, 0 halo)
#> Supports: 2
#> Scale: 0.414 (multiplier 1.41, theoretical halo:core 1.00)
#>
#> Simple feature collection with 4 features and 4 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: 25 ymin: 50 xmax: 75 ymax: 50
#> Projected CRS: WGS 84 / UTM zone 31N
#> point_id support_id aoe_class id geometry
#> 1 1 1 core 1 POINT (25 50)
#> 2 2 1 core 2 POINT (50 50)
#> 3 2 2 core 2 POINT (50 50)
#> 4 3 2 core 3 POINT (75 50)Points can appear multiple times (once per support whose AoE contains them).
For coastal regions, sea is a hard boundary. Provide a mask to constrain the AoE:
# Create a coastal support
support_coast <- st_as_sf(
data.frame(id = 1),
geometry = st_sfc(st_polygon(list(
cbind(c(40, 80, 80, 40, 40), c(20, 20, 60, 60, 20))
))),
crs = 32631
)
# Create land mask (irregular coastline)
land_mask <- st_as_sf(
data.frame(id = 1),
geometry = st_sfc(st_polygon(list(cbind(
c(0, 100, 100, 70, 50, 30, 0, 0),
c(0, 0, 50, 60, 55, 70, 60, 0)
)))),
crs = 32631
)
# Create some points
pts_coast <- st_as_sf(
data.frame(id = 1:4),
geometry = st_sfc(
st_point(c(60, 40)), # core
st_point(c(50, 30)), # core
st_point(c(30, 40)), # halo (on land)
st_point(c(90, 70)) # would be halo but in sea
),
crs = 32631
)
# Apply with mask
result_coast <- aoe(pts_coast, support_coast, mask = land_mask)
# Get geometries for visualization
aoe_masked <- aoe_geometry(result_coast, "aoe")
support_geom <- aoe_geometry(result_coast, "original")
par(mar = c(1, 1, 1, 1), bty = "n")
plot(st_geometry(land_mask), col = NA, border = "steelblue", lwd = 2,
xlim = c(-10, 110), ylim = c(-10, 90))
plot(st_geometry(aoe_masked), col = rgb(0.5, 0.5, 0.5, 0.3),
border = "steelblue", lty = 2, add = TRUE)
plot(st_geometry(support_geom), border = "black", lwd = 2, add = TRUE)
# Add points with colors
cols <- ifelse(result_coast$aoe_class == "core", "forestgreen", "darkorange")
plot(st_geometry(result_coast), col = cols, pch = 16, cex = 1.5, add = TRUE)
# Show pruned point
plot(st_geometry(pts_coast)[4], col = "gray60", pch = 4, cex = 1.2, add = TRUE)
text(85, 75, "SEA", col = "steelblue", font = 2, cex = 1.2)
legend("topleft",
legend = c("Support", "AoE (masked)", "Coastline", "Core", "Halo", "Pruned"),
col = c("black", "steelblue", "steelblue", "forestgreen", "darkorange", "gray60"),
lty = c(1, 2, 1, NA, NA, NA),
lwd = c(2, 1, 2, NA, NA, NA),
pch = c(NA, NA, NA, 16, 16, 4),
pt.cex = c(NA, NA, NA, 1.5, 1.5, 1.2),
inset = 0.02)The package includes bundled country boundaries and a global land
mask. Use mask = "land" to clip AoE to coastlines:
# Create a point inside Portugal (approximate center of mainland)
dummy <- st_as_sf(
data.frame(id = 1),
geometry = st_sfc(st_point(c(-8, 39.5))),
crs = 4326
)
# Without mask
result_no_mask <- aoe(dummy, "PT")
#> Using largest polygon (96.8% of total area); 8 smaller polygon(s) dropped. Set largest_polygon = FALSE to include all.
aoe_no_mask <- aoe_geometry(result_no_mask, "aoe")
# With mask + area=1 for equal land area
result_masked <- aoe(dummy, "PT", mask = "land", area = 1)
#> Using largest polygon (96.8% of total area); 8 smaller polygon(s) dropped. Set largest_polygon = FALSE to include all.
aoe_masked <- aoe_geometry(result_masked, "aoe")
# Get support geometry
support_geom <- aoe_geometry(result_masked, "original")
# Transform to equal area for plotting
crs_ea <- st_crs("+proj=laea +lat_0=39.5 +lon_0=-8 +datum=WGS84")
aoe_no_mask_ea <- st_transform(aoe_no_mask, crs_ea)
aoe_masked_ea <- st_transform(aoe_masked, crs_ea)
support_ea <- st_transform(support_geom, crs_ea)
# Plot - expand xlim for legend, crop bottom margin
bbox <- st_bbox(aoe_no_mask_ea)
x_range <- bbox[3] - bbox[1]
y_range <- bbox[4] - bbox[2]
par(mar = c(1, 1, 1, 1), bty = "n")
plot(st_geometry(aoe_no_mask_ea), border = "gray50", lty = 2, lwd = 1.5,
xlim = c(bbox[1], bbox[3]),
ylim = c(bbox[2] + y_range * 0.25, bbox[4]),
axes = FALSE, xaxt = "n", yaxt = "n")
plot(st_geometry(aoe_masked_ea), col = rgb(0.3, 0.5, 0.7, 0.3),
border = "steelblue", lty = 2, lwd = 1.5, add = TRUE)
plot(st_geometry(support_ea), border = "black", lwd = 2, add = TRUE)
legend("topright",
legend = c("Portugal", "AoE (unmasked)", "AoE (land only)"),
col = c("black", "gray50", "steelblue"),
lty = c(1, 2, 2),
lwd = c(2, 1.5, 1.5),
bty = "n",
inset = 0.05)The area = 1 parameter ensures the halo has equal land
area to the core, even after the ocean is masked out. Without this,
coastline clipping would reduce the effective halo area.
The scale parameter controls halo size as a proportion
of core area.
# Default: equal core/halo areas (scale = sqrt(2) - 1)
result_default <- aoe(pts, support)
# Scale = 1: larger halo (3:1 area ratio)
result_large <- aoe(pts, support, scale = 1)| Scale | Halo:Core Area |
|---|---|
sqrt(2) - 1 (default) |
1:1 |
1 |
3:1 |
0.5 |
1.25:1 |
Sometimes you need a specific halo area regardless of masking. The
area parameter specifies the target halo area as a
proportion of the original support:
# Halo area = original area (same as scale = sqrt(2) - 1 without mask)
result <- aoe(pts, support, area = 1)
# Halo area = half of original
result <- aoe(pts, support, area = 0.5)Unlike scale, area accounts for masking:
the function finds the scale that produces the target halo area
after mask intersection. This is useful for coastal regions
where scale alone would produce inconsistent effective areas.
aoe_expand()When some supports have too few points at baseline AoE,
aoe_expand() finds the minimum scale needed to capture a
target number of points:
# Create sparse data
set.seed(42)
pts_sparse <- st_as_sf(
data.frame(id = 1:15),
geometry = st_sfc(c(
lapply(1:5, function(i) st_point(c(runif(1, 20, 80), runif(1, 20, 80)))),
lapply(1:10, function(i) st_point(c(runif(1, -50, 150), runif(1, -50, 150))))
)),
crs = 32631
)
# Expand until at least 10 points are captured
result_expand <- aoe_expand(pts_sparse, support, min_points = 10)Two safety caps prevent unreasonable expansion:
max_area = 2 (default): halo area cannot exceed 2×
the original
max_dist: maximum expansion distance in CRS
units
# Strict caps
result <- aoe_expand(pts, support,
min_points = 50,
max_area = 1.5, # halo ≤ 1.5× original
max_dist = 500) # max 500m expansionCheck expansion details:
aoe_sample()Core regions often dominate due to point density.
aoe_sample() provides stratified sampling to balance
core/halo representation:
# Create imbalanced data (many core, few halo)
set.seed(42)
pts_imbal <- st_as_sf(
data.frame(id = 1:60),
geometry = st_sfc(c(
lapply(1:50, function(i) st_point(c(runif(1, 10, 90), runif(1, 10, 90)))),
lapply(1:10, function(i) st_point(c(runif(1, 110, 140), runif(1, 10, 90))))
)),
crs = 32631
)
result_imbal <- aoe(pts_imbal, support, scale = 1)
# Default: balance core/halo (downsamples core to match halo)
set.seed(123)
balanced <- aoe_sample(result_imbal)
table(balanced$aoe_class)
#>
#> core halo
#> 10 10Custom ratios and fixed sample sizes:
# Fixed n with 70/30 split
set.seed(123)
sampled <- aoe_sample(result_imbal, n = 20, ratio = c(core = 0.7, halo = 0.3))
table(sampled$aoe_class)
#>
#> core halo
#> 14 6For multiple supports, use by = "support" to sample
within each:
aoe_border()When your study involves a boundary line rather than a
polygon (e.g., a river, mountain range, or political border), use
aoe_border() to classify points by their distance from and
side of the border.
# Create a diagonal border line
border_line <- st_as_sf(
data.frame(id = 1),
geometry = st_sfc(st_linestring(matrix(
c(0, 0,
100, 100), ncol = 2, byrow = TRUE
))),
crs = 32631
)
# Create points on both sides
set.seed(42)
pts_border <- st_as_sf(
data.frame(id = 1:30),
geometry = st_sfc(c(
# Points on side 1 (above the line)
lapply(1:15, function(i) st_point(c(runif(1, 10, 90), runif(1, 10, 90) + 20))),
# Points on side 2 (below the line)
lapply(1:15, function(i) st_point(c(runif(1, 10, 90), runif(1, 10, 90) - 20)))
)),
crs = 32631
)
# Classify by distance from border
result_border <- aoe_border(
pts_border, border_line,
width = 30,
side_names = c("north", "south")
)
# Built-in plot method
plot(result_border)The aoe_border() function:
Creates symmetric buffer zones on both sides of the border
Classifies points as “core” (near border) or “halo” (farther away)
Assigns each point to a side based on position relative to the line
Use the area parameter to specify target zone areas
instead of fixed widths:
aoe_sample() also works with border results, allowing
stratification by side or class:
# Balance by side (equal north/south)
set.seed(123)
balanced_side <- aoe_sample(result_border, ratio = c(north = 0.5, south = 0.5))
table(balanced_side$side)
#>
#> north south
#> 12 12
# Balance by distance class
set.seed(123)
balanced_class <- aoe_sample(result_border, by = "class")
table(balanced_class$aoe_class)
#>
#> core halo
#> 11 11aoe() classifies points as “core” or “halo”
Works with dataframes or sf objects
Pass country codes directly: aoe(df, "AT")
Area-based halos enable consistent cross-country comparisons
Use mask for coastlines and hard boundaries
Use area parameter for target halo area (accounts
for masking)
Use aoe_expand() for adaptive expansion to capture
minimum points
Use aoe_sample() for balanced core/halo
sampling
Use aoe_border() for border/line-based
classification
Use aoe_summary() for diagnostics