실제 영화데이터를 이용한 추천시스템 구현실습
2019-01-29
실제 영화데이터를 이용한 추천시스템 구현실습
그림, 실습코드 등 학습자료 출처 : https://gitlab.com/radajin
[ 실제 영화데이터를 이용한 추천시스템 구현 ]
- 데이터 컨텐츠 : https://www.kaggle.com/rounakbanik/the-movies-dataset/home
- 활용데이터 : links_small.csv, movies_metadata.csv, ratings_small.csv
import numpy as np
import pandas as pd
from scipy import spatial
1. 데이터 로드
# ratings_small 데이터 로드
rating_df = pd.read_csv("ratings_small.csv")
rating_df.tail()
userId | movieId | rating | timestamp | |
---|---|---|---|---|
99999 | 671 | 6268 | 2.5 | 1065579370 |
100000 | 671 | 6269 | 4.0 | 1065149201 |
100001 | 671 | 6365 | 4.0 | 1070940363 |
100002 | 671 | 6385 | 2.5 | 1070979663 |
100003 | 671 | 6565 | 3.5 | 1074784724 |
## 타임스템프 컬럼은 불필요하므로 제거
rating_df.drop('timestamp', axis=1, inplace = True)
rating_df.tail()
userId | movieId | rating | |
---|---|---|---|
99999 | 671 | 6268 | 2.5 |
100000 | 671 | 6269 | 4.0 |
100001 | 671 | 6365 | 4.0 |
100002 | 671 | 6385 | 2.5 |
100003 | 671 | 6565 | 3.5 |
links_df = pd.read_csv("links_small.csv")
links_df.tail()
movieId | imdbId | tmdbId | |
---|---|---|---|
9120 | 162672 | 3859980 | 402672.0 |
9121 | 163056 | 4262980 | 315011.0 |
9122 | 163949 | 2531318 | 391698.0 |
9123 | 164977 | 27660 | 137608.0 |
9124 | 164979 | 3447228 | 410803.0 |
# 결측값 제거
links_df = links_df.dropna()
pd.options.display.float_format = '{:.0f}'.format
## 판다스의 디스플레이 옵션을 설정하는데 float format을 소수점 제거를 해준다.
links_df['tmdbId'] = links_df['tmdbId'].astype('int64')
links_df.tail()
movieId | imdbId | tmdbId | |
---|---|---|---|
9120 | 162672 | 3859980 | 402672 |
9121 | 163056 | 4262980 | 315011 |
9122 | 163949 | 2531318 | 391698 |
9123 | 164977 | 27660 | 137608 |
9124 | 164979 | 3447228 | 410803 |
metadata_df = pd.read_csv("movies_metadata.csv", low_memory = False)
## low_memory = False -> 메모리를 많이 사용해서 데이터를 크게 가져오겠다.
metadata_df.tail()
adult | belongs_to_collection | budget | genres | homepage | id | imdb_id | original_language | original_title | overview | ... | release_date | revenue | runtime | spoken_languages | status | tagline | title | video | vote_average | vote_count | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
45461 | False | NaN | 0 | [{'id': 18, 'name': 'Drama'}, {'id': 10751, 'n... | http://www.imdb.com/title/tt6209470/ | 439050 | tt6209470 | fa | رگ خواب | Rising and falling between a man and woman. | ... | NaN | 0 | 90 | [{'iso_639_1': 'fa', 'name': 'فارسی'}] | Released | Rising and falling between a man and woman | Subdue | False | 4 | 1 |
45462 | False | NaN | 0 | [{'id': 18, 'name': 'Drama'}] | NaN | 111109 | tt2028550 | tl | Siglo ng Pagluluwal | An artist struggles to finish his work while a... | ... | 2011-11-17 | 0 | 360 | [{'iso_639_1': 'tl', 'name': ''}] | Released | NaN | Century of Birthing | False | 9 | 3 |
45463 | False | NaN | 0 | [{'id': 28, 'name': 'Action'}, {'id': 18, 'nam... | NaN | 67758 | tt0303758 | en | Betrayal | When one of her hits goes wrong, a professiona... | ... | 2003-08-01 | 0 | 90 | [{'iso_639_1': 'en', 'name': 'English'}] | Released | A deadly game of wits. | Betrayal | False | 4 | 6 |
45464 | False | NaN | 0 | [] | NaN | 227506 | tt0008536 | en | Satana likuyushchiy | In a small town live two brothers, one a minis... | ... | 1917-10-21 | 0 | 87 | [] | Released | NaN | Satan Triumphant | False | 0 | 0 |
45465 | False | NaN | 0 | [] | NaN | 461257 | tt6980792 | en | Queerama | 50 years after decriminalisation of homosexual... | ... | 2017-06-09 | 0 | 75 | [{'iso_639_1': 'en', 'name': 'English'}] | Released | NaN | Queerama | False | 0 | 0 |
5 rows × 24 columns
# 필요한 컬럼들의 데이터만 추출
metadata_df = metadata_df[['id','original_title','title','runtime']]
metadata_df.tail()
id | original_title | title | runtime | |
---|---|---|---|---|
45461 | 439050 | رگ خواب | Subdue | 90 |
45462 | 111109 | Siglo ng Pagluluwal | Century of Birthing | 360 |
45463 | 67758 | Betrayal | Betrayal | 90 |
45464 | 227506 | Satana likuyushchiy | Satan Triumphant | 87 |
45465 | 461257 | Queerama | Queerama | 75 |
# links_small를 이용하면 rating_df과 metadata를 검색할 수 있다.
2. movieId 값으로 영화정보를 출력
def id_to_movie(id_num):
pd.options.display.float_format = '{:.0f}'.format
## 판다스의 디스플레이 옵션을 설정하는데 float format을 소수점 제거를 해준다.
tmdbId = links_df.loc[links_df["movieId"]==id_num]['tmdbId'].values[0]
movie_info = metadata_df.loc[metadata_df['id'] == str(tmdbId)]
pd.reset_option('display')
## 위에 설정한 옵션해제
return movie_info
id_to_movie(6365)
id | original_title | title | runtime | |
---|---|---|---|---|
6221 | 604 | The Matrix Reloaded | The Matrix Reloaded | 138.0 |
3. 데이터 탐색
[unique count]
- rating
- user
- movie
u_user = rating_df['userId'].unique()
len(u_user)
## 671명의 유저가 약 10만건의 rating을 매긴것으로 확인됨
671
u_movie = rating_df['movieId'].unique()
len(u_movie)
## 약 9000개의 영화
9066
u_rating = rating_df['rating'].unique()
len(u_rating)
## 10가지 카테고리의 평점
10
rating_df.groupby('rating').size().reset_index(name='rating_count')
## 이 테이블을 보고 추천시스템 개발에 대한 기준을 잡을 수 있다.
rating | rating_count | |
---|---|---|
0 | 0.5 | 1101 |
1 | 1.0 | 3326 |
2 | 1.5 | 1687 |
3 | 2.0 | 7271 |
4 | 2.5 | 4449 |
5 | 3.0 | 20064 |
6 | 3.5 | 10538 |
7 | 4.0 | 28750 |
8 | 4.5 | 7723 |
9 | 5.0 | 15095 |
user_counts_df = rating_df.groupby("userId").size().reset_index(name = 'user_rating_count')
user_counts_df = user_counts_df.sort_values(by=["user_rating_count"], ascending=False)
user_counts_df.tail()
userId | user_rating_count | |
---|---|---|
295 | 296 | 20 |
288 | 289 | 20 |
248 | 249 | 20 |
220 | 221 | 20 |
0 | 1 | 20 |
user_counts_df.head()
userId | user_rating_count | |
---|---|---|
546 | 547 | 2391 |
563 | 564 | 1868 |
623 | 624 | 1735 |
14 | 15 | 1700 |
72 | 73 | 1610 |
movie_counts_df = rating_df.groupby("movieId").size().reset_index(name = 'movie_rating_count')
movie_counts_df = movie_counts_df.sort_values(by=["movie_rating_count"], ascending=False)
movie_counts_df.tail()
movieId | movie_rating_count | |
---|---|---|
6045 | 31956 | 1 |
6046 | 31963 | 1 |
6047 | 31973 | 1 |
6050 | 32022 | 1 |
9065 | 163949 | 1 |
movie_counts_df.head()
movieId | movie_rating_count | |
---|---|---|
321 | 356 | 341 |
266 | 296 | 324 |
284 | 318 | 311 |
525 | 593 | 304 |
232 | 260 | 291 |
4. 데이터 전처리
- 데이터 set 감축
user_limit, movie_limit = 100, 100
## 100개 이상 평가한 유저만 걸러냄
## 100번 이상 평가받은 영화면 걸러냄
filtered_userId = user_counts_df[user_counts_df['user_rating_count'] > user_limit]['userId']
filtered_userId = list(filtered_userId)
len(filtered_userId)
258
filtered_movieId = movie_counts_df[movie_counts_df['movie_rating_count'] > user_limit]['movieId']
filtered_movieId = list(filtered_movieId)
len(filtered_movieId)
149
filtered_df = rating_df[rating_df['userId'].isin(filtered_userId)]
len(filtered_df)
80487
filtered_df = filtered_df[filtered_df['movieId'].isin(filtered_movieId)]
len(filtered_df)
15567
filtered_df.tail()
userId | movieId | rating | |
---|---|---|---|
99982 | 671 | 4993 | 5.0 |
99983 | 671 | 4995 | 4.0 |
99987 | 671 | 5349 | 4.0 |
99989 | 671 | 5445 | 4.5 |
99994 | 671 | 5952 | 5.0 |
5. pivot을 이용하여 user_base로 데이터 프레임 생성
user_df = filtered_df.pivot_table(values='rating', index = 'userId',\
columns = 'movieId', fill_value=0,\
aggfunc = np.average, dropna=False)
## aggfunc = np.average 중복값은 평균값으로 처리
## 데이터가 없는 경우에 0으로 채워짐
user_df.tail()
movieId | 1 | 2 | 6 | 10 | 25 | 32 | 34 | 36 | 39 | 47 | ... | 6377 | 6539 | 6874 | 7153 | 7361 | 7438 | 8961 | 33794 | 58559 | 79132 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
userId | |||||||||||||||||||||
656 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
659 | 0.0 | 0.0 | 3.0 | 0.0 | 5.0 | 4.0 | 0.0 | 4.0 | 0.0 | 4.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
664 | 3.5 | 0.0 | 4.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | 0.0 | 4.5 | ... | 0.0 | 4.0 | 4.0 | 4.0 | 4.0 | 4.0 | 4.0 | 4.0 | 4.5 | 5.0 |
665 | 0.0 | 3.0 | 0.0 | 0.0 | 0.0 | 4.0 | 2.0 | 0.0 | 2.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
671 | 5.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 4.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
5 rows × 149 columns
6. 유사도 행렬 생성
- 유사도 측정함수 구현
def Euclidean_Distance_Similarity(vector_1, vector_2):
## 0으로 비어있는 데이터 제거
idx = vector_1.nonzero()[0]
## 모든데이터가 0인 경우 0을 리턴
if len(idx) == 0:
return 0
vector_1, vector_2 = np.array(vector_1)[idx], np.array(vector_2)[idx]
## 0으로 비어있는 데이터 제거
idx = vector_2.nonzero()[0]
## 모든데이터가 0인 경우 0을 리턴
if len(idx) == 0:
return 0
vector_1, vector_2 = np.array(vector_1)[idx], np.array(vector_2)[idx]
return np.linalg.norm(vector_1 - vector_2)
## 임시테스트
Euclidean_Distance_Similarity(user_df.loc[4], user_df.loc[8])
4.8218253804964775
def Cosine_Similarity(vector_1, vector_2):
## 0으로 비어있는 데이터 제거
idx = vector_1.nonzero()[0]
## 모든데이터가 0인 경우 0을 리턴
if len(idx) == 0:
return 0
vector_1, vector_2 = np.array(vector_1)[idx], np.array(vector_2)[idx]
## 0으로 비어있는 데이터 제거
idx = vector_2.nonzero()[0]
## 모든데이터가 0인 경우 0을 리턴
if len(idx) == 0:
return 0
vector_1, vector_2 = np.array(vector_1)[idx], np.array(vector_2)[idx]
return 1 - spatial.distance.cosine(vector_1, vector_2)
## 임시테스트
Cosine_Similarity(user_df.loc[4], user_df.loc[8])
0.9911164579376771
- 유사도 메트릭스 생성 함수구현
def similarity_matrix(user_df, similarity_func):
index = user_df.index
matrix = []
# sample_df의 로우를 하나씩 돌면서 데이터를 가져옴
for idx_1, value_1 in user_df.iterrows():
row = []
for idx_2, value_2 in user_df.iterrows():
row.append(similarity_func(value_1, value_2))
matrix.append(row)
return pd.DataFrame(matrix, columns = index, index=index)
- 유사도 행렬 생성
%%time
sm_df = similarity_matrix(user_df, Cosine_Similarity)
Wall time: 11.8 s
sm_df.head(5)
userId | 4 | 8 | 15 | 17 | 19 | 21 | 22 | 23 | 26 | 30 | ... | 647 | 648 | 652 | 654 | 655 | 656 | 659 | 664 | 665 | 671 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
userId | |||||||||||||||||||||
4 | 1.000000 | 0.991116 | 0.956762 | 0.948457 | 0.985932 | 0.980286 | 0.981591 | 0.982744 | 0.986789 | 0.979119 | ... | 0.979131 | 0.951088 | 0.986368 | 0.991149 | 0.983037 | 0.997707 | 0.970241 | 0.994377 | 0.968998 | 0.985579 |
8 | 0.991116 | 1.000000 | 0.914253 | 0.966828 | 0.972568 | 0.985269 | 0.964117 | 0.982010 | 0.984022 | 0.971471 | ... | 0.974777 | 0.947942 | 0.970261 | 0.988689 | 0.979823 | 0.998645 | 0.972875 | 0.990196 | 0.974638 | 0.982713 |
15 | 0.956762 | 0.914253 | 1.000000 | 0.914953 | 0.950125 | 0.950927 | 0.906975 | 0.923247 | 0.888292 | 0.920392 | ... | 0.957841 | 0.856947 | 0.893839 | 0.917356 | 0.900642 | 0.873927 | 0.938017 | 0.930106 | 0.903008 | 0.892096 |
17 | 0.948457 | 0.966828 | 0.914953 | 1.000000 | 0.949537 | 0.933276 | 0.939038 | 0.961024 | 0.966644 | 0.942020 | ... | 0.963750 | 0.933889 | 0.869626 | 0.947757 | 0.964055 | 0.960849 | 0.932213 | 0.964792 | 0.933463 | 0.952986 |
19 | 0.985932 | 0.972568 | 0.950125 | 0.949537 | 1.000000 | 0.963805 | 0.955135 | 0.980127 | 0.954985 | 0.962846 | ... | 0.971151 | 0.966500 | 0.980166 | 0.979269 | 0.957911 | 0.977106 | 0.962211 | 0.979273 | 0.954240 | 0.971782 |
5 rows × 258 columns
7. 예측 메트릭스 구현
- 예측 메트릭스 생성 함수구현
def mean_score(df, sm_df, target, closer_count):
ms_df = sm_df.drop(target)
ms_df = ms_df.sort_values(target, ascending = False)
ms_df = ms_df[:closer_count]
ms_df = df.loc[ms_df.index]
# 결과데이터 생성
pred_df = pd.DataFrame(columns = df.columns)
pred_df.loc['User'] = df.loc[target]
pred_df.loc['Mean'] = ms_df.mean()
return pred_df
## 작동테스트
pred_df = mean_score(user_df, sm_df, 4, 5)
pred_df
movieId | 1 | 2 | 6 | 10 | 25 | 32 | 34 | 36 | 39 | 47 | ... | 6377 | 6539 | 6874 | 7153 | 7361 | 7438 | 8961 | 33794 | 58559 | 79132 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
User | 0.0 | 0.0 | 0.0 | 4.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
Mean | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.8 | ... | 1.8 | 0.0 | 1.7 | 2.0 | 1.8 | 0.8 | 1.5 | 0.8 | 1.9 | 1.0 |
2 rows × 149 columns
- 예측 메트릭스 내 상위 평점 10개 추출함수 (추천리스트 산출함수) 구현
def recommand(pred_df, r_count=10):
recommand_df = pred_df.T
recommand_df = recommand_df[recommand_df["User"]==0]
recommand_df = recommand_df.sort_values("Mean",ascending=False)
return list(recommand_df[:r_count].index)
## 작동테스트
movie_ids = recommand(pred_df)
movie_ids
[4226, 2858, 2959, 4973, 912, 50, 5952, 4306, 3996, 4993]
8. 위에서 구한 movieId 값을 이용해서 영화정보를 가져오기
def movie_info(movie_ids):
datas = []
for movie_id in movie_ids:
data = id_to_movie(movie_id).to_dict('records')[0]
datas.append(data)
return pd.DataFrame(datas)
df = movie_info(movie_ids)
df
id | original_title | runtime | title | |
---|---|---|---|---|
0 | 77 | Memento | 113.0 | Memento |
1 | 14 | American Beauty | 122.0 | American Beauty |
2 | 550 | Fight Club | 139.0 | Fight Club |
3 | 194 | Le fabuleux destin d'Amélie Poulain | 122.0 | Amélie |
4 | 289 | Casablanca | 102.0 | Casablanca |
5 | 629 | The Usual Suspects | 106.0 | The Usual Suspects |
6 | 121 | The Lord of the Rings: The Two Towers | 179.0 | The Lord of the Rings: The Two Towers |
7 | 808 | Shrek | 90.0 | Shrek |
8 | 146 | 卧虎藏龙 | 120.0 | Crouching Tiger, Hidden Dragon |
9 | 120 | The Lord of the Rings: The Fellowship of the Ring | 178.0 | The Lord of the Rings: The Fellowship of the Ring |
9. 위에서 구현한 코드들을 실행하는 함수생성
def run(df, similarity_func, target, closer_count, r_count):
# 유사도 행렬 데이터 만들기
sm_df = similarity_matrix(df, similarity_func)
# 예측 행렬 데이터 만들기
pred_df = mean_score(df, sm_df, target, closer_count)
# 추천 영화 movie_ids 출력
movie_ids = recommand(pred_df, r_count)
# movie_ids로 영화 정보 데이터 프레임 만들기
result = movie_info(movie_ids)
return result
# 작동테스트
result_df = run(user_df, Cosine_Similarity, 8, 5, 10)
result_df
id | original_title | runtime | title | |
---|---|---|---|---|
0 | 601 | E.T. the Extra-Terrestrial | 115.0 | E.T. the Extra-Terrestrial |
1 | 954 | Mission: Impossible | 110.0 | Mission: Impossible |
2 | 62 | 2001: A Space Odyssey | 149.0 | 2001: A Space Odyssey |
3 | 602 | Independence Day | 145.0 | Independence Day |
4 | 808 | Shrek | 90.0 | Shrek |
5 | 2164 | Stargate | 121.0 | Stargate |
6 | 812 | Aladdin | 90.0 | Aladdin |
7 | 863 | Toy Story 2 | 92.0 | Toy Story 2 |
8 | 9487 | A Bug's Life | 95.0 | A Bug's Life |
9 | 329 | Jurassic Park | 127.0 | Jurassic Park |
10. MAE를 이용한 모델성능 평가
- MAE 함수구현
def mae(value, pred):
idx = value.nonzero()[0]
value, pred = np.array(value)[idx], np.array(pred)[idx]
idx = pred.nonzero()[0]
value, pred = np.array(value)[idx], np.array(pred)[idx]
return np.absolute(sum(value - pred)) / len(idx)
- 모델에 대한 성능평가
def evaluate(df, sm_df, closer_count):
users = df.index
evaluate_list = []
for target in users:
pred_df = mean_score(df, sm_df, target, closer_count)
evaluate_list.append(mae(pred_df.loc["User"], pred_df.loc["Mean"]))
return np.average(evaluate_list)
## 작동테스트
evaluate(user_df, sm_df, 5)
2.7123888840303985