library(sf) library(nngeo) # create a sf object from your data frame sf <- st_as_sf(df, coords = c("LAT", "LONG"), crs = 4326) # iterate over individual nests, filter by year and territory before `st_nn` for (i in 1:dim(df)[1]) { nest <- sf[i, ] # which neighbours to consider? exlude self first n_nests <- sf[-i, ] |> dplyr::filter(HD_YEAR == nest[["HD_YEAR"]] & TERRITORY != nest[["TERRITORY"]]) # skip iteration if there are no neighbours if (dim(n_nests)[1] == 0) { next } else { # perform nearest neighbour search for simple features result <- st_nn(nest, n_nests, k = 3, returnDist = TRUE) } sf[i, "n1_name"] <- sf$NEST_NUMBER[result[["nn"]][[1]][1]]n_nests$NEST_NUMBER[result[["nn"]][[1]][1]] sf[i, "n1_dist"] <- result[["dist"]][[1]][1] sf[i, "n2_name"] <- sf$NEST_NUMBER[result[["nn"]][[1]][2]]n_nests$NEST_NUMBER[result[["nn"]][[1]][2]] sf[i, "n2_dist"] <- result[["dist"]][[1]][2] sf[i, "n3_name"] <- sf$NEST_NUMBER[result[["nn"]][[1]][3]]n_nests$NEST_NUMBER[result[["nn"]][[1]][3]] sf[i, "n3_dist"] <- result[["dist"]][[1]][3] } # inspect result sf #> Simple feature collection with 50 features and 10 fields #> Geometry type: POINT #> Dimension: XY #> Bounding box: xmin: 33.41594 ymin: -79.0571 xmax: 33.83423 ymax: -78.54372 #> Geodetic CRS: WGS 84 #> First 10 features: #> NEST_NUMBER TERRITORY HATCH_DATE HD_YEAR geometry #> 1 LOSH-2019-01 RIVERF 2019-03-26 2019 POINT (33.8313 -79.0466) #> 2 LOSH-2019-02 DISCGO 2019-04-03 2019 POINT (33.7975 -79.0038) #> 3 LOSH-2019-03 SCHOLA 2019-04-06 2019 POINT (33.7963 -79.0057) #> 4 LOSH-2019-04 COXFER 2019-04-17 2019 POINT (33.8071 -79.0085) #> 5 LOSH-2019-05 LOWESP 2019-04-01 2019 POINT (33.7924 -78.9978) #> 6 LOSH-2019-06 SEACOA 2019-04-04 2019 POINT (33.8075 -79.0132) #> 7 LOSH-2019-07 HGTCCC 2019-04-12 2019 POINT (33.7936 -79.0015) #> 8 LOSH-2019-08 WPDEDI 2019-04-17 2019 POINT (33.80068 -79.0006) #> 9 LOSH-2019-09 CMWSSS 2019-04-24 2019 POINT (33.79995 -78.99844) #> 10 LOSH-2019-10 IMSTOP 2019-04-29 2019 POINT (33.7998 -78.9945) #> n1_name n1_dist n2_name n2_dist n3_name n3_dist #> 1 LOSH-2019-2426 60.95754 LOSH-2019-1517 68.19119 LOSH-2019-1618 1173.5578 #> 2 LOSH-2019-3941 199.82044 LOSH-2019-0203 213.67488 LOSH-2019-2730 223.3503 #> 3 LOSH-2019-2627 195.30050 LOSH-2019-02 213.67488 LOSH-2019-0304 388.1173 #> 4 LOSH-2019-2730 369.08852 LOSH-2019-03 388.11729 LOSH-2019-3941 390.0631 #> 5 LOSH-2019-3739 149.19667 LOSH-2019-0809 176.01593 LOSH-2019-3335 201.1317 #> 6 LOSH-2019-2223 491.31455 LOSH-2019-4144 502.51049 LOSH-2019-04 524.8380 #> 7 LOSH-2019-2425 161.56390 LOSH-2019-0708 181.28822 LOSH-2019-3840 189.7436 #> 8 LOSH-2019-07 181.28822 LOSH-2019-2931 189.42955 LOSH-2019-0809 241.7774 #> 9 LOSH-2019-3233 169.80511 LOSH-2019-05 176.01593 LOSH-2019-3840 230.9364 #> 10 LOSH-2019-3435 216.08536 LOSH-2019-3132 263.90729 LOSH-2019-2729 362.9705 Order has changed at first glance in comparison to the first approach, because indices are determined now based on the overall sf object, not anyanother subset (internally). Moreover, names were added directly to sf instead of listing decoupled indices and distances.