Project

내 집을 찾아서: 서울 집값 비교하기

주댕이 2024. 2. 13. 11:48

# 프로젝트 개요

  • 멀티캠퍼스 멀티잇 데이터 분석 & 엔지니어 34회차 미니 프로젝트
  • 주제: 내 집을 찾아서-서울 집값 비교하기
  • 진행 일시: 2024.02.02 ~ 2024.02.08

 

# 프로젝트의 목적

  • 본 프로젝트는 서울의 부동산 시장에서 자신이 원하는 집을 찾는 것을 지원하는 것을 목표로 한다.
  • 사용자가 원하는 조건을 입력하면 자치구, 법정동, 또는 건물에 따른 부동산 시세를 그래프 및 도표 형태로 제공한다.
  • 이를 통해 사용자는 쉽게 전·월세 실거래 정보를 확인하고, 위치별 시세를 비교하여 집을 구하는 시간을 단축할 수 있다.

 

# 프로젝트에서 사용한 주요 개발환경

  • Programming Languages : Python(ver. 3.12.1)
  • Web Framework : Streamlit (ver. 1.31.0)

## 사용된 라이브러리

  • python-dotenv
  • email
  • json
  • math
  • os
  • pandas
  • plotly
  • requests
  • smtplib
  • streamlit
  • streamlit_option_menu
from dotenv import load_dotenv
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import json
import math
import os
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import requests
import smtplib
import streamlit as st
from streamlit_option_menu import option_menu

 

 

# 데이터 설명

  • 서울 열린데이터 광장 > 서울시 부동산 전월세가 정보
    • 서울 부동산 정보광장에서 제공하는 전월세가 정보
    • 자치구, 법정동, 건물명, 임대건물명, 전월세구분, 보증금, 임대료, 계약년도 등의 전월세 정보를 제공한다.
    • http://data.seoul.go.kr/dataList/OA-21276/S/1/datasetView.do
    • 본 프로젝트에서는 2023년까지의 데이터를 API 크롤링한 후, csv 파일로 저장하여 사용하였다.

 

# 변수 설명

  • SGG_NM: 자치구명
  • BJDONG_NM: 법정동명
  • CNTRCT_DE: 계약일
  • RENT_GBN: 전월세 구분
  • RENT_AREA: 임대면적
  • RENT_GTN: 보증금(만원)
  • RENT_FEE: 임대료(만원)
  • BLDG_NM: 건물명
  • HOUSE_GBN_NM: 건물용도
  • 평수: 임대면적 * 0.3025

 

# Project Flow

## 데이터 크롤링하기

  • 현재 시점(24.02.02)에서 가장 최근 데이터부터 2023년 1월 데이터까지 API 크롤링을 진행하였다.
  • 크롤링 진행 후 csv 파일로 저장하였다.
# 환경 변수 로딩
load_dotenv()
SEOUL_PUBLIC_API = os.getenv("SEOUL_PUBLIC_API")

# 데이터 csv 파일 생성
# API 크롤링
def fetch_data(start, end):
    URL = f'http://openapi.seoul.go.kr:8088/{SEOUL_PUBLIC_API}/json/tbLnOpendataRentV/{start}/{end}/'
    req = requests.get(URL)
    content = req.json()
    # 데이터 추출 및 DataFrame으로 변환
    return pd.DataFrame(content['tbLnOpendataRentV']['row'])

# 1~130,000 데이터 수집
data = None
start = 1
end = 130000
for i in range(start, end+1, 1000):
    result = fetch_data(i, i+999)
    data = pd.concat([data, result], ignore_index=True)
    
# csv 파일 변환
data = data.reset_index(drop = True)
data.to_csv('data1.csv', encoding='utf-8', index = False)

# 130,001~260,000 데이터 수집
data = None
start = 130001
end = 260000
for i in range(start, end+1, 1000):
    result = fetch_data(i, i+999)
    data = pd.concat([data, result], ignore_index=True)
    
# csv 파일 변환
data = data.reset_index(drop = True)
data.to_csv('data2.csv', encoding='utf-8', index = False)

# 260,001~390,000 데이터 수집
data = None
start = 260001
end = 390000
for i in range(start, end+1, 1000):
    result = fetch_data(i, i+999)
    data = pd.concat([data, result], ignore_index=True)
    
# csv 파일 변환
data = data.reset_index(drop = True)
data.to_csv('data3.csv', encoding='utf-8', index = False)

# 390,001~580,000 데이터 수집
data = None
start = 390001
end = 580000
for i in range(start, end+1, 1000):
    result = fetch_data(i, i+999)
    data = pd.concat([data, result], ignore_index=True)

# 580,000~580,507 데이터 수집
result = fetch_data(580001, 580507)
data = pd.concat([data, result], ignore_index=True)

# csv 파일 변환
data = data.reset_index(drop = True)
data.to_csv('data4.csv', encoding='utf-8', index = False)

# 데이터 합치기
data1 = pd.read_csv('./data/data1.csv')
data2 = pd.read_csv('./data/data2.csv')
data3 = pd.read_csv('./data/data3.csv')
data4 = pd.read_csv('./data/data4.csv')

data = pd.concat([data1, data2, data3, data4], ignore_index=True)

data.to_csv('./data/merged_data.csv', encoding='utf-8', index=False)

 

 

## 데이터 불러오기

  • 필요한 데이터만 불러오기 하였다.
  • 임대 면적의 단위가 제곱미터로 되어 있었는데, 한국에서는 대개 평수를 단위로 많이 사용하므로 새로운 열을 생성하여 제곱미터를 평수로 변환한 데이터를 저장하였다.
  • 데이터 중 건물명 데이터가 없는 경우 건물용도로 대체하도록 하였다.
## app.py

@st.cache_data
def load_data():
    df = pd.read_csv('./data/data.csv')  # 데이터 불러오기
    data = df.loc[:, ['SGG_NM',  # 자치구명
    'BJDONG_NM',  # 법정동명
    'CNTRCT_DE',  # 계약일
    'RENT_GBN',  # 전월세 구분
    'RENT_AREA',  # 임대면적
    'RENT_GTN',  # 보증금(만원)
    'RENT_FEE',  # 임대료(만원)
    'BLDG_NM',  # 건물명
    'HOUSE_GBN_NM',  # 건물용도
    data['평수'] = data['RENT_AREA'] * 0.3025  # 제곱미터를 평수로 변환하여 새로운 열 생성하기
    data['BLDG_NM'] = data['BLDG_NM'].fillna(data['HOUSE_GBN_NM'])  # 건물명이 결측값인 경우 건물용도로 대체하기
    return data

 

 

## 임대료, 보증금 평균 그래프를 생성하는 함수 정의하기

  • 전세의 경우 보증금 평균을 나타내는 그래프만 생성하면 되지만, 월세의 경우 보증금과 임대료 평균을 나타내는 그래프가 모두 필요하였다.
  • 보증금 평균은 막대 그래프로 나타내고, 임대료 평균은 선 그래프로 나타내었다.
  • 따라서 y2(아래에서는 임대료 평균)가 존재하는 경우에만 막대 그래프 위에 선 그래프를 추가하도록 하였다.
## app.py

# 임대료 보증금 평균 그래프 생성
def plot_graph(data, x, y1, y2=None, secondary_y=False, title=''):
    fig = make_subplots(specs=[[{"secondary_y": secondary_y}]])    
    # y1에 대한 막대 그래프 추가
    fig.add_trace(go.Bar(x=data[x], y=data[y1],
                         name='보증금 평균', marker=dict(color=data[y1], colorscale='Blues')), secondary_y=False)    
    # y2가 제공되면 y2에 대한 선 그래프 추가
    if y2:    
        fig.add_trace(go.Scatter(x=data[x], y=data[y2], name='임대료 평균', line=dict(color='white')), secondary_y=True)
    # 레이아웃 및 축 제목 업데이트
    fig.update_layout(title=title)
    fig.update_yaxes(title_text='보증금(만 원)', secondary_y=False, tickformat=',.0f')
    if y2:
        fig.update_yaxes(title_text='임대료(만 원)', secondary_y=True, tickformat=',.0f')
    # Streamlit에서 Plotly 차트 표시
    st.plotly_chart(fig, use_container_width=True)

 

 

## 표 보이는 옵션을 생성하는 함수 정의하기

  • 그래프 아래에 체크박스를 만들고, 사용자가 체크박스를 선택하면 그래프에 해당하는 데이터를 표로 보여주도록 하였다.
## app.py

# 표 보이기 옵션 생성
def show_dataframe(dataframe):
    # 사용자가 체크박스를 선택하면 표를 보여줌
    if st.checkbox('표 보이기'):
        # 표를 출력함
        st.dataframe(dataframe, hide_index=True, use_container_width=True)

 

 

## 메일 보내는 기능을 가진 함수 정의하기

  • gmail을 이용하여 사용자가 개발자에게 메일을 전송할 수 있도록 하였다.
## app.py

# 이메일 보내기
def send_email(name, email, inquiry_type, inquiry_details):
    # 보내는 사람, 받는 사람 이메일 설정
    sender_email = "sender_eamil@gmail.com"  # 보내는 사람 이메일 주소
    receiver_emails = ["receiver_email1@gmail.com", "receiver_email2.com", "receiver_email3@gmail.com"]  # 받는 사람 이메일 주소
    # 이메일 제목과 내용 설정
    subject = f"새로운 문의: {inquiry_type} - {name}"
    body = f"""
    이름: {name}
    이메일: {email}
    문의 유형: {inquiry_type}
    문의 내용:
    {inquiry_details}
    """
    # 이메일 메시지 설정
    message = MIMEMultipart()
    message["From"] = sender_email
    message["To"] = ", ".join(receiver_emails)  # 여러 이메일 주소를 쉼표로 구분하여 문자열로 변환
    message["Subject"] = subject
    message.attach(MIMEText(body, "plain"))
    # SMTP 서버에 연결하여 이메일 보내기
    try:
        server = smtplib.SMTP("smtp.gmail.com", 587)  # SMTP 서버 주소와 포트
        server.starttls()  # TLS 암호화 시작
        server.login(sender_email, "google app password")  # 이메일 계정 로그인
        server.sendmail(sender_email, receiver_emails, message.as_string())  # 이메일 보내기
        st.success("이메일이 성공적으로 전송되었습니다!")
    except Exception as e:
        st.error(f"이메일을 보내는 중 오류가 발생했습니다: {e}")
    finally:
        server.quit()  # SMTP 서버 연결 종료

 

 

## 메인 페이지

  • 메인 페이지에 프로젝트에 대한 간략한 설명이 나타나도록 하였다.
## app.py

# 메인 페이지
def main_page():
    st.title("🏠 내집을 찾아서")
    st.subheader("서울 집 값, 어디까지 알아보고 오셨어요?")
    st.markdown("* 본 프로젝트는 서울 부동산 시장에서 적절한 주택을 찾는 과정을 지원하는 것을 목표로 합니다.")
    st.markdown("* 사용자가 원하는 조건을 입력하면 서울에서 필요한 조건에 따른 부동산 시세를 그래프 및 도표 형태로 보여줍니다.")
    st.markdown("* 이를 통해 사용자 입장에서 필요한 전·월세 실거래 정보를 한눈에 확인하고, 위치별 시세를 비교하여 집을 구하는 시간을 단축할 수 있습니다.\n\n")
    st.markdown("\n")
    st.markdown("\n")

    st.subheader("프로젝트 개요")
    st.markdown("멀티캠퍼스 멀티잇 데이터 분석 & 엔지니어 34회차")
    st.markdown("Team 1 Mini-Project : '내 집을 찾아서'  [GitHub](https://github.com/Ju0s/Prj-FindMyHouse)")

 

 

## 자치구별 시세 페이지

  • 자치구별 시세 페이지에서는 서울의 자치구별로 시세를 알아볼 수 있도록 하였다.
  • 사용자가 전·월세와 건물용도, 평수를 선택하도록 하였다. 전·월세는 전세와 월세 중 하나만 선택할 수 있도록 하였고, 건물용도는 다중으로 선택할 수 있도록 하였으며, 평수는  원하는 범위를 슬라이더로 설정하도록 하였다. 
  • 사용자가 설정한 옵션에 맞는 거래 내역을 그래프와 표로 나타낼 수 있도록 하였고, 설정한 옵션에 해당하는 데이터가 없는 경우 다른 옵션을 설정하라는 안내 문구가 표시되도록 하였다.
## app.py

# 자치구별 시세 페이지
def sgg_page(recent_data):
    st.title("자치구별 시세")

    # 최대 평수 구해서 정수로 나타내기(반올림)
    max_area_value = math.ceil(recent_data['평수'].max())

    # 필터 설정
    rent_filter = st.selectbox('전·월세', recent_data['RENT_GBN'].unique())
    house_filter = st.multiselect('건물용도', recent_data['HOUSE_GBN_NM'].unique())
    area_filter = st.slider('평수', min_value=0, max_value=max_area_value, value=(0, max_area_value))

    # 필터 적용
    filtered_recent_data = recent_data[(recent_data['RENT_GBN'] == rent_filter) &
                    (recent_data['HOUSE_GBN_NM'].isin(house_filter)) &
                    (recent_data['평수'] >= area_filter[0]) &
                    (recent_data['평수'] <= area_filter[1])]

    # 자치구별 평균 계산
    average_data = filtered_recent_data.groupby('SGG_NM').agg({'RENT_FEE': 'mean', 'RENT_GTN': 'mean', '평수': 'mean'}).reset_index()

    # 그래프 및 표 생성
    if rent_filter == '월세' and not average_data.empty:
        plot_graph(average_data, x='SGG_NM', y1='RENT_GTN', y2='RENT_FEE', secondary_y=True, title='자치구별 시세')
        show_dataframe(average_data[['SGG_NM', 'RENT_GTN', 'RENT_FEE', '평수']].rename(columns={'SGG_NM': '자치구', 'RENT_GTN': '보증금 평균', 'RENT_FEE': '임대료 평균', '평수': '평수 평균'}))
    elif rent_filter == '전세' and not average_data.empty:
        plot_graph(average_data, x='SGG_NM', y1='RENT_GTN', title='자치구별 시세')
        show_dataframe(average_data[['SGG_NM', 'RENT_GTN', '평수']].rename(columns={'SGG_NM': '자치구', 'RENT_GTN': '보증금 평균', 'RENT_FEE': '임대료 평균', '평수': '평수 평균'}))
    else:
        st.write("최근 1개월 내 계약 내역이 없습니다. 다른 옵션을 선택하세요.")

·

 

 

## 법정동별 시세 페이지

  • 법정동별 시세 페이지에서는 서울의 법정동별로 시세를 알아볼 수 있도록 하였다.
  • 사용자가 전·월세와 자치구, 건물용도, 평수를 선택하도록 하였다. 전·월세와 자치구는 각각 하나만 선택할 수 있도록 하였고, 건물용도는 다중으로 선택할 수 있도록 하였으며, 평수는 원하는 범위를 슬라이더로 설정하도록 하였다. 
  • 사용자가 설정한 옵션에 맞는 거래 내역을 그래프와 표로 나타낼 수 있도록 하였고, 설정한 옵션에 해당하는 데이터가 없는 경우 다른 옵션을 설정하라는 안내 문구가 표시되도록 하였다.
## app.py

# 법정동별 시세 페이지
def bjdong_page(recent_data):
    st.title("법정동별 시세")

    # 최대 평수 구해서 정수로 나타내기(반올림)
    max_area_value = math.ceil(recent_data['평수'].max())

    # 필터 설정
    rent_filter = st.selectbox('전·월세', recent_data['RENT_GBN'].unique())
    sgg_filter = st.selectbox('자치구', recent_data['SGG_NM'].unique())
    house_filter = st.multiselect('건물용도', recent_data['HOUSE_GBN_NM'].unique())
    area_filter = st.slider('평수', min_value=0, max_value=max_area_value, value=(0, max_area_value))

    # 필터 적용
    filtered_recent_data = recent_data[(recent_data['SGG_NM'] == sgg_filter) &
                    (recent_data['RENT_GBN'] == rent_filter) &
                    (recent_data['HOUSE_GBN_NM'].isin(house_filter)) &
                    (recent_data['평수'] >= area_filter[0]) &
                    (recent_data['평수'] <= area_filter[1])]

    # 법정동별 평균 계산
    average_data = filtered_recent_data.groupby('BJDONG_NM').agg({'RENT_FEE': 'mean', 'RENT_GTN': 'mean', '평수': 'mean'}).reset_index()

    # 그래프 및 표 생성
    if rent_filter == '월세' and not average_data.empty:
        plot_graph(average_data, x='BJDONG_NM', y1='RENT_GTN', y2='RENT_FEE', secondary_y=True, title='법정동별 시세')
        show_dataframe(average_data[['BJDONG_NM', 'RENT_GTN', 'RENT_FEE', '평수']].rename(columns={'BJDONG_NM': '법정동', 'RENT_GTN': '보증금 평균', 'RENT_FEE': '임대료 평균', '평수': '평수 평균'}))
    elif rent_filter == '전세' and not average_data.empty:
        plot_graph(average_data, x='BJDONG_NM', y1='RENT_GTN', title='법정동별 시세')
        show_dataframe(average_data[['BJDONG_NM', 'RENT_GTN', '평수']].rename(columns={'BJDONG_NM': '법정동', 'RENT_GTN': '보증금 평균', '평수': '평수 평균'}))
    else:
        st.write("최근 1개월 내 계약 내역이 없습니다. 다른 옵션을 선택하세요.")

 

 

## 건물별 시세 페이지

  • 건물별 시세 페이지에서는 서울에 있는 건물별로 시세를 알아볼 수 있도록 하였다.
  • 사용자가 전·월세와 자치구, 법정동, 건물용도, 평수를 선택하도록 하였다. 전·월세와 자치구, 법정동은 각각 하나만 선택할 수 있도록 하였고, 건물용도는 다중으로 선택할 수 있도록 하였으며, 평수는 원하는 범위를 슬라이더로 설정하도록 하였다. 법정동 옵션에는 앞서 선택한 자치구에 해당하는 법정동만 나타나도록 하였다.
  • 사용자가 설정한 옵션에 맞는 거래 내역을 그래프와 표로 나타낼 수 있도록 하였고, 설정한 옵션에 해당하는 데이터가 없는 경우 다른 옵션을 설정하라는 안내 문구가 표시되도록 하였다.
## app.py

# 건물별 시세 페이지
def bldg_page(recent_data):
    st.title("건물별 시세")

    # 최대 평수 구해서 정수로 나타내기(반올림)
    max_area_value = math.ceil(recent_data['평수'].max())

    # 필터 설정
    rent_filter = st.selectbox('전·월세', recent_data['RENT_GBN'].unique())
    sgg_filter = st.selectbox('자치구', recent_data['SGG_NM'].unique())
    bjdong_options = recent_data[recent_data['SGG_NM'] == sgg_filter]['BJDONG_NM'].unique()
    bjdong_filter = st.selectbox('법정동', bjdong_options)
    house_filter = st.multiselect('건물용도', recent_data['HOUSE_GBN_NM'].unique())
    area_filter = st.slider('평수', min_value=0, max_value=max_area_value, value=(0, max_area_value))

    # 필터 적용
    filtered_recent_data = recent_data[(recent_data['BJDONG_NM'] == bjdong_filter) &
                    (recent_data['RENT_GBN'] == rent_filter) &
                    (recent_data['HOUSE_GBN_NM'].isin(house_filter)) &
                    (recent_data['평수'] >= area_filter[0]) &
                    (recent_data['평수'] <= area_filter[1])]

    # 건물별 평균 계산
    average_data = filtered_recent_data.groupby('BLDG_NM').agg({'RENT_FEE': 'mean', 'RENT_GTN': 'mean', '평수': 'mean'}).reset_index()

    # 그래프 및 표 생성
    if rent_filter == '월세' and not average_data.empty:
        plot_graph(average_data, x='BLDG_NM', y1='RENT_GTN', y2='RENT_FEE', secondary_y=True, title='건물별 시세')
        show_dataframe(average_data[['BLDG_NM', 'RENT_GTN', 'RENT_FEE', '평수']].rename(columns={'BLDG_NM': '건물명', 'RENT_GTN': '보증금 평균', 'RENT_FEE': '임대료 평균', '평수': '평수 평균'}))
    elif rent_filter == '전세' and not average_data.empty:
        plot_graph(average_data, x='BLDG_NM', y1='RENT_GTN', title='법정동별 시세')
        show_dataframe(average_data[['BLDG_NM', 'RENT_GTN', '평수']].rename(columns={'BLDG_NM': '건물명', 'RENT_GTN': '보증금 평균', '평수': '평수 평균'}))
    else:
        st.write("최근 1개월 내 계약 내역이 없습니다. 다른 옵션을 선택하세요.")

 

 

## 최근 1개월 계약 현황 페이지

  • 최근 1개월 계약 건물별 시세 페이지에서는 서울에 있는 건물별로 시세를 알아볼 수 있도록 하였다.
  • 사용자가 전·월세와 자치구, 법정동, 건물용도, 건물명, 평수를 선택하도록 하였다. 전·월세와 자치구, 법정동은 각각 하나만 선택할 수 있도록 하였고, 건물용도와 건물명은 다중으로 선택할 수 있도록 하였으며, 평수는 원하는 범위를 슬라이더로 설정하도록 하였다. 법정동 옵션에는 앞서 선택한 자치구에 해당하는 법정동만 나타나도록 하였고, 건물명에는 앞서 선택한 법정동과 건물용도에 해당하는 건물명만 나타나도록 하였다.
  • 사용자가 설정한 옵션에 맞는 거래 내역을 그래프와 표로 나타낼 수 있도록 하였고, 설정한 옵션에 해당하는 데이터가 없는 경우 다른 옵션을 설정하라는 안내 문구가 표시되도록 하였다.
## app.py

# 최근 1개월 계약 현황 페이지
def onemonth_page(recent_data):
    st.title("건물별 시세")

    # 최대 평수 구해서 정수로 나타내기(반올림)
    max_area_value = math.ceil(recent_data['평수'].max())

    # 계약일 날짜만 나타내기
    recent_data['CNTRCT_DE'] = recent_data['CNTRCT_DE'].dt.date

    # 필터 설정
    rent_filter = st.selectbox('전·월세', recent_data['RENT_GBN'].unique())
    sgg_filter = st.selectbox('자치구', recent_data['SGG_NM'].unique())
    bjdong_options = recent_data[recent_data['SGG_NM'] == sgg_filter]['BJDONG_NM'].unique()
    bjdong_filter = st.selectbox('법정동', bjdong_options)
    house_filter = st.multiselect('건물용도', recent_data['HOUSE_GBN_NM'].unique())
    bldg_options = recent_data[(recent_data['RENT_GBN'] == rent_filter) & (recent_data['BJDONG_NM'] == bjdong_filter) & (recent_data['HOUSE_GBN_NM'].isin(house_filter))]['BLDG_NM'].unique()
    bldg_filter = st.multiselect('건물명', bldg_options)
    area_filter = st.slider('평수', min_value=0, max_value=max_area_value, value=(0, max_area_value))

    # 필터 적용
    filtered_recent_data = recent_data[(recent_data['BLDG_NM'].isin(bldg_filter)) &
                    (recent_data['RENT_GBN'] == rent_filter) &
                    (recent_data['HOUSE_GBN_NM'].isin(house_filter)) &
                    (recent_data['평수'] >= area_filter[0]) &
                    (recent_data['평수'] <= area_filter[1])]

    # 표 생성
    if rent_filter == '월세' and not filtered_recent_data.empty:
        st.dataframe(filtered_recent_data[['CNTRCT_DE', 'BLDG_NM', 'RENT_GTN', 'RENT_FEE', '평수']].rename(columns={'CNTRCT_DE': '계약일', 'BLDG_NM': '건물명', 'RENT_GTN': '보증금', 'RENT_FEE': '임대료'}), hide_index=True, use_container_width=True)
    elif rent_filter == '전세' and not filtered_recent_data.empty:
        st.dataframe(filtered_recent_data[['CNTRCT_DE', 'BLDG_NM', 'RENT_GTN', '평수']].rename(columns={'CNTRCT_DE': '계약일', 'BLDG_NM': '건물명', 'RENT_GTN': '보증금'}), hide_index=True, use_container_width=True)
    else:
        st.write("최근 1개월 내 계약 내역이 없습니다. 다른 옵션을 선택하세요.")

 

 

## 2023년 평균 시세 조회 페이지

  • 2023년 평균 시세 조회 페이지에서는 2023년에 이루어진 임대 계약 데이터를 기반으로 서울의 월별 평균 시세를 알아볼 수 있도록 하였다.
  • 사용자가 전·월세와 자치구, 법정동, 건물용도, 건물명, 평수를 선택하도록 하였다. 전·월세와 자치구, 법정동은 각각 하나만 선택할 수 있도록 하였고, 건물용도와 건물명은 다중으로 선택할 수 있도록 하였으며, 평수는 원하는 범위를 슬라이더로 설정하도록 하였다. 법정동 옵션에는 앞서 선택한 자치구에 해당하는 법정동만 나타나도록 하였고, 건물명에는 앞서 선택한 법정동과 건물용도에 해당하는 건물명만 나타나도록 하였다.
  • 사용자가 설정한 옵션에 맞는 거래 내역을 그래프와 표로 나타낼 수 있도록 하였고, 설정한 옵션에 해당하는 데이터가 없는 경우 다른 옵션을 설정하라는 안내 문구가 표시되도록 하였다.
## app.py

# 2023년 평균 시세 조회 페이지
def yearly_page(recent_data):
    def calculate_monthly_averages(data):
        # 'CNTRCT_DE' 열을 datetime 형식으로 변환
        data['CNTRCT_DE'] = pd.to_datetime(data['CNTRCT_DE'])

        # 월별로 데이터를 나누고 각 월별 보증금과 임대료의 평균을 계산하여 리스트로 반환
        monthly_averages = []
        for month in range(1, 13):
            # 해당 월의 데이터 추출
            monthly_data = data[data['CNTRCT_DE'].dt.month == month]
            # 해당 월의 보증금과 임대료의 평균 계산
            avg_rent_gtn = monthly_data['RENT_GTN'].mean()
            avg_rent_fee = monthly_data['RENT_FEE'].mean()
            avg_rent_area = monthly_data['RENT_AREA'].mean()
            # 결과를 튜플로 추가
            monthly_averages.append((avg_rent_gtn, avg_rent_fee, avg_rent_area))

        return monthly_averages


    st.title("2023년 월별 평균 보증금, 임대료 조회")

    # 데이터 불러오기
    data = load_data()

    # 정수로 된 날짜 열을 날짜로 변환
    data['CNTRCT_DE'] = pd.to_datetime(data['CNTRCT_DE'], format='%Y%m%d')
    # 데이터 중에서 2023년 데이터만 선택
    recent_data = data[(data['CNTRCT_DE'] >= pd.to_datetime('20230101', format='%Y%m%d')) & (data['CNTRCT_DE'] < pd.to_datetime('20240101', format='%Y%m%d'))]

    # 최대 평수 구해서 정수로 나타내기(반올림)
    max_area_value = math.ceil(recent_data['평수'].max())

    # 계약일 날짜만 나타내기
    recent_data['CNTRCT_DE'] = recent_data['CNTRCT_DE'].dt.date
    
    # 필터 설정
    rent_filter = st.selectbox('전·월세', recent_data['RENT_GBN'].unique())
    sgg_filter = st.selectbox('자치구', recent_data['SGG_NM'].unique())
    bjdong_options = recent_data[recent_data['SGG_NM'] == sgg_filter]['BJDONG_NM'].unique()
    bjdong_filter = st.selectbox('법정동', bjdong_options)
    house_filter = st.multiselect('건물용도', recent_data['HOUSE_GBN_NM'].unique())
    bldg_options = recent_data[(recent_data['RENT_GBN'] == rent_filter) & (recent_data['BJDONG_NM'] == bjdong_filter) & (recent_data['HOUSE_GBN_NM'].isin(house_filter))]['BLDG_NM'].unique()
    bldg_filter = st.selectbox('건물명', bldg_options)
    area_filter = st.slider('평수', min_value=0, max_value=max_area_value, value=(0, max_area_value))

    # 필터 적용
    filtered_recent_data = recent_data[(recent_data['BLDG_NM'] == bldg_filter) &
                    (recent_data['RENT_GBN'] == rent_filter) &
                    (recent_data['HOUSE_GBN_NM'].isin(house_filter)) &
                    (recent_data['평수'] >= area_filter[0]) &
                    (recent_data['평수'] <= area_filter[1])]

    # 월별 평균 계산
    monthly_averages = calculate_monthly_averages(filtered_recent_data)

    # 월별 보증금과 임대료 데이터 프레임 생성
    months = [f"{month}월" for month in range(1, 13)]
    avg_rent_gtn = [avg[0] for avg in monthly_averages]
    avg_rent_fee = [avg[1] for avg in monthly_averages]
    avg_rent_area = [avg[2] for avg in monthly_averages]
    monthly_data = pd.DataFrame({'Month': months, 'Avg_Rent_GTN': avg_rent_gtn, 'Avg_Rent_Fee': avg_rent_fee, 'Avg_Rent_Area': avg_rent_area})

    # 그래프, 표 생성
    if rent_filter == '월세' and not filtered_recent_data.empty:
        # 보증금과 임대료 평균 그래프 시각화
        plot_graph(monthly_data, x='Month', y1='Avg_Rent_GTN', y2='Avg_Rent_Fee', secondary_y=True, title='월별 보증금 및 월 임대료 평균(2023)')
        show_dataframe(monthly_data[['Month', 'Avg_Rent_GTN', 'Avg_Rent_Fee', 'Avg_Rent_Area']].rename(columns={'Month': '월', 'Avg_Rent_GTN': '보증금 평균', 'Avg_Rent_Fee': '월 임대료 평균', 'Avg_Rent_Area': '면적 평균'}))
    elif rent_filter == '전세' and not filtered_recent_data.empty:
        # 보증금과 임대료 평균 그래프 시각화
        plot_graph(monthly_data, x='Month', y1='Avg_Rent_GTN', secondary_y=False, title='월별 전세 보증금 평균(2023)')
        show_dataframe(monthly_data[['Month', 'Avg_Rent_GTN', 'Avg_Rent_Area']].rename(columns={'Month': '월', 'Avg_Rent_GTN': '보증금 평균', 'Avg_Rent_Area': '면적 평균'}))
    else:
        st.write("2023년 계약 내역이 없습니다. 다른 옵션을 선택하세요.")

 

 

## 지원 및 문의 페이지

  • 지원 및 문의 페이지에서는 사용자가 문의할 사항이 있을 경우 개발자에게 메일을 전송할 수 있도록 하였다.
## app.py

# 지원 및 문의 페이지
def support_page():
    st.title("지원 및 문의")

    # 사용자 정보 입력
    name = st.text_input("이름")
    email = st.text_input("이메일 주소")
    inquiry_type = st.selectbox("문의 유형", ["기술 지원", "문의 사항", "기타"])
    inquiry_details = st.text_area("문의 내용", height=200)

    # 문의 제출 버튼
    if st.button("문의 제출"):
        send_email(name, email, inquiry_type, inquiry_details)

 

 

# 앱 실행

  • 페이지 탭을 프로젝트 테마에 맞게 디자인하였다.
  • 최신 현황을 반영하기 위해 현재 날짜를 기준으로 최근 한 달의 데이터를 가져오도록 하였다. 이는 추후 csv 파일을 새로 업데이트 하였을 때를 고려한 부분이다.
  • 사이드바에 메뉴를 만들고, 해당 메뉴를 선택할 때마다 해당 페이지로 이동하도록 하였다.
## app.py

def main():
    # 페이지 탭 디자인
    st.set_page_config(
        page_title="내 집을 찾아서",
        page_icon="🏠",
        layout="wide",
        initial_sidebar_state="expanded",
        # menu_items={
        #     'Get Help': 'https://www.extremelycoolapp.com/help',
        #     'Report a bug': "https://www.extremelycoolapp.com/bug",
        #     'About': "# This is a header. This is an *extremely* cool app!"
        # }
    )    

    # 데이터 불러오기
    data = load_data()

    # 최근 한 달 데이터만 가져오기
    # 정수로 된 날짜 열을 날짜로 변환
    data['CNTRCT_DE'] = pd.to_datetime(data['CNTRCT_DE'], format='%Y%m%d')
    # 데이터 중에서 가장 최근의 날짜 찾기
    latest_date = data['CNTRCT_DE'].max()
    # 최근 한 달 데이터 선택
    recent_data = data[data['CNTRCT_DE'] >= (latest_date - pd.DateOffset(days=30))]

    # 사이드바 메뉴
    with st.sidebar:
        selected_menu = option_menu("메뉴 선택", ["메인 페이지", "내가 살 곳 찾기", "집 값 파악하기", "지원 및 문의"],
                            icons=['bi bi-house-fill','bi bi-geo-alt-fill', 'bi bi-graph-up-arrow', 'bi bi-info-circle'], menu_icon='bi bi-check',
                            styles={"container": {"background-color": "#3081D0", "padding": "0px"},
                                    "nav-link-selected": {"background-color": "#EEEEEE", "color": "#262730"}})

        if selected_menu == "메인 페이지":
            choice = "메인 페이지"
            
        elif selected_menu == "내가 살 곳 찾기":
            choice = option_menu("내가 살 곳 찾기", ["자치구 정하기", "동네 정하기", "건물 정하기"],
                                 icons=['bi bi-1-circle','bi bi-2-circle', 'bi bi-3-circle'], menu_icon='bi bi-house-fill',
                                 styles={"container": {"background-color": "#FC6736"}, "nav-link-selected": {"background-color": "#EEEEEE", "color": "#262730"}})

        elif selected_menu == "집 값 파악하기":
            choice = option_menu("집 값 파악하기", ["최근 1개월 계약 현황", "2023년 실거래가 추이"],
                                 icons=['bi bi-pen-fill','bi-graph-up-arrow'], menu_icon='bi bi-currency-dollar',
                                 styles={"container": {"background-color": "#FC6736"}, "nav-link-selected": {"background-color": "#EEEEEE", "color": "#262730"}})

        if selected_menu == "지원 및 문의":
            choice = "지원 및 문의"

    # 페이지 보이기
    if choice == "메인 페이지":
        main_page()

    elif choice == "자치구 정하기":
        sgg_page(recent_data)
    
    elif choice == "동네 정하기":
        bjdong_page(recent_data)
    
    elif choice == "건물 정하기":
        bldg_page(recent_data)
    
    elif choice == "최근 1개월 계약 현황":
        onemonth_page(recent_data)

    elif choice == "2023년 실거래가 추이":
         yearly_page(recent_data)

    elif choice == "지원 및 문의":
        support_page()


if __name__ == '__main__':
    main()

 

 

# 데모페이지

 

내 집을 찾아서

본 프로젝트는 부동산 시장에서 적절한 주택을 찾는 과정을 지원하는 것을 목표로 합니다.사용자가 원하는 조건을 입력하면 자치구, 법정동, 또는 건물에 따른 부동산 시세를 그래프...

prj-findmyhouse.streamlit.app

 

 

# 프로젝트 결론

  • 본 프로젝트에서 생성한 앱을 통해 사용자는 서울의 부동산 시장에서 자신이 원하는 집을 찾을 수 있다. 사용자는 앱의 '내가 살 곳 찾기' 메뉴의 프로세스를 따라가면서 쉽게 전·월세 실거래 정보를 확인하고, 위치별 시세를 비교하며 자신의 상황에 맞는 집의 위치와 건물을 알아볼 수 있다. '최근 1개월 계약 현황' 메뉴를 통해서는 사용자가 알아보고 싶은 건물의 계약 현황을 볼 수 있고, '2023년 실거래가 추이' 메뉴를 통해서는 2023년의 대략적인 실거래가 추이를 파악하여 앞으로의 시세 변동 추이를 예측해볼 수 있다.
  • 사용자가 자신이 원하는 집을 이미 찾은 경우에도 해당 앱의 '집값 파악하기' 메뉴를 통해 자신이 원하는 건물의 임대 계약 추이를 파악하여 계약 시기를 결정할 수 있다.
  • 임대 계약과 상관 없이 서울의 부동산에 관심이 있는 사용자도 해당 앱의 '집값 파악하기' 메뉴를 통해 서울의 임대 계약 추이를 파악할 수 있다.
  • 해당 앱은 csv 파일의 데이터를 기반으로 하고 있어서, csv 파일 자체를 지속적으로 업데이트 하지 않으면 최신 거래 현황을 파악하기 어렵다는 한계를 가지고 있다. 따라서, DB를 이용하는 방법 등을 통해 이를 보완할 필요가 있다.
  • 해당 앱의 메뉴 중 '내가 살 곳 찾기'의 페이지들 간 이동은 사이드바의 메뉴를 선택해야만 가능하게 되어 있는데, 사용자가 이용하기 편리하도록 페이지 내에서도 다른 페이지로 이동할 수 있도록 기능을 추가할 필요가 있다.

 

# 기여한 부분

  • 기획
  • 데이터 크롤링
  • 데이터 불러오기, 그래프 생성하기, 표 보이기, 메일 보내기 함수 정의
  • 자치구별 시세, 법정동별 시세, 건물별 시세, 최근 1개월 계약 현황 페이지 코딩
  • 메인 함수의 기반이 되는 부분 코딩
728x90