korea APT Trade Analysis Visualization

KATA_Vis



한국 아파트 매매 분석 및 시각화

Chapter 1.


1. Packags

# data handling
library(dplyr)
library(lazyeval)
library(tidyr)
library(data.table)
library(lubridate)
# chapter 1
library(budongsan) # devtools::install_github("keepcosmos/budongsan")
library(highcharter)
library(purrr)
library(viridis)
# chapter 2
library(XML)
library(stringr)
library(ggplot2)
library(leaflet)
library(jsonlite)

2. Data

Used Data

  • 해당 데이터는 budongsan 패키지에 내장돼 있는 데이터입니다.
  • molit.rt.{type}.{year} 함수를 통해서 특정 데이터를 불러올 수 있습니다.
  • type:
    • apt: 아파트
    • sh: 단독 / 다가구
    • rh: 연립 / 다세대
  • year: 2006 ~ 2016


ko_apt_all <- molit.rt.get("apt")
load("ko_apt.RData")
knitr::kable(head(ko_apt_all[, 1:9]))
region zipcode housing.estate net.area day trading.price floor build.year street
서울특별시 강남구 개포동 141 개포주공 1단지 35.64 1~10 38000 3 1982 개포로
서울특별시 강남구 개포동 141 개포주공 1단지 45.26 1~10 51000 4 1982 개포로
서울특별시 강남구 개포동 141 개포주공 1단지 49.56 11~20 70000 1 1982 선릉로
서울특별시 강남구 개포동 141 개포주공 1단지 49.56 11~20 70000 4 1982 선릉로
서울특별시 강남구 개포동 141 개포주공 1단지 50.64 1~10 65700 3 1982 개포로
서울특별시 강남구 개포동 141 개포주공 1단지 50.64 1~10 67000 2 1982 개포로
seoul_apt <- data.table(molit.rt.get("apt", "서울"))
seoul_apt_buy <- seoul_apt[deal.type == "매매" , ]
knitr::kable(head(seoul_apt_buy[, 1:9]))
region zipcode housing.estate net.area day trading.price floor build.year street
서울특별시 강남구 개포동 141 개포주공 1단지 35.64 1~10 38000 3 1982 개포로
서울특별시 강남구 개포동 141 개포주공 1단지 45.26 1~10 51000 4 1982 개포로
서울특별시 강남구 개포동 141 개포주공 1단지 49.56 11~20 70000 1 1982 선릉로
서울특별시 강남구 개포동 141 개포주공 1단지 49.56 11~20 70000 4 1982 선릉로
서울특별시 강남구 개포동 141 개포주공 1단지 50.64 1~10 65700 3 1982 개포로
서울특별시 강남구 개포동 141 개포주공 1단지 50.64 1~10 67000 2 1982 개포로


3. Motionable charts

Motionable Charts_1

seoul_apt_buy_date <- seoul_apt_buy %>%
    group_by(year, month) %>% 
    summarise(mean = mean(trading.price),
              lower = quantile(trading.price)[2],
              upper = quantile(trading.price)[4]) %>% 
    mutate(date = paste(year, ".", month, ".01", sep = ""))

seoul_apt_buy_date$date <- ymd(seoul_apt_buy_date$date)

df <- seoul_apt_buy_date %>% 
    mutate(tmpstmp = datetime_to_timestamp(date),
           color_m = colorize(mean, viridis(10, option = "B")),
           color_m = hex_to_rgba(color_m, 0.65))
knitr::kable(head(df))
year month mean lower upper date tmpstmp color_m
2006 1 27003.20 16700 32700 2006-01-01 1.136074e+12 rgba(244,131,20,0.65)
2006 2 25819.76 16000 32000 2006-02-01 1.138752e+12 rgba(168,46,93,0.65)
2006 3 29134.20 17000 35800 2006-03-01 1.141171e+12 rgba(252,255,164,0.65)
2006 4 27583.18 16600 34400 2006-04-01 1.143850e+12 rgba(249,173,25,0.65)
2006 5 26953.20 16900 33000 2006-05-01 1.146442e+12 rgba(228,94,46,0.65)
2006 6 25031.76 15500 31000 2006-06-01 1.149120e+12 rgba(57,12,92,0.65)
dfcolyrs <- seoul_apt_buy_date %>%
    select(year, mean) %>% 
    group_by(year) %>% 
    summarise(mean = mean(mean)) %>% 
    ungroup() %>% 
    mutate(color_y = colorize(mean, viridis(10, option = "B")),
           color_y = hex_to_rgba(color_y, 0.65)) %>% 
    select(-mean)
df_dfcolyrs <- left_join(df, dfcolyrs, by = "year")


lsseries2_bu <- df_dfcolyrs %>% 
    group_by(year) %>% 
    do(data = .$mean, color = "transparent", enableMouseTracking = FALSE, color2 = first(.$color_y)) %>% 
    mutate(name = year) %>% 
    list.parse3()
## Warning: 'list.parse3' is deprecated.
## Use 'list_parse' instead.
## See help("Deprecated")
motion_1 <- highchart() %>% 
    hc_chart(polar = TRUE) %>%
    hc_plotOptions(series = list(marker = list(enabled = FALSE), animation = TRUE, pointIntervalUnit = "month")) %>%
    hc_legend(enabled = FALSE) %>% 
    hc_title(text = "Animated Spiral") %>% 
    hc_xAxis(type = "datetime", min = 0, max = 365 * 24 * 36e5, labels = list(format = "{value:%B}")) %>%
    hc_tooltip(headerFormat = "{point.key}", xDateFormat = "%B", pointFormat = " {series.name}: {point.y}") %>% 
    hc_add_series_list(lsseries2_bu) %>% 
    hc_chart(events = list(load = JS("

function() {
    console.log('ready');
    var duration = 16 * 1000
    var delta = duration/this.series.length;
    var delay = 1000;

    this.series.map(function(e){
        setTimeout(function() {
        e.update({color: e.options.color2, enableMouseTracking: true});
        e.chart.setTitle({text: e.name})
        }, delay)
        delay = delay + delta;
    });
}
                    ")))
  • 해당 그래프는 월별 아파트 가격을 polar line chart로 그렸습니다.
  • 또한 JS를 통해 2006년부터 2016년까지 연도별로 시간에 따라 차트가 추가됩니다.
  • 만약 이미 2016년까지 차트가 나왔다면 해당 차트를 프레임 새로고침 해야합니다.
motion_1


Motionable Charts_2

KATA <- ko_apt_all[trading.price > 0 & floor > 0, c("net.area", "trading.price", "floor","year", "deal.type")]
KATA[net.area < 100, size := "30평 이하"]
KATA[net.area >= 100, size := "30평 이상"]

names(KATA) <- tolower(names(KATA))
KATA_1 <- KATA %>%
    group_by(year, floor, size) %>% 
    summarise(price_sp = round(mean(trading.price), 0)) %>% 
    mutate(price = price_sp*ifelse(size == "30평 이상", -1, 1)) %>% 
    select(-price_sp)

KATA_2 <- KATA_1 %>% 
    group_by(year) %>% 
    spread(floor, price) %>%
    mutate_each(funs_(interp(~replace(., is.na(.), 0)))) %>%
    gather(floor, price, 3:82)
knitr::kable(head(KATA_2))
year size floor price
2006 30평 이상 1 -28915
2006 30평 이하 1 11425
2007 30평 이상 1 -24148
2007 30평 이하 1 10350
2008 30평 이상 1 -27416
2008 30평 이하 1 11410
KATA_series <- KATA_2 %>% 
    group_by(size, floor) %>% 
    do(data = list(sequence = .$price)) %>% 
    ungroup() %>% 
    group_by(size) %>% 
    do(data = .$data) %>%
    mutate(name = size) %>% 
    list_parse()

xaxis <- list(categories = sort(as.numeric(unique(KATA_2$floor))),
              reversed = FALSE, tickInterval = 2,
              labels = list(step = 2))

maxpop <- max(abs(KATA_1$price))
motion_2 <- highchart() %>%
    
    hc_title(text = "층별 아파트 평균 매매가") %>% 
    hc_chart(type = "bar") %>%
    hc_motion(enabled = T, labels = 2006:2016, series = c(0:1), loop = TRUE , updateInterval = 1) %>% 
    hc_add_series_list(KATA_series) %>%
    
    hc_plotOptions(series = list(stacking = "normal"),
                   bar = list(groupPadding = 0, pointPadding =  0, borderWidth = 0)) %>% 
    
    hc_tooltip(shared = TRUE) %>% 
    
    hc_yAxis(labels = list(formatter = JS("function(){ return Math.abs(this.value) /10000   + '억'; }")),
             tickInterval = 100000,
             min = -maxpop,
             max = maxpop) %>% 
    
    hc_xAxis(xaxis,
             rlist::list.merge(xaxis, list(opposite = TRUE, linkedTo = 0))) %>% 
    
    hc_tooltip(shared = FALSE,
               formatter = JS(
                   
                   "function () { return '<b>' + this.series.name + ', floor ' + this.point.category + '</b><br/>' +
                              'price: ' + Highcharts.numberFormat(Math.abs(this.point.y), 0);}"
               )
    ) 
  • 해당 그래프는 층수별 아파트 가격을 bar chart로 그렸습니다.
  • 또한 위 Motion_1 차트와 비슷하게 JS 함수를 이용했습니다.
  • 위 그래프의 단점은 한 움직임이 끝나면 새로고침을 해주어야 하는데에 반해 이번 차트는 loop = True 함수를 통해 자동적으로 반복되게 만들었습니다.
motion_2
  • 재생버튼을 눌러주세요


SVG maps

korea_apt <- list(

    na = ko_apt_all[grepl("세종특별자치시", ko_apt_all$region), ],
    Gyeonggi = ko_apt_all[grepl("경기도", ko_apt_all$region), ],
    North_Jeolla = ko_apt_all[grepl("전라북도", ko_apt_all$region), ],
    South_Gyeongsang = ko_apt_all[grepl("경상남도", ko_apt_all$region), ],
    South_Jeolla = ko_apt_all[grepl("전라남도", ko_apt_all$region), ],
    Busan = ko_apt_all[grepl("부산광역시", ko_apt_all$region), ],
    North_Gyeongsang = ko_apt_all[grepl("경상북도", ko_apt_all$region), ],
    Sejong = ko_apt_all[grepl("세종특별자치시", ko_apt_all$region), ],
    Daejeon = ko_apt_all[grepl("대전광역시", ko_apt_all$region), ],
    Ulsan = ko_apt_all[grepl("울산광역시", ko_apt_all$region), ],
    Incheon = ko_apt_all[grepl("인천광역시", ko_apt_all$region), ],
    Gangwon = ko_apt_all[grepl("강원도", ko_apt_all$region), ],
    South_Chungcheong = ko_apt_all[grepl("충청남도", ko_apt_all$region), ],
    Jeju = ko_apt_all[grepl("제주특별자치도", ko_apt_all$region), ],
    North_Chungcheong = ko_apt_all[grepl("충청북도", ko_apt_all$region), ],
    Seoul = ko_apt_all[grepl("서울특별시", ko_apt_all$region), ],
    Daegu = ko_apt_all[grepl("대구광역시", ko_apt_all$region), ],
    Gwangju = ko_apt_all[grepl("광주광역시", ko_apt_all$region), ]

)
  • SVG mapshttps://www.amcharts.com/svg-maps/는 다양한 맵차트에 폴리곤형식의 base data를 주는 오픈소스입니다.
  • 한국은 18개의 지역으로, (한개는 결측치) 분리되어있습니다.


mapdata <- get_data_from_map(download_map_data("countries/kr/kr-all"))
glimpse(mapdata)
## Observations: 18
## Variables: 20
## $ hc-group    <chr> "admin1", "admin1", "admin1", "admin1", "admin1", ...
## $ hc-middle-x <dbl> 0.93, 0.81, 0.51, 0.35, 0.72, 0.89, 0.32, 0.50, 0....
## $ hc-middle-y <dbl> 0.70, 0.72, 0.51, 0.46, 0.32, 0.43, 0.67, 0.73, 0....
## $ hc-key      <chr> "kr-4194", "kr-kg", "kr-cb", "kr-kn", "kr-2685", "...
## $ hc-a2       <chr> "NU", "KG", "CB", "KN", "SJ", "PU", "NG", "SJ", "T...
## $ labelrank   <chr> "20", "6", "6", "6", "6", "9", "6", "9", "9", "6",...
## $ hasc        <chr> "-99", "KR.KG", "KR.CB", "KR.KN", "KR.KJ", "KR.PU"...
## $ woe-id      <chr> "-99", "2345969", "2345964", "2345967", "2345972",...
## $ country     <chr> "South Korea", "South Korea", "South Korea", "Sout...
## $ longitude   <chr> "126.592", "127.205", "127.081", "128.217", "126.8...
## $ latitude    <chr> "34.1079", "37.2562", "35.679", "35.3192", "34.816...
## $ alt-name    <chr> NA, "Kyonggi-do|Keiki-do|Kyunggi", "Chollabuk-Do|C...
## $ fips        <chr> NA, "KS13", "KS03", "KS20", "KS18", "KS10", "KS14"...
## $ postal-code <chr> NA, "KG", "CB", "KN", NA, "PU", NA, "SJ", "TJ", "U...
## $ name        <chr> NA, "Gyeonggi", "North Jeolla", "South Gyeongsang"...
## $ type-en     <chr> NA, "Province", "Province", "Province", "Metropoli...
## $ woe-name    <chr> NA, "Gyeonggi-do", "Jeollabuk-do", "Gyeongsangnam-...
## $ woe-label   <chr> NA, "Kyeongki-Do, KR, South Korea", "Jeollabuk-Do,...
## $ type        <chr> NA, "Do", "Do", "Do", "Gwangyeoksi", "Gwangyeoksi"...
## $ region      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, "Incheon",...
DT::datatable(korea)

highmaps chart

hcmap_1 <- hcmap("countries/kr/kr-all",
      data = korea,
      value = "value",
      joinBy = c("hc-key", "code"),
      name = "아파트 평균가",
      dataLabels = list(enabled = TRUE, format = '{point.name}'),
      borderColor = "#FAFAFA", borderWidth = 0.1,
      tooltip = list(valueDecimals = 2, valuePrefix = "", valueSuffix = " 만원")) 
hcmap_1
  • 위 hcmap chart는 hcmap 의 기본적인 사용한 모습입니다.
  • 18개의 도시를 아파트 평균 가격대별로 시각화를 하였습니다.


  • 추가적으로 특정 금액구간별로 색깔을 입히고 원하는 금액대를 클릭해서 원하는 지역만 볼 수 있게 만들었습니다.
  • 노른자 땅(서울)을 보기 위해서는 50000~60000만원만 남기고 클릭해제하면 됩니다.
  • zoomType 함수를 통해서 확대가 가능합니다. (드래그 해보세요)
hcmap_2 <- hcmap("countries/kr/kr-all",
      data = korea,
      value = "value",
      joinBy = c("hc-key", "code"),
      name = "평균가",
      dataLabels = list(enabled = TRUE, format = '{point.name}'),
      borderColor = "#FAFAFA",
      borderWidth = 1,
      tooltip = list(valueDecimals = 0,valuePrefix = "", valueSuffix = " 만원")) %>% 
    hc_colorAxis(dataClasses = color_classes(c(10000, 15000, 20000, 25000, 30000, 50000, 60000)),
                 minColor = "#FFFFFF", maxColor = "#434348", type = "logarithmic") %>% 
    hc_legend(layout = "vertical", align = "right", floating = TRUE, valueDecimals = 0, valueSuffix = "만원") %>% 
    hc_chart(zoomType = "xy", panKey = "shift")
hcmap_2


Back to top

Chapter 2.

4. OpenAPI

  • budongsan패키지의 단점은 제작자가 2016년 11월 이후에 업데이트를 안한다는 것입니다.
  • 한국의 2017년의 최신 아파트 매매가 데이터를 사용하기 위해서는 직접 데이터를 수집하는 수밖에 없습니다.
  • 공공데이터포털http://www.data.go.kr/는 국가가 정부3.0 취지에 맞게 한국의 여러 자료들을 공개하는 사이트입니다.
  • 공공데이터포털에서 국토교통부 실거래가 정보를 REST(Representational State Transfer) 제공합니다.
  • R 에서 위API의 기본적인 사용방법은 원하는 데이터를 Query 형태로 불러들여 JSON 형태로 읽어주면 됩니다.


api_url <- "http://openapi.molit.go.kr/OpenAPI_ToolInstallPackage/service/rest/RTMSOBJSvc/getRTMSDataSvcAptTradeDev?LAWD_CD="
service_key <- "OM9kzkoOD0ZxX3nsqQZ9BfhTIl7e%2FNMFLDoKTCifJlcb%2B2wzMqYFtQ1%2FfO68g5fmoGfa7LW8w1IzCI7AnxCX0w%3D%3D"
#서울시 지역코드

    
locCode <-c("11110","11140","11170","11200","11215","11230","11260","11290","11305","11320",
            "11350","11380","11410","11440","11470","11500","11530","11545","11560","11590",
            "11620","11650","11680","11710","11740")

locCode_nm <-c("종로구","중구","용산구","성동구","광진구","동대문구","중랑구","성북구","강북구","도봉구",
               "노원구","은평구","서대문구","마포구","양천구","강서구","구로구","금천구","영등포구","동작구",
               "관악구","서초구","강남구","송파구","강동구")



datelist <-c("201701","201702","201703")
urllist <- list()
cnt <-0

for(i in 1:length(locCode)){
    for(j in 1:length(datelist)){
        cnt = cnt + 1
        urllist[cnt] <- paste0(api_url,locCode[i],"&DEAL_YMD=",datelist[j],"&numOfRows=1000","&serviceKey=",service_key) 
  }
}
total<-list()

for(i in 1:length(urllist)){
    
    item <- list()
    item_temp_dt<-data.table()
    
    raw.data <- xmlTreeParse(urllist[i], useInternalNodes = TRUE,encoding = "utf-8")
    rootNode <- xmlRoot(raw.data)
    items <- rootNode[[2]][['items']]

    size <- xmlSize(items)
    
    for(j in 1:size){
        item_temp <- xmlSApply(items[[j]],xmlValue)
        item_temp_dt <- data.table( price = item_temp[1],
                                    con_year = item_temp[2],
                                    year = item_temp[3],
                                    street = item_temp[4],
                                    dong = item_temp[11],
                                    aptnm = item_temp[17],
                                    month = item_temp[18],
                                    dat = item_temp[19],
                                    area = item_temp[21],
                                    bungi = item_temp[22],
                                    floor = item_temp[24],
                                    gu_code = locCode[((j-1)%/%12)+1],
                                    gu = locCode_nm[((j-1)%/%12)+1]
                                    )
        item[[j]]<-item_temp_dt
    }
    total[[i]] <- rbindlist(item)
}
  • 최대한 budongsan 패키지와 비슷한 형태로 핸들링했습니다.
seoul_apt_api_2017 <- rbindlist(total)
seoul_apt_api_2017 <- na.omit(seoul_apt_api_2017)
seoul_apt_api_2017$price <- as.numeric(gsub(",", "",seoul_apt_api_2017$price))
hcboxplot(x = seoul_apt_api_2017$price, var = seoul_apt_api_2017$gu,
          name = "Length", color = "#2980b9") %>% hc_add_theme(hc_theme_sandsignika())
  • 2017년 서울의 아파트 매매가격을 본 결과 25개의 자치구가 큰 차이가 없을을 볼 수 있습니다. 예를들어 강남구 의 아파트 매매가가 다른 곳과 크게 다르지 않습니다.

  • 아파트 금액을 이용해 시각화할 아이디어를 찾다가 부동산이나 복덕방에 있는 친근한 사진이 생각났습니다.



5. leaflet

  • leaflet 패키지를 이용하기 위해서는 지구의 x, y축 좌표 즉 위경도 좌표가 필요합니다.
  • 위 공공데이터 포털에서 데이터를 가져오는 API 형식을 다시한번 이용해서 다음지도 API http://apis.map.daum.net/를 이용해서 주소좌표로 변환시켰습니다.
  • 모든 아파트주소를 지도위에 표기하면 과부화가 걸리므로 별로 아파트 매매가격의 평균을 이용해서 표시했습니다.
seoul_apt_api_2017_map <- seoul_apt_api_2017 %>% group_by(dong) %>% summarise(n = n(), mean = round(mean(price), 0))
seoul_apt_api_2017_map <- as.data.table(seoul_apt_api_2017_map)
for(i in 1:nrow(seoul_apt_api_2017_map)){
    
    post <- paste("서울특별시", seoul_apt_api_2017_map[i, ]$dong, sep = " ")
    # post <- iconv(post, from = "cp949", to = "UTF-8") # win version
    post_url <- URLencode(post)
    
    url <- paste0("https://apis.daum.net/local/geo/addr2coord?apikey=1d21e7ebdcb92cd633f79cc7c93a7667&q=",
                  post_url,"&output=json")
    
    url_query <- readLines(url)
    
    url_json <- fromJSON(paste0(url_query, collapse = ""))
    
    x_point <- mean(url_json$channel$item$point_x)
    y_point <- mean(url_json$channel$item$point_y)
    
    seoul_apt_api_2017_map[i, x_ := x_point]
    seoul_apt_api_2017_map[i, y_ := y_point]
    seoul_apt_api_2017_map[i, n_ := i]
    
}
DT::datatable(seoul_apt_api_2017_map)
seoul_apt_api_2017_map_purr <- seoul_apt_api_2017_map %>% 
    mutate(level = cut(mean, c(0, 50000, 100000, 200000, 500000), labels = c("~ 5억", "5억 ~ 10억", "10억 ~ 20억", "20억 ~ 50억")))

seoul_apt_api_2017_map_purr_split <- split(seoul_apt_api_2017_map_purr, seoul_apt_api_2017_map_purr$level)
seoul_leaf <- leaflet(width = "100%") %>% addTiles()

seoul_leaf_ <- names(seoul_apt_api_2017_map_purr_split) %>%
    walk( function(df) {
        seoul_leaf <<- seoul_leaf %>%
            addMarkers(data = seoul_apt_api_2017_map_purr_split[[df]],
                       lng = ~x_, lat = ~y_,
                       popup = ~as.character(dong),
                       label = ~as.character(mean),
                       labelOptions = labelOptions(noHide = T, textsize = "15px", direction  =  'auto'),
                       group  =  df,
                       clusterOptions  =  markerClusterOptions(removeOutsideVisibleBounds  =  F))
        })
    

seoul_leaf <- seoul_leaf %>%
  addLayersControl(
    overlayGroups = names(seoul_apt_api_2017_map_purr_split),
    options = layersControlOptions(collapsed = FALSE)
  ) %>% addMiniMap(toggleDisplay = TRUE)
  • leaflet 패키지가 가끔식 R의 Markdown 과 호환상 충돌을 일으킬 때가 있습니다.
  • 지도상에서 금액대별 군집화를 통해 시각화했습니다.
  • 원하는 금액대를 선택해서 볼 수 있습니다.
  • 위 결과를 RPubs에 올려놓았습니다. https://rpubs.com/J-Park/KATA_leaflet
seoul_leaf

6. Thank you


Back to top