본문 바로가기

Function

통계청 집계구 인구를 격자로 재할당하기

통계청 SGIS에서 신청하면 받을 수 있는 공개 자료 중, 집계구 기반 데이터들은 꽤 자세하기 때문에 상세하게 들여다보고 싶을 때는 아주 유용하다. 그렇지만 자세하다는 장점은 늘 다루기 까다롭다는 단점과 함께하기 마련이다. 빠르게 훑어보고 싶거나 전체를 놓고 비교해보고 싶을 때는 그 커다란 덩치 때문에 다소 불편하다.

 

그리고, 형상이 불규칙하고 크기도 제각각이다. 거주 인구를 기반으로 구획되었기 때문에 사람이 많이 살지 않는 곳은 아주 넓게 잡혀 있고, 반대로 집계구 하나가 아파트 한 동 크기에 불과한 경우도 있다. 때문에, 집계구 경계에 인구를 그대로 join 시켜서 GIS에서 인구에 연동되도록 색상을 설정하게 되면 한 눈에 밀도를 파악하기 어렵다.

 

여기서는 집계구에 할당된 인구를 격자로 재분배하는 작업을 설명한다. 산이나 넓은 논밭에 인구가 할당되지 않도록 건물이 있는 곳 주변으로만 할당하도록 고려했다. R을 이용하여 작업했다. 데이터는 모두 공개된 데이터지만, 신청해야 받을 수 있다.

 

글에서 다루는 내용을 요약하자면 다음과 같다. 좌표계는 모두 UTMK(EPSG:5179)를 사용하여 작업했다.

R과 QGIS를 다룰 수 있어야 하며, 기본적인 내용은 설명하지 않는다.

 

1. 전국 건물 shape 파일을 바탕으로 건물이 존재하는 250m 격자를 만든다.

2. 각각의 집계구 내부에 50m 그리드를 생성한 후, 건물 250m격자와 대조하여 건물 없는 곳은 지운다.

3. 그래서 남게 된 집계구 안의 격자가 10개, 집계구의 인구가 150명이면 각 격자당 15명씩 할당한다.

4. 격자를 포함하지 않는 작은 집계구는, 해당 집계구의 인구를 집계구와 겹치는 50m 격자로 모두 보낸다.

5. 50m 격자에 할당이 완료되었다.

6. 50m 격자는 전체를 조망하기 어려우므로 선택적으로 100m나 250m, 500m 격자로 재집계한다.

7. shape 파일로 저장해서 Qgis에서 살펴본다.

 

그럼 이제 시작!

 

 

준비물

 

 

우선, 전국 건물 shape 파일이 필요하다. 

도로명 주소 사이트인 아래에 접속해서 전국 지도를 신청하자. 이 곳 말고 국가공간정보포털의 수치지도 기반 건물 shape 파일은 최신 건물의 업데이트가 느리고, 대단지 아파트들이 전부 빠져 있는 경우도 있어서 이 경우는 사용을 권장하지 않는다.

 

도로명주소 전자지도 신청 | 도로명주소 개발자센터

도로명주소 전자지도 --> 건물, 건물군, 도로구간, 실폭도로, 기초구간, 출입구, 기초구역, 행정구역경계(시도, 시군구, 읍면동, 법정리)등 11종을 제공합니다. 도로명주소 전자지도는 전국 광역시

www.juso.go.kr

다운받으면 폴더를 정해서 아래와 같이 압축을 풀어놓자. 나중에 루프를 돌면서 읽을 것이다.

정상적으로 압축이 풀렸다면 각 폴더 안에는 TL_SPBD_BULD.shp 파일이 있을 것이다. prj 파일은 폴더 내부에 없을텐데 신경쓰지 말자. 어차피 좌표값만 읽어와서 처리할 것이므로 각각의 shp 파일에 붙어있지 않아도 된다.

 

 

 

이번에는 통계청 sgis.kostat.go.kr 에서 집계구 경계와 인구를 다운받자. 사이트에 접속한 후 상단의 자료제공 -> 자료신청에서 신청할 수 있다.

 

 

인구를 신청할 때, 집계구 기준년도를 2020년으로 설정하였다면, 집계구 경계를 받을 때도 2020년의 것을 받아야 한다. 

일단 여기서는 2019년과 2000년의 성연령별 인구만 사용할 것이지만 받는 김에 관련 자료들을 받아두면 좋다.

참고로, 2000년의 인구지만, 통계청에서 내려받을 때, 2020년의 행정구역과 집계구 코드에 맞춰서 재할당해준 자료를 받게 된다. 2000년에는 세종시가 없었지만, 과거 연기군 등의 인구를 현재 세종시 집계구에 맞게 할당해 준다는 말이다. 

 

이 글을 따라가기 위해 받아야 할 필수 자료는 다음과 같다.

-------------------------------

2020년 집계구 경계

2020년 센서스용 행정구역경계(전체)

2000년 성연령별인구 - 2020년 집계구 기준

2019년 성연령별인구 - 2020년 집계구 기준

-------------------------------

 

이제 자료가 모두 준비되었다. 두 가지 다 오전에 신청하면 오후에는 보통 승인이 떨어진다.

그럼 이제 R 코드로 넘어가보자.

 

 

 

 

 

건물이 있는 그리드만 추출하기

 

 

아래의 코드들은 연속된 형태로 한 번에 정리해서 깃헙에 올려놓았다. 

 

 

vuski/populationDistribution

Contribute to vuski/populationDistribution development by creating an account on GitHub.

github.com

 

우선 필요한 라이브러리를 로드한다. 라이브러리가 없으면 install하자. 기본적인 내용은 건너뛰겠다.

각각의 건물 파일을 읽어서 처리하는 과정이 다소 답답하다고 느낄 수 있어, 병렬 프로세서를 이용하도록 doParallel 패키지를 사용하였다.

library(foreach)
library(doParallel)
library(tidyverse)
library(data.table)

######### 병렬 처리 ##########
# 코어 개수 획득
numCores <- parallel::detectCores() - 1

# 클러스터 초기화
myCluster <- parallel::makeCluster(numCores)
doParallel::registerDoParallel(myCluster)

라이브러리를 로드하고 병렬 프로세서를 돌릴 준비를 마쳤다.

보통 코어 개수에서 여분으로 한 개를 빼 주던데, 필요없으면 그냥 몽땅 써도 된다.

 

 

이제 루프를 돌면서 건물을 읽어서 처리한다. 3-4분 정도에 처리되는 것 같다.

각각의 코어에서 독립적으로 메모리를 사용하므로, 램이 부족하다고 판단되면 병렬 처리 코드를 걷어낸 후 싱글 프로세서용으로 코드를 변경하여 작업하면 된다.

folderRaw <- "D:/건물 SHAPE 파일 경로" #마지막에 슬래시를 붙이지 않는다.
folderWrite <- "D:/작업할 폴더/" #마지막에 슬래시를 넣는다.
src_folders <- list.dirs(folderRaw, recursive = FALSE) # list

loopSize <- length(src_folders)

result <- foreach::foreach(index = 1:loopSize,
                 .combine = rbind)  %dopar% {
                   
  library(sf)              
  library(data.table)  
  library(dplyr)                 
                   
  fileName <- paste0(src_folders[index],"/TL_SPBD_BULD.shp")
  print(paste0("read.....",fileName))
        
  bldg <- fileName %>% read_sf() #파일을 읽는다.
  bldgCoord <- as.data.table(st_coordinates(bldg)) #좌표들만 추출해서 table에 담는다.
  
  #250m 격자에 할당한다.
  gridTrue <- bldgCoord %>% mutate( xx = as.integer(X/250), yy = as.integer(Y/250) ) %>%
              distinct( gridx = xx, gridy = yy)           

  return(gridTrue)
}

# 클러스터 중지
parallel::stopCluster(myCluster)

각 코어당 한번에 하나의 파일을 처리하는 방식이다.

건물 폴더가 17개인데, 코어가 7개(8-1 = 7)로 잡혔을 경우,  7+7+3 으로 총 3바퀴를 돈다.

필요한 라이브러리는 각각의 코어 안에서 로드해줘야 에러가 나지 않는다.

 

sf 라이브러리는 공간 데이터를 처리하는 라이브러리다.

shape 파일을 읽고, 좌표들을 모두 추출한다.

 

그 다음은 건물에 존재하는 좌표들이 속한 250m 격자를 찾는다. UTMK는 미터법 좌표계이므로 각 좌표를 250으로 나누어 정수 부분만 취하면, 일종의 격자 인덱스처럼 사용할 수 있다. 그렇게 각각의 좌표에서 인덱스를 추출한 후 고유값만 취한다.(distinct)

 

각 코어의 결과들은 모두 rbind 되어 result 에 들어오게 된다.

 

uniqueGrid <- result %>%  distinct( gridx,gridy) %>%
               mutate( x = gridx*250, y = gridy*250)

fwrite(uniqueGrid, file=paste0(folderWrite,"bldgGrid_parallel.tsv"),
         quote=FALSE, sep = "\t", row.names = FALSE, col.names = TRUE)

rm(result)
rm(myCluster)
rm(loopSize)
rm(numCores)
rm(src_folders)
rm(folderRaw)
rm(uniqueGrid)
rm(folderWrite)

이제 겹치는 250m 격자 인덱스에서 다시 distinct를 통해 고유값을 추출한 후, 저장을 위해 250을 각각 곱해준다.

 

만약 어느 격자의 좌하단, 우상단 좌표가 (940000, 1850000) , (940250, 1850250) 이라면 이 격자는 940000, 1850000 으로 표시되어 저장됨을 기억하자. 

예를 들어 어떤 점의 좌표가 (940185, 1850042) 라면,  xy값을 각각 250으로 나누어 정수를 취한 후, 다시 250을 곱해서 위의 격자 집합과 비교하면, 해당 점이 건물이 있는 250m 격자에 들어오는지 아닌지를 쉽게 판단할 수 있다.

 

 

 

결과물로 저장된 tsv 파일을 Qgis에서 지도 위에 뿌려보면, 아래와 같다. 각각의 점을 중심으로 지름 250m 원을 그렸으므로, 정확히 말하자면 격자와 가로세로 125m 씩 어긋난 셈이 된다. 일단 확인용이므로 상관 없다.

서울은 건물이 많으므로 거의 모두 들어차 있고,

 

전국을 보면 이런 느낌이다.

 

이 작업은 매번 할 수 없고, 그럴 필요도 없으므로 결과를 tsv에 저장했다.

이제 다음 작업으로 넘어가자.

 

 

 

 

인구 배분

 

 

이제 집계구 경계와 집계구 인구를 바탕으로 격자에 배분하는 과정을 시작해보자.

library(Rcpp)
library(sf)

folderWrite <- "d:/작업용 폴더/"

#Rcpp 파일 로드(별도로 설명한다)
sourceCpp("distributeValueToGrid.cpp")

#건물이 존재하는 250m 그리드를 읽는다. 바로 앞에서 저장한 파일
bldgGrid <- fread(paste0(folderWrite,"bldgGrid_parallel.tsv"),
                            sep = "\t", header = TRUE, stringsAsFactors = FALSE)

#Rcpp 변수에 입력한다. 추후 인구를 할당할 때 사용할 준비작업
bldgGridToSet(as.integer(bldgGrid$gridx), as.integer(bldgGrid$gridy))
rm(bldgGrid)

그냥 R 라이브러리만 이용하려다 보니, 두 어 시간 이상이 걸렸는데도 필요한 내용을 잘 찾지 못했다. 사실 나는 R 초보라서 그때그때 필요한 내용들을 찾아서 사용하는데, sf 자체가 자료형식이 복잡하다보니 다루기가 영 쉽지 않았다. 게다가 처리할 내용이 좀 되는데, R에서 루프를 돌 수도 없고(성능이 극악이다), apply계열을 사용하자니, 기존에 사용하던 코드 기반 알고리즘을 R 개념으로 바꾸는데만 한참이 걸릴 듯 했다.

그래서 결국 Rcpp 라이브러리를 이용해서 필요한 내용을 cpp로 작성했다. 일단 여기서는 결과로 만든 함수들만을 설명한다.

 

앞에서 건물이 존재하는 그리드를 저장해두었으므로 다시 읽어서 확인한다. 이제 bldgGridToSet 함수로 이 값들을 cpp 변수에 저장한다. Rstudio 변수에서는 아무 변화가 없겠지만 "그리드 입력 끝"이라고 순식간에 표시될 것이다.

그리드 값들을 cpp의 unordered_set에 입력해두고 나중에 존재 여부를 검토하게 된다.

모두 함수 안에서 돌아가는 작업이므로 특별히 신경쓰지 않아도 된다.

 

참고로, sourceCpp 는 이미 만들어둔 cpp 파일을 컴파일하는 함수다. 정상적으로 컴파일 되었다면, R 의 function 에 함수 다섯개가 새로 추가된다. 각 함수의 원형은 아래와 같다. 각 함수는 아래에서 차례차례 등장하게 된다.

//빌딩이 있는 그리드를 cpp 변수 안에 넣는다.
void bldgGridToSet(IntegerVector gridx, IntegerVector gridy);

//집계구 인구를 cpp 변수 안에 넣는다.
void putAdmPopu(NumericVector popu);

//집계구 경계를 cpp 변수 안에 넣는다.
void putAdmBoundary(NumericVector xvec, NumericVector yvec,
                    IntegerVector L1, IntegerVector L2, IntegerVector L3);
                    
//집계구 인구를 그리드로 배분한다. 가장 메인 함수
DataFrame distributeValue(int threads);

//특정 시군구 경계안에 들어오는 격자만 추출한다.
DataFrame filteringSggGrid(IntegerVector gridx, IntegerVector gridy, NumericVector valuevec,
                      NumericVector xvec, NumericVector yvec,
                      IntegerVector L1, IntegerVector L2, IntegerVector L3);

 

 

이제 집계구 경계를 읽는다.

##집계구 경계를 읽는다.

folderAdm <- "d:/집계구 경계가 있는 폴더/"
fileName <- paste0(folderAdm,"bnd_oa_00_2020_2020_2Q.shp")
system.time( admBndry <- fileName %>% read_sf())

 

#앞에서 읽은 집계구별 총 인구에 집계구를 join한다.
#집계구 도형 순서와 일치하는 집계구~인구 데이터프레임을 만드는 작업이다.
admCD <- as.data.frame(admBndry$TOT_REG_CD)
colnames(admCD) <- c("TOT_REG_CD")


#집계구 경계를 입력한다. 한번만 입력하면 된다.
admcoord <- admBndry %>% st_coordinates()
putAdmBoundary(admcoord[,1], admcoord[,2],
                  as.integer(admcoord[,3]), as.integer(admcoord[,4]),
                  as.integer(admcoord[,5]))
rm(admcoord)
rm(admBndry)

집계구 경계가 sf 파일 형식으로 admBndry에 들어가 있으므로 여기서 집계구 코드를 추출한다.

admCD 변수는 다른 연도의 집계구 인구를 처리할 때도 계속 반복적으로 사용할 것이므로 잘 보관(?)한다.

 

putAdmBoundart 함수를 이용하여 집계구 경계를 cpp 변수에 입력한다. st_coordinates의 결과물로 생성되는 매트릭스의 열들을 그대로 집어넣으면 된다. cpp 쪽에서 그대로 받아주도록 처리하였으므로, 그냥 저렇게 입력하면 된다.

 

참고로, 1번 2번 열은 각각 경계의 x,y 좌표고, 3,4,5번 열은 멀티폴리곤 파일의 정보들이다. 

5번 열이 집합 도형의 일련번호다. admBndry의 행 번호와 일치하게 된다.

4번 열은 멀티 폴리곤 안의 각각 폴리곤 번호,

3번 열은 각 폴리곤에서 외곽선인지 구멍(hole)인지를 가리킨다.

cpp에서는 5번 열을 기준으로 vector 변수 안에 넣고, 나중에 특정 격자 점과 대조해서 사용하게 된다.

 

제대로 입력되었다면, "경계 및 bbox 입력 완료 :103612개의 개체" 라는 내용이 출력된다. 2020년 기준이다.

 

 

이제 재분배 하는 절차를 함수형식으로 R에서 작성해보자.

#재분배 함수
distributePopulation <- function(fileName, admCD_) {
  
  
  
  # 통계청 집계구별 인구를 읽는다.
  system.time(popu <- fread(fileName,
                              sep = "^", header = TRUE, stringsAsFactors = FALSE,
                              colClasses = c('integer', 'character','character','character')))
  
  #집계구별 총 인구를 계산한다.
  #집계구별 인구 중 1~4인은 NA로 처리되어 있으므로 NA대신 2.5를 입력한다.
  popu <- popu %>% mutate(code = as.integer(substr(item,8, length(item)))) %>%
    filter(code<=21 | code == 999) %>% #21까지가 남녀인구 999는 자료없는 집계구
    mutate(popu = replace_na(as.numeric(value), 2.5)) %>%
    group_by(tot_oa_cd) %>%
    summarise(.groups="keep", popu = sum(popu)) %>%
    ungroup()
  

  #집계 확인
  print(paste0("인구 집계(파일 총계) :",sum(popu$popu)))
  
  #Rcpp 변수에 입력한다. 집계구 polygon의 일련번호와 일치하는 인구다.
  #뒤에서 그리드에 할당할 때 사용한다.
  admJoined <- admCD_ %>% left_join(., popu,
                         by=c("TOT_REG_CD"="tot_oa_cd"))
  putAdmPopu(admJoined$popu)
  rm(admJoined)
  rm(popu)
  
  
  numCores <- parallel::detectCores()-1
  result <- distributeValue(numCores)
  
  print(paste0("인구 집계(파일 총계) :",sum(result$value)))
   
  return(result)
}

먼저 집계구별 인구를 읽는다. raw data 에서 구분자가 ^로 되어있음에 유의하자.

여기서 사용하는 인구는 성 연령별 인구이므로 in_age_001 부터 in_age_021 까지만 취해야 한다. 그 이후는 성별 인구다. 자세한 내용은 통계청의 코드 설명서를 참고하면 된다.

그리고 비식별화 문제로 5인 미만인 집계구는 NA로 처리되어 있다. 0명일 경우에는 아예 생략되어 있으므로 NA 처리는 1,2,3,4 명 중 하나다. 그러므로 2.5명을 NA대신 입력했다.

 

처리가 끝났으면 sum을 해서 전국 인구와 같은지 확인해보자.

사실 같아야 하는데 1~2백만명 적게 나온다. 비식별 처리를 모두 4명으로 높여도 여전히 약간 모자란다. 처리상 문제는 없는데, 집계구 인구의 특성을 아직 잘 이해하지 못하는 것 같다. 시간이 날 때 한번 왜 차이가 나는지 살펴봐야겠다.

 

이제 앞에서 만든 admCD 변수(집계구 코드)에 집계구별 인구를 left_join 해준다. admCD 의 열 순서가 뒤바뀌면 안되므로 주의한다. (그냥 그대로 따라하며 된다)

cpp함수인 putAdmPopu 함수를 이용하여 집계구 코드와 집계구별 인구가 연결된 내용을 cpp 변수에 넣어준다.

 

이제 마지막으로 distributeValue를 이용해서 집계구 인구를 50m 격자에 할당한다. 십몇 초 쯤 걸리는 것 같다.

cpp 안에서도 OpenMP를 이용하여 병렬처리 해주므로, cpu의 코어 수를 계산해서 함수에 태워보낸다. 램 사용량은 전혀 높지 않은데, 혹시라도 메모리 문제로 병렬 처리에 문제가 생기는 것 같으면 numCores를 1로 강제 설정해서 보내면 된다.

 

여기까지는 함수를 정의한 것이므로 당연히 아무 일도 일어나지 않는다. 이제 파일을 지정하여 실행시켜보자.

folderPopu <- "d:/통계청 인구가 있는 폴더/"
fileName <- paste0(folderPopu, "2020년기준_2000년_성연령별인구.txt")

popuGrid50 <- distributePopulation(fileName, admCD)

fwrite(popuGrid50, file=paste0(folderWrite,"resultDistributed_50m_2000.tsv"),
         quote=FALSE, sep = "\t", row.names = FALSE, col.names = TRUE)

내용이 간단하므로 설명은 생략한다. 역시 계산 결과를 저장해두자.

여기까지 하면 50m 격자 할당이 모두 끝났다.

전국을 한다고 하니 대단한 작업 같지만, 생각보다 순식간에 실행되어 끝난다.

 

 

 

 

 

 

상위 그리드로 인구 재집계

 

 

 

이제 50m 그리드를 상위 그리드로 재집계해보자.

경험상, 특정 시군구 단위의 전반적인 분포를 확인할 때는 300m~500m 정도의 그리드가 적당한 것 같다. 50m 는 자세해서 좋을것 같지만, 너무 국부적인 수치를 그대로 표현해버려서 '분포'를 확인하기 어려운 점이 있다. 

 

기왕이면 국토부 표준격자에 맞춰서 250m로 작업해보자. 50m 격자로 처음에 분배한 이유가 여기에 있다.

50m 격자는 국토부 표준 격자 중 100m 계열과 250m 계열 모두 재집계가 가능하기 때문이다.

#50m 그리드를 상위 그리드로 재집계 하는 함수
aggregatePopuGrid <- function(gridPopu, fromGrid, toGrid) {
  
  if (fromGrid >= toGrid) {
    print("그리드 설정 오류")
    return(NULL)
  }
  
  temp <- gridPopu %>% mutate(x = as.integer(as.integer(x/toGrid)*toGrid) + (toGrid/2) ,
                            y = as.integer(as.integer(y/toGrid)*toGrid) + (toGrid/2)) %>%
    group_by(x,y) %>%
    summarise(.groups="keep", value = sum(value)) %>%
    ungroup()
  
  return(temp)
  
}

재집계 함수는 위와 같다. 원리는 앞과 같다. 

그리드로 나누어 정수값을 취하면 일종의 인덱스로 취급할 수 있는 숫자를 얻을 수 있고, 다시 곱하면 격자의 좌하단 좌표를 얻게 된다. 그래서 격자의 중점으로 만들기 위해 다시 그리드를 2로 나눈 값을 각 좌표에 더해주는 과정을 거친다.

 

 

함수를 정의했으니 재집계해보자.

#인구를 재집계. 두번째 변수는 현재 그리드인데, 실수 확인용
system.time(popuGrid100 <- aggregatePopuGrid(popuGrid50, 50, 100))

sum(popuGrid100$value)

system.time(popuGrid250 <- aggregatePopuGrid(popuGrid50, 50, 250))

sum(popuGrid250$value)

함수에 들어가는 변수 중 두 번째 변수인 50은 사실 별 필요가 없는데, 없애도 무방하다. 물론 함수 정의도 고쳐주어야 한다. 

sum을 해서 인구 총합이 유지되는지 검증해보자.

 

 

 

 

특정 시군구만 추출하기

 

 

이제 필요한 과정이 모두 끝났다. R에서 한번 확인해보자. 

50m 그리드 전부를 ggplot 하는 것은 너무 무거우므로 추천하지 않는다. 그 작업이 필요하다면 QGIS할 것을 권장한다.

 

library(rmapshaper)

fileName <- paste0(folderAdm,"bnd_sigungu_00_2020_2020_2Q.shp")
system.time( sggBndry <- fileName %>% read_sf())

#맵 단순화. 그리드 중 특정 시군구 소속 경계를 추출하기 위함이므로 적당히 단순화시킨다.
#약 1분 가까이 걸린다.
system.time(sggSimple <- sggBndry %>% ms_simplify())

#정상적으로 읽고 변환되었는지 확인
ggplot() +
  geom_sf(data = sggSimple)

sggcoord <- sggSimple %>% st_coordinates()
sggcoord <- as.data.frame(sggcoord)

sggCode <- data.frame(as.integer(rownames(sggBndry)), sggSimple$SIGUNGU_CD, sggSimple$SIGUNGU_NM)
colnames(sggCode) <- c("index", "sggcode", "sggname")

sggcoord <- sggcoord %>% left_join(sggCode, by=c("L3"="index"))


rm(sggBndry)

통계청에서 받은 자료 중 시군구 경계를 읽고, 해당 시군구안에 포함되는 격자만 추출해서 그림을 그려보려고 한다.

우선 시군구 경계를 읽는다.

시군구 경계 원본은 약간 자세하므로 rmapshaper 라이브러리의 ms_simplify 함수로 단순화를 한다.

1-2분 이상 시간이 걸리는 것 같은데, 그래도 되긴 된다. 더 용량이 큰 집계구 경계 파일은 ms_simplify를 하게 되면 Rstudio가 다운된다.

 

변환이 끝나면 ggplot을 이용하여 시군구 경계를 확인한다.

그 다음 과정은 시군구 경계의 좌표만 추출해서 데이터 프레임으로 만든 뒤, 시군구 코드와 join 시켜서 나중에 사용할 수 있도록 하는 과정이다. sggCode와 sggcoord 모두 사용할 것이다.

 

 

extractSggGrid <- function(popuGridData, sggCode, sggcoordData) {
  
  sggcoordExtracted <- sggcoordData %>% filter(sggcode==sggCode)
  
  popuGridFiltered <- filteringSggGrid(popuGridData$x, popuGridData$y,
                                       popuGridData$value, sggcoordExtracted$X,
                                       sggcoordExtracted$Y,sggcoordExtracted$L1,
                                       sggcoordExtracted$L2,sggcoordExtracted$L3)   
  return (popuGridFiltered)
 }

이제 특정 시군구만 추출하는 함수를 정의한다. 그때그때 cpp 의 filteringSggGrid 함수를 호출하고, 시군구 경계와 추출할 시군구 코드를 태워 보냄으로써 필요한 그리드만 받아오게 된다. 약간 비효율적이긴 하지만, 1초 안에 실행된다.

 

popuPart <- extractSggGrid(popuGrid250, "32030", sggcoord)

이제 함수를 호출해본다. 강릉시를 한번 추출해봤다.

 

이제 한번 그려본다.

ggplot(data = popuPart %>% filter(value>1), aes(x=x, y=y, color = value)) +
  geom_point() +
  theme_bw() +
  #scale_size(range = c(1,8), breaks = c(0,1000,2000,3000, 4000, 5000, 6000, 7000))+
  #scale_colour_gradient2()+
  scale_color_continuous(low = "#ffcc11", high = "red", breaks = c(1000,4000,7000) ) +
  coord_fixed(ratio = 1)

ggsave(paste0(folderWrite,"강릉_2000.png"),
       antialias = "default", width = 240, height = 240, unit = c("mm"), dpi = 300)

몇 개 바꿔보던 옵션들을 주석처리 해놓았으므로, 필요하면 변경해서 사용하면 된다.

 

아래처럼 보인다. 색상이 약간 못마땅하지만, 확인용이므로 일단 넘어간다.

 

 

shape 파일로도 저장해보자.

popuShp <- st_as_sf(popuGrid250, coords = c("x", "y"), crs = 5179)
st_write(popuShp, paste0(folderWrite,"popu250.shp"), driver="ESRI Shapefile")

약간의 시간이 걸리지만 popuPart 나 전국 그리드 모두 저장할 수 있다. QGIS에서 충분히 빠르게 그릴 수 있다.

여기서는 EPSG:5179 좌표계(UTMK, Korea 2000 Unified CS)로 저장했다.

 

 

 

간단하게 지도 그려보기

 

 

이제 위에서 저장한 shp 파일을 Qgis로 가져가보자.

 

 

 

 

 

 

그냥 띄우면 위와 같은 그림이 나온다.

 

이제 조금 더 보기 좋게 바꿔보자.

 

 

색상은 value 즉 인구 값에 따라 단계구분으로 둔다.

색상램프는 Viridis, 그리고 색상램프 반전을 선택했다.

 

 

단계 구분은 목적에 따라 달리 두면 된다.

일단 여기서는 등간격으로 10개 구간을 두었다.

 

 

 

그리고 심볼 옆의 칸을 클릭하면 전체 심볼 설정에 들어갈 수 있다.

단순 마커를 선택하고, 크기를 250, 그리고 지도단위로 선택한다.

선 색상은 없앤다.

 

 

그럼 이렇게 나온다.

전주인데, 어디가 어디인지 알기 어렵다.

 

 

 

원의 크기를 좀 바꿔본다. 다시 심볼 설정 -> 단순마커에서 [크기] 맨 우측의 아이콘을 클릭하여 직접 편집한다.

 

 

 

반지름을 선택하는 것이므로, 원의 면적이 인구에 비례하도록 value의 루트 값을 구한다.

그리고 4를 곱하는데, 이 숫자는 격자의 크기에 따라 달라질 수 있다. 250m의 경우에는 4 정도로 하면 좋다.

4를 곱하면 어떤 원의 크기는 자기자신 그리드 크기 밖으로 나오는데, 적당히 강조되어 보여서 보기에 나쁘지 않다.

 

 

기왕이면 숫자도 표시하자

라벨로 가서 value 항목에는 위와 같이 입력한다.

인구 값이 1보다 작으면 아무것도 표시하지 않고(홑따옴표 두개다), 그보다 크면 인구값을 표시한다. 소수점 이하는 반올림한다.

 

[배치] 항목에서,

글자가 원의 중앙에 오도록 [포인트에서 옵셋] 하고 사분위의 중심을 택한다.

 

 

글꼴을 적당히 선택하고

크기는 적당히 두면 되는데, 여기서는 그냥 확대축소에 연동되도록 하고(밀리미터 선택), 5로 두었다.

 

 

 

이제 이렇게 보인다.

 

 

 

우선 급한대로 밑에 지도도 깔아본다.

 

어디가 어딘지 알 수 있게 되었다.

 

끝!

 

 

 

시각화 그림 몇 장

 

 

아래에는 몇 장의 그림을 넣었는데,

2019년 집계구 기준으로 2000년과 2018년을 비교한 그림이다.

 

처음에 이 작업을 시작했던 것은, 인구가 점점 넓은 국토에서 특정 지역으로만 집중되는 것 같아 그 양상을 비교해보고 싶었기 때문이었다. 이를테면 전국 스케일에서는 서울로 몰리고, 각 지방 중소도시에서는 신도심으로 몰린다고 생각했었다.

 

그런데, 직접 비교해보니 두 가지 양상이 혼합되어 드러나는 것 같다.

우선, 2000년에는 전국 곳곳에 과밀화된 지역들이 보인다. 진한 남색으로 표시된 지역들이다. 2018년에 와서는 그 그리드들의 인구가 줄어들고 주변으로 분산되었다. 인구의 국부적 과밀화가 해소되었다.

 

그런데, 동시에 연관된 다른 현상도 나타났다. 새로 개발된 지역들은 기존의 과밀화 지역을 해소하는 역할을 했지만 기존의 저밀도 지역의 인구도 흡수해갔다. 서울과 수도권 같은 경우는 여기에 해당하는 경우가 드물지만, 지방중소도시에서는 그 양상이 심각하게 드러난다.

아마도 추측컨대, 서울이나 수도권에서 주변 개발로 사람들이 떠나간 지역의 빈자리는 지방에서 이주해와서 채우게 되었을 것이다. 이러한 연쇄 이동의 피라미드 구조에서 맨 밑바닥의 지방 원도심들은, 결국 더 이상 채워 줄 사람들이 없으므로 인구가 줄어들 수 밖에 없게 되었을 것 같다.

 

이렇게 긍정적인 측면과 부정적인 측면이 동시에 나타나다보니, 각자가 보고 싶은 측면만 보게 되는 것 같다. 자세하고 정확한 데이터와 그것들을 다룰 수 있는 기술은 이러한 현상들을 좀 더 세분화하여 읽어낼 수 있도록 도와줄 수 있다. 

 

물론, 분석과 판단은 언제나 사람의 몫으로 남지만.

 

 

대표