[EDA 과제] 이디야커피 매장 위치와 스타벅스 매장의 상관관계

·2023년 4월 12일
0

✒️ 개요

1. 스타벅스 매장 위치 데이터 확보

2. 이디야커피 매장 위치 데이터 확보

3. 데이터 전처리

4. 데이터 검증

  • 이상치, 결측치 파악

5. 그래프를 통한 데이터 시각화

  • seaborn

  • folium

6. 결론


*❗제공되는 해설지와 비교하여 다른 부분 표시


문제 1. 서울시 스타벅스 위치 데이터 확보

# 셀레니움을 이용해 웹사이트 원격 조종할 준비를 한다.
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

스타벅스 데이터 정보 확인

  • 모든 데이터에 Null 값 없음
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')

문제 2. 서울시 이디야커피 위치 데이터 확보

# 셀레니움을 이용해 웹사이트 원격 조종할 준비를 한다.
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

이디야 데이터 정보 확인

  • 모든 데이터에 Null 값 없음
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')

문제 3. 이디야 커피와 스타벅스 매장 위치의 상관관계 분석

  • 스타벅스와 이디야커피 데이터 비교


3 데이터 전처리

스타벅스와 이디야커피 데이터 가져오기

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()

Unnamed: 0 컬럼 삭제

del ediya_list['Unnamed: 0']
del starbucks_list['Unnamed: 0']
ediya_list.tail()

starbucks_list.tail()

컬럼 순서 변경

ediya_list = ediya_list[['구','매장이름','주소']]
starbucks_list = starbucks_list[['구','매장이름','주소']]

4 데이터 검증

  • 공통된 구 데이터만 비교하기 위해서 데이터 전처리
  • 이상치, 결측치 파악

컬럼 구 unique 값 비교

# 스타벅스
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

지도 시각화를 위해서 경도와 위도를 추가해준다

  • 구글맵 geocoding 이용
  • 구글맵으로 안나오는 주소는 파이썬 geopy 이용
#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()

이디야 lat, lng 에서 이상치 발견

  • lat max : 41.96
  • lng min : -91.44
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 확인

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


5 그래프를 통한 데이터 시각화

seaborn의 countplot을 통해 살펴본 서울시 스타벅스/이디야 매점 분포도

# %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()
  • 이디야 매장의 수가 스타벅스 보다 많다.

  • 스타벅스 매장은 특정 구(강남구, 서초구) 에 분포되었다.

  • 이디야 매장은 상대적으로 고르게 분포되었다.

Folium을 이용한 데이터 시각화

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

지도에 서울시 구별을 표시

  • 좀더 쉽게 구별가능하게 서울시 구를 지도에 표시한다
  • Folium DivIcon 이용

구글맵을 이용해 서울시 구별 위도, 경도 추출

# 구글맵을 이용해 서울 구별 위도, 경도 추출 
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



결론

이디야 커피의 위치는 스타벅스 매장과 밀접한 상관관계가 있어 보인다.

이유

스타벅스와 이디야 각 매장의 위치 데이터를 지도에 시각화했을때 두 매장의 위치는 매우 유사해 보이며 아예 같은 위치에 겹쳐 보이는 매장도 있었다.

결론적으로 이디야가 전략적으로 스타벅스 매장 주변에 위치한지는 확실히 알 수 없으나 두 매장의 위치는 유사한 것으로 분석됐다



profile
개발하고싶은사람

0개의 댓글