🍗

우리 동네 맛집 추천엔진 직접, 쉽게 만들기 (크롤링과 코사인 유사도)

👍
이상한 외국 영화 추천 알고리즘 실습은 그만! 기본적인 라이브러리로 우리 동네 식당 · 카페 추천 프로그램을 만들어 보자
머신러닝을 배우면 자연스럽게 추천 알고리즘에 대해서 공부하게 됩니다.
하지만 추상적이고 다른 나라 언어로 된 예제로 공부하면 영 지루하겠죠? 당장 우리 일상의 데이터를 활용해 직접 추천 엔진을 구현해봅시다. 😎
수많은 추천 알고리즘이 있지만, 코사인 유사도 (cosine similarity) 이용하면 가장 간단하면서도 그럴싸한 추천 시스템을 만들 수 있습니다.
그럴싸하게 만들 수 있다고 했지 완벽하게 만든다는 말은 안 했습니다
🚀 다음과 같은 방식으로 만들어 볼 예정입니다.
1.
우리 동네에 어떤 요식업 업체가 있는지를 찾는다 👀
→ 공공데이터를 이용한다
2.
요식업 업체의 카테고리 데이터를 활용한다 🍕
→ 비슷한 업체를 추천 받는다 (치킨 or 중화요리 or 카페 등등)
3.
요식업 업체의 카카오, 네이버 지도 블로그 리뷰를 가져와 데이터로 활용한다 💭
→ 비슷한 리뷰를 남기는 업체를 추천 받는다 ("연남동의 힙한 카페", "존맛 짬뽕집" 등등)
4.
요식업 업체의 다음, 네이버 별점 점수를 가져와 데이터로 활용한다 ⭐️
→ 평균 별점이 높거나, 여러번 평가를 받으면 유명한 집, 맛집으로 인식해 점수를 높인다
5.
데이터로 가져온 다양한 요소를 종합시키는 계산식을 구성하고 추천 받는 함수를 짠다 🧮
Step 1. 우리 동네에 어떤 업체들이 있는지 찾는다
홍길동은 서울시 동작구 상도동에 거주한다고 가정해봅시다.
홍길동의 활동 범위는 마을버스를 타고 이동할 만한 '흑석동' '상도역' '숭실대입구역' 부근입니다.
그 동네에 어떤 요식업체들이 있는지, 공공데이터를 통해서 파악해봅시다.
다운로드를 누르면 바로 zip 파일이 다운로드 됨
💾 상가(상권)정보 데이터 다운로드하기 : 공공데이터 포털 - 링크 클릭
csv 파일을 다운받아 사용합니다. 그러나 해당 지역에 등록된 모든 업체가 나오기 때문에 파일이 방대한 편입니다.
필요한 데이터만 잘라내 활용합시다.
import pandas as pd import numpy as np # csv 파일이되 데이터 구분을 '|' 로 해둔 파일입니다. sep 지정을 안 하면 읽을 수 없습니다. df = pd.read_csv('shops.csv', sep='|')
Python
데이터 프레임이 굉장히 칼럼 수도 많고 지저분합니다. 필요한 정보만 잘라내도록 합시다
# 이렇게나 많은 칼럼들을 갖고 있습니다. Index(['상가업소번호', '상호명', '지점명', '상권업종대분류코드', '상권업종대분류명', '상권업종중분류코드', '상권업종중분류명', '상권업종소분류코드', '상권업종소분류명', '표준산업분류코드', '표준산업분류명', '시도코드', '시도명', '시군구코드', '시군구명', '행정동코드', '행정동명', '법정동코드', '법정동명', '지번코드', '대지구분코드', '대지구분명', '지번본번지', '지번부번지', '지번주소', '도로명코드', '도로명', '건물본번지', '건물부번지', '건물관리번호', '건물명', '도로명주소', '구우편번호', '신우편번호', '동정보', '층정보', '호정보', '경도', '위도'], dtype='object')
Plain Text
여기서 다음과 같은 칼럼만 쓰도록 합시다
상호명
도로명주소
상권업종대분류명, 상권업종중분류명, 상권업종소분류명 (대중소 분류명)
표준산업분류명
행정동명 (흑석동 상도1동만 빼서 쓸 것임)
위도 경도
# 음식점 데이터만 쓸 겁니다 df = df.loc[df['상권업종대분류명'] == '음식'] # 다음과 같은 칼럼만 있으면 됩니다 df = df[['상호명', '상권업종중분류명', '상권업종소분류명', '표준산업분류명', '행정동명', '위도', '경도']] # 그 중에서도 흑석동과 상도1동만 쓸 겁니다. df = df.loc[(df['행정동명'] == '흑석동') | (df['행정동명'] == '상도1동')]
Python
이렇게 데이터를 잘라냅니다.
칼럼 이름들이 우리말로 구성되어 있고 중복 어휘가 많아 헷갈리니 보다 이용에 편리한 칼럼명으로 바꿔줍시다
# 칼럼명 단순화 df.columns = ['name', # 상호명 'cate_1', # 중분류명 'cate_2', # 소분류명 'cate_3', # 표준산업분류명 'dong', # 행정동명 'lon', # 위도 'lat' # 경도 ]
Python
Step 2. 요식업 업체의 카테고리 데이터를 활용한다
상권업종 중(中)분류명과 소(小)분류명의 데이터는 간단해보이지만 충분히 활용 가치가 큰 데이터입니다.
이들을 모두 카테고리 데이터라고 부르겠습니다.
양식 > 정통양식/경양식
커피점/카페 > 커피전문점/카페/다방
한식 > 한식/백반/한정식
이런 키워드를 담은 데이터들을 하나의 칼럼에 몰아두면, 이를 바탕으로 유사도를 계산할 수 있기 때문입니다.
양식 > 정통양식/경양식 → 양식 전통양식 경양식
커피점/카페 > 커피전문점/카페/다방 → 커피점 카페 커피전문점 카페 다방
한식 > 한식/백반/한정식 → 한식 한식 백반 한정식
여기서 코사인 유사도 개념이 활용됩니다. 텍스트를 기반으로 가장 쉽게 "비슷함"의 정도를 파악해줍니다.
코사인 유사도 개념 이해하기 (클릭) 👯‍♂️
1.
카테고리 데이터들을 하나로 묶어주는 전처리를 해줍니다
df['cate_mix'] = df['cate_1'] + df['cate_2'] + df['cate_3'] df['cate_mix'] = df['cate_mix'].str.replace("/", " ")
Python
2.
하나로 뭉쳐진 카테고리 텍스트 데이터를 피쳐 벡터화하고, 코사인 유사도를 계산하는 코드를 짭니다.
이때 사용하는 패키지는 대표적인 머신러닝 학습 패키지인 사이킷런(sklearn)입니다.
# pip install sklearn 혹은 conda install sklearn으로 사이킷런을 미리 다운 받아주세요. from sklearn.feature_extraction.text import CountVectorizer # 피체 벡터화 from sklearn.metrics.pairwise import cosine_similarity # 코사인 유사도 count_vect_category = CountVectorizer(min_df=0, ngram_range=(1,2)) place_category = count_vect_category.fit_transform(df['cate_mix']) place_simi_cate = cosine_similarity(place_category, place_category) place_simi_cate_sorted_ind = place_simi_cate.argsort()[:, ::-1]
Python
이렇게 하면, 각 데이터 vs 데이터 로 서로 카테고리 텍스트가 얼마나 유사한지를 따져줍니다.
500개의 데이터가 있다면 1번 데이터는 자기 자신과 한번 비교하고, 나머지 499개와 비교를 하는 것입니다.
따라서 데이터의 유사도는 대각행렬 꼴을 취합니다.
1번(A김밥)은 자기 자신인 1번(A김밥)과 유사도를 따졌을 때 당연히 100%인 1.0 값을 갖게 됩니다.
1번은 그 다음 2번(B치킨)과 유사도를 따지게 되고, 이때는 1보다 낮은 값이 형성됩니다 (0.32). 치킨과 김밥은 다르니까요.
그렇게 500번까지 비교를 하게 되며, 역으로 2번 입장에서 1번과 비교한 결과도 나오게 되는데 이는 아까 결과와 같게 됩니다 (0.32 = 0.32)
👉 이 데이터를 바탕으로 마지막 단계에 최종 채점 단계에서 활용합니다.
Step 3,4. 포털의 블로그 리뷰와 별점 데이터를 가져와 데이터로 활용한다
인스타 감성의 개인 카페와 대형 프랜차이즈 카페, 혹은 디저트 카페 ... 서로 각자의 결이 조금씩 다르죠?
업종 카테고리 상으로는 모두 같은 "카페"이지만 그 안에서 더 비슷한지 아닌지를 판별해야 좀 더 그럴싸한 추천엔진을 만들 수 있습니다.
이를 위해 '방문자'의 평가 리뷰를 더 확보해 봅시다. 솔직한 공간 체험 경험이 담긴, 블로그 리뷰를 써 볼 것입니다.
블로그 리뷰 데이터는 웹 사이트 크롤링을 통해 확보해야 합니다. 절차는 다음과 같습니다.
1.
공공데이터로 확보한 상호명 + 행정동 명을 검색어로 변환하여 포털에 검색하기
2.
셀레늄 (Selenium) 크롤러를 통해 검색 결과로 나온 블로그 리뷰, 블로그 별점 데이터 확보하기
이때, 크롤링 할 포털의 구성에 대해서 꼼꼼하게 살펴야 합니다.
카카오 지도의 경우, (1) 단순 검색과 (2) 상세보기 의 페이지가 각각 다릅니다.
(1) 단순 검색
(2) 상세보기 페이지
그렇기 때문에
1.
단순 검색에 원하는 맛집을 검색하고, 상세 페이지 url을 따로 수집한 뒤
2.
상세 페이지 속 별점과 블로그 리뷰 텍스트를 다시 한번 크롤링 해봅시다.
🌝 셀레늄을 통한 크롤링이 처음이라면?? (셀레늄에 대해서 알아보기)
이제 (1) 단순 검색 페이지의 크롤링을 진행해봅시다.
상호명을 카카오 지도에 검색하면서 데이터를 수집할 계획입니다.
하지만 스타벅스 같은 경우 똑같은 상호명이 전국에 엄청 많겠죠?
그러니 흑석동 스타벅스 처럼 검색하는 게 보다 정확한 정보를 수집할 수 있을 것입니다.
따라서 검색할 키워드를 재차 형성한 뒤 원하는 결과물을 가져와 봅시다.
코드 보기 👀
크롤러가 아래처럼 돌면서 상세보기의 url을 수집해줍니다.
하지만 검색 결과가 없을 때도 있습니다.
공공데이터로 등록된 상호가 폐점하거나 페이퍼 컴퍼니(?) 같은 상황일 경우 카카오 지도에서 검색이 되지 않습니다. 그래서 아래처럼 오류가 나곤 합니다. (no such element : Unable to locate element)
상세 페이지가 수집되지 않은 항목은 유령 가게일 가능성이 높으니 데이터에서 제외해도 큰 상관은 없겠죠?
(1)을 통해 수집한 (2) 상세 페이지 url을 다시 크롤러가 돌면서, 블로그 리뷰와 별점 데이터 등을 수집해 줍시다.
하지만 블로그 리뷰라는 게 맛집이고 유명한 집이면 많을 것이고, 어쩌면 아예 없을 가능성도 큽니다.
총 리뷰를 3개까지 수집하되, 적으면 적은대로 최대한 많이 수집하는 코드를 짜서 진행해봅시다.
코드 보기 👀
이런식으로 결과가 추출되며 상세 페이지의 데이터를 수집합니다.
만약 카카오 상의 블로그 리뷰가 없다면, 같은 방식으로 네이버에서 블로그 리뷰를 가져와 데이터 공백을 채우는 것이 가능하겠죠?
Step 2에서 카테고리 mix 데이터 간의 코사인 유사도를 계산했던 것처럼
리뷰 텍스트를 가지고도 같은 작업을 진행해줍니다.
# 리뷰 텍스트 데이터 간의 텍스트 피쳐 벡터라이징 count_vect_review = CountVectorizer(min_df=2, ngram_range=(1,2)) place_review = count_vect_review.fit_transform(df['kakao_blog_review_txt']) # 리뷰 텍스트 간의 코사인 유사도 따지기 place_simi_review = cosine_similarity(place_review, place_review) place_simi_review_sorted_ind = place_simi_review.argsort()[:, ::-1]
Python
Step 5. 다양한 요소를 종합하는 계산식을 구성하고 추천 받는 함수를 짠다
이제 데이터를 모두 가져오고, 후 가공까지 마쳤다면 추천 알고리즘을 짜봅시다.
공식 1 : 카테고리가 얼마나 유사한지
공식 2 : 블로그 리뷰가 얼마나 유사한지
공식 3 : 블로그 리뷰가 얼마나 많이 올라왔는지
공식 4 : 블로그 별점이 얼마나 높은지
공식 5 : 블로그 별점 평가가 얼마나 많이 됐는지
위 공식 5개를 적절한 비율로 가중치를 주면서, 체감상 더 추천이 잘 되는 최적의 공식을 짜야합니다.
# 공식 1~5의 중요성을 짬뽕시키는 공식 # * 0.003 등의 가중치를 줘서 조절합니다. place_simi_co = ( + place_simi_cate * 0.3 # 공식 1. 카테고리 유사도 + place_simi_review * 1 # 공식 2. 리뷰 텍스트 유사도 + np.repeat([df['kakao_blog_review_qty'].values], len(df['kakao_blog_review_qty']) , axis=0) * 0.001 # 공식 3. 블로그 리뷰가 얼마나 많이 올라왔는지 + np.repeat([df['kakao_star_point'].values], len(df['kakao_star_point']) , axis=0) * 0.005 # 공식 4. 블로그 별점이 얼마나 높은지 + np.repeat([df['kakao_star_point_qty'].values], len(df['kakao_star_point_qty']) , axis=0) * 0.001 # 공식 5. 블로그 별점 평가가 얼마나 많이 됐는지 ) # 아래 place_simi_co_sorted_ind 는 그냥 바로 사용하면 됩니다. place_simi_co_sorted_ind = place_simi_co.argsort()[:, ::-1] # 최종 구현 함수 def find_simi_place(df, sorted_ind, place_name, top_n=10): place_title = df[df['name'] == place_name] place_index = place_title.index.values similar_indexes = sorted_ind[place_index, :(top_n)] similar_indexes = similar_indexes.reshape(-1) return df.iloc[similar_indexes] # 상도국수를 포함해 5개 업체를 뽑아봅시다. find_simi_place(df, place_simi_co_sorted_ind, '상도국수', 5)
Python
np.repeat() 는 뭘 의미하는 것일까? 🧐
🤖 최종 결과는 다음과 같이 나옵니다.
'상도국수'와 가장 유사한 업체는 사리원(만두 집), 손칼국수, 국수마당 등이 나왔습니다.
코사인 유사도로 간단하고 빠르게 만들어 본 추천엔진, 생각보다 그럴싸하네요! 🤩
😓 이번 포스팅은 다음과 같은 내용이 사실 부실합니다.
블로그 리뷰 데이터를 크롤링해서 막상 모아도, 노이즈가 정말 많습니다.
1.
하나의 리뷰에 여러 매장 리뷰를 하는 경우
→ 시간이 많다면... 인간지능으로 걸러줍시다
2.
텍스트라는 게 같은 어휘를 써도 부정적인 어감으로 말하는 경우
→ 이럴 때는 감정분석 기법을 적용하는 걸로 접근해봅시다
별점 리뷰가 없는 업체를 0점으로 두는 것은 정확한 평가라 볼 수 없습니다
→ 신규 업체나, 리뷰 자체가 없는 경우 이를 최하점인 0점으로 둔다면 이는 객관적인 평가를 했다고 볼 수 없습니다.
특정 업체에 블로그 리뷰 수, 별점 평가 수가 극단적으로 많다면 타 데이터에 영향력을 행사합니다
→ 방송에 나올 맛집이라면 별점 평가 횟수가 다른 업체랑 지나치게 차이가 날 것입니다.
→ 이때는 해당 데이터들의 영향력을 줄이는 조치를 생각해 볼 수 있습니다 (로그 변환 or 투키 펜스 적용)
🙋‍♂️ 김문과의 데이터
직접 공부한 데이터 분석 · 활용법을 기록합니다.
포스팅에 대한 Q&A 및 다양한 논의를 기다립니다.
contact : 우하단 메신저 버튼 or e-mail
Today