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

amo*_*ine 7 geocoding r ggmap 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
)
Run Code Online (Sandbox Code Playgroud)

挑战

我想对当前列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()
Run Code Online (Sandbox Code Playgroud)

问题

虽然我可以使上述工作,甚至将新识别的lonlat坐标回到原始数据框架中,整个方案感觉很脏.我相信有利用管道和purrr要经过数据帧和有条件地地理标记基于缺少的位置优雅的方式lonlat.

purrr::pmap在构建完整地址(以及rowwise()by_row())时,我已经陷入了许多兔子洞,包括试图并行穿过多个列.尽管如此,我还是无法构建任何符合优雅解决方案的东西.

提供的任何见解将非常感激.

ali*_*ire 6

实际上,您希望避免拨打geocode任何超过必要的内容,因为它很慢,如果您使用Google,则每天只有2500次查询.因此,最好从同一个调用中创建两个列,这可以通过列表列来完成,使用新的版本的data.frame do或自联接.


1.使用列表列

使用列表列,您可以创建新版本lonlat带有ifelse地理编码(如果有)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
Run Code Online (Sandbox Code Playgroud)

随着 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]
    }))
Run Code Online (Sandbox Code Playgroud)

返回与第一个版本完全相同的东西.


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')))
Run Code Online (Sandbox Code Playgroud)

返回相同的东西,但顶部有新的地理编码行.相同的方法适用于列列或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
Run Code Online (Sandbox Code Playgroud)

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)])
Run Code Online (Sandbox Code Playgroud)

结果与第一个版本相同.


所有版本只调用geocode两次.

请注意,虽然您可以使用purrr该作业,但它并不比常规更适合dplyr.purrr擅长处理列表,虽然列表列是一个选项,但它实际上不必被操纵.