Quick Start"

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  dev = "svglite",
  fig.ext = "svg",
  fig.width = 7,
  fig.height = 5
)
# Save par settings and restore on exit (CRAN policy)
oldpar <- par(no.readonly = TRUE)
knitr::knit_hooks$set(document = function(x) { par(oldpar); x })

Overview

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:

By default, halos are defined as equal area to the core—a proportion-based definition that enables consistent cross-region comparisons.

Getting Started

library(areaOfEffect)
library(sf)

From a Dataframe

The 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.).

From sf Objects

sf objects work directly:

result <- aoe(pts_sf, "AT")

Austria Example

# 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)

# 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)

Basic Usage with Custom Polygons

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)

The result contains only points inside the AoE, with their classification:

result$aoe_class

Multiple Supports

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)

Points can appear multiple times (once per support whose AoE contains them).

Using a Mask (Coastlines)

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)

Real-World Example: Portugal

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")
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)
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.

Scale Parameter

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 |

Area Parameter (Target Halo Area)

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.

# Target area = 1 means halo = original, even after coastline clipping
result <- aoe(pts, support, area = 1, mask = "land")

Adaptive Expansion with 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:

# Strict caps
result <- aoe_expand(pts, support,
                     min_points = 50,
                     max_area = 1.5,    # halo ≤ 1.5× original
                     max_dist = 500)    # max 500m expansion

Check expansion details:

info <- attr(result_expand, "expansion_info")
info

Balanced Sampling with 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)

Custom 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)

For multiple supports, use by = "support" to sample within each:

sampled <- aoe_sample(result_multi, by = "support")

Border Classification with 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:

Area-Based Border Zones

Use the area parameter to specify target zone areas instead of fixed widths:

# Each side's core zone has area 5000 (in CRS units²)
result <- aoe_border(pts, border, area = 5000)

Sampling from Border Results

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)

# Balance by distance class
set.seed(123)
balanced_class <- aoe_sample(result_border, by = "class")
table(balanced_class$aoe_class)

Diagnostics

aoe_summary(result)

Summary



Try the areaOfEffect package in your browser

Any scripts or data that you put into this service are public.

areaOfEffect documentation built on Feb. 7, 2026, 1:08 a.m.