Purrr-Fection:寻找利用 Purrr 的条件数据帧操作的优雅解决方案

Purrr-Fection: In Search of An Elegant Solution to Conditional Data Frame Operations Leveraging Purrr

背景

我有一个问题,可能有多种解决方案,但我相信有一个尚未发现的利用 purrr 的优雅解决方案。

示例代码

我有一个大数据框如下,我在下面包含了一个例子:

library(tibble)
library(ggmap)
library(purrr)
library(dplyr)

# Define Example Data
df <- frame_data(
  ~Street,                ~City,        ~State,     ~Zip,  ~lon,      ~lat,
  "226 W 46th St",        "New York",   "New York", 10036, -73.9867,  40.75902,
  "5th Ave",              "New York",   "New York", 10022, NA,        NA,
  "75 Broadway",          "New York",   "New York", 10006, -74.01205, 40.70814,
  "350 5th Ave",          "New York",   "New York", 10118, -73.98566, 40.74871,
  "20 Sagamore Hill Rd",  "Oyster Bay", "New York", 11771, NA,        NA,
  "45 Rockefeller Plaza", "New York",   "New York", 10111, -73.97771, 40.75915
)

挑战

我想对 lonlat 列当前为 NA 的所有位置进行地理标记。我有很多方法可以解决这个问题,其中之一如下所示:

# Safe Code is Great Code
safe_geocode <- safely(geocode)

# Identify Data to be Geotagged by Absence of lon and lat
data_to_be_geotagged <- df %>% filter(is.na(lon) | is.na(lat))

# GeoTag Addresses of Missing Data Points
fullAddress <- paste(data_to_be_geotagged$Street,
                     data_to_be_geotagged$City,
                     data_to_be_geotagged$State,
                     data_to_be_geotagged$Zip,
                     sep = ", ")

fullAddress %>% 
  map(safe_geocode) %>% 
  map("result") %>%
  plyr::ldply()

问题

虽然我可以让上面的方法起作用,甚至可以将新识别的 lonlat 坐标重新放入原始数据框中,但整个方案感觉很脏。我相信有一种优雅的方法可以利用管道和 purrr 来遍历数据框,并根据缺少 lonlat.

有条件地对位置进行地理标记

我在构建完整地址时试图并行遍历多个列(以及 rowwise()by_row() ).然而,我未能构建任何符合优雅解决方案条件的东西。

如有任何见解,我们将不胜感激。

我不确定 purrr 但下面是使用管道的内容:

df <- frame_data(
  ~Street,                ~City,        ~State,     ~Zip,  ~lon,      ~lat,
  "226 W 46th St",        "New York",   "New York", 10036, -73.9867,  40.75902,
  "5th Ave",              "New York",   "New York", 10022, NA,        NA,
  "75 Broadway",          "New York",   "New York", 10006, -74.01205, 40.70814,
  "350 5th Ave",          "New York",   "New York", 10118, -73.98566, 40.74871,
  "20 Sagamore Hill Rd",  "Oyster Bay", "New York", 11771, NA,        NA,
  "45 Rockefeller Plaza", "New York",   "New York", 10111, -73.97771, 40.75915
)

df2<-df %>%
  filter(is.na(lon) | is.na(lat)) %>%
  group_by(Street, City, State) %>% #not really necessary but it suppresses a warning
  mutate(lon=ifelse(is.na(lon) | is.na(lat), 
    geocode(paste(Street, City,State, sep=" ")), 0)) %>%
  mutate(lat=ifelse(is.na(lon) | is.na(lat), 
    rev(geocode(paste(Street, City,State, sep=" "))), 0))

如果您想要上面示例代码中的部分输出:

as.data.frame(df2)[,5:6]
       lon       lat
1 40.77505 -73.96515
2 40.88259 -73.50538

或包括所有列:

as.data.frame(df2)
              Street       City    State   Zip      lon       lat
1             5th Ave   New York New York 10022 40.77505 -73.96515
2 20 Sagamore Hill Rd Oyster Bay New York 11771 40.88259 -73.50538

如果您想将原始数据与新数据结合起来,您可以执行以下操作:

as.data.frame(rbind(filter(df, !is.na(lon) | !is.na(lat)),df2 ))
                Street       City    State   Zip       lon       lat
1        226 W 46th St   New York New York 10036 -73.98670  40.75902
2          75 Broadway   New York New York 10006 -74.01205  40.70814
3          350 5th Ave   New York New York 10118 -73.98566  40.74871
4 45 Rockefeller Plaza   New York New York 10111 -73.97771  40.75915
5              5th Ave   New York New York 10022  40.77505 -73.96515
6  20 Sagamore Hill Rd Oyster Bay New York 11771 -73.96515  40.77505

...或者您可以像下面这样将其全部简化(保持原始顺序):

df2<-df %>%
  #group_by(Street, City, State) %>% # unescape if you want to suppress warning
  mutate(lon=ifelse(is.na(lon) | is.na(lat), 
    geocode(paste(Street, City,State, sep=" ")), lon)) %>%
  mutate(lat=ifelse(is.na(lon) | is.na(lat), 
    rev(geocode(paste(Street, City,State, sep=" "))), lat))

as.data.frame(df2)
                Street       City    State   Zip       lon       lat
1        226 W 46th St   New York New York 10036 -73.98670  40.75902
2              5th Ave   New York New York 10022 -73.98670  40.75902
3          75 Broadway   New York New York 10006 -74.01205  40.70814
4          350 5th Ave   New York New York 10118 -73.98566  40.74871
5  20 Sagamore Hill Rd Oyster Bay New York 11771  40.75902 -73.98670
6 45 Rockefeller Plaza   New York New York 10111 -73.97771  40.75915

真的,您要避免调用 geocode 不必要的次数,因为它很慢,而且如果您使用 Google,您每天只有 2500 个查询。因此,最好从同一个调用中创建两列,这可以通过列表列来完成,使用 do 或自连接创建 data.frame 的新版本。


1。带有列表列

使用列表列,您可以使用 ifelse 创建 lonlat 的新版本,如果有 NA 则进行地理编码,否则只需复制现有版本值。之后,删除旧版本的列并取消嵌套新的列:

library(dplyr)
library(ggmap)
library(tidyr)    # For `unnest`

       # Evaluate each row separately
df %>% rowwise() %>% 
    # Add a list column. If lon or lat are NA,
    mutate(data = ifelse(any(is.na(c(lon, lat))), 
                         # return a data.frame of the geocoded results,
                         list(geocode(paste(Street, City, State, Zip))), 
                         # else return a data.frame of existing columns.
                         list(data_frame(lon = lon, lat = lat)))) %>% 
    # Remove old columns
    select(-lon, -lat) %>% 
    # Unnest newly created ones from list column
    unnest(data)

## # A tibble: 6 × 6
##                 Street       City    State   Zip       lon      lat
##                  <chr>      <chr>    <chr> <dbl>     <dbl>    <dbl>
## 1        226 W 46th St   New York New York 10036 -73.98670 40.75902
## 2              5th Ave   New York New York 10022 -73.97491 40.76167
## 3          75 Broadway   New York New York 10006 -74.01205 40.70814
## 4          350 5th Ave   New York New York 10118 -73.98566 40.74871
## 5  20 Sagamore Hill Rd Oyster Bay New York 11771 -73.50538 40.88259
## 6 45 Rockefeller Plaza   New York New York 10111 -73.97771 40.75915

2。随着 do

do,另一方面,从旧的部分创建一个全新的 data.frame。它需要稍微笨拙的 $ 符号,用 . 表示分组的 data.frame 管道输入。使用 ifelse 而不是 ifelse 可以让你避免在列表中嵌套结果(无论如何它们必须在上面)。

       # Evaluate each row separately
df %>% rowwise() %>% 
    # Make a new data.frame from the first four columns and the geocode results or existing lon/lat
    do(bind_cols(.[1:4], if(any(is.na(c(.$lon, .$lat)))){
        geocode(paste(.[1:4], collapse = ' '))
    } else {
        .[5:6]
    }))

returns与第一个版本完全一样。


3。在一个子集上,用自连接重新组合

如果 ifelse 过于混乱,您可以只对一个子集进行地理编码,然后通过将行绑定到 anti_join 重新组合,即 df 中的所有行,但是不是子集 .:

df %>% filter(is.na(lon) | is.na(lat)) %>% 
    select(1:4) %>% 
    bind_cols(geocode(paste(.$Street, .$City, .$State, .$Zip))) %>% 
    bind_rows(anti_join(df, ., by = c('Street', 'Zip')))

与 returns 相同,但新地理编码的行位于顶部。同样的方法适用于列表列或 do,但由于不需要组合两组列,因此只需 bind_cols 即可。


4。在 mutate_geocode

的子集上

ggmap 实际上包含一个 mutate_geocode 函数,当传递 data.frame 和一列地址时,该函数将添加 lon 和 lat 列。它有一个问题:它不能接受超过一个地址的列名,因此需要一个包含整个地址的列。因此,虽然这个版本可能非常好,但它需要创建和删除一个包含整个地址的额外列,使其不简洁:

df %>% filter(is.na(lon) | is.na(lat)) %>% 
    select(1:4) %>% 
    mutate(address = paste(Street, City, State, Zip)) %>%    # make an address column
    mutate_geocode(address) %>% 
    select(-address) %>%    # get rid of address column
    bind_rows(anti_join(df, ., by = c('Street', 'Zip')))

##                 Street       City    State   Zip       lon      lat
## 1              5th Ave   New York New York 10022 -73.97491 40.76167
## 2  20 Sagamore Hill Rd Oyster Bay New York 11771 -73.50538 40.88259
## 3 45 Rockefeller Plaza   New York New York 10111 -73.97771 40.75915
## 4          350 5th Ave   New York New York 10118 -73.98566 40.74871
## 5          75 Broadway   New York New York 10006 -74.01205 40.70814
## 6        226 W 46th St   New York New York 10036 -73.98670 40.75902

5。基础 R

Base R可以直接赋值给一个子集,这使得这里的习语简单很多,即使它需要很多子集:

df[is.na(df$lon) | is.na(df$lat), c('lon', 'lat')] <- geocode(paste(df$Street, df$City, df$State, df$Zip)[is.na(df$lon) | is.na(df$lat)])

结果与第一个版本相同。


所有版本只调用 geocode 两次。

请注意,虽然您 可以 使用 purrr 来完成这项工作,但它并不比常规 dplyr 更适合。 purrr 擅长处理列表,虽然列表列是一种选择,但实际上并不需要对其进行操作。

使用 dplyr:

df %>% mutate( lon = case_when( is.na(lon) ~ geocode(paste(Street, City, State, Zip))[,1],
                                TRUE       ~ lon),

               lat = case_when( is.na(lat) ~ geocode(paste(Street, City, State, Zip))[,2],
                                TRUE       ~ lat )
               )