장강의 뒷물이 앞물을 밀어내며 흐르고

package sf

새로운 기술이 태동하여 기존의 방법을 대체하는 과정에서 나는 익숙했던 기존 방법을 고수하다가, 왜 좀 더 일찍 새로운 기술을 받아들이지 않았는가하는 생각을 하기도 합니다. 생각컨데 익숙함과 새로움 사이에서 적당한 줄타기를 해야하는 시대임은 틀림이 없습니다.

유충현 https://choonghyunryu.github.io (한국알사용자회)
2023-02-04

들어가기

그 많은 패키지에서 어떤 것이 옥일까요?
옥석을 가린다는 말을 하곤 하죠. R 패키지에서도 통할 수 있는 이야기입니다.
19,155개. 오늘 CRAN 등록된 패키지개수입니다. 이들 중에서 여러분들이 사용하는 패키지는 몇개나 될까요?

TL;DR

장강의 뒷물이 앞물을 밀어내며 흐르고

개인적으로 무협 만화를 좋아합니다. 무림의 원로 고수가 자신을 뛰어넘는 신진 협객의 무위를 견식하고, 공통적으로 하는 말이 있습니다. 정확한 워딩은 기억나지 않지만, 다음과 같은 뉘앙스로 이야기를 합니다.

“장강의 새로운 물이 밀려오니, 이제는 은퇴하고 뒷방 늙은이가 되어야겠구나.”

‘장강후랑추전랑(長江後浪推前浪)’.

중국 명나라 말기 격언집 증광현문장강후랑추전랑(長江後浪推前浪:장강(양츠강)은 뒷물이 앞물을 밀어내며 흐르고)에서 유래됐다고 합니다.

세대교체

공통적인 것은 강압에 의한 물리적인 교체가 아니라 마치 물이 흐르듯 자연스럽게 세대가 교체되는 것입니다. 어찌보면 적자생존이라는 정글의 법칙일 수 있습니다. 알파고, tensorflow, chatGPT가 무림에서 파란을 일으킨 것처럼 무협의 허구 속에서도 신진 고수의 새로원 권법이나 도법이 무림의 판도를 재편하곤 합니다. 기존의 유명 고수를 꺾고 패권을 갖는 것이죠.

돌이켜보면 R 패키지를 사용하는 과정에서도 장강후랑추전랑을 경험한 적이 있습니다.

일찌기 S-PLUS라는 R이 카피한 모태를 접해서, 나름 고전적인(?) R 표현식으로도 데이터 조작과 원하는 시각화는 문제없이 해결하고 했습니다. 익숙했기에 상대적으로 dplyr 기반의 tidyverse 패키지군과 ggplot2 패키지의 습득 타이밍이 늦었었습니다. 익숙함을 포기하고 새로운 기술을 받아들이는 것이 쉽지 않았습니다. 그러나 어느 순간 장강후랑추전랑을 느끼고 새로운 기술을 받아들이게 되었습니다.

R 세계에서의 알파고, tensorflow, chatGPTggplot2, tidyverse이라 볼 수 있습니다. 지금은 이 두 패키지를 빼고 R을 사용할 수 없습니다.

sp 패키지와 sf 패키지

지리공간 데이터분석의 전문가는 아닙니다. 그러나 직장에서 공공데이터를 수집하고 Shape 파일에 집적한 후 이를 분석하고 시각화하는 작업의 니즈가 있어서 hliSpatial이라는 private 패키지를 개발한 적이 있습니다. 광역시도 > 시군구 > 읍면동 > 블록 레벨의 행정구역 경계 수치지도와 행정구역별로 집계한 통계를 시각화하는 것이 주된 작업이었습니다.

최근에 bitSpatial이라는 public 패키지를 개발하고 있습니다. 아직은 걸음마 단계입니다. 2010년대 중번에 개발했던 hliSpatial의 경험을 복기하면서 개발하는데 가장 큰 차이는 다음 두 가지입니다.

sp 패키지는 R에서 지리공간 분석을 위한 메소드와 클래스를 구현한 패키지입니다. R에서 지리공간 데이터분석을 수행한다면 이 패키지를 직간접적으로 사용하지 않을 수 없습니다.

sf 패키지는 지리공간 분석을 좀 더 심플하게 수행할 수 있는 방법을 제시한 패키지입니다. 가장 큰 장점은 dplyr 패키지와 협업이 용이한 점입니다. 그리고 지리정보의 시각화 기능에서 ggplot2를 지원한다는 것도 장점 중의 하나입니다.

결국 dplyr과 ggplot2를 주력으로 사영한다면 sp가 아니라 sf를 사용해야 합니다.

장강의 새로운 물결인 sf 패키지가 sp 패지키를 밀어내는 셈입니다.

sp vs sf

앞에서 언급한 것처럼 지리공간 데이터분석의 전문가는 아닙니다. 그래서 앞서 개발했던 패키지가 잘 만들어진 패키지는 아닙니다. 그런데 이번에 sf 기반으로 패키지를 만들다 보니 기존 패키지의 로직이 비효율적이었습니다. 그것은 이 분야의 비전문가인 한계와 sf의 유용한 기능 때문인 것 같습니다.

몇 가지 사례만 가볍게 소개합니다.

두 위치의 거리 구하기

두 위치의 거리를 구하는 함수를 다음처럼 사용자 정의함수로 구현했었습니다. 결국 삼각함수 기반의 알고리즘으로 구현한 것입니다.

# 주어진 도(degree) 값을 라디언으로 변환하는 사용자 정의함수
deg2rad <- function(deg) {
  deg * pi / 180
}

# 주어진 라디언(radian) 값을 도(degree) 값으로 변환하는 사용자 정의함수
rad2deg <- function(rad) {
  rad * 180 / pi
}

theta <- lon1 - lon2

dist <- sin(deg2rad(lat1)) * sin(deg2rad(lat2)) + cos(deg2rad(lat1)) *
  cos(deg2rad(lat2)) * cos(deg2rad(theta))

dist <- acos(dist)
dist <- rad2deg(dist)

dist <- dist * 60 * 1.1515

# 단위 mile 에서 m 변환 후 반환
dist * 1.609344 * 1000
}

그러나 sp 패키지는 st_distance() 라는 함수를 지원해서 데이터의 형 변환만 수행한 후 간단하게 계산할 수 있습니다.

pos_1 <- data.frame(x = lon1, y = lat1) %>% 
  st_as_sf(coords = c("x", "y")) %>% 
  st_set_crs(crs) 

pos_2 <- data.frame(x = lon2, y = lat2) %>% 
  st_as_sf(coords = c("x", "y")) %>% 
  st_set_crs(crs)   

st_distance(pos_1, pos_2, by_element = TRUE) %>% 
  as.numeric()

bitSpatial 패키지의 calc_distance() 함수로 서울과 부산의 거리를 구해봅니다.

seoul <- c(126.9779692, 37.566535)
busan <- c(129.0756416, 35.1795543)

bitSpatial::calc_distance(seoul[1], seoul[2], busan[1], busan[2]) 
[1] 325122.8

결과를 보니 약 325km 떨어져 있습니다.

이 예제에서 두 도시의 경도와 위도는 구글링으로 입수했습니다만, 다음처럼 bitSpatial 패키지의 리소스의 지도 데이터와 sf 패키지를 이용해서도 구할 수 있습니다.

library(bitSpatial)

mega %>% 
  filter(mega_nm %in% c("서울특별시", "부산광역시")) %>% 
  st_centroid() %>% 
  st_distance() %>% 
  "["(1, 2)
320364.7 [m]

두 도시의 거리가 약 320km로 나타났습니다. 두 결과에서 5km정도의 오차가 발생했습니다. 여기서의 차이점은 두 도시의 위치가 물리적인 지리정보에서의 정 가운데 점을 기준으로 했다는 점입니다. 그러나 앞에서의 위치는 두 도시의 대표 지역의 위치를 의미하기 때문입니다.

mega %>% 
  filter(mega_nm %in% c("서울특별시", "부산광역시")) %>% 
  st_centroid() %>% 
  select(geometry)
Simple feature collection with 2 features and 0 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 126.9918 ymin: 35.20046 xmax: 129.0596 ymax: 37.55204
Geodetic CRS:  WGS 84
# A tibble: 2 × 1
             geometry
          <POINT [°]>
1 (126.9918 37.55204)
2 (129.0596 35.20046)

시각화 방법 비교

기존 방법의 서브 데이터 추출

기존 방법에서는 다음처럼 Shape 파일을 읽고, 이 객체에서 데이터를 서브 데이터를 추출하는 방법이 다음처럼 복잡했습니다. primitive call을 subset이라는 인수로 넘기는 것을 가정합니다.

  map <- eval(get(map_name))

  if (!missing(subset)) {
    x <- map@data

    e <- substitute(subset)
    r <- eval(e, x)

    if (!is.logical(r))
      stop("'subset' must be logical")

    map <- map[r & !is.na(r), ]
  }

그런데 구현할 함수 내의 로직에서 tidiverse 패키지군을 사용하면 다음처럼 간단하게 원하는 서브 데이터를 가져옵니다.

  map_df <- eval(get(zoom))
  
  if (!missing(subset)) {
    map_df <- map_df %>% 
      filter(!!rlang::enquo(subset))
  }

그리고 기존에는 다음처럼 데이터 프레임 형식으로 데이터를 변환하는 로직을 fortify() 함수 등으로 구현해야 했으며,

  map@data$id = rownames(map@data)
  map.pts <- fortify(map, region = "id")

  map_df <- inner_join(map.pts, map@data, by = "id")

포인트를 출력하기 위해서 ploygon에서 중심점을 만드는 작업을 작업도 수행해야 했습니다.

  if (!is.null(stat) & type %in% c("all", "point")) {
    pos <- t(sapply(map@polygons, function(x) x@labpt))
    value <- map@data[, stat]

    pos <- data.frame(x = pos[, 1], y = pos[, 2], value = value)
  }

그러나 sf 패키지는 이러한 불편한 작업이 필요없습니다. ggplot2에서는 포인트를 출력하는 geom_point() 함수에서 그냥 geometry = geometry와 stat = “sf_coordinates”를 사용하면 중심점에 포인트 심볼을 출력해줍니다.

geom_point(aes(size = statists, geometry = geometry),
           stat = "sf_coordinates", color = point_col)   

bitSpatial 패키지의 주제도 예제

서울특별시 양천구의 평균연령 분포를 시각화한 예입니다. 아직, 시각화함수인 thematic_map()의 최종 기능이 다 구현되어 있지 않습니다다만, 이 하나의 함수로 여러 기능을 구현할 수 있습니다. 나중에 완성되면 자세한 사용법을 공유하겠습니다.

이 주제도에서 연령의 분포보다는 양천구가 강아지 모양이라는 정보가 더 와 닿는 이유는 왜일까요?

thematic_map(zoom = "admi", subset = mega_nm == "서울특별시" & cty_nm %in% "양천구", 
             stat = "age_mean", label = "name",
             title = "서울 양천구 인구통계 주제도",
             subtitle = "동별 평균 연령 현황", legend_pos = "right")

단상

다음 중 어느 하나도 멈춰 있다면, 우리들은 곧 뒷방 늙은이로 남게 됩니다. 왜냐하면 장강은 지금도 소리없이 흘러가고 있으니까요.

Citation

For attribution, please cite this work as

유충현 (2023, Feb. 4). Dataholic: 장강의 뒷물이 앞물을 밀어내며 흐르고. Retrieved from https://choonghyunryu.github.io/2023-02-04-new_package.Rmd

BibTeX citation

@misc{유충현2023장강의,
  author = {유충현, },
  title = {Dataholic: 장강의 뒷물이 앞물을 밀어내며 흐르고},
  url = {https://choonghyunryu.github.io/2023-02-04-new_package.Rmd},
  year = {2023}
}