• Home

아파트 매매 추이 분석/예측 해보기

필자가 R을 처음 써본게 2008년 즈임이었고, 본격적으로 업무의 필수 도구로 사용하기 시작한건 2011년 정도다. 그 당시만 해도 부족한 컴퓨팅 리소스와 경이로울 정도로 성능이 낮은 퍼포먼스 그리고 필자의 경험 부족이 결합해 매우 어렵게 이런 저런 분석 프로젝트를 수행했던것 같다. 같은 방식의 분석을 3년 정도 지난 시점에서 비슷한 량의 데이터로 수행하는데 엄청난 분석 퍼포먼스 향상을 피부로 느낄정도여서 참 많이 좋아졌다는 생각을 해본다. 게다가 다양한 패키지의 개발로 인해 이번과 같이 R을 이용한 공공 데이터 수집에 대한 내용도 다룰 수 있게 되었다.

데이터 수집은 전통적으로 Python이 가장 강세를 보였던 분야인데, Python이 전문 라이브러리로 특화가 되어가는 와중에 여러 오픈소스 프로젝트들로 인해 간단한 수집 정도는 R을 기반으로 수행할 수 있게 되었다. 이러한 분위기에 힘입어 여러 CRAN Task View에서 웹 테크놀러지 분과가 최근 추가되고 이제는 R로 수행할 수 있는 대표적인 영역중에 하나로 발돋움 할 수 있게 되었다.

웹에는 엄청난 양의 데이터가 숨쉬고 있고, 특징적으로 비정형적인 특성을 가진 텍스트가 대부분이어서 이를 분석 가능하게 하기 위해 가져오고, 정리하는데 많은 공수가 소요된다. 일반적으로 분석 목적에 맞게 데이터를 가공하는데 전체 데이터 분석 시간의 70% 이상이 소요된다고 알려져 있는데, 웹 데이터를 기반으로 한다면 80%이상 소요된다고 필자는 자신있게 이야기 할 수 있을 것 같다.

분석 데이터 수집과 데이터 얼개 살펴보기

분석을 위해 사용할 원천 데이터는 국토교통부 실거래가 홈페이지에서 획득가능하며, 이 데이터를 수집하고 정리하는 코드는 아래와 같다.

#웹 데이터 수집과 저장을 위한 패키지
library(rvest) 
#rvest를 구성하는 저수준 API를 저장하고 있으며 
#rvest에 대한 자세한 핸들링을 하기 위해 사용한다. 
library(httr)  
#문자열 처리에 대한 패키지로 
library(stringi)
#국토교통부에서 원 데이터를 엑셀로 제공하기 때문에 
#엑셀 데이터를 읽어들이기 위해서 필요함 
library(XLConnect)
# data.frame보다 편리하고 효율적인 테이블 핸들링 패키지 
library(data.table)


## 엑셀 데이터 다운로딩 부분(데이터는 working dir아래 data 디렉토리에 쌓인다.)
su <- file("succ.txt", "w")

#흡사 웹브라우저처럼 웹서버에서 인식하게 한다.  
agent_nm <- paste0("Mozilla/5.0 (Macintosh;", 
                   "Intel Mac OS X 10.10; rv:35.0) Gecko/20100101 Firefox/35.0")

#게시판 번호의 최대값을 가져온다. 
maxidx <- html('http://rt.molit.go.kr/rtFile.do?cmd=list') %>% 
 html_nodes('.notiWhite1 .notiBorad01') %>%
 html_text %>% as.numeric %>% max

#가져온 게시판 번호를 이용해 전체 실거래가 페이지를 방문해 파일을 다운로드한다.
for(i in maxidx:1){
  urls_view <- sprintf("http://rt.molit.go.kr/rtFile.do?cmd=view&seqNo=%d", i)
  r <- GET(urls_view,
           user_agent(agent_nm))
  htxt <- html(r, "text")
  html_nodes(htxt, "td.notiBorad14")[[1]] %>%
    html_text() 
  #만일 페이지에 아래와 같은 텍스트가 존재하면 
  #다음 페이지를 수집한다. 
  if((html_nodes(htxt, "td.notiBorad14")[[2]]  %>% 
      html_text()  %>% stri_trim_both) == 
       '첨부파일이 존재하지 않습니다.') next

  download_tags <- html_nodes(htxt, "td.notiBorad14")[[2]] %>%
    html_nodes('a[href^="javascript:jsDown"]')  
  #페이지 내에 있는 다운로드 태그를 순회하며 태그 이름(파일명)과 
  #링크 정보(파일 다운로드 링크)를 추출해 각각 저장한다. 
  for(dtag in download_tags){

    dtag %>% html_attr("href") %>% 
      stri_match_altl_regex(pattern="javascript:jsDown\\('([0-9]+)','([0-9]+)'\\);") %>%
      .[[1]]  %>%  
          { 
            f_idx <<- .[2] %>% as.numeric
            s_idx <<- .[3] %>% as.numeric
          }

    f_nm <- dtag %>% html_text

    urls <- sprintf(paste0("http://rt.molit.go.kr/", 
                           "rtFile.do?cmd=fileDownload&seq_no=%d&file_seq_no=%d"), 
                    f_idx,s_idx)
    r <- GET(urls,
             user_agent(agent_nm))
    bin <- content(r, "raw")

    #1kb 미만의 데이터는 버림(에러 페이지?) 
    if(length(bin) < 1000) next
    writeBin(bin, sprintf("data/%s",f_nm))
    cat(sprintf("%d, %d\n", f_idx,s_idx), file = su)
    print(sprintf("%d, %d", f_idx,s_idx))
  }
}

close(su)




## 엑셀 데이터에서 테이블을 추출해 하나의 아파트 매매 데이터로 통합하는 코드 
## 연립 다세대, 단독 다가 데이터도 간단한 코드 변환으로 통합할 수 있다. 


f_list <- list.files('data') %>%  stri_trans_nfc  %>% .[stri_detect_fixed(.,'매매아파트')]

total_list <- list()

#cnts <- 0

for(xlsf in f_list){
  wb <- loadWorkbook(paste0('data/',xlsf))
  sells <- list()
  fname <- stri_replace_last_fixed(xlsf, '.xls',"")
  yyyymm <- substring(fname, 1, 6)
  typenm <- substring(fname, 9)
  for(nm in getSheets(wb)) {
    df <- data.table(readWorksheet(wb, sheet = nm, header = TRUE))
    df[,`:=`(region=nm, yyyymm=yyyymm, typenm=typenm)]
    df[,`거래금액.만원.`:= stri_replace_all_fixed(`거래금액.만원.`, ',', '')]
    sells[[nm]] <- df
  }
  total_list[[paste0(yyyymm,typenm)]] <- rbindlist(sells)
  #cnts <- cnts + 1
  #if(cnts > 10) break
}

result_sales_dt <- rbindlist(total_list)



setnames(result_sales_dt, 1:10,
         c('si_gun_gu', 'm_bun', 's_bun', 'dangi', 'area',
           'cont_date', 'price', 'floor', 'year_of_construct', 'road_nm'))

result_sales_dt[,price:=as.numeric(price)]
result_sales_dt[,floor:=as.numeric(floor)]
result_sales_dt[,mm:=substr(yyyymm, 5,6)]
result_sales_dt[between(as.numeric(mm), 1, 3), qrt:='Q1']
result_sales_dt[between(as.numeric(mm), 4, 6), qrt:='Q2']
result_sales_dt[between(as.numeric(mm), 7, 9), qrt:='Q3']
result_sales_dt[between(as.numeric(mm), 10, 12), qrt:='Q4']
result_sales_dt[,yyyyqrt:=paste0(substr(yyyymm, 1,4), qrt)]
result_sales_dt[,yyyy:=factor(substr(yyyymm, 1,4))]
result_sales_dt[,yyyyqrt:=factor(yyyyqrt)]


#결과 데이터 저장 
save(result_sales_dt, file='result_sales_dt.RData')

참고로 웹 데이터를 수집하는 코드는 웹 페이지의 구조가 변경되면 반드시 바뀌어야 될 수 밖에 없는 운명을 가진 코드이다. 따라서 실거래가 홈페이지 자료제공 방식이 바뀌거나 혹은 조그마한 개편이라도 되면 코드를 자료제공 레이아웃에 맞게 바꿔야 된다는 것을 기억하길 바란다. result_sales_dt.RData 데이터 파일은 실습을 위해 GitHub 링크에서 제공하고 있으니 코드를 실행하거나 이해해야 되는 부담은 일단 떨치길 바란다.

데이터 수집 코드가 동작하는 방식은 매매정보가 포함된 엑셀 파일이 존재하는 URL에 대해서 어떠한 순차적인 패턴이 있을거라는 판단을 하고 이 순차 패턴을 쭉 따라가 엑셀파일을 다운로드 받는 것이다. 이렇게 받은 엑셀 파일들은 분석 목적에 맞게 정리되 data.table객체로 저장되게 된다.

앞으로 코드를 이해하기 위해 필요한 패키지 설명을 간단하게 하겠다.

  • data.table : data.frame과 같은 데이터 저장 클래스를 공유하며, data.frame에 비해 수십에서 수백배 빠른 데이터 처리 능력을 보여주며 간단한 코드로 다양한 데이터 전처리를 할 수 있게 한다. 오백만건 이상의 레코드의 데이터를 사용해야 되기 때문에 해당 패키지를 사용했다.
  • dplyr : 이 글에서는 data.table과 함께 사용이 되며 파이프 연산자를 통해 코드의 가독성을 높여주고, data.table 혹은 data.frame이든 원본 소스에 상관없이 같은 전처리 코드로 다양한 소스 데이터를 다룰 수 있게 해준다.
  • ggplot2 : 대표적인 R기반 시각화 도구
  • lubridate : R에서 다소 복잡한 시간에 관련된 데이터를 쉽게 다룰 수 있게 해주는 패키지

이상의 패키지들이 대표적인 시각화 및 데이터 전처리 패키지들이다. 이정도 패키지들은 손에 익혀 두어야 어떠한 데이터든 빠르게 정리할 수 있다.

  • forecast : 시계열 분석을 위한 패키지
  • randtests : 램덤성을 검정하는 패키지

필자가 분석에 사용한 머신은 16GB의 메인 메모리를 가지고 있는 맥북 프로이다. 최대 약 5백만건의 데이터를 로딩해야 되기 때문에 메인 메모리 8GB 이상의 머신에서의 실습을 추천드린다. 참고로 수집한 5백만건의 매매 데이터의 메모리 로딩 크기는 약 633Mb이다.

# 수집한 매매 데이터 로딩 
load('result_sales_dt.RData')

데이터가 주어지면 전체적으로 어떻게 구성이 되어 있는지 확인이 필요하다. 많은 경우 head 명령어나 str 명령어를 주로 사용하나 필자는 dplyrglimpse 명령어를 주로 사용한다.

glimpse(result_sales_dt, width=60)
## Observations: 5160941
## Variables:
## $ si_gun_gu         (chr) " 서울특별시 강남구 개포동", " 서울특별시 강남구...
## $ m_bun             (dbl) 12, 12, 12, 12, 12, 12, 12, 1...
## $ s_bun             (dbl) 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,...
## $ dangi             (chr) "대청", "대청", "대청", "대청", "대청",...
## $ area              (dbl) 39.53, 39.53, 39.53, 51.12, 6...
## $ cont_date         (chr) "21~31", "1~10", "11~20", "11...
## $ price             (dbl) 22300, 24800, 23720, 33700, 4...
## $ floor             (dbl) 15, 5, 15, 8, 14, 7, 9, 1, 5,...
## $ year_of_construct (dbl) 1992, 1992, 1992, 1992, 1992,...
## $ road_nm           (chr) "개포로109길", "개포로109길", "개포로109...
## $ region            (chr) "서울", "서울", "서울", "서울", "서울",...
## $ yyyymm            (chr) "200601", "200601", "200601",...
## $ typenm            (chr) "아파트", "아파트", "아파트", "아파트", "...
## $ mm                (chr) "01", "01", "01", "01", "01",...
## $ qrt               (chr) "Q1", "Q1", "Q1", "Q1", "Q1",...
## $ yyyyqrt           (fctr) 2006Q1, 2006Q1, 2006Q1, 2006...
## $ yyyy              (fctr) 2006, 2006, 2006, 2006, 2006...

si_gun_gu는 매매가 일어날 시군을 의미하며, m_bun, s_bun은 번지를 의미한다. area는 \(m^2\) 단위의 면적을 의미한다. cont_date는 계약일, price는 만원단위의 매매가격이다. road_nm은 도로명 주소, region은 지역, yyyymm 매매 년월을 의미한다.

관심을 두는 문제는 아래와 같은 질문에 대한 나름의 답을 구하는 것이다.

  • 아파트 매매 추이는 어떻게 되는가? 그리고 매매량 예측이 가능한가?

일단 분석전 일반적인 예상을 해보자면 부동산이라는건 가격을 결정하고 수요를 결정하는 많은 외부 요인들이 많기 때문에 예측이 어려울 것이라는 생각을 해본다. 하지만 그 불확실한 정보량이 어느정도 되는지 가늠해 보는것도 의미가 있을 것이라 생각한다.

매매 추이

ggplot2로 간단하게 분기별 아파트 매매건수를 시각화 해보겠다. 2015년 2분기는 아직 완전한 데이터가 수집된 상황이 아니므로 제거한다.

첫번째 라인이 처음 등장하는 data.table 문법인데, 아주 간단하게 data.table 문법을 SQL의 문(statement)으로 설명하자면 아래와 같다.

data.table명[where, select, group by]

SQLwhere절에 해당하는 곳은 데이터를 조건에 맞게 필터링 하는 곳이며, select는 어떠한 필드를 보여줄지 선택하는 곳이고, group by문은 어떠한 기준으로 데이터를 요약해서 보여줄지 결정하는 곳이다.

따라서 아래 구문에서는 쿼터별로 매매수(.Ngroup by 조건에 해당되는 레코드 수를 리턴하는 함수)를 카운팅해서 qrt_cnts라는 이름의 data.table객체를 만들게 된다. data.table객체는 data.frame객체를 입력받는 ggplot()과 같은 함수에 그대로 적용이 가능해 별도의 변환작업 없이 활용이 가능하다는게 가장 큰 장점중에 하나이다.

ggplot2 패키지의 시각화 방식은 데이터와 그래프로 표현되는 미적(aesthetic)객체를 어떻게 매핑시키는지를 서술하는게 가장 기본이다. 그림1의 그래프의 경우 X축에 data.table 객체의 쿼터컬럼(yyyyqrt), Y축에 쿼터별 매매횟수(N)을 매핑 시키고 보여줄 시계열은 1종류라는것을 group 파라메터로 명시해 준다. 이런 매핑 정보를 기반으로 geom_point 함수나, 이후 + 연산자로 추가되는 모든 레이어관련 함수들이 하나의 그래프를 그리기 위해 동작하게 된다. 필자의 경우 이런 미적객체들의 정보가 추가 레이어들에 상속된다라고 설명을 하곤한다.

물론 각 레이어에서 별도의 미적매핑을 사용할 수 있는데, 이런 미적매핑은 해당 레이어 에서만 유효하게 된다. theme 명령어는 각 축이나 레이블에 다양한 표현을 하기 위해서 제공되는 명령어로 X축의 레이블 텍스트가 겹치는 현상을 없애기 위해 명령어로 텍스트를 90도 회전해 표현하게 했다. stat_smooth의 경우 X,Y 변수간의 선형, 혹은 비선형적인 패턴을 시각화 하기 위해 주로 쓰이며, 여기서는 선형회귀 모형으로 피팅된 값을 뿌려주도록 했다.

좀더 자세한 설명은 필자가 온라인으로 오픈해둔 R기반 데이터 시각화라는 책을 참고하길 바란다.

qrt_cnts <- result_sales_dt[yyyyqrt != '2015Q2',.N,yyyyqrt]

ggplot(qrt_cnts, aes(x=yyyyqrt, y=N,group=1)) + 
  geom_line() + xlab("년도분기") + ylab("매매건수") + 
  theme(axis.text.x=element_text(angle=90)) + stat_smooth(method='lm')

분기별 아파트 매매건수

그림1은 전체 추이를 분기별로 뿌려본 것이다. 부동산 활황기(2006년)에 엄청난 매매 건수 상승을 가져왔으며, 이후 크고 작은 매매건수 변동이 있었으나 추이에 큰 영향이 없다가 최근 2014년부터 점차 매매건수가 상승하는 추이를 보이고 있음을 알 수 있다. 물론 이런 추이가 어느 지역에서 발생하는지 확인해볼 필요가 있으니 지역별 추이를 시각화 해보도록 하자!

#group by 절에 region을 추가해서 쿼터별 지역별 매매량을 계산하게 함 
region_cnts <- result_sales_dt[yyyyqrt != '2015Q2',.N,.(yyyyqrt,region)]

#지면 여건상 ` theme(axis.text.x = element_blank())`로 X 레이블을 제거했다. 
ggplot(region_cnts, aes(yyyyqrt, N,group=region)) + 
  geom_line() + facet_wrap(~region,scale='free_y', ncol=3) + stat_smooth(method = 'lm') + 
  theme(axis.text.x = element_blank())