밐폭도 @Godtsune_miku
"한 도시의 발전 수준은 (버거킹의 개수+맥도날드의 개수+KFC의 개수)/롯데리아의 개수를 계산하여 높게 나올수록 더 발전된 도시라고 할 수 있다"
https://twitter.com/godtsune_miku/status/513648274406789120
사람이 적은 곳일 수록 롯데리아의 개수가 많은 것 같긴 한데.. 진짜로 그럴려나?
자 그럼..
$\xi=\frac {B+M+K} {L}$ 을 계산해 보자. 시군구 단위로. ($\xi$는 버거의 옆모습을 닮아서..ㅋ)
일단 준비~
import urllib.request
import json
import pandas as pd
import bs4
아주 쓰기 좋게 JSON으로 딱 제공해 준다. 만세 'ㅁ'/
response = urllib.request.urlopen('http://www.burgerking.co.kr/api/store/searchmap/empty/?areacd=')
bgk_data = json.loads(response.read().decode('utf-8'))
bgk_tbl = pd.DataFrame(bgk_data)
bgk_tbl.head()
AllHour | CloseCleaning | ClosePeakSeason | CloseWeekday | Delivery | DriveThrough | Morning | NewAddr | NewAddr2 | OpenTime | PhoneNumber | PointX | PointY | StoreNM | StoreSQ | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | N | 22:00 | N | N | N | 서울특별시 금천구 가산디지털1로 | 168 우림라이온스밸리 A동 | 9:00 | 02-853-0332 | 37.479965 | 126.882637 | 가산디지털점 | 1 | ||
1 | N | / 금, 토: 11:00~24:00 | 22:30 | N | N | N | 서울특별시 금천구 디지털로 | 10길 9 현대아울렛 6층 | 11:00 | 02-2136-9962 | 37.477620 | 126.889053 | 가산현대아울렛점 | 2 | |
2 | N | 23:30 | N | N | N | 서울특별시 강서구 양천로 | 559 이마트3층 (가양동) | 10:00 | 02-3664-0221 | 37.558193 | 126.861816 | 가양이마트점 | 3 | ||
3 | Y | 셋째주 월요일 02:00~08:00 Close | N | N | Y | 서울특별시 서초구 잠원로 | 69 | 02-595-9042 | 37.509400 | 127.007309 | 강남NC점 | 11 | |||
4 | Y | 둘째주 월요일 02:00~08:00 Close | N | N | Y | 서울특별시 서초구 사평대로 | 371 영풍빌딩1층 (반포동) | 02-517-0236 | 37.504570 | 127.023590 | 강남교보점 | 4 |
다른 건 그닥 필요 없고, 주소만 구단위까지 추린다.
bgk_locs = pd.DataFrame(bgk_tbl['NewAddr'].apply(lambda v: v.split()[:2]).tolist(),
columns=('d1', 'd2'))
bgk_locs.head()
d1 | d2 | |
---|---|---|
0 | 서울특별시 | 금천구 |
1 | 서울특별시 | 금천구 |
2 | 서울특별시 | 강서구 |
3 | 서울특별시 | 서초구 |
4 | 서울특별시 | 서초구 |
광역단체 이름이 제대로 돼 있나 확인.
bgk_locs['d1'].unique()
array(['서울특별시', '서울시', '경기도', '수원시', '인천광역시', '강원도', '충청남도', '충남', '대전광역시', '충청북도', '부산광역시', '울산광역시', '대구광역시', '경북', '경상북도', '경남', '경상남도', '전라남도', '전남', '광주광역시', '광주시', '전라북도', '전북', '제주특별자치도'], dtype=object)
줄여 쓴 이름들은 풀어 써준다.
d1_aliases = """서울시:서울특별시 충남:충청남도 강원:강원도 경기:경기도 충북:충청북도 경남:경상남도 경북:경상북도
전남:전라남도 전북:전라북도 제주도:제주특별자치도 제주:제주특별자치도 대전시:대전광역시 대구시:대구광역시 인천시:인천광역시
광주시:광주광역시 울산시:울산광역시"""
d1_aliases = dict(aliasset.split(':') for aliasset in d1_aliases.split())
bgk_locs['d1'] = bgk_locs['d1'].apply(lambda v: d1_aliases.get(v, v))
bgk_locs['d1'].unique()
array(['서울특별시', '경기도', '수원시', '인천광역시', '강원도', '충청남도', '대전광역시', '충청북도', '부산광역시', '울산광역시', '대구광역시', '경상북도', '경상남도', '전라남도', '광주광역시', '전라북도', '제주특별자치도'], dtype=object)
..! 수원시가 있다. =.= 고치자..
bgk_locs[bgk_locs['d1'] == '수원시']
d1 | d2 | |
---|---|---|
101 | 수원시 | 영통구 |
bgk_locs.iloc[101] = ['경기도', '수원시']
시군구 단위는 제대로 돼 있나 보자.
bgk_locs['d2'].unique()
array(['금천구', '강서구', '서초구', '강남구', '광진구', '관악구', '구로구', '용산구', '동작구', '영등포구', '중구', '강동구', '양천구', '송파구', '성북구', '은평구', '마포구', '서대문구', '도봉구', '종로구', '중랑구', '동대문구', '과천시', '광명시', '광주시', '파주시', '부천시', '성남시', '군포시', '수원시', '용인시', '안산시', '여주군', '오산시', '의정부시', '이천시', '고양시', '안양시', '하남시', '남구', '부평구', '연수구', '계양구', '홍천군', '원주시', '당진시', '아산시', '천안시', '서구', '유성구', '청주시', '청원군', '동구', '부산진구', '해운대구', '수성구', '달서구', '북구', '상주시', '경산시', '구미시', '안동시', '포항시', '거제시', '창원시', '김해시', '진주시', '순천시', '여수시', '무안군', '광산구', '군산시', '익산시', '전주시', '제주시'], dtype=object)
오오케이. 대충 잘 돼 있군~
B = bgk_locs.apply(lambda r: r['d1'] + ' ' + r['d2'], axis=1).value_counts()
B.head()
서울특별시 강남구 20 서울특별시 서초구 10 서울특별시 송파구 7 경기도 수원시 7 경기도 성남시 7 dtype: int64
B 계산이 끝났다.
맥도날드는 JSON은 따로 없고 그냥 HTML 파싱을 해야한다. 여러 번 검색한 것을 합치면 겹칠 수 있으니까 점포 이름도 같이 모아서 중첩 검사를 한다.
MCDONALDS_URL = 'http://www.mcdonalds.co.kr/www/kor/findus/district.do?sSearch_yn=Y&skey=2&pageIndex={page}&skeyword={location}'
def search_mcdonalds_stores_one_page(location, page):
response = urllib.request.urlopen(
MCDONALDS_URL.format(location=urllib.parse.quote(location.encode('utf-8')), page=page))
mcd_data = response.read().decode('utf-8')
soup = bs4.BeautifulSoup(mcd_data)
ret = []
for storetag in soup.findAll('dl', attrs={'class': 'clearFix'}):
storename = storetag.findAll('a')[0].contents[-1].strip()
storeaddr = storetag.findAll('dd', attrs={'class': 'road'})[0].contents[0].split(']')[1]
storeaddr_district = storeaddr.split()[:2]
ret.append([storename] + storeaddr_district)
return pd.DataFrame(ret, columns=('store', 'd1', 'd2')) if ret else None
# 여러 페이지를 쭉 찾아서 안 나올 때 까지 합친다.
def search_mcdonalds_stores(location):
from itertools import count
found = []
for pg in count():
foundinpage = search_mcdonalds_stores_one_page(location, pg+1)
if foundinpage is None:
break
found.append(foundinpage)
return pd.concat(found)
search_mcdonalds_stores('전라북도').head()
store | d1 | d2 | |
---|---|---|---|
0 | 군산 나운DT점 | 전라북도 | 군산시 |
1 | 이마트 군산점 | 전라북도 | 군산시 |
2 | 익산영등DT점 | 전라북도 | 익산시 |
3 | 익산원광대점 | 전라북도 | 익산시 |
4 | 전주덕진DT점 | 전라북도 | 전주시 |
이제 전체 지역에 대해서 찾아서 모두 합침. 시도명은 버거킹 목록을 이용.
found = []
for distr in bgk_locs['d1'].unique():
found.append(search_mcdonalds_stores(distr))
mcd_tbl = pd.concat(found)
mcd_tbl['store'].value_counts().head()
양주휴게소DT 2 가산디지털점 1 대구희망DT점 1 대동점 1 양재SK DT점 1 dtype: int64
양주휴게소DT가 두 번 들어가 있다. (지도 확인해 보면 완전히 같은 위치에 있음) 제거하자.!
mcd_tbl = mcd_tbl.drop_duplicates(subset=['store'])
M = mcd_tbl.apply(lambda r: r['d1'] + ' ' + r['d2'], axis=1).value_counts()
M.head()
경기도 수원시 13 경상남도 창원시 12 서울특별시 강남구 12 대구광역시 달서구 9 경기도 고양시 9 dtype: int64
오! 버거킹과 달리 맥도날드는 수원이 가장 많다. 창원도 눈에 띄게 많다.
KFC는 시도를 먼저 선택하고 군구를 선택해서 검색한다.
kfc_dists = "강원 경기 경남 경북 광주 대구 대전 부산 서울 울산 인천 전남 전북 제주 충남 충북".split()
KFC_DISTSEARCH_URL = 'http://www.kfckorea.com/store/store_addr_search.asp?addr_div=gugun&sido={location}'
def kfc_search_subdists(location):
response = urllib.request.urlopen(
KFC_DISTSEARCH_URL.format(location=urllib.parse.quote(location.encode('utf-8'))))
kfc_data = response.read().decode('utf-8')
soup = bs4.BeautifulSoup(kfc_data)
return list(filter(None, [tag.attrs['value'] for tag in soup.findAll('option')]))
kfc_alldist = [(d, subd) for d in kfc_dists for subd in kfc_search_subdists(d)]
kfc_alldist[:5], len(kfc_alldist)
([('강원', '강릉시'), ('강원', '고성군'), ('강원', '동해시'), ('강원', '삼척시'), ('강원', '속초시')], 251)
다행히도 그 다음 검색은 JSON 리턴이다! 'o'/
KFC_STORESEARCH_URL = ('http://www.kfckorea.com/store/store_search.asp?sales_24_yn_=&'
'sales_wifi_yn_=&sales_order_group_yn_=&sales_park_yn_=&sales_subway_yn_=&'
'sales_mart_in_yn_=&searchFlag=0&addr_div1={div1}&addr_div2={div2}&keyword=')
def kfc_search_stores_in_dist(d1, d2):
response = urllib.request.urlopen(
KFC_STORESEARCH_URL.format(div1=urllib.parse.quote(d1.encode('utf-8')),
div2=urllib.parse.quote(d2.encode('utf-8'))))
return json.loads(response.read().decode('utf-8'))['store']
found = []
for d1, d2 in kfc_alldist:
found.extend(kfc_search_stores_in_dist(d1, d2))
kfc_tbl = pd.DataFrame(found)
kfc_tbl.head()
addr_div1 | addr_div2 | clean_day | map_url1 | map_url2 | new_addr1 | old_addr1 | phone | post_no | sales_time | shop_code | shop_location | shop_name | shop_seq | shop_use_div | use_area | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 강원 | 강원 | 37.76419798981757 | 128.87763186572403 | 강원도 강릉시 솔올로 25 현진빌딩 1층 (교동) | 강원도 강릉시 교동 1882-3 현진빌딩 1층 | 033)642-6332 | 210-100 | 10:00 ~ 22:00 | 1410723 | 교동주공 3단지 308동 옆 | 강릉교동 | 162 | Y | A | |
1 | 강원 | 강원 | 37.52217245343172 | 129.11533830315352 | 강원도 동해시 한섬로 111-7, 현진롯데시네마타운107호~108호 (천곡동) | 강원 동해시 천곡동 863번지 현진롯데시네마타운 107호~108호 | 033)531-4956 | 240-812 | 10:00 ~ 21:30 | 1410680 | 시청로터리 롯데시네마 1층 | 동해 | 8 | Y | A | |
2 | 강원 | 강원 | 10:00~22:00 | 37.2039550 | 128.8370300 | 강원도 고한읍 고한리 산289-6 하이원리조트 밸리스키 하우스 | 강원 정선군 고한읍 고한리 산289-6 하이원리조트 밸리스키 하우스 3층 | 033)591-9749 | 233-901 | 10:00~22:00 (11월 23 일 개점, 겨울시즌 영업중) | 1410696 | 하이원리조트 벨리스키하우스 3층 | 하이원리조트 | 29 | Y | A |
3 | 강원 | 강원 | 37.863814444176135 | 127.71834104991627 | 강원도 춘천시 경춘로 2341 이마트 춘천점 1층 푸드코트 內 | 강원도 춘천시 온의동 511번지 이마트 춘천점 1층 푸드코트 內 | 033)252-3369 | 200-938 | 10:00~22:00 | 1410714 | 이마트 춘천점 1층 푸드코트 내 | 춘천이마트 | 152 | Y | A | |
4 | 강원 | 강원 | 37.87899033651872 | 127.72646272309694 | 강원도 춘천시 중앙로67번길 4 (중앙로2가) | 강원 춘천시 중앙로2가 31 | 033)243-9646 | 200-042 | (주중) 11:00~21:00 / (주말) 10:00~22:00 | 1410621 | 명동 닭갈비골목 인근 국민은행 맞은편 | 춘천1 | 95 | Y | A |
간단히 완성! 이제 시군구까지 점포 수 센다.
kfc_locs = pd.DataFrame(kfc_tbl['old_addr1'].apply(
lambda v: v.replace(' ', ' ').replace(' ', ' ').replace('광주 광역', '광주광역').split()[:2]).tolist(),
columns=('d1', 'd2'))
kfc_locs['d1'].unique()
array(['강원도', '강원', '경기', '경기도', '경남', '경북', '광주광역시', '광주', '대구광역시', '대구', '대전시', '대전', '대전광역시', '부산시', '부산광역시', '부산', '서울특별시', '서울시', '서울', '울산', '인천시', '인천', '인천광역시', '전북', '전라북도', '제주', '충남', '충청북도', '충북'], dtype=object)
아.. KFC도 버거킹처럼 줄여 쓴 주소와 풀어 쓴 주소가 섞여있다.
d1_aliases = """서울시:서울특별시 충남:충청남도 강원:강원도 경기:경기도 충북:충청북도 경남:경상남도 경북:경상북도
전남:전라남도 전북:전라북도 제주도:제주특별자치도 제주:제주특별자치도 대전시:대전광역시 대구시:대구광역시 인천시:인천광역시
광주시:광주광역시 울산시:울산광역시 광주:광주광역시 대구:대구광역시 대전:대전광역시 부산:부산광역시 부산시:부산광역시
인천:인천광역시 서울:서울특별시 울산:울산광역시"""
d1_aliases = dict(aliasset.split(':') for aliasset in d1_aliases.split())
kfc_locs['d1'] = kfc_locs['d1'].apply(lambda v: d1_aliases.get(v, v))
kfc_locs['d1'].unique()
array(['강원도', '경기도', '경상남도', '경상북도', '광주광역시', '대구광역시', '대전광역시', '부산광역시', '서울특별시', '울산광역시', '인천광역시', '전라북도', '제주특별자치도', '충청남도', '충청북도'], dtype=object)
kfc_locs['d2'].unique()
array(['강릉시', '동해시', '정선군', '춘천시', '고양시', '과천시', '광명시', '구리시', '김포시', '남양주시', '부천시', '성남시', '수원시', '안산시', '안양시', '오산시', '용인시', '의정부시', '이천시', '평택시', '하남시', '화성시', '거제시', '김해시', '양산시', '창원시', '경산시', '포항시', '서구', '남구', '달서구', '북구', '수성구', '중구', '유성구', '금정구', '기장군', '동래구', '부산진구', '사상구', '해운대구', '강남구', '강동구', '강북구', '강서구', '관악구', '광진구', '구로구', '금천구', '노원구', '도봉구', '동대문구', '동작구', '마포구', '서대문구', '서초구', '성동구', '성북구', '송파구', '양천구', '영등포구', '용산구', '종로구', '중랑구', '계양구', '남동구', '부평구', '연수구', '전주시', '서귀포시', '천안시', '청주시'], dtype=object)
다행히도 시군구는 줄여쓰거나 오타내거나 그런 경우가 없다.
K = kfc_locs.apply(lambda r: r['d1'] + ' ' + r['d2'], axis=1).value_counts()
K.head()
서울특별시 강남구 10 서울특별시 서초구 7 경기도 수원시 7 경기도 성남시 5 서울특별시 용산구 5 dtype: int64
이제 롯데리아를 해야 하는데.. 계속 하기는 좀 지루하니까 B, M, K만 테이블로 모아 보자.
BMK = pd.DataFrame({'B': B, 'M': M, 'K': K}).fillna(0)
BMK['total'] = BMK.sum(axis=1)
BMK = BMK.sort('total', ascending=False)
BMK.head(10)
B | K | M | total | |
---|---|---|---|---|
서울특별시 강남구 | 20 | 10 | 12 | 42 |
경기도 수원시 | 7 | 7 | 13 | 27 |
서울특별시 서초구 | 10 | 7 | 7 | 24 |
경기도 성남시 | 7 | 5 | 8 | 20 |
충청북도 청주시 | 5 | 5 | 8 | 18 |
경상남도 창원시 | 3 | 3 | 12 | 18 |
경기도 고양시 | 4 | 3 | 9 | 16 |
서울특별시 송파구 | 7 | 4 | 5 | 16 |
대구광역시 달서구 | 2 | 3 | 9 | 14 |
서울특별시 강동구 | 3 | 4 | 7 | 14 |
from matplotlib import pyplot as plt
from matplotlib import rcParams, style
style.use('ggplot')
rcParams['font.size'] = 12
일단은 매장 수 비교.
plt.figure(figsize=(4, 3))
BMK.sum(axis=0).iloc[:3].plot(kind='bar')
<matplotlib.axes._subplots.AxesSubplot at 0x7f2c5a150048>
맥도날드가 많고, 버거킹, KFC는 비슷하다.
import scipy.stats
서로서로 매장수 비교를 해 볼까~
fig = plt.figure(figsize=(9, 3))
def plot_nstores(b1, b2, label1, label2):
plt.scatter(BMK[b1] + np.random.random(len(BMK)),
BMK[b2] + np.random.random(len(BMK)),
edgecolor='none', alpha=0.75, s=6, c='black')
plt.xlim(-1, 15)
plt.ylim(-1, 15)
plt.xlabel(label1)
plt.ylabel(label2)
r = scipy.stats.pearsonr(BMK[b1], BMK[b2])
plt.annotate('r={:.3f}'.format(r[0]), (10, 12.5))
ax = fig.add_subplot(1, 3, 1)
plot_nstores('B', 'M', 'Burger King', "McDonald's")
ax = fig.add_subplot(1, 3, 2)
plot_nstores('B', 'K', 'Burger King', 'KFC')
ax = fig.add_subplot(1, 3, 3)
plot_nstores('M', 'K', "McDonald's", 'KFC')
plt.tight_layout()
버거킹과 KFC 매장수가 상당히 연관성이 높아서, 매장 배치 전략이 비슷한 것 같다. 버거킹과 맥도날드는 이 셋중에 가장 다르다. 맥도날드의 DT매장들 배치를 보면 이해가 간다.
이 그림을 보면, 버거킹은 일부 지역에 굉장히 집중하는 것처럼 보이는데, 집중도를 비교해 보자.
plt.figure(figsize=(4, 3))
for col, label in [('B', 'Burger King'), ('K', 'KFC'), ('M', "McDonald's")]:
cumulv = np.cumsum(sorted(BMK[col], reverse=True)) / BMK[col].sum()
plt.plot(cumulv, label='{} ({})'.format(label, int(BMK[col].sum())))
plt.legend(loc='best')
plt.xlabel('Number of districts (si/gun/gu)')
plt.ylabel('Cumulative fraction')
<matplotlib.text.Text at 0x7f2c58190e10>
맥도날드가 가장 매장이 고루 흩어져 있지만, 이건 매장 개수가 많은 것으로 해석할 수도 있어서, 결론을 내리자면 좀 더 자세히 봐야한다. KFC와 버거킹은 매장수가 거의 같으니, 버거킹이 좀 더 일부 시군구에 집중되어 있음을 볼 수 있다.
LOTTERIA_URL = 'http://www.lotteria.com/Shop/Shop_Ajax.asp'
LOTTERIA_VALUES = {
'Page': 1, 'PageSize': 2000, 'BlockSize': 2000,
'SearchArea1': '', 'SearchArea2': '', 'SearchType': "TEXT",
'SearchText': '', 'SearchIs24H': '', 'SearchIsWifi': '',
'SearchIsDT': '', 'SearchIsHomeService': '', 'SearchIsGroupOrder': '',
'SearchIsEvent': ''}
LOTTERIA_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:12.0) Gecko/20100101',
'Host': 'www.lotteria.com',
'Accept': 'text/html, */*; q=0.01',
'Accept-Language': 'en-us,en;q=0.5',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'Referer': 'http://www.lotteria.com/Shop/Shop_List.asp?Page=1&PageSize=2000&BlockSize=2000&Se'
'archArea1=&SearchArea2=&SearchType=TEXT&SearchText=&SearchIs24H=&SearchIsWifi=&Se'
'archIsDT=&SearchIsHomeService=&SearchIsGroupOrder=&SearchIsEvent=',
}
postdata = urllib.parse.urlencode(LOTTERIA_VALUES).encode('utf-8')
req = urllib.request.Request(LOTTERIA_URL, postdata, LOTTERIA_HEADERS)
response = urllib.request.urlopen(req)
ltr_data = response.read().decode('utf-8')
soup = bs4.BeautifulSoup(ltr_data)
found = []
for tag in soup.findAll('tr', {'class': 'shopSearch'}):
subtag = [tag.findAll('td', {'style': 'padding-right:10px;'}
)[i].contents[0].contents[0]
for i in (0, 1)]
found.append([subtag[0]] + subtag[1].replace('광주 광역', '광주광역').split()[:2])
ltr_tbl = pd.DataFrame(found, columns=('storename', 'd1', 'd2'))
ltr_tbl.head()
storename | d1 | d2 | |
---|---|---|---|
0 | 서울대 | 서울 | 관악구 |
1 | 센텀프라자빌딩 | 부산 | 해운대구 |
2 | 광주농성D/T | 광주 | 서구 |
3 | 김포구래 | 경기 | 김포시 |
4 | 롯데아울렛동부산1층 | 부산 | 기장군 |
ltr_tbl['d1'].unique()
array(['서울', '부산', '광주', '경기', '강원', '충북', '전남', '경북', '대구', '세종', '경남', '충남', '대전', '울산', '제주', '인천', '전북', '대전시', '광주광역시', '충남시', '청북'], dtype=object)
충남시, 청북 (..) 고치자.
d1_aliases = """강원:강원도 충북:충청북도 부산:부산광역시 경기:경기도 전남:전라남도 경북:경상북도
대구:대구광역시 서울:서울특별시 세종:세종특별자치시 경남:경상남도 충남:충청남도 대전:대전광역시
울산:울산광역시 제주:제주특별자치도 인천:인천광역시 전북:전라북도 광주:광주광역시 대전시:대전광역시
충남시:충청남도 청북:충청북도"""
d1_aliases = dict(aliasset.split(':') for aliasset in d1_aliases.split())
ltr_tbl['d1'] = ltr_tbl['d1'].apply(lambda v: d1_aliases.get(v, v))
ltr_tbl['d1'].unique()
array(['서울특별시', '부산광역시', '광주광역시', '경기도', '강원도', '충청북도', '전라남도', '경상북도', '대구광역시', '세종특별자치시', '경상남도', '충청남도', '대전광역시', '울산광역시', '제주특별자치도', '인천광역시', '전라북도'], dtype=object)
자.. 이제 시군구 체크. 제발.. >_<
ltr_tbl['d2'].unique()
array(['관악구', '해운대구', '서구', '김포시', '기장군', '원주시', '청주시', '가평군', '나주시금천면', '안산시', '안동시', '여수시', '장성군장성읍', '수성구', '사상구', '강남구', '봉화군', '광명시', '연기면', '하동군', '수원시', '이천시', '금남면', '거제시', '천안시', '광주시', '동구', '중구', '성북구', '고양시', '양평군', '제주시', '송파구', '김천시', '고령군', '전주시', '남동구', '수영구', '창원시', '양산시', '남양주시', '강릉시', '연제구', '구미시', '고흥군고흥읍', '진주시', '용인시', '마포구', '순창군', '영등포구', '부천시', '북구', '성남시', '남구', '김해시', '달서구', '논산시연무읍', '사하구', '평택시', '영주시', '임실군임실읍', '기장군정관면', '유성구', '시흥시', '익산시', '창녕군남지읍', '서귀포시', '광진구', '용산구', '당진시', '강동구', '영천시', '완주군', '순천시', '화성시', '담양군', '부산진구', '함안군', '강북구', '연수구', '음성군', '서산시', '부안군', '구로구', '의정부시', '성주군', '은평구', '평창군', '중랑구', '아산시', '화천군', '하남시', '문경시', '안양시', '강진군', '경산시', '부여군', '양주시', '성동구', '장흥군', '군산시', '달성군', '경주시', '오산시', '대덕구', '포항시', '도봉구', '포천시', '계양구', '여주시', '동두천시', '부평구', '동작구', '목포시', '춘천시', '양천구', '종로구', '진구', '단양군', '속초시', '남원시', '금천구', '정선군', '동해시', '무안군', '노원구', '과천시', '광산구', '철원군', '무주군', '괴산군', '강서구', '파주시', '충주시', '영광군', '서초구', '청양군', '동래구', '광양시', '진천군', '아산신', '구리시', '안성시', '청원군', '인제군', '완도군', '금정구', '제천시', '공주시', '밀양시', '함양군', '영암군', '동대문구', '울주군', '서대문구', '군포시', '통영시', '의왕시', '을주군', '구로', '화순군', '정읍시', '원주시지정면', '태백시', '조치원읍', '영덕군', '칠곡군', '계룡시', '보령시', '논산시', '고창군', '서천군', '영월군', '청도군', '횡성군', '예천군', '옥천군', '양구군', '합천군', '종로2가', '창녕군', '사천시', '강화군', '울진군', '삼척시', '해남군', '고성군', '태안군', '연천군', '나주시', '남해군', '금산군', '홍천군', '영동군', '증평군', '영도구', '김제시', '홍성군', '거창군', '예산군', '보은군', '상주시'], dtype=object)
띄어쓰기 빠진 것이 제법 있다. 따로 따로 분리해주자.
d2_aliases = """나주시금천면:나주시 장성군장성읍:장성군 고흥군고흥읍:고흥군 기장군정관면:기장군
창녕군남지읍:창녕군 임실군임실읍:임실군 원주시지정면:원주시 진구:부산진구 논산시연무읍:논산시"""
d2_aliases = dict(aliasset.split(':') for aliasset in d2_aliases.split())
ltr_tbl['d2'] = ltr_tbl['d2'].apply(lambda v: d2_aliases.get(v, v))
ltr_tbl['d2'].unique()
array(['관악구', '해운대구', '서구', '김포시', '기장군', '원주시', '청주시', '가평군', '나주시', '안산시', '안동시', '여수시', '장성군', '수성구', '사상구', '강남구', '봉화군', '광명시', '연기면', '하동군', '수원시', '이천시', '금남면', '거제시', '천안시', '광주시', '동구', '중구', '성북구', '고양시', '양평군', '제주시', '송파구', '김천시', '고령군', '전주시', '남동구', '수영구', '창원시', '양산시', '남양주시', '강릉시', '연제구', '구미시', '고흥군', '진주시', '용인시', '마포구', '순창군', '영등포구', '부천시', '북구', '성남시', '남구', '김해시', '달서구', '논산시', '사하구', '평택시', '영주시', '임실군', '유성구', '시흥시', '익산시', '창녕군', '서귀포시', '광진구', '용산구', '당진시', '강동구', '영천시', '완주군', '순천시', '화성시', '담양군', '부산진구', '함안군', '강북구', '연수구', '음성군', '서산시', '부안군', '구로구', '의정부시', '성주군', '은평구', '평창군', '중랑구', '아산시', '화천군', '하남시', '문경시', '안양시', '강진군', '경산시', '부여군', '양주시', '성동구', '장흥군', '군산시', '달성군', '경주시', '오산시', '대덕구', '포항시', '도봉구', '포천시', '계양구', '여주시', '동두천시', '부평구', '동작구', '목포시', '춘천시', '양천구', '종로구', '단양군', '속초시', '남원시', '금천구', '정선군', '동해시', '무안군', '노원구', '과천시', '광산구', '철원군', '무주군', '괴산군', '강서구', '파주시', '충주시', '영광군', '서초구', '청양군', '동래구', '광양시', '진천군', '아산신', '구리시', '안성시', '청원군', '인제군', '완도군', '금정구', '제천시', '공주시', '밀양시', '함양군', '영암군', '동대문구', '울주군', '서대문구', '군포시', '통영시', '의왕시', '을주군', '구로', '화순군', '정읍시', '태백시', '조치원읍', '영덕군', '칠곡군', '계룡시', '보령시', '고창군', '서천군', '영월군', '청도군', '횡성군', '예천군', '옥천군', '양구군', '합천군', '종로2가', '사천시', '강화군', '울진군', '삼척시', '해남군', '고성군', '태안군', '연천군', '남해군', '금산군', '홍천군', '영동군', '증평군', '영도구', '김제시', '홍성군', '거창군', '예산군', '보은군', '상주시'], dtype=object)
아.. 역시 매장이 많아서 오류도 많다. 다시 확인.
ltr_tbl[ltr_tbl['d2'].apply(lambda v: v[-1] not in '시군구')]
storename | d1 | d2 | |
---|---|---|---|
19 | 세종첫마을 | 세종특별자치시 | 연기면 |
23 | 홈플러스세종 | 세종특별자치시 | 금남면 |
409 | 아산탕정 | 충청남도 | 아산신 |
547 | 오류동역 | 서울특별시 | 구로 |
662 | 홈플러스조치원 | 세종특별자치시 | 조치원읍 |
739 | 조치원 | 세종특별자치시 | 조치원읍 |
879 | 종각역 | 서울특별시 | 종로2가 |
945 | 개봉역 | 서울특별시 | 구로 |
오류는 고치고, 세종는 시군구 단위가 없으므로 세종시로 지정.
d2_aliases = """연기면:세종시 금남면:세종시 조치원읍:세종시 아산신:아산시 구로:구로구
종로2가:종로구"""
d2_aliases = dict(aliasset.split(':') for aliasset in d2_aliases.split())
ltr_tbl['d2'] = ltr_tbl['d2'].apply(lambda v: d2_aliases.get(v, v))
ltr_tbl['d2'].unique()
array(['관악구', '해운대구', '서구', '김포시', '기장군', '원주시', '청주시', '가평군', '나주시', '안산시', '안동시', '여수시', '장성군', '수성구', '사상구', '강남구', '봉화군', '광명시', '세종시', '하동군', '수원시', '이천시', '거제시', '천안시', '광주시', '동구', '중구', '성북구', '고양시', '양평군', '제주시', '송파구', '김천시', '고령군', '전주시', '남동구', '수영구', '창원시', '양산시', '남양주시', '강릉시', '연제구', '구미시', '고흥군', '진주시', '용인시', '마포구', '순창군', '영등포구', '부천시', '북구', '성남시', '남구', '김해시', '달서구', '논산시', '사하구', '평택시', '영주시', '임실군', '유성구', '시흥시', '익산시', '창녕군', '서귀포시', '광진구', '용산구', '당진시', '강동구', '영천시', '완주군', '순천시', '화성시', '담양군', '부산진구', '함안군', '강북구', '연수구', '음성군', '서산시', '부안군', '구로구', '의정부시', '성주군', '은평구', '평창군', '중랑구', '아산시', '화천군', '하남시', '문경시', '안양시', '강진군', '경산시', '부여군', '양주시', '성동구', '장흥군', '군산시', '달성군', '경주시', '오산시', '대덕구', '포항시', '도봉구', '포천시', '계양구', '여주시', '동두천시', '부평구', '동작구', '목포시', '춘천시', '양천구', '종로구', '단양군', '속초시', '남원시', '금천구', '정선군', '동해시', '무안군', '노원구', '과천시', '광산구', '철원군', '무주군', '괴산군', '강서구', '파주시', '충주시', '영광군', '서초구', '청양군', '동래구', '광양시', '진천군', '구리시', '안성시', '청원군', '인제군', '완도군', '금정구', '제천시', '공주시', '밀양시', '함양군', '영암군', '동대문구', '울주군', '서대문구', '군포시', '통영시', '의왕시', '을주군', '화순군', '정읍시', '태백시', '영덕군', '칠곡군', '계룡시', '보령시', '고창군', '서천군', '영월군', '청도군', '횡성군', '예천군', '옥천군', '양구군', '합천군', '사천시', '강화군', '울진군', '삼척시', '해남군', '고성군', '태안군', '연천군', '남해군', '금산군', '홍천군', '영동군', '증평군', '영도구', '김제시', '홍성군', '거창군', '예산군', '보은군', '상주시'], dtype=object)
됐다. 이제 마지막 시도별 매장 수 계산!
L = ltr_tbl.apply(lambda r: r['d1'] + ' ' + r['d2'], axis=1).value_counts()
L.head()
경상남도 창원시 29 경기도 수원시 28 충청북도 청주시 22 경기도 고양시 22 충청남도 천안시 22 dtype: int64
오. 롯데리아는 역시 강남이 상위권에 없다. 천안과 청주가 앞으로 튀어올랐다.
distr_latlon = pd.read_table('../../../../p/tiny/2014-12/burgerindex/latlon/lonlat.csv')
distr_latlon.head()
d1 | d2 | area | population | density | lat | lon | |
---|---|---|---|---|---|---|---|
0 | 경기도 | 가평군 | 843.04 | 58540 | 69.439173 | 37.831540 | 127.509883 |
1 | 서울특별시 | 강남구 | 39.50 | 569499 | 14417.696203 | 37.517236 | 127.047325 |
2 | 서울특별시 | 강동구 | 24.60 | 489655 | 19904.674797 | 37.530125 | 127.123762 |
3 | 강원도 | 강릉시 | 1040.07 | 219067 | 210.627169 | 37.751853 | 128.876057 |
4 | 서울특별시 | 강북구 | 23.60 | 343912 | 14572.542373 | 37.639610 | 127.025657 |
distr_latlon.index = distr_latlon.apply(lambda r: r['d1'] + ' ' + r['d2'], axis=1)
bgt = pd.DataFrame({'B': B, 'M': M, 'K': K, 'L': L}).fillna(0)
bgt = pd.merge(distr_latlon, bgt, how='outer', left_index=True, right_index=True)
bgt.head()
d1 | d2 | area | population | density | lat | lon | B | K | L | M | |
---|---|---|---|---|---|---|---|---|---|---|---|
강원도 강릉시 | 강원도 | 강릉시 | 1040.07 | 219067 | 210.627169 | 37.751853 | 128.876057 | 0 | 1 | 6 | 1 |
강원도 고성군 | 강원도 | 고성군 | 664.19 | 30802 | 46.375284 | 38.380129 | 128.467439 | NaN | NaN | NaN | NaN |
강원도 동해시 | 강원도 | 동해시 | 180.01 | 95850 | 532.470418 | 37.524719 | 129.114292 | 0 | 1 | 3 | 0 |
강원도 삼척시 | 강원도 | 삼척시 | 1185.80 | 72431 | 61.081970 | 37.449868 | 129.165206 | 0 | 0 | 1 | 0 |
강원도 속초시 | 강원도 | 속초시 | 105.25 | 84568 | 803.496437 | 38.207015 | 128.591849 | 0 | 0 | 3 | 1 |
행정구역 정보가 없는데 버거 매장은 있는 지역을 찾아 봄.
bgt[np.isnan(bgt['area'])]
d1 | d2 | area | population | density | lat | lon | B | K | L | M | |
---|---|---|---|---|---|---|---|---|---|---|---|
경기도 여주군 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 0 | 0 | 0 |
울산광역시 을주군 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0 | 0 | 1 | 0 |
충청북도 천안시 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0 | 0 | 2 | 0 |
충청북도 청원군 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 1 | 0 | 3 | 0 |
흠.. 이름이 잘못 기록되거나 바뀐 것이 제법 있다. 고쳐주자.
bgidx_cols = ['B', 'K', 'L', 'M']
bgt.loc['경기도 여주시', bgidx_cols] += bgt.loc['경기도 여주군', bgidx_cols]
bgt.loc['울산광역시 울주군', bgidx_cols] += bgt.loc['울산광역시 을주군', bgidx_cols]
bgt.loc['충청남도 천안시', bgidx_cols] += bgt.loc['충청북도 천안시', bgidx_cols]
bgt.loc['충청북도 청주시', bgidx_cols] += bgt.loc['충청북도 청원군', bgidx_cols] # 2014년 7월 1일 통합.
bgt = bgt[~np.isnan(bgt['area'])].fillna(0)
bgt.head()
d1 | d2 | area | population | density | lat | lon | B | K | L | M | |
---|---|---|---|---|---|---|---|---|---|---|---|
강원도 강릉시 | 강원도 | 강릉시 | 1040.07 | 219067 | 210.627169 | 37.751853 | 128.876057 | 0 | 1 | 6 | 1 |
강원도 고성군 | 강원도 | 고성군 | 664.19 | 30802 | 46.375284 | 38.380129 | 128.467439 | 0 | 0 | 0 | 0 |
강원도 동해시 | 강원도 | 동해시 | 180.01 | 95850 | 532.470418 | 37.524719 | 129.114292 | 0 | 1 | 3 | 0 |
강원도 삼척시 | 강원도 | 삼척시 | 1185.80 | 72431 | 61.081970 | 37.449868 | 129.165206 | 0 | 0 | 1 | 0 |
강원도 속초시 | 강원도 | 속초시 | 105.25 | 84568 | 803.496437 | 38.207015 | 128.591849 | 0 | 0 | 3 | 1 |
이제 모든 데이터가 갖춰졌으니 모아서 계산해보자!
우선, pseudocount같은 것이 필요할 지 보기 위해 롯데리아가 하나도 없고 다른 버거 체인이 있는 시군구가 있는지 확인해 본다.
bgt[(bgt['L'] == 0) & (bgt['B'] + bgt['M'] + bgt['K'] > 0)]
d1 | d2 | area | population | density | lat | lon | B | K | L | M |
---|
다행히도 없다. 롯데리아도 하나도 없는 곳은?
bgt[bgt['L'] == 0]
d1 | d2 | area | population | density | lat | lon | B | K | L | M | |
---|---|---|---|---|---|---|---|---|---|---|---|
강원도 고성군 | 강원도 | 고성군 | 664.19 | 30802 | 46.375284 | 38.380129 | 128.467439 | 0 | 0 | 0 | 0 |
강원도 양양군 | 강원도 | 양양군 | 628.68 | 28196 | 44.849526 | 38.075392 | 128.618850 | 0 | 0 | 0 | 0 |
경상남도 산청군 | 경상남도 | 산청군 | 794.59 | 35239 | 44.348658 | 35.415588 | 127.873498 | 0 | 0 | 0 | 0 |
경상남도 의령군 | 경상남도 | 의령군 | 482.95 | 30965 | 64.116368 | 35.322190 | 128.261658 | 0 | 0 | 0 | 0 |
경상북도 군위군 | 경상북도 | 군위군 | 614.15 | 25377 | 41.320524 | 36.242835 | 128.572770 | 0 | 0 | 0 | 0 |
경상북도 영양군 | 경상북도 | 영양군 | 815.11 | 18666 | 22.899977 | 36.666656 | 129.112401 | 0 | 0 | 0 | 0 |
경상북도 울릉군 | 경상북도 | 울릉군 | 72.82 | 10398 | 142.790442 | 37.484417 | 130.905800 | 0 | 0 | 0 | 0 |
경상북도 의성군 | 경상북도 | 의성군 | 1175.89 | 59608 | 50.691816 | 36.352658 | 128.697005 | 0 | 0 | 0 | 0 |
경상북도 청송군 | 경상북도 | 청송군 | 842.45 | 27067 | 32.128910 | 36.435904 | 129.057108 | 0 | 0 | 0 | 0 |
인천광역시 동구 | 인천광역시 | 동구 | 7.05 | 78496 | 11134.184397 | 37.473818 | 126.643338 | 0 | 0 | 0 | 0 |
인천광역시 옹진군 | 인천광역시 | 옹진군 | 164.30 | 18328 | 111.552039 | 37.213889 | 126.178333 | 0 | 0 | 0 | 0 |
전라남도 곡성군 | 전라남도 | 곡성군 | 547.37 | 32482 | 59.341944 | 35.281955 | 127.291918 | 0 | 0 | 0 | 0 |
전라남도 구례군 | 전라남도 | 구례군 | 443.01 | 27698 | 62.522291 | 35.202495 | 127.462653 | 0 | 0 | 0 | 0 |
전라남도 보성군 | 전라남도 | 보성군 | 663.16 | 49495 | 74.635081 | 34.771456 | 127.079894 | 0 | 0 | 0 | 0 |
전라남도 신안군 | 전라남도 | 신안군 | 653.13 | 45687 | 69.950852 | 34.827332 | 126.101074 | 0 | 0 | 0 | 0 |
전라남도 진도군 | 전라남도 | 진도군 | 420.32 | 34181 | 81.321374 | 34.486871 | 126.263485 | 0 | 0 | 0 | 0 |
전라남도 함평군 | 전라남도 | 함평군 | 392.77 | 37502 | 95.480816 | 35.065940 | 126.516552 | 0 | 0 | 0 | 0 |
전라북도 장수군 | 전라북도 | 장수군 | 533.64 | 23740 | 44.486920 | 35.647277 | 127.521136 | 0 | 0 | 0 | 0 |
전라북도 진안군 | 전라북도 | 진안군 | 788.94 | 27828 | 35.272644 | 35.791730 | 127.424836 | 0 | 0 | 0 | 0 |
의외로 상당히 많다. 롯데리아는 전국에 다 있는 줄 알았는데. -O-; 얘네들은 나중에 NaN으로 처리해서 다른 색깔로 표시.
그나저나 인천 동구에는 왜 버거킹, 롯데리아, KFC, 맥도날드가 하나도 없을까? 인구밀도도 1만이 넘는데.. 이상하다. --> 찾아보니 롯데리아는 구 경계에 딱 붙어서 남구에 3개가 있다.
bgt['BMK'] = bgt['B'] + bgt['M'] + bgt['K']
bgt['BgIdx'] = bgt['BMK'] / bgt['L']
bgt = bgt.sort('BgIdx', ascending=False)
bgt.head(10)
d1 | d2 | area | population | density | lat | lon | B | K | L | M | BMK | BgIdx | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
서울특별시 강남구 | 서울특별시 | 강남구 | 39.50 | 569499 | 14417.696203 | 37.517236 | 127.047325 | 20 | 10 | 9 | 12 | 42 | 4.666667 |
서울특별시 서초구 | 서울특별시 | 서초구 | 47.04 | 431131 | 9165.199830 | 37.483712 | 127.032411 | 10 | 7 | 6 | 7 | 24 | 4.000000 |
서울특별시 종로구 | 서울특별시 | 종로구 | 23.90 | 177543 | 7428.577406 | 37.572950 | 126.979358 | 2 | 5 | 6 | 6 | 13 | 2.166667 |
강원도 홍천군 | 강원도 | 홍천군 | 1817.94 | 70264 | 38.650340 | 37.696952 | 127.888683 | 2 | 0 | 1 | 0 | 2 | 2.000000 |
서울특별시 서대문구 | 서울특별시 | 서대문구 | 17.60 | 336649 | 19127.784091 | 37.579116 | 126.936779 | 3 | 2 | 5 | 4 | 9 | 1.800000 |
서울특별시 중구 | 서울특별시 | 중구 | 9.96 | 137861 | 13841.465863 | 37.564091 | 126.997940 | 6 | 3 | 8 | 3 | 12 | 1.500000 |
인천광역시 중구 | 인천광역시 | 중구 | 110.60 | 93550 | 845.840868 | 37.473734 | 126.621480 | 2 | 2 | 6 | 5 | 9 | 1.500000 |
서울특별시 동작구 | 서울특별시 | 동작구 | 16.36 | 407973 | 24937.224939 | 37.512402 | 126.939252 | 2 | 3 | 6 | 4 | 9 | 1.500000 |
서울특별시 양천구 | 서울특별시 | 양천구 | 17.40 | 506684 | 29119.770115 | 37.516872 | 126.866399 | 3 | 1 | 7 | 6 | 10 | 1.428571 |
서울특별시 동대문구 | 서울특별시 | 동대문구 | 14.20 | 374277 | 26357.535211 | 37.574368 | 127.040019 | 1 | 1 | 5 | 5 | 7 | 1.400000 |
오오.. 역시 강남구, 서초구 버거지수는 유난히 튄다. 신기하다. +_+
홍천군이 3위! 그것은.. 오션월드와 대명비발디에 있는 버거킹 때문이다.
B+M+K와 롯데리아 매장 수 비교해 볼까?
rcParams['font.family'] = 'NanumGothic'
plt.figure(figsize=(5, 5))
r = lambda: np.random.random(len(bgt))
plt.scatter(bgt['L'] + r(), bgt['BMK'] + r(), s=6, c='black', edgecolor='none', alpha=0.6)
plt.xlabel('롯데리아')
plt.ylabel('버거킹+맥도날드+KFC')
plt.xlim(0, 45)
plt.ylim(0, 45)
plt.gca().set_aspect(1)
# 추세선 그린다.
trendfun = np.poly1d(np.polyfit(bgt['L'], bgt['BMK'], 1))
trendx = np.linspace(0, 45, 2)
plt.plot(trendx, trendfun(trendx))
# 튀는 점 몇 개는 이름도 표시한다.
tolabel = bgt[(bgt['L'] > 17) | (bgt['BMK'] >= 14)]
for idx, row in tolabel.iterrows():
label_name = idx.split()[1][:-1]
plt.annotate(label_name, (row['L'], row['BMK']))
음.. 역시 이렇게 보니까 강남, 서초는 대놓고 차이가 너무 많이 난다.
bgt.head()
d1 | d2 | area | population | density | lat | lon | B | K | L | M | BMK | BgIdx | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
서울특별시 강남구 | 서울특별시 | 강남구 | 39.50 | 569499 | 14417.696203 | 37.517236 | 127.047325 | 20 | 10 | 9 | 12 | 42 | 4.666667 |
서울특별시 서초구 | 서울특별시 | 서초구 | 47.04 | 431131 | 9165.199830 | 37.483712 | 127.032411 | 10 | 7 | 6 | 7 | 24 | 4.000000 |
서울특별시 종로구 | 서울특별시 | 종로구 | 23.90 | 177543 | 7428.577406 | 37.572950 | 126.979358 | 2 | 5 | 6 | 6 | 13 | 2.166667 |
강원도 홍천군 | 강원도 | 홍천군 | 1817.94 | 70264 | 38.650340 | 37.696952 | 127.888683 | 2 | 0 | 1 | 0 | 2 | 2.000000 |
서울특별시 서대문구 | 서울특별시 | 서대문구 | 17.60 | 336649 | 19127.784091 | 37.579116 | 126.936779 | 3 | 2 | 5 | 4 | 9 | 1.800000 |
미리 디자인해 놓은 블록모양 지도에서 좌표를 계산해서 옆에 붙인다. 지명이 길면 블록모양 지도 만들 때 번거로우므로 짧은 이름으로 매치한다.
def short_distr(name):
wide, narrow = name.split()
if narrow.endswith('구'):
return wide[:2] + (narrow[:-1] if len(narrow) > 2 else narrow)
elif narrow == '고성군': # 고성군은 강원도, 경상남도에 있다.
return '고성({})'.format({'강원도': '강원', '경상남도': '경남'}[wide])
else:
return narrow[:-1]
bgt['shortname'] = list(map(short_distr, bgt.index))
bgt.head()
d1 | d2 | area | population | density | lat | lon | B | K | L | M | BMK | BgIdx | shortname | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
서울특별시 강남구 | 서울특별시 | 강남구 | 39.50 | 569499 | 14417.696203 | 37.517236 | 127.047325 | 20 | 10 | 9 | 12 | 42 | 4.666667 | 서울강남 |
서울특별시 서초구 | 서울특별시 | 서초구 | 47.04 | 431131 | 9165.199830 | 37.483712 | 127.032411 | 10 | 7 | 6 | 7 | 24 | 4.000000 | 서울서초 |
서울특별시 종로구 | 서울특별시 | 종로구 | 23.90 | 177543 | 7428.577406 | 37.572950 | 126.979358 | 2 | 5 | 6 | 6 | 13 | 2.166667 | 서울종로 |
강원도 홍천군 | 강원도 | 홍천군 | 1817.94 | 70264 | 38.650340 | 37.696952 | 127.888683 | 2 | 0 | 1 | 0 | 2 | 2.000000 | 홍천 |
서울특별시 서대문구 | 서울특별시 | 서대문구 | 17.60 | 336649 | 19127.784091 | 37.579116 | 126.936779 | 3 | 2 | 5 | 4 | 9 | 1.800000 | 서울서대문 |
블록 지도 데이터 읽어온다.
blockpositions = pd.read_csv('../../../../../p/tiny/2014-12/burgerindex/blockmap-positions.csv', names=range(15))
blockpositions.head()
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | NaN | NaN | NaN | NaN | NaN | NaN | 철원 | 화천 | 양구 | 고성(강원) | NaN | NaN | NaN | NaN | NaN |
1 | NaN | NaN | NaN | NaN | NaN | 서울도봉 | 서울노원 | 연천 | 포천 | 속초 | NaN | NaN | NaN | NaN | NaN |
2 | NaN | NaN | NaN | 파주 | 고양 | 서울강북 | 서울성북 | 동두천 | 양주 | 인제 | 양양 | NaN | NaN | NaN | NaN |
3 | NaN | 강화 | 김포 | 광명 | 서울은평 | 서울서대문 | 서울종로 | 의정부 | 남양주 | 가평 | 춘천 | NaN | NaN | NaN | NaN |
4 | NaN | 인천서구 | 부천 | 안양 | 서울강서 | 서울마포 | 서울중구 | 서울동대문 | 서울중랑 | 구리 | 홍천 | 강릉 | NaN | NaN | NaN |
x, y 좌표 테이블로 변환해서 버거 데이터랑 합친다.
flatrows = []
for y, colcities in blockpositions.iterrows():
for x, city in colcities.iteritems():
if isinstance(city, str):
flatrows.append((x, y, city))
blockpositions_tbl = pd.DataFrame(flatrows, columns=('x', 'y', 'city')).set_index('city').sort_index()
bgtb = pd.merge(bgt, blockpositions_tbl, how='left', left_on='shortname', right_index=True)
bgtb.head()
d1 | d2 | area | population | density | lat | lon | B | K | L | M | BMK | BgIdx | shortname | x | y | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
서울특별시 강남구 | 서울특별시 | 강남구 | 39.50 | 569499 | 14417.696203 | 37.517236 | 127.047325 | 20 | 10 | 9 | 12 | 42 | 4.666667 | 서울강남 | 6 | 7 |
서울특별시 서초구 | 서울특별시 | 서초구 | 47.04 | 431131 | 9165.199830 | 37.483712 | 127.032411 | 10 | 7 | 6 | 7 | 24 | 4.000000 | 서울서초 | 6 | 6 |
서울특별시 종로구 | 서울특별시 | 종로구 | 23.90 | 177543 | 7428.577406 | 37.572950 | 126.979358 | 2 | 5 | 6 | 6 | 13 | 2.166667 | 서울종로 | 6 | 3 |
강원도 홍천군 | 강원도 | 홍천군 | 1817.94 | 70264 | 38.650340 | 37.696952 | 127.888683 | 2 | 0 | 1 | 0 | 2 | 2.000000 | 홍천 | 10 | 4 |
서울특별시 서대문구 | 서울특별시 | 서대문구 | 17.60 | 336649 | 19127.784091 | 37.579116 | 126.936779 | 3 | 2 | 5 | 4 | 9 | 1.800000 | 서울서대문 | 5 | 3 |
혹시 위치가 할당되지 않은 곳이 있는지 확인한다.
bgtb[bgtb['x'].apply(np.isnan)]
d1 | d2 | area | population | density | lat | lon | B | K | L | M | BMK | BgIdx | shortname | x | y |
---|
없다. 모든 지역이 다 위치가 잡혔으니 이제 그림을 그린다!
from matplotlib import rcParams
from matplotlib import cm, colors, _cm
rcParams['font.family'] = 'NanumBarunGothic'
BgIdx가 NaN이면 색을 제대로 칠하기 힘들기 때문에, 롯데리아가 없는 곳은 그냥 0으로 처리한다.
bgtb['BgIdx'] = bgtb['BgIdx'].fillna(0)
시도간 경계 그림 데이터 (열심히) 만든다.
BORDER_LINES = [
[(3, 2), (5, 2), (5, 3), (9, 3), (9, 1)], # 인천
[(2, 5), (3, 5), (3, 4), (8, 4), (8, 7), (7, 7), (7, 9), (4, 9), (4, 7), (1, 7)], # 서울
[(1, 6), (1, 9), (3, 9), (3, 10), (8, 10), (8, 9),
(9, 9), (9, 8), (10, 8), (10, 5), (9, 5), (9, 3)], # 경기도
[(9, 12), (9, 10), (8, 10)], # 강원도
[(10, 5), (11, 5), (11, 4), (12, 4), (12, 5), (13, 5),
(13, 4), (14, 4), (14, 2)], # 충청남도
[(11, 5), (12, 5), (12, 6), (15, 6), (15, 7), (13, 7),
(13, 8), (11, 8), (11, 9), (10, 9), (10, 8)], # 충청북도
[(14, 4), (15, 4), (15, 6)], # 대전시
[(14, 7), (14, 9), (13, 9), (13, 11), (13, 13)], # 경상북도
[(14, 8), (16, 8), (16, 10), (15, 10),
(15, 11), (14, 11), (14, 12), (13, 12)], # 대구시
[(15, 11), (16, 11), (16, 13)], # 울산시
[(17, 1), (17, 3), (18, 3), (18, 6), (15, 6)], # 전라북도
[(19, 2), (19, 4), (21, 4), (21, 3), (22, 3), (22, 2), (19, 2)], # 광주시
[(18, 5), (20, 5), (20, 6)], # 전라남도
[(16, 9), (18, 9), (18, 8), (19, 8), (19, 9), (20, 9), (20, 10)], # 부산시
]
def draw_blockcolormap(tbl, datacol, vmin, vmax, whitelabelmin, cmapname, gamma, datalabel, dataticks):
cmap = colors.LinearSegmentedColormap(cmapname + 'custom',
getattr(_cm, '_{}_data'.format(cmapname)), gamma=gamma)
cmap.set_bad('white', 1.)
mapdata = tbl.pivot(index='y', columns='x', values=datacol)
masked_mapdata = np.ma.masked_where(np.isnan(mapdata), mapdata)
plt.figure(figsize=(9, 16))
plt.pcolor(masked_mapdata, vmin=vmin, vmax=vmax, cmap=cmap,
edgecolor='#aaaaaa', linewidth=0.5)
# 지역 이름 표시
for idx, row in tbl.iterrows():
annocolor = 'white' if row[datacol] > whitelabelmin else 'black'
# 광역시는 구 이름이 겹치는 경우가 많아서 시단위 이름도 같이 표시한다. (중구, 서구)
if row['d1'].endswith('시') and not row['d1'].startswith('세종'):
dispname = '{}\n{}'.format(row['d1'][:2], row['d2'][:-1])
if len(row['d2']) <= 2:
dispname += row['d2'][-1]
else:
dispname = row['d2'][:-1]
# 서대문구, 서귀포시 같이 이름이 3자 이상인 경우에 작은 글자로 표시한다.
if len(dispname.splitlines()[-1]) >= 3:
fontsize, linespacing = 12, 1.2
else:
fontsize, linespacing = 14, 1.03
plt.annotate(dispname, (row['x']+0.5, row['y']+0.5), weight='bold',
fontsize=fontsize, ha='center', va='center', color=annocolor,
linespacing=linespacing)
# 시도 경계 그린다.
for path in BORDER_LINES:
ys, xs = zip(*path)
plt.plot(xs, ys, c='black', lw=2)
plt.gca().invert_yaxis()
plt.gca().set_aspect(1)
plt.axis('off')
cb = plt.colorbar(shrink=.1, aspect=10)
cb.set_label(datalabel)
cb.set_ticks(dataticks)
plt.tight_layout()
자, 이제 긴장되는 버거지수 지도 그리는 순간!
draw_blockcolormap(bgtb, 'BgIdx', 0, 3, 1.42, 'Blues', 0.75, '버거지수', np.arange(0, 3.1, 0.5))
plt.savefig('bmap-burgerindex.pdf')
그렸다! 제법 멋지게 나온다. 이제 따로 따로 보자. 인구 대비 롯데리아 수는?
bgtb['Lp10T'] = bgtb['L'] / bgtb['population'] * 10000
draw_blockcolormap(bgtb, 'Lp10T', 0, 1, 0.45, 'YlGn', 1, '1만명당 롯데리아 점포수', np.arange(0, 1.1, 0.2))
plt.savefig('bmap-lotteria.pdf')
인구 대비 버거킹+맥도날드+KFC 수는?
bgtb['BMKp10T'] = bgtb['BMK'] / bgtb['population'] * 10000
draw_blockcolormap(bgtb, 'BMKp10T', 0, 1, 0.45, 'YlGn', 1, '1만명당 버거킹+맥도날드+KFC 점포수', np.arange(0, 1.1, 0.2))
plt.savefig('bmap-bmkshops.pdf')
그렇다면, 버거킹만 따로 떼서 보면?
bgtb['Bp10T'] = bgtb['B'] / bgtb['population'] * 10000
draw_blockcolormap(bgtb, 'Bp10T', 0, 0.5, 0.25, 'RdPu', 1, '1만명당 버거킹 점포수', np.arange(0, 0.6, 0.1))
plt.savefig('bmap-burgerking.pdf')
역시 버거킹은 도심에 강남, 서초와 다른 대도시 시내에 집중되어 있다. 맥도날드 매장의 경우는?
bgtb['Mp10T'] = bgtb['M'] / bgtb['population'] * 10000
draw_blockcolormap(bgtb, 'Mp10T', 0, 0.5, 0.25, 'RdPu', 1, '1만명당 맥도날드 점포수', np.arange(0, 0.6, 0.1))
plt.savefig('bmap-mcdonalds.pdf')
대도시의 도심에 몰리는 현상이 여전히 있긴 하지만, 버거킹보다 쏠림현상이 좀 덜하다.
bgtb['Kp10T'] = bgtb['K'] / bgtb['population'] * 10000
draw_blockcolormap(bgtb, 'Kp10T', 0, 0.5, 0.25, 'RdPu', 1, '1만명당 KFC 점포수', np.arange(0, 0.6, 0.1))
plt.savefig('bmap-KFC.pdf')
이제 그냥 그려보는 보너스 네 버거집 모두 합쳐서 보면?
bgtb['LBMKp10T'] = (bgtb['L'] + bgtb['BMK']) / bgtb['population'] * 10000
draw_blockcolormap(bgtb, 'LBMKp10T', 0, 2, 0.7, 'Oranges', 0.8, '1만명당 롯데리아/버거킹/맥도날드/KFC 점포수', np.arange(0, 2.1, 0.5))
plt.savefig('bmap-LBMK.pdf')
재미삼아 그려보는 인구밀도 그림.
bgtb['logdensity'] = np.log10(bgtb['density'])
draw_blockcolormap(bgtb, 'logdensity', 0, 6, 4, 'Greens', 1, '인구밀도 (명/$km^2$)', np.arange(0, 6.1, 1))
plt.savefig('bmap-density.pdf')
재미삼아 그려보는 면적 그림.
draw_blockcolormap(bgtb, 'area', 0, 1500, 500, 'Greys', 0.6, '면적 ($km^2$)', np.arange(0, 510, 100))
plt.savefig('bmap-area.pdf')
역시 강원도, 경상북도가 군 면적으로는 우월하다.
각 브랜드마다 매장 개수는.. 롯데리아가 다른 셋을 모두 합쳐도 더 많다.
plt.figure(figsize=(4, 3))
subcnt = bgt[['B', 'K', 'L', 'M']]
subcnt.columns = ['버거킹', 'KFC', '롯데리아', '맥도날드']
p = subcnt.sum(axis=0).plot(kind='bar')
plt.setp(p.get_xticklabels(), rotation=0)
plt.ylabel('매장 수')
plt.savefig('plot-shops-count.pdf')
fig = plt.figure(figsize=(9, 9))
def plot_nstores(b1, b2, label1, label2):
plt.scatter(bgt[b1] + np.random.random(len(bgt)),
bgt[b2] + np.random.random(len(bgt)),
edgecolor='none', alpha=0.75, s=6, c='black')
plt.xlim(-1, 15 if b1 != 'L' else 35)
plt.ylim(-1, 15 if b2 != 'L' else 35)
plt.xlabel(label1)
plt.ylabel(label2)
r = scipy.stats.pearsonr(bgt[b1], bgt[b2])
plt.annotate('r={:.3f}'.format(r[0]), (9, 12.5), fontsize=14)
bgbrands = [
('B', '버거킹'), ('K', 'KFC'),
('L', '롯데리아'), ('M', '맥드날드'),
]
for a in range(len(bgbrands) - 1):
for b in range(1, len(bgbrands)):
if a >= b:
continue
ax = fig.add_subplot(len(bgbrands)-1, len(bgbrands)-1, a * 3 + b)
acol, alabel = bgbrands[a]
bcol, blabel = bgbrands[b]
plot_nstores(bcol, acol, blabel, alabel)
plt.tight_layout()
plt.savefig('plot-shopcount-correlations.pdf')
약간 예상과 비슷하게 롯데리아:맥도날드가 연관성이 높다. 눈에 띄는 것은 이 관계 곡선이 대략 2개로 나뉘는 현상이다 무엇일까?
cate = bgt.apply(lambda r: 'S' if (r['M'] <= 5) and (r['L'] <= 2) else (
'L' if r['M'] == 0 or r['L'] / r['M'] > 2.1 else 'M'),
axis=1)
colors = [{'S': 'gray', 'L': 'green', 'M': 'red'}[c] for c in cate]
plt.figure(figsize=(6, 6))
plt.scatter(bgt['M'] + np.random.random(len(bgt)),
bgt['L'] + np.random.random(len(bgt)), s=8, c=colors, edgecolor='none')
<matplotlib.collections.PathCollection at 0x7f2c4f691780>
빨간 지역과 초록 지역의 차이는??
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(1, 1, 1)
bgt[cate == 'M'].plot(kind='scatter', x='population', y='density', ax=ax, c='red')
bgt[cate == 'L'].plot(kind='scatter', x='population', y='density', ax=ax, c='green')
plt.xscale('log')
plt.yscale('log')
scipy.stats.mannwhitneyu(bgt.loc[cate == 'M', 'population'], bgt.loc[cate == 'L', 'population'])
(1439.0, 0.037834701755873786)
인구밀도가 높은 지역에서는 맥도날드와 롯데리아 매장 수 차이가 덜 벌어지고 (빨간 점), 인구밀도가 낮은 지역에서는 매장 수가 많이 벌어진다. 같은 분석을 버거지수에 적용해 보자.
validbgt = bgt.dropna(subset=['BgIdx']).copy()
validbgt['logBgIdx'] = np.log2(validbgt['BgIdx'])
validbgt = validbgt.dropna(subset=['logBgIdx'])
validbgt['logDensity'] = np.log10(validbgt['density'])
fig = plt.figure(figsize=(4, 4))
ax = fig.add_subplot(1, 1, 1)
validbgt.plot(kind='scatter', y='logBgIdx', x='logDensity', ax=ax,
edgecolor='none', s=8, c='black')
plt.xlabel('인구밀도 (log$_{10}$ 인/km$^2$)')
plt.ylabel('버거지수 (log$_2$)')
tau, taup = scipy.stats.kendalltau(validbgt['logBgIdx'], validbgt['logDensity'])
print("Kendall's tau: {} (p={})".format(tau, taup))
plt.annotate('$\\tau$ = {:.3f}'.format(tau), (2, 2), fontsize=14)
Kendall's tau: 0.614912791348164 (p=4.172733288940445e-40)
<matplotlib.text.Annotation at 0x7f2c47e7d940>
역시 인구밀도와 버거지수는 상당한 상관관계를 가진다. 그렇다면 인구와 매장수가 비례관계 이상의 상관관계가 버거킹, KFC 매장 수에서 관찰될까?
lotteria_per_population = bgt['L'].sum() / bgt['population'].sum()
lotteria_to_random = bgt['L'] / (lotteria_per_population * bgt['population'])
BK_per_population = (bgt['B'] + bgt['K']).sum() / bgt['population'].sum()
BK_to_random = (bgt['B'] + bgt['K']) / (BK_per_population * bgt['population'])
mcdonalds_per_population = bgt['M'].sum() / bgt['population'].sum()
mcdonalds_to_random = bgt['M'] / (mcdonalds_per_population * bgt['population'])
점만 뿌리면 트렌드가 뚜렷하게 보이지 않으니 loess smoothing곡선을 같이 그린다.
import rpy2.robjects as ro
def loess_fit(x, y, px=None, model=None, alpha=0.5):
if model is None:
model = ro.r('y ~ x')
if px is None:
px = np.linspace(min(x), max(x), 22)[1:-1]
fitframe = ro.DataFrame({'x': ro.FloatVector(x), 'y': ro.FloatVector(y)})
loessmodel = ro.r.loess(model, fitframe, span=alpha)
predframe = ro.DataFrame({'x': ro.FloatVector(px)})
predy = ro.r.predict(loessmodel, predframe)
preddata = [(x, predy[i]) for i, x in enumerate(px)]
return np.array(preddata).transpose()
lotteria_trend = loess_fit(np.log10(bgt['density']), lotteria_to_random)
BK_trend = loess_fit(np.log10(bgt['density']), BK_to_random)
mcdonalds_trend = loess_fit(np.log10(bgt['density']), mcdonalds_to_random)
fig = plt.figure(figsize=(10, 4))
ax = fig.add_subplot(1, 3, 1)
plt.scatter(np.log10(bgt['density']), lotteria_to_random, s=6, c='black', edgecolor='none')
plt.axhline(1, lw=2, c='black', alpha=0.2)
plt.plot(lotteria_trend[0], lotteria_trend[1], c='red', alpha=0.7, lw=1.5, zorder=3)
plt.ylim(-0.1, 7.0)
plt.xlabel('인구밀도 (log10 인/$km^2$)')
plt.ylabel('기대 매장 수 대비 실제 매장 수 비율')
plt.title('롯데리아')
ax = fig.add_subplot(1, 3, 2)
plt.scatter(np.log10(bgt['density']), BK_to_random, s=6, c='black', edgecolor='none')
plt.plot(BK_trend[0], BK_trend[1], c='red', alpha=0.7, lw=1.5, zorder=3)
plt.axhline(1, lw=2, c='black', alpha=0.2)
plt.ylim(-0.1, 7.0)
plt.xlabel('인구밀도 (log10 인/$km^2$)')
plt.ylabel('기대 매장 수 대비 실제 매장 수 비율')
plt.title('버거킹+KFC')
ax = fig.add_subplot(1, 3, 3)
plt.scatter(np.log10(bgt['density']), mcdonalds_to_random, s=6, c='black', edgecolor='none')
plt.plot(mcdonalds_trend[0], mcdonalds_trend[1], c='red', alpha=0.7, lw=1.5, zorder=3)
plt.axhline(1, lw=2, c='black', alpha=0.2)
plt.ylim(-0.1, 7.0)
plt.xlabel('인구밀도 (log10 인/$km^2$)')
plt.ylabel('기대 매장 수 대비 실제 매장 수 비율')
plt.title('맥도날드')
plt.tight_layout()
아주 크게 눈에 띄는 패턴은 없지만, 롯데리아에서 약하게 인구밀도가 높은 지역에 오히려 점포수가 적은 경향이 있다. 아마도 점포 간 거리 때문에 포화된 것이 아닌가 싶다. 버거킹, KFC, 맥도날드의 경우에도 일부 지역을 제외하고는 특별히 튀는 점이 많지 않다. 단순히 롯데리아가 커버리지가 높다보니 나오는 패턴이 아닌가 싶다.
자 이제 그럼 대충 시뮬레이션으로 커버리지 차이에 의해서 버거지수 차이가 나올 수 있는지 확인해 보자. 국회의원 지역구 획정 방식과 대충 비슷하게 가장 인구가 많은 시군구부터 순서대로 분구해가면서 점포를 할당하는 방법으로 버거지수를 계산해 본다.
def sim_positions(nstores):
simulated_nstores = pd.Series([0] * len(bgt), index=bgt.index)
for i in range(int(nstores)):
maxloc = (bgt['population'] / (simulated_nstores + 1)).argmax()
simulated_nstores.loc[maxloc] += 1
return simulated_nstores
sim_BMK = sim_positions(bgt['B'].sum()) + sim_positions(bgt['M'].sum()) + sim_positions(bgt['K'].sum())
sim_L = sim_positions(bgt['L'].sum())
plt.scatter(bgt['BgIdx'] + np.random.normal(0, 0.05, len(bgt)),
sim_BMK / sim_L + np.random.normal(0, 0.05, len(bgt)),
s=5, c='black')
valid = (np.isfinite(bgt['BgIdx']) & (bgt['L'] > 0) & (sim_L > 0) & (sim_BMK > 0))
pr, pp = scipy.stats.pearsonr(bgt['BgIdx'][valid], sim_BMK[valid] / sim_L[valid])
plt.ylim(-0.2, 1)
plt.xlim(-0.2, 5)
plt.gca().set_aspect(1)
plt.xlabel('실제 버거지수')
plt.ylabel('시뮬레이션 결과')
plt.annotate('r={:.3f}'.format(pr), (3, 0.2))
<matplotlib.text.Annotation at 0x7f2c4c3de9e8>
시뮬레이션 결과, 어느 정도는 인구와 커버리지 차이로 설명이 되는 걸 확인할 수 있다. Pearson's correlation으로 시뮬레이션과 실제 지수가 0.25 정도 나오니까, 매장 수가 많고 적음만 가지고도 버거지수 차이가 어느 정도는 나타난다고 볼 수 있겠다. 한편, 브랜드 별로 매장이 살아남을 수 있는 위치 자체가 많거나 적을 수가 있고, 매장 운영 형태 (롯데리아는 주로 가맹점 운영, 버거킹은 주로 직영점 운영)에 따라 나타나는 것일 수도 있어서, 매장수가 적고 많은 원인을 단순히 어떤 하나로 보기는 어렵다.
그 외의 다른 요인에 대해서는 상권, 유동인구, 연령별 인구구조 등을 고려해서 같이 분석해야 할 것 같다. -O-;