본문 바로가기

미니 프로젝트

첫 번째 미니 프로젝트 - 21.08.26(목)

728x90

목표 : 최근 7일간 코로나19 신규 확진자 및 지역별 누적 확진자 수를 크롤링하고 데이터 분석하여 시각화하기. 

기한 : 2021년 8월 26일 오전 9시 ~ 오후 4시

평가기준 : 얼마나 배운 것을 잘 활용하여 목표를 수행하였는지. 

 

# 필요한 라이브러리 불러오기.
import re # 정규표현식 사용할 때 필요
import time	# 크롤링할 때 등 대기 시간 줄 때 필요
import json	# folium에 이용할 때 등 json 파일 로드할 때 필요
import folium	# 지도를 이용한 시각화 툴

import pymysql # python에서 mysql 사용하기 위해 임폴트
import unicodedata	# 유니코드 데이터베이스에 대한 엑세스 제공
import pandas as pd	# 판다스, 데이터 분석 및 전처리 도구
import matplotlib.pyplot as plt	# 데이터 시각화 툴
from matplotlib import font_manager, rc # 데이터 시각화할때 한글 안깨지게 하기 위한 도구

from selenium import webdriver	# 웹드라이버(크롬 드라이버)를 이용한 크롤링 도구
from bs4 import BeautifulSoup	# bs4 웹 크롤링 도구
from sqlalchemy import create_engine	# SQL 데이터베이스로 데이터 밀어넣기 위한 도구
from branca.element import Template, MacroElement # 


## 드라이브 설정 및 url 설정
driver = webdriver.Chrome('./chromedriver.exe')
url = "https://search.naver.com/search.naver?where=nexearch&sm=top_hty&fbm=0&ie=utf8&query=%EC%BD%94%EB%A1%9C%EB%82%98+%ED%98%84%ED%99%A9"
driver.get(url)


# 크롬 드라이버를 이용해서 최근 7일간 신규확진자수 데이터 크롤링
## 날짜 클릭하는 함수 및 확진자 수 불러오는 함수 만들기 

def select_day(driver):
    time.sleep(1)
    day = driver.find_element_by_css_selector("dd.value")
    day.click()
    time.sleep(2)

def get_content(driver):
    html = driver.page_source
    soup = BeautifulSoup(html, 'html.parser')
    
    
    try:
        day = "2021-0" + soup.select("dd.date._x_value")[0].text.replace(".", "-")
        cnt = soup.select("dd.desc_em._total")[0].text.replace(",", "")
    
    except:
        cnt = " "
        day = " "
    
    return (day, cnt)

### 가져온 날짜 및 확진자 수를 담을 리스트 생성
weeks_cnt = []


### 반복문을 통해 가져온 데이터 리스트에 튜플 형식으로 담기
for _ in range(7):
    select_day(driver)
    weeks_cnt.append(get_content(driver))


### 리스트를 데이터프레임으로 바꾸기

newly = pd.DataFrame(weeks_cnt, columns = ['date', 'decide_cnt'])
newly['date'] = newly['date'].astype('datetime64')
newly['decide_cnt'] = newly['decide_cnt'].astype('int')


# 시각화 툴 폰트설정
font_path = './malgun.ttf'
font_name = font_manager.FontProperties(fname=font_path).get_name()
rc('font', family=font_name)

# plt를 이용한 라인 그래프

plt.plot(newly.date, newly.decide_cnt)
plt.title('최근 일주일간 신규확진자 추이')
plt.xlabel('날짜')
plt.ylabel('신규 확진자 수')
plt.show()

# 데이터프레임 엑셀로 저장
newly.to_excel('./newly.xlsx', index=False)

# 판다스 read_html을 이용해서 지역별 확진자 테이블 가져오기

table = pd.read_html(url)

### 3개로 나눠진 테이블 합쳐주기

regional = pd.concat([table[0], table[1], table[2]])
regional = regional.reset_index().drop('index', axis=1)

### 컬럼명 설정

regional.rename(columns={'지역':'location', '누적확진자':'acc_decide_cnt', '신규확진자':'regional_decide_cnt'}, inplace=True)


### 검열(해외입국) 행 제거해주기

regional = regional.drop(8).reset_index().drop('index', axis=1)

regional.to_excel('./regional.xlsx', index=False)

# pymysql을 이용해서 SQL문으로 데이터 저장하기
### pymysql 라이브러리 불러오기



### 데이터베이스에 연결 하고 커서 가져오기
conn = pymysql.connect(host='localhost', user='root', password = '1234', charset = 'utf8')
cur = conn.cursor()

### 데이터베이스(스키마) 만들기
cur.execute("CREATE SCHEMA IF NOT EXISTS first_mini_project")

### 만든 데이터베이스 연결하기
conn = pymysql.connect(host='localhost', user='root', password = '1234', db = 'first_mini_project', charset = 'utf8')
cur = conn.cursor()

### 테이블 만들기
cur.execute("CREATE TABLE IF NOT EXISTS newly (date char(4), decide_cnt int)")
cur.execute("CREATE TABLE IF NOT EXISTS regional (location char(4), acc_decide_cnt int, regional_decide_cnt int)")

#### 테이블 컬럼 형식 바꿔주기 ; 아래서 데이터 입력할때 충돌나서 수정해준것.)
cur.execute("ALTER TABLE newly MODIFY decide_cnt MEDIUMTEXT")


### 데이터 프레임을 sql로 

db_connection_str = 'mysql+pymysql://root:1234@localhost/first_mini_project'

engine = create_engine(db_connection_str, echo=False)

newly.to_sql('newly', con=engine, if_exists='append', index=False)

regional.to_sql('regional', con=engine, if_exists='append', index=False)

### 데이터베이스에서 데이터 불러오기

newly_from_db = pd.read_sql_table('newly', con=engine) 

newly_from_db

regional_from_db = pd.read_sql_table('regional', con=engine) 

regional_from_db




# 데이터 분석

regional_sum = pd.DataFrame([['전체', regional['acc_decide_cnt'].sum(), regional['regional_decide_cnt'].sum()]], columns=['location', 'acc_decide_cnt', 'regional_decide_cnt'])

regional_sum = pd.concat([regional, regional_sum], ignore_index=True)

regional_sum

### 전체 누적 확진자의 지역별 비중

regional_sum['acc_percentage'] = (regional_sum['acc_decide_cnt']/regional_sum['acc_decide_cnt'].iloc[-1])*100

regional_sum

### 신규 확진자의 지역별 비중

regional_sum['daily_percentage'] = (regional_sum['regional_decide_cnt']/regional_sum['regional_decide_cnt'].iloc[-1])*100

regional_sum

### 확진자 지역별 비중 증감률

regional_sum['up/down'] = (regional_sum['daily_percentage'] - regional_sum['acc_percentage'])

regional_sum['rate_of_change'] =  (regional_sum['daily_percentage'] / regional_sum['acc_percentage']) * 100 -100

regional_sum

## 확진자가 증가 중인 도시 확진자 관련 뉴스 알아보기 - 뉴스 크롤링

def get_news(driver, url):
    title = []
    driver.get(url)
    html = driver.page_source
    soup = BeautifulSoup(html, 'html.parser')
    
    try:
        for i in range(5):
            title.append(soup.select('a.news_tit')[i].text)
        
    except:
        title = '실패'
    
    return title

search_url = 'https://search.naver.com/search.naver?where=news&sm=tab_jum&query=확진자+{}'

for i in range(len(regional_sum)):
    if regional_sum['rate_of_change'][i] > 0:
        print(regional_sum['location'][i], " : ", get_news(driver, search_url.format(regional_sum['location'][i])))

# 데이터 시각화 (데이터 베이스 이후)

## 위도, 경도 데이터 전처리

geo_df = pd.read_excel('./행정_법정동 중심좌표.xlsx')
geo_df.head()

geo_df = geo_df.drop(columns=['코드', '시군구', '읍면동', '하위', '코드종류'])

geo_df['시도'] = geo_df['시도'].drop_duplicates()
geo_df = geo_df.dropna()
geo_df = geo_df.sort_values("시도", ignore_index=True)
geo_df

regional = regional.sort_values(by=['location'], axis=0).reset_index().drop('index', axis=1)

final_df = pd.concat([regional, geo_df], axis=1, join="inner")
final_df

## folium을 이용한 데이터 시각화 (1)

geojson_file_path = './korea_geo.json'
korea_geo = json.load(open(geojson_file_path, encoding='utf-8'))
korea_geo['features'][0]['properties']

covid_mean = final_df['acc_decide_cnt'].mean()
print(covid_mean)

covid_bubble = folium.Map(
    location=[36.607996, 127.286277], tiles ='CartoDB positron', zoom_start=6)

def style_function(feature):
    return {
        'opacity': 0.1,
        'weight': 1,
        'color': 'white',
        'fillOpacity':0,
        'dashArray': '1, 1',
    }

folium.GeoJson(
    korea_geo,
    style_function=style_function
).add_to(covid_bubble)


for idx in final_df.index:
    lat = final_df.loc[idx, '위도']
    lng = final_df.loc[idx, '경도']
    count = final_df.loc[idx, 'acc_decide_cnt']

    if count > covid_mean:
        fillColor = '#de6464'
    else:
        fillColor = '#6499de'
    
    folium.CircleMarker(
        location=[lat, lng], 
        color='#ffffff',
        fill_color=fillColor, 
        fill_opacity=1.0,
        weight=0.1,
        radius=count/1500,
    ).add_to(covid_bubble)
    


template = """
{% macro html(this, kwargs) %}

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>jQuery UI Draggable - Default functionality</title>
  <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">

  <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
  <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
  
  <script>
  $( function() {
    $( "#maplegend" ).draggable({
                    start: function (event, ui) {
                        $(this).css({
                            right: "auto",
                            top: "auto",
                            bottom: "auto"
                        });
                    }
                });
});

  </script>
</head>
<body>

 
<div id='maplegend' class='maplegend' 
    style='position: absolute; z-index:9999; border:2px solid grey; background-color:rgba(255, 255, 255, 0.8);
     border-radius:6px; padding: 10px; font-size:14px; right: 20px; bottom: 20px;'>
     
<div class='legend-title'>우리나라 누적 확진자 분포</div>
<div class='legend-scale'>
  <ul class='legend-labels'>
    <li><span style='background:#de6464;'></span>전체 평균 이상</li>
    <li><span style='background:#6499de;'></span>전체 평균 이하</li>

  </ul>
</div>
</div>
 
</body>
</html>

<style type='text/css'>
  .maplegend .legend-title {
    text-align: left;
    margin-bottom: 5px;
    font-weight: bold;
    font-size: 90%;
    }
  .maplegend .legend-scale ul {
    margin: 0;
    margin-bottom: 5px;
    padding: 0;
    float: left;
    list-style: none;
    }
  .maplegend .legend-scale ul li {
    font-size: 80%;
    list-style: none;
    margin-left: 0;
    line-height: 18px;
    margin-bottom: 2px;
    }
  .maplegend ul.legend-labels li span {
    display: block;
    float: left;
    height: 16px;
    width: 30px;
    margin-right: 5px;
    margin-left: 0;
    border: 1px solid #999;
    }
  .maplegend .legend-source {
    font-size: 80%;
    color: #777;
    clear: both;
    }
  .maplegend a {
    color: #777;
    }
</style>
{% endmacro %}"""

macro = MacroElement()
macro._template = Template(template)

covid_bubble.get_root().add_child(macro)


covid_bubble

korea_geo_2 = json.load(open(geojson_file_path, encoding='utf-8'))
covid_choropleth = folium.Map(
    location=[36.607996, 127.286277],
    tiles='CartoDB positron',
    zoom_start=6
)

folium.Choropleth(
    geo_data=korea_geo_2,
    data=final_df,
    columns=['시도', 'acc_decide_cnt'],
    fill_color = 'BuGn',
    fill_opacity=1.5,
    line_opacity=0.2,
    key_on='feature.properties.CTP_KOR_NM',
    legend_name="지역별 누적 확진자 수"
    ).add_to(covid_choropleth)

folium.LayerControl().add_to(covid_choropleth)

for idx in final_df.index:
    lat = final_df.loc[idx, '위도']
    lng = final_df.loc[idx, '경도']
    count = final_df.loc[idx, 'regional_decide_cnt']
    
    folium.CircleMarker(
        location=[lat, lng], 
        color='#bd1919',
        fill_color=fillColor, 
        fill_opacity=0.0,
        weight=1.1,
        radius=count/30,
    ).add_to(covid_choropleth)

covid_choropleth

https://github.com/HonorJay/first_mini/blob/main/first_mini_project(%EC%B5%9C%EC%B5%9C%EC%B5%9C%EC%A2%85).ipynb