07_Data_Analysis

20_별다방, 서울시 데이터

chuu_travel 2025. 3. 21. 17:56
728x90
별다방, 서울시 데이터를 통해 인구수별 별다방 매장 수를 분석

 

from selenium import webdriver
from seleniuhttp://m.webdriver.common.by import By
from bs4 import BeautifulSoup
from seleniuhttp://m.webdriver.common.keys import Keys
import time
from seleniuhttp://m.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import ElementNotInteractableException
import pandas as pd
import folium
import json

# 통계검정을 위해 import
from scipy import stats

 

 

데이터 수집

별다방 데이터 수집

url = "https://www.starbucks.co.kr/store/store_map.do?disp=locale"
driver = webdriver.Chrome()
driver.get(url)

# 시도 버튼 정보 올 때까지 명시적 대기
WebDriverWait(driver, 3).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "a.set_sido_cd_btn"))
)

# 시도 버튼 리스트
sido = driver.find_elements(By.CSS_SELECTOR, "a.set_sido_cd_btn")

# 시도(서울) 클릭
sido[0].click()

# 구군 명시적 대기
WebDriverWait(driver, 3).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "a.set_gugun_cd_btn"))
)

# 구군 버튼 리스트
gugun = driver.find_elements(By.CSS_SELECTOR, "a.set_gugun_cd_btn")

# 구군 클릭
target = gugun[0]

try:
    target.click()

except ElementNotInteractableException as e:
    driver.execute_script("arguments[0].click();", target)

# 매장 리스트 명시적 대기
WebDriverWait(driver, 3).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "li.quickResultLstCon"))
)

soup = BeautifulSoup(driver.page_source, "lxml")
stores = soup.select("li.quickResultLstCon > strong")

for i in stores:
    print(i.string)
역삼아레나빌딩  
논현역사거리  
신사역성일빌딩  
국기원사거리  
대치재경빌딩  
봉은사역  
압구정윤성빌딩  
코엑스별마당  
삼성역섬유센터R  
압구정R  
수서역R  
양재강남빌딩R  
선릉동신빌딩R  
봉은사로선정릉  
강남오거리  
스타필드코엑스몰R  
...

 

len(stores)
634

 

stores = soup.select("li.quickResultLstCon")
# 매장명
stores[0].select_one("strong").string.strip()
'역삼아레나빌딩'

 

# 위도
stores[0].get("data-lat", "")
'37.501087'

 

# 경도
stores[0].get("data-long", "")
'127.043069'

 

# 매장 타입
stores[0].select_one("i").get("class", "")[0][4:]
'general'

 

# 주소
stores[0].select_one("p.result_details").next_element
'서울특별시 강남구 언주로 425 (역삼동)'

 

# 전화번호
stores[0].select_one("p.result_details").next_element.next_element.next_element
'1522-3232'

 

starbucks = {"매장명" : [],
             "위도" : [],
             "경도" : [],
             "매장타입" : [],
             "주소" : [],
             "전화번호" : []}

for i in stores:
    starbucks["매장명"].append(i.select_one("strong").text.strip())
    starbucks["위도"].append(i.get("data-lat", ""))
    starbucks["경도"].append(i.get("data-long", ""))
    starbucks["매장타입"].append(i.select_one("i").get("class", "")[0][4:])
    starbucks["주소"].append(i.select_one("p.result_details").next_element)
    starbucks["전화번호"].append(i.select_one("p.result_details").next_element.next_element.next_element)
seoul_starbucks_df = pd.DataFrame(starbucks)
seoul_starbucks_df.head()

 

seoul_starbucks_df.shape
(634, 6)

 

seoul_starbucks_df.dtypes
매장명     object
위도      object
경도      object
매장타입    object
주소      object
전화번호    object
dtype: object

 

 

seoul_starbucks_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 634 entries, 0 to 633
Data columns (total 6 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   매장명     634 non-null    object
 1   위도      634 non-null    object
 2   경도      634 non-null    object
 3   매장타입    634 non-null    object
 4   주소      634 non-null    object
 5   전화번호    634 non-null    object
dtypes: object(6)
memory usage: 29.8+ KB

 

seoul_starbucks_df.to_excel("seoul_starbucks.xlsx", index = False)

 

 

 

서울시 데이터

 
행정구역 데이터
sgg_df = pd.read_excel("./data/seoul_sgg_list.xlsx")
sgg_df.head()

 

 

sgg_df.shape
(25, 4)

 

 

인구 데이터

sgg_pop_df = pd.read_csv("./data/report.txt", sep = "\t", header = 2)
sgg_pop_df.head()

 

sgg_pop_df.shape
(26, 14)

 

sgg_pop_df["자치구"].unique()
array(['합계', '종로구', '중구', '용산구', '성동구', '광진구', '동대문구', '중랑구', '성북구',
       '강북구', '도봉구', '노원구', '은평구', '서대문구', '마포구', '양천구', '강서구', '구로구',
       '금천구', '영등포구', '동작구', '관악구', '서초구', '강남구', '송파구', '강동구'],
      dtype=object)

 

sgg_pop_df_selected = sgg_pop_df[sgg_pop_df["자치구"] != "합계"]
sgg_pop_df_selected.head()

 

sgg_pop_df_selected.info()
<class 'pandas.core.frame.DataFrame'>
Index: 25 entries, 1 to 25
Data columns (total 14 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   기간        25 non-null     object 
 1   자치구       25 non-null     object 
 2   세대        25 non-null     object 
 3   계         25 non-null     object 
 4   남자        25 non-null     object 
 5   여자        25 non-null     object 
 6   계.1       25 non-null     object 
 7   남자.1      25 non-null     object 
 8   여자.1      25 non-null     object 
 9   계.2       25 non-null     object 
 10  남자.2      25 non-null     object 
 11  여자.2      25 non-null     object 
 12  세대당인구     25 non-null     float64
 13  65세이상고령자  25 non-null     object 
dtypes: float64(1), object(13)
memory usage: 2.9+ KB

 

 

sgg_pop_df_final = sgg_pop_df_selected[["자치구", "계"]]
sgg_pop_df_final.head()

 

sgg_pop_df_final.columns = ["시군구명", "주민등록인구"]
sgg_pop_df_final.head()

 

 

사업체 데이터

sgg_biz_df = pd.read_csv("./data/report2.txt", sep = "\t", header = 2)
sgg_biz_df.head()

 

  • 시군구별 합계만 필요하기 때문에 "동" 컬럼이 "소계" 인 행만 추출
sgg_biz_df_selected = sgg_biz_df[sgg_biz_df["동"] == "소계"]
sgg_biz_df_selected.head()

 

sgg_biz_df_final = sgg_biz_df_selected[["자치구", "계", "사업체수"]]
sgg_biz_df_final = sgg_biz_df_final.reset_index(drop = True)
sgg_biz_df_final.columns = ["시군구명", "종사자수", "사업체수"]
sgg_biz_df_final.head()

 

sgg_biz_df_final.to_excel("./data/sgg_biz.xlsx", index = False)

 

 

데이터 전처리

seoul_starbucks_df.head()

 

seoul_starbucks_df.loc[0, "주소"].split()[1]
'강남구'

 

seoul_starbucks_df["주소"].map(lambda x: x.split()[1])
0      강남구
1      강남구
2      강남구
3      강남구
4      강남구
      ... 
629    중랑구
630    중랑구
631    중랑구
632    중랑구
633    중랑구
Name: 주소, Length: 634, dtype: object

 

seoul_starbucks_df["시군구명"] = seoul_starbucks_df["주소"].str.split().str[1]
seoul_starbucks_df.head()

 

 

분석 데이터 생성

sgg_df.head()

 

시군구별 매장 수 파악

seoul_starbucks_df.head()

 

starbucks_sgg_count = seoul_starbucks_df.groupby("시군구명")[["매장명"]].count().rename(
    columns = {"매장명" : "스타벅스_매장수"}
)
starbucks_sgg_count.head(10)

 

 

 

서울시 시군구 목록 데이터와 스타벅스 매장 수 데이터를 병합

seoul_sgg = pd.merge(sgg_df, starbucks_sgg_count, how = "left", on = "시군구명")
seoul_sgg.head()

 

 

서울시 시군구 목록 데이터와 인구통계 데이터를 병합

seoul_sgg = pd.merge(seoul_sgg, sgg_pop_df_final, how = "left", on = "시군구명")
seoul_sgg.head()

 

 

서울시 시군구 목록 데이터에 사업체 수 통계 데이터를 병합

seoul_sgg = pd.merge(seoul_sgg, sgg_biz_df_final, how = "left", on = "시군구명")
seoul_sgg.head()

 

seoul_sgg.to_excel("./data/seoul_sgg_stat.xlsx", index = False)

 

 

데이터 시각화

  • 앞에서 정리한 데이터 시각화
    • 스타벅스 매장 위치를 지도에 시각화
      • 스타벅스 매장 분포 확인
      • 일반 매장과 리저브, 드라이브스루 매장 분포의 차이 확인
    • 시군구 단위로 집계한 별다방 매장수/인구수/사업체수를 시각화

 

지도 시각화

seoul_starbucks_df.head()

 

starbucks_map = folium.Map(
    location = [37.55, 126.98],
    tiles = "Cartodb dark_matter",
    zoom_start = 11
)
starbucks_map

 

# 스타벅스 매장 위치 서클 마커 그리기
for idx in seoul_starbucks_df.index:
    lat = seoul_starbucks_df.loc[idx, "위도"]
    lng = seoul_starbucks_df.loc[idx, "경도"]

    folium.CircleMarker(
        location = [lat, lng],
        fill = True,
        fill_color = "green",
        fill_opacity = 1,
        color = "yellow",
        weight = 1,
        radius = 3
    ).add_to(starbucks_map)
starbucks_map

 

 

  • 스타벅스 매장이 대체로 강남권과 중구, 종로구에 많이 분포함
# 스타벅스 매장 타입별 위치 서클 마커 그리기
starbucks_map2 = folium.Map(
    location = [37.55, 126.98],
    tiles = "Cartodb dark_matter",
    zoom_start = 11
)

 

seoul_starbucks_df["매장타입"].unique()
array(['general', 'reserve', 'generalDT', 'generalWT'], dtype=object)

 

for idx in seoul_starbucks_df.index:
    lat = seoul_starbucks_df.loc[idx, "위도"]
    lng = seoul_starbucks_df.loc[idx, "경도"]
    store_type = seoul_starbucks_df.loc[idx, "매장타입"]

    # 매장 타입별 색상 선택을 위한 조건문
    if store_type == "general":
        fill_color = "gray"
        size = 1

    elif store_type == "reserve":
        fill_color = "blue"
        size = 5

    elif store_type == "generalDT":
        fill_color = "red"
        size = 5

    elif store_type == "generalWT":
        fill_color = "green"
        size = 5

    folium.CircleMarker(
        location = [lat, lng],
        fill = True,
        fill_color = fill_color,
        fill_opacity = 1,
        color = fill_color,
        weight = 1,
        radius = size
    ).add_to(starbucks_map2)

 

 

starbucks_map2

 

 

  • 드라이브스루(빨간색) 매장들은 주로 서울 외곽에 위치해 있음
  • 리저브(파란색), 워킹스루(초록색)은 주로 서울 중심지에 분포

 

시군구별 매장 수 시각화

seoul_sgg.head()

 

 

seoul_sgg_geo = json.load(open("./data/seoul_sgg.geojson", encoding = "utf-8"))
seoul_sgg_geo["features"][0]["properties"]
{'SIG_CD': '11320',
 'SIG_KOR_NM': '도봉구',
 'SIG_ENG_NM': 'Dobong-gu',
 'ESRI_PK': 0,
 'SHAPE_AREA': 0.00210990544544,
 'SHAPE_LEN': 0.239901251347}

 

starbucks_bubble = folium.Map(
    location = [37.55, 126.98],
    tiles = "Cartodb dark_matter",
    zoom_start = 11
)
# 서울시 시군구 경계 지도 그리기
def style_function(feature):
    return {
        "opacity" : 0.7,
        "weight" : 1,
        "color" : "white",
        "fillOpacity" : 0,
        "dashArray" : "5, 5"
    }
folium.GeoJson(
    seoul_sgg_geo,
    style_function = style_function
).add_to(starbucks_bubble)
<folium.features.GeoJson at 0x1e1d0595220>

 

 

starbucks_bubble

 

# 서울시 시군구 평균 스타벅스 매장 수 계산
# 평균값보다 많은 스타벅스 매장이 있는 곳은 다르게 표현
starbucks_mean = seoul_sgg["스타벅스_매장수"].mean()
starbucks_mean
25.36

 

for idx in seoul_sgg.index:
    lat = seoul_sgg.loc[idx, "위도"]
    lng = seoul_sgg.loc[idx, "경도"]
    count = seoul_sgg.loc[idx, "스타벅스_매장수"]

    if count > starbucks_mean:
        fill_color = "red"

    else:
        fill_color = "blue"

    folium.CircleMarker(
        location = [lat, lng],
        fill_color = fill_color,
        fill_opacity = 0.7,
        color = "#FFFF00",
        weight = 1.5,
        radius = count/2
    ).add_to(starbucks_bubble)

 

 

starbucks_bubble

 

 

 

서울 시군구별 별다방 매장 수를 단계구분도로 시각화

starbucks_choropleth = folium.Map(
    location = [37.55, 126.98],
    tiles = "Cartodb dark_matter",
    zoom_start = 11
)
folium.Choropleth(
    geo_data = seoul_sgg_geo,
    data = seoul_sgg,
    columns = ["시군구명", "스타벅스_매장수"],
    fill_color = "YlGn",
    fill_opacity = 0.7,
    line_opacity = 0.5,
    key_on = "properties.SIG_KOR_NM"
).add_to(starbucks_choropleth)
<folium.features.Choropleth at 0x1e1d05978f0>
starbucks_choropleth

 

 

별다방 매장 수와 인구 수 비교

  • 가설 : 거주 인구가 많은 지역에 스타벅스 매장이 많이 입지해 있을 것이다
seoul_sgg.head()

 

 

seoul_sgg.dtypes
시군구코드         int64
시군구명         object
위도          float64
경도          float64
스타벅스_매장수      int64
주민등록인구       object
종사자수         object
사업체수         object
dtype: object

 

# 주민등록인구 전처리
seoul_sgg["주민등록인구"] = seoul_sgg["주민등록인구"].str.replace(",", "").astype(int)
seoul_sgg.head()

 

 

seoul_sgg.dtypes
시군구코드         int64
시군구명         object
위도          float64
경도          float64
스타벅스_매장수      int64
주민등록인구        int32
종사자수         object
사업체수         object
dtype: object
starbucks_choropleth = folium.Map(
    location = [37.55, 126.98],
    tiles = "Cartodb dark_matter",
    zoom_start = 11
)

folium.Choropleth(
    geo_data = seoul_sgg_geo,
    data = seoul_sgg,
    columns = ["시군구명", "주민등록인구"],
    fill_color = "YlGn",
    fill_opacity = 0.7,
    line_opacity = 0.5,
    key_on = "properties.SIG_KOR_NM"
).add_to(starbucks_choropleth)
<folium.features.Choropleth at 0x1e1d117c890>

 

 

starbucks_choropleth

 

  • 시군구별 주민등록인구수와 스타벅스 매장 수는 확연히 차이가 있음
    • 주민등록인구수 : 송파구, 강서구, 관악구, 노원구가 높음
 

인구 만 명당 별다방 매장 수 컬럼 추가

  • 시각적으로 확인한 내용을 수치적으로 추가 확인

 

seoul_sgg["만명당_매장수"] = seoul_sgg["스타벅스_매장수"] / (seoul_sgg["주민등록인구"] / 10000)
seoul_sgg.head()

 

viz_map1 = folium.Map(
    location = [37.55, 126.98],
    tiles = "Cartodb dark_matter",
    zoom_start = 11
)

folium.GeoJson(
    seoul_sgg_geo,
    style_function = style_function
).add_to(viz_map1)

# 만 명당 매장 수 기준 상위 10% 추출 값
top = seoul_sgg["만명당_매장수"].quantile(0.9)

 

top
1.5220545466038038

 

 

for idx in seoul_sgg.index:
    lat = seoul_sgg.loc[idx, "위도"]
    lng = seoul_sgg.loc[idx, "경도"]
    r = seoul_sgg.loc[idx, "만명당_매장수"]

    if r > top:
        fill_color = "#FF3300"

    else:
        fill_color = "#CCFF33"

    folium.CircleMarker(
        location = [lat, lng],
        fill_color = fill_color,
        fill_opacity = 0.7,
        color = "#FFFF00",
        weight = 1.5,
        radius = r * 10
    ).add_to(viz_map1)
viz_map1

 

  • 인구 단계구분도에서 인구가 적은 지역으로 표시되었던 중구와 종로구가 1만명당 매장 수가 가장 많은 것으로 표시됨
    • 주민등록인구와 스타벅스 매장 수는 서로 비례하지 않음
  • 가설 "거주 인구가 많은 지역에 스타벅스 매장이 많이 입지해 있을 것이다" 는 틀렸음

 

 

스타벅스 매장 수와 사업체 수 비교

  • 가설 : 직장인이 많은 지역에 스타벅스 매장이 많이 입지해 있을 것이다
  • 가설 검증을 위해 스타벅스 매장 수와 사업체 종사자 수 사이의 관계 분석
seoul_sgg.head()

 

seoul_sgg["종사자수"] = seoul_sgg["종사자수"].str.replace(",", "").astype(int)
# 종사자 1만명당 스타벅스 매장 수
seoul_sgg["종사자수_만명당_매장수"] = seoul_sgg["스타벅스_매장수"] / (seoul_sgg["종사자수"] / 10000)

 

seoul_sgg.head()

 

 

viz_map2 = folium.Map(
    location = [37.55, 126.98],
    tiles = "Cartodb dark_matter",
    zoom_start = 11
)

folium.GeoJson(
    seoul_sgg_geo,
    style_function = style_function
).add_to(viz_map2)

# 종사자수 만 명당 매장 수 기준 상위 10% 추출 값
top = seoul_sgg["종사자수_만명당_매장수"].quantile(0.9)
top
1.6906625790253358

 

 

for idx in seoul_sgg.index:
    lat = seoul_sgg.loc[idx, "위도"]
    lng = seoul_sgg.loc[idx, "경도"]
    r = seoul_sgg.loc[idx, "종사자수_만명당_매장수"]

    if r > top:
        fill_color = "#FF3300"

    else:
        fill_color = "#CCFF33"

    folium.CircleMarker(
        location = [lat, lng],
        fill_color = fill_color,
        fill_opacity = 0.7,
        color = "#FFFF00",
        weight = 1.5,
        radius = r * 10
    ).add_to(viz_map2)

 

viz_map2

 

 

seoul_sgg.sort_values("종사자수_만명당_매장수", ascending = False).head()

 

  • 상위 3개 지역은 서대문구, 은평구, 용산구
  • 상위 3개 지역과 다른 지역의 원 크기가 크게 차이 나지는 않음
    • 사업체 종사자 수와 스타벅스 매장 수는 대체로 비례하는 경향이 있음

 

 

 

통계 검정

 

seoul_sgg[["스타벅스_매장수", "주민등록인구", "종사자수"]].corr()

 

stats.pearsonr(seoul_sgg["스타벅스_매장수"], seoul_sgg["종사자수"])
PearsonRResult(statistic=0.9514251645853266, pvalue=2.944134184975513e-13)
728x90