영화 별점 데이터를 이용한 추천 시스템 구축
이 데이터 세트는
9742 개 영화에 걸쳐 100836 개의 평가가 포함되어 있습니다.
1996-3-29 부터 2018-9-24 까지 약 22년 6개월 간 610 명의 사용자의 데이터입니다.
사용자는 무작위로 선별되었고, 나이/성별 등의 정보는 포함되지 않습니다.
최소 20 개 이상 평가한 사용자의 데이터만 추렸습니다.
컬럼 설명
movies.csv
-
movieId
- 최소 한 개 이상의 별점이 있는 영화입니다.
- movies.csv 와 ratings.csv 를 이어줄 수 있는 역할을 합니다.
-
title
- https://www.themoviedb.org/ 로부터 획득한 해당 영화의 제목 정보 입니다.
- genres
- 아래 목록 중에서 선별된(중복 가능) 정보입니다.
- Action, Adventure, Animation, Children's, Comedy, Crime, Documentary, Drama, Fantasy, Film-Noir
- Horror, Musical, Mystery, Romance, Sci-Fi, Thriller, War, Western, (no genres listed)
ratings.csv
-
userId
- 익명화된 사용자 정보를 의미합니다.
-
movieId
- 최소 한 개 이상의 별점이 있는 영화입니다.
- movies.csv 와 ratings.csv 를 이어줄 수 있는 역할을 합니다.
-
rating
- 0 에서 5 사이, 0.5 간격으로 구성된 별점 정보입니다.
-
timestamp
- UTC + 0 기준 1970년도 1월 1일 자정부터 몇 초가 지났는가 하는 정보입니다.
협업 필터링(collaborative filtering)
사용자들로부터 얻은 기호정보로 관심사를 예측하는 방법입니다.
사용자들의 과거의 경향이 미래에서도 그대로 유지 될 것이라 가정합니다.
협업 필터링 기반 추천시스템은 사용자들의 기호(좋음, 싫음)에 대한 정보를 기반으로,
나와 비슷한 사람이 좋아하는 것 또는 특정 물건과 비슷한 물건을 찾아서 추천해 줍니다.
단순 인기투표 기반 추천과는 접근방법부터 차별화 되어있습니다.
1) 비슷한 취향을 가진 고객들에게 서로 아직 구매하지 않은 상품들을 교차 추천하거나
2) 또는 특정 물건 검색시 관련 상품을 추천하는 형태의 서비스를 제공하는 경우 사용됩니다.
수업에서는 두 가지 모두 다루게 됩니다.
Item-Based
Load Dataset
모든 데이터 분석의 시작은 데이터를 읽어오는 것입니다.
파일의 경로를 지정하는 방법에 주의하셔야 합니다.
만일 read_csv를 실행할 때 (FileNotFoundError)라는 이름의 에러가 난다면 경로가 제대로 지정이 되지 않은 것입니다.
다음의 링크에서 경로를 지정하는 법을 다루고 있습니다.
# 판다스의 read_csv로 movies.csv, ratings.csv 파일을 읽어옵니다.
# 읽어온 데이터를 각각 movies, ratings 이라는 이름의 변수에 할당합니다.
import pandas as pd
movies = pd.read_csv('data/movies.csv')
ratings = pd.read_csv('data/ratings.csv')
# 새로 불러온 변수 movies의
# 가로, 세로 크기와
# 첫 5 개 행을 확인합니다.
print(movies.shape)
movies.head()
(9742, 3)
movieId | title | genres | |
---|---|---|---|
0 | 1 | Toy Story (1995) | Adventure|Animation|Children|Comedy|Fantasy |
1 | 2 | Jumanji (1995) | Adventure|Children|Fantasy |
2 | 3 | Grumpier Old Men (1995) | Comedy|Romance |
3 | 4 | Waiting to Exhale (1995) | Comedy|Drama|Romance |
4 | 5 | Father of the Bride Part II (1995) | Comedy |
# 새로 불러온 변수 ratings의 첫 5 개 행을 확인합니다.
ratings.head()
userId | movieId | rating | timestamp | |
---|---|---|---|---|
0 | 1 | 1 | 4.0 | 964982703 |
1 | 1 | 3 | 4.0 | 964981247 |
2 | 1 | 6 | 4.0 | 964982224 |
3 | 1 | 47 | 5.0 | 964983815 |
4 | 1 | 50 | 5.0 | 964982931 |
Combine Dataset
movies 와 ratings 를 하나의 데이터프레임으로 묶기
# pd.merge() 안에 묶고자 하는 데이터프레임들을 넣어주면 됩니다.
# movieId 가 같기 때문에, 이를 기준으로 오른쪽에 붙여주게 됩니다.
# 이어붙인 값을 = 을 통해 combined 라는 변수에 할당합니다.
combined = pd.merge(movies, ratings)
# 잘 실행되었는지 확인하기 위해 head() 를 사용합니다.
# movieId 를 기준으로 컬럼들 추가된 것이므로, 모든 컬럼들이 있는지 확인하면 됩니다.
combined.head()
movieId | title | genres | userId | rating | timestamp | |
---|---|---|---|---|---|---|
0 | 1 | Toy Story (1995) | Adventure|Animation|Children|Comedy|Fantasy | 1 | 4.0 | 964982703 |
1 | 1 | Toy Story (1995) | Adventure|Animation|Children|Comedy|Fantasy | 5 | 4.0 | 847434962 |
2 | 1 | Toy Story (1995) | Adventure|Animation|Children|Comedy|Fantasy | 7 | 4.5 | 1106635946 |
3 | 1 | Toy Story (1995) | Adventure|Animation|Children|Comedy|Fantasy | 15 | 2.5 | 1510577970 |
4 | 1 | Toy Story (1995) | Adventure|Animation|Children|Comedy|Fantasy | 17 | 4.5 | 1305696483 |
피벗 테이블
# 파이썬에서도 피벗 테이블을 할 수 있습니다.
# combined.pivot_table() 안에 피벗 테이블의 핵심 요소 3 가지를 넣어주면 됩니다.
# 어떤 수치를 보고자 하는지, 어떤 행, 어떤 열로 보고자 하는지가 필요합니다.
# 우리는 별점을 어떤 사람이 어떤 영화에 매겼는지로 나타내고자 합니다.
pivoted = combined.pivot_table(index = 'userId', columns = 'title', values = 'rating')
# pivoted 변수에는 영화의 제목이 컬럼으로, 사용자가 행(row) 으로 들어있습니다.
# 610 명의 사용자가 9719 개의 영화를 대상으로 남긴 별점이므로 .shape 에서 (610, 9719) 가 나오게 됩니다.
print(pivoted.shape)
(610, 9719)
# 이 데이터를 .head() 를 이용하여 앞 5 개 행만 잘라서 본다면 다음과 같습니다.
pivoted.head()
title | '71 (2014) | 'Hellboy': The Seeds of Creation (2004) | 'Round Midnight (1986) | 'Salem's Lot (2004) | 'Til There Was You (1997) | 'Tis the Season for Love (2015) | 'burbs, The (1989) | 'night Mother (1986) | (500) Days of Summer (2009) | *batteries not included (1987) | ... | Zulu (2013) | [REC] (2007) | [REC]² (2009) | [REC]³ 3 Génesis (2012) | anohana: The Flower We Saw That Day - The Movie (2013) | eXistenZ (1999) | xXx (2002) | xXx: State of the Union (2005) | ¡Three Amigos! (1986) | À nous la liberté (Freedom for Us) (1931) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
userId | |||||||||||||||||||||
1 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 4.0 | NaN |
2 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
3 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
4 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
5 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
5 rows × 9719 columns
# pivoted.columns 로 column 들만 뽑아서 titles 변수에 넣고
titles = pivoted.columns
# titles 변수에서 0 번째 인덱스를 뽑아와, 영화 제목임을 확인합니다.
titles[0]
"'71 (2014)"
# 인덱싱이 된다는 것은 0 번째, 1 번째... 로 구성되어있는 묶음이므로
# for 문으로 하나씩 뽑아올 수 있습니다.
for i in pivoted.columns:
# 검색하고자 하는 제목을 정확히 알기 위해 if문을 사용합니다.
# 영화 제목인 i 에 Godfather 와 일치하는 것이 들어있으면
# print() 로 i 인 영화제목을 출력해 줍니다.
if 'Godfather' in i:
print(i)
Godfather, The (1972)
Godfather: Part II, The (1974)
Godfather: Part III, The (1990)
The Godfather Trilogy: 1972-1990 (1992)
Tokyo Godfathers (2003)
# 대부 를 알고 싶었는데, Godfather, The (1972) 라고 되어있습니다.
# 이를 변수에 집어넣어 pivoted 변수에서 키워드로 삼아
# 대부 컬럼 전체를 가져옵니다.
target = 'Godfather, The (1972)'
matrix_rating = pivoted[target]
# .shape 를 통해 잘 가져왔음을 확인합니다.
# 한 영화에 대해 610 명이 매긴 별점 이므로, (610,) 이 나오는 것이 맞습니다.
# 전체 사람 중에서, 영화 대부에 대해 별점을 단 사람과 안 단 사람에 대한 정보입니다.
matrix_rating.shape
(610,)
# 행을 사람으로, 열을 영화 제목으로 하는 피벗 테이블인 pivoted 의 영화 제목들과
# pivoted 에서 한 컬럼을 뽑아낸 matrix_rating 사이의 상호 상관계수를 구합니다.
# 영화 대부에 달린 별점과 패턴이 비슷할 수록 1 에 가깝게 나오게 됩니다.
# 흔히 양/음의 상관관계 라고 이야기할 때 이 수치를 기준으로 말합니다.
# method='pearson' 부분은, 상관계수를 구하는 방법을 의미합니다.
matrix_similar = pivoted.corrwith(matrix_rating, method='pearson')
# 잘 되었는지 확인하기 위해 .shape 와 .head() 로 알아봅니다.
print(matrix_similar.shape)
matrix_similar.head(10)
(9719,)
title
'71 (2014) NaN
'Hellboy': The Seeds of Creation (2004) NaN
'Round Midnight (1986) NaN
'Salem's Lot (2004) NaN
'Til There Was You (1997) NaN
'Tis the Season for Love (2015) NaN
'burbs, The (1989) -0.745465
'night Mother (1986) NaN
(500) Days of Summer (2009) 0.093103
*batteries not included (1987) -0.852803
dtype: float64
# 수치가 없는 것 끼리는 계산이 안 되기 때문에, NaN 이 나오게 됩니다.
# 수치가 없다면 연관관계를 찾을 수 없는 것이므로, dropna() 이용해 제거합니다.
matrix_similar = matrix_similar.dropna()
# 수치가 높을수록 연관관계가 있는 것이므로,
# ascending=False 로 내림차순 정렬하고 .head() 로 점수가 높은 것만 추려봅니다.
# .head() 안에 숫자를 넣으면, 위에서 부터 몇 개를 조회할 것인지 결정할 수 있습니다.
matrix_similar.sort_values(ascending=False).head(10)
title
Going My Way (1944) 1.0
Shakes the Clown (1992) 1.0
Guess Who (2005) 1.0
Serbian Film, A (Srpski film) (2010) 1.0
White Man's Burden (1995) 1.0
After the Thin Man (1936) 1.0
Grey Zone, The (2001) 1.0
Vera Drake (2004) 1.0
Crazies, The (2010) 1.0
Shaggy Dog, The (1959) 1.0
dtype: float64
생소한 영화들이 잔뜩 추천되었습니다.
알고리즘을 개선하려면 어떻게 해야 할까요?
분야마다 용어는 다를 수 있지만 (threshold / confidence-level / trigger-level)
기준을 정해서, 걸러낼 필요가 있습니다.
추천 할 영화가 몇 명으로부터 별점을 받았는지를 기준으로 삼아보겠습니다.
너무 적은 사람이 본 영화는 판단하기 어렵기 때문입니다.
개선_1단계 : 전체 영화에 대해, 각 영화당 몇 명이 평가했는지 파악하기
# 숫자를 다루는 도구인 numpy 를 불러와, 향후 np 로 사용합니다.
import numpy as np
# 원본인 rating 으로부터 title, userId, rating 컬럼만 사용할 것이므로, 변수에 리스트로 할당합니다.
target_col = ['title', 'userId', 'rating']
# target_col 변수를 이용하여 combined 변수에서 해당 3 개의 컬럼만 뽑아 count_target 변수에 넣습니다.
count_target = combined[target_col]
# 같은 영화끼리 묶어 계산하기 위해 .groupby() 를 사용해 같은 제목끼리 묶어줍니다.
grouped = count_target.groupby('title')
# aggrigate => 집합하다, 모으다 의 뜻이 있습니다.
# 영화의 별점(rating) 에 대해 모아서 그 크기와 평균값을 나타내 줍니다.
counted = grouped.agg({'rating' : [np.size, np.mean, np.std]})
# 제대로 되었는지 .head() 를 이용하여 확인해 줍니다.
counted.head()
rating | |||
---|---|---|---|
size | mean | std | |
title | |||
'71 (2014) | 1.0 | 4.0 | NaN |
'Hellboy': The Seeds of Creation (2004) | 1.0 | 4.0 | NaN |
'Round Midnight (1986) | 2.0 | 3.5 | 0.000000 |
'Salem's Lot (2004) | 1.0 | 5.0 | NaN |
'Til There Was You (1997) | 2.0 | 4.0 | 1.414214 |
개선_2단계 : 100명 이상이 본 영화만을 대상으로 상관계수 적용하기
# counted 변수에서 size 가 100 이 넘는 영화들의 제목만 가져와 popular 변수에 할당합니다.
popular = counted['rating']['size'] >= 100
# 600여 명 중 최소 100 명 이상이 본 영화만을 대상으로 하기 위해, 해당 영화들만 뽑아줍니다.
popular_movies = counted[popular]
# 상호 상관관계인 matrix_similar는 데이터프레임이 아닙니다.
# 데이터끼리 합치려면 같은 데이터프레임 형태로 만들어야 하므로 pd.DataFrame 을 이용합니다.
# 상관계수에 해당하는 정보는 corr 라는 이름의 컬럼으로 할당 합니다.
movie_corr = pd.DataFrame(matrix_similar, columns = ['corr'])
# 100인 이상이 본 영화인 popular_movies 에 상관계수 정보 movie_corr 값을 넣어줍니다.
combined_result = popular_movies.join(movie_corr)
# 상관계수 정보인 corr 컬럼 기준 내림차순 정렬하여 .head() 로 상위 5 개만 확인합니다.
combined_result.sort_values(by = ['corr'], ascending = False).head()
(rating, size) | (rating, mean) | (rating, std) | corr | |
---|---|---|---|---|
title | ||||
Godfather, The (1972) | 192.0 | 4.289062 | 0.904344 | 1.000000 |
Godfather: Part II, The (1974) | 129.0 | 4.259690 | 0.803072 | 0.782643 |
Schindler's List (1993) | 220.0 | 4.225000 | 0.975996 | 0.456661 |
Fight Club (1999) | 218.0 | 4.272936 | 0.861384 | 0.445205 |
Saving Private Ryan (1998) | 188.0 | 4.146277 | 0.811770 | 0.441377 |
.head() 등 중간 확인없는 순수 데이터 처리 코드는 아래와 같습니다.
target = 'Godfather, The (1972)'
movies = pd.read_csv('data/movies.csv')
ratings = pd.read_csv('data/ratings.csv')
combined = pd.merge(movies, ratings)
pivoted = combined.pivot_table(index = 'userId', columns = 'title', values = 'rating')
matrix_similar = pivoted.corrwith(pivoted[target], method='pearson').dropna()
target_col = ['title', 'userId', 'rating']
counted = combined[target_col].groupby('title').agg({'rating' : [np.size, np.mean, np.std]})
popular = counted['rating']['size'] >= 100
combined_result = counted[popular].join(pd.DataFrame(matrix_similar, columns = ['corr']))
combined_result.sort_values(by = ['corr'], ascending = False).head()
(rating, size) | (rating, mean) | (rating, std) | corr | |
---|---|---|---|---|
title | ||||
Godfather, The (1972) | 192.0 | 4.289062 | 0.904344 | 1.000000 |
Godfather: Part II, The (1974) | 129.0 | 4.259690 | 0.803072 | 0.782643 |
Schindler's List (1993) | 220.0 | 4.225000 | 0.975996 | 0.456661 |
Fight Club (1999) | 218.0 | 4.272936 | 0.861384 | 0.445205 |
Saving Private Ryan (1998) | 188.0 | 4.146277 | 0.811770 | 0.441377 |
User-Based? 간단해요~
Item-Based 에서는 아이템 간의 유사도를 계산하였다면
User-Based 에서는 사람들 간의 유사도를 계산합니다.
pivot_table 시 행 / 열을 반대로 집어넣고
aggregate 시 title 이 아닌, userId 를 기준으로 삼아주면 됩니다.
사람들의 선호에 의해 나오게 되는 의사결정 데이터를 해석하는 기준의 차이 입니다.
비슷한 물건을 추천 VS 비슷한 사람을 추천
target = 1
movies = pd.read_csv('data/movies.csv')
ratings = pd.read_csv('data/ratings.csv')
combined = pd.merge(movies, ratings)
# 여기에서 pivot_table 기준이 되는 행/열을 서로 바꿨습니다. Transpose 한 것과 같은 효과 입니다.
pivoted = combined.pivot_table(index = 'title', columns = 'userId', values = 'rating')
matrix_similar = pivoted.corrwith(pivoted[target], method='pearson').dropna()
target_col = ['title', 'userId', 'rating']
# 여기에서 group_by 기준을 userId로 바꿔줍니다. 유사도 판단 기준이 영화 -> 사람으로 바뀌었기 때문입니다.
counted = combined[target_col].groupby('userId').agg({'rating' : [np.size, np.mean, np.std]})
popular = counted['rating']['size'] >= 100
combined_result = counted[popular].join(pd.DataFrame(matrix_similar, columns = ['corr']))
combined_result.sort_values(by = ['corr'], ascending = False).head()
(rating, size) | (rating, mean) | (rating, std) | corr | |
---|---|---|---|---|
userId | ||||
1 | 232.0 | 4.366379 | 0.800048 | 1.000000 |
139 | 194.0 | 2.144330 | 0.894597 | 0.790569 |
210 | 138.0 | 4.079710 | 0.861264 | 0.767649 |
369 | 129.0 | 3.391473 | 0.684543 | 0.612098 |
351 | 141.0 | 3.666667 | 0.672593 | 0.600000 |
더 나아가기 위한 탐색적 데이터 분석
# 현재 combined_result 변수에는 한 row 마다 한 사람이 있고,
# size는 별점을 준 횟수, mean은 평균별점, std에서는 표준편차가 있습니다.
# 이를 히스토그램으로 나타내면, 분포를 한 눈에 확인할 수 있습니다.
combined_result.hist(bins=20)
# 하나씩 확인해보겠습니다.
# 데이터프레임의 뒤에 .hist() 를 이용하여, 컬럼명을 넣으면 됩니다.
# 상관계수의 분포는 양의 상관관계가 상대적으로 낮게 나왔습니다.
combined_result.hist('corr')
# 사람들이 준 별점의 분포입니다. 낮은 별점이 별로 없음을 알 수 있습니다.
# 0 부터 5까지 이므로 중간값은 2.5 이지만, 평균은 5에 편향되어있습니다.
# !! 편향이 발생되는 부분입니다.
combined_result.hist(('rating', 'mean'))
# 별점을 주는 사람의 표준편차 분포입니다.
# 낮을수록 일관된 평가를 하고, 높을수록 호불호 표현이 명확함을 의미합니다.
# 상대적으로 호불호 표현이 명확한 사람들이 비교적 적음을 알 수 있습니다.
combined_result.hist(('rating', 'std'))
# 사람들이 영화 평을 몇 번 했는지를 나타내는 분포입니다.
# 대부분의 사람들이 500회 미만의 평을 남긴 것을 알 수 있습니다.
# 우리는 이미 100회 미만인 경우를 제외하였으므로, 원본 데이터에서도 확인이 필요합니다.
# 2000회 이상 평가한 사람들도 있지만, 대부분의 평가가 500회 미만에서 일어납니다.
# !! 편향이 발생되는 부분입니다.
combined_result.hist(('rating', 'size'))
개선 가능할 부분?
[생각해 볼 만한 포인트 몇 가지]
- 사람들이 잘 모르거나, 최신 영화는 별점을 매긴 사람이 적어서 추천되지 않습니다.
- 아직 개봉하지 않은 영화는 데이터가 존재하지 않아 추천되지 않습니다.
- 72년도에 개봉한 대부가 12년도에 나온 영화라면 추천인과 평이 다를 것입니다.
[현재 추천모델을 기반으로 개선]
- 최소 추천인원 수 조정해보기
- 상관계수 구하는 방법 바꿔보기(method=) -> pearson / Kendall / Spearman
- 점수를 후하게 또는 박하게 주는 사람 제외? / 너무 많이 또는 적게 본 사람 제외?
[새로운 방법으로 모델링하여 개선]
- Item-Based + User-Based 모델을 따로 만들고, 점수를 평균하여 사용
- correlation 이 아닌 머신러닝 / 딥러닝 방법 사용
- Item의 상관관계 도출시 사람들의 선택이 아니라 영화정보 기반으로 한다면?
[더 나아간다면?]
필터의 다단화를 통해 더 촘촘한 추천 모델을 만드는 것이 가능합니다.
Item-Based 로 후보군 추출 -> 딥러닝 적용
'[중급] 가볍게 이것저것' 카테고리의 다른 글
[뷰티 상품] 추천 시스템 구축하기 (0) | 2019.08.21 |
---|---|
[도서] 추천 시스템 구축, ,데이터가 너무 'big' 해서 생기는 문제 해결법? (0) | 2019.08.19 |
[도서] 추천 시스템 구축 초간단한 방법 (0) | 2019.08.19 |
발전소에서 나온 데이터 분석해보기 (0) | 2019.08.15 |
산업 현장에서 다루는 데이터 분석하기 (0) | 2019.08.14 |