Correcting a dataframe

The tidyverse way.

Ken Butler http://ritsokiguess.site/blogg
04-26-2021

Packages

Introduction

So I had a dataframe today, in which I wanted to make some small corrections. Specifically, I had this one:

my_url <- "http://ritsokiguess.site/datafiles/wisconsin.txt"
wisc <- read_table(my_url)
wisc %>% select(location)
# A tibble: 12 × 1
   location     
   <chr>        
 1 Appleton     
 2 Beloit       
 3 Fort.Atkinson
 4 Madison      
 5 Marshfield   
 6 Milwaukee    
 7 Monroe       
 8 Superior     
 9 Wausau       
10 Dubuque      
11 St.Paul      
12 Chicago      

These are mostly, but not all, cities in Wisconsin, and I want to draw them on a map. To do that, though, I need to affix their states to them, and I thought a good starting point was to start by pretending that they were all in Wisconsin, and then correct the ones that aren’t:

wisc %>% select(location) %>% 
  mutate(state = "WI") -> wisc
wisc
# A tibble: 12 × 2
   location      state
   <chr>         <chr>
 1 Appleton      WI   
 2 Beloit        WI   
 3 Fort.Atkinson WI   
 4 Madison       WI   
 5 Marshfield    WI   
 6 Milwaukee     WI   
 7 Monroe        WI   
 8 Superior      WI   
 9 Wausau        WI   
10 Dubuque       WI   
11 St.Paul       WI   
12 Chicago       WI   

The last three cities are in the wrong state: Dubuque is in Iowa (IA), St. Paul in Minnesota (MN), and Chicago is in Illinois (IL). I know how to fix this in base R: I write something like

wisc$state[12] <- "IL"

but how do you do this the Tidyverse way?

A better way

The first step is to make a small dataframe with the cities that need to be corrected, and the states they are actually in:

corrections <- tribble(
  ~location, ~state,
  "Dubuque", "IA",
  "St.Paul", "MN",
  "Chicago", "IL"
)
corrections
# A tibble: 3 × 2
  location state
  <chr>    <chr>
1 Dubuque  IA   
2 St.Paul  MN   
3 Chicago  IL   

Note that the columns of this dataframe have the same names as the ones in the original dataframe wisc.

So, I was thinking, this is a lookup table (of a sort), and so joining this to wisc might yield something helpful. We want to look up locations and not match states, since we want to have these three cities have their correct state as a possibility. So what does this do?

wisc %>% 
  left_join(corrections, by = "location")
# A tibble: 12 × 3
   location      state.x state.y
   <chr>         <chr>   <chr>  
 1 Appleton      WI      <NA>   
 2 Beloit        WI      <NA>   
 3 Fort.Atkinson WI      <NA>   
 4 Madison       WI      <NA>   
 5 Marshfield    WI      <NA>   
 6 Milwaukee     WI      <NA>   
 7 Monroe        WI      <NA>   
 8 Superior      WI      <NA>   
 9 Wausau        WI      <NA>   
10 Dubuque       WI      IA     
11 St.Paul       WI      MN     
12 Chicago       WI      IL     

Now, we have two states for each city. The first one is always Wisconsin, and the second one is usually missing, but where the state in state.y has a value, that is the true state of the city. So, the thought process is that the actual state should be:

I had an idea that there was a function that would do exactly this, only I couldn’t remember its name, so I couldn’t really search for it. My first thought was na_if. What this does is every time it sees a certain value, it replaces it with NA. This, though, is the opposite way from what I wanted. So I looked at the See Also, and saw replace_na. This replaces NAs with a given value. Not quite right, but closer.

In the See Also for replace_na, I saw one more thing: coalesce, “replace NAs with values from other vectors”. Was that what I was thinking of? It was. The way it works is that you feed it several vectors, and the first one that is not missing gives its value to the result. Hence, what I needed was this:

wisc %>% 
  left_join(corrections, by = "location") %>% 
  mutate(state=coalesce(state.y, state.x))
# A tibble: 12 × 4
   location      state.x state.y state
   <chr>         <chr>   <chr>   <chr>
 1 Appleton      WI      <NA>    WI   
 2 Beloit        WI      <NA>    WI   
 3 Fort.Atkinson WI      <NA>    WI   
 4 Madison       WI      <NA>    WI   
 5 Marshfield    WI      <NA>    WI   
 6 Milwaukee     WI      <NA>    WI   
 7 Monroe        WI      <NA>    WI   
 8 Superior      WI      <NA>    WI   
 9 Wausau        WI      <NA>    WI   
10 Dubuque       WI      IA      IA   
11 St.Paul       WI      MN      MN   
12 Chicago       WI      IL      IL   

Where state.y has a value, it is used; if it’s missing, the value in state.x is used instead.

The best way

I was quite pleased with myself for coming up with this, but I had missed the actual best way of doing this. In SQL, there is UPDATE, and what that does is to take a table of keys to look up and some new values for other columns to replace the ones in the original table. Because dplyr has a lot of things in common with SQL, it is perhaps no surprise that there is a rows_update, and for this job it is as simple as this:

wisc %>% 
  rows_update(corrections) -> wisc
wisc
# A tibble: 12 × 2
   location      state
   <chr>         <chr>
 1 Appleton      WI   
 2 Beloit        WI   
 3 Fort.Atkinson WI   
 4 Madison       WI   
 5 Marshfield    WI   
 6 Milwaukee     WI   
 7 Monroe        WI   
 8 Superior      WI   
 9 Wausau        WI   
10 Dubuque       IA   
11 St.Paul       MN   
12 Chicago       IL   

The values to look up (the “keys”) are by default in the first column, which is where they are in corrections. If they had not been, I would have used a by in the same way as with a join.

Mind. Blown. (Well, my mind was, anyway.)

Geocoding

I said I wanted to draw a map with these cities on it. For that, I need to look up the longitude and latitude of these places, and for that, I need to glue the state onto the name of each city, to make sure I don’t look up the wrong one. It is perhaps easy to forget that unite is the cleanest way of doing this, particularly if you don’t want the individual columns any more:

wisc %>% unite(where, c(location, state), sep = " ") -> wisc
wisc
# A tibble: 12 × 1
   where           
   <chr>           
 1 Appleton WI     
 2 Beloit WI       
 3 Fort.Atkinson WI
 4 Madison WI      
 5 Marshfield WI   
 6 Milwaukee WI    
 7 Monroe WI       
 8 Superior WI     
 9 Wausau WI       
10 Dubuque IA      
11 St.Paul MN      
12 Chicago IL      

The function geocode_OSM from tmaptools will find the longitude and latitude of a place. It expects one place as input, not a vector of placenames, so we will work rowwise to geocode one at a time. (Using map from purrr is also an option.) The geocoder returns a list, which contains, buried a little deeply, the longitudes and latitudes:

wisc %>% 
  rowwise() %>% 
  mutate(ll = list(geocode_OSM(where))) -> wisc
wisc
# A tibble: 12 × 2
# Rowwise: 
   where            ll              
   <chr>            <list>          
 1 Appleton WI      <named list [3]>
 2 Beloit WI        <named list [3]>
 3 Fort.Atkinson WI <named list [3]>
 4 Madison WI       <named list [3]>
 5 Marshfield WI    <named list [3]>
 6 Milwaukee WI     <named list [3]>
 7 Monroe WI        <named list [3]>
 8 Superior WI      <named list [3]>
 9 Wausau WI        <named list [3]>
10 Dubuque IA       <named list [3]>
11 St.Paul MN       <named list [3]>
12 Chicago IL       <named list [3]>

The column ll is a list-column, and the usual way to handle these is to unnest, but that isn’t quite right here:

wisc %>% unnest(ll)
# A tibble: 36 × 2
   where            ll          
   <chr>            <named list>
 1 Appleton WI      <chr [1]>   
 2 Appleton WI      <dbl [2]>   
 3 Appleton WI      <bbox [4]>  
 4 Beloit WI        <chr [1]>   
 5 Beloit WI        <dbl [2]>   
 6 Beloit WI        <bbox [4]>  
 7 Fort.Atkinson WI <chr [1]>   
 8 Fort.Atkinson WI <dbl [2]>   
 9 Fort.Atkinson WI <bbox [4]>  
10 Madison WI       <chr [1]>   
# … with 26 more rows

Unnesting a list of three things produces three rows for each city. It would make more sense to have the unnesting go to the right and produce a new column for each thing in the list. The new tidyr has a variant called unnest_wider that does this:

wisc %>% 
  unnest_wider(ll)
# A tibble: 12 × 4
   where            query            coords    bbox      
   <chr>            <chr>            <list>    <list>    
 1 Appleton WI      Appleton WI      <dbl [2]> <bbox [4]>
 2 Beloit WI        Beloit WI        <dbl [2]> <bbox [4]>
 3 Fort.Atkinson WI Fort.Atkinson WI <dbl [2]> <bbox [4]>
 4 Madison WI       Madison WI       <dbl [2]> <bbox [4]>
 5 Marshfield WI    Marshfield WI    <dbl [2]> <bbox [4]>
 6 Milwaukee WI     Milwaukee WI     <dbl [2]> <bbox [4]>
 7 Monroe WI        Monroe WI        <dbl [2]> <bbox [4]>
 8 Superior WI      Superior WI      <dbl [2]> <bbox [4]>
 9 Wausau WI        Wausau WI        <dbl [2]> <bbox [4]>
10 Dubuque IA       Dubuque IA       <dbl [2]> <bbox [4]>
11 St.Paul MN       St.Paul MN       <dbl [2]> <bbox [4]>
12 Chicago IL       Chicago IL       <dbl [2]> <bbox [4]>

The longitudes and latitudes we want are still hidden in a list-column, the one called coords, so with luck, if we unnest that wider as well, we should be in business:

wisc %>% 
  unnest_wider(ll) %>% 
  unnest_wider(coords) -> wisc
wisc
# A tibble: 12 × 5
   where            query                x     y bbox      
   <chr>            <chr>            <dbl> <dbl> <list>    
 1 Appleton WI      Appleton WI      -88.4  44.3 <bbox [4]>
 2 Beloit WI        Beloit WI        -89.0  42.5 <bbox [4]>
 3 Fort.Atkinson WI Fort.Atkinson WI -88.8  42.9 <bbox [4]>
 4 Madison WI       Madison WI       -89.4  43.1 <bbox [4]>
 5 Marshfield WI    Marshfield WI    -90.2  44.7 <bbox [4]>
 6 Milwaukee WI     Milwaukee WI     -87.9  43.0 <bbox [4]>
 7 Monroe WI        Monroe WI        -89.6  42.6 <bbox [4]>
 8 Superior WI      Superior WI      -92.1  46.6 <bbox [4]>
 9 Wausau WI        Wausau WI        -89.6  45.0 <bbox [4]>
10 Dubuque IA       Dubuque IA       -90.7  42.5 <bbox [4]>
11 St.Paul MN       St.Paul MN       -93.1  44.9 <bbox [4]>
12 Chicago IL       Chicago IL       -87.6  41.9 <bbox [4]>

And now we are. x contains the longitudes (negative for degrees west), and y the latitudes (positive for degrees north).

Making a map with these on them

The most enjoyable way to make a map in R is to use the leaflet package. Making a map is a three-step process:

leaflet(data = wisc) %>% 
  addTiles() %>% 
  addCircleMarkers(lng = ~x, lat = ~y) 

The nice thing about Leaflet maps is that you can zoom, pan and generally move about in them. For example, you can zoom in to find out which city each circle represents.

Citation

For attribution, please cite this work as

Butler (2021, April 26). Ken's Blog: Correcting a dataframe. Retrieved from http://ritsokiguess.site/blogg/posts/2021-11-07-correcting-a-dataframe/

BibTeX citation

@misc{butler2021correcting,
  author = {Butler, Ken},
  title = {Ken's Blog: Correcting a dataframe},
  url = {http://ritsokiguess.site/blogg/posts/2021-11-07-correcting-a-dataframe/},
  year = {2021}
}