- movies.csv: 영화 정보 데이터

- ratings.csv: 평점정보 데이터

 

1. 데이터 읽어오기

movies = pd.read_csv('./python_machine_learning-main/data/movielens/movies.csv')
ratings = pd.read_csv('./python_machine_learning-main/data/movielens/ratings.csv')
print(movies.shape)
print(ratings.shape)
(9742, 3)
(100836, 4)



 

 

# ratings에서 timestamp 제거

ratings = ratings[['userId', 'movieId', 'rating']]
ratings_matrix.head(3)

 

 

 

- 추천 시스템 데이터 형태

  • user id별로 상품 id가 펼쳐지거나, 상품 id별로 user id가 펼쳐져야 한다.
  • 둘중 하나를 인덱스로 보내야 함 

# user id를 인덱스로 해서 movie id 별로 평점을 확인할 수 있도록 변경

ratings_matrix = ratings.pivot_table('rating', index='userId', columns='movieId')
  • 컬럼에 영화 제목이 아니라 movie id가 출력됨
  • movie id를 영화 제목으로 변경

 

# movies와 ratings를 movieid 컬럼을 기준으로 join(merge)

# title 컬럼을 얻기 이해 movies 와 조인 수행
rating_movies = pd.merge(ratings, movies, on='movieId')
rating_movies.head(3)

 

 

# pivot

# columns='title' 로 title 컬럼으로 pivot 수행. 
ratings_matrix = rating_movies.pivot_table('rating', index='userId', columns='title')

# NaN 값을 모두 0 으로 변환
ratings_matrix = ratings_matrix.fillna(0)
ratings_matrix.head(3)
  • 행렬을 펼쳐낼 때 유저별로 유사도를 계산할지, 영화별로 유사도를 계산할지에 따라서 펼쳐내는 방향이 달라진다.
  • 유사도를 계산할 항목이 인덱스로 존재하면 됨

 

 

 

2. 영화간 유사도 산출

# 아이템 기반으로 유사도 측정. 아이템인 영화제목이 index가 되도록 설정

ratings_matrix_T = ratings_matrix.transpose()
ratings_matrix_T.head(3)

 

 

# 코사인 유사도

#영화를 기준으로 코사인 유사도 산출
from sklearn.metrics.pairwise import cosine_similarity

item_sim = cosine_similarity(ratings_matrix_T, ratings_matrix_T)
print(item_sim)
[[1.         0.         0.         ... 0.32732684 0.         0.        ]
 [0.         1.         0.70710678 ... 0.         0.         0.        ]
 [0.         0.70710678 1.         ... 0.         0.         0.        ]
 ...
 [0.32732684 0.         0.         ... 1.         0.         0.        ]
 [0.         0.         0.         ... 0.         1.         0.        ]
 [0.         0.         0.         ... 0.         0.         1.        ]]

 

 

# 영화 제목 붙이기

# cosine_similarity() 로 반환된 넘파이 행렬을 영화명을 매핑하여 DataFrame으로 변환
item_sim_df = pd.DataFrame(data=item_sim, index=ratings_matrix.columns,
                          columns=ratings_matrix.columns)
print(item_sim_df.shape)
item_sim_df.head(3)

 

 

# Godfather, The(1972)와 가장 유사한 영화 5개 추출

item_sim_df["Godfather, The (1972)"].sort_values(ascending=False)[:6]
title
Godfather, The (1972)                        1.000000
Godfather: Part II, The (1974)               0.821773
Goodfellas (1990)                            0.664841
One Flew Over the Cuckoo's Nest (1975)       0.620536
Star Wars: Episode IV - A New Hope (1977)    0.595317
Fargo (1996)                                 0.588614
Name: Godfather, The (1972), dtype: float64

 

 

 

 

3. 아이템 기반 최근접 이웃 협업 필터링으로 개인화된 영화 추천

  • 아이템 기반의 영화 유사도 데이터는 모든 사용자의 평점을 기준으로 영화의 유사도를 생성했기 때문에 영화를 추천할 수는 있지만 개인의 취향을 전혀 반영하지 않음
  • 개인화된 영화 추천은 유저가 아직 관람하지 않은 영화를 추천해야 함
  • 아직 관람하지 않은 영화에 대해서 아이템 유사도와 기존에 관람한 영화의 평점데이터를 기반으로 모든 영화의 평점을 예측하고 그중에서 높은 예측 평점을 가진 영화를 추천

- 계산식: 사용자가 본 영화에 대한 실제 평점과 다른 모든 영화와의 코사인 유사도를 내적 곱 하고 그 값을 전체 합으로 나눔

 

 

# 사용자 별로 평점을 예측해주는 함수

def predict_rating(ratings_arr, item_sim_arr ):
    ratings_pred = ratings_arr.dot(item_sim_arr)/ np.array([np.abs(item_sim_arr).sum(axis=1)])
    return ratings_pred

 

 

#개인화된 예측 평점 확인

ratings_pred = predict_rating(ratings_matrix.values , item_sim_df.values)
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index= ratings_matrix.index,
                                   columns = ratings_matrix.columns)
ratings_pred_matrix.head(3)

 

 

# 평가

from sklearn.metrics import mean_squared_error

# 사용자가 평점을 부여한 (0이 아닌) 영화에 대해서만 예측 성능 평가 MSE 를 구함. 
def get_mse(pred, actual):
    pred = pred[actual.nonzero()].flatten()
    actual = actual[actual.nonzero()].flatten()
    return mean_squared_error(pred, actual)

print('아이템 기반 모든 인접 이웃 MSE: ', get_mse(ratings_pred, ratings_matrix.values ))
아이템 기반 모든 인접 이웃 MSE:  9.895354759094706
  • MSE가 너무 높다. 
  • 평점이 5.0인데, 제곱근인 3정도 차이가 난다는 건 추천 시스템으로 사용하기 어렵다는 뜻

 

 

 

4. 추천 수정

  • 현재는 모든 영화와의 유사도를 이용해서 평점을 예측했는데 모든 영화보다는 유저가 본 영화 중 유사도가 가장 높은 영화 몇개를 이용해서 예측하는 것이 더 나을 가능성이 높다.
# 유사도를 계산할 영화의 개수를 배개변수로 추가
def predict_rating_topsim(ratings_arr, item_sim_arr, n=20):
    # 사용자-아이템 평점 행렬 크기만큼 0으로 채운 예측 행렬 초기화
    pred = np.zeros(ratings_arr.shape)

    # 사용자-아이템 평점 행렬의 열 크기만큼 Loop 수행 
    for col in range(ratings_arr.shape[1]):
        # 유사도 행렬에서 유사도가 큰 순으로 n개 데이터 행렬의 index 반환
        top_n_items = [np.argsort(item_sim_arr[:, col])[:-n-1:-1]]
        # 개인화된 예측 평점을 계산
        for row in range(ratings_arr.shape[0]):
            pred[row, col] = item_sim_arr[col, :][top_n_items].dot(ratings_arr[row, :][top_n_items].T) 
            pred[row, col] /= np.sum(np.abs(item_sim_arr[col, :][top_n_items]))        
    return pred

 

# 수정 후 성능

ratings_pred = predict_rating_topsim(ratings_matrix.values , item_sim_df.values, n=20)
print('아이템 기반 인접 TOP-20 이웃 MSE: ', get_mse(ratings_pred, ratings_matrix.values ))


# 계산된 예측 평점 데이터는 DataFrame으로 재생성
ratings_pred_matrix = pd.DataFrame(data=ratings_pred, index= ratings_matrix.index,
                                   columns = ratings_matrix.columns)
아이템 기반 인접 TOP-20 이웃 MSE:  3.695009387428144
  • 아이템이나 유저를 가지고 유사도를 측정할 때 모든 데이터를 사용하는 것보다는 유사도가 높은 데이터 몇개를 이용해서 예측을 하는 것이 성능이 좋은 경우가 많다.
#9번째 유저에 영화 추천
user_rating_id = ratings_matrix.loc[9, :]
user_rating_id[ user_rating_id > 0].sort_values(ascending=False)[:10]
title
Adaptation (2002)                                                                 5.0
Citizen Kane (1941)                                                               5.0
Raiders of the Lost Ark (Indiana Jones and the Raiders of the Lost Ark) (1981)    5.0
Producers, The (1968)                                                             5.0
Lord of the Rings: The Two Towers, The (2002)                      5.0
Lord of the Rings: The Fellowship of the Ring, The (2001)       5.0
Back to the Future (1985)                                                         5.0
Austin Powers in Goldmember (2002)                                   5.0
Minority Report (2002)                                                            4.0
Witness (1985)                                                                    4.0
Name: 9, dtype: float64

 

 

# 유저가 보지 않은 영화 목록을 리턴하는 함수

def get_unseen_movies(ratings_matrix, userId):
    # userId로 입력받은 사용자의 모든 영화정보 추출하여 Series로 반환함. 
    # 반환된 user_rating 은 영화명(title)을 index로 가지는 Series 객체임. 
    user_rating = ratings_matrix.loc[userId,:]
    
    # user_rating이 0보다 크면 기존에 관람한 영화임. 대상 index를 추출하여 list 객체로 만듬
    already_seen = user_rating[ user_rating > 0].index.tolist()
    
    # 모든 영화명을 list 객체로 만듬. 
    movies_list = ratings_matrix.columns.tolist()
    
    # list comprehension으로 already_seen에 해당하는 movie는 movies_list에서 제외함. 
    unseen_list = [ movie for movie in movies_list if movie not in already_seen]
    
    return unseen_list

 

 

# 유저가 보지 않은 영화 목록에서 예측 평점이 높은 영화 제목을 리턴하는 함수

def recomm_movie_by_userid(pred_df, userId, unseen_list, top_n=10):
    # 예측 평점 DataFrame에서 사용자id index와 unseen_list로 들어온 영화명 컬럼을 추출하여
    # 가장 예측 평점이 높은 순으로 정렬함. 
    recomm_movies = pred_df.loc[userId, unseen_list].sort_values(ascending=False)[:top_n]
    return recomm_movies

 

# 유저가 보지 않은 영화 목록 추출

# 사용자가 관람하지 않는 영화명 추출   
unseen_list = get_unseen_movies(ratings_matrix, 9)

# 아이템 기반의 인접 이웃 협업 필터링으로 영화 추천 
recomm_movies = recomm_movie_by_userid(ratings_pred_matrix, 9, unseen_list, top_n=10)

# 평점 데이타를 DataFrame으로 생성. 
recomm_movies = pd.DataFrame(data=recomm_movies.values,index=recomm_movies.index,columns=['pred_score'])
recomm_movies

 

 

 

 

5. 작업 과정 정리

- 각 영화 간의 유사도 측정

 

- 유저가 본 영화의 평점을 기반으로 해서 유사도가 높은 20개의 영화를 추출하고, 그 영화들의 평점으로 않은 영화의 평점을 예측