Для некоторых объектов атрибут идентифицирует специальный столбец, например столбец геометрии в объекте sf
. Для проведения некоторых расчетов в dplyr
было бы хорошо легко идентифицировать эти столбцы. Я ищу способ создать функцию, которая помогает идентифицировать этот столбец. В приведенном ниже примере я могу создать функцию, которая идентифицирует этот столбец, но мне все еще нужно использовать оператор вставки rlang
(!!!
).
require(sf)
require(dplyr)
n<-4
df = st_as_sf(data.frame(x = 1:n, y = 1:n, cat=gl(2,2)), coords = 1:2, crs = 3857) %>% group_by(cat)
# this is the example I start from however the geometry column is not guaranteed to have that name
df %>% mutate(d=st_distance(geometry, geometry[row_number()==1]))
#> Simple feature collection with 4 features and 2 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: 1 ymin: 1 xmax: 4 ymax: 4
#> Projected CRS: WGS 84 / Pseudo-Mercator
#> # A tibble: 4 × 3
#> # Groups: cat [2]
#> cat geometry d[,1]
#> * <fct> <POINT [m]> [m]
#> 1 1 (1 1) 0
#> 2 1 (2 2) 1.41
#> 3 2 (3 3) 0
#> 4 2 (4 4) 1.41
# this works, however the code does not get easier to read
df %>% mutate(d=st_distance(!!!syms(attr(., "sf_column")), (!!!syms(attr(., "sf_column")))[row_number()==1]))
#> Simple feature collection with 4 features and 2 fields
#> ...
#> 4 2 (4 4) 1.41
# this works and is already better:
geometry_name<-function(x) syms(attr(x, 'sf_column'))
df %>% mutate(d=st_distance(!!!geometry_name(.), (!!!geometry_name(.))[row_number()==1]))
#> Simple feature collection with 4 features and 2 fields
#> ...
#> 4 2 (4 4) 1.41
В идеале я хотел бы найти функцию, которая заставляет работать следующий код, так как это будет проще всего для пользователей:
df %>% mutate(d=st_distance(geometry_name(), geometry_name()[row_number()==1]))
Вызов такого рода функций без аргументов требует, чтобы вы предполагали, что символы присутствуют в вызывающем фрейме (в данном случае заполнитель .
и местоимение .data
), поэтому вне глаголов dplyr
он не будет работать должным образом, но если это подходит для вашего рабочего процесса, тогда вы можете сделать:
geometry_name <- function() {
.data <- eval(quote(.data), parent.frame())
nms <- names(eval(quote(.), parent.frame()))
geo <- which(sapply(nms, function(x) inherits(.data[[x]], 'sfc')))
if (length(geo) == 0) {
stop('No geometry column detected')
}
if (length(geo) > 1) {
warning('More than one geometry column. Only the first will be used.')
geo <- geo[1]
}
.data[[nms[geo]]]
}
Используя ваш пример, это позволяет вам использовать указанный вами синтаксис:
df %>%
mutate(d = st_distance(geometry_name(), geometry_name()[row_number()==1]))
#> Simple feature collection with 4 features and 2 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: 1 ymin: 1 xmax: 4 ymax: 4
#> Projected CRS: WGS 84 / Pseudo-Mercator
#> # A tibble: 4 x 3
#> # Groups: cat [2]
#> cat geometry d[,1]
#> * <fct> <POINT [m]> [m]
#> 1 1 (1 1) 0
#> 2 1 (2 2) 1.41
#> 3 2 (3 3) 0
#> 4 2 (4 4) 1.41
Потенциально вы могли бы сделать функцию немного более полезной, разрешив ей принимать аргумент data
, который, если missing
запускает приведенный выше код (после проверки наличия .
и .data
), но в противном случае просто находит и возвращает столбец sf
из data
. Это позволит использовать глаголы вне dplyr
, но сохранит желаемое поведение внутри dplyr
.
Например:
geometry_name <- function(data) {
if (missing(data)) {
.data <- tryCatch( {
eval(quote(.data), parent.frame())
}, error = function(e){
stop("Argument 'data' missing, with no default")
})
plchlder <- tryCatch({
eval(quote(.), parent.frame())
}, error = function(e) {
stop("geometry_name can only be used without a 'data' argument ",
"inside dplyr verbs")
})
nms <- names(plchlder)
geo <- which(sapply(nms, function(x) inherits(.data[[x]], 'sfc')))
if (length(geo) == 0) {
stop('No geometry column detected')
}
if (length(geo) > 1) {
warning('More than one geometry column. Only the first will be used.')
geo <- geo[1]
}
return(.data[[nms[geo]]])
}
geo <- which(sapply(data, function(x) inherits(x, 'sfc')))
if (length(geo) == 0) stop('No geometry column detected')
if (length(geo) > 1) {
warning('More than one geometry column. Only the first will be used.')
geo <- geo[1]
}
return(data[[geo]])
}
Что дает следующее поведение
geometry_name(df)
#> [1] "geometry"
geometry_name()
#> Error in value[[3L]](cond) :
#> geometry_name can only be used without a 'data' argument inside
#> dplyr verbs
df %>%
mutate(d = st_distance(geometry_name(), geometry_name()[row_number()==1]))
#> Simple feature collection with 4 features and 2 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: 1 ymin: 1 xmax: 4 ymax: 4
#> Projected CRS: WGS 84 / Pseudo-Mercator
#> # A tibble: 4 x 3
#> # Groups: cat [2]
#> cat geometry d[,1]
#> * <fct> <POINT [m]> [m]
#> 1 1 (1 1) 0
#> 2 1 (2 2) 1.41
#> 3 2 (3 3) 0
#> 4 2 (4 4) 1.41
Используйте оператор {{
после извлечения имени столбца геометрии в качестве символа.
gcol = sym(attr(df, "sf_column"))
df %>%
mutate(d = st_distance({{gcol}}, {{gcol}}[row_number() == 1]))
#> Simple feature collection with 4 features and 2 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: 1 ymin: 1 xmax: 4 ymax: 4
#> Projected CRS: WGS 84 / Pseudo-Mercator
#> # A tibble: 4 × 3
#> # Groups: cat [2]
#> cat geometry d[,1]
#> * <fct> <POINT [m]> [m]
#> 1 1 (1 1) 0
#> 2 1 (2 2) 1.41
#> 3 2 (3 3) 0
#> 4 2 (4 4) 1.41
Это отличное решение! Хорошо чему-то научиться. Я заметил, что он работает только с трубками
magrittr
(%>%
), а не с трубкой по умолчанию (|>
). Я попытался посмотреть, смогу ли я заставить его работать с заполнителем_
для базовых каналов R, но, поскольку он работает совсем по-другому и на самом деле является всего лишь заполнителем, я не уверен, что это возможно. Спасибо!