티스토리 뷰
import time
import operator
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")
# Data Source : https://grouplens.org/datasets/movielens
rating_file_path = '/home/jaeyoon89/python-data-analysis/data/ml-1m/ratings.dat'
movie_file_path = '/home/jaeyoon89/python-data-analysis/data/ml-1m/movies.dat'
user_file_path = '/home/jaeyoon89/python-data-analysis/data/ml-1m/users.dat'
rating_data = pd.io.parsers.read_csv(rating_file_path,
names=['user_id', 'movie_id', 'rating', 'time'],
delimiter='::')
movie_data = pd.io.parsers.read_csv(movie_file_path,
names=['movie_id', 'title', 'genre'], delimiter='::')
user_data = pd.io.parsers.read_csv(user_file_path,
names=['user_id', 'gender', 'age', 'occupation', 'zipcode'], delimiter='::')
rating_data.head()
user_id | movie_id | rating | time | |
---|---|---|---|---|
0 | 1 | 1193 | 5 | 978300760 |
1 | 1 | 661 | 3 | 978302109 |
2 | 1 | 914 | 3 | 978301968 |
3 | 1 | 3408 | 4 | 978300275 |
4 | 1 | 2355 | 5 | 978824291 |
movie_data.head()
movie_id | title | genre | |
---|---|---|---|
0 | 1 | Toy Story (1995) | Animation|Children's|Comedy |
1 | 2 | Jumanji (1995) | Adventure|Children's|Fantasy |
2 | 3 | Grumpier Old Men (1995) | Comedy|Romance |
3 | 4 | Waiting to Exhale (1995) | Comedy|Drama |
4 | 5 | Father of the Bride Part II (1995) | Comedy |
user_data.head()
user_id | gender | age | occupation | zipcode | |
---|---|---|---|---|---|
0 | 1 | F | 1 | 10 | 48067 |
1 | 2 | M | 56 | 16 | 70072 |
2 | 3 | M | 25 | 15 | 55117 |
3 | 4 | M | 45 | 7 | 02460 |
4 | 5 | M | 25 | 20 | 55455 |
step.2 분석: 탐색적 데이터 분석하기¶
이번엔 탐색적 분석을 통해 영화 데이터를 살펴보자. 다음의 실행 결과는 영화의 갯수와 연도별 탐색에 대한 출력 결과이다.
- 분석할 영화의 정보 탐색하기
print("total number of movie in data:", len(movie_data['movie_id'].unique()))
movie_data['year'] = movie_data['title'].apply(lambda x:x[-5:-1])
movie_data['year'].value_counts().head(10)
total number of movie in data: 3883
1996 345 1995 342 1998 337 1997 315 1999 283 1994 257 1993 165 2000 156 1986 104 1992 102 Name: year, dtype: int64
다음으로 영화 데이터에서 가장 많이 등장한 장르가 무엇인지를 탐색해보자.movie_data의 피처인 genre는 '드라마|코미디|액션|' 처럼 '|' 이라는 구분자를 포함하여 여러 장르를 하나의 문자열에 포함하고 있다. 따라서 데이터에 등장하는 모든 개별 장르를 세기 위해서는 split() 함수로 genre 데이터를 분리해야 한다. 각 장르마다의 등장 개수는 dictionary 자료로 저장한다.
- 장르의 속성 탐색하기
unique_genre_dict = {}
for index, row in movie_data.iterrows():
genre_combination = row['genre']
parsed_genre = genre_combination.split('|')
for genre in parsed_genre:
if genre in unique_genre_dict:
unique_genre_dict[genre] += 1
else:
unique_genre_dict[genre] = 1
plt.rcParams['figure.figsize'] = [20,16]
sns.barplot(list(unique_genre_dict.keys()), list(unique_genre_dict.values()),
alpha=0.8)
plt.title('Popular genre in movies')
plt.ylabel('Count of genre', fontsize=12)
plt.xlabel('Genre', fontsize=12)
plt.show()
그리고 분석 대상이 되는 유저의 수를 탐색해 보면 총 6040명으로 나타난다.
- 분석할 유저의 정보 탐색하기
print("total number of user in data :", len(user_data['user_id'].unique()))
total number of user in data : 6040
지금까지 user_data, movie_data 데이터의 특징을 살펴본 것은 '평점 예측'의 측면에서는 중요한 탐색이라고 볼 수 없다. 하지만 rating 데이터는 평점 예측 데이터 분석에 중요한 데이터이기 때문에 조금 더 자세히 탐색을 수행할 필요가 있다.
- 평점 데이터의 정보 탐색하기
movie_rate_count = rating_data.groupby('movie_id')['rating'].count().values
plt.rcParams['figure.figsize'] = [8,8]
fig = plt.hist(movie_rate_count, bins = 200)
plt.ylabel('Count', fontsize=12)
plt.xlabel("Movie's rated count", fontsize=12)
plt.show()
print("total number of movie in data :", len(movie_data['movie_id'].unique()))
print("total number of movie rated below 100 :", len(movie_rate_count[movie_rate_count<100]))
total number of movie in data : 3883 total number of movie rated below 100 : 1687
다음은 각 영화의 평균 평점을 알아보자. 아래의 코드에서는 agg()함수로 각 영화당 rating의 개수와 평균값을 계산한다.
movie_grouped_rating_info = rating_data.groupby("movie_id")['rating'].agg(['count','mean'])
movie_grouped_rating_info.columns = ['rated_count', 'rating_mean']
movie_grouped_rating_info.head(5)
rated_count | rating_mean | |
---|---|---|
movie_id | ||
1 | 2077 | 4.146846 |
2 | 701 | 3.201141 |
3 | 478 | 3.016736 |
4 | 170 | 2.729412 |
5 | 296 | 3.006757 |
movie_grouped_rating_info['rating_mean'].hist(bins=150, grid=False)
<AxesSubplot:>
평균값에 대한 시각화는 위와 같다. 대부분의 평점은 2점 ~ 4점 사이로 나타났으며, 이를 통해 대부분의 영화 평점은 2점 ~ 4점 사이의 값으로 예측될 것이라는 가설을 수립할 수 있다.
현재 분석 중인 MovieLens 데이터는 U-I-R 데이터셋이다. 이러한 데이터는 행렬로 나타내기 매우 용이하며 다음의 실행결과처럼 시각화할 수 있다.
- user-movie 형태의 표로 살펴보기
rating_data.head()
user_id | movie_id | rating | time | |
---|---|---|---|---|
0 | 1 | 1193 | 5 | 978300760 |
1 | 1 | 661 | 3 | 978302109 |
2 | 1 | 914 | 3 | 978301968 |
3 | 1 | 3408 | 4 | 978300275 |
4 | 1 | 2355 | 5 | 978824291 |
rating_table = rating_data[['user_id', 'movie_id', 'rating']].set_index(["user_id", "movie_id"]).unstack()
rating_table.head(10)
rating | |||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
movie_id | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ... | 3943 | 3944 | 3945 | 3946 | 3947 | 3948 | 3949 | 3950 | 3951 | 3952 |
user_id | |||||||||||||||||||||
1 | 5.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 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 | 2.0 | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
6 | 4.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
7 | NaN | NaN | NaN | NaN | NaN | 4.0 | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
8 | 4.0 | NaN | NaN | 3.0 | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
9 | 5.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | 3.0 | NaN | NaN | NaN | NaN |
10 | 5.0 | 5.0 | NaN | NaN | NaN | NaN | 4.0 | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | 4.0 | NaN | NaN | NaN | NaN |
10 rows × 3706 columns
plt.rcParams['figure.figsize'] = [10, 10]
plt.imshow(rating_table)
plt.grid(False)
plt.xlabel("Movie")
plt.ylabel("User")
plt.title("User-movie Matrix")
plt.show()
step.3 예측: 수학적 기법을 활용해 평점 예측하기¶
행렬 완성은 행렬 분해 방법을 이용한다. 행렬 분해는 수학적 성질을 이용하여 하나의 행렬을 여러 개의 행렬 곱으로 나타내는 방법이다. 본 예제에서는 행렬 분해 중에서도 가장 활용도가 높은 특이값 분해(SVD) 라는 방법을 활용하여 영화 평점을 예측할 것이다. 우선 SVD를 사용하기 위해 surprise 라이브러리를 설치한다.
라이브러리 사용법은 아래의 코드와 같다. 먼저 평점의 범위가 1 ~ 5인 Reader 객체를 생성하고, load_from_df() 함수와 build_full_trainset() 함수를 이용하여 rating 데이터를 surprise 라이브러리의 데이터셋 형태로 변환해준다. 마지막으로는 SVD라는 클래스를 선언한 뒤, model.fit(train_data)로 행렬 완성 모델을 학습하자.
- MoveiLens 데이터에 SVD 적용하기
from surprise import SVD, Dataset, Reader, accuracy
from surprise.model_selection import train_test_split
reader = Reader(rating_scale=(1,5))
data = Dataset.load_from_df(rating_data[['user_id', 'movie_id', 'rating']], reader)
train_data = data.build_full_trainset()
train_start = time.time()
model = SVD(n_factors=8,
lr_all=0.005,
reg_all=0.02,
n_epochs=100)
model.fit(train_data)
train_end = time.time()
print("training time of model: %.2f seconds" % (train_end - train_start))
training time of model: 45.03 seconds
다음으로 학습한 모델의 평점 예측 결과를 살펴보기 위해 한 명의 데이터를 선정한다. 예제에선 user_id가 4인 유저를 선정했다.
- 영화의 점수를 예측할 타겟 유저 선정하기
target_user_id = 4
target_user_data = rating_data[rating_data['user_id']==target_user_id]
target_user_data.head(5)
user_id | movie_id | rating | time | |
---|---|---|---|---|
233 | 4 | 3468 | 5 | 978294008 |
234 | 4 | 1210 | 3 | 978293924 |
235 | 4 | 2951 | 4 | 978294282 |
236 | 4 | 1214 | 4 | 978294260 |
237 | 4 | 1036 | 4 | 978294282 |
4번 유저가 평가한 영화의 목록을 추출하는 과정은 다음과 같다. 아래 출력 결과는 유저의 영화 관람 히스토리를 {movie_id : rating} 형태로 추출한 것이다.
target_user_movie_rating_dict = {}
for index, row in target_user_data.iterrows():
movie_id = row['movie_id']
target_user_movie_rating_dict[movie_id] = row['rating']
print(target_user_movie_rating_dict)
{3468: 5, 1210: 3, 2951: 4, 1214: 4, 1036: 4, 260: 5, 2028: 5, 480: 4, 1196: 2, 1198: 5, 1954: 5, 1097: 4, 3418: 4, 3702: 4, 2366: 4, 1387: 5, 3527: 1, 1201: 5, 2692: 5, 2947: 5, 1240: 5}
이제 예측 모델에 4번 유저의 정보를 입력하여 '아직 보지 않은 영화들의 평점'을 예측해 보자. 이를 위해 model.test() 함수를 사용한다. 다음 코드에선 4번 유저가 아직 보지 않은 영화의 리스트로 test_data를 구성하였다. model.test(test_data)를 실행하면 4번 유저가 아직 보지 않은 영화들의 예측 평점을 반환한다.
- 타겟 유저가 보지 않은 영화 중, 예상 펴점이 높은 10개 선정
test_data = []
for index, row in movie_data.iterrows():
movie_id = row['movie_id']
rating = 0
if movie_id in target_user_movie_rating_dict:
continue
test_data.append((target_user_id, movie_id, rating))
target_user_predictions = model.test(test_data)
def get_user_predicted_ratings(predictions, user_id, user_history):
target_user_movie_predict_dict = {}
for uid, mid, rating, predicted_rating, _ in predictions:
if user_id == uid:
if mid not in user_history:
target_user_movie_predict_dict[mid] = predicted_rating
return target_user_movie_predict_dict
target_user_movie_predict_dict = get_user_predicted_ratings(predictions=target_user_predictions,
user_id=target_user_id,
user_history=target_user_movie_rating_dict)
target_user_top10_predicted = sorted(target_user_movie_predict_dict.items(),
key=operator.itemgetter(1), reverse=True)[:10]
target_user_top10_predicted
[(106, 5), (326, 5), (527, 5), (602, 5), (615, 5), (858, 5), (912, 5), (922, 5), (1096, 5), (1104, 5)]
그리고 다음의 실행 결과는 TOP 10 영화의 제목을 매칭하여 출력한 것이다.
movie_dict = {}
for index, row in movie_data.iterrows():
movie_id = row['movie_id']
movie_title = row['title']
movie_dict[movie_id] = movie_title
for predicted in target_user_top10_predicted:
movie_id = predicted[0]
predicted_rating = predicted[1]
print(movie_dict[movie_id], ":", predicted_rating)
Nobody Loves Me (Keiner liebt mich) (1994) : 5 To Live (Huozhe) (1994) : 5 Schindler's List (1993) : 5 Great Day in Harlem, A (1994) : 5 Bread and Chocolate (Pane e cioccolata) (1973) : 5 Godfather, The (1972) : 5 Casablanca (1942) : 5 Sunset Blvd. (a.k.a. Sunset Boulevard) (1950) : 5 Sophie's Choice (1982) : 5 Streetcar Named Desire, A (1951) : 5
step.4 평가: 예측 모델 평가하기¶
이제 우리가 스스로 의문을 가져야 한다. '과연 이 예측이 얼마나 정확한 예측일까?" 라는 의문이다. 이를 해소하기 위해서는 모델이 얼마나 정확하게 행렬을 완성했는지 평가해야 한다. 행렬 완성의 가장 보편적인 평가 방법은 RMSE를 계산하는 것이다. SVD 모델에서 RMSE를 출력하는 코드는 다음과 같다.
- 예측 모델의 평가 방법
reader = Reader(rating_scale=(1,5))
data = Dataset.load_from_df(rating_data[['user_id', 'movie_id', 'rating']], reader)
train_data, test_data = train_test_split(data, test_size=0.2)
train_start = time.time()
model = SVD(n_factors=8,
lr_all=0.005,
reg_all=0.02,
n_epochs=100)
model.fit(train_data)
train_end = time.time()
print("training time of model: %.2f seconds" % (train_end - train_start))
predictions = model.test(test_data)
print("RMSE of test in SVD model:")
accuracy.rmse(predictions)
training time of model: 45.04 seconds RMSE of test in SVD model: RMSE: 0.8607
0.8606913183397654
이번엔 4번 유저의 예측 평점과 실제 평점을 비교하는 시각화 그래프를 출력해보자. 아래의 코드는 4번 유저가 영화를 아직 보지 않았다는 가정하에 실제로 보았던 21개 영화의 가상 예측 평점을 계산한 것이다.
- 실제 평점과의 비교 시각화하기 : 평점 예측 단계
test_data = []
for index, row in movie_data.iterrows():
movie_id = row['movie_id']
if movie_id in target_user_movie_rating_dict:
rating = target_user_movie_rating_dict[movie_id]
test_data.append((target_user_id, movie_id, rating))
target_user_predictions = model.test(test_data)
def get_user_predicted_ratings(predictions, user_id, user_history):
target_user_movie_predict_dict = {}
for uid, mid, rating, predicted_rating, _ in predictions:
if user_id == uid:
if mid in user_history:
target_user_movie_predict_dict[mid] = predicted_rating
return target_user_movie_predict_dict
target_user_movie_predict_dict = get_user_predicted_ratings(predictions=target_user_predictions,
user_id=target_user_id,
user_history=target_user_movie_rating_dict)
target_user_movie_predict_dict
{260: 3.9430986951928593, 480: 3.682508876258768, 1036: 4.361131075846246, 1097: 4.254047176433112, 1196: 3.669192083937819, 1198: 4.373158139422043, 1201: 4.4762447217678565, 1210: 3.0923035755733803, 1214: 4.498483394791014, 1240: 4.487038891028856, 1387: 4.643582253537883, 1954: 4.8516889512440144, 2028: 4.431559624604528, 2366: 4.059585582700539, 2692: 4.001241103390469, 2947: 4.323632665147499, 2951: 4.304399345628873, 3418: 4.009121087715634, 3468: 4.634814838398916, 3527: 3.903596607901292, 3702: 4.346687656334132}
4번 유저가 실제로 관람했던 21개 영화에 대한 가상 예측 평점, 실제 평점, 그리고 영화의 제목을 하나라 출력한 결과는 다음과 같다.
- 실제 평점과의 비교 시작하기
origin_rating_list = []
predicted_rating_list = []
movie_title_list = []
idx = 0
for movie_id, predicted_rating in target_user_movie_predict_dict.items():
idx = idx + 1
predicted_rating = round(predicted_rating,2)
origin_rating = target_user_movie_rating_dict[movie_id]
movie_title = movie_dict[movie_id]
print("movie", str(idx), ":", movie_title, ":", origin_rating, "/", predicted_rating)
origin_rating_list.append(origin_rating)
predicted_rating_list.append(predicted_rating)
movie_title_list.append(str(idx))
movie 1 : Star Wars: Episode IV - A New Hope (1977) : 5 / 3.94 movie 2 : Jurassic Park (1993) : 4 / 3.68 movie 3 : Die Hard (1988) : 4 / 4.36 movie 4 : E.T. the Extra-Terrestrial (1982) : 4 / 4.25 movie 5 : Star Wars: Episode V - The Empire Strikes Back (1980) : 2 / 3.67 movie 6 : Raiders of the Lost Ark (1981) : 5 / 4.37 movie 7 : Good, The Bad and The Ugly, The (1966) : 5 / 4.48 movie 8 : Star Wars: Episode VI - Return of the Jedi (1983) : 3 / 3.09 movie 9 : Alien (1979) : 4 / 4.5 movie 10 : Terminator, The (1984) : 5 / 4.49 movie 11 : Jaws (1975) : 5 / 4.64 movie 12 : Rocky (1976) : 5 / 4.85 movie 13 : Saving Private Ryan (1998) : 5 / 4.43 movie 14 : King Kong (1933) : 4 / 4.06 movie 15 : Run Lola Run (Lola rennt) (1998) : 5 / 4.0 movie 16 : Goldfinger (1964) : 5 / 4.32 movie 17 : Fistful of Dollars, A (1964) : 4 / 4.3 movie 18 : Thelma & Louise (1991) : 4 / 4.01 movie 19 : Hustler, The (1961) : 5 / 4.63 movie 20 : Predator (1987) : 1 / 3.9 movie 21 : Mad Max (1979) : 4 / 4.35
아래의 그래프는 지금까지의 분석 결과를 시각화한 것이다. 약 1 ~ 2개 정도의 영화를 제외하면 실제 평점과 가상 예측 평점이 크게 다르지 않은 것을 알 수 있다.
origin = origin_rating_list
predicted = predicted_rating_list
plt.rcParams['figure.figsize'] = (10,6)
index = np.arange(len(movie_title_list))
bar_width = 0.2
rects1 = plt.bar(index, origin, bar_width,
color = 'orange',
label = 'Origin')
rects2 = plt.bar(index + bar_width, predicted, bar_width,
color = 'green',
label = 'Predicted')
plt.xticks(index, movie_title_list)
plt.legend()
plt.show()
출처 : 이것이 데이터 분석이다.
'이것이 데이터분석이다 with 파이썬' 카테고리의 다른 글
이것이 데이터 분석이다 with 파이썬 ch5-2(구매 데이터를 분석하여 상품 추천하기) (0) | 2021.04.24 |
---|---|
이것이 데이터 분석이다 with 파이썬 ch4-1(타이타닉 생존자 가려내기) (0) | 2021.04.16 |
이것이 데이터 분석이다 with 파이썬 ch3-2(비트코인 시세 예측하기) (0) | 2021.04.14 |
이것이 데이터 분석이다 with 파이썬 ch3-1(프로야구 선수의 다음 해 연봉 예측하기) (0) | 2021.04.12 |
이것이 데이터 분석이다 with 파이썬 ch1-2(국가별 음주 데이터 분석하기) (0) | 2021.04.11 |