최근 몇 년 사이 부동산 열풍이 식을 줄 모르고 달아오르고 있습니다. 이번 분석은 정부의 정책 등 외부적인 요인을 배제한 채 주택의 내용연수, 평수 등 질적 요소를 대상으로 횡단면 분석을 진행하였습니다. 질적 요소 중 어떤 변수가 아파트 매매가에 가장 큰 영향을 미치는 지, 얼마나 영향을 미치는 지 분석하고 결과를 도출 하였습니다.
library(tidyverse)
library(ggplot2)
library(dplyr)
library(lubridate)
library(knitr)
library(funModeling)
library(corrplot)
library(recipes)
library(ranger)
library(vip)
library(fpp2)
library(patchwork)
# 2020년 서울 내 아파트 실거래가 데이터셋(출처 : 국토교통부)
apt_raw <- read.csv("./dataset/apt_raw.csv", fileEncoding = 'euc-kr', encoding = 'utf-8')
head(apt_raw)
## X 시군구 번지 본번 부번 단지명 전용면적...
## 1 1 서울특별시 강남구 개포동 655-2 655 2 개포2차현대아파트(220) 77.75
## 2 2 서울특별시 강남구 개포동 655-2 655 2 개포2차현대아파트(220) 77.75
## 3 3 서울특별시 강남구 개포동 655-2 655 2 개포2차현대아파트(220) 77.75
## 4 4 서울특별시 강남구 개포동 655-2 655 2 개포2차현대아파트(220) 77.75
## 5 5 서울특별시 강남구 개포동 655-2 655 2 개포2차현대아파트(220) 77.75
## 6 6 서울특별시 강남구 개포동 655-2 655 2 개포2차현대아파트(220) 77.75
## 계약년월 계약일 거래금액.만원. 층 건축년도 도로명 해제사유발생일
## 1 200603 10 59,500 7 1988 언주로 103 NA
## 2 200603 29 60,000 6 1988 언주로 103 NA
## 3 200604 29 67,000 9 1988 언주로 103 NA
## 4 200606 1 60,000 4 1988 언주로 103 NA
## 5 200610 20 72,250 5 1988 언주로 103 NA
## 6 200610 30 73,500 8 1988 언주로 103 NA
apt <- apt_raw %>%
as_tibble() %>%
separate(시군구, into = c("Si", "Gu", "Dong"), sep = " ") %>%
select(-c(Si, 번지, 본번, 부번, 도로명, 해제사유발생일, 계약일),
LvArea = 전용면적..., SalePrice = 거래금액.만원., Floor = 층,
BuiltYr = 건축년도, apt_name = 단지명) %>%
separate(계약년월, into = c("YrSold", "MoSold"), sep = 4, convert = T) %>%
filter(YrSold %in% 2010:2020) %>%
mutate(SalePrice = parse_number(SalePrice))
head(apt)
## # A tibble: 6 x 10
## X Gu Dong apt_name LvArea YrSold MoSold SalePrice Floor BuiltYr
## <int> <chr> <chr> <chr> <dbl> <int> <int> <dbl> <int> <int>
## 1 310725 강남구 개포~ 개포6차우성~ 55.0 2010 1 66000 3 1987
## 2 310726 강남구 개포~ 개포6차우성~ 80.0 2010 8 80600 5 1987
## 3 310727 강남구 개포~ 개포6차우성~ 67.3 2010 8 80000 4 1987
## 4 310728 강남구 개포~ 개포6차우성~ 80.0 2010 9 83000 1 1987
## 5 310729 강남구 개포~ 개포6차우성~ 67.3 2010 12 67000 4 1987
## 6 310730 강남구 개포~ 개포우성3차 104. 2010 1 88000 2 1984
# 단위조정 (억원)
apt$SalePrice <- apt$SalePrice/10000
apt %>%
ggplot(aes(SalePrice)) +
geom_histogram(binwidth = 1) +
labs(caption = "(단위: 억원)")
아파트 매매가의 분포를 히스토그램을 통해 살펴 보면 분포가 심하게 왼쪽으로 치우쳐 져 있는 것을 확인할 수 있다. 분석 성능을 떨어 뜨릴 수 있는 일부 초고가 프리미엄형 아파트에 대한 처리가 필요해 보인다.
apt %>%
filter(SalePrice < quantile(SalePrice, probs = 0.995)) %>%
ggplot(aes(SalePrice)) +
geom_histogram(binwidth = 1)
매매가 상위 0.5%의 데이터를 이상치로 인식하고 처리한 결과 왜도가 현저하게 개선되었으며 더 합리적인 분포로 보인다. 이에 따라 이상치로 인식한 상위 0.5 % 의 아파트 매매거래를 제거하였다.
# 매매가 상위 0.5% 데이터 제거
apt <- apt %>%
filter(SalePrice < quantile(SalePrice, probs = 0.995))
전용면적 분포
apt %>%
count(cut_width(LvArea, 2)) %>%
arrange(desc(n)) %>%
head(10)
## # A tibble: 10 x 2
## `cut_width(LvArea, 2)` n
## <fct> <int>
## 1 (83,85] 282702
## 2 (59,61] 169622
## 3 (113,115] 35172
## 4 (49,51] 28142
## 5 (57,59] 17931
## 6 (39,41] 15196
## 7 (79,81] 13573
## 8 (81,83] 11923
## 9 (41,43] 11609
## 10 (43,45] 11582
apt %>%
ggplot(aes(LvArea)) +
geom_histogram(binwidth = 2)
\(84m^{2}\)형과 \(60m^{2}\)이 가장 매매가 많이 이루어 지고 있음을 알 수 있었다.
전용면적과 매매가
apt %>%
ggplot(aes(cut_width(LvArea, 20), SalePrice)) +
geom_boxplot() +
coord_flip() +
labs(x = "전용면적(m2)",
title = "전용면적크기와 매매가간 관계") +
theme(plot.title = element_text(size = 16))
전용면적과 아파트 매매가는 뚜렷한 양의 상관관계를 보이고 있다. 또한 전용면적 이외의 다른 변수의 영향 또한 크게 작용하고 있는 것으로 기대된다.
# 구별 중위 매매가
apt %>%
ggplot(aes(fct_reorder(Gu, SalePrice, .fun = median), SalePrice, fill = Gu)) +
stat_summary(geom = "bar", fun = median) +
coord_flip() +
theme(legend.position = "none") +
labs(x = "구",
y = "매매가",
title = "아파트 매매가 중위값")
# 구별 평균 매매가
apt %>%
ggplot(aes(fct_reorder(Gu, SalePrice, .fun = mean), SalePrice, fill = Gu)) +
stat_summary(geom = "bar", fun = mean) +
coord_flip() +
theme(legend.position = "none") +
labs(x = "구",
y = "매매가",
title = "아파트 매매가 평균값")
# 매매가의 중위값과 평균값의 Boxplot
apt %>%
ggplot(aes(fct_reorder(Gu, SalePrice, .fun = mean), SalePrice, group = Gu)) +
geom_boxplot() +
stat_summary(geom = "point", fun = mean, color = "red") +
xlab("구") +
coord_flip()
구 별로 매매가격이 뚜렷한 차이를 보여 행정구역은 매매가를 예측하는 데 중요한 변수일 것으로 생각된다. 매매 중위가와 평균가를 모두 놓고 비교 했을 때, 강남구, 서초구, 용산구, 송파구를 하나의 그룹으로, 광진구, 중구, 마포구, 양천구, 동작구, 성동구, 종로구, 강동구, 영등포구를 다른 하나의 그룹으로, 그리고 그 이외의 구로 클러스터링하여 범주를 단순화 하여 변수를 생성하였다.
apt$Gu_cl <- case_when(
apt$Gu %in% c("강남구", "서초구", "용산구", "송파구") ~ 2,
apt$Gu %in% c("광진구", "성동구", "마포구", "중구", "동작구", "양천구", "영등포구", "종로구", "강동구") ~ 1,
TRUE ~ 0)
지정한 각 클러스터 내에 포함 된 동 별 평균 매매가를 시각화 하였다.
apt$Dong <- factor(apt$Dong) # Dong 변수 factor화
apt %>%
filter(YrSold == 2020) %>%
group_by(Gu_cl, Gu, Dong) %>%
summarise(SalePrice_평균 = mean(SalePrice)) %>%
ungroup %>%
arrange(desc(SalePrice_평균)) %>%
head(50) %>%
ggplot(aes(fct_reorder(Dong, SalePrice_평균, .fun = mean), SalePrice_평균)) +
geom_col(aes(fill = factor(Gu_cl))) +
coord_flip() +
labs(fill = "구 클러스터",
x = "동",
title = "2020년 구 클러스터별 아파트 평균매매가 상위 50개 동") +
theme(plot.title = element_text(size = 16))
평균 매매가 상위 50개의 동이 대부분 클러스터 1과 2에 속한 동 임을 확인할 수 있다.
층수 분포
apt %>%
ggplot(aes(Floor)) +
geom_histogram(binwidth = 1)
apt %>%
ggplot(aes(Floor)) +
geom_boxplot()
마이너스 층수가 눈에 띈다. 실제로 지하 층수의 매매건이 발생한 것인지 알아보았다. 만약 아파트가 지하에 위치해 있다면 지상층에 위치한 아파트에 비해 매매가가 현저하게 차이날 것으로 예상되므로 지상층과의 매매가 비교를 통해 정말로 지하에 위치해 있는 지, 오타인지 판단해 본다.
지상층에 비교해 가격차이가 거의 나지 않는 것으로 보아 단순히 기입 과정에서 오류일 것으로 추정되어 마이너스 층수를 모두 플러스로 변환 하였다.
# 층별 평균 매매가
apt %>%
filter(Floor <= 4) %>%
group_by(Floor) %>%
summarise(평균매매가 = mean(SalePrice))
## # A tibble: 8 x 2
## Floor 평균매매가
## <int> <dbl>
## 1 -4 6.15
## 2 -3 8.41
## 3 -2 7.81
## 4 -1 4.32
## 5 1 5.16
## 6 2 5.25
## 7 3 5.38
## 8 4 5.35
apt$Floor[apt$Floor <= 0] <- abs(apt$Floor[apt$Floor <= 0])
층수와 매매가
층수와 매매가의 상관관계를 시각화 하였다.
apt %>%
filter(Floor <= quantile(Floor, probs = 0.75) + 1.5*IQR(Floor) & YrSold >= 2020) %>%
ggplot(aes(Floor, SalePrice)) +
geom_bar(stat = "summary", fun = mean) +
labs(x = "층수",
y = "층별 평균 매매가",
title = "2020년 층수 별 아파트 평균 매매가") +
theme(plot.title = element_text(size = 16))
고층일 수록 평균 매매가격은 높아짐을 확인할 수 있다.
건축연도분포
apt %>%
ggplot(aes(BuiltYr)) +
geom_histogram(binwidth = 1)
건축연도와 매매가
apt %>%
ggplot(aes(x = BuiltYr, y= SalePrice)) +
stat_summary(geom = "bar", fun = mean) +
scale_x_continuous(breaks = scales::breaks_width(10),
minor_breaks = scales::breaks_width(5)) +
guides(x = guide_axis(angle = 20)) +
labs(y = "평균 매매가")
최근에 지어진 아파트 일수록 매매가가 증가하는 경향을 보이나 1990년 이전에 지어진 오래된 아파트의 경우 매매가가 아주 높게 형성되어 있다. 오래된 아파트는 재건축 대상으로 신형 아파트에 비해 매매가가 높게 형성되는 경향이 반영되어 있는 것 같다.
apt %>%
mutate(is_old = ifelse(BuiltYr < 1989, 1, 0)) %>%
group_by(is_old, Gu) %>%
summarise(평균매매가 = mean(SalePrice)) %>%
ungroup %>%
arrange(Gu) %>%
print(n = 10)
## # A tibble: 50 x 3
## is_old Gu 평균매매가
## <dbl> <fct> <dbl>
## 1 0 용산구 8.89
## 2 1 용산구 9.50
## 3 0 종로구 5.84
## 4 1 종로구 3.32
## 5 0 중구 6.39
## 6 1 중구 2.89
## 7 0 도봉구 3.34
## 8 1 도봉구 2.71
## 9 0 강북구 3.81
## 10 1 강북구 1.18
## # ... with 40 more rows
오래된 아파트인지 여부는 고려해 볼 수 있는 변수로 판단하여 이 부분에 대한 변수를 새롭게 추가해 주었다.
apt$is_old <- ifelse(apt$BuiltYr < 1989, 1, 0)
apt %>%
count(apt_name) %>%
arrange(desc(n)) %>%
print(n = 30)
## # A tibble: 7,421 x 2
## apt_name n
## <chr> <int>
## 1 현대 8856
## 2 두산 6566
## 3 한신 6068
## 4 삼성래미안 5966
## 5 신동아 5873
## 6 주공2 5539
## 7 벽산 5398
## 8 삼성 4149
## 9 우성 3940
## 10 대우 3864
## 11 파크리오 3824
## 12 극동 3354
## 13 에스케이북한산시티 3018
## 14 대림e-편한세상 2927
## 15 쌍용 2829
## 16 미성 2712
## 17 잠실엘스 2707
## 18 동아 2700
## 19 리센츠 2683
## 20 주공5 2673
## 21 현대1 2644
## 22 현대3 2546
## 23 중계그린1단지 2504
## 24 성원 2500
## 25 대림 2469
## 26 중앙하이츠 2468
## 27 경남 2393
## 28 롯데캐슬 2322
## 29 경남아너스빌 2270
## 30 개포주공 1단지 2251
## # ... with 7,391 more rows
아파트 단지명은 무질서 하게 보이는 듯 하나 지명, 아파트 브랜드, 혹은 건설사의 이름을 포함하고 있음을 확인할 수 있었다. 지명은 앞서 살펴 보았던 행정구역을 통해 살펴 보았으니 아파트 단지명을 통해 아파트의 브랜드, 건설사 정보를 추출하고자 한다.
# 아파트 브랜드명 추출
apt <- apt %>%
mutate(apt_name = str_replace(apt_name, ".*(미안).*", "래미안"),
apt_name = str_replace(apt_name, ".*(힐스테이트).*", "힐스테이트"),
apt_name = str_replace(apt_name, ".*(자이).*", "자이"),
apt_name = str_replace(apt_name, ".*(푸르지오).*", "푸르지오"),
apt_name = str_replace(apt_name, ".*(아이파크).*", "아이파크"),
apt_name = str_replace(apt_name, ".*(PARK).*", "아이파크"),
apt_name = str_replace(apt_name, ".*(롯데캐슬).*", "롯데캐슬"),
apt_name = str_replace(apt_name, ".*(편한세상).*", "e편한세상"),
apt_name = str_replace(apt_name, ".*(더샵).*", "더샵"),
apt_name = str_replace(apt_name, ".*(주공).*", "주공"),
apt_name = str_replace(apt_name, ".*(엘에이치).*", "주공"),
apt_name = str_replace(apt_name, ".*(휴먼시아).*", "주공"))
# 이외의 아파트는 '일반'으로 이름 바꾸기.
apt <- apt %>%
mutate(apt_name = ifelse(!apt_name %in% c("래미안", "힐스테이트", "자이", "푸르지오", "아이파크", "롯데캐슬", "e편한세상", "더샵", "주공"), "일반", apt_name))
apt %>%
filter(YrSold == 2020) %>%
ggplot(aes(fct_reorder(apt_name, SalePrice, mean), SalePrice)) +
geom_bar(stat = "summary", fun = mean) +
labs(x = "브랜드",
y = "매매가",
title = "아파트 브랜드 별 평균 매매가") +
coord_flip() +
theme(plot.title = element_text(size = 16))
아파트 단지명에 브랜드의 이름이 들어간 경우 확실히 그렇지 않은 경우에 비해 가격이 높은 경향을 발견할 수 있었다. 주공 아파트는 다른 아파트에 비해 비교적 낮은 가격을 형성하고 있다.
apt %>%
ggplot(aes(factor(MoSold), SalePrice)) +
geom_bar(stat = "summary", fun = median) +
labs(x = "MoSold",
title = "월별 평균 매매가") +
theme(plot.title = element_text(size = 16)) +
coord_cartesian(ylim = c(2,NA))
평균매매가는 봄과 가을에 낮아지고 여름과 겨울에 높은 경향이 있는 것으로 보여진다.
# 수치형 변수 추출
numVars <- profiling_num(apt)$variable
# 상관관계 매트릭스
Cor_data <- round(cor(apt[, numVars]), 2)
# SalePrice 변수와 상관관계가 높은 순으로 변수 정렬
Corhigh <- names(sort(abs(Cor_data[,"SalePrice"]), decreasing = T))
Cor_data <- Cor_data[Corhigh, Corhigh]
Cor_data[lower.tri(Cor_data)] <- NA
melted_cor <- reshape2::melt(Cor_data, na.rm = TRUE)
ggplot(data = melted_cor, aes(Var2, Var1, fill = value)) +
geom_tile(color = "white") +
scale_fill_gradient2(low = "skyblue",
high = "red",
mid = "white",
midpoint = 0,
limit = c(-1, 1),
name = "Pearson\nCorrelation") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, vjust = 1,
size = 12, hjust = 1)) +
coord_fixed() +
geom_text(aes(Var2, Var1, label = value), color = "black", size = 4) +
theme(panel.grid.major = element_blank()) +
labs(x = NULL,
y = NULL)
수치형 변수를 추출하여 변수간 상관관계를 알아 보았다. 매매가와 가장 관계가 높아 보이는 수치형 변수는 전용면적과 행정구역인 것으로 보인다.
Extracting skewed predictor variables
수치형 변수들의 왜도를 확인한다.
# 수치형 변수의 왜도
profiling_num(apt) %>%
select(variable, skewness) %>%
arrange(desc(abs(skewness)))
## variable skewness
## 1 SalePrice 1.998097244
## 2 is_old 1.743739872
## 3 Floor 1.099813895
## 4 LvArea 1.053445755
## 5 Gu_cl 0.618797937
## 6 BuiltYr -0.389057744
## 7 YrSold -0.225820494
## 8 MoSold -0.022485640
## 9 X 0.005231059
Preprocessing predictor variables
불필요한 변수를 제거하고 범주형 변수를 factor로 변환하였다. 또한, 수치형 변수들에 대한 표준화 작업을 한다. 또한 왜도가 높은 반응변수에 대해 log변환 하였다.
apt <- apt %>%
select(-X,) %>% # 불필요한 변수 제거
mutate(apt_name = factor(apt_name), # 단지명 factor화
MoSold = factor(MoSold)) # 매매 월 factor화
numVars <- profiling_num(apt)$variable
numVars <- numVars[which(!numVars %in% c("SalePrice", "is_old"))] # 반응변수및 이항변수를 제외한 수치형 변수
aptE <- apt %>%
recipe(SalePrice ~.) %>%
step_center(numVars) %>% # 수치형 변수들에 대한 표준화
step_scale(numVars) %>%
prep(training = apt) %>%
juice()
aptE$SalePrice <- log(aptE$SalePrice)
모델의 성과 평가를 위해 데이터세트를 훈련용 데이터와 테스트용 데이터로 7:3 분할 하였다.
set.seed(210508)
train_id <- sample(nrow(apt), 0.7*nrow(apt))
train <- aptE[train_id, ]
test <- aptE[-train_id, ]
Fitting random forest model on the train set
간단한 랜덤포레스트 알고리즘을 이용하여 훈련용 데이터셋을 통해 모델을 적합시킨다.
n_features <- length(train) -1
start_t <- Sys.time()
rf <- ranger(
SalePrice ~.,
data = train,
num.trees = 10*n_features,
mtry = floor(n_features/3),
respect.unordered.factors = "order",
importance = "impurity",
seed = 210508
)
## Growing trees.. Progress: 37%. Estimated remaining time: 59 seconds.
## Growing trees.. Progress: 74%. Estimated remaining time: 23 seconds.
end_t <- Sys.time()
end_t - start_t
## Time difference of 1.817529 mins
훈련용 데이터 세트
pred_train <- exp(predict(rf, train)$predictions)
# 매매가 표준편차
sd(apt[train_id, ]$SalePrice)
## [1] 3.749746
accuracy(pred_train, apt[train_id, ]$SalePrice)
## ME RMSE MAE MPE MAPE
## Test set 0.04635296 0.4384507 0.2506137 -0.2522519 4.396759
테스트용 데이터 세트
pred_test <- exp(predict(rf, test)$predictions)
# 매매가 표준편차
sd(apt[-train_id, ]$SalePrice)
## [1] 3.737965
accuracy(pred_test, apt[-train_id, ]$SalePrice)
## ME RMSE MAE MPE MAPE
## Test set 0.05886427 0.6466568 0.3586191 -0.5645573 6.36669
vip(rf, num_features = 20) +
labs(x = "예측변수",
title = "서울 아파트 매매가 예측을 위한 각 변수의 중요도",
y = "중요도") +
theme(aspect.ratio = 12/16)
횡단면 분석에 있어서, 아파트 매매가를 결정하는 데 가장 중요한 요소는 전용면적 이다. 평수가 커질 수록 매매가에 미치는 영향이 가장 컸으며, 이외에 구, 동 등의 행정구역, 건축년도, 아파트의 브랜드 또한 중요도가 높은 변수 임을 알 수 있었다.