1. 수집할 데이터
공공 데이터포털 (data.go.kr) 소개
공공데이터포털은 공공기관이 생성 또는 취득하여 관리하고 있는 공공데이터를 한 곳에서 제공하는 통합 창구입니다. 포털에서는 국민이 쉽고편리하게 공공데이터를 이용할 수 있도록 파일데이터, 오픈API, 시각화 등 다양한 방식으로 제공하고 있으며, 누구라도 쉽고 편리한 검색을통해 원하는 공공데이터를 빠르고 정확하게 찾을 수 있습니다.
아파트매매 실거래 상세 자료
- 주소 : https://www.data.go.kr/data/15057511/openapi.do
- 설명 : 부동산 거래신고에 관한 법률에 따라 신고된 주택의 실거래 자료를 제공
- 방법 : 지역코드와 기간을 이용하여 해당기간, 해당지역의 아파트 매매 신고자료를 제공하는 아파트 매매 신고 정보 조회
위 주소에 들어가서 로그인 후 아파트 실거래 상세자료 활용신청을 진행한다.
약 10분 후 부터 api 호출할 수 있다.
- 요청변수(Request Parameter)
항목명(국문) | 항목명(영문) | 항목크기 | 항목구분 | 샘플데이터 | 항목설명 |
---|---|---|---|---|---|
서비스키 | ServiceKey | 20 | 필수 | - | 공공데이터포털에서 받은 인증키 |
페이지 번호 | pageNo | 4 | 옵션 | 1 | 페이지번호 |
한 페이지 결과 수 | numOfRows | 4 | 옵션 | 10 | 한 페이지 결과 수 |
지역코드 | LAWD_CD | 5 | 필수 | 11110 | 지역코드 |
계약월 | DEAL_YMD | 6 | 필수 | 201512 | 계약월 |
- 출력결과(Response Element)
항목명(국문) | 항목명(영문) | 항목크기 | 항목구분 | 샘플데이터 | 항목설명 |
---|---|---|---|---|---|
결과코드 | resultCode | 2 | 필수 | 00 | 결과코드 |
결과메시지 | resultMsg | 50 | 필수 | OK | 결과메시지 |
한 페이지 결과 수 | numOfRows | 4 | 필수 | 10 | 한 페이지 결과 수 |
페이지 번호 | pageNo | 4 | 필수 | 1 | 페이지번호 |
전체 결과 수 | totalCount | 4 | 필수 | 3 | 전체 결과 수 |
거래금액 | 거래금액 | 40 | 필수 | 82,500 | 거래금액 |
건축년도 | 건축년도 | 4 | 필수 | 2008 | 건축년도 |
년 | 년 | 4 | 필수 | 2015 | 년 |
도로명 | 도로명 | 40 | 필수 | 사직로8길 | 도로명 |
도로명건물본번호코드 | 도로명건물본번호코드 | 5 | 필수 | 00004 | 도로명건물본번호코드 |
도로명건물부번호코드 | 도로명건물부번호코드 | 5 | 필수 | 00000 | 도로명건물부번호코드 |
도로명시군구코드 | 도로명시군구코드 | 5 | 필수 | 11110 | 도로명시군구코드 |
도로명일련번호코드 | 도로명일련번호코드 | 2 | 필수 | 03 | 도로명일련번호코드 |
도로명지상지하코드 | 도로명지상지하코드 | 1 | 필수 | 0 | 도로명지상지하코드 |
도로명코드 | 도로명코드 | 7 | 필수 | 4100135 | 도로명코드 |
법정동 | 법정동 | 40 | 필수 | 사직동 | 법정동 |
법정동본번코드 | 법정동본번코드 | 4 | 필수 | 0009 | 법정동본번코드 |
법정동부번코드 | 법정동부번코드 | 4 | 필수 | 0000 | 법정동부번코드 |
법정동시군구코드 | 법정동시군구코드 | 5 | 필수 | 11110 | 법정동시군구코드 |
법정동읍면동코드 | 법정동읍면동코드 | 5 | 필수 | 11500 | 법정동읍면동코드 |
법정동지번코드 | 법정동지번코드 | 1 | 필수 | 1 | 법정동지번코드 |
아파트 | 아파트 | 40 | 필수 | 광화문풍림스페이스본(9-0) | 아파트 |
월 | 월 | 2 | 필수 | 12 | 월 |
일 | 일 | 6 | 필수 | 1~10 | 일 |
일련번호 | 일련번호 | 14 | 필수 | 11110-2203 | 일련번호 |
전용면적 | 전용면적 | 20 | 필수 | 94.51 | 전용면적 |
지번 | 지번 | 10 | 필수 | 9 | 지번 |
지역코드 | 지역코드 | 5 | 필수 | 11110 | 지역코드 |
층 | 층 | 4 | 필수 | 11 | 층 |
2. 데이터 수집
요청 방법 : Rest API
매매 내역을 API 방식으로 제공하고 있다. google 홈페이지에 접속하고 싶을 때 브라우저에 https://google.com
를 치면 브라우저에 화면이 떠지는 것처럼 받고싶은 데이터를 지정된 양식에 맞춰서 요청하면 데이터가 받아지는 방식이다.
http://openapi.molit.go.kr/OpenAPI_ToolInstallPackage/service/rest/RTMSOBJSvc/getRTMSDataSvcAptTradeDev?serviceKey={개인서비스키}&LAWD_CD={시군구코드}&DEAL_YMD={년월}&pageNo=1&numOfRows=1000
위 URL에서 3가지 조건을 바꿔가며 검색할 수 있다.
- {개인서비스키} : 공공데이터포털에 활용신청하면 받을 수 있음.
- {시군구코드} :
https://www.code.go.kr/stdcode/regCodeL.do
에서 확인할 수 있다. 시코드 2자리와 구코드 3자리를 합쳐서 5자리 코드로 입력하면 된다.- 서울특별시 종로구 청운동 : 1111010100
- 서울특별시 : 11
- 종로구 : 110
- 청운동 : 10100
- {년월} : 매매 월 - 201111 (YYYYMM)
이렇게 완성된 url을 브라우저 url 입력하는 곳에 넣어서 확인해볼 수 있다.
python으로 10, 11월만 빠르게 수집
1
2
3
4
5
6
import time
import requests
import xml.etree.ElementTree as ET
import pandas as pd
from tqdm import tqdm
1
2
3
4
5
6
7
8
9
10
11
12
def get_items(get):
root = ET.fromstring(get.content)
item_list = []
for child in root.find('body').find('items'):
elements = child.findall('*')
data = {}
for element in elements:
tag = element.tag.strip()
text = element.text.strip()
data[tag] = text
item_list.append(data)
return item_list
1
2
3
4
5
6
7
8
9
serviceKey = "################################공유하면안됨#################################"
url = "http://openapi.molit.go.kr/OpenAPI_ToolInstallPackage/service/rest/RTMSOBJSvc/getRTMSDataSvcAptTradeDev?serviceKey={}&LAWD_CD={}&DEAL_YMD={}&pageNo=1&numOfRows=1000"
locCode = ["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 = ["종로구", "중구", "용산구", "성동구", "광진구", "동대문구", "중랑구", "성북구", "강북구", "도봉구", "노원구", "은평구", "서대문구", "마포구", "양천구", "강서구", "구로구", "금천구", "영등포구", "동작구", "관악구", "서초구", "강남구", "송파구", "강동구"]
yyyymm = ['202110', '202111']
1
2
3
4
5
6
7
8
9
10
11
12
13
DataFrame = pd.DataFrame([])
for ym in tqdm(yyyymm):
for code, name in zip(locCode, locCode_nm):
time.sleep(0.1)
req = url.format(serviceKey, code, ym)
get = requests.get(req)
items_list = get_items(get)
items = pd.DataFrame(items_list)
items["지역구"] = name
DataFrame = pd.concat([DataFrame, items])
1
DataFrame.shape
(2036, 29)
1
DataFrame.head()
거래금액 | 거래유형 | 건축년도 | 년 | 도로명 | 도로명건물본번호코드 | 도로명건물부번호코드 | 도로명시군구코드 | 도로명일련번호코드 | 도로명지상지하코드 | 도로명코드 | 법정동 | 법정동본번코드 | 법정동부번코드 | 법정동시군구코드 | 법정동읍면동코드 | 법정동지번코드 | 아파트 | 월 | 일 | 일련번호 | 전용면적 | 중개사소재지 | 지번 | 지역코드 | 층 | 해제사유발생일 | 해제여부 | 지역구 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 140,000 | 2007 | 2021 | 사직로 | 00102 | 00000 | 11110 | 02 | 0 | 3100005 | 필운동 | 0254 | 0000 | 11110 | 11300 | 1 | 신동아블루아광화문의꿈 | 10 | 25 | 11110-2164 | 111.97 | 254 | 11110 | 8 | 종로구 | ||||
1 | 146,000 | 2008 | 2021 | 사직로8길 | 00004 | 00000 | 11110 | 03 | 0 | 4100135 | 사직동 | 0009 | 0000 | 11110 | 11500 | 1 | 광화문풍림스페이스본(101동~105동) | 10 | 12 | 11110-2203 | 108.07 | 9 | 11110 | 1 | 종로구 | ||||
2 | 180,000 | 2003 | 2021 | 사직로8길 | 00020 | 00000 | 11110 | 05 | 0 | 4100135 | 내수동 | 0095 | 0000 | 11110 | 11800 | 1 | 경희궁파크팰리스 | 10 | 20 | 11110-107 | 147.62 | 95 | 11110 | 2 | 종로구 | ||||
3 | 36,800 | 2005 | 2021 | 율곡로2길 | 00007 | 00000 | 11110 | 01 | 0 | 4100234 | 수송동 | 0085 | 0000 | 11110 | 12400 | 1 | 로얄팰리스스위트 | 10 | 1 | 11110-205 | 39.67 | 85 | 11110 | 13 | 종로구 | ||||
4 | 33,000 | 2003 | 2021 | 돈화문로11가길 | 00059 | 00000 | 11110 | 03 | 0 | 4100050 | 익선동 | 0055 | 0000 | 11110 | 13300 | 1 | 현대뜨레비앙 | 10 | 7 | 11110-102 | 46.64 | 55 | 11110 | 3 | 종로구 |
3. 데이터 정제 작업
도로명 주소 없는 아파트는 거래 대상이 아니다. 보통 신축인 경우가 그렇다.
1
2
3
4
5
DataFrame = DataFrame.loc[(
~(DataFrame.도로명.isna()) &
~(DataFrame.도로명코드.isna()) &
~(DataFrame.도로명건물본번호코드.isna())
)]
데이터 타입 변경
1
2
3
4
5
DataFrame["년"] = DataFrame["년"].astype(int)
DataFrame["월"] = DataFrame["월"].astype(int)
DataFrame["일"] = DataFrame["일"].astype(int)
DataFrame["전용면적"] = DataFrame["전용면적"].astype(float)
DataFrame["아파트가격"] = DataFrame["거래금액"].apply(lambda x: int(x.replace(",", "")))
계약 취소 거래 제거 : 가격 조작
1
DataFrame.해제여부 = DataFrame.해제여부.apply(lambda x: "정상거래" if len(x) == 0 else '취소거래')
지역 코드 규격화
1
2
DataFrame["지역구_법정동"] = DataFrame[["지역코드", "법정동읍면동코드"]].apply(lambda x: "{}{}".format(x[0], x[1]), 1)
DataFrame["지역구_도로명"] = DataFrame[["지역코드", "도로명코드"]].apply(lambda x: "{}{}".format(x[0], x[1]), 1)
법정동 도로명 주소
1
2
3
DataFrame["도로명건물번호"] = DataFrame[["도로명건물본번호코드", "도로명건물부번호코드"]].apply(lambda x: "{}".format(int(x[0]))if x[1] == "00000" else "{}-{}".format(int(x[0]), int(x[1])), 1)
DataFrame["도로명주소"] = DataFrame[["지역구", "도로명", "도로명건물번호"]].apply(lambda x: "{} {} {}".format(x[0], x[1], x[2]), 1)
DataFrame["법정동주소"] = DataFrame[["지역구", "법정동", "지번"]].apply(lambda x: "{} {} {}".format(x[0], x[1], x[2]), 1)
살고(사고) 싶은 지역을 검색
1
2
3
4
DataFrame.loc[(
(DataFrame.법정동 == "상도동") &
(DataFrame.지역구 == "동작구")
), ['지역구_도로명', '도로명주소', '아파트', '전용면적', '아파트가격', '년', '월', '일', '해제여부']]
지역구_도로명 | 도로명주소 | 아파트 | 전용면적 | 아파트가격 | 년 | 월 | 일 | 해제여부 |
---|---|---|---|---|---|---|---|---|
115903005080 | 동작구 양녕로 170 | 롯데캐슬비엔 | 68.56 | 88500 | 2021 | 10 | 2 | 정상거래 |
115903119005 | 동작구 상도로 346-2 | 힐스테이트 상도 프레스티지 | 120.004 | 195200 | 2021 | 10 | 4 | 정상거래 |
115903119008 | 동작구 장승배기로 26 | 아이파크 | 84.96 | 128500 | 2021 | 10 | 6 | 정상거래 |
115903119002 | 동작구 만양로 26 | 건영 | 114.96 | 135000 | 2021 | 10 | 11 | 정상거래 |
115903119002 | 동작구 만양로 6 | e편한세상상도노빌리티 | 59.7827 | 154500 | 2021 | 10 | 12 | 정상거래 |
115904157013 | 동작구 국사봉1길 214 | 성우 | 72 | 35700 | 2021 | 10 | 15 | 정상거래 |
115904157571 | 동작구 장승배기로10길 100 | 상도더샵2차 | 84.99 | 147000 | 2021 | 10 | 15 | 정상거래 |
115903119005 | 동작구 상도로 320 | 중앙하이츠빌 | 123.603 | 134000 | 2021 | 10 | 16 | 정상거래 |
115903119005 | 동작구 상도로 407 | 상도동삼호아파트 | 84.52 | 93500 | 2021 | 10 | 18 | 정상거래 |
115903119002 | 동작구 만양로 26 | 건영 | 114.96 | 130000 | 2021 | 10 | 20 | 정상거래 |
115904157378 | 동작구 상도로55길 25 | 삼환나우빌(520-0) | 84.87 | 123500 | 2021 | 10 | 20 | 정상거래 |
115904157508 | 동작구 양녕로23길 9 | 경향렉스빌 | 114.653 | 105000 | 2021 | 10 | 23 | 정상거래 |
115903119002 | 동작구 만양로 6 | e편한세상상도노빌리티 | 84.9704 | 173000 | 2021 | 10 | 23 | 정상거래 |
115903005080 | 동작구 양녕로 170 | 롯데캐슬비엔 | 84.95 | 122000 | 2021 | 10 | 25 | 정상거래 |
115903119005 | 동작구 상도로 107 | 쌍용스윗닷홈 | 59.578 | 102000 | 2021 | 10 | 28 | 정상거래 |
115904157318 | 동작구 상도로15길 131 | 상도휴엔하임 | 14.7 | 13900 | 2021 | 11 | 5 | 정상거래 |
115904157386 | 동작구 상도로62길 81-1 | 씨티아모리움 | 14.1881 | 15600 | 2021 | 11 | 6 | 정상거래 |
문제점
현행법상 부동산 거래 후 30일 이내에 신고를 해야한다.
즉 10월 30일 거래여도 11월 20일에 신고를 하면 10월 마감으로 돌린 데이터를 다시 업데이트 해야한다.
또한 허위 거래같은 경우 해제여부
가 정상->취소로 바뀌므로 과거 건을 가끔식 업데이트 해야한다.
4. full code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
yyyymm = ['202101', '202102', '202103', '202104', '202105', '202106', '202107', '202108', '202109', '202110', '202111', '202112']
for ym in tqdm(yyyymm):
DataFrame = pd.DataFrame([])
for code, name in zip(locCode, locCode_nm):
time.sleep(0.1)
req = url.format(serviceKey, code, ym)
get = requests.get(req)
items_list = get_items(get)
items = pd.DataFrame(items_list)
items["지역구"] = name
DataFrame = pd.concat([DataFrame, items])
################################################################################
DataFrame.to_csv("../data/seoul_apt_trade_{}_raw.csv".format(ym), index=None)
DataFrame = DataFrame.loc[(
~(DataFrame.도로명.isna()) &
~(DataFrame.도로명코드.isna()) &
~(DataFrame.도로명건물본번호코드.isna())
)]
DataFrame["년"] = DataFrame["년"].astype(int)
DataFrame["월"] = DataFrame["월"].astype(int)
DataFrame["일"] = DataFrame["일"].astype(int)
DataFrame["전용면적"] = DataFrame["전용면적"].astype(float)
DataFrame["아파트가격"] = DataFrame["거래금액"].apply(lambda x: int(x.replace(",", "")))
DataFrame.해제여부 = DataFrame.해제여부.apply(lambda x: "취소거래" if len(x) == 0 else '정상거래')
DataFrame["지역구_법정동"] = DataFrame[["지역코드", "법정동읍면동코드"]].apply(lambda x: "{}{}".format(x[0], x[1]), 1)
DataFrame["지역구_도로명"] = DataFrame[["지역코드", "도로명코드"]].apply(lambda x: "{}{}".format(x[0], x[1]), 1)
DataFrame["도로명건물번호"] = DataFrame[["도로명건물본번호코드", "도로명건물부번호코드"]].apply(lambda x: "{}".format(int(x[0]))if x[1] == "00000" else "{}-{}".format(int(x[0]), int(x[1])), 1)
DataFrame["도로명주소"] = DataFrame[["지역구", "도로명", "도로명건물번호"]].apply(lambda x: "{} {} {}".format(x[0], x[1], x[2]), 1)
DataFrame["법정동주소"] = DataFrame[["지역구", "법정동", "지번"]].apply(lambda x: "{} {} {}".format(x[0], x[1], x[2]), 1)
################################################################################
DataFrame.to_csv("../data/seoul_apt_trade_{}.csv".format(ym), index=None)
2021년 서울시 지역구 아파트 매매가 분포
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import plotly.graph_objects as go
from plotly.colors import n_colors
trace = DataFrame.groupby("지역구").agg({"아파트가격" : "mean"}).sort_values("아파트가격", ascending=True).index.to_list()
colors = n_colors('rgb(10, 10, 200)', 'rgb(200, 10, 10)', len(trace), colortype='rgb')
fig = go.Figure()
for sigungu, color in zip(trace, colors):
fig.add_trace(go.Violin(x = DataFrame.loc[((DataFrame.지역구 == sigungu)), "아파트가격"], line_color = color, name = sigungu))
fig.update_traces(orientation='h', side='positive', width=3, points=False)
fig.update_layout(xaxis_showgrid=True, xaxis_zeroline=True, legend_traceorder="reversed", height=700, width=800)
fig.show()