지도 기본기

지도 그리기 기본기

저자
소속

1 데이터셋

1.1 충전소 데이터

공공데이터포털에서 한국전력공사 전기차 충전소 설치현황 엑셀 데이터를 다운로드 받는다. 이를 위해서 전력데이터 개방 포털시스템 전기차 충전소 설치 현황에서 전국을 선택하고 엑셀파일을 다운로드 한다.

코드
library(readxl)
library(tidyverse)

charger_raw <- read_excel("data/전기차 충전소 설치현황_20230822.xlsx", skip = 2)

charger_raw
#> # A tibble: 4,692 × 6
#>    시구  설치장소             주소    `급속충전기(대)` `완속충전기(대)` 지원차종
#>    <chr> <chr>                <chr>   <chr>            <chr>            <chr>   
#>  1 전체  가평소방서           경기도… 1                0                SM3 Z.E…
#>  2 전체  가평종합운동장       경기도… 2                0                BMW i3,…
#>  3 전체  가평지사             경기도… 1                1                SM3 Z.E…
#>  4 전체  가평하수도사업소     경기도… 1                0                SM3 Z.E…
#>  5 전체  상면사무소           경기도… 1                0                SM3 Z.E…
#>  6 전체  설악면사무소         경기도… 1                0                SM3 Z.E…
#>  7 전체  조종면사무소         경기도… 1                0                SM3 Z.E…
#>  8 전체  조종소방서           경기도… 1                0                SM3 Z.E…
#>  9 전체  청평소방서           경기도… 1                0                SM3 Z.E…
#> 10 전체  청평호반문화체육센터 경기도… 1                0                SM3 Z.E…
#> # ℹ 4,682 more rows

1.2 지도데이터

vuski/admdongkor 최신 행정동 데이터를 가져온다.

코드
library(sf)
sf_use_s2(FALSE)

korea_map <- read_sf("data/HangJeongDong_ver20230401.geojson")

sido_map <- korea_map |> 
  group_by(sido, sidonm) |> 
  summarise(geometry = sf::st_union(geometry))

st_geometry(sido_map) |> plot()

2 지오코딩

2.1 API 설정

주소를 지도에 올릴 수 있도록 위경도 변환한다. 구글 공간정보서비스가 위세를 떨치는 기간에 다음 지도 API가 진화를 하여 kakaomap Maps API로 사용법과 사용량 등 개발자 관점에서 나름 쓸만한 형태로 개선되었다.

C:\Program Files\R\R-3.5.2\etc 디렉토리 Rprofile.site 파일에 카카오 개발자센터 앱개발에 사용할 API 키를 KAKAO_MAP_API_KEY 변수에 저장시킨다. 재미있는 것은 Authorization에서 "KakaoAK "를 꼭 앞에 붙여야함으로 paste0() 함수로 결합시켜 전달한다. 혹은 usethis 팩키지를 사용해서 edit_r_environ() 명령어를 사용해서 .Renviron 파일에 KAKAO_MAP_API_KEY값을 설정하여 사용하는 것도 좋다.

카카오 개발자센터 로컬 → REST API 문서에 지오코딩하는 자세한 사항이 나와있다. 한가지 사례로 삼정KPMG주소를 지도위에 표시하는 것을 목표로 먼저 삼정KPMG 주소를 던져서 위도경도를 받아내는 헬로월드 코드를 작성해본다.

코드
library(httr)
library(tidyverse)
library(jsonlite)

# usethis::edit_r_environ()

# 요청 URL 및 파라미터 설정
base_url <- "https://dapi.kakao.com/v2/local/search/address.json"
params <- list(query = '서울특별시 강남구 역삼동 737' ) # 삼정KPMG 주소

# GET 요청 실행
response <- GET(base_url, 
                add_headers(Authorization = paste("KakaoAK", Sys.getenv("KAKAO_MAP_API_KEY"))),
                query = params)

# 응답 확인
print(content(response, "text"))
#> [1] "{\"documents\":[{\"address\":{\"address_name\":\"서울 강남구 역삼동 737\",\"b_code\":\"1168010100\",\"h_code\":\"1168064000\",\"main_address_no\":\"737\",\"mountain_yn\":\"N\",\"region_1depth_name\":\"서울\",\"region_2depth_name\":\"강남구\",\"region_3depth_h_name\":\"역삼1동\",\"region_3depth_name\":\"역삼동\",\"sub_address_no\":\"\",\"x\":\"127.036628730251\",\"y\":\"37.4998101243238\"},\"address_name\":\"서울 강남구 역삼동 737\",\"address_type\":\"REGION_ADDR\",\"road_address\":{\"address_name\":\"서울 강남구 테헤란로 152\",\"building_name\":\"강남파이낸스센터\",\"main_building_no\":\"152\",\"region_1depth_name\":\"서울\",\"region_2depth_name\":\"강남구\",\"region_3depth_name\":\"역삼동\",\"road_name\":\"테헤란로\",\"sub_building_no\":\"\",\"underground_yn\":\"N\",\"x\":\"127.036508620542\",\"y\":\"37.5000242405515\",\"zone_no\":\"06236\"},\"x\":\"127.036628730251\",\"y\":\"37.4998101243238\"}],\"meta\":{\"is_end\":true,\"pageable_count\":1,\"total_count\":1}}"

# KPMG 지리정보 데이터프레임
kpmg_list <- response %>% 
  content(as = 'text') %>% 
  fromJSON()

## 도로명주소
kpmg_list$documents$road_address %>% 
  select(address_name, building_name, x,y)
#>               address_name    building_name                x                y
#> 1 서울 강남구 테헤란로 152 강남파이낸스센터 127.036508620542 37.5000242405515

## 지명주소
kpmg_list$documents$address %>% 
  select(address_name, x,y)
#>             address_name                x                y
#> 1 서울 강남구 역삼동 737 127.036628730251 37.4998101243238

2.2 함수 제작

코드

get_lnglat <- function(address = '서울특별시 강남구 역삼동 737') {
  
  base_url <- "https://dapi.kakao.com/v2/local/search/address.json"
  params <- list(query = address ) # 삼정KPMG 주소
  
  # GET 요청 실행
  response <- GET(base_url, 
                  add_headers(Authorization = paste("KakaoAK", Sys.getenv("KAKAO_MAP_API_KEY"))),
                  query = params)
  

  # KPMG 지리정보 데이터프레임
  json_list <- response %>% 
    content(as = 'text') %>% 
    fromJSON()
  

  ## 위경도 주소
  return(json_list$documents$address %>% select(x,y) %>% unlist)
}

get_lnglat("경기도 가평군 가평읍 대곡리 213-5")
#>                  x                  y 
#> "127.516690566855" "37.8236644879834"

2.3 충전소 지오코딩

get_lnglat() 함수는 모든 것이 예정된 방식으로 동작하는 것을 가정한다. 따라서, 가정 중 하나라도 충족하지 않는 경우 오류가 나서 반복 작업이 중단된다.

코드
charger_tbl <- charger_raw |> 
  janitor::clean_names(ascii = FALSE) |> 
  mutate(data = map(주소, get_lnglat))
Error in `mutate()`:
 In argument: `data = map(주소, get_lnglat)`.
Caused by error in `map()`:
 In index: 248.
Caused by error in `UseMethod()`:
! no applicable method for 'select' applied to an object of class "NULL"

이를 보완하기 위해 safely(), possibly() 등 부사를 사용해서 가능한 많은 주소를 지오코딩한다.

코드
safely_get_lnglat <- safely(get_lnglat, otherwise = "error")

charger_tbl <- charger_raw |> 
  janitor::clean_names(ascii = FALSE) |> 
  mutate(data = map(주소, safely_get_lnglat))

# 데이터프레임 출력
# charger_tbl |> 
#   write_rds("data/charger_tbl_raw.rds")

# 리스트 출력
# charger_tbl |>
#   write_rds("data/charger_tbl_raw_list.rds")

charger_tbl <- 
  read_rds("data/charger_tbl_raw_list.rds")

charger_lnglat <- charger_tbl |> 
  mutate(result = map(data, "result")) |> 
  filter(result != "error") |> 
  
  ## 위경도 변환 --------------------
  select(-시구, -data) |> 
  mutate(lng = map_chr(result, 1),
         lat  = map_chr(result, 2)) |> 
  select(-result)

charger_lnglat |> 
  write_rds("data/charger_lnglat.rds")
코드
charger_lnglat <- 
  read_rds("data/charger_lnglat.rds")

charger_lnglat
#> # A tibble: 4,546 × 7
#>    설치장소             주소    급속충전기_대 완속충전기_대 지원차종 lng   lat  
#>    <chr>                <chr>   <chr>         <chr>         <chr>    <chr> <chr>
#>  1 가평소방서           경기도… 1             0             SM3 Z.E… 127.… 37.8…
#>  2 가평종합운동장       경기도… 2             0             BMW i3,… 127.… 37.8…
#>  3 가평지사             경기도… 1             1             SM3 Z.E… 127.… 37.8…
#>  4 가평하수도사업소     경기도… 1             0             SM3 Z.E… 127.… 37.8…
#>  5 상면사무소           경기도… 1             0             SM3 Z.E… 127.… 37.8…
#>  6 설악면사무소         경기도… 1             0             SM3 Z.E… 127.… 37.6…
#>  7 조종면사무소         경기도… 1             0             SM3 Z.E… 127.… 37.8…
#>  8 조종소방서           경기도… 1             0             SM3 Z.E… 127.… 37.8…
#>  9 청평소방서           경기도… 1             0             SM3 Z.E… 127.… 37.7…
#> 10 청평호반문화체육센터 경기도… 1             0             SM3 Z.E… 127.… 37.7…
#> # ℹ 4,536 more rows

3 지도 제작

3.1 시도 지도

코드
sido_map |> 
  ggplot() +
    geom_sf(aes(geometry = geometry)) +
    geom_sf_text(aes(label = sidonm), size = 3, color = "blue") +
    theme_void()

3.2 충전소 위치

코드
charger_sf <- charger_lnglat |> 
  mutate(across(lng:lat, as.numeric)) |> 
  filter(lat < 50) |> ## 충전소 위경도 오류 ㅠㅠ
  st_as_sf(coords = c("lng", "lat"),
           crs = st_crs(sido_map)) 

charger_sf |> 
  ggplot() +
    geom_sf(aes(geometry = geometry)) +
    theme_void()

3.3 위치 결합

코드
ggplot() +
  geom_sf(data = sido_map, aes(geometry = geometry), fill = "transparent", color = "blue") +
  geom_sf(data = charger_sf, aes(geometry = geometry), color = "black", size = 0.1) 

3.4 인터랙티브

코드
library(leaflet)

leaflet(data = charger_lnglat |> 
          mutate(across(lng:lat, as.numeric)) |> 
          filter(lat < 50)
        ) %>% 
  addProviderTiles(providers$OpenStreetMap) %>% 
  ## 충전소 상세정보
  addMarkers(lng=~lng, lat=~lat, 
             clusterOptions = markerClusterOptions(),
             popup = ~ as.character(paste0("<strong>", 설치장소, "</strong><br>",
                                   "----------------------------------<br>",
                                   "&middot; 주소: ", `주소`, "<br>",
                                   "&middot; 급속: ", `급속충전기_대`, "<br>",
                                   "&middot; 완속: ", `완속충전기_대`, "<br>",
                                   "&middot; 지원차종: ", 지원차종, "<br>"))
             ) |> 
  ## 시도 경계 추가
  addPolygons(data = sido_map,
              opacity = 1.0, fillOpacity = 0.1,
              weight = 1,
              highlightOptions = highlightOptions(color = "black", weight = 3,  bringToFront = TRUE),
              labelOptions = labelOptions(
              style = list("font-weight" = "normal", padding = "3px 8px"),
              textsize = "15px",
              direction = "auto"))