<jupyter_start><jupyter_text>Content-Based Filtering TMDB 5000 Movie DatasetTMDB 5000 영화 데이터 세트는 인기 영화 데이터 정보 사이트인 IMDB의 주요 영화 5,000편을 가공한 메타 데이터이다. 이는 Kaggle에서 구할 수 있다.The TMDB 5000 movie dataset is a processed meta-data for 5,000 of the major movies of IMDB's popular film data information site. It is available at Kaggle. https://www.kaggle.com/tmdb/tmdb-movie-metadata - tmdb_5000_credits.csv- tmdb_5000_movies.csv ----  Content-based filtering using genre properties 장르 속성을 이용한 콘텐츠 기반 필터링 영화를 선택하는 데 중요한 요소 중 하나인 영화 장르 속성을 바탕으로 콘텐츠 기반 필터링 추천 시스템을 만들어보자.장르 열 값의 유사성 비교하고, 높은 등급의 영화를 추천한다.Let's create a content-based filtering recommendation system based on movie genre attributes, one of the important factors in choosing movies.Comparing the similarity of the column values of the genre, recommending the movie with a high rating.  Data Loading and Processing<jupyter_code>import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
movies = pd.read_csv('./data/TMDB_5000_Movie_Dataset/tmdb_5000_movies.csv')
print(movies.shape)
movies.head(3)<jupyter_output>(4803, 20)<jupyter_text>- Extract major columns for content-based filtering recommendation analysis내용 기반 필터링 권장 사항 분석을 위한 주요 열 추출 - Make them into new DataFrame새로운 데이터 프레임으로 만들기<jupyter_code>movies_df = movies[['id','title','genres','vote_average','vote_count','popularity','keywords','overview']]
movies_df.sample()<jupyter_output><empty_output><jupyter_text>'genres', 'keywords' 컬럼 값은 파이썬 리스트 내부에 여러 개의 딕셔너리가 있는 형태이다. 이는 한꺼번에 여러 개의 값을 표기하기 위한 방법으로, str 형태로 로딩된 데이터를 다시 딕셔너리 형태로 바꿔주는 과정이 필요하다.---The 'genres' and 'keywords' column values are in the form of multiple dictionaries inside the Python list. This is a method to indicate multiple values at once, requiring the process of changing the data loaded in str form back into dictionary form.<jupyter_code>pd.set_option('max_colwidth', 100)
movies_df[['genres','keywords']].sample()<jupyter_output><empty_output><jupyter_text>- genres와 keywords 모두 id와 name을 딕셔너리의 key로 가지며, name이라는 key를 이용해 해당하는 명칭을 가져올 수 있다- 이 두 컬럼을 분해하여 파이썬 리스트 객체로 추출해보자---- Both genres and keywords have id and name as keys to the dictionary, and the corresponding name can be obtained using the key 'name'- Let's break down these two columns and extract them into Python list-objects<jupyter_code>from ast import literal_eval
movies_df['genres'] = movies_df['genres'].apply(literal_eval)
movies_df['keywords'] = movies_df['keywords'].apply(literal_eval)
movies_df[['genres','keywords']].sample()<jupyter_output><empty_output><jupyter_text>- 겉보기에는 달라진 것이 없어보이지만, 실제로는 문자열 형태의 값에서, 리스트 내부 딕셔너리 형태의 값으로 변경되었다- 이제 이 중 명칭만을 리스트 객체로 추출해보자---- Nothing seems to have changed on the surface, but in reality, it was changed from the value of string form to the value of dictionary form within the list.- Now, let's extract only the names from the list.<jupyter_code>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']].sample()<jupyter_output><empty_output><jupyter_text>----  Measure genre content similarity 장르 콘텐츠 유사도 측정 영화 간의 장르 콘텐츠 유사도를 측정하는 방법에는 여러가지가 있지만, 그중 가장 간단한 방법은 코사인 유사도를 구하는 것이다genres를 str 형태로 변환 후 이를 CountVectorizer로 feature vectorize한 행렬 데이터 값을 비교하는 것으로, 다음과 같은 단계로 구현된다1. str 형태로 변환된 genres 칼럼을 Count 기반으로 feature vectorize2. genres string을 feature vectorize된 matrix로 변환한 데이터셋을 코사인 유사도 통해 비교3. 장르 유사도가 높은 영화 중 평점이 높은 순으로 영화를 추천---There are many ways to measure the similarity of genre content between movies, but the simplest way is to obtain cosine similarity.The transformation of genres into str forms and comparing them with the value of the matrix data, which is characterized by the CountVectorize, is implemented in the following steps:1. Feature vectorize based on the count of the genres column that converted to str form2. Compare feature vectorized matrix with genres string through cosine similarity3. Recommend movies in order of high ratings among movies with high genre similarity<jupyter_code>from sklearn.feature_extraction.text import CountVectorizer


movies_df['genres_literal'] = movies_df['genres'].apply(lambda x: (' ').join(x)) 
# CountVectorize 적용 위해 공백문자로 word 구분하여 문자열로 반환

count_vect = CountVectorizer(min_df=0, ngram_range=(1,2)) 
# max_df / min_df: 토큰이 나타난 횟수를 기준으로, max_df 값보다 크거나, min_df 값보다 작으면 무시
# ngram_range: (min_n, max_n)으로, BoW 생성에 사용할 토큰의 크기인 n-gram의 범위를 결정 - 여기서는 최소 모노그램, 최대 바이그램

genre_mat = count_vect.fit_transform(movies_df['genres_literal']) # csr_matrix: CSR 형식 희소 행렬
genre_mat.shape<jupyter_output><empty_output><jupyter_text>- CountVectorizer로 변환해 4,803개의 레코드와 276개의 개별 단어 피처로 구성된 피처 벡터 행렬이 만들어졌다- 이렇게 생성된 피처 벡터 행렬에 사이킷런의 cosine_similarity() 메서드를 이용해 코사인 유사도를 계산하자---- Convert with CountVectorize, a feature vector matrix of 4,803 records and 276 individual word features are created- Let's calculate cosine similarity using the cosine_similarity() method in the generated feature vector matrix<jupyter_code>from sklearn.metrics.pairwise import cosine_similarity

genre_sim = cosine_similarity(genre_mat, genre_mat)
print(genre_sim.shape)
print(genre_sim)<jupyter_output>(4803, 4803)
[[1.         0.59628479 0.4472136  ... 0.         0.         0.        ]
 [0.59628479 1.         0.4        ... 0.         0.         0.        ]
 [0.4472136  0.4        1.         ... 0.         0.         0.        ]
 ...
 [0.         0.         0.         ... 1.         0.         0.        ]
 [0.         0.         0.         ... 0.         0.         0.        ]
 [0.         0.         0.         ... 0.         0.         1.        ]]<jupyter_text>- cosine_similalities() 호출로 생성된 genre_sim instance는 movies_df의 genre_literal 칼럼을 피처 벡터화한 행렬(genre_mat) 데이터의 레코드별 유사도 정보를 가지고 있으며 이는 movie_df DataFrame의 행별 장르 유사도 값과 동일하다.- movies_df를 장르 기준으로 콘텐츠 기반 필터링을 수행하려면 movies_Df의 개별 레코드에 대해서 가장 장르 유사도가 높은 순으로 다른 레코드를 추출해야 하는데, 이때 앞에서 생성한 genre_sim instance를 이용하게된다. - genre_sim instance의 기준 행별로 비교 대상이 되는 행의 유사도 값이 높은 순으로 정렬된 행렬의 위치 인덱스 값을 추출하면 된다.- 값이 높은 순으로 정렬된 비교 대상 행의 유사도 값이 아니라 비교 대상 행의 위치 인덱스임에 주의하자.- 넘파이의 argsort()을 이용하여 genre_sim을 유사도가 높은 순으로 정리한 후 비교 행 위치를 가져오자.<jupyter_code>genre_sim_sorted_ind = genre_sim.argsort()[:, ::-1]
print(genre_sim_sorted_ind[:1])<jupyter_output>[[   0 3494  813 ... 3038 3037 2401]]<jupyter_text>- 위 출력값이 의미하는 바는 0번 레코드인 경우 0번을 제외한 가장 높은 유사도를 가지는 레코드가 3494번이라는 뜻이며 가장 낮은 유사도를 가진 레코드는 2401번이라는 뜻이다.  Movie Recommendation using Genre Contest Filtering 장르 콘텐츠 필터링을 이용한 영화 추천 - 이제 장르 유사도에 따라 영화를 추천하는 함수를 생성해보자.- 해당 함수는 인자로 movies_df, genre_sim_sorted_ind, 고객이 비슷한 장르로 추천 받고자 하는 영화제목, 그리고 추천할 영화 건수이다.- 위 인자를 모두 입력하면 추천 영화 정보를 가지는 DataFrame이 반환된다.<jupyter_code>def find_sim_movie(df, sorted_ind, title_name, top_n = 10):
    # 인자로 입력된 movies_df에서 'title' 칼럼이 입력된 criteria 값인 df 추출
    title_movie = df[df['title'] == title_name]
    
    #title_named을 가진 df의 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차원 데이터
    #df에서 index로 사용하기 위해서 1차원 array로 바꾸는 과정 필요
#     print(similar_indexes)
    similar_indexes = similar_indexes.reshape(-1)
    
    return df.iloc[similar_indexes]<jupyter_output><empty_output><jupyter_text>- find_sim_movie() 함수를 이용해 영화 '대부'와 유사한 장르의 영화 10개를 추천해보자.<jupyter_code>sim_movies = find_sim_movie(df = movies, sorted_ind = genre_sim_sorted_ind, title_name = 'The Godfather', top_n = 10)
sim_movies[['title', 'vote_average']]<jupyter_output><empty_output><jupyter_text>- 가장 비슷한 장르의 영화로 '대부 2편'이 추천되었으며, '좋은 친구(GoodFellas)'같은 영화도 추천되었다.- 그러나 Light Sleeper, Mi America와 같은 다소 생소하고 평점이 낮은 영화도 추천되었는데, 이는 보완이 필요해보인다.<jupyter_code>movies_df[['title', 'vote_average', 'vote_count']].sort_values('vote_average', ascending=False)[:10]<jupyter_output><empty_output><jupyter_text>- 전반적으로 평점이 높은 영화가 먼저 추천 되었으나, 일부는 여전히 생소하다.- 이는 vote_count가 적어서 상대적인 평점이 높게 측정되었기 때문이므로, 이 또한 보완이 필요하다. - IMDB에서는 이러한 문제를 해결하기 위해 새로운 평가 기준을 적용하여 추천을 진행하는데, 이 방식을 Weighted rating이라고 한다. $Weighted Rating = (v/(v+m)) * r + (m/(v+m)) * c$ v: 개별 영화에 평점을 투표한 횟수 (vote_count)m: 평점을 부여하기 위한 최소 투표 횟수 (조절 가능. 높을수록 투표수가 많은 영화를 우선시) > 상위 60%에 해당하는 횟수 이용r: 개별 영화에 대한 평균 평점 (vote_average)c: 전체 영화에 대한 평균 평점 (movies_df['vote_average'].mean())<jupyter_code>c = movies_df['vote_average'].mean()
m = movies_df['vote_count'].quantile(0.6)
print('c:', round(c,3), '\nm:', round(m,3))<jupyter_output>c: 6.092 
m: 370.2<jupyter_text>- 이로써 기존 평점을 새로운 가중 평점으로 변경하는 함수를 생성하고 이를 이용해 새로운 평점 정보를 만들 수 있게 되었다.- 가중 평점을 생성하는 함수를 만들어보자.- 이 함수는 df의 레코드를 인자로 받아 해당 레코드의 vote_count와 vote_average, 그리고 미리 추출된 c와 m값을 적용해 가중 평점을 반환한다.<jupyter_code>percentile = 0.6

c = movies_df['vote_average'].mean()
m = movies_df['vote_count'].quantile(percentile)

def weighted_vote_average(record):
    v = record['vote_count']
    r = record['vote_average']
#     print(v,r)
    return ((v/(v+m)) * r) + ((m/(v+m)) * c)

movies_df['weighted_vote'] = movies_df.apply(weighted_vote_average, axis=1)
movies_df[['title','vote_average','weighted_vote', 'vote_count']].sort_values('weighted_vote', ascending=False)[:10]<jupyter_output><empty_output><jupyter_text>- 전반적으로 유명한 영화들이 잘 추천되었음을 확인할 수 있다.- 이와 같이 새롭게 평가된 기준을 바탕으로 추천을 다시 진행해보도록 하자<jupyter_code>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배에 해당하는 유사 장르 인덱스 추출
    similar_indexes = sorted_ind[title_index, :(top_n * 2)]
    similar_indexes = similar_indexes.reshape(-1)
    
    #기준 영화 인덱스는 제외
    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']]<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09406973279030496, 'fraction_numerical': 0.021456243170755934, 'mean_word_length': 4.203100775193798, 'pattern_counts': {'":': 0, '<': 53, '<?xml version=': 0, '>': 54, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 5, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '548129', 'license': 'MIT', 'n_tokens_mistral': 5235, 'n_tokens_neox': 6099, 'n_words': 1216, 'path': '1. Content-Based Filtering Practice - TMDB 5000 Movie Dataset.ipynb', 'repo_name': 'KevinTheRainmaker/Recommendation_Algorithms', 'size': '10067'}