티스토리 뷰

 

 

전국에는 편의점 점포 수 보다 미용실 점포 수가 더 많다고 한다.

이 말을 듣고 보니, 집 주변 번화가의 건물마다 미용실 하나씩 있는 것 같다.

서울시에 위치한 미용실의 위치와 어떤 체인점이 가장 많은지 알아보면 재밌을 것 같다.

 

1. 데이터 다운로드

서울 점포 수 데이터는 공공데이터 홈페이지에서 가져왔다.

홈페이지에서 상공인시장진흥공단_상가(상권)정보 을 검색하면 csv로 파일을 받을 수 있다.

<데이터 정보>

영업 중인 전국 상가업소 데이터를 제공합니다.
(상호명, 업종코드, 업종명, 지번주소, 도로명주소, 경도, 위도 등)

등록일자 : 2023-10-25

https://www.data.go.kr/

 

2. 데이터 전처리

(1) 준비

라이브러리 세팅 및 한글 깨짐 방지 폰트 설정

# 폰트 설치
!apt-get update -qq
!apt-get install fonts-nanum* -qq
!rm ~/.cache/matplotlib -rf

# 라이브러리 불러오기
import folium
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import matplotlib as mpl
import seaborn as sns

import warnings
warnings.filterwarnings("ignore")

# 폰트 설정 (한글깨짐 방지)
fe = fm.FontEntry(
    fname=r'/usr/share/fonts/truetype/nanum/NanumGothic.ttf', # ttf 파일이 저장되어 있는 경로
    name='NanumGothic')
fm.fontManager.ttflist.insert(0, fe)
plt.rcParams.update({'font.size': 10, 'font.family': 'NanumGothic'})
mpl.rcParams['axes.unicode_minus'] = False
%matplotlib inline

# 드라이브 마운트
from google.colab import drive
drive.mount('/content/drive')

 

 

(2) 데이터 로딩 및 확인

위에서 다운로드 받은 csv 파일을 로딩한다.

df = pd.read_csv('/content/drive/MyDrive/소상공인시장진흥공단_상가(상권)정보_서울.csv')

 

info 함수를 사용하여, 어떠한 컬럼들을 갖고 있는지 확인한다

상호명, 지점명, 위치 등등 필요한 컬럼을 확인한다.

df.columns
>> Index(['상가업소번호', '상호명', '지점명', '상권업종대분류코드', '상권업종대분류명', '상권업종중분류코드',
       '상권업종중분류명', '상권업종소분류코드', '상권업종소분류명', '표준산업분류코드', '표준산업분류명', '시도코드',
       '시도명', '시군구코드', '시군구명', '행정동코드', '행정동명', '법정동코드', '법정동명', '지번코드',
       '대지구분코드', '대지구분명', '지번본번지', '지번부번지', '지번주소', '도로명코드', '도로명', '건물본번지',
       '건물부번지', '건물관리번호', '건물명', '도로명주소', '구우편번호', '신우편번호', '동정보', '층정보',
       '호정보', '경도', '위도'],
      dtype='object')

cols = ['상호명', '지점명', '도로명주소', '시군구명',  '경도', '위도']

 

 

다양하고 많은 상권 정보 중에 미용실만 추출해야한다.

하지만 미용실 업종을 뭐라고 표현했는지 모르기 때문에, 이러한 정보를 줄 것 같은 상권업종중분류, 상권업종소분류 등의 값을 검색해본다.

 

만약 unique함수로 확인이 가능하다면, 바로 원하는 값을 추출하면 된다.

df['상권업종중분류명'].unique()
>> array(['건강/미용식품', '취미/오락관련소매', '시계/귀금속소매', '학원-보습교습입시', '한식', '유흥주점',
       '학원-창업취업취미', '자동차/자동차용품', '부동산중개', '사진/광학/정밀기기소매', '도서관/독서실',
       '커피점/카페', '대중목욕탕/휴게', '주유소/충전소', '패스트푸드', '닭/오리요리', '선물/팬시/기념품',
       '이/미용/건강', '사무/문구/컴퓨터', '중식', '자동차/이륜차', 'PC/오락/당구/볼링등', '의복의류',
       '음/식료품소매', '종합소매점', '세탁/가사서비스', '기타서비스업', '가방/신발/액세서리', '양식',
       '제과제빵떡케익', '무도/유흥/가무', '가정/주방/인테리어', '운동/경기용품소매', '개인서비스',
       '학원-예능취미체육', '학원기타', '가전제품소매', '유아교육', '물품기기대여', '학문교육기타', '기타판매업',
       '사진', '분식', '화장품소매', '애견/애완/동물', '학원-음악미술무용', '대행업',
       '철물/난방/건설자재소매', '별식/퓨전요리', '주택수리', '연극/영화/극장', '모텔/여관/여인숙',
       '호텔/콘도', '의약/의료품소매', '실외운동시설', '학원-어학', '기타음식업', '광고/인쇄',
       '예식/의례/관혼상제', '실내운동시설', '일식/수산물', '책/서적/도서', '가구소매', '운송/배달/택배',
       '요가/단전/마사지', '개인/가정용품수리', '인력/고용/용역알선', '평가/개발/관리', '운영관리시설',
       '법무세무회계', '부페', '중고품소매/교환', '장례/묘지', '유아용품', '분양', '예술품/골동품/수석/분재',
       '페인트/유리제품소매', '행사/이벤트', '스포츠/운동', '음식배달서비스', '놀이/여가/취미', '종교용품판매',
       '경마/경륜/성인오락', '민박/하숙', '학원-자격/국가고시', '학원-컴퓨터', '캠프/별장/펜션',
       '부동산관련서비스', '유스호스텔'], dtype=object)

 

하지만 만약 값이 너무 많고, 키워드로 확인하고 싶다면 ?

Series.str.contains 함수를 사용해서 키워드로 검색한다. 

df.loc[df['상권업종중분류명'].str.contains('미용')]['상권업종중분류명'].unique()
>> array(['건강/미용식품', '이/미용/건강'], dtype=object)

 

원하는 중분류값을 찾았으니, 이제 소분류 값으로 한번 더 필터링 해본다.

df.loc[df['상권업종중분류명'].isin(['이/미용/건강'])]['상권업종소분류명'].unique()
>> array(['비만/피부관리', '여성미용실', '남성미용실', '발/네일케어'], dtype=object)

 

 

미용실 업종만 확인하고 싶으니, 위에서 확인한대로 업종소분류명이 '여성미용실', '남성미용실'인 데이터만 추출한다.

또한, 확실하지 않은 정보를 가진 데이터도 제거한다. (상호명 '명칭없음', '나')

원본 데이터에서 일부 데이터를 추출하였으니, 인덱스 리셋이 필요하다.

그리고 위에서 정의한 필요한 컬럼값들(cols)만 추출한다.

hair_df = df.loc[(df['상권업종소분류명'].isin(['여성미용실', '남성미용실'])) \
                 & (~df['상호명'].isin(['명칭없음', '나']))].reset_index()[cols]

 

hair_df.head()
  상호명 지점명 상권업종중분류명 도로명주소 시군구명 경도 위도
0 뷰티헤어샵 NaN 이/미용/건강 서울특별시 강동구 천호대로168길 46 강동구 127.131526 37.533847
1 정은정헤어샵 NaN 이/미용/건강 서울특별시 광진구 구의로 45 광진구 127.089857 37.541976
2 은숙 NaN 이/미용/건강 서울특별시 노원구 상계로 295-2 노원구 127.078265 37.669915
3 헤어필미용실 NaN 이/미용/건강 서울특별시 강남구 언주로 3 강남구 127.055915 37.476789
4 이봄헤어 NaN 이/미용/건강 서울특별시 양천구 목동중앙남로11길 23 양천구 126.864511 37.539105

 

 

매장수 top 20 바그래프 !

라인이나 준오헤어가 1위인줄 알았으나, 예상외로 태후사랑이 1등이었다.

생전 처음 듣는 태후사랑이 뭔가 했더니.. 천연염색 가맹점이었다 ㅎ

plt.figure(figsize=(15,10))
hair_df['상호명'].value_counts()[:20].sort_values().plot(kind='barh')

 

 

위의 방법처럼 전처리한 데이터프레임에서 카운팅하여 그래프를 만들 수도 있으나

한번 더 전처리하여 그래프를 만들어 보았다.

  (ㄱ) groupby() 로 반환된 DataFrameGroupBy 객체에 해당 칼럼을 필터링한 뒤 aggregation 함수(count) 를 적용한다.

  (ㄴ) count 값을 '매장수' 컬럼으로 재정의한다.

  (ㄷ) '매장수'를 내림차순으로 정렬하고, ignore_index 값을 True로 하여 (ㄴ)에서 재정의하며 생성된 인덱스값이 컬럼에 제외되도록 한다.

counted_hair_df = pd.DataFrame(hair_df.groupby(by=['상호명'])['상호명'].count().reset_index(name='매장수'))
counted_hair_df = counted_hair_df.sort_values(by='매장수', ascending=False, ignore_index=True)
counted_hair_df.head()
  상호명 매장수
0 태후사랑 108
1 블루클럽 68
2 리안 68
3 리안헤어 56
4 오땡큐 45

 

 

음.. 데이터 전체값을 확인해보면, 개인샵 등이 대거 포함되어있다,

서울의 미용실 가맹점 점포수를 확인하고 싶기에, 5개 이상을 가진 상호명들만 추출해야 유의미한 데이터를 볼 수 있을 것 같다.

counted_hair_df.info()
>> 
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 11800 entries, 0 to 11799
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   상호명     11800 non-null  object
 1   매장수     11800 non-null  int64 
dtypes: int64(1), object(1)
memory usage: 184.5+ KB

 

 

오 5개 이상 조건을 추가했더니, 276개로 추려졌다

counted_hair_df = counted_hair_df.loc[counted_hair_df['매장수'] >= 5]

counted_hair_df.info()
>> <class 'pandas.core.frame.DataFrame'>
Int64Index: 276 entries, 0 to 275
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   상호명     276 non-null    object
 1   매장수     276 non-null    int64 
dtypes: int64(1), object(1)
memory usage: 6.5+ KB

 

 

 

3.  Top20 그래프로 시각화하기

top20_hair_df = counted_hair_df[:20]

 

  상호명 매장수
0 태후사랑 108
1 블루클럽 68
2 리안 68
3 리안헤어 56
4 오땡큐 45
5 준오헤어 44
6 제오헤어 39
7 이철헤어커커 34
8 박승철헤어스투디오 33
9 수헤어 32
10 헤어스케치 32
11 머리하는날 29
12 헤어스토리 28
13 나이스가이 27
14 제이헤어 25
15 머리사랑 24
16 헤어갤러리 22
17 코코헤어 21
18 진헤어 20
19 더헤어 20

 

 

그런데 리안과 리안 헤어의 차이는 무엇일까요.. 

흠... 상호명:리안 + 지점명:헤어명일역점 이라면, 상호명: 리안헤어 + 지점명: 명일역점 인 데이터가 잘못 들어간 것 같네요

정부데이터이나 100% 정확하진 않은 것 같습니다 

hair_df.loc[hair_df['상호명'].isin(['리안', '리안헤어'])]
  상호명 지점명 상권업종중분류명 도로명주소 시군구명 경도 위도
505 리안 헤어명일역점 이/미용/건강 서울특별시 강동구 구천면로 405 강동구 127.143539 37.550956
667 리안헤어 NaN 이/미용/건강 서울특별시 서대문구통일로 414 서대문구 126.947035 37.586472
708 리안헤어 관악현대점 이/미용/건강 서울특별시 관악구 은천로 110 관악구 126.947419 37.486684
751 리안 신금호하이리버점 이/미용/건강 서울특별시 성동구 금호로 107 성동구 127.024294 37.552265
858 리안헤어 NaN 이/미용/건강 서울특별시 광진구 아차산로 462 광진구 127.093406 37.539489
... ... ... ... ... ... ... ...
16132 리안 헤어중계은행사거리점 이/미용/건강 서울특별시 노원구 한글비석로 270 노원구 127.077381 37.651563
16138 리안 헤어고덕역점 이/미용/건강 서울특별시 강동구 고덕로 262 강동구 127.154611 37.554658
16164 리안 헤어상일동역2호점 이/미용/건강 서울특별시 강동구 고덕로 399 강동구 127.169655 37.557432
16232 리안헤어 NaN 이/미용/건강 서울특별시 성동구 마장로 293-2 성동구 127.042384 37.566055
16436 리안 헤어마곡점 이/미용/건강 서울특별시 강서구 수명로 82 강서구 126.826424 37.553465

 

 

점포명이 '리안'인 row들을 '리안헤어'로 바꾸어주겠습니다

다시 상호명 기준으로 점포수를 추려냈습니다. 

hair_df.replace(to_replace='리안', value='리안헤어', inplace=True)
counted_hair_df = pd.DataFrame(hair_df.groupby(by=['상호명'])['상호명'].count().reset_index(name='매장수'))
counted_hair_df = counted_hair_df.sort_values(by='매장수', ascending=False, ignore_index=True)
counted_hair_df.head()
  상호명 매장수
0 리안헤어 124
1 태후사랑 108
2 블루클럽 68
3 오땡큐 45
4 준오헤어 44

 

 

위의 오점은 잠시 뒤로 두고, 배웠던 그래프들로 다 그려보면서 어떤 그래프가 가장 효과적일지 알아보겠습니다.

우선 파이차트로 그려보았는데요. 

내림차순으로 브랜드명과 점포수가 잘 보여졌으면 하는데, 파이차트는 그걸 한눈에 보기엔 아쉽네요

plt.figure(figsize=(7,7))
plt.pie(x=top20_hair_df['매장수'], labels=top20_hair_df['상호명'])
plt.title('서울 미용실 체인점 점포수')
plt.show()

 

순차적인 순위와 각 브랜드별로 어느정도의 차이가 존재하는지는 바그래프가 좀 더 시각적으로 효과적인 것 같다.

리안헤어가 서울에서 가장 많은 점포수를 가지고 있다고 볼 수 있겠습니다~

top20_hair_df_reverse = top20_hair_df.sort_values(by=['매장수'])
plt.barh(y=top20_hair_df_reverse['상호명'], width=top20_hair_df_reverse['매장수'])
plt.show()

 

4. 리안헤어 지역구별로 지도에 나타내기

서울시 소재 미용실 점포수 1위 브랜드 '리안헤어'를 지도에 나타내보겠습니다 ~ 

 

리안헤어

riahn_hair_df = hair_df.loc[hair_df['상호명'].isin(['리안헤어'])].reset_index(drop=True)
riahn_hair_df.head()
  상호명 지점명 상권업종중분류명 도로명주소 시군구명 경도 위도
0 리안헤어 헤어명일역점 이/미용/건강 서울특별시 강동구 구천면로405 강동구 127.143539 37.550956
1 리안헤어 NaN 이/미용/건강 서울특별시 서대문구 통일로414 서대문구 126.947035 37.586472
2 리안헤어 관악현대점 이/미용/건강 서울특별시 관악구 은천로110 관악구 126.947419 37.486684
3 리안헤어 신금호하이리버점 이/미용/건강 서울특별시 성동구 금호로107 성동구 127.024294 37.552265
4 리안헤어 NaN 이/미용/건강 서울특별시 광진구 아차산로462 광진구 127.093406 37.539489

 

시군구별 리안헤어 점포수

counted_riahn_hair_df = pd.DataFrame(riahn_hair_df.groupby(by=['시군구명', '상호명'])['상호명'].count().reset_index(name='매장수'))
counted_riahn_hair_df = counted_riahn_hair_df.sort_values(by='매장수', ascending=False, ignore_index=True)
counted_riahn_hair_df
  시군구명 상호명 매장수
0 강서구 리안헤어 12
1 송파구 리안헤어 9
2 동작구 리안헤어 9
3 서초구 리안헤어 9
4 동대문구 리안헤어 8

 

 

지도 세팅하기 

(ㄱ) 우선 지도의 중심점을 잡아줍니다

mean_lat = riahn_hair_df['위도'].mean()
mean_lon = riahn_hair_df['경도'].mean()
m = folium.Map(location=[mean_lat, mean_lon], zoom_start=11)

 

(ㄴ) 지도를 세팅해줍니다

f = folium.Figure(width=1000, height=500)
m.add_to(f)

 

(ㄷ) 서울 미용실이므로 서울 지역구를 구분해줍니다

# 구별 경계선 표시
import requests
import json

# 서울시 행정구역 GeoJSON 파일 로딩
soul_geo = 'https://raw.githubusercontent.com/southkorea/seoul-maps/master/kostat/2013/json/seoul_municipalities_geo_simple.json'
r = requests.get(soul_geo)
seoul_json = json.loads(r.content)

# 지도에 지역구 구분 표시
m.choropleth(geo_data = seoul_json, fill_color = 'white', edge_color='green')

 

 

리안헤어 시군구별 매장수 컬러링 그래프

riahn_map = folium.Map(location=[mean_lat, mean_lon], zoom_start=11)
f = folium.Figure(width=1000, height=500)
riahn_map = riahn_map.add_to(f)
riahn_map.choropleth(geo_data = seoul_json,
                    data = counted_riahn_hair_df,
                    columns = ['시군구명', '매장수'],
                    fill_color = 'YlGn',
                    color='gray',
                    key_on = 'properties.name', # geo_data, data 를 묶어줄 수 있는 공통 데이터값
                    fill_opacity=0.5,
                    line_opacity=0.5,
                    legend_name = '지역구 별 리안헤어 매장 수')

 

리안헤어 매장 위치 마커 표시하기

for idx, row in riahn_hair_df.iterrows():
    folium.Marker(
    	[row['위도'], row['경도']],
      	popup = f'{row["상호명"]} {row["지점명"]}',
	    icon = folium.Icon(color='green'),
    ).add_to(riahn_map)

댓글