[Project] 연희-연남-신촌동 카페 추천기

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
import re
import googlemaps
import missingno as msno
from tqdm import tqdm_notebook
import math
# 스타일 변경으로 인해 폰트 다시 설정
plt.rc('font', family='NanumGothic')
plt.rc('font', size=13)
driver = webdriver.Chrome('C:/Users\jej_0312_@naver.com/chromedriver_win32/chromedriver.exe')
driver.get('https://map.kakao.com/')
# driver.find_element_by_id('search.keyword.bounds').click()    # 현 지도 내 장소 검색
# driver.implicitly_wait(10)
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()    # 더 보기 클릭
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): # 한 페이지에 총 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() # 현재 페이지 +1인 페이지 선택
        time.sleep(1)
    except:
        # print('페이지 초과: {}'.format(pagenum))
        continue
df_raw = pd.DataFrame(dict_cafe)
df_raw.to_csv('source/cafe_in_yeonhee.csv')
  • 크롤링 과정을 다시 거치지 않기 위해 우선은 여기까지를 저장했다.
df_raw = pd.read_csv('source/cafe_in_yeonhee.csv', index_col=0)
df_raw.head()
name address score score_cnt review_cnt
0 앤트러사이트 연희점 (지번) 연희동 89-19 3.5 40건 리뷰 135
1 콘하스 연희점 (지번) 연희동 90-1 2.8 67건 리뷰 211
2 로도덴드론 (지번) 연희동 90-8 4.3 17건 리뷰 50
3 스타벅스 연희DT점 (지번) 연희동 87-8 3.7 20건 리뷰 22
4 매뉴팩트커피 연희본점 (지번) 연희동 130-2 4.5 61건 리뷰 157

2. 데이터 전처리

Null 값 삭제

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       501 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
  • address가 다른 column들에 비해 4개가 적은 것을 보면 address 열에서 4개의 결측치가 발견되었다.
df_raw[df_raw.address.isnull()]
name address score score_cnt review_cnt
280 스타벅스 가재울뉴타운점 NaN 4.8 5건 리뷰 12
316 카페마이바움 NaN 0.0 0건 리뷰 0
407 다르다 NaN 0.0 1건 리뷰 28
485 스타벅스 이대ECC점 NaN 3.0 8건 리뷰 11
  • ‘카페마이바움’과 ‘다르다’는 평점이 없으므로 삭제해도 무방할 듯 하다.
  • 나머지는 검색하여 값을 넣어주었다.
df_raw.drop(df_raw[(df_raw["name"] == '카페마이바움') | (df_raw["name"] == '다르다')].index, inplace = True)
df_raw[df_raw.address.isnull()]
name address score score_cnt review_cnt
280 스타벅스 가재울뉴타운점 NaN 4.8 5건 리뷰 12
485 스타벅스 이대ECC점 NaN 3.0 8건 리뷰 11
df_raw.reset_index(drop=True, inplace=True)
df_raw[df_raw.address.isnull()]
name address score score_cnt review_cnt
280 스타벅스 가재울뉴타운점 NaN 4.8 5건 리뷰 12
483 스타벅스 이대ECC점 NaN 3.0 8건 리뷰 11
df_raw.iloc[280,1] = "(지번) 남가좌동 165-1"
df_raw.iloc[483,1] = "(지번) 대현동 11-1"
df_raw[df_raw.address.isnull()]
name address score score_cnt review_cnt
df_raw.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 503 entries, 0 to 502
Data columns (total 5 columns):
name          503 non-null object
address       503 non-null object
score         503 non-null float64
score_cnt     503 non-null object
review_cnt    503 non-null object
dtypes: float64(1), object(4)
memory usage: 19.8+ KB

텍스트 처리

  • address는 ‘(지번)’을 삭제하고
  • 리뷰 건수와 평점 건수는 숫자만 남기자.
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 앤트러사이트 연희점 연희동 89-19 3.5 40건 리뷰 135
1 콘하스 연희점 연희동 90-1 2.8 67건 리뷰 211
2 로도덴드론 연희동 90-8 4.3 17건 리뷰 50
3 스타벅스 연희DT점 연희동 87-8 3.7 20건 리뷰 22
4 매뉴팩트커피 연희본점 연희동 130-2 4.5 61건 리뷰 157
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 앤트러사이트 연희점 연희동 89-19 3.5 40 135
1 콘하스 연희점 연희동 90-1 2.8 67 211
2 로도덴드론 연희동 90-8 4.3 17 50
3 스타벅스 연희DT점 연희동 87-8 3.7 20 22
4 매뉴팩트커피 연희본점 연희동 130-2 4.5 61 157
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'>
RangeIndex: 503 entries, 0 to 502
Data columns (total 5 columns):
name          503 non-null object
address       503 non-null object
score         503 non-null float64
score_cnt     503 non-null int32
review_cnt    503 non-null int32
dtypes: float64(1), int32(2), object(2)
memory usage: 15.8+ KB
# 전처리가 끝났으니 다시 저장하자.
# df_raw = pd.read_csv('source/cafe_in_yeonhee.csv', index_col=0)

3. 데이터 분석

  • 전처리는 끝났다.

별점이 높은 카페 찾기

df_raw.sort_values('score', ascending=False)
name address score score_cnt review_cnt
409 스완카페 남가좌동 211-19 5.0 2 1
95 브이카페 연희점 연희동 193-10 5.0 1 9
481 커피스튜디오 남가좌동 119-27 5.0 1 1
116 카페로이 연희동 163-1 5.0 1 1
435 매드히피 연남동 239-44 5.0 3 17
... ... ... ... ... ...
302 제이쇼콜라 홍은동 410-36 0.0 0 2
304 다향 연희동 137-13 0.0 0 0
305 피안타 연희동 190-21 0.0 0 6
310 오빌 연희동 92-18 0.0 0 0
502 커피볶는집 명지대점 남가좌동 342-10 0.0 0 0

503 rows × 5 columns

  • 평가 수가 적은 카페들이 상위에 랭크된다.
  • 따라서 평가 수가 일정 기준 이상인 데이터들을 위주로 분석하자.
  • 우선, 평가를 받지 않은 카페들을 제외하고 보자.
plt.hist(df_raw[df_raw.score_cnt != 0].score_cnt)
(array([338.,  32.,   9.,   2.,   2.,   0.,   0.,   0.,   1.,   1.]),
 array([  1. ,  25.8,  50.6,  75.4, 100.2, 125. , 149.8, 174.6, 199.4,
        224.2, 249. ]),
 <a list of 10 Patch objects>)

png

df_raw[df_raw.score_cnt != 0].score_cnt.describe()
count    385.000000
mean      12.150649
std       22.015339
min        1.000000
25%        2.000000
50%        5.000000
75%       13.000000
max      249.000000
Name: score_cnt, dtype: float64
  • 평균은 12개, 75%는 13개이다.
  • 12번 이상의 평가를 받은 카페들을 대상으로 다시 구해보자.
df_filter1 = df_raw[df_raw.score_cnt >= np.mean(df_raw[df_raw.score_cnt != 0].score_cnt)]
print('평가 개수가 평균 이상인 카페: 총 {}개 중 {}개 ({:.2f}%)'.format(len(df_raw), len(df_filter1), (len(df_filter1) / len(df_raw)*100)))
평가 개수가 평균 이상인 카페: 총 503개 중 99개 (19.68%)
df_filter1.sort_values('score', ascending=False).head()
name address score score_cnt review_cnt
89 땡스오트 연남동 375-113 4.8 17 233
303 하이드미플리즈 홍제동 330-226 4.8 17 81
28 쿳사 연희동 100-2 4.8 13 4
189 미드나잇플레저 연남동 241-18 4.7 14 35
20 르솔레이 연희동 192-4 4.7 15 36
  • 2번째로 나타난 ‘쿳사’는 리뷰 수가 상당히 적다.
  • 리뷰 카운트도 조건으로 설정해서 검색하는 것이 더 합당한 듯 하다.
df_filter1.review_cnt.describe()
count     99.000000
mean     142.212121
std      128.119545
min        1.000000
25%       53.500000
50%      104.000000
75%      200.500000
max      673.000000
Name: review_cnt, dtype: float64
plt.hist(df_filter1.review_cnt)
(array([31., 31., 12., 10.,  6.,  3.,  3.,  1.,  1.,  1.]),
 array([  1. ,  68.2, 135.4, 202.6, 269.8, 337. , 404.2, 471.4, 538.6,
        605.8, 673. ]),
 <a list of 10 Patch objects>)

png

  • 25%에 해당하는 값도 53개 정도이기 때문에 꽤나 신뢰할 수 있는 정도인 한 것 같다.
  • 따라서 25%를 기준으로 잡고 보자.
df_filter2 = df_filter1[df_filter1.review_cnt >= (df_filter1.review_cnt).quantile(.25)]
print('평점 개수가 평균 이상, 리뷰 개수가 전체의 25% 이상인 카페: 총 {}개 중 {}개 ({:.2f}%)'.format(len(df_filter1), len(df_filter2), (len(df_filter2) / len(df_filter1)*100)))
평점 개수가 평균 이상, 리뷰 개수가 전체의 25% 이상인 카페: 총 99개 중 74개 (74.75%)
df_filter2.sort_values('score', ascending=False).reset_index(drop=True).head(10)
name address score score_cnt review_cnt
0 하이드미플리즈 홍제동 330-226 4.8 17 81
1 땡스오트 연남동 375-113 4.8 17 233
2 루온루온 연남동 390-77 4.7 15 59
3 오랑지 연남동 390-71 4.6 21 184
4 매뉴팩트커피 연희본점 연희동 130-2 4.5 61 157
5 연남온도 연남동 241-26 4.5 15 77
6 테일러커피 서교1호점 서교동 329-15 4.4 39 174
7 러빈허 동교동 177-12 4.3 15 112
8 테일러커피 연남2호점 연남동 224-57 4.3 27 215
9 이미 동교동 201-10 4.3 69 150
  • 하이드미플리즈는 홍제동에 위치해있다.
    • 근처에 유명한 카페의 수가 적어 비교적 높은 평점을 받은 것이라 예상해본다.
    • 검색결과, 인스타감성을 저격한 카페 겸 맥주 바였다.

가장 유명한 카페 찾기

  • 리뷰 수가 많을수록 사람들이 많이 찾는 카페라 생각하고 찾아보자.
df_raw.sort_values(['score_cnt', 'review_cnt'], ascending=False).reset_index(drop=True).head(10)
name address score score_cnt review_cnt
0 호밀밭 창천동 4-77 3.9 249 120
1 커피리브레 연남점 연남동 227-15 3.3 209 112
2 테일러커피 서교2호점 서교동 338-1 4.2 108 198
3 클로리스 신촌점 창천동 13-35 4.0 103 123
4 아오이토리 서교동 327-17 3.5 94 249
5 수카라 서교동 327-9 4.2 88 134
6 옥루몽 신촌본점 대신동 50-3 3.1 75 11
7 이미 동교동 201-10 4.3 69 150
8 콘하스 연희점 연희동 90-1 2.8 67 211
9 연남살롱 연남동 504-33 3.6 62 117
  • 아까와는 달리, 신촌 부근의 카페들이 많이 등장했다.
    • 연남동이나 연희동에 비해 유동인구가 많아 더 잘 알려진 곳이라 판단했다.

인지도 지표

  • 이번에는 리뷰 수와 평가 수가 모두 높은 카페들을 찾아보려고 한다.
    • 평가보다 리뷰를 남기는 것이 더 정성적인 방법의 평가이며, 리뷰를 통해 또 다른 소비자를 끌어낼 수 있다는 점에서 리뷰수에 더 큰 점수를 부여하자.
    • 평가 수를 10%의 가산점이라 생각하고 계산한다.

인지도 지수 = 평가 수(70%) + 리뷰 수(100%)

  • 우선, 각 기준들에 똑같은 범위의 값을 주기 위해, 스케일링을 해준 후에 사용하자.
plt.hist(df_raw.review_cnt)
(array([395.,  61.,  19.,  12.,   6.,   4.,   3.,   1.,   1.,   1.]),
 array([  0. ,  67.3, 134.6, 201.9, 269.2, 336.5, 403.8, 471.1, 538.4,
        605.7, 673. ]),
 <a list of 10 Patch objects>)

png

df_raw.review_cnt.describe()
count    503.000000
mean      46.276342
std       81.662221
min        0.000000
25%        1.000000
50%       13.000000
75%       54.000000
max      673.000000
Name: review_cnt, dtype: float64
plt.hist(df_raw.score_cnt)
(array([454.,  34.,   8.,   3.,   2.,   0.,   0.,   0.,   1.,   1.]),
 array([  0. ,  24.9,  49.8,  74.7,  99.6, 124.5, 149.4, 174.3, 199.2,
        224.1, 249. ]),
 <a list of 10 Patch objects>)

png

df_raw.score_cnt.describe()
count    503.000000
mean       9.300199
std       19.932639
min        0.000000
25%        1.000000
50%        3.000000
75%       10.000000
max      249.000000
Name: score_cnt, dtype: float64
  • 두 기준 모두 right-skewed 되어있고 0의 값이 존재한다.
  • 리뷰 수와 평가 건수의 범위가 다르기 때문에 각각 스케일링을 한 후 지표에 사용해야할 것 같다.
    • 같은 범위로 만들기 위해 min max scaler를 사용하여 0과 1 사이의 값들로 만들어준다.
# 정규화를 할 때는 제곱근 변환보다는 로그 변환이 적합할 것 같다.  
#  - 0 값들은 0.1로 대체한 후 로그 변환을 해준다?
#   - 이 방법은 기존에 건수가 0이었던 카페들의 인지도 지수가 음수가 나올 수 있다.
#   - 리뷰 건수가 많이 있다고 하더라도 평가 건수의 음수값이 커서 평점과 리뷰 건수가 모두 조금씩만 있는 카페들보다 더 낮은 값들이 나올 수 있겠다.
#  - 따라서 0은 영향이 없다는 의미에서 0으로 그대로 두고 1은 로그함수를 취하면 0이 되기 때문에 미묘한 차이를 두어 1.1로 바꾸어서 하자.
# df_new = df_raw.copy()
# df_new["score_cnt"] = df_new["score_cnt"].apply(lambda x: 1.1 if x == 1 else x)
# df_new["review_cnt"] = df_new["review_cnt"].apply(lambda x: 1.1 if x == 1 else x)
# df_new["score_cnt"] = df_new["score_cnt"].apply(lambda x: np.log(x) if x != 0 else x)
# plt.hist(df_new.score_cnt)
df_new = df_raw.copy()
df_new["score_cnt"] = (df_new["score_cnt"] - df_new["score_cnt"].min())/(df_new["score_cnt"].max() - df_new["score_cnt"].min())
df_new["review_cnt"] = (df_new["review_cnt"] - df_new["review_cnt"].min())/(df_new["review_cnt"].max() - df_new["review_cnt"].min())
plt.hist(df_new["score_cnt"])
(array([454.,  34.,   8.,   3.,   2.,   0.,   0.,   0.,   1.,   1.]),
 array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ]),
 <a list of 10 Patch objects>)

png

plt.hist(df_new["review_cnt"])
(array([395.,  61.,  19.,  12.,   6.,   4.,   3.,   1.,   1.,   1.]),
 array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ]),
 <a list of 10 Patch objects>)

png

  • 이를 지표에 사용할 수 있도록 70%의 가중치를 주자.
df_new["score_cnt"] = 0.7 * df_new["score_cnt"]
df_new["score_cnt"]
0      0.112450
1      0.188353
2      0.047791
3      0.056225
4      0.171486
         ...   
498    0.042169
499    0.000000
500    0.008434
501    0.014056
502    0.000000
Name: score_cnt, Length: 503, dtype: float64
plt.hist(df_new["score_cnt"])
(array([454.,  34.,   8.,   3.,   2.,   0.,   0.,   0.,   1.,   1.]),
 array([0.  , 0.07, 0.14, 0.21, 0.28, 0.35, 0.42, 0.49, 0.56, 0.63, 0.7 ]),
 <a list of 10 Patch objects>)

png

df_raw['popularity'] = df_new['score_cnt'] + df_new['review_cnt']
df_raw.sort_values('popularity', ascending=False).reset_index(drop=True).head(10)
name address score score_cnt review_cnt popularity
0 레이어드 연남점 연남동 223-20 2.5 44 673 1.123695
1 하이웨이스트 연남동 223-80 2.8 20 566 0.897235
2 호밀밭 창천동 4-77 3.9 249 120 0.878306
3 딩가케이크 연남동 252-18 2.6 45 496 0.863505
4 커피리브레 연남점 연남동 227-15 3.3 209 112 0.753969
5 모파상 동교동 153-5 2.6 49 412 0.749935
6 콩카페 연남점 연남동 223-114 3.3 41 407 0.720016
7 카페스콘 연남동 239-4 3.6 20 413 0.669895
8 아오이토리 서교동 327-17 3.5 94 249 0.634242
9 얼스어스 연남동 239-49 4.2 40 350 0.632509
  • 아까는 보이지 않던 카페들이 대거 등장했다.
    • 리뷰는 남기지 않고 평점만 기록한 카페인 경우로 생각할 수 있다.
    • 적극적으로 평가를 남기는 소비자보다 평점만 기록한 소비자가 많다는 것은 입소문을 타고 온 소비자들이 많다는 의미일 수 있겠다.

인지도와 평점을 한 눈에 보자.

plt.figure(figsize=(10, 10))
sns.scatterplot('score', 'popularity', s=30, data=df_raw)
plt.title('별점 - 인지도 산점도')
for row in df_raw.sort_values('popularity', ascending=False).head(10).index:
    plt.text(df_raw.loc[row, 'score'],
             df_raw.loc[row, 'popularity'],
             df_raw.loc[row, 'name'],
             rotation=20)

png

  • 레이어드 연남이나 딩가케이크, 하이웨스트, 모파상 등의 인지도는 높은 반면, 평점은 3~4점 사이를 기록하고 있다.
    • 소문에 비해 맛이나 가격 등이 적당하지 않았던 듯하다.
    • 이런 카페들은 다시 가고 싶은 카페라기 보다는 일회성 방문이 많을 것이라 예상해본다.
  • 호밀밭은 적당한 인지도에 적당한 평점을 갖고 있다.
    • 이런 카페들일수록 재방문율이 높고, 주변 지인들에게 소개할 확률이 높아질 것이다.
  • 얼스어스의 경우는 상위 10개의 카페들 중 평점은 가장 높고 인지도는 가장 낮다.
    • 더 유명해질 가능성이 있는 카페이며, 재방문율이 가장 높을 것이라 예상된다.

지도 시각화

gmap_key = "#######################"
gmaps = googlemaps.Client(key=gmap_key)
# 혹시 모를 사태를 대비하여 원본을 복사하여 사용
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=503), HTML(value='')))
name address score score_cnt review_cnt popularity lat lng
0 앤트러사이트 연희점 연희동 89-19 3.5 40 135 0.313044 37.569648 126.932820
1 콘하스 연희점 연희동 90-1 2.8 67 211 0.501875 37.570284 126.932176
2 로도덴드론 연희동 90-8 4.3 17 50 0.122085 37.570100 126.932233
3 스타벅스 연희DT점 연희동 87-8 3.7 20 22 0.088914 37.570118 126.933859
4 매뉴팩트커피 연희본점 연희동 130-2 4.5 61 157 0.404770 37.567758 126.929598
msno.matrix(df, figsize=(10, 5))
plt.show()

png

# 위치까지 포함된 파일로 다시 저장하자.
df.to_csv('source/cafe_in_yeonhee_map.csv')
df = pd.read_csv('source/cafe_in_yeonhee_map.csv', index_col=0)
# 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([df.lat.median(), df.lng.median()], zoom_start=15)
#                 tiles='Stamen WaterColor')

# 지하철 표시
folium.Marker([37.557646, 126.9222673], popup='홍대입구역 2호선').add_to(map)
folium.Marker([37.558335, 126.9237427], popup='홍대입구역 공항철도').add_to(map)
folium.Marker([37.559778, 126.9401363], popup='신촌역 경의중앙선').add_to(map)
folium.Marker([37.555143, 126.9346963], popup='신촌역 2호선').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, popup=([lat,lng])).add_to(map)

# 카페 밀집 지역 표시
folium.CircleMarker([37.5681379, 126.9311559], color='#F247F5', radius=50).add_to(map)
folium.CircleMarker([37.5652905, 126.9235006], color='#F247F5', radius=50).add_to(map)
folium.CircleMarker([37.5621316, 126.9264844], color='#F247F5', radius=50).add_to(map)
folium.CircleMarker([37.56632949999999, 126.9279715], color='#F247F5', radius=50).add_to(map)


map
  • (시각화 결과는 생략한다.)
  • 카페가 많이 위치한 지역은 크게 4군데로 나눌 수 있다.
  • 대부분은 연남동이거나 연희동이며 그 중에서도 연남동에 훨씬 많은 카페들이 있다는 것을 알 수 있다.
    • 그 외에도 신촌역이나 명지대학교 근처는 대학교 주변 상권이라 카페들이 일정 수준 존재한다는 것을 볼 수 있다.
map = folium.Map([df.lat.median(), df.lng.median()], zoom_start=16)
#                tiles='stamen Toner')


for row in df.index:
    lat = df.lat[row]
    lng = df.lng[row]
    folium.CircleMarker([lat, lng], radius=df.loc[row, 'popularity']*20, color='', fill=True, fill_opacity=.6,
                        popup=(df.loc[row, 'name']),
                        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.557646, 126.9222673], popup='홍대입구역 2호선').add_to(map)
folium.Marker([37.558335, 126.9237427], popup='홍대입구역 공항철도').add_to(map)
folium.Marker([37.559778, 126.9401363], popup='신촌역 경의중앙선').add_to(map)
folium.Marker([37.555143, 126.9346963], popup='신촌역 2호선').add_to(map)


map.save('cafe_in_yeonhee_map.html')
map

  • 색이 진할수록 평점이 높고, 원이 클수록 인지도가 높다.
    • 노란색으로 나타난 카페들은 리뷰는 존재하지만 평점이 없는 경우이다.
  • 연남동에는 인지도가 높은 카페들이 많은 반면, 연희동은 그렇지 않다.
    • 아직 연희동은 연남동에 비해 잘 알려지지 않은 동네이지만 유명한 카페들이 일정 수준 존재하기 때문에 이 카페들을 중심으로 주변 상권을 살리기 좋을 것 같다.
  • 연남동은 평균적으로 카페들의 평점이 높은 편이며, 연희동은 평점이 존재하는 대부분의 카페들이 좋은 평가를 받고 있다.
    • 그러나 여전히 잘 알려지지 않은 카페들이 많고, 이 때문에 평점이 메겨지지 않는다고 생각할 수 있다.
    • 혹은 이제 막 생겨나기 시작한 카페들이 많을 것이다. 유명해질 가능성이 있는 카페들이다.
    • 숨은 보석 찾기가 필요하다..라고 해두자.
df[df["score"] == 0]
name address score score_cnt review_cnt popularity lat lng
7 일룸 엄마의서재 연희동 87-5 0.0 0 26 0.038633 37.570210 126.934134
16 연희입니다 연희동 151-148 0.0 0 23 0.034175 37.575933 126.933732
32 연희라운지 연희동 80-1 0.0 0 2 0.002972 37.571397 126.934932
39 늬에게 연희동 195-12 0.0 0 4 0.005944 37.573028 126.928954
45 라파르벨라 연희동 76-19 0.0 0 1 0.001486 37.573656 126.936133
... ... ... ... ... ... ... ... ...
492 마이 블러드타입이즈커피 연남동 224-51 0.0 0 4 0.005944 37.563560 126.927189
494 515티룸 홍은동 401-1 0.0 0 1 0.001486 37.581797 126.925234
497 커피볶는집 명지대점 남가좌동 342-10 0.0 0 0 0.000000 37.578273 126.923529
499 515티룸 홍은동 401-1 0.0 0 1 0.001486 37.581797 126.925234
502 커피볶는집 명지대점 남가좌동 342-10 0.0 0 0 0.000000 37.578273 126.923529

121 rows × 8 columns

현재 거주지인 연희동만 위주로 보자.

df = pd.read_csv('source/cafe_in_yeonhee_map.csv', index_col = 0)
df_yh = df.copy()

for row in df_yh.index:
    if (df_yh.address[row][0:3] != "연희동"):
        df_yh.drop(row, inplace=True)

df_yh.head()
name address score score_cnt review_cnt popularity lat lng
0 앤트러사이트 연희점 연희동 89-19 3.5 40 135 0.313044 37.569648 126.932820
1 콘하스 연희점 연희동 90-1 2.8 67 211 0.501875 37.570284 126.932176
2 로도덴드론 연희동 90-8 4.3 17 50 0.122085 37.570100 126.932233
3 스타벅스 연희DT점 연희동 87-8 3.7 20 22 0.088914 37.570118 126.933859
4 매뉴팩트커피 연희본점 연희동 130-2 4.5 61 157 0.404770 37.567758 126.929598
plt.figure(figsize=(10, 10))
sns.scatterplot('score', 'popularity', s=30, data=df_yh)
plt.title('연희동 별점 - 인지도 산점도')
for row in df_yh.sort_values('popularity', ascending=False).head(10).index:
    plt.text(df_yh.loc[row, 'score'],
             df_yh.loc[row, 'popularity'],
             df_yh.loc[row, 'name'],
             rotation=20)

png

df_yh[df_yh.score_cnt != 0].score_cnt.describe()
count    99.000000
mean      7.181818
std      11.251634
min       1.000000
25%       1.000000
50%       3.000000
75%       8.000000
max      67.000000
Name: score_cnt, dtype: float64
df_filter = df_yh[df_yh.score_cnt >= np.mean(df_yh[df_yh.score_cnt != 0].score_cnt)]
print('평가 개수가 평균 이상인 카페: 총 {}개 중 {}개 ({:.2f}%)'.format(len(df_yh), len(df_filter), (len(df_filter) / len(df_yh)*100)))
df_filter.sort_values(['score', 'popularity'], ascending=False).head(10)
평가 개수가 평균 이상인 카페: 총 161개 중 26개 (16.15%)
name address score score_cnt review_cnt popularity lat lng
28 쿳사 연희동 100-2 4.8 13 4 0.042490 37.572141 126.929376
104 카페공든 연희동 310-5 4.8 9 10 0.040160 37.564220 126.932816
13 시간이머무는홍차가게 연희동 69-3 4.7 20 33 0.105259 37.568310 126.932584
20 르솔레이 연희동 192-4 4.7 15 36 0.095661 37.566329 126.927972
36 푸어링아웃 연희동 128-27 4.6 11 38 0.087387 37.567252 126.928118
4 매뉴팩트커피 연희본점 연희동 130-2 4.5 61 157 0.404770 37.567758 126.929598
320 마리아칼라스 연희동 340-16 4.5 27 3 0.080361 37.563277 126.932272
34 맨팅 연희동 79-8 4.5 8 3 0.026948 37.572796 126.936156
2 로도덴드론 연희동 90-8 4.3 17 50 0.122085 37.570100 126.932233
41 올레무스 연희동 220-40 4.1 16 93 0.183167 37.565373 126.926028

왜 인기가 많은걸까.

  • 평점과 함께 적혀있는 리뷰들을 분석해보겠다.
    • 평점의 점수와 직접적으로 관련이 있을 것이다.
  • 상위 10개의 카페들만 분석해보겠다.

  • 검색속도 향상을 위해 각 카페의 고유 번호를 먼저 찾았다.
    • 쿳사: 705202577
    • 카페공든: 1369321588
    • 시간이머무는홍차가게: 23446983
    • 르솔레이: 1161755224
    • 푸어링아웃: 1980694762
    • 매뉴팩트커피 연희본점: 21542432
    • 마리아칼라스: 22482531
    • 맨팅: 98345425
    • 로도덴드론: 205823441
    • 올레무스: 961299046
df_high = df_filter.sort_values(['score', 'popularity'], ascending=False).head(10)
df_high['number'] = [705202577, 1369321588, 23446983, 1161755224, 1980694762, 21542432, 22482531, 98345425, 205823441, 961299046]
df_high
name address score score_cnt review_cnt popularity lat lng number
28 쿳사 연희동 100-2 4.8 13 4 0.042490 37.572141 126.929376 705202577
104 카페공든 연희동 310-5 4.8 9 10 0.040160 37.564220 126.932816 1369321588
13 시간이머무는홍차가게 연희동 69-3 4.7 20 33 0.105259 37.568310 126.932584 23446983
20 르솔레이 연희동 192-4 4.7 15 36 0.095661 37.566329 126.927972 1161755224
36 푸어링아웃 연희동 128-27 4.6 11 38 0.087387 37.567252 126.928118 1980694762
4 매뉴팩트커피 연희본점 연희동 130-2 4.5 61 157 0.404770 37.567758 126.929598 21542432
320 마리아칼라스 연희동 340-16 4.5 27 3 0.080361 37.563277 126.932272 22482531
34 맨팅 연희동 79-8 4.5 8 3 0.026948 37.572796 126.936156 98345425
2 로도덴드론 연희동 90-8 4.3 17 50 0.122085 37.570100 126.932233 205823441
41 올레무스 연희동 220-40 4.1 16 93 0.183167 37.565373 126.926028 961299046

크롤링

number = list(df_high.number)
name = list(df_high.name)
urls = []

for row in range(len(number)):
    url = 'https://place.map.kakao.com/' + str(number[row])
    urls.append(url)
    print(url)
    
https://place.map.kakao.com/705202577
https://place.map.kakao.com/1369321588
https://place.map.kakao.com/23446983
https://place.map.kakao.com/1161755224
https://place.map.kakao.com/1980694762
https://place.map.kakao.com/21542432
https://place.map.kakao.com/22482531
https://place.map.kakao.com/98345425
https://place.map.kakao.com/205823441
https://place.map.kakao.com/961299046
driver = webdriver.Chrome('C:/Users/jej_0312_@naver.com/chromedriver_win32/chromedriver.exe')

dt_review = {'name': [], 'rating': [], 'review': []}
for url in tqdm_notebook(urls):
    driver.get(url)
    time.sleep(4)
    html = driver.page_source # url마다 parsing하고
    soup = BeautifulSoup(html, 'lxml')
    reviewcount = int(soup.find_all('span', class_='color_b')[2].text)
    pagenum = 1

    while pagenum <= (math.ceil(reviewcount / 5)):
        html = driver.page_source # page마다 parsing하자
        soup = BeautifulSoup(html, 'lxml')

        for cafenum in range(len(soup.find_all('p', class_='txt_comment'))):
            information = []
            try:
                information.append(soup.select('.tit_location')[0].text)
                information.append(soup.find_all('em', class_='num_rate')[cafenum + 2].text)
                information.append(soup.find_all('p', class_='txt_comment')[cafenum].text)
            except Exception as e:
                print(soup.select('.tit_location')[0].text, e)
                continue
            if len(information) == 3: # 카페 이름, 평점, 리뷰가 다 있을 경우에만 parsing하자
                information[2].replace("더보기","")
                dt_review['name'].append(information[0])
                dt_review['rating'].append(information[1])
                dt_review['review'].append(information[2])
                # print('{} 페이지 {}/{}번째 댓글 완료'.format(pagenum, cafenum + 1, len(soup.find_all('p', class_='txt_comment'))))

        if pagenum == 5:
            driver.find_element_by_css_selector('a.btn_next').click()
            pagenum += 1
        if pagenum == 10:
            driver.find_element_by_css_selector('a.btn_next').click()
        elif pagenum == math.ceil(reviewcount / 5):
            break
        else:
            driver.find_element_by_xpath('//a[@data-page="{}"]'.format(pagenum+1)).click()

        pagenum += 1
        driver.implicitly_wait(10)
        time.sleep(2)

#print(dt_review)

HBox(children=(IntProgress(value=0, max=10), HTML(value='')))

  name rating                                             review
0   쿳사     5점                       너무 좋아요! 재방문 몇 번이고 하고 싶은 곳더보기
1   쿳사     5점  맛있어요, 가게는 작지만 식물이 가득하고 햇살도 잘 들어서 오랫동안 앉아있다 오고싶...
2   쿳사     4점  사장님이 굉장히 착하시고 요즘엔 마스크도 다 쓰고 영업 하신다. 서비스는 대만족. ...
3   쿳사     5점   후 나만 알고싶지만 이걸 읽고있다면 어차피 여길 오겠죠 ㅋㅋㅋ 사진을 보세요.. 더보기
4   쿳사     4점  에그 베네딕트와 뇨끼, 라떼. 매우 만족스러운 브런치였다. 근데 가게 분들 왜 마스...
5   쿳사     5점                                     맛있고 멋있는 곳 !더보기
6   쿳사     5점                                   친절하고 맛있고 주차되고더보기
7   쿳사     5점  콰트로치즈뇨끼와 하우스와인이 정말 맛있었고 티라미수, 커피도 다 맛있었다. 매장 예...
8   쿳사     5점                  밑의 분이 추천해 주셔서 잘 먹었습니다 뇨끼 맛있네요!더보기
9   쿳사     5점  뇨끼뇨끼!!! 여러분 뇨끼를 드십시오 1인 1뇨끼도 아쉽습니다 식전 메뉴와 파스타는...
df_yh = pd.DataFrame(dt_review)
df_yh.head(5)
name rating review
0 쿳사 5점 너무 좋아요! 재방문 몇 번이고 하고 싶은 곳더보기
1 쿳사 5점 맛있어요, 가게는 작지만 식물이 가득하고 햇살도 잘 들어서 오랫동안 앉아있다 오고싶...
2 쿳사 4점 사장님이 굉장히 착하시고 요즘엔 마스크도 다 쓰고 영업 하신다. 서비스는 대만족. ...
3 쿳사 5점 후 나만 알고싶지만 이걸 읽고있다면 어차피 여길 오겠죠 ㅋㅋㅋ 사진을 보세요.. 더보기
4 쿳사 4점 에그 베네딕트와 뇨끼, 라떼. 매우 만족스러운 브런치였다. 근데 가게 분들 왜 마스...
for i in range(len(df_yh)):
    df_yh.iloc[i,2] = df_yh.iloc[i,2].replace("더보기","")
  • 저장해주자.
df_yh.to_csv('source/cafe_in_yeonhee_review.csv', encoding = 'UTF-8')

댓글남기기