[학회] 리뷰 데이터 기반 개인 맞춤형 음식점 추천 시스템

📌 프로젝트 개요


주관 : 2022 동덕여자대학교 정보통계학과 과동아리 OUTLIER 프로젝트
2023 한국정보처리학회 학부생 논문 경진대회 - 빅데이터 분야
프로젝트 명 : 리뷰데이터 기반 개인 맞춤형 음식점 추천 시스템
프로젝트 기간 : 22년 3월 ~ 22년 7월 / 23년 4월 ~ 23년 5월
인원 : 4명 (동덕여자대학교 정보통계학과 4명)
활용 Tool : Python, Notion, Selenium, PPT
☐ 프로젝트 개요 : 많은 리뷰를 분석하여 기준(맛·분위기·서비스)에 따라 음식점 평가를 객관화 한 뒤, 사용자가 원하는 조건에 부합하는 음식점 추천 시스템 개발
☐ 본인이 맡은 역할 :
  • 자료 수집 및 전처리 :
    • Selenium을 이용하여 음식점 메뉴/가격/리뷰 크롤링
    • TF-IDF를 사용하여 각 상품에 맞는 키워드 선별
    • 긍.부정 단어 추출 후 코사인 유사도 분석
  • PPT, 논문 총괄 제작
  • 발표
☐ 성과/의의 :
  • 소비자 측면에서는 시간과 노력을 줄여 현명한 선택을 할 수 있도록 도모
  • 판매자 측면에서는 소비자의 니즈를 파악하여 마케팅 전략/제품개발전략 수립을 목적으로 활용할 수 있도록 도모
 

📑 프로젝트 내용


❓주제 선정 배경

고객들은 음식점에 방문하기 전, 인터넷 검색을 통해 음식점의 메뉴/가격/리뷰를 확인합니다. 하지만, 다수의 리뷰를 모두 확인하기 어렵고 광고 매장이 상단에 위치함에 따라, 고객이 음식점에 방문했을 때 불만족의 원인이 될 수 있습니다.
이를 해결하기 위해, 크게 다음 두 가지의 목표를 세웠습니다.
  1. 많은 리뷰를 분석하여 기준(맛/분위기/서비스)에 따라 음식점 평가를 객관화
  1. 사용자가 원하는 조건에 부합하는 음식점을 추천하는 시스템 개발

🔗 데이터 수집

토글을 펼치면 활용 데이터 목록을 확인할 수 있습니다.
데이터
시점
출처
데이터 설명 및 목적
교통카드 데이터
2022.1 ~ 2022.3
티머니
유동인구 많은 20개역 선정
음식점 크롤링 데이터
2022.1 ± 2023.7
네이버/카카오 지도
선정된 20개역 하에, 음식점 정보 크롤링
활용한 모든 데이터는, 프로젝트 진행 당시 기준 최신 데이터로 수집하였습니다.

🙋🏻‍♀️ 본인이 맡은 역할


⓵ 음식점 상세 정보 데이터 크롤링

주제 선정 배경에 따라, 음식점의 정보를 수집하여 분석을 진행하기 위해 다양한 정보들에 대해 수집하였습니다.
수집 데이터 종류 및 개수 (단위 : 개)
리뷰
(가게 당 리뷰)
가게까지의 거리
(= 가게 수)
메뉴
가격
182,790
(50~600)
716
4,433
4,433
데이터 수집 기준
  • 크롤링 진행 당시 기준, 서울 내 지점이 10개 이상인 가게는 프랜차이즈로 간주하여 수집에서 제외하였습니다.
  • 거리는 지도에 나와있는 지하철역 기준으로 걸리는 도보 시간으로 지정
  • '가게 별 메뉴 카테고리'는 양식, 아시안, 고기, 일식, 중식, 한식, 해산물, 분식, 카페로 분류
데이터 추출
💡
아래 코드는, 선정된 20개 역에 존재하는 음식점 정보 데이터를 수집하는 과정입니다.
  1. 리뷰 거리 크롤링 코드
    1. import time import sys import os import requests import pandas as pd import numpy as np import requests from bs4 import BeautifulSoup from selenium import webdriver import chromedriver_autoinstaller from selenium import webdriver from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import Select import warnings warnings.filterwarnings('ignore') path = chromedriver_autoinstaller.install() driver = webdriver.Chrome(path) driver.get("https://m.naver.com/") item='홍대입구역 연남물갈비' driver.get("https://m.search.naver.com/search.naver?sm=mtp_sly.hst&where=m&query="+item) time.sleep(2) driver.find_element_by_xpath('//*[@id="_title"]/a').click() time.sleep(1) #리뷰 버튼 클릭 driver.find_element_by_xpath('/html/body/div[3]/div/div/div/div[5]/div/div/div/div/a[5]/span').click() time.sleep(1) #//*[@id="app-root"]/div/div/div/div[5]/div/div/div/div/a[3] index=1 n=1 #수집된 리뷰 개수 #app-root > div > div > div > div:nth-child(7) > div:nth-child(2) > div.place_section._3fSeV > div > ul > li:nth-child(1) > div.faZHB > a > span #//*[@id="app-root"]/div/div/div/div[7]/div[2]/div[3]/div/ul/li[1]/div[2]/a/span path='#app-root > div > div > div > div:nth-child(7) > div:nth-child(2) > div.place_section._3fSeV > div > ul > li:nth-child(' user=[] review=[] while(index<=600): print("n: ",n," index: ",index) try: #//*[@id="app-root"]/div/div/div/div[7]/div[2]/div[3]/div[2]/a driver.find_element_by_xpath('//*[@id="app-root"]/div/div/div/div[7]/div[2]/div[3]/div[2]/ul/li['+str(index)+']/div[3]/a/span[1]').click() time.sleep(1) except: try: driver.find_element_by_xpath('//*[@id="app-root"]/div/div/div/div[7]/div[2]/div[3]/div[1]/ul/li['+str(index)+']/div[2]/a/span[1]').click() time.sleep(1) except: print('fail') try: html = driver.page_source soup = BeautifulSoup(html, 'lxml') review.append(soup.select_one(path+str(index)+') > div.faZHB > a > span').text) user.append(soup.select_one(path+str(index)+') > div._1orw_ > a._3VOZq > div._1vou-').text) n+=1 index+=1 except: index+=1 if(index%10==0): driver.find_element_by_xpath('//*[@id="app-root"]/div/div/div/div[7]/div[2]/div[3]/div[2]/a').click() time.sleep(1) #에러떠도 다음으로 진행 one_review = soup.find_all('li', attrs = {'class':'_3l2Wz'}) review_num = len(one_review) # 특정 식당의 리뷰 총 개수 print('리뷰 총 개수 : '+str(review_num)) # 리뷰 개수 for i in range(review_num): # user url try : review_content = one_review[i].find('span', attrs = {'class':'WoYOw'}).text except: # 리뷰가 없다면 review_content = '' print('리뷰 내용 : '+review_content) review.append(review_content) one_review = soup.find_all('li', attrs = {'class':'_3l2Wz'}) review_num = len(one_review) # 특정 식당의 리뷰 총 개수 print('리뷰 총 개수 : '+str(review_num)) # 리뷰 개수 for i in range(review_num): # user url try : user_content = one_review[i].find('div', attrs = {'class':'_1vou-'}).text except: # 리뷰가 없다면 user_content = '' print('user id : '+user_content) user.append(user_content) review len(review) user len(user) list1=list(zip(user,review)) test_df=pd.DataFrame(zip(user,review),columns=['user','review']) test_df test_df['title']='연남물갈비' test_df['loaction']='홍대입구역' test_df['category']='고기' test_df['num']=513 test_df df=pd.DataFrame() df=pd.concat([df, test_df]) df df.to_csv('/Users/hyewon/Desktop/navermap/hongdae/food/연남물갈비.csv',encoding='utf-8-sig', index=None)
  1. 메뉴 가격 크롤링 코드
    1. import numpy as np import pandas as pd from selenium import webdriver from selenium.webdriver.common.keys import Keys from selenium.common.exceptions import NoSuchElementException import time import re from bs4 import BeautifulSoup from tqdm import tqdm import chromedriver_autoinstaller df = pd.read_csv('naver_map_review_선릉역카페.csv') df=pd.DataFrame(df['title'].unique()) df.columns = ['title'] df['naver_map_url'] = '' df path = chromedriver_autoinstaller.install() driver = webdriver.Chrome(path) for i, keyword in enumerate(df['title'].tolist()): print("이번에 찾을 키워드 :", i, f"/ {df.shape[0]} 행", keyword) try: naver_map_search_url = f'https://map.naver.com/v5/search/선릉역%20{keyword}/place' # 검색 url 만들기 driver.get(naver_map_search_url) # 검색 url 접속, 즉 검색하기 time.sleep(4) # 중요함 cu = driver.current_url # 검색이 성공된 플레이스에 대한 개별 페이지 res_code = re.findall(r"place/(\d+)", cu) final_url = 'https://pcmap.place.naver.com/restaurant/'+res_code[0]+'/menu/list#' print(final_url) df['naver_map_url'][i]=final_url except IndexError: df['naver_map_url'][i]= '' print('none') df.to_csv('url_completed.csv', encoding = 'utf-8-sig') # 입력안된주소 입력 df.at[17, 'naver_map_url'] = 'https://pcmap.place.naver.com/restaurant/1637164879/menu/list#' df.to_csv('url_completed.csv', encoding = 'utf-8-sig') df #dfdf.drop([df.index[4]]) import pandas as pd from selenium import webdriver from selenium.webdriver.common.keys import Keys from selenium.common.exceptions import NoSuchElementException import time import re from bs4 import BeautifulSoup from tqdm import tqdm # 웹드라이버 접속 path = chromedriver_autoinstaller.install() driver = webdriver.Chrome(path) df = pd.read_csv('url_completed.csv') #null값 처리 완료 데이터 # 수집할 정보들 rating_list = [] # 평점 # 유저의 id - 추천시스템에서는 필수 review_json = {} # 리뷰 df=df.dropna() df.reset_index(inplace=True) df.to_csv('url_completed.csv', encoding = 'utf-8-sig') df Menu=pd.DataFrame() for i in range(len(df)): driver.get(df['naver_map_url'][i]) #thisurl = df['naver_map_url'][i] time.sleep(2) # 파싱 html = driver.page_source soup = BeautifulSoup(html, 'lxml') time.sleep(1) # 식당 구분 restaurant_name = df.loc[i,'title'] print('식당 이름 : '+restaurant_name) try: restaurant_classificaton = soup.find_all('span',attrs = {'class':'_3ocDE'})[0].text except: restaurant_classificaton = 'none' print('식당 구분 : '+restaurant_classificaton) #print('----------------------------------------------') menu_list = soup.find_all('a',attrs = {'class':'_1HA9l'}) #menu_list = driver.find_elements_by_css_selector('a._1HA9l') menu_name = [] # 메뉴이름 menu_price = [] # 메뉴가격 menu_num = len(menu_list) # 특정 식당의 리뷰 총 개수 print('메뉴 총 개수 : '+str(menu_num)) # 리뷰 개수 print('----------------------------------------------') for i in range(menu_num): try : name = menu_list[i].find('span', attrs = {'class':'_3yfZ1'}).text price=menu_list[i].find('div', attrs = {'class':'_3qFuX'}).text menu_name.append(name) #price = re.sub('원\d{2},\d{3}', '', price) # 할인 전 가격 제거 # 재정의 menu_price.append(price) # 추가 except: # 리뷰가 없다면 pass menu = pd.DataFrame({'title':restaurant_name, 'menu':menu_name, 'price':menu_price}) Menu=pd.concat([Menu, menu]) Menu=Menu.reset_index(drop=True) #Menu.to_csv('sadang_cafe_menu.csv', encoding = 'utf-8-sig') Menu Menu=Menu.reset_index(drop=True) Menu Menu.to_csv('sun_cafe_menu.csv', encoding = 'utf-8-sig')

⓶ 데이터 전처리 및 분석

워드 클라우드
  • KoNLPy의 Okt 형태소 분석기를 이용하여 명사&동사&형용사를 추출한뒤, 빈도 순 단어리스트 생성하여 빈도수 3개 이하인 단어들은 제거
CounterVectorizer
  • CounterVectorizer를 이용하여 단어들의 출현 빈도로 벡터화
TF-IDF
  • 가게별로 TF-IDF를 산출하여 가게별 키워드를 추출
가게별 리뷰 분석
  • 긍.부정 단어 20개씩 추출한 뒤, Word2Vec을 사용하여 단어 벡터 간 코사인 유사도 분석을 진행
  • 맛.분위기.서비스 별로 3가지에 대한 연관성 점수를 계산하여 상위 20개 단어를 추출
  • 추출된 키워드에 대해 맛.분위기.서비스 카테고리로 Word2Vec과 코사인유사도를 활용하여 유사성을 측정
  • 각 카테고리별 긍정으로 분류된 유사도가 높은 단어를 각 상위 20개 추출하여 해당 키워드에 해당하는 지표(가중치)로 사용

💡성과/의의


📍프로젝트 의의

리뷰 데이터 기반 개인 맞춤형 음식점 추천 시스템 의의는 다음과 같습니다.
  1. 개인 맞춤형 추천 시스템
    1. 음식점 방문자의 리뷰를 통해 사용자가 원하는 장소, 가격, 음식 종류에 맞는 음식점을 추천한 후 지도에 위치를 표시합니다.
  1. 모델 활용
    1. 소비자 측면 : 시간과 노력을 줄여, 현명한 선택을 할 수 있도록 도와줍니다.
    2. 판매자 측면 : 소비자의 니즈를 파악하여 마케팅전략이나 재품 개발 전략 수립을 목적으로 활용할 수 있도록 도와줍니다.
 

💬 회고


➕ 보완할 점

  • 약간의 노가다가 포함된 크롤링이었기에, 더욱 유연한 동적 크롤링 하는 방안을 모색한다면 더욱 편리하게 데이터를 수집할 수 있을 것이라 예상합니다.
  • 개인별 데이터를 수집할 수 있다면 소비자별 데이터 분석이나 제품 개발 분석과 같은 초개인화 추천 시스템을 제공할 수 있을 것이라 예상합니다.
  • 2호선에 뿐만 아니라 전국 혹은 서울시 전체로 범위를 확장하고, 포털사이트에 등록되지 않은 음식점의 모든 정보를 포함하는 방안이 마련된다면 더욱 풍부한 추천 시스템이 제공될 수 있을 것이라 생각합니다.

🚶🏻‍♀️‍➡️ 성장한 점

  • 약간의 노가다가 있었지만, 평소 관심있었던 크롤링을 이용하여 프로젝트를 해 볼 수 있었습니다.