1 JSON 파일

R - JSON Files 웹사이트에서 JSON 파일을 다운로드 받아 json_data.json 파일로 저장시킨 후에 이를 데이터프레임으로 변환시킨다.

## json_data.json 데이터셋
{ 
   "ID":["1","2","3","4","5","6","7","8" ],
   "Name":["Rick","Dan","Michelle","Ryan","Gary","Nina","Simon","Guru" ],
   "Salary":["623.3","515.2","611","729","843.25","578","632.8","722.5" ],
   "StartDate":[ "1/1/2012","9/23/2013","11/15/2014","5/11/2014","3/27/2015","5/21/2013",
      "7/30/2013","6/17/2014"],
   "Dept":[ "IT","Operations","IT","HR","Finance","IT","Operations","Finance"]
}

1.1 JSON 시각화

상기 데이터는 5개 노드에 8개 값이 묶여진 전형적인 데이터프레임 자료구조를 갖추고 있다. listviewer 팩키지를 통해 파일 크기가 그다지 크지 않아 시각화를 해본다.

library(tidyverse)
library(jsonlite)

json_list <- jsonlite::fromJSON("data/json_data.json")

listviewer::jsonedit(json_list)

1.2 JSON → 데이터프레임

“r class(json_list)” 명령어를 통해 json_list 객체가 list 리스트임을 알 수 있다. 데이터프레임은 동일한 길이를 갖는 리스트의 특별한 형태임을 알고 있기 때문에 데이터프레임으로 변환시키는 것이 그다지 어렵지 않다. ``

json_tbl <- as_tibble(json_list)

json_tbl
# A tibble: 8 x 5
  ID    Name     Salary StartDate  Dept      
  <chr> <chr>    <chr>  <chr>      <chr>     
1 1     Rick     623.3  1/1/2012   IT        
2 2     Dan      515.2  9/23/2013  Operations
3 3     Michelle 611    11/15/2014 IT        
4 4     Ryan     729    5/11/2014  HR        
5 5     Gary     843.25 3/27/2015  Finance   
6 6     Nina     578    5/21/2013  IT        
7 7     Simon    632.8  7/30/2013  Operations
8 8     Guru     722.5  6/17/2014  Finance   

1.3 데이터프레임 → JSON

데이터프레임을 JSON으로 바꿀 수 있는 방법도 살펴보자. 이유는 나중에 데이터프레임을 RESTful API에 JSON으로 변환시켜 보내는 경우가 흔히 발생하기 때문이다. jsonlite::toJSON() 함수로 JSON 형식으로 변환시킨다. jsonlight::prettify() 함수로 JSON 파일을 눈에 보기 편한 형태로 변환시킨다.

json_tbl %>% 
  select(Name, Salary) %>% 
  filter(Salary > 700) %>% 
  jsonlite::toJSON() %>% 
  prettify(., indent = 4)
[
    {
        "Name": "Ryan",
        "Salary": "729"
    },
    {
        "Name": "Gary",
        "Salary": "843.25"
    },
    {
        "Name": "Guru",
        "Salary": "722.5"
    }
]
 

2 스타워즈 JSON

dplyr 팩키지에 starwars 데이터프레임이 포함되어 있다. JSON 데이터로 저장시킨 후에 이를 불러와서 JSON 파일로 사용한다. 다시 한번 강조하지만 R에서 불러들인 JSON 파일은 list 자료구조를 갖게 된다. 데이터프레임과 동일하지만, films 칼럼에 영화가 최소 하나에서 다수 출연한 영화명이 정리되어 있다.

starwars %>% 
  select(name, height, sex, films) %>% 
  jsonlite::write_json("data/starwars.json")

starwars_list <- jsonlite::fromJSON("data/starwars.json")

listviewer::jsonedit(starwars_list)

먼저 JSON 데이터를 본격적으로 들어가기 전에 스타워즈 영화에 가장 많이 출연한 배우가 누구인지 찾아보고 그 배우가 출연한 영화를 출력해보자. 리스트를 데이터프레임으로 변환시킨 후에 purrr 팩키지를 사용해서 films 칼럼에 포함된 값의 길이를 length() 함수로 갯수를 센 다음 내림차순으로 정리하고 “R2-D2”가 최다 출연 배우임을 확인한 후에 “R2-D2”가 출연한 영화를 쭉 출력한다.

starwars_list %>% 
  as_tibble() %>% 
  mutate(num_appearance = map_int(films, length)) %>% 
  arrange(desc(num_appearance))
# A tibble: 87 x 5
   name           height sex    films     num_appearance
   <chr>           <int> <chr>  <list>             <int>
 1 R2-D2              96 none   <chr [7]>              7
 2 C-3PO             167 none   <chr [6]>              6
 3 Obi-Wan Kenobi    182 male   <chr [6]>              6
 4 Luke Skywalker    172 male   <chr [5]>              5
 5 Leia Organa       150 female <chr [5]>              5
 6 Chewbacca         228 male   <chr [5]>              5
 7 Yoda               66 male   <chr [5]>              5
 8 Palpatine         170 male   <chr [5]>              5
 9 Darth Vader       202 male   <chr [4]>              4
10 Han Solo          180 male   <chr [4]>              4
# … with 77 more rows
starwars_list %>% 
  as_tibble() %>% 
  filter(str_detect(name, "R2-D2")) %>% 
  pull(films) %>% 
  unlist()
[1] "The Empire Strikes Back" "Attack of the Clones"   
[3] "The Phantom Menace"      "Revenge of the Sith"    
[5] "Return of the Jedi"      "A New Hope"             
[7] "The Force Awakens"      

2.1 JSON 파일 가져오기

먼저, JSON 파일으로 리스트(starwars_json)로 변환시킨 후에 unnest() 동사로 깔끔한 데이터 형태로 변환시킨다. 이런 경우 as_tibble()unnest() 두단계를 거치게 된다.

starwars_json <- jsonlite::fromJSON("data/starwars.json")

starwars_json %>% 
  as_tibble() %>% 
  unnest(films)
# A tibble: 173 x 4
   name           height sex   films                  
   <chr>           <int> <chr> <chr>                  
 1 Luke Skywalker    172 male  The Empire Strikes Back
 2 Luke Skywalker    172 male  Revenge of the Sith    
 3 Luke Skywalker    172 male  Return of the Jedi     
 4 Luke Skywalker    172 male  A New Hope             
 5 Luke Skywalker    172 male  The Force Awakens      
 6 C-3PO             167 none  The Empire Strikes Back
 7 C-3PO             167 none  Attack of the Clones   
 8 C-3PO             167 none  The Phantom Menace     
 9 C-3PO             167 none  Revenge of the Sith    
10 C-3PO             167 none  Return of the Jedi     
# … with 163 more rows

상기 과정이 번잡하게 느껴진다면 바로 unnest_longer() 혹은 unnest_wider() 동사를 사용해서 깔끔한 데이터로 변환시킨다.

starwars_json %>% 
  unnest_longer(films)
# A tibble: 173 x 4
   name           height sex   films                  
   <chr>           <int> <chr> <chr>                  
 1 Luke Skywalker    172 male  The Empire Strikes Back
 2 Luke Skywalker    172 male  Revenge of the Sith    
 3 Luke Skywalker    172 male  Return of the Jedi     
 4 Luke Skywalker    172 male  A New Hope             
 5 Luke Skywalker    172 male  The Force Awakens      
 6 C-3PO             167 none  The Empire Strikes Back
 7 C-3PO             167 none  Attack of the Clones   
 8 C-3PO             167 none  The Phantom Menace     
 9 C-3PO             167 none  Revenge of the Sith    
10 C-3PO             167 none  Return of the Jedi     
# … with 163 more rows

3 구글 위경도1

Rectangling - Geocoding with google에 나온 예제로 주소를 넣어 GeoCoding을 해야 되는데 이를 위해서 Google Maps API를 사용해서 특정 주소 (“서울시 대한민국”)를 넣어 위경도를 뽑아내보자.

readRenviron("~/.Renviron")

geocode <- function(address, api_key = Sys.getenv("GOOGLE_MAPS_API_KEY")) {
  url <- "https://maps.googleapis.com/maps/api/geocode/json"
  url <- paste0(url, "?address=", URLencode(address), "&key=", api_key)

  jsonlite::read_json(url)
}

seoul_list <- geocode("서울시 대한민국")

listviewer::jsonedit(seoul_list)

3.1 위경도 추출

전통적인 방식으로 한땀 한땀 정성스럽게 위도와 경도를 추출해보자.

location_df <- tibble(
  location = "서울시",
  json = seoul_list
) %>% 
  slice(1)

location_df %>% 
  unnest_longer(json) %>% 
  unnest_longer(json) %>% 
  filter(json_id == "geometry") %>% 
  unnest(json) %>% 
  unnest_wider(json) %>% 
  filter(!is.na(lat)) %>% 
  select(location, lat, lng)
# A tibble: 1 x 3
  location   lat   lng
  <chr>    <dbl> <dbl>
1 서울시    37.6  127.

3.2 대한민국 도시 위경도

Rectangling - Geocoding with google 웹사이트에 나온 사례를 그대로 구현해보자.

city <- c("Houston", "LA", "New York", "Chicago", "Springfield")
city_geo <- purrr::map(city, geocode)

loc_tbl <- tibble(city = city, json = city_geo)
loc_tbl
# A tibble: 5 x 2
  city        json            
  <chr>       <list>          
1 Houston     <named list [2]>
2 LA          <named list [2]>
3 New York    <named list [2]>
4 Chicago     <named list [2]>
5 Springfield <named list [2]>
loc_tbl %>%
  unnest_wider(json) %>%
  unnest_longer(results) %>%
  unnest_wider(results) %>%
  unnest_wider(geometry) %>%
  unnest_wider(location) %>% 
  select(city, formatted_address, lat, lng)
# A tibble: 14 x 4
   city      formatted_address                                        lat    lng
   <chr>     <chr>                                                  <dbl>  <dbl>
 1 Houston   Houston, TX, USA                                        29.8  -95.4
 2 LA        Los Angeles, CA, USA                                    34.1 -118. 
 3 New York  New York, NY, USA                                       40.7  -74.0
 4 Chicago   Chicago, IL, USA                                        41.9  -87.6
 5 Springfi… Springfield, MO, USA                                    37.2  -93.3
 6 Springfi… 12 Burns Ave, Springfield, MN 56087, USA                44.2  -95.0
 7 Springfi… Springfield, IL, USA                                    39.8  -89.7
 8 Springfi… Springfield Plain, Oklahoma 74370, USA                  36.7  -94.6
 9 Springfi… 840 N Boonville Ave, Springfield, MO 65802, USA         37.2  -93.3
10 Springfi… Springfield-Branson National Airport (SGF), 2300 N Ai…  37.2  -93.4
11 Springfi… 955 E Trafficway St, Springfield, MO 65802, USA         37.2  -93.3
12 Springfi… 840 N Boonville Ave, Springfield, MO 65802, USA         37.2  -93.3
13 Springfi… Springfield, OH, USA                                    39.9  -83.8
14 Springfi… 305 S Market Ave, Springfield, MO 65806, USA            37.2  -93.3

unnest_wider(), unnest_wider() 을 조합해서 필요한 위경도 정보를 추출하는 것이 그다지 효율적이지 않을 수도 있어 unnest_auto() 명령어를 사용해서 필요하는 위경도 정보를 추출한다.

loc_tbl %>%
  unnest_auto(json) %>%
  unnest_auto(results) %>%
  unnest_auto(results) %>%
  unnest_auto(geometry) %>%
  unnest_auto(location) %>% 
  select(city, formatted_address, lat, lng)
# A tibble: 14 x 4
   city      formatted_address                                        lat    lng
   <chr>     <chr>                                                  <dbl>  <dbl>
 1 Houston   Houston, TX, USA                                        29.8  -95.4
 2 LA        Los Angeles, CA, USA                                    34.1 -118. 
 3 New York  New York, NY, USA                                       40.7  -74.0
 4 Chicago   Chicago, IL, USA                                        41.9  -87.6
 5 Springfi… Springfield, MO, USA                                    37.2  -93.3
 6 Springfi… 12 Burns Ave, Springfield, MN 56087, USA                44.2  -95.0
 7 Springfi… Springfield, IL, USA                                    39.8  -89.7
 8 Springfi… Springfield Plain, Oklahoma 74370, USA                  36.7  -94.6
 9 Springfi… 840 N Boonville Ave, Springfield, MO 65802, USA         37.2  -93.3
10 Springfi… Springfield-Branson National Airport (SGF), 2300 N Ai…  37.2  -93.4
11 Springfi… 955 E Trafficway St, Springfield, MO 65802, USA         37.2  -93.3
12 Springfi… 840 N Boonville Ave, Springfield, MO 65802, USA         37.2  -93.3
13 Springfi… Springfield, OH, USA                                    39.9  -83.8
14 Springfi… 305 S Market Ave, Springfield, MO 65806, USA            37.2  -93.3

동일한 방식으로 대한민국 대표(?) 도시를 5개 추출하여 위경도를 포함한 데이터프레임으로 제작해보자.

korea_city <- c("서울시", "대전시", "속초시", "부산", "성남시")
korea_city_geo <- purrr::map(korea_city, geocode)

korea_loc_tbl <- tibble(city = korea_city, json = korea_city_geo)
korea_loc_tbl
# A tibble: 5 x 2
  city   json            
  <chr>  <list>          
1 서울시 <named list [2]>
2 대전시 <named list [2]>
3 속초시 <named list [2]>
4 부산   <named list [2]>
5 성남시 <named list [2]>

unnest_auto() 명령어를 사용하여 원하는 결과를 추출해본다.

korea_loc_tbl %>%
  unnest_auto(json) %>%
  unnest_auto(results) %>%
  unnest_auto(results) %>%
  unnest_auto(geometry) %>%
  unnest_auto(location) %>% 
  select(city, formatted_address, lat, lng)
# A tibble: 5 x 4
  city   formatted_address                       lat   lng
  <chr>  <chr>                                 <dbl> <dbl>
1 서울시 Seoul, South Korea                     37.6  127.
2 대전시 Daejeon, South Korea                   36.4  127.
3 속초시 Sokcho-si, Gangwon-do, South Korea     38.2  129.
4 부산   Busan, South Korea                     35.2  129.
5 성남시 Seongnam-si, Gyeonggi-do, South Korea  37.4  127.

4 hoist() 필요한 것만 쏙!!!

hoist() 함수를 사용해서 위도와 경도만 쏙 빼낼 수 있다.

korea_loc_tbl %>%
  hoist(json,
    lat = list("results", 1, "geometry", "location", "lat"),
    lng = list("results", 1, "geometry", "location", "lng")
  )
# A tibble: 5 x 4
  city     lat   lng json            
  <chr>  <dbl> <dbl> <list>          
1 서울시  37.6  127. <named list [2]>
2 대전시  36.4  127. <named list [2]>
3 속초시  38.2  129. <named list [2]>
4 부산    35.2  129. <named list [2]>
5 성남시  37.4  127. <named list [2]>

unnest_*()hoist() 두 함수를 조합하여 동일한 결과를 얻어낼 수도 있다.

korea_loc_tbl %>%
  unnest_wider(json) %>%
  hoist(results, first_result = 1) %>%
  unnest_wider(first_result) %>%
  unnest_wider(geometry) %>%
  unnest_wider(location)
# A tibble: 5 x 11
  city  address_compone… formatted_addre… bounds   lat   lng location_type
  <chr> <list>           <chr>            <list> <dbl> <dbl> <chr>        
1 서울시… <list [2]>       Seoul, South Ko… <name…  37.6  127. APPROXIMATE  
2 대전시… <list [3]>       Daejeon, South … <name…  36.4  127. APPROXIMATE  
3 속초시… <list [3]>       Sokcho-si, Gang… <name…  38.2  129. APPROXIMATE  
4 부산  <list [3]>       Busan, South Ko… <name…  35.2  129. APPROXIMATE  
5 성남시… <list [3]>       Seongnam-si, Gy… <name…  37.4  127. APPROXIMATE  
# … with 4 more variables: viewport <list>, place_id <chr>, types <list>,
#   status <chr>
 

데이터 과학자 이광춘 저작

kwangchun.lee.7@gmail.com