[Python] ์ปจํ ์ธ ๊ธฐ๋ฐ ์ถ์ฒ(CB) ์ค์ต - TMDB 5000 ์ํ ๋ฐ์ดํฐ ์ธํธ
[K-Data x ๋ฌ๋์คํผ์ฆ] 2-1. ์ปจํ ์ธ ๊ธฐ๋ฐ ์ถ์ฒ(CB), TF-IDF
# ์ปจํ ์ธ ๊ธฐ๋ฐ ์ถ์ฒ? : CB(Content-based Recommendation) ์ ์ A๋ผ๋ ์ฌ๋์ด ๊ณผ๊ฑฐ์ ์ ํธํ ์์ดํ ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๊ณ ๋น์ทํ ์์ดํ ์ ์ ์ A์๊ฒ ์ถ์ฒํ๋ค. => ์์ดํ ์ ๋ฉํ๋ฐ์ดํฐ์ ์) - ์ํ : ๋ฐฐ
xod22.tistory.com
CB์ ๋ํ ์ด๋ก ์ ๋ค๋ค๋ณด์๋๋ฐ ์ด๋ฒ์ ์ง์ ๋ฐ์ดํฐ๋ฅผ ํ์ฉํ์ฌ ์ปจํ ์ธ ๊ธฐ๋ฐ ์ถ์ฒ(CB)๋ฅผ ์ค์ตํด๋ณด๋ ค๊ณ ํฉ๋๋ค!

CB(Content-based Recommendation)
: ๋จผ์ ์ค์ต์ ์์ ๊ฐ๋จํ๊ฒ ๋ค์ CB(Content-based Recommendation, ์ปจํ ์ธ ๊ธฐ๋ฐ์ถ์ฒ)์ ๋ํด ์ค๋ช ํ์๋ฉด
์ ์ A๋ผ๋ ์ฌ๋์ด ๊ณผ๊ฑฐ์ ์ ํธํ ์์ดํ ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๊ณ ๋น์ทํ ์์ดํ ์ ์ ์ A์๊ฒ ์ถ์ฒํ๋ ๋ฐฉ์์ ๋๋ค.
* ์์ดํ ํ๋กํ์ผ๋ฒกํฐ๋ฅผ ํตํด ์์ดํ ๋ผ๋ฆฌ์ ์ ์ฌ๋๋ฅผ ์ธก์ ํ๊ณ
์ ์ฌ๋๊ฐ ๋์ ์์ดํ ์ ์ถ์ฒํ๋ ๊ณผ์ ์ผ๋ก ์ถ์ฒ์ด ์งํ๋ฉ๋๋ค!
์ค์ต - ๋ฐฉ๋ฒ1
1. ํจํค์ง ์ํฌํธ ๋ฐ ๋ฐ์ดํฐ ๋ถ๋ฌ์ค๊ธฐ
https://www.kaggle.com/tmdb/tmdb-movie-metadata
TMDB 5000 Movie Dataset
Metadata on ~5,000 movies from TMDb
www.kaggle.com
์บ๊ธ ๋งํฌ์์ tmdb_5000_movies.csv ๋ฐ์ดํฐ๋ฅผ ๋ค์ด๋ฐ์ต๋๋ค.
import pandas as pd
import numpy as np
import warnings; warnings.filterwarnings('ignore')
movies=pd.read_csv('tmdb_5000_movies.csv')
print(movies.shape)
movies.head(1)
->๋ฐ์ดํฐ๊ฐ 4803๊ฐ์ ๋ ์ฝ๋์ 20๊ฐ์ ํผ์ฒ๋ก ๊ตฌ์ฑ๋์ด์์
~ํ์ํ ์ปฌ๋ผ๋ง ์ ์ฅ~
์ฝํ ์ธ ๊ธฐ๋ฐ ํํฐ๋ง์ ์ฌ์ฉ์๊ฐ ์ข์ํ๋ ์ํ์ ๋น์ทํ ํน์ฑ/์์ฑ, ๊ตฌ์ฑ ์์ ๋ฑ์ ๊ฐ์ง ๋ค๋ฅด ์ํ๋ฅผ ์ถ์ฒํด์ฃผ๋ ๋ฐฉ์์ ๋๋ค..!
๋ฐ๋ผ์ id, title, genres, vote_average(ํ์ ), vote_count, popularity, keywords, overview ์ปฌ๋ผ๋ง ์ฌ์ฉํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
movies_df=movies[['id','title', 'genres', 'vote_average', 'vote_count', 'popularity', 'keywords', 'overview']]
~์ฅ๋ฅด, ํค์๋ ์ปฌ๋ผ์ ํํ ํ์ธ~
movies_df[['genres','keywords']]
๋ฆฌ์คํธ ๋ด๋ถ์ ๋์ ๋๋ฆฌ๊ฐ ์๋ ํํ์ ๋ฌธ์์ด๋ก ์ ์ฅ๋์ด ์์ต๋๋ค..
~ํ๋์ ํ ์ดํด๋ณด๊ธฐ~
: ์ปฌ๋ผ์ ๊ฐ๊ฒฉ์ ๋ํ ๋ง์ ๋ฐ์ดํฐ๊ฐ ์ถ๋ ฅ๋ ์ ์๋๋ก ํ์ฌ ํ ํ๊ฐ๋ง ์ถ๋ ฅํด๋ณด๊ฒ ์ต๋๋ค!
pd.set_option('max_colwidth', 100)
#ํ ํ๊ฐ๋ง ์ถ๋ ฅํด๋ด
movies_df[['genres', 'keywords']][:1]
์ด ๊ฐ๋ณ ์ฅ๋ฅด์ ๋ช ์นญ์ ๋์ ๋๋ฆฌ์ ํค(key)์ธ "name"์ผ๋ก ์ถ์ถํ ์ ์์ต๋๋ค.
~genres(์ฅ๋ฅด)์ปฌ๋ผ์ ๋ฌธ์์ด์ ๋ถํด-> ๊ฐ๋ณ ์ฅ๋ฅด๋ฅผ ํ์ด์ฌ ๋ฆฌ์คํธ ๊ฐ์ฒด๋ก ์ถ์ถ~
: genres, keywords ์ปฌ๋ผ์ ๋ฌธ์์ด์ด ์๋ ๋ฆฌ์คํธ ๋ด๋ถ์ ์ฌ๋ฌ ์ฅ๋ฅด ๋์ ๋๋ฆฌ๋ก ๊ตฌ์ฑ๋ ๊ฐ์ฒด๊ฐ๋จ
from ast import literal_eval
movies_df['genres']=movies_df['genres'].apply(literal_eval)
movies_df['keywords']=movies_df['keywords'].apply(literal_eval)
#์ปฌ๋ผ์์ ['Action']/['Adventure']๊ณผ ๊ฐ์ ์ฅ๋ฅด๋ช ๋ง ๋ฆฌ์คํธ์ ๊ฐ์ฒด๋ก ์ถ์ถ
movies_df['genres']=movies_df['genres'].apply(lambda x : [y['name'] for y in x])
movies_df['keywords']=movies_df['keywords'].apply(lambda x : [y['name'] for y in x])
#ํ์ธ
movies_df[['genres', 'keywords']][:1]
์ ์ถ์ถ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
2. ์ฅ๋ฅด๊ฐ์ CountVectorizer
#CB : ์ฅ๋ฅด๊ฐ์ผ๋ก ์ ์ฌ๋๋ฅผ ๋น๊ตํ ๋ค ๋์ ํ์ ์ ๊ฐ๋ ์ํ๋ฅผ ์ถ์ฒ
#genres์ปฌ๋ผ์ ๋ฌธ์์ด๋ก ๋ณ๊ฒฝํ ๋ค CountVectorizer๋ก ํผ์ฒ ๋ฒกํฐํํ ํ๋ ฌ๊ฐ์ ์ฝ์ฌ์ธ ์ ์ฌ๋๋ฅผ ์ ์ฉํด ์ํ๋ณ ์ ์ฌ์ฑ ํ๋จ
from sklearn.feature_extraction.text import CountVectorizer
#CountVectorizer๋ฅผ ์ ์ฉํ๊ธฐ ์ํด ๊ณต๋ฐฑ๋ฌธ์๋ก word๋จ์๊ฐ ๊ตฌ๋ถ๋๋ ๋ฌธ์์ด๋ก ๋ณํ
movies_df['genres_literal']=movies_df['genres'].apply(lambda x : (' ').join(x))
~๋ณํ ํ์ธ~
#๋ณํ ํ์ธ
print(movies_df[['genres']][:1])
print(movies_df[['genres_literal']][:1])
์ ๋ณํ๋์์์ ํ์ธ
~CountVectorizer ์ ์ฉ~
#CountVectorizerํจ์->count_vect์ด๋ผ๋ ํจ์๋ช
์ผ๋ก ์์ฑ
count_vect=CountVectorizer(min_df=0, ngram_range=(1,2))
#min_df : ๋จ์ด์ฅ์ ํฌํจ๋๊ธฐ ์ํ ์ต์๋น๋
#'genres_literal'์ปฌ๋ผ์ผ๋ก CountVector์์ฑ
genre_mat=count_vect.fit_transform(movies_df['genres_literal'])
print(genre_mat.shape)
: 4803๊ฐ ์ปฌ๋ผ์ด์์ผ๋ฏ๋ก 4803๊ฐ ๋ ์ฝ๋, 276๊ฐ์ ๊ฐ๋ณ๋จ์ด ํผ์ฒ๋ก ๊ตฌ์ฑ๋ ํผ์ฒ๋ฒกํฐ ํ๋ ฌ ์์ฑ
3. ์ฅ๋ฅด๊ฐ์ ์ฝ์ฌ์ธ ์ ์ฌ๋
from sklearn.metrics.pairwise import cosine_similarity
genre_sim=cosine_similarity(genre_mat, genre_mat)
print(genre_sim.shape)
#2๊ฐ ํ๋ง ํ์ธํด๋ณด๊ธฐ!
print(genre_sim[:2])
~์ ์ฌ๋ ๊ฐ์ด ๋์ ์ธ๋ฑ์ค ์ถ์ถ~
: ์ ์ฌ๋ ๊ฐ์ด ๋์ ์์ผ๋ก ์์น ์ธ๋ฑ์ค ์ถ์ถ(?)
genre_sim_sorted_ind=genre_sim.argsort()[:, ::-1]
print(genre_sim_sorted_ind[:1])
์ฌ๊ธฐ์ ::-1์ ๋ด๋ฆผ์ฐจ์์ด๋ผ๋ ์๋ฏธ
์ฒซ๋ฒ์งธ ํ๋ง ํ์ธํด๋ณด๋ฉด ์ ์ฌ๋ ๊ฐ์ด ๋์ ์ธ๋ฑ์ค๊ฐ์ด 0(๋ณธ์ธ)-> 3494๋ฒ์งธ ํ-> 813๋ฒ์งธ ํ...์์๋๋ก ๋์ด๋ ๊ฒ์ ํ์ธํ ์ ์๋ค..!
4. ์ฅ๋ฅด ์ ์ฌ๋์ ๋ฐ๋ผ ์ํ๋ฅผ ์ถ์ฒ
# ์ฅ๋ฅด ์ ์ฌ๋์ ๋ฐ๋ผ ์ํ๋ฅผ ์ถ์ฒํ๋ ํจ์ find_sim_movie()์์ฑ
def find_sim_movie(df, sorted_ind, title_name, top_n=10):
#์ธ์๋ก ์
๋ ฅ๋ movies_df(๋ฐ์ดํฐํ๋ ์)์์ ์
๋ ฅ๋ฐ์ 'title'(์ ๋ชฉ) ์ปฌ๋ผ์ด ์
๋ ฅ๋ ๊ฐ๋ง ์ถ์ถํ์ฌ ์ ์ฅ
title_movie = df[df['title'] == title_name]
#title_named๋ฅผ ๊ฐ์ง ๋ฐ์ดํฐ ํ๋ ์์ index ๊ฐ์ฒด๋ฅผ ndarray๋ก ๋ณํ -> ๋ช๋ฒ์งธ ์ํ์ธ์ง? ์ธ๋ฑ์ค ์ ์ฅ
#sorted_ind(์ ์ฌ๋๊ฐ) ์ธ์๋ก ์
๋ ฅ๋ genre_sim_sorted_ind ๊ฐ์ฒด์์ ์ ์ฌ๋ ์์ผ๋ก top_n๊ฐ์ index ์ถ์ถ
title_index = title_movie.index.values
similar_indexes = sorted_ind[title_index, :(top_n)]
#์ถ์ถ๋ top_n index๋ฅผ ์ถ๋ ฅ. top_n index๋ 2์ฐจ์ ๋ฐ์ดํฐ
print(similar_indexes)
#๋ฐ์ดํฐ ํ๋ ์์์ index๋ก ์ฌ์ฉํ๊ธฐ ์ํด 1์ฐจ์ array๋ก ๋ณ๊ฒฝ
similar_indexes = similar_indexes.reshape(-1)
#์๋ df์ค์ ์ธ๋ฑ์ค์ ํฌํจ๋ ํ์ return
return df.iloc[similar_indexes]
์์์ ๋ง๋ find_sim_movie() ํจ์๋ฅผ ์ฌ์ฉํด ์ํ 'The Godfather(๋๋ถ)'์ ์ฅ๋ฅด๋ณ๋ก ์ ์ฌํ ์ํ 10๊ฐ๋ฅผ ์ถ์ฒ
similar_movies = find_sim_movie(movies_df, genre_sim_sorted_ind, 'The Godfather',10)
similar_movies[['title', 'vote_average']]
๋ค์๊ณผ ๊ฐ์ ์ถ์ฒ๊ฒฐ๊ณผ๋ฅผ ์ ๊ณตํ๋ค..!
๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด '๋๋ถ 2ํธ'์ด ๊ฐ์ฅ ๋จผ์ ์ถ์ฒ๋์์ต๋๋ค.
ํ์ง๋ง 'Light Sleeper', 'Mi America', 'Kids' ๋ฑ ๋๋ถ๋ฅผ ์ข์ํ๋ ๊ณ ๊ฐ์๊ฒ ์ถ์ฒํ๊ธฐ ์ด๋ ค์ด ์ํ๋ ์์ต๋๋ค.
'Light Sleeper'์ ๊ฒฝ์ฐ ํ์ ์ด ๋ฎ์ ํธ์ด๊ณ , 'Mi America'์ ๊ฒฝ์ฐ ํ์ ์ด 0์ ์ ๋๋ค..!
์ด๋ฌํ ์ถ์ฒ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ํ๊ธฐ ์ํด ์ข ๋ ๋ง์ ํ๋ณด๊ตฐ์ ์ ์ ํ ๋ค ํ์ ์ ๋ฐ๋ผ ํํฐ๋งํด์ ์ต์ข ์ถ์ฒํ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ์ฌ ๋ค์ ๊ตฌํํด๋ณด๊ฒ ์ต๋๋ค.
์ค์ต - ๋ฐฉ๋ฒ2 (๊ฐ์ค์น๋ฅผ ๊ณ ๋ ค)
: ์ค์ต1๊ณผ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง๊ณ ์ข ๋ ๋ง์ ํ๋ณด๊ตฐ ์ ์ , ํ์ ์ ๋ฐ๋ผ ํํฐ๋งํ๋ ๋ฐฉ๋ฒ์ ์ฌ์ฉํด๋ณด๊ฒ ์ต๋๋ค!
1. ๋ฐ์ดํฐ ํ์ธ
# vote_average : ์ํ์ ํ์ ํ๊ท (0~10์ )
# vote_count : ํ๊ฐ ํ์
movies_df[['title', 'vote_average','vote_count']].sort_values('vote_average', ascending=False)[:10]
# ํ์ ์ด ๋์ ์์๋๋ก(๋ด๋ฆผ์ฐจ์) ์ ๋ ฌ
ํ๊ฐ ํ์๊ฐ ๋งค์ฐ ์ ์ ์ํ๋ค์ด ์์๊ถ์ ์๋ค๋ ๊ฒ์ ํ์ธ.
์ด๋ ๊ฒ ์๊ณก๋ ํ์ ๋ฐ์ดํฐ๋ฅผ ํํผํ๊ธฐ ์ํด ๊ฐ์ค์น๊ฐ ๋ถ์ฌ๋ ํ์ ์ ์ฌ์ฉ!
2. ๊ธฐ์กด ํ์ ์ ๊ฐ์ค ํ์ ์ผ๋ก ๋ณ๊ฒฝ!
- v(vote_count) : ๊ฐ๋ณ ์ํ์ ํ์ ์ ํฌํํ ํ์-
- m : ํ์ ์ ๋ถ์ฌํ๊ธฐ ์ํ ์ต์ ํฌํ ํ์
- R(vote_average) : ๊ฐ๋ณ ์ํ์ ๋ํ ํ๊ท ํ์
- C(vote_average.mean()) : ์ ์ฒด ์ํ์ ๋ํ ํ๊ท ํ์
์ฌ๊ธฐ์ m๊ฐ์ ํฌํ ํ์์ ๋ฐ๋ฅธ ๊ฐ์ค์น๋ฅผ ์กฐ์ ํ๋ ์ญํ ์ ํ๋๋ฐ
m๊ฐ์ ๋์ด๋ฉด ํ์ ํฌํ ํ์๊ฐ ๋ง์ ์ํ์ ๋ ๋ง์ ๊ฐ์ค ํ๊ท ์ ๋ถ์ฌํฉ๋๋ค..!
*ํ๊ฐํ ์ฌ๋์ด ๋ง์ ์๋ก ์ ๋ขฐํ ์ ์๋ค๋ ์๋ฏธ(?)
C=movies_df['vote_average'].mean()
#m๊ฐ์ ์์ 60ํผ์ผํธ์ ํด๋นํ๋ ํ์๋ฅผ ๊ธฐ์ค
m=movies_df['vote_count'].quantile(0.6)
print('C :', round(C,3), 'm: ', round(m,3))
~ํ์ ์ ๊ฐ์ค์น ํ์ ์ผ๋ก ๋ฐ๊พธ๋ ํจ์~
def weighted_vote_average(record):
v=record['vote_count']
R=record['vote_average']
return ((v/(v+m))*R)+((m/(m+v))*C)
~ํจ์ ์ ์ฉ~
: ๊ฐ์คํ์ ์ 'weighted_vote' ์ปฌ๋ผ์ ์๋ก ๋ง๋ค์ด ๊ฐ์ ๋ฃ์ด์ฃผ์๋ค..!
movies_df['weighted_vote']=movies_df.apply(weighted_vote_average, axis=1)
์๋กญ๊ฒ ๋ถ์ฌ๋ weighted_vote ํ์ ์ด ๋์ ์์ผ๋ก ์์ 10๊ฐ์ ์ํ๋ฅผ ์ถ๋ ฅํด๋ณด๊ฒ ์ต๋๋ค!
movies_df[['title', 'vote_average', 'weighted_vote', 'vote_count']].sort_values('weighted_vote', ascending=False)[:10]
ํ๊ฐํ ์ฌ๋์ด ๋ง์ ์ํ๊ฐ ์์๊ถ์ ์๋ค์ฅ..!
3. ์ฅ๋ฅด ์ ์ฌ์ฑ์ด ๋์ ์ํ top_n์ 2๋ฐฐ์ -> weighted_vote๊ฐ์ด ๋์ ์์ผ๋ก ์ถ์ถ
~ํจ์ ์ ์~
#๊ฐ์ค์นํ์ ์ด ํฌํจ๋ ์๋ก์ด ํจ์ ์ ์
def find_sim_movie(df, sorted_ind, title_name, top_n=10):
title_movie=df[df['title']==title_name]
title_index=title_movie.index.values
#top_n์ 2๋ฐฐ์ ํด๋นํ๋ ์ฅ๋ฅด ์ ์ฌ์ฑ์ด ๋์ index ์ถ์ถ
similar_indexes=sorted_ind[title_index, :(top_n*2)]
similar_indexes=similar_indexes.reshape(-1)
#๊ธฐ์ค ์ํ index๋ ์ ์ธ(์๊ธฐ์์ ์ ์ธ(?))
similar_indexes=similar_indexes[similar_indexes !=title_index]
#top_n์ 2๋ฐฐ์ ํด๋นํ๋ ํ๋ณด๊ตฐ์์ weighted_vote ๋์ ์์ผ๋ก top_n๋งํผ ์ถ์ถ
return df.iloc[similar_indexes].sort_values('weighted_vote', ascending=False)[:top_n]
~ํจ์ ์ ์ฉ~
similar_movies=find_sim_movie(movies_df, genre_sim_sorted_ind, 'The Godfather', 10)
similar_movies[['title', 'vote_average', 'weighted_vote']]
์ด์ ์ถ์ฒ ์ํ๋ณด๋ค ํจ์ฌ ๋์ ์ํ๊ฐ ์ถ์ฒ๋์์ต๋๋ค..!