by Sean Park, 20-04-16
내가 거주하고있는 성수동에는 카페가 참 많다.
재생 인테리어의 매력이 깃든 어니언부터, 오픈 당일 인스타그래머들을 몇 시간 씩 줄세운 블루보틀 1호점까지.
힙스터와 커피성애자들이 모이는 성수동에서, 가장 핫한 카페는 어디일까?
import folium
import warnings
from selenium import webdriver
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
plt.style.use('seaborn-whitegrid')
warnings.filterwarnings('ignore')
from bs4 import BeautifulSoup
import time
# 스타일 변경으로 인해 폰트 다시 설정
plt.rc('font', family='NanumGothic')
plt.rc('font', size=13)
driver = webdriver.Chrome('chromedriver/chromedriver.exe')
driver.get('https://map.kakao.com/')
driver.find_element_by_id('search.keyword.query').send_keys('카페') # 카페 검색
driver.implicitly_wait(10)
time.sleep(1)
driver.find_element_by_id('search.keyword.submit').click() # 검색 클릭
driver.implicitly_wait(10)
time.sleep(1)
driver.find_element_by_xpath('//*[@id="info.search.place.more"]').click() # 더 보기 클릭
from tqdm import tqdm_notebook
dict_cafe = {'name': [], 'address': [], 'score': [], 'score_cnt': [], 'review_cnt': []}
for pagenum in tqdm_notebook(range(1, 36)):
try:
page = pagenum % 5 # 페이지 이동 버튼의 id넘버가 5의 나머지로 되어있다. (1페이지 = p1, 6페이지 = p1, 7페이지 = p2 ...)
html = driver.page_source
soup = BeautifulSoup(html, 'lxml')
for cafenum in range(15):
dict_cafe['name'].append(soup.find_all('a', 'link_name')[cafenum].text)
dict_cafe['address'].append(soup.find_all('p', 'lot_number')[cafenum].text)
dict_cafe['score'].append(soup.find_all('em', 'num')[cafenum].text)
dict_cafe['score_cnt'].append(soup.find_all('a', 'numberofscore')[cafenum].text)
dict_cafe['review_cnt'].append(soup.find_all('a', 'review', 'em')[cafenum].text)
# 페이지가 5페이지씩 나뉘어져있는데(1-5, 6-10..), 마지막 페이지(5의 배수 페이지)에 도착했을 경우, 다음 버튼 클릭
if page == 0:
driver.find_element_by_id('info.search.page.next').click()
else:
driver.find_element_by_id('info.search.page.no{}'.format(page+1)).click()
time.sleep(1)
except:
print('페이지 초과: {}'.format(pagenum))
df_raw = pd.DataFrame(dict_cafe)
df_raw.to_csv('source/cafe_in_seongsu.csv')
df_raw = pd.read_csv('source/cafe_in_seongsu.csv', index_col=0)
df_raw.head()
name | address | score | score_cnt | review_cnt | |
---|---|---|---|---|---|
0 | 블루보틀 성수점 | (지번) 성수동1가 656-302 | 2.8 | 85건 | 리뷰 635 |
1 | 할아버지공장 | (지번) 성수동2가 309-133 | 3.0 | 27건 | 리뷰 257 |
2 | 차 | (지번) 성수동1가 685-408 | 2.6 | 28건 | 리뷰 120 |
3 | 어니언 | (지번) 성수동2가 277-135 | 3.4 | 140건 | 리뷰 1,257 |
4 | 대림창고 | (지번) 성수동2가 322-32 | 3.3 | 143건 | 리뷰 601 |
df_raw.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 505 entries, 0 to 504 Data columns (total 5 columns): name 505 non-null object address 504 non-null object score 505 non-null float64 score_cnt 505 non-null object review_cnt 505 non-null object dtypes: float64(1), object(4) memory usage: 23.7+ KB
df_raw[df_raw.address.isnull()]
name | address | score | score_cnt | review_cnt | |
---|---|---|---|---|---|
484 | 코지카페 | NaN | 0.0 | 0건 | 리뷰 0 |
df_raw.drop(df_raw[df_raw.address.isnull()].index, axis=0, inplace=True)
df_raw.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 504 entries, 0 to 504 Data columns (total 5 columns): name 504 non-null object address 504 non-null object score 504 non-null float64 score_cnt 504 non-null object review_cnt 504 non-null object dtypes: float64(1), object(4) memory usage: 23.6+ KB
df_raw.head()
name | address | score | score_cnt | review_cnt | |
---|---|---|---|---|---|
0 | 블루보틀 성수점 | (지번) 성수동1가 656-302 | 2.8 | 85건 | 리뷰 635 |
1 | 할아버지공장 | (지번) 성수동2가 309-133 | 3.0 | 27건 | 리뷰 257 |
2 | 차 | (지번) 성수동1가 685-408 | 2.6 | 28건 | 리뷰 120 |
3 | 어니언 | (지번) 성수동2가 277-135 | 3.4 | 140건 | 리뷰 1,257 |
4 | 대림창고 | (지번) 성수동2가 322-32 | 3.3 | 143건 | 리뷰 601 |
for row in df_raw.index:
df_raw.address[row] = df_raw.address[row][5:]
df_raw.head()
name | address | score | score_cnt | review_cnt | |
---|---|---|---|---|---|
0 | 블루보틀 성수점 | 성수동1가 656-302 | 2.8 | 85건 | 리뷰 635 |
1 | 할아버지공장 | 성수동2가 309-133 | 3.0 | 27건 | 리뷰 257 |
2 | 차 | 성수동1가 685-408 | 2.6 | 28건 | 리뷰 120 |
3 | 어니언 | 성수동2가 277-135 | 3.4 | 140건 | 리뷰 1,257 |
4 | 대림창고 | 성수동2가 322-32 | 3.3 | 143건 | 리뷰 601 |
# score_cnt, review_cnt에서 숫자만 유지
import re
p = re.compile('\D')
for row in df_raw.index:
df_raw.loc[row, 'score_cnt'] = p.sub('', df_raw.loc[row, 'score_cnt'])
df_raw.loc[row, 'review_cnt'] = p.sub('', df_raw.loc[row, 'review_cnt'])
df_raw.head()
name | address | score | score_cnt | review_cnt | |
---|---|---|---|---|---|
0 | 블루보틀 성수점 | 성수동1가 656-302 | 2.8 | 85 | 635 |
1 | 할아버지공장 | 성수동2가 309-133 | 3.0 | 27 | 257 |
2 | 차 | 성수동1가 685-408 | 2.6 | 28 | 120 |
3 | 어니언 | 성수동2가 277-135 | 3.4 | 140 | 1257 |
4 | 대림창고 | 성수동2가 322-32 | 3.3 | 143 | 601 |
df_raw['score_cnt'] = df_raw['score_cnt'].astype(int)
df_raw['review_cnt'] = df_raw['review_cnt'].astype(int)
df_raw['score'] = df_raw['score'].astype(float)
df_raw.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 504 entries, 0 to 504 Data columns (total 5 columns): name 504 non-null object address 504 non-null object score 504 non-null float64 score_cnt 504 non-null int32 review_cnt 504 non-null int32 dtypes: float64(1), int32(2), object(2) memory usage: 39.7+ KB
df_raw.sort_values('score', ascending=False)
name | address | score | score_cnt | review_cnt | |
---|---|---|---|---|---|
504 | 큐토스침니 | 화양동 33-30 | 5.0 | 3 | 0 |
78 | 더메이즈 건대점 | 자양동 10-1 | 5.0 | 4 | 81 |
187 | 커피코코 | 성수동2가 278-35 | 5.0 | 1 | 1 |
93 | 위커파크 성수점 | 성수동2가 301-3 | 5.0 | 1 | 21 |
92 | 업사이드 뚝섬점 | 성수동1가 7-8 | 5.0 | 2 | 28 |
... | ... | ... | ... | ... | ... |
370 | 얌커피로스터스 | 성수동2가 279-50 | 0.0 | 0 | 0 |
135 | 카페도르 | 성수동2가 275-75 | 0.0 | 0 | 2 |
372 | CM카페 | 성수동1가 13-17 | 0.0 | 0 | 0 |
373 | 에잇어클락 | 성수동2가 299-241 | 0.0 | 0 | 2 |
255 | 언니커피 | 성수동2가 532-8 | 0.0 | 0 | 1 |
504 rows × 5 columns
df_raw.score_cnt.describe()
count 504.000000 mean 4.529762 std 12.070663 min 0.000000 25% 0.000000 50% 1.000000 75% 4.000000 max 143.000000 Name: score_cnt, dtype: float64
df_raw.score_cnt.mean()
4.529761904761905
df_raw[df_raw.score_cnt != 0].score_cnt.describe()
count 306.000000 mean 7.460784 std 14.776591 min 1.000000 25% 1.000000 50% 3.000000 75% 7.000000 max 143.000000 Name: score_cnt, dtype: float64
df_filter1 = df_raw[df_raw.score_cnt >= 7]
print('평가를 7건 이상 받은 카페: 총 {}개 중 {}개 ({:.2f}%)'.format(len(df_raw), len(df_filter1), (len(df_filter1) / len(df_raw)*100)))
평가를 7건 이상 받은 카페: 총 504개 중 84개 (16.67%)
df_filter1.sort_values('score', ascending=False).head()
name | address | score | score_cnt | review_cnt | |
---|---|---|---|---|---|
318 | 스텀필즈커피 성수점 | 성수동2가 289-319 | 4.8 | 16 | 0 |
102 | 브루잉세레모니 | 성수동2가 315-27 | 4.8 | 10 | 30 |
84 | 커피오스 | 성수동2가 333-72 | 4.7 | 25 | 130 |
38 | 하루앤원데이 | 성수동2가 314-5 | 4.6 | 7 | 38 |
55 | 카페라잌유 | 성수동1가 656-1017 | 4.5 | 8 | 100 |
df_filter1.review_cnt.describe()
count 84.000000 mean 142.357143 std 181.435550 min 0.000000 25% 38.750000 50% 94.500000 75% 160.250000 max 1257.000000 Name: review_cnt, dtype: float64
어떤 것을 기준으로 해야할까?
print('중앙값 기준: {}개 중 {}개 (당연하게도, {:.2f}%)\n평균 기준: {}개 중 {}개 ({:.2f}%)'.format(
len(df_filter1),
len(df_filter1[df_filter1.review_cnt > 94]),
len(df_filter1[df_filter1.review_cnt > 94]) / len(df_filter1)*100,
len(df_filter1),
len(df_filter1[df_filter1.review_cnt > 142]),
len(df_filter1[df_filter1.review_cnt > 142]) / len(df_filter1)*100))
중앙값 기준: 84개 중 42개 (당연하게도, 50.00%) 평균 기준: 84개 중 25개 (29.76%)
df_filter2 = df_filter1[df_filter1.review_cnt >= 94]
df_filter2.sort_values('score', ascending=False).reset_index(drop=True).head(10)
name | address | score | score_cnt | review_cnt | |
---|---|---|---|---|---|
0 | 커피오스 | 성수동2가 333-72 | 4.7 | 25 | 130 |
1 | 카페라잌유 | 성수동1가 656-1017 | 4.5 | 8 | 100 |
2 | 치카치카 | 성수동2가 512 | 4.5 | 12 | 117 |
3 | 리도엘리펀트 | 성수동2가 333-97 | 4.4 | 18 | 137 |
4 | 로우커피스탠드 | 성수동1가 8-16 | 4.0 | 26 | 107 |
5 | 카페오롯 | 성수동1가 13-428 | 3.9 | 10 | 151 |
6 | 우디집 | 성수동1가 311 | 3.9 | 7 | 192 |
7 | 멜로워 성수플래그쉽 | 성수동2가 333-94 | 3.8 | 27 | 392 |
8 | 커피식탁 | 성수동1가 656-1096 | 3.8 | 13 | 117 |
9 | 훔볼트 | 성수동2가 325-17 | 3.7 | 27 | 324 |
df_raw.sort_values(['score_cnt', 'review_cnt'], ascending=False).reset_index(drop=True).head(10)
name | address | score | score_cnt | review_cnt | |
---|---|---|---|---|---|
0 | 대림창고 | 성수동2가 322-32 | 3.3 | 143 | 601 |
1 | 어니언 | 성수동2가 277-135 | 3.4 | 140 | 1257 |
2 | 블루보틀 성수점 | 성수동1가 656-302 | 2.8 | 85 | 635 |
3 | 자그마치 | 성수동2가 317-12 | 3.2 | 71 | 319 |
4 | 오르에르 | 성수동1가 16-39 | 3.4 | 63 | 325 |
5 | 메쉬커피 | 성수동1가 685-307 | 4.1 | 40 | 58 |
6 | 카페성수 | 성수동1가 668-30 | 3.3 | 39 | 187 |
7 | 어반소스 | 성수동2가 301-16 | 3.6 | 36 | 605 |
8 | 카멜 성수점 | 성수동2가 570-1 | 3.6 | 35 | 383 |
9 | 바이산 | 성수동2가 322-32 | 3.1 | 33 | 329 |
인지도 점수(Awareness) = 평가 수 + (리뷰 수/10)
df_raw['awareness'] = df_raw['score_cnt'] + df_raw['review_cnt']/10
df_raw.sort_values('awareness', ascending=False).reset_index(drop=True).head(10)
name | address | score | score_cnt | review_cnt | awareness | |
---|---|---|---|---|---|---|
0 | 어니언 | 성수동2가 277-135 | 3.4 | 140 | 1257 | 265.7 |
1 | 대림창고 | 성수동2가 322-32 | 3.3 | 143 | 601 | 203.1 |
2 | 블루보틀 성수점 | 성수동1가 656-302 | 2.8 | 85 | 635 | 148.5 |
3 | 자그마치 | 성수동2가 317-12 | 3.2 | 71 | 319 | 102.9 |
4 | 어반소스 | 성수동2가 301-16 | 3.6 | 36 | 605 | 96.5 |
5 | 오르에르 | 성수동1가 16-39 | 3.4 | 63 | 325 | 95.5 |
6 | 카멜 성수점 | 성수동2가 570-1 | 3.6 | 35 | 383 | 73.3 |
7 | 멜로워 성수플래그쉽 | 성수동2가 333-94 | 3.8 | 27 | 392 | 66.2 |
8 | 바이산 | 성수동2가 322-32 | 3.1 | 33 | 329 | 65.9 |
9 | 훔볼트 | 성수동2가 325-17 | 3.7 | 27 | 324 | 59.4 |
plt.figure(figsize=(10, 10))
sns.scatterplot('score', 'awareness', s=30, data=df_raw)
plt.title('별점 - 인지도 산점도')
for row in df_raw.sort_values('awareness', ascending=False).head(10).index:
plt.text(df_raw.loc[row, 'score'],
df_raw.loc[row, 'awareness'],
df_raw.loc[row, 'name'],
rotation=20)
import googlemaps
gmap_key = "*****"
gmaps = googlemaps.Client(key=gmap_key)
from tqdm import tqdm_notebook
# 혹시 모를 사태를 대비하여 원본을 복사하여 사용
df = df_raw.copy()
# 구글맵API를 활용하여 각 카페의 주소에 해당하는 지리 정보를 얻어오기
for row in tqdm_notebook(df.index):
try:
geo = gmaps.geocode(str(df.loc[row, 'address']))
df.loc[row, 'lat'] = geo[0].get('geometry')['location']['lat']
df.loc[row, 'lng'] = geo[0].get('geometry')['location']['lng']
except:
df.loc[row, 'lat'] = np.nan
df.loc[row, 'lng'] = np.nan
df.head()
HBox(children=(IntProgress(value=0, max=504), HTML(value='')))
name | address | score | score_cnt | review_cnt | awareness | lat | lng | |
---|---|---|---|---|---|---|---|---|
0 | 블루보틀 성수점 | 성수동1가 656-302 | 2.8 | 85 | 635 | 148.5 | 37.548074 | 127.045617 |
1 | 할아버지공장 | 성수동2가 309-133 | 3.0 | 27 | 257 | 52.7 | 37.541084 | 127.054905 |
2 | 차 | 성수동1가 685-408 | 2.6 | 28 | 120 | 40.0 | 37.547794 | 127.041883 |
3 | 어니언 | 성수동2가 277-135 | 3.4 | 140 | 1257 | 265.7 | 37.544644 | 127.058323 |
4 | 대림창고 | 성수동2가 322-32 | 3.3 | 143 | 601 | 203.1 | 37.541797 | 127.056481 |
import missingno as msno
msno.matrix(df, figsize=(10, 5))
plt.show()
df.isnull().sum()
name 0 address 0 score 0 score_cnt 0 review_cnt 0 awareness 0 lat 0 lng 0 dtype: int64
df.to_csv('source/cafe_in_seongsu_map.csv')
map = folium.Map([df.lat.median(), df.lng.median()], zoom_start=15)
for row in df.index:
lat = df.lat[row]
lng = df.lng[row]
folium.Marker([lat, lng]).add_to(map)
map
map = folium.Map([37.5438344, 127.05451205], zoom_start=15,
tiles='Stamen WaterColor')
# 지하철 표시
folium.Marker([37.543512, 127.044663], popup='서울숲역').add_to(map)
folium.Marker([37.547169, 127.047424], popup='뚝섬역').add_to(map)
folium.Marker([37.544474, 127.056012], popup='성수역').add_to(map)
folium.Marker([37.540333, 127.069316], popup='건대입구역').add_to(map)
# 카페 표시
for row in df.index:
lat = df.lat[row]
lng = df.lng[row]
folium.CircleMarker([lat, lng], color='', fill=True, fill_color='#044275', radius=15).add_to(map)
# 카페 밀집 지역 표시
folium.CircleMarker([37.547072, 127.041576], color='#F247F5', radius=50).add_to(map)
folium.CircleMarker([37.542219, 127.055480], color='#F247F5', radius=50).add_to(map)
folium.CircleMarker([37.540277, 127.067754], color='#F247F5', radius=50).add_to(map)
map
map = folium.Map([37.544964, 127.049826], zoom_start=16,
tiles='stamen Toner')
for row in df.index:
lat = df.lat[row]
lng = df.lng[row]
folium.CircleMarker([lat, lng], radius=np.log(df.loc[row, 'awareness'])*10, color='', fill=True, fill_opacity=.6,
fill_color='#F8F8C7' if df.score[row] < 1 else
('#DFF7BE' if df.score[row] < 2 else
('#00AD2E' if df.score[row] < 3 else
('#00611A' if df.score[row] < 4 else '#007508')))).add_to(map)
# 지하철 표시
folium.Marker([37.543512, 127.044663], popup='서울숲역').add_to(map)
folium.Marker([37.547169, 127.047424], popup='뚝섬역').add_to(map)
folium.Marker([37.544474, 127.056012], popup='성수역').add_to(map)
folium.Marker([37.540333, 127.069316], popup='건대입구역').add_to(map)
map