1. 스타벅스 매장 위치 데이터 확보
2. 이디야커피 매장 위치 데이터 확보
3. 데이터 전처리
4. 데이터 검증
이상치, 결측치 파악
5. 그래프를 통한 데이터 시각화
seaborn
folium
6. 결론
*❗제공되는 해설지와 비교하여 다른 부분 표시
# 셀레니움을 이용해 웹사이트 원격 조종할 준비를 한다.
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
url = 'https://www.starbucks.co.kr/store/store_map.do?disp=locale' #스타벅스 매장 검색 주소
driver=webdriver.Chrome('../driver/chromedriver.exe')
# 화면 최대 크기 설정
driver.maximize_window()
driver.get(url)
time.sleep(2) # 로딩 기다리기
#매장 찾기에서 서울을 선택
search_seoul=driver.find_element(By.CSS_SELECTOR,"#container > div > form > fieldset > div > section > article.find_store_cont > article > article:nth-child(4) > div.loca_step1 > div.loca_step1_cont > ul > li:nth-child(1) > a")
search_seoul.click()
❗ 나는 하나하나 직접 클릭후 데이터를 가져왔지만 전체를 클릭 후 모든 데이터 가져오기 가능
# 각 구/군 선택 버튼
gu_btn_list=driver.find_elements(By.CLASS_NAME,"set_gugun_cd_btn")
len(gu_btn_list) # '전체' 가 포함된 값
from tqdm import tqdm
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup
from selenium.webdriver import ActionChains
gu_list=[] # 구 리스트
address_list=[] # 주소 리스트
store_list=[] # 매장 이름 리스트
def getStarbucksInfo():
for idx in tqdm(range(1,len(gu_btn_list))):
driver.execute_script('window.scrollTo(0,0)') # 스크롤 맨위
time.sleep(2)
# 지역 클릭 : 클릭 가능한 상태가 될때까지 최대 10초 wait
WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.CSS_SELECTOR,'#container > div > form > fieldset > div > section > article.find_store_cont > article > header.loca_search > h3 > a'))).click()
# 서울 클릭 : 클릭 가능한 상태가 될때까지 최대 10초 wait
WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.CSS_SELECTOR,"#container > div > form > fieldset > div > section > article.find_store_cont > article > article:nth-child(4) > div.loca_step1 > div.loca_step1_cont > ul > li:nth-child(1) > a"))).click()
# loading wait
time.sleep(3)
# 구/군 버튼 리스트 가져오기
gu_btn_list_renew=driver.find_elements(By.CLASS_NAME,"set_gugun_cd_btn")
#내부 스크롤 조정
action = ActionChains(driver)
# 각 버튼의 위치로 스크롤 내려줌
action.move_to_element(gu_btn_list_renew[idx]).perform()
time.sleep(2)
gu_btn_list_renew[idx].click()
#beautifulsoup을 이용해서 데이터 가져오기
req=driver.page_source
soup=BeautifulSoup(req,'html.parser')
# 매장이름/주소/구 정보 추출
info_list_raw=soup.select('.quickResultLstCon')
for info in info_list_raw:
store=re.search("\w+ ", info.text).group() #정규표현식 사용 : 첫번쩨 공백 전까지
store_list.append(store)
address=re.search(" \s\w+[ \S]*(?=1522)", info.text).group() # 정규표현식 사용 : 전화번호 전까지
address_list.append(address)
gu=address.split()[1]
gu_list.append(gu)
driver.quit() #종료
len(gu_list), len(store_list), len(address_list)
(601, 601, 601)
# 데이터 프레임으로 만들기
import pandas as pd
data = {
"구": gu_list,
"주소": address_list,
"매장이름": store_list
}
df_starbucks = pd.DataFrame(data)
df_starbucks
df_starbucks.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 601 entries, 0 to 600
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 구 601 non-null object
1 주소 601 non-null object
2 매장이름 601 non-null object
dtypes: object(3)
memory usage: 14.2+ KB
df_starbucks.to_csv('../data/starbucks_raw_data.csv')
# 셀레니움을 이용해 웹사이트 원격 조종할 준비를 한다.
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
url = 'https://www.ediya.com/contents/find_store.html' #이디야커피 매장 검색 주소
driver=webdriver.Chrome('../driver/chromedriver.exe')
# 화면 최대 크기 설정
driver.maximize_window()
driver.get(url)
search_input=driver.find_element(By.CSS_SELECTOR,'#storename')
action = ActionChains(driver)
action.move_to_element(search_input).perform()
# 검색할 구 리스트
gu_list_uniq=df_starbucks['구'].unique()
gu_list_uniq
array(['중랑구', '강남구', '강동구', '강북구', '강서구', '관악구', '광진구', '구로구', '금천구',
'노원구', '도봉구', '동대문구', '동작구', '마포구', '서대문구', '서초구', '성동구', '성북구',
'송파구', '양천구', '영등포구', '용산구', '은평구', '종로구', '중구'], dtype=object)
address_tab=driver.find_element(By.CSS_SELECTOR,'#contentWrap > div.contents > div > div.store_search_pop > ul > li:nth-child(2) > a')
address_tab.click()
❗ 서울을 붙이지 않고 검색시 중구와 강서구가 검색결과가 너무 많아 에러 발생
from tqdm import tqdm
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup
from selenium.webdriver import ActionChains
gu_list=[] # 구 리스트
address_list=[] # 주소 리스트
store_list=[] # 매장 이름 리스트
# 서울시 이디야 매장 정보 검색 함수
def getEdiyaInfo():
keyword=driver.find_element(By.CSS_SELECTOR,'#keyword')
search_btn=driver.find_element(By.CSS_SELECTOR,'#keyword_div > form > button')
for gu in tqdm(gu_list_uniq):
try:
keyword.send_keys(gu)
# 에러 팝업이 뜰 경우
search_btn.click()
time.sleep(2)
error_popup = driver.switch_to_alert()
# 팝업창 확인
error_popup.accept()
# 팝업창 닫기
error_popup.dismiss()
continue
# 팝업창이 없을 경우 데이터 추출
except:
#beautifulsoup을 이용해서 데이터 가져오기
req=driver.page_source
soup=BeautifulSoup(req,'html.parser')
# 매장이름/주소/구 정보 추출
info_list_raw=soup.select('#placesList > .item')
for info in info_list_raw:
store=re.search("\w+ ", info.text).group() #정규표현식 사용 : 첫번쩨 공백 전까지
store_list.append(store.strip())
address=re.search("\s\w+[ \S]*", info.text).group() # 정규표현식 사용 : 전화번호 전까지
address_list.append(address.lstrip())
gu_list.append(gu)
keyword.clear()
driver.quit()
getEdiyaInfo()
len(gu_list), len(store_list), len(address_list)
(630, 630, 630)
# 데이터 프레임으로 만들기
import pandas as pd
data = {
"구": gu_list,
"주소": address_list,
"매장이름": store_list
}
df_ediya = pd.DataFrame(data)
df_ediya
df_ediya.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 630 entries, 0 to 629
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 구 630 non-null object
1 주소 630 non-null object
2 매장이름 630 non-null object
dtypes: object(3)
memory usage: 14.9+ KB
df_ediya.to_csv('../data/ediya_raw_data.csv')
import pandas as pd
import numpy as np
# 구를 인덱스로 설정
starbucks_list=pd.read_csv('../data/starbucks_raw_data.csv',header=0)
ediya_list=pd.read_csv('../data/ediya_raw_data.csv', header=0)
starbucks_list.tail()
ediya_list.tail()
del ediya_list['Unnamed: 0']
del starbucks_list['Unnamed: 0']
ediya_list.tail()
starbucks_list.tail()
ediya_list = ediya_list[['구','매장이름','주소']]
starbucks_list = starbucks_list[['구','매장이름','주소']]
# 스타벅스
print(len(starbucks_list['구'].unique()))
# 이디야
print(len(ediya_list['구'].unique()))
set(starbucks_list['구'].unique())-set(ediya_list['구'].unique())
{'강서구', '중구'}
❗원래 모든 구 컬럼이 동일하지만 데이터 수집 과정에서의 오류로 이디야 구 컬럼에 강서구와 중구가 제외됨
set(ediya_list['구'].unique())-set(starbucks_list['구'].unique())
set()
starbucks_list_copy=starbucks_list.copy()
ediya_list_copy=ediya_list.copy()
tmp_list=set(starbucks_list_copy['구'].unique())-set(ediya_list_copy["구"].unique())
for tmp in tmp_list:
starbucks_list_copy=starbucks_list_copy.drop(starbucks_list_copy[starbucks_list_copy["구"]==tmp].index)
set(starbucks_list_copy['구'].unique())-set(ediya_list_copy["구"].unique())
starbucks_list_copy.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 522 entries, 0 to 546
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 구 522 non-null object
1 매장이름 522 non-null object
2 주소 522 non-null object
dtypes: object(3)
memory usage: 16.3+ KB
starbucks_list_copy.reset_index(drop=True, inplace=True)
starbucks_list_copy.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 522 entries, 0 to 521
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 구 522 non-null object
1 매장이름 522 non-null object
2 주소 522 non-null object
dtypes: object(3)
memory usage: 12.4+ KB
#geocode
!pip install geopy
from geopy.geocoders import Nominatim
from fake_useragent import UserAgent
from tqdm import tqdm
import re
#구글맵
import googlemaps
gmaps_key='YOUR KEY'
gmaps=googlemaps.Client(key=gmaps_key)
ua = UserAgent()
def getLatLng(data):
for idx, rows in tqdm(data.iterrows()):
try:
tmp=gmaps.geocode(rows['주소'],language='ko')
if len(tmp)>0:
lat=tmp[0]['geometry']['location']['lat']
lng=tmp[0]['geometry']['location']['lng']
else:
road_name=re.search(" \w*로(\w)* (\w)*", rows['주소']).group() # 도로명 주소만 가져옴
tmp=gmaps.geocode(road_name,language='ko')
lat=tmp[0]['geometry']['location']['lat']
lng=tmp[0]['geometry']['location']['lng']
data.loc[idx,'lat']=lat
data.loc[idx,'lng']=lng
except:
print((rows['주소']))
geolocator = Nominatim(user_agent=ua.ie,timeout=10)
coord = geolocator.geocode(road_name.strip())[-1]
data.loc[idx,'lat'] = coord[0]
data.loc[idx,'lng'] = coord[1]
getLatLng(starbucks_list_copy)
starbucks_list_copy.describe()
getLatLng(ediya_list_copy)
ediya_list_copy.d
escribe()
ediya_list_copy[ediya_list_copy['lng']==min(ediya_list_copy['lng'])]
ediya_list_copy.loc[296,'lng']=np.float64(127.060662)
ediya_list_copy.loc[296,'lat']=np.float64(37.5898420)
# 시각화 과정에서 이상치 하나 더 발견
ediya_list_copy.loc[343,'lng']=np.float64(126.933267)
ediya_list_copy.loc[343,'lat']=np.float64(37.556115)
ediya_list_copy.describe()
starbucks_list_copy['카페']='스타벅스'
starbucks_list_copy
ediya_list_copy['카페']='이디야'
ediya_list_copy
concat_data = pd.concat([starbucks_list_copy, ediya_list_copy])
concat_data
concat_data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1152 entries, 0 to 629
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 구 1152 non-null object
1 매장이름 1152 non-null object
2 주소 1152 non-null object
3 lat 1152 non-null float64
4 lng 1152 non-null float64
5 카페 1152 non-null object
dtypes: float64(2), object(4)
memory usage: 63.0+ K
concat_data.reset_index(drop=True, inplace=True)
concat_data
# %load set_matplotlib_hangul
import matplotlib.pyplot as plt
from matplotlib import font_manager, rc
import seaborn as sns
import platform
# %matplotlib inline
get_ipython().run_line_magic("matplotlib","inline")
path='C:/Windows/Fonts/malgun.ttf'
plt.rcParams['axes.unicode_minus'] = False
if platform.system()=="Darwin": #mac
rc("font",family='Arial Unicodes MS')
print('MAC Hangul OK')
elif platform.system()=="Windows": #window
font_name=font_manager.FontProperties(fname=path).get_name()
rc("font",family=font_name)
print('WIndow Hangul OK')
else:
print('Unknown System')
def drawGraph():
plt.figure(figsize=(14,8))
cnt_plot=sns.countplot(data=concat_data, x="구",
order = concat_data["구"].value_counts().index,
hue="카페",
palette='Set1',
)
# x 라벨 각도 조절
cnt_plot.set_xticklabels(cnt_plot.get_xticklabels(), rotation=40,
horizontalalignment='right')
# 라벨명 설정
cnt_plot.set(xlabel='서울시 군/구', ylabel='서울시 매장 개수')
plt.title('서울시 스타벅스/이디야 매장 분포도')
plt.show()
def drawFolium(my_map):
#서울시 경계선 표시
folium.Choropleth(
geo_data=geo_str,
key_on='feature.id',
fill_color='#ffb2ce',
fill_opacity= 0.3,
line_color='#ff5656',
line_weight=3,
show=True,
).add_to(my_map)
# 마커에 이미지 넣기 - 에러 발생해서 제외
# ediya_img = os.path.abspath("../data/ediyalogo.png")
# starbucks_img = os.path.abspath("../data/starbuckslogo.png")
# ediya_icon = CustomIcon(
# ediya_img,
# icon_size=(3, 3),
# )
# starbucks_icon = CustomIcon(
# starbucks_img,
# icon_size=(3, 3),
# )
# 스타벅스
for idx, rows in starbucks_list_copy.iterrows():
circle=folium.CircleMarker(
location=[rows['lat'],rows['lng']],
radius=8,
fill=True,
color='#056C3C',
fill_color='#056C3C',
weight =2,
tooltip=rows['카페'],
icon=starbucks_icon
).add_to(my_map)
#이디야
for idx, rows in ediya_list_copy.iterrows():
folium.CircleMarker(
location = [rows['lat'],rows['lng']],
radius = 8,
fill= True,
color='#243C84',
fill_color='#243C84',
weight = 2,
tooltip=rows['카페'],
).add_to(my_map)
my_map=folium.Map(
location=[37.541, 126.986], #서울시 좌표
zoom_start=12,
tiles='CartoDB positron'
)
drawFolium(my_map)
my_map
# 구글맵을 이용해 서울 구별 위도, 경도 추출
seoul_gu_list=starbucks_list['구'].unique()
seoul_lat_lng={}
for gu in seoul_gu_list:
tmp=gmaps.geocode(gu,language='ko')
lat=tmp[0]['geometry']['location']['lat']
lng=tmp[0]['geometry']['location']['lng']
seoul_lat_lng[gu]=(lat,lng)
seoul_lat_lng
{'중랑구': (37.5978139, 127.0928927),
'강남구': (37.4966645, 127.0629804),
'강동구': (37.5504483, 127.1470117),
'강북구': (37.6434801, 127.0111839),
'강서구': (37.5612346, 126.8228132),
'관악구': (37.4673709, 126.9453359),
'광진구': (37.5467284, 127.0857543),
'구로구': (37.4944134, 126.8563336),
'금천구': (37.4605655, 126.9008183),
'노원구': (37.6525076, 127.075042),
'도봉구': (37.66910650000001, 127.0323527),
'동대문구': (37.5819561, 127.054846),
'동작구': (37.4988794, 126.9516345),
'마포구': (37.5593115, 126.9082589),
'서대문구': (37.5777796, 126.9390623),
'서초구': (37.4732933, 127.0312101),
'성동구': (37.5510171, 127.0410394),
'성북구': (37.6056991, 127.0175664),
'송파구': (37.5056205, 127.1152992),
'양천구': (37.5247402, 126.8553909),
'영등포구': (37.5223245, 126.9101692),
'용산구': (37.5313805, 126.9798839),
'은평구': (37.6191784, 126.9270142),
'종로구': (37.5949159, 126.977339),
'중구': (37.5601443, 126.9959649)}
def setGuPosition(my_map):
for key, value in seoul_lat_lng.items():
folium.map.Marker(
# 위경도 위치
[value[0], value[1]],
# DivIcon 을 이용해 서울시 구 표시
icon=folium.DivIcon(
icon_size=(0, 0),
icon_anchor=(0, 0),
# html 형식으로 text 추가
html='<div\
style="\
font-size: 40;\
color: black;\
width:85px;\
height:15px;\
text-align:center;\
background-color:rgba(255, 255, 255, 0.2);\
margin:3px;\
"><b>'+key+'</b></div>',
)).add_to(my_map)
setGuPosition(my_map)
my_map
스타벅스와 이디야 각 매장의 위치 데이터를 지도에 시각화했을때 두 매장의 위치는 매우 유사해 보이며 아예 같은 위치에 겹쳐 보이는 매장도 있었다.