부동산 매매가격 추이 분석

해당 분석의 주된 목적은 R을 이용한 정량적 데이터 분석에 대한 이해를 도모하고 평소 관심을 가졌던 부동산 매매가격에 대한 추이를 분석하는데 있다. 부동산은 살아가면서 특히 중요한 의사결정이 필요한 분야이다. 최근에는 다방이나 직방 같은 어플리케이션이 제공되며 부동산에 대한 정보를 제공해주고 있다. 이러한 정보는 객관적인 데이터를 제공하여 사람들의 의사결정을 도와 주고 주관적인 의사결정에 도움을 주고 있다. 이러한 맥락에서 이번 분석을 통한 매매가격 분석은 부동산을 사고 팔때 도움을 줄 수 있을 것이라 생각한다. 이 분석에 큰 도움이 된 글은 전희원 님의 “R을 이용한 부동산 데이터 분석 케이스 스터디”임을 밝힌다.

1. 분석 목적

  • 서울 지역 아파트 매매가의 추이를 살펴본다.
  • 서울 특정구(관심을 갖는 구)의 매매가를 예측 해본다.

2. 데이터 수집 및 처리

먼저, 분석을 위해 데이터가 필요하다. 어디에서 데이터를 어떤 방법으로 수집 할 수 있는지 구글링이나 네이버를 통해 찾아보았다. 처음에 찾은 데이터는 국토교통부에서 제공하는 실거래가 공개시스템에서 해당 데이터에 대한 엑셀 파일을 다운로드 받을 수 있었다. 그러나 엑셀로 데이터를 다운받을 수 있는 날짜 범위가 최대 한달 밖에 되지 않아 수동적으로 데이터를 수집해야 하는 문제점이 있었다. 다른 방법을 찾다가 공공데이터포털에서 제공되는 API를 찾을 수 있었다.공공데이터포털링크 이 방법에도 제한 사항(일반사용자에게는 1일 1000회트래픽 제한)이 있었지만 앞에 방법보다는 더 좋아 보여 이 방법으로 데이터 수집을 하고자 결정했다.

2.1 데이터 수집

-필요한 라이브러리 추가

  • XML : API에서 xml 데이터 수집을 위한 패키지
  • data.table : data.frame 보다 더 빠르고 효율적인 data.table 사용을 위한 패키지
  • stringr : 문자열 처리 및 파이프 연산자 사용을 위한 패키기
  • ggplot2 : 시각화를 위한 패키지
library(XML)
## Warning: package 'XML' was built under R version 3.3.2
library(data.table)
## Warning: package 'data.table' was built under R version 3.3.2
library(stringr)
## Warning: package 'stringr' was built under R version 3.3.2
library(ggplot2)
## Warning: package 'ggplot2' was built under R version 3.3.2

-API 접근을 위한 변수 설정

  • service_key : 공공데이터 포털에서 API 사용 신청 시 받는 키값
  • locCode, datelist : API 접근 시 사용되는 매개변수
  • 공공데이터 포털에서 일반용 API 신청시 1일 1000번 이상 접근 제한이 있음. 따라서 많은 데이터를 수집하고 싶으면 datelist의 양을 조절해야 함. 여기서는 2013~2016년 까지 총 4년간의 서울시 아파트 매매 데이터를 수집했다.
service_key <- "EeBjN2xdCzzcqHvefO0rZXaycAim0uGpKxnOX72PY1UpkSZnifzIK1kxLm61XXaQ4pFxhbW%2F%2FZbmQDKFiAFNVA%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("201501","201502","201503","201504","201505","201506","201507","201508","201509","201510","201511","201512","201401","201402","201403","201404","201405","201406","201407","201408","201409","201410","201411","201412","201601","201602","201603","201604","201605","201606","201607","201608","201609","201610","201611","201612")
#datelist <-c("201301","201302","201303","201304","201305","201306","201307","201308","201309","201310","201311","201312")

-데이터 수집할 url list 작성

urllist <- list()
cnt <-0
for(i in 1:length(locCode)){
  for(j in 1:length(datelist)){
    cnt=cnt+1
    urllist[cnt] <-paste0("http://openapi.molit.go.kr:8081/OpenAPI_ToolInstallPackage/service/rest/RTMSOBJSvc/getRTMSDataSvcAptTrade?LAWD_CD=",locCode[i],"&DEAL_YMD=",datelist[j],"&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']]
 # price','con_year','year','dong','aptnm','month','dat','area','bungi','loc','floor'
  size <- xmlSize(items)
  for(i in 1:size){
     item_temp <- xmlSApply(items[[i]],xmlValue)
     item_temp_dt <- data.table(price=item_temp[1],
                                con_year=item_temp[2],
                                year=item_temp[3],
                                dong=item_temp[4],
                                aptnm=item_temp[5],
                                month=item_temp[6],
                                dat=item_temp[7],
                                area=item_temp[8],
                                bungi=item_temp[9],
                                loc=item_temp[10],
                                floor=item_temp[11])
     item[[i]]<-item_temp_dt
  }
  total[[i]]<-rbindlist(item)
}
result_apt_data <- rbindlist(total)
save(result_apt_data, file="apt_item_sales_dt.Rdata")

2.2 데이터 전처리

-데이터 로드

2014~2016년도 데이터 수집한 뒤 2013년도 데이터를 수집할 시 1일 1000회 트래픽 제한을 초과하므로 하루 뒤에 2013년도 데이터를 수집하였다.

result_apt_data1<-get(load("apt_item_sales_dt.Rdata")) #load 2014~2016년도 데이터
result_apt_data2<-get(load("apt_item_sales_dt2.Rdata")) #load 2013년도 데이터
result_apt_data3<-get(load("apt_item_sales_dt3.Rdata")) #2011~2012년도 데이터 데이터

-데이터 전처리

일단 2013년 데이터와 2014~2016년 데이터를 합친 뒤 데이터 전처리 과정을 거친다.

apt_data1<-data.table(result_apt_data1)
apt_data2<-data.table(result_apt_data2)
apt_data3<-data.table(result_apt_data3)
apt_data<-rbindlist(list(apt_data1,apt_data2,apt_data3))
# 데이터 내 결측 값 확인
colSums(is.na(apt_data)) 

#loc가 잘못 들어가 있는 데이터에 대한 처리
index_na <-is.na(apt_data$floor)
apt_data[index_na]$floor <- apt_data[index_na]$loc
apt_data[index_na]$loc <- apt_data[index_na]$bungi
apt_data[index_na]$bungi <- NA

#컬럼 속성 수정 및 필요한 컬럼 생성
apt_data[,price:=as.character(price)%>%str_trim()%>%sub(",","",.)%>%as.numeric()]
apt_data[,con_year:=as.character(con_year)%>%str_trim()%>%as.numeric()]
apt_data[,year:=as.character(year)%>%str_trim()%>%as.numeric()]
apt_data[between(as.numeric(as.character(month)),1,3),qrt:='Q1']
apt_data[between(as.numeric(as.character(month)),4,6),qrt:='Q2']
apt_data[between(as.numeric(as.character(month)),7,9),qrt:='Q3']
apt_data[between(as.numeric(as.character(month)),10,12),qrt:='Q4']
apt_data[,dong:=as.character(dong)%>%str_trim()]
apt_data[,yyyyqrt:=paste0(year,qrt)]
apt_data[,month:=as.character(month)%>%str_trim()%>%as.numeric()]
apt_data[,yyyym:=paste0(year,month)]
lapply(locCode,function(x){apt_data[loc==x,gu:=locCode_nm[which(locCode==x)]]})

3. 데이터 분석

데이터 수집 및 전처리 과정을 통해 정제된 데이터를 분석합니다. 먼저 서울 지역 평균 매매가 추이를 살펴본다.

apt_data_seo_price <- aggregate(apt_data$price,by=list(apt_data$yyyyqrt),mean)
names(apt_data_seo_price) <- c("yyyyqrt","price")
ggplot(apt_data_seo_price,aes(x=yyyyqrt,y=price,group=1))+
  geom_line() +
  xlab("분기별")+
  ylab("평균매매가격")+
  theme(axis.text.x=element_text(angle=90))+
  stat_smooth(method='lm')
[그림1]서울 지역 평균 매매가 추이

[그림1]서울 지역 평균 매매가 추이

그림1은 서울 지역 평균 매매가 추이를 분기별로 그려본 것이다. 서울 지역 평균 매매가는 2012년 1분기, 2013년 3분기, 2015년3분기에 하락세를 보였지만 전체적인 추이에는 영향이 없었고 꾸준히 상승하는 추이를 보이고 있다. 그 다음은 지역별(구) 평균 매매가 추이를 살펴보고자 한다.

지역별(구) 아파트 평균 매매가 추이

apt_data_by_gu_qrt <- aggregate(apt_data$price, by=list(apt_data$yyyyqrt,apt_data$gu), mean)
names(apt_data_by_gu_qrt) <- c('yyyyqrt','gu','pricemean')

ggplot(apt_data_by_gu_qrt,aes(x=yyyyqrt,y=pricemean,group=gu))+
  geom_line()+
  facet_wrap(~gu,scale='free_y', ncol=3)+
  theme(axis.text.x=element_blank())+
  stat_smooth(method="lm")
[그림2]지역별(구) 아파트 평균 매매가 추이

[그림2]지역별(구) 아파트 평균 매매가 추이

그림2는 서울 지역별(구) 평균 매매가 추이를 지역별 분기별로 플로팅하고 선형모형을 피팅하여 추세를 살펴봤다. 대부분의 구에서 상승 추이가 보이는 것을 볼 수 있다. 마포구와 종로구는 다른 구에 비해 평평한 선형모형을 보여 평균 매매가가 유지되고 있는 것으로 보인다.

시계열 분석 시계열에 패턴이 존재한다면 미래에도 이러한 패턴이 계속 될 것이라는 가정하에 예측을 해볼 수 있다. 그렇다면 시계열에 패턴이 존재하는지 확인하는 방법은 시계열이 랜덤한지를 알아보는 랜덤성 검증이 있다. 랜덤성을 검증하는 런 검정을 통해서 시계열이 랜덤화 시계열 인지 확인해 보자.

gu_meanprice <- as.data.table(aggregate(apt_data$price,by=list(apt_data$yyyyqrt,apt_data$gu),mean))
head(gu_meanprice)
##    Group.1 Group.2        x
## 1:  2011Q1  강남구 83439.29
## 2:  2011Q2  강남구 81559.04
## 3:  2011Q3  강남구 83605.25
## 4:  2011Q4  강남구 86068.22
## 5:  2012Q1  강남구 79932.36
## 6:  2012Q2  강남구 85356.35
names(gu_meanprice)<- c('yyyyqrt','gu','price')
#중복없이 구 추출
gu_list <- unique(gu_meanprice$gu)
#랜덤성 검정을 위한 패키지 로드
library(lawstat)
# 각 구별 매매가격의 랜덤성 검정 결과를 runs_p변수에 추가
runs_p<-c()
for(g in gu_list){
  runs_p <- c(runs_p, runs.test(gu_meanprice[gu %in% g,price])$p.value)
}

ggplot(data.table(gu_list, runs_p), aes(x=gu_list, y=runs_p, group=1)) +
  geom_line() + geom_point() +
  ylab('P-value') + xlab('구') +
  theme(axis.text.x=element_text(angle=90))

gu_list[which(runs_p>0.05)]
## [1] "강북구" "강서구" "광진구" "동작구" "양천구" "중구"

강북구, 광진구, 동작구, 양천구, 용산구, 중구를 제외하고에 p값이 0.05 보다 작으므로 귀무가설 (랜덤하다)기각 할 수 있다 따라서 랜덤하지 않은 어떤 패턴이 있다고 할 수 있다.

그럼 이중에서 내가 살고 있는 관악구를 추출하여 시계열 패턴을 그려보겠다. ts함수를 통해 데이터를 시계열 데이터로 변경해준다. stl함수를 통해 시계열 데이터가 갖고 있는 요소들을 분해 한다.

gwanak_data <- apt_data[gu=="관악구",]
gwanak_price <- aggregate(gwanak_data$price,by=list(gwanak_data$yyyyqrt),mean) 
names(gwanak_price)<-c('yyyyqrt','price')
tot_ts <- ts(gwanak_price$price,start=c(2011,1),frequency = 4)
plot(stl(tot_ts,s.window = 'periodic'))
[그림4]시계열 분할

[그림4]시계열 분할

여기서 사용한 모델링 방법은 ARIMA 모형을 사용했다. ARIMA 모델의 적합 판별을 위해 KPSS test, ACF, PACF를 그려 확인하여 적절한 d,p,q를 찾아야 하나 여기서는 auto.arima 를 통해 자동 모델링을 하였다.

library(forecast)
arima_mdl <- auto.arima(tot_ts)
arima_mdl
## Series: tot_ts 
## ARIMA(1,1,0)                    
## 
## Coefficients:
##           ar1
##       -0.4359
## s.e.   0.1834
## 
## sigma^2 estimated as 4461338:  log likelihood=-208.31
## AIC=420.61   AICc=421.21   BIC=422.88
tsdiag(arima_mdl)
[그림5]ARIMA 모델링 수행

[그림5]ARIMA 모델링 수행

tsdiag함수로 구한 모델의 타당성 검정을 실시한다. 실시 결과 모형의 가정이 대부분 만족하는 결과를 보였으므로 해당 모델은 예측 모델로 사용 가능하다. auto.arima 함수를 사용하여 관악구 평균 매매가 시계열 자료에 적절한 모형은 ARIMA(1,1,0) 모형을 결정했다.

accuracy(arima_mdl)
##                    ME     RMSE      MAE       MPE     MAPE      MASE
## Training set 192.1009 2022.266 1276.514 0.2352073 3.978102 0.4925517
##                     ACF1
## Training set -0.01218384
plot(forecast(arima_mdl,h=8))
[그림6]서울 관악구 매매가 추이 예측

[그림6]서울 관악구 매매가 추이 예측

pred <- predict(arima_mdl, n.ahead = 8)
#미래 예측값 그래프를 점선으로 표시
ts.plot(tot_ts,pred$pred,lty=c(1,3))
[그림6]서울 관악구 매매가 추이 예측

[그림6]서울 관악구 매매가 추이 예측

forecast 함수로 이후 8분기(2년) 간의 매매가 추이를 예측 해보았다. 그림에서 보는 것과 같이 상승, 하강 추이가 나타나지 않고 매매가가 유지 될 것이라는 예측이 나왔다.