Week 09. MovieLens 100K DataSet
01. How are the ratings distributed?
- 데이터 프레임에서 가져온 평점 데이터를 상대적인 빈도로 시각화하여, 각 평점의 분포를 한눈에 파악
norm_counts = (
ratings_df['rating']
.value_counts(normalize=True, sort=False)
.multiply(100) #상대적인 비율을 퍼센트로 변환
.reset_index() #데이터 프레임을 재설정
)
ax = sns.barplot(x='rating', y='proportion', data=norm_counts) #막대그래프 생성
ax.set_title('Rating Frequencies')
plt.show()
- norm_counts : ‘ratings_df’ 데이터프레임에서 rating 열의 값을 가져와서, value_counts() 메서드를 사용하여 각 평점 값의 빈도를 계산
- normalize=True : 각 평점 값의 상대적인 비율을 계산
- ‘rating’ 열 : 평점 값
- proprotion 열 : 해당 평점의 상대적인 빈도를 나타냄
- Seaborn의 ‘barplot()’ 함수 : 막대그래프 생성
02. How many ratings were submitted per month?
- 월별 평점 수를 계산하고 시각화하는 과정을 담음
# <!-- collapse=True -->
month_counts = ratings_df[['year', 'month', 'rating']].groupby(['year', 'month']).count()
month_counts = month_counts.rename(index=str, columns={'rating': '# of Ratings'})
month_counts = month_counts.reset_index()
month_counts['Date'] = month_counts[['year', 'month']].apply(
lambda x: datetime(year=int(x[0]), month=int(x[1]), day=1), axis=1
)
month_counts = month_counts.set_index('Date', drop=True)
display(month_counts)
month_counts['# of Ratings'].plot(style='o-')
plt.ylabel('# of Ratings')
plt.title('# of Ratings per Month')
plt.ylim([0, 25000])
plt.gca().grid(which='minor')
plt.show()
- month_counts : year, month, rating 열을 이용하여 데이터 프레임 생성 후, year와 month를 기준으로 그룹화하여 각 월별 평점 수를 계산
- 1997년 11월에 큰 스파이크가 일어난 것을 볼 수 있으나 다른 월들을 상대적으로 constant함
05. How consistent are the average ratings over time?
- 월별로 평균 평점과 표준편차를 계산하고, 이를 시각화하여 시간에 따른 평점의 일관성을 살펴봄
# <!-- collapse=True -->
month_counts = ratings_df.groupby(['year', 'month'])['rating'].agg([np.mean, np.std])
month_counts = month_counts.rename(index=str, columns={'mean': 'Rating'})
month_counts = month_counts.reset_index()
month_counts['Date'] = month_counts[['year', 'month']].apply(
lambda x: datetime(year=int(x[0]), month=int(x[1]), day=1), axis=1
)
month_counts = month_counts.set_index('Date', drop=True)
display(month_counts)
month_counts['Rating'].plot(style='o-')
plt.fill_between(month_counts.index,
month_counts['Rating'] - month_counts['std'],
month_counts['Rating'] + month_counts['std'],
alpha=0.3,
)
plt.ylim([0, 5])
plt.ylabel('Rating')
plt.gca().grid(which='minor')
plt.title('Rating Consistency over Time')
plt.show()
- month_counts : year와 month를 기준으로 그룹화된 데이터에서 rating 열의 평균과 표준 편차를 계산 —> groupby() 메서드 사용, agg() 함수로 각 그룹에 대해 평균과 표준편차 계산
- fill_between() : 그래프의 아래쪽을 표준편차로 채움 —> 평균평점 주변의 변동성을 시각적으로 표시
- 파란색 부분 : 표준편차를 의미
- 평균평점 : 균일하게 3.5
06. How quickly do the movie and user bases grow over time?
- 평점 데이터를 기반으로 월별로 사용자 수와 영화 수의 변화를 추적하고 시각화함
- 월별로 평가 시스템에 접속한 사람들이 어떠한 경향을 가지고 있는지를 파악하고자 함
# <!-- collapse=True -->
ratings_df['Date'] = ratings_df[['year', 'month']].apply(
lambda x: datetime(year=int(x[0]), month=int(x[1]), day=1), axis=1
) #year, month 열을 기반으로 각 행의 날짜를 생성 → 이를 ‘Date’ 열에 저장
display(ratings_df.head())
n_users = []
n_movies = []
dates = np.unique(ratings_df['Date']) #특정 날짜를 뽑음
for date in dates:
n_users.append(ratings_df[ratings_df['Date'] <= date]['userId'].nunique())
n_movies.append(ratings_df[ratings_df['Date'] <= date]['movieId'].nunique())
df_users = pd.DataFrame({'Date': dates, '# of Users': n_users}).set_index('Date')
df_movies = pd.DataFrame({'Date': dates, '# of Movies': n_movies}).set_index('Date')
fig, ax = plt.subplots()
df_movies['# of Movies'].plot(style='o-', ax=ax)
df_users['# of Users'].plot(style='o-', ax=ax)
plt.ylabel('Count')
plt.ylim([0, 2000])
ax.grid(which='minor')
plt.tight_layout()
plt.legend()
plt.show()
- year, month 열을 기반으로 각 행의 날짜를 생성 → 이를 ‘Date’ 열에 저장
- np.unique() : Date 열을 기준으로 각 날짜에 대해 고유한 값들을 뽑음 (특정 Dates를 뽑음)
np.unique (디테일하게 알 필요는 X)
- 중복된 값을 제거하고, 중복되지 않은 값을 추출할 때 사용
- 디폴트 : 오름차순 정렬
- 내림차순 정렬 : unique_arr = np.unique(arr, reverse=True)
- reverse 값을 인자로 넣어줘야 내림차순 정렬이 됨
- return_counts 파라미터를 True로 지정하면 각 값의 빈도를 함께 추출
- nunique() : 고유한 사용자 및 영화의 수 계산 → 각 날짜에 대해 Loop를 돌면서, 해당 날짜 이전까지의 사용자 수와 영화수를 구함
- 주황색 선 : 신규 평점을 매긴 user
- 파란색 선 : 신규 평점이 매겨진 영화
- 새로운 users는 꾸준히 선형적인 분포를 띔을 볼 수 있음에 반해, 영화의 수는 1000건 이상부터 시작함을 볼 수 있다.
ratings_df : 총 10만건이 기록되어 있음
07. How sparse is the user/movies matrix we’ll be dealing with?
- Sparsity : CF 알고리즘에서 극복해야하는 일반적인 문제
Sparse = 차원 Nusers X Nmovies 인 행렬 R을 생성할 때,
각 요소 rij : 사용자 i가 영화 j를 평가한 단일 평점을 의미 & 매우 Sparse함
→ 대부분의 사용자는 25,000개가 넘는 영화 중 일부만 평가하기 때문
- 사용자와 영화 행렬이 얼마나 sparse한지 알아보자
# <!-- collapse=True -->
from cf_utils import get_rating_matrix
rating_matrix, user_map, item_map = get_rating_matrix(ratings_df)
with plt.style.context('seaborn-white'):
rating_matrix_binary = rating_matrix > 0
plt.imshow(rating_matrix_binary)
plt.xlabel('Movie')
plt.ylabel('User')
plt.show()
Markdown(
r"The matrix density is $n_{{ratings}}/(n_{{users}} \\times n_{{movies}}) = {:0.3f}$"
.format(np.sum(rating_matrix_binary) / np.prod(rating_matrix.shape))
)
- get_rating_matrix() : 평점 데이터프레임을 기반으로 사용자 - 영화 평점 행렬을 불러옴
- 평점 행렬이 0보다 큰 값을 가져옴
- 평점이 매겨지면 검은색으로 찍힘 / 평점이 안 매겨지면 흰색
- 하얀색 부분이 sparse한 부분
- (np.sum(rating_matrix_binary) : 평점이 매겨진 element의 수 (10만건)
- (np.prod(rating_matrix.shape) : 행렬의 크기로 나눔
- Matrix density : 0.063 —> 대략 0.6이 아닌 정도만 평점이 매겨져 있음
def get_rating_matrix() : 반환값은 2차원 행렬
<aside> 💡 def get_rating_matrix(X): """Function to generate a ratings matrx and mappings for the user and item ids to the row and column indices
Parameters
----------
X : pandas.DataFrame, shape=(n_ratings,>=3)
First 3 columns must be in order of user, item, rating.
Returns
-------
rating_matrix : 2d numpy array, shape=(n_users, n_items)
user_map : pandas Series, shape=(n_users,)
Mapping from the original user id to an integer in the range [0,n_users)
item_map : pandas Series, shape=(n_items,)
Mapping from the original item id to an integer in the range [0,n_items)
"""
user_col, item_col, rating_col = X.columns[:3]
rating = X[rating_col]
user_map = pd.Series(
index=np.unique(X[user_col]),
data=np.arange(X[user_col].nunique()),
name='user_map',
)
item_map = pd.Series(
index=np.unique(X[item_col]),
data=np.arange(X[item_col].nunique()),
name='columns_map',
)
user_inds = X[user_col].map(user_map)
item_inds = X[item_col].map(item_map)
rating_matrix = (
pd.pivot_table( #행은 user, 열은 item로 table을 만들어서 해당 valuse값을 table 안에 넣어줌
data=X,
values=rating_col,
index=user_inds,
columns=item_inds,
)
.fillna(0) #10만건의 값을 채우는데 sparse한 공간은 0으로 채워줌
.values
)
return rating_matrix, user_map, item_map
</aside>
ratings_df['userId'].value_counts(ascending=True)
- 사용자당 평점을 매긴 횟수를 counts함
- 최소 20건 매긴 사용자들에 대해서만 count하고 해당 사용자들을 오름차순으로 정렬 & 평점을 매긴 횟수 counts해서 반환
# <!-- collapse=True -->
user_counts = ratings_df['userId'].value_counts(ascending=True)
user_counts.index = np.arange(len(user_counts)) / len(user_counts) #user_counts를 기반으로 index값을 (0,0)부터 쭉 출력함
plt.plot(user_counts, user_counts.index, '.', label='Users')
movie_counts = ratings_df['movieId'].value_counts(ascending=True)
movie_counts.index = np.arange(len(movie_counts)) / len(movie_counts)
plt.plot(movie_counts, movie_counts.index, '.', label='Movies')
plt.xlabel('Number of Ratings')
plt.ylabel('ECDF')
plt.legend()
plt.show()
- 사용자당 평점을 매긴 횟수를 counts해서 누적확률분포로 나타냄
- 737건을 매긴 user까지 합치면 1이 됨
—> 그래프가 앞쪽에 평점을 20건 매긴 사용자가 많음을 의미함
- 누적확률분포 계산하는 방법 : Empirical Cumulative Distribution Function (ECDF) plot 사용
02. Baseline.jpynb
2.1 Simple Average Model
- 가장 simple한 모델 : 평균 내기
# <!-- collapse=True -->
class SimpleAverageModel():
"""A very simple model that just uses the average of the ratings in the
training set as the prediction for the test set.
Attributes
----------
mean : float
Average of the training set ratings
"""
def __init__(self):
pass
def fit(self, X):
"""Given a ratings dataframe X, compute the mean rating
Parameters
----------
X : pandas dataframe, shape = (n_ratings, >=3)
User, item, rating dataframe. Only the 3rd column is used.
Returns
-------
self
"""
self.mean = X.iloc[:, 2].mean()
return self
def predict(self, X):
return np.ones(len(X)) * self.mean
- Class SimpleAverageModel() : mean 속성을 가지고 있음 → 학습 세트의 평균 평점을 저장함
- 학습 def fit(self, X) : 학습 수행 함수 / ratings 행렬을 평균 #X 학습 데이터 ex) 8만건
- def predict(self, X) : 주어진 데이터 X에 대한 예측값을 반환 #X를 입력으로 받음 Training Test의 X로 생각 ex) 2만건
학습 데이터에서 만든 self.mean 값을 예측 행렬에 넣어줌
- user 번호와 item 번호로만 예측함
2.2 Average by ID Model
- user 마다 평균 평점을 매기기 / item 마다 평균 평점 매기기
- Parameter를 하나 더 추가하기 —> 어떤 아이디를 기준으로 할지 선택하기 위해서
- Main Idea : user id 마다 평균 내자!!
# <!-- collapse=True -->
class AverageByIdModel():
"""Simple model that predicts based on average ratings for a given Id
(movieId or userId) from training data
Parameters
----------
id_column : string
Name of id column (i.e. 'itemId', 'userId') to average by in
dataframe that will be fitted to
Attributes
----------
averages_by_id : pandas Series, shape = [n_ids]
Pandas series of rating averages by id
overall_average : float
Average rating over all training samples
"""
def __init__(self, id_column):
self.id_column = id_column
def fit(self, X):
"""Fit training data.
Parameters
----------
X : pandas dataframe, shape = (n_ratings, >=3)
User, item, rating dataframe. Columns beyond 3 are ignored
Returns
-------
self : object
"""
rating_column = X.columns[2]
X = X[[self.id_column, rating_column]].copy() // 새로운 X 행렬을 user id, 평점 값으로 생성
X.columns = ['id', 'rating']
self.averages_by_id = (
X
.groupby("id")['rating'] //id와 rating 별로 grouping / prepare to calculate mean rating for each id.
.mean()
.rename('average_rating')
)
self.overall_average = X['rating'].mean() #전체 평균도 계산 why? test data 안에 해당 user 값이 존재하지 않을 때 overall_average 값을 넣어주기 위해서!!
return self
def predict(self, X): #Test Data 이용
"""Return rating predictions
Parameters
----------
X : pandas dataframe, shape = (n_ratings, >=3)
Array of n_ratings movieIds or userIds
Returns
-------
y_pred : numpy array, shape = (n_ratings,)
Array of n_samples rating predictions
"""
rating_column = X.columns[2]
X = X[[self.id_column, rating_column]].copy()
X.columns = ['id', 'rating']
X = X.__TODO__ # joining the fitted value with the key 'id' --> id별 평균 평점을 join시켜 매칭시킴 / 존재하지 않는 값은 nan으로 채워져 있을거임
X['average_rating'].fillna(__TODO__) # fill the missing values by overall_average --> nan 값은 overall_average 값으로 채워주기
return X['average_rating'].values
self.overall_average = X['rating'].mean() #전체 평균도 계산
- why? test data 안에 해당 user 값이 존재하지 않을 때 overall_average 값을 넣어주기 위해서!!
- 신규 user나 신규 item에 대해서는 overall_average로 행렬 값을 채울 수 있음
2.3 Damped Baseline with User + Movie Data (디테일하게 살펴보진 X)
𝑏𝑢,𝑖=𝜇+𝑏𝑢+𝑏𝑖
→ overall_average + bu + bi
𝑏𝑢=1|𝐼𝑢|+𝛽𝑢∑𝑖∈𝐼𝑢(𝑟𝑢,𝑖−𝜇)
→ user 입장에서, user가 매긴 item들이 평균평점과 얼마나 차이가 나는지
𝑏𝑖=1|𝑈𝑖|+𝛽𝑖∑𝑢∈𝑈𝑖(𝑟𝑢,𝑖−𝑏𝑢−𝜇).
→ bu로 설명이 안되는 부분에서 user item 평균 평점에서 bu와 u를 빼줌 (??)
- 베타 u와 베타 i가 매우 큰 숫자라면 bu와 bi가 0이 되어버리기 때문에 예측 자체를 overall_average로 하는 것과 동일함
- damping term을 없애버리면 overall_average + user가 평균평점 대비 자신이 매긴 평점이 얼마나 차이 나는지를 평균을 낸 값
- bi : bu에서 부족한 부분을 추가적으로
#
class DampedUserMovieBaselineModel():
"""Baseline model that of the form mu + b_u + b_i,
where mu is the overall average, b_u is a damped user
average rating residual, and b_i is a damped item (movie)
average rating residual. See eqn 2.1 of
<http://files.grouplens.org/papers/FnT%20CF%20Recsys%20Survey.pdf>
Parameters
----------
damping_factor : float, default=0
Factor to bring residuals closer to 0. Must be positive.
Attributes
----------
mu : float
Average rating over all training samples
b_u : pandas Series, shape = [n_users]
User residuals
b_i : pandas Series, shape = [n_movies]
Movie residuals
damping_factor : float, default=0
Factor to bring residuals closer to 0. Must be >= 0.
"""
def __init__(self, damping_factor=0):
self.damping_factor = damping_factor
def fit(self, X):
"""Fit training data.
Parameters
----------
X : DataFrame, shape = [n_samples, >=3]
User, movie, rating dataFrame. Columns beyond 3 are ignored
Returns
-------
self : object
"""
X = X.iloc[:, :3].copy()
X.columns = ['user', 'item', 'rating']
self.mu = np.mean(X['rating']) #overall_average를 구함
user_counts = X['user'].value_counts()
movie_counts = X['item'].value_counts()
b_u = ( #user입장에서의 계산
X[['user', 'rating']]
.groupby('user')['rating']
.sum()
.subtract(user_counts * self.mu)
.divide(user_counts + self.damping_factor)
.rename('b_u')
)
X = X.join(b_u, on='user') #X 행렬 + 새로 생성한 b_u 행렬
X['item_residual'] = X['rating'] - X['b_u'] - self.mu
b_i = ( #item 입장에서의 계싼
X[['item', 'item_residual']]
.groupby('item')['item_residual'] #item에 대해서 groupby 진행
.sum()
.divide(movie_counts + self.damping_factor)
.rename('b_i')
)
self.b_u = b_u
self.b_i = b_i
return self
def predict(self, X):
"""Return rating predictions
Parameters
----------
X : DataFrame, shape = (n_ratings, 2)
User, item dataframe
Returns
-------
y_pred : numpy array, shape = (n_ratings,)
Array of n_samples rating predictions
"""
X = X.iloc[:, :2].copy()
X.columns = ['user', 'item']
X = X.join(self.b_u, on='user').fillna(0) #b_u와 X join -> user 입장에서 sparse한 값은 0으로 채워넣기
X = X.join(self.b_i, on='item').fillna(0) #b_i와 X join -> item 입장에서 sparse한 값은 0으로 채움
return (self.mu + X['b_u'] + X['b_i']).values # 나머지 존재하는 값에 대해서는 u + b_u + b_i로 return
- b_u = ( X[['user', 'rating']] .groupby('user')['rating'] .sum() // user마다 rating에 대한 평균을 계산함 .subtract(user_counts * self.mu) // 평균 * usercount를 빼줌 .divide(user_counts + self.damping_factor) .rename('b_u') )
- ∑𝑖∈𝐼𝑢(𝑟𝑢,𝑖−𝜇) :
X[['user', 'rating']].groupby('user')['rating'].sum() // user마다 rating에 대한 평균을 계산함
.subtract(user_counts * self.mu) //ex) 1번 user가 20건을 평점을 매긴 수 * 평균평점
- 𝑟𝑢,𝑖−𝑏𝑢−𝜇 : item 입장에서 b_u 와 평균평점 u를 빼줌
- b_i = ( X[['item', 'item_residual']] .groupby('item')['item_residual'] #item에 대해서 groupby 진행 .sum() .divide(movie_counts + self.damping_factor) .rename('b_i') )
2.4 Cross-validation framework 교차 검증 기법 (기계학습 측면에서 생각)
def get_xval_errs_and_res(df, model, n_splits=5, random_state=0, rating_col='rating'):
kf = KFold(n_splits=n_splits, random_state=random_state, shuffle=True)
errs, stds = [], []
residuals = np.zeros(len(df))
for train_inds, test_inds in kf.split(df):
train_df, test_df = df.iloc[train_inds], df.iloc[test_inds]
pred = model.fit(train_df).predict(test_df)
residuals[test_inds] = pred - test_df[rating_col] #예측 - 정답
mae = mean_absolute_error(pred, test_df[rating_col])
errs.append(mae) #scalar 값 하나로 나옴
return errs, residuals
- errs에는 다섯개가 존재 : split을 다섯번 돌렸기 때문
# <!-- collapse=True -->
errs_1, res_1 = get_xval_errs_and_res(ratings_df, SimpleAverageModel())
errs_2, res_2 = get_xval_errs_and_res(ratings_df, AverageByIdModel('movieId'))
errs_3, res_3 = get_xval_errs_and_res(ratings_df, AverageByIdModel('userId'))
errs_4, res_4 = get_xval_errs_and_res(ratings_df, DampedUserMovieBaselineModel(0)) #damping term값 넣어줌
errs_5, res_5 = get_xval_errs_and_res(ratings_df, DampedUserMovieBaselineModel(10))
errs_6, res_6 = get_xval_errs_and_res(ratings_df, DampedUserMovieBaselineModel(25))
errs_7, res_7 = get_xval_errs_and_res(ratings_df, DampedUserMovieBaselineModel(50))
df_errs = pd.DataFrame(
OrderedDict(
(
('Average', errs_1),
('Item Average', errs_2),
('User Average', errs_3),
('Combined 0', errs_4),
('Combined 10', errs_5),
('Combined 25', errs_6),
('Combined 50', errs_7),
)
)
)
display(df_errs)
df_errs = (
pd.melt(df_errs, value_vars=df_errs.columns) #pd.melt : pd.DataFrame의 구조를 바꿔줌
.rename({'variable': 'Baseline Model', 'value': 'MAE'}, axis=1) #x축 : BaselineModel y축 : MAE
)
df_res = pd.DataFrame(
OrderedDict(
(
('Average', res_1),
('Item Average', res_2),
('User Average', res_3),
('Combined 0', res_4),
('Combined 10', res_5),
('Combined 25', res_6),
('Combined 50', res_7),
)
)
)
display(df_res.tail())
df_res = (
pd.melt(df_res, value_vars=df_res.columns)
.rename({'variable': 'Baseline Model', 'value': 'Residual'}, axis=1)
)
- 한번 돌릴 때마다 errs와 res가 나옴
- df_errs = pd.DataFrame( OrderedDict( ( ('Average', errs_1), ('Item Average', errs_2), ('User Average', errs_3), ('Combined 0', errs_4), ('Combined 10', errs_5), ('Combined 25', errs_6), ('Combined 50', errs_7), ) ) ) display(df_errs) df_errs = ( pd.melt(df_errs, value_vars=df_errs.columns) .rename({'variable': 'Baseline Model', 'value': 'MAE'}, axis=1) ) —> simple average에 대해서 dataframe이 나옴
# <!-- collapse=True -->
fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(12,8))
sns.swarmplot(data=df_errs, x='Baseline Model', y='MAE', ax=ax0)
sns.violinplot(data=df_res, x='Baseline Model', y='Residual', ax=ax1)
ax0.xaxis.set_visible(False)
plt.tight_layout()
plt.show()
- sns.swarmplot(data=df_errs, x='Baseline Model', y='MAE', ax=ax0)
→ MAE plot : Damping 인자가 0 또는 10인 결합 모델이 가장 우수한 성능을 보임
- user와 item의 평균에서의 평균 이탈을 고려하는 것이 가장 우수한 결과를 보임
why? 각 baseline prediciton에 대해 고려되는 데이터가 더 많기 때문!!
- item 평균이 user 평균보다 더 나은 성능을 보이는 이유
why? 이 데이터셋에는 사용자보다 item이 더 많기 때문에, 아이템을 평균화하면 사용자를 평균화하는 것보다 각 기준선 예측 당 더 많은 데이터가 고려됨
- sns.violinplot(data=df_res, x='Baseline Model', y='Residual', ax=ax1)
→ Damped Model을 보면 0을 기준으로 많이 분포함
CF 모델로 넘어가기 전에, baseline으로 사용할 모델을 선택해야 함
0, 10 모델의 성능은 동일하지만, 결합 10을 선택할 것 !!
why? 더 높은 감쇠 계수가 사실상 더 강한 정규화를 의미하기 때문에 감쇠 계수가 0인 모델보다 과적합을 더 잘 방지하기 때문!!
01. How are the ratings distributed?
- 데이터 프레임에서 가져온 평점 데이터를 상대적인 빈도로 시각화하여, 각 평점의 분포를 한눈에 파악
norm_counts = (
ratings_df['rating']
.value_counts(normalize=True, sort=False)
.multiply(100) #상대적인 비율을 퍼센트로 변환
.reset_index() #데이터 프레임을 재설정
)
ax = sns.barplot(x='rating', y='proportion', data=norm_counts) #막대그래프 생성
ax.set_title('Rating Frequencies')
plt.show()
- norm_counts : ‘ratings_df’ 데이터프레임에서 rating 열의 값을 가져와서, value_counts() 메서드를 사용하여 각 평점 값의 빈도를 계산
- normalize=True : 각 평점 값의 상대적인 비율을 계산
- ‘rating’ 열 : 평점 값
- proprotion 열 : 해당 평점의 상대적인 빈도를 나타냄
- Seaborn의 ‘barplot()’ 함수 : 막대그래프 생성
02. How many ratings were submitted per month?
- 월별 평점 수를 계산하고 시각화하는 과정을 담음
# <!-- collapse=True -->
month_counts = ratings_df[['year', 'month', 'rating']].groupby(['year', 'month']).count()
month_counts = month_counts.rename(index=str, columns={'rating': '# of Ratings'})
month_counts = month_counts.reset_index()
month_counts['Date'] = month_counts[['year', 'month']].apply(
lambda x: datetime(year=int(x[0]), month=int(x[1]), day=1), axis=1
)
month_counts = month_counts.set_index('Date', drop=True)
display(month_counts)
month_counts['# of Ratings'].plot(style='o-')
plt.ylabel('# of Ratings')
plt.title('# of Ratings per Month')
plt.ylim([0, 25000])
plt.gca().grid(which='minor')
plt.show()
- month_counts : year, month, rating 열을 이용하여 데이터 프레임 생성 후, year와 month를 기준으로 그룹화하여 각 월별 평점 수를 계산
- 1997년 11월에 큰 스파이크가 일어난 것을 볼 수 있으나 다른 월들을 상대적으로 constant함
05. How consistent are the average ratings over time?
- 월별로 평균 평점과 표준편차를 계산하고, 이를 시각화하여 시간에 따른 평점의 일관성을 살펴봄
# <!-- collapse=True -->
month_counts = ratings_df.groupby(['year', 'month'])['rating'].agg([np.mean, np.std])
month_counts = month_counts.rename(index=str, columns={'mean': 'Rating'})
month_counts = month_counts.reset_index()
month_counts['Date'] = month_counts[['year', 'month']].apply(
lambda x: datetime(year=int(x[0]), month=int(x[1]), day=1), axis=1
)
month_counts = month_counts.set_index('Date', drop=True)
display(month_counts)
month_counts['Rating'].plot(style='o-')
plt.fill_between(month_counts.index,
month_counts['Rating'] - month_counts['std'],
month_counts['Rating'] + month_counts['std'],
alpha=0.3,
)
plt.ylim([0, 5])
plt.ylabel('Rating')
plt.gca().grid(which='minor')
plt.title('Rating Consistency over Time')
plt.show()
- month_counts : year와 month를 기준으로 그룹화된 데이터에서 rating 열의 평균과 표준 편차를 계산 —> groupby() 메서드 사용, agg() 함수로 각 그룹에 대해 평균과 표준편차 계산
- fill_between() : 그래프의 아래쪽을 표준편차로 채움 —> 평균평점 주변의 변동성을 시각적으로 표시
- 파란색 부분 : 표준편차를 의미
- 평균평점 : 균일하게 3.5
06. How quickly do the movie and user bases grow over time?
- 평점 데이터를 기반으로 월별로 사용자 수와 영화 수의 변화를 추적하고 시각화함
- 월별로 평가 시스템에 접속한 사람들이 어떠한 경향을 가지고 있는지를 파악하고자 함
# <!-- collapse=True -->
ratings_df['Date'] = ratings_df[['year', 'month']].apply(
lambda x: datetime(year=int(x[0]), month=int(x[1]), day=1), axis=1
) #year, month 열을 기반으로 각 행의 날짜를 생성 → 이를 ‘Date’ 열에 저장
display(ratings_df.head())
n_users = []
n_movies = []
dates = np.unique(ratings_df['Date']) #특정 날짜를 뽑음
for date in dates:
n_users.append(ratings_df[ratings_df['Date'] <= date]['userId'].nunique())
n_movies.append(ratings_df[ratings_df['Date'] <= date]['movieId'].nunique())
df_users = pd.DataFrame({'Date': dates, '# of Users': n_users}).set_index('Date')
df_movies = pd.DataFrame({'Date': dates, '# of Movies': n_movies}).set_index('Date')
fig, ax = plt.subplots()
df_movies['# of Movies'].plot(style='o-', ax=ax)
df_users['# of Users'].plot(style='o-', ax=ax)
plt.ylabel('Count')
plt.ylim([0, 2000])
ax.grid(which='minor')
plt.tight_layout()
plt.legend()
plt.show()
- year, month 열을 기반으로 각 행의 날짜를 생성 → 이를 ‘Date’ 열에 저장
- np.unique() : Date 열을 기준으로 각 날짜에 대해 고유한 값들을 뽑음 (특정 Dates를 뽑음)
np.unique (디테일하게 알 필요는 X)
- 중복된 값을 제거하고, 중복되지 않은 값을 추출할 때 사용
- 디폴트 : 오름차순 정렬
- 내림차순 정렬 : unique_arr = np.unique(arr, reverse=True)
- reverse 값을 인자로 넣어줘야 내림차순 정렬이 됨
- return_counts 파라미터를 True로 지정하면 각 값의 빈도를 함께 추출
- nunique() : 고유한 사용자 및 영화의 수 계산 → 각 날짜에 대해 Loop를 돌면서, 해당 날짜 이전까지의 사용자 수와 영화수를 구함
- 주황색 선 : 신규 평점을 매긴 user
- 파란색 선 : 신규 평점이 매겨진 영화
- 새로운 users는 꾸준히 선형적인 분포를 띔을 볼 수 있음에 반해, 영화의 수는 1000건 이상부터 시작함을 볼 수 있다.
ratings_df : 총 10만건이 기록되어 있음
07. How sparse is the user/movies matrix we’ll be dealing with?
- Sparsity : CF 알고리즘에서 극복해야하는 일반적인 문제
Sparse = 차원 Nusers X Nmovies 인 행렬 R을 생성할 때,
각 요소 rij : 사용자 i가 영화 j를 평가한 단일 평점을 의미 & 매우 Sparse함
→ 대부분의 사용자는 25,000개가 넘는 영화 중 일부만 평가하기 때문
- 사용자와 영화 행렬이 얼마나 sparse한지 알아보자
# <!-- collapse=True -->
from cf_utils import get_rating_matrix
rating_matrix, user_map, item_map = get_rating_matrix(ratings_df)
with plt.style.context('seaborn-white'):
rating_matrix_binary = rating_matrix > 0
plt.imshow(rating_matrix_binary)
plt.xlabel('Movie')
plt.ylabel('User')
plt.show()
Markdown(
r"The matrix density is $n_{{ratings}}/(n_{{users}} \\times n_{{movies}}) = {:0.3f}$"
.format(np.sum(rating_matrix_binary) / np.prod(rating_matrix.shape))
)
- get_rating_matrix() : 평점 데이터프레임을 기반으로 사용자 - 영화 평점 행렬을 불러옴
- 평점 행렬이 0보다 큰 값을 가져옴
- 평점이 매겨지면 검은색으로 찍힘 / 평점이 안 매겨지면 흰색
- 하얀색 부분이 sparse한 부분
- (np.sum(rating_matrix_binary) : 평점이 매겨진 element의 수 (10만건)
- (np.prod(rating_matrix.shape) : 행렬의 크기로 나눔
- Matrix density : 0.063 —> 대략 0.6이 아닌 정도만 평점이 매겨져 있음
def get_rating_matrix() : 반환값은 2차원 행렬
<aside> 💡 def get_rating_matrix(X): """Function to generate a ratings matrx and mappings for the user and item ids to the row and column indices
Parameters
----------
X : pandas.DataFrame, shape=(n_ratings,>=3)
First 3 columns must be in order of user, item, rating.
Returns
-------
rating_matrix : 2d numpy array, shape=(n_users, n_items)
user_map : pandas Series, shape=(n_users,)
Mapping from the original user id to an integer in the range [0,n_users)
item_map : pandas Series, shape=(n_items,)
Mapping from the original item id to an integer in the range [0,n_items)
"""
user_col, item_col, rating_col = X.columns[:3]
rating = X[rating_col]
user_map = pd.Series(
index=np.unique(X[user_col]),
data=np.arange(X[user_col].nunique()),
name='user_map',
)
item_map = pd.Series(
index=np.unique(X[item_col]),
data=np.arange(X[item_col].nunique()),
name='columns_map',
)
user_inds = X[user_col].map(user_map)
item_inds = X[item_col].map(item_map)
rating_matrix = (
pd.pivot_table( #행은 user, 열은 item로 table을 만들어서 해당 valuse값을 table 안에 넣어줌
data=X,
values=rating_col,
index=user_inds,
columns=item_inds,
)
.fillna(0) #10만건의 값을 채우는데 sparse한 공간은 0으로 채워줌
.values
)
return rating_matrix, user_map, item_map
</aside>
ratings_df['userId'].value_counts(ascending=True)
- 사용자당 평점을 매긴 횟수를 counts함
- 최소 20건 매긴 사용자들에 대해서만 count하고 해당 사용자들을 오름차순으로 정렬 & 평점을 매긴 횟수 counts해서 반환
# <!-- collapse=True -->
user_counts = ratings_df['userId'].value_counts(ascending=True)
user_counts.index = np.arange(len(user_counts)) / len(user_counts) #user_counts를 기반으로 index값을 (0,0)부터 쭉 출력함
plt.plot(user_counts, user_counts.index, '.', label='Users')
movie_counts = ratings_df['movieId'].value_counts(ascending=True)
movie_counts.index = np.arange(len(movie_counts)) / len(movie_counts)
plt.plot(movie_counts, movie_counts.index, '.', label='Movies')
plt.xlabel('Number of Ratings')
plt.ylabel('ECDF')
plt.legend()
plt.show()
- 사용자당 평점을 매긴 횟수를 counts해서 누적확률분포로 나타냄
- 737건을 매긴 user까지 합치면 1이 됨
—> 그래프가 앞쪽에 평점을 20건 매긴 사용자가 많음을 의미함
- 누적확률분포 계산하는 방법 : Empirical Cumulative Distribution Function (ECDF) plot 사용
02. Baseline.jpynb
2.1 Simple Average Model
- 가장 simple한 모델 : 평균 내기
# <!-- collapse=True -->
class SimpleAverageModel():
"""A very simple model that just uses the average of the ratings in the
training set as the prediction for the test set.
Attributes
----------
mean : float
Average of the training set ratings
"""
def __init__(self):
pass
def fit(self, X):
"""Given a ratings dataframe X, compute the mean rating
Parameters
----------
X : pandas dataframe, shape = (n_ratings, >=3)
User, item, rating dataframe. Only the 3rd column is used.
Returns
-------
self
"""
self.mean = X.iloc[:, 2].mean()
return self
def predict(self, X):
return np.ones(len(X)) * self.mean
- Class SimpleAverageModel() : mean 속성을 가지고 있음 → 학습 세트의 평균 평점을 저장함
- 학습 def fit(self, X) : 학습 수행 함수 / ratings 행렬을 평균 #X 학습 데이터 ex) 8만건
- def predict(self, X) : 주어진 데이터 X에 대한 예측값을 반환 #X를 입력으로 받음 Training Test의 X로 생각 ex) 2만건
학습 데이터에서 만든 self.mean 값을 예측 행렬에 넣어줌
- user 번호와 item 번호로만 예측함
2.2 Average by ID Model
- user 마다 평균 평점을 매기기 / item 마다 평균 평점 매기기
- Parameter를 하나 더 추가하기 —> 어떤 아이디를 기준으로 할지 선택하기 위해서
- Main Idea : user id 마다 평균 내자!!
# <!-- collapse=True -->
class AverageByIdModel():
"""Simple model that predicts based on average ratings for a given Id
(movieId or userId) from training data
Parameters
----------
id_column : string
Name of id column (i.e. 'itemId', 'userId') to average by in
dataframe that will be fitted to
Attributes
----------
averages_by_id : pandas Series, shape = [n_ids]
Pandas series of rating averages by id
overall_average : float
Average rating over all training samples
"""
def __init__(self, id_column):
self.id_column = id_column
def fit(self, X):
"""Fit training data.
Parameters
----------
X : pandas dataframe, shape = (n_ratings, >=3)
User, item, rating dataframe. Columns beyond 3 are ignored
Returns
-------
self : object
"""
rating_column = X.columns[2]
X = X[[self.id_column, rating_column]].copy() // 새로운 X 행렬을 user id, 평점 값으로 생성
X.columns = ['id', 'rating']
self.averages_by_id = (
X
.groupby("id")['rating'] //id와 rating 별로 grouping / prepare to calculate mean rating for each id.
.mean()
.rename('average_rating')
)
self.overall_average = X['rating'].mean() #전체 평균도 계산 why? test data 안에 해당 user 값이 존재하지 않을 때 overall_average 값을 넣어주기 위해서!!
return self
def predict(self, X): #Test Data 이용
"""Return rating predictions
Parameters
----------
X : pandas dataframe, shape = (n_ratings, >=3)
Array of n_ratings movieIds or userIds
Returns
-------
y_pred : numpy array, shape = (n_ratings,)
Array of n_samples rating predictions
"""
rating_column = X.columns[2]
X = X[[self.id_column, rating_column]].copy()
X.columns = ['id', 'rating']
X = X.__TODO__ # joining the fitted value with the key 'id' --> id별 평균 평점을 join시켜 매칭시킴 / 존재하지 않는 값은 nan으로 채워져 있을거임
X['average_rating'].fillna(__TODO__) # fill the missing values by overall_average --> nan 값은 overall_average 값으로 채워주기
return X['average_rating'].values
self.overall_average = X['rating'].mean() #전체 평균도 계산
- why? test data 안에 해당 user 값이 존재하지 않을 때 overall_average 값을 넣어주기 위해서!!
- 신규 user나 신규 item에 대해서는 overall_average로 행렬 값을 채울 수 있음
2.3 Damped Baseline with User + Movie Data (디테일하게 살펴보진 X)
𝑏𝑢,𝑖=𝜇+𝑏𝑢+𝑏𝑖
→ overall_average + bu + bi
𝑏𝑢=1|𝐼𝑢|+𝛽𝑢∑𝑖∈𝐼𝑢(𝑟𝑢,𝑖−𝜇)
→ user 입장에서, user가 매긴 item들이 평균평점과 얼마나 차이가 나는지
𝑏𝑖=1|𝑈𝑖|+𝛽𝑖∑𝑢∈𝑈𝑖(𝑟𝑢,𝑖−𝑏𝑢−𝜇).
→ bu로 설명이 안되는 부분에서 user item 평균 평점에서 bu와 u를 빼줌 (??)
- 베타 u와 베타 i가 매우 큰 숫자라면 bu와 bi가 0이 되어버리기 때문에 예측 자체를 overall_average로 하는 것과 동일함
- damping term을 없애버리면 overall_average + user가 평균평점 대비 자신이 매긴 평점이 얼마나 차이 나는지를 평균을 낸 값
- bi : bu에서 부족한 부분을 추가적으로
#
class DampedUserMovieBaselineModel():
"""Baseline model that of the form mu + b_u + b_i,
where mu is the overall average, b_u is a damped user
average rating residual, and b_i is a damped item (movie)
average rating residual. See eqn 2.1 of
<http://files.grouplens.org/papers/FnT%20CF%20Recsys%20Survey.pdf>
Parameters
----------
damping_factor : float, default=0
Factor to bring residuals closer to 0. Must be positive.
Attributes
----------
mu : float
Average rating over all training samples
b_u : pandas Series, shape = [n_users]
User residuals
b_i : pandas Series, shape = [n_movies]
Movie residuals
damping_factor : float, default=0
Factor to bring residuals closer to 0. Must be >= 0.
"""
def __init__(self, damping_factor=0):
self.damping_factor = damping_factor
def fit(self, X):
"""Fit training data.
Parameters
----------
X : DataFrame, shape = [n_samples, >=3]
User, movie, rating dataFrame. Columns beyond 3 are ignored
Returns
-------
self : object
"""
X = X.iloc[:, :3].copy()
X.columns = ['user', 'item', 'rating']
self.mu = np.mean(X['rating']) #overall_average를 구함
user_counts = X['user'].value_counts()
movie_counts = X['item'].value_counts()
b_u = ( #user입장에서의 계산
X[['user', 'rating']]
.groupby('user')['rating']
.sum()
.subtract(user_counts * self.mu)
.divide(user_counts + self.damping_factor)
.rename('b_u')
)
X = X.join(b_u, on='user') #X 행렬 + 새로 생성한 b_u 행렬
X['item_residual'] = X['rating'] - X['b_u'] - self.mu
b_i = ( #item 입장에서의 계싼
X[['item', 'item_residual']]
.groupby('item')['item_residual'] #item에 대해서 groupby 진행
.sum()
.divide(movie_counts + self.damping_factor)
.rename('b_i')
)
self.b_u = b_u
self.b_i = b_i
return self
def predict(self, X):
"""Return rating predictions
Parameters
----------
X : DataFrame, shape = (n_ratings, 2)
User, item dataframe
Returns
-------
y_pred : numpy array, shape = (n_ratings,)
Array of n_samples rating predictions
"""
X = X.iloc[:, :2].copy()
X.columns = ['user', 'item']
X = X.join(self.b_u, on='user').fillna(0) #b_u와 X join -> user 입장에서 sparse한 값은 0으로 채워넣기
X = X.join(self.b_i, on='item').fillna(0) #b_i와 X join -> item 입장에서 sparse한 값은 0으로 채움
return (self.mu + X['b_u'] + X['b_i']).values # 나머지 존재하는 값에 대해서는 u + b_u + b_i로 return
- b_u = ( X[['user', 'rating']] .groupby('user')['rating'] .sum() // user마다 rating에 대한 평균을 계산함 .subtract(user_counts * self.mu) // 평균 * usercount를 빼줌 .divide(user_counts + self.damping_factor) .rename('b_u') )
- ∑𝑖∈𝐼𝑢(𝑟𝑢,𝑖−𝜇) :
X[['user', 'rating']].groupby('user')['rating'].sum() // user마다 rating에 대한 평균을 계산함
.subtract(user_counts * self.mu) //ex) 1번 user가 20건을 평점을 매긴 수 * 평균평점
- 𝑟𝑢,𝑖−𝑏𝑢−𝜇 : item 입장에서 b_u 와 평균평점 u를 빼줌
- b_i = ( X[['item', 'item_residual']] .groupby('item')['item_residual'] #item에 대해서 groupby 진행 .sum() .divide(movie_counts + self.damping_factor) .rename('b_i') )
2.4 Cross-validation framework 교차 검증 기법 (기계학습 측면에서 생각)
def get_xval_errs_and_res(df, model, n_splits=5, random_state=0, rating_col='rating'):
kf = KFold(n_splits=n_splits, random_state=random_state, shuffle=True)
errs, stds = [], []
residuals = np.zeros(len(df))
for train_inds, test_inds in kf.split(df):
train_df, test_df = df.iloc[train_inds], df.iloc[test_inds]
pred = model.fit(train_df).predict(test_df)
residuals[test_inds] = pred - test_df[rating_col] #예측 - 정답
mae = mean_absolute_error(pred, test_df[rating_col])
errs.append(mae) #scalar 값 하나로 나옴
return errs, residuals
- errs에는 다섯개가 존재 : split을 다섯번 돌렸기 때문
# <!-- collapse=True -->
errs_1, res_1 = get_xval_errs_and_res(ratings_df, SimpleAverageModel())
errs_2, res_2 = get_xval_errs_and_res(ratings_df, AverageByIdModel('movieId'))
errs_3, res_3 = get_xval_errs_and_res(ratings_df, AverageByIdModel('userId'))
errs_4, res_4 = get_xval_errs_and_res(ratings_df, DampedUserMovieBaselineModel(0)) #damping term값 넣어줌
errs_5, res_5 = get_xval_errs_and_res(ratings_df, DampedUserMovieBaselineModel(10))
errs_6, res_6 = get_xval_errs_and_res(ratings_df, DampedUserMovieBaselineModel(25))
errs_7, res_7 = get_xval_errs_and_res(ratings_df, DampedUserMovieBaselineModel(50))
df_errs = pd.DataFrame(
OrderedDict(
(
('Average', errs_1),
('Item Average', errs_2),
('User Average', errs_3),
('Combined 0', errs_4),
('Combined 10', errs_5),
('Combined 25', errs_6),
('Combined 50', errs_7),
)
)
)
display(df_errs)
df_errs = (
pd.melt(df_errs, value_vars=df_errs.columns) #pd.melt : pd.DataFrame의 구조를 바꿔줌
.rename({'variable': 'Baseline Model', 'value': 'MAE'}, axis=1) #x축 : BaselineModel y축 : MAE
)
df_res = pd.DataFrame(
OrderedDict(
(
('Average', res_1),
('Item Average', res_2),
('User Average', res_3),
('Combined 0', res_4),
('Combined 10', res_5),
('Combined 25', res_6),
('Combined 50', res_7),
)
)
)
display(df_res.tail())
df_res = (
pd.melt(df_res, value_vars=df_res.columns)
.rename({'variable': 'Baseline Model', 'value': 'Residual'}, axis=1)
)
- 한번 돌릴 때마다 errs와 res가 나옴
- df_errs = pd.DataFrame( OrderedDict( ( ('Average', errs_1), ('Item Average', errs_2), ('User Average', errs_3), ('Combined 0', errs_4), ('Combined 10', errs_5), ('Combined 25', errs_6), ('Combined 50', errs_7), ) ) ) display(df_errs) df_errs = ( pd.melt(df_errs, value_vars=df_errs.columns) .rename({'variable': 'Baseline Model', 'value': 'MAE'}, axis=1) ) —> simple average에 대해서 dataframe이 나옴
# <!-- collapse=True -->
fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(12,8))
sns.swarmplot(data=df_errs, x='Baseline Model', y='MAE', ax=ax0)
sns.violinplot(data=df_res, x='Baseline Model', y='Residual', ax=ax1)
ax0.xaxis.set_visible(False)
plt.tight_layout()
plt.show()
- sns.swarmplot(data=df_errs, x='Baseline Model', y='MAE', ax=ax0)
→ MAE plot : Damping 인자가 0 또는 10인 결합 모델이 가장 우수한 성능을 보임
- user와 item의 평균에서의 평균 이탈을 고려하는 것이 가장 우수한 결과를 보임
why? 각 baseline prediciton에 대해 고려되는 데이터가 더 많기 때문!!
- item 평균이 user 평균보다 더 나은 성능을 보이는 이유
why? 이 데이터셋에는 사용자보다 item이 더 많기 때문에, 아이템을 평균화하면 사용자를 평균화하는 것보다 각 기준선 예측 당 더 많은 데이터가 고려됨
- sns.violinplot(data=df_res, x='Baseline Model', y='Residual', ax=ax1)
→ Damped Model을 보면 0을 기준으로 많이 분포함
CF 모델로 넘어가기 전에, baseline으로 사용할 모델을 선택해야 함
0, 10 모델의 성능은 동일하지만, 결합 10을 선택할 것 !!
why? 더 높은 감쇠 계수가 사실상 더 강한 정규화를 의미하기 때문에 감쇠 계수가 0인 모델보다 과적합을 더 잘 방지하기 때문!!
01. How are the ratings distributed?
- 데이터 프레임에서 가져온 평점 데이터를 상대적인 빈도로 시각화하여, 각 평점의 분포를 한눈에 파악
norm_counts = (
ratings_df['rating']
.value_counts(normalize=True, sort=False)
.multiply(100) #상대적인 비율을 퍼센트로 변환
.reset_index() #데이터 프레임을 재설정
)
ax = sns.barplot(x='rating', y='proportion', data=norm_counts) #막대그래프 생성
ax.set_title('Rating Frequencies')
plt.show()
- norm_counts : ‘ratings_df’ 데이터프레임에서 rating 열의 값을 가져와서, value_counts() 메서드를 사용하여 각 평점 값의 빈도를 계산
- normalize=True : 각 평점 값의 상대적인 비율을 계산
- ‘rating’ 열 : 평점 값
- proprotion 열 : 해당 평점의 상대적인 빈도를 나타냄
- Seaborn의 ‘barplot()’ 함수 : 막대그래프 생성
02. How many ratings were submitted per month?
- 월별 평점 수를 계산하고 시각화하는 과정을 담음
# <!-- collapse=True -->
month_counts = ratings_df[['year', 'month', 'rating']].groupby(['year', 'month']).count()
month_counts = month_counts.rename(index=str, columns={'rating': '# of Ratings'})
month_counts = month_counts.reset_index()
month_counts['Date'] = month_counts[['year', 'month']].apply(
lambda x: datetime(year=int(x[0]), month=int(x[1]), day=1), axis=1
)
month_counts = month_counts.set_index('Date', drop=True)
display(month_counts)
month_counts['# of Ratings'].plot(style='o-')
plt.ylabel('# of Ratings')
plt.title('# of Ratings per Month')
plt.ylim([0, 25000])
plt.gca().grid(which='minor')
plt.show()
- month_counts : year, month, rating 열을 이용하여 데이터 프레임 생성 후, year와 month를 기준으로 그룹화하여 각 월별 평점 수를 계산
- 1997년 11월에 큰 스파이크가 일어난 것을 볼 수 있으나 다른 월들을 상대적으로 constant함
05. How consistent are the average ratings over time?
- 월별로 평균 평점과 표준편차를 계산하고, 이를 시각화하여 시간에 따른 평점의 일관성을 살펴봄
# <!-- collapse=True -->
month_counts = ratings_df.groupby(['year', 'month'])['rating'].agg([np.mean, np.std])
month_counts = month_counts.rename(index=str, columns={'mean': 'Rating'})
month_counts = month_counts.reset_index()
month_counts['Date'] = month_counts[['year', 'month']].apply(
lambda x: datetime(year=int(x[0]), month=int(x[1]), day=1), axis=1
)
month_counts = month_counts.set_index('Date', drop=True)
display(month_counts)
month_counts['Rating'].plot(style='o-')
plt.fill_between(month_counts.index,
month_counts['Rating'] - month_counts['std'],
month_counts['Rating'] + month_counts['std'],
alpha=0.3,
)
plt.ylim([0, 5])
plt.ylabel('Rating')
plt.gca().grid(which='minor')
plt.title('Rating Consistency over Time')
plt.show()
- month_counts : year와 month를 기준으로 그룹화된 데이터에서 rating 열의 평균과 표준 편차를 계산 —> groupby() 메서드 사용, agg() 함수로 각 그룹에 대해 평균과 표준편차 계산
- fill_between() : 그래프의 아래쪽을 표준편차로 채움 —> 평균평점 주변의 변동성을 시각적으로 표시
- 파란색 부분 : 표준편차를 의미
- 평균평점 : 균일하게 3.5
06. How quickly do the movie and user bases grow over time?
- 평점 데이터를 기반으로 월별로 사용자 수와 영화 수의 변화를 추적하고 시각화함
- 월별로 평가 시스템에 접속한 사람들이 어떠한 경향을 가지고 있는지를 파악하고자 함
# <!-- collapse=True -->
ratings_df['Date'] = ratings_df[['year', 'month']].apply(
lambda x: datetime(year=int(x[0]), month=int(x[1]), day=1), axis=1
) #year, month 열을 기반으로 각 행의 날짜를 생성 → 이를 ‘Date’ 열에 저장
display(ratings_df.head())
n_users = []
n_movies = []
dates = np.unique(ratings_df['Date']) #특정 날짜를 뽑음
for date in dates:
n_users.append(ratings_df[ratings_df['Date'] <= date]['userId'].nunique())
n_movies.append(ratings_df[ratings_df['Date'] <= date]['movieId'].nunique())
df_users = pd.DataFrame({'Date': dates, '# of Users': n_users}).set_index('Date')
df_movies = pd.DataFrame({'Date': dates, '# of Movies': n_movies}).set_index('Date')
fig, ax = plt.subplots()
df_movies['# of Movies'].plot(style='o-', ax=ax)
df_users['# of Users'].plot(style='o-', ax=ax)
plt.ylabel('Count')
plt.ylim([0, 2000])
ax.grid(which='minor')
plt.tight_layout()
plt.legend()
plt.show()
- year, month 열을 기반으로 각 행의 날짜를 생성 → 이를 ‘Date’ 열에 저장
- np.unique() : Date 열을 기준으로 각 날짜에 대해 고유한 값들을 뽑음 (특정 Dates를 뽑음)
np.unique (디테일하게 알 필요는 X)
- 중복된 값을 제거하고, 중복되지 않은 값을 추출할 때 사용
- 디폴트 : 오름차순 정렬
- 내림차순 정렬 : unique_arr = np.unique(arr, reverse=True)
- reverse 값을 인자로 넣어줘야 내림차순 정렬이 됨
- return_counts 파라미터를 True로 지정하면 각 값의 빈도를 함께 추출
- nunique() : 고유한 사용자 및 영화의 수 계산 → 각 날짜에 대해 Loop를 돌면서, 해당 날짜 이전까지의 사용자 수와 영화수를 구함
- 주황색 선 : 신규 평점을 매긴 user
- 파란색 선 : 신규 평점이 매겨진 영화
- 새로운 users는 꾸준히 선형적인 분포를 띔을 볼 수 있음에 반해, 영화의 수는 1000건 이상부터 시작함을 볼 수 있다.
ratings_df : 총 10만건이 기록되어 있음
07. How sparse is the user/movies matrix we’ll be dealing with?
- Sparsity : CF 알고리즘에서 극복해야하는 일반적인 문제
Sparse = 차원 Nusers X Nmovies 인 행렬 R을 생성할 때,
각 요소 rij : 사용자 i가 영화 j를 평가한 단일 평점을 의미 & 매우 Sparse함
→ 대부분의 사용자는 25,000개가 넘는 영화 중 일부만 평가하기 때문
- 사용자와 영화 행렬이 얼마나 sparse한지 알아보자
# <!-- collapse=True -->
from cf_utils import get_rating_matrix
rating_matrix, user_map, item_map = get_rating_matrix(ratings_df)
with plt.style.context('seaborn-white'):
rating_matrix_binary = rating_matrix > 0
plt.imshow(rating_matrix_binary)
plt.xlabel('Movie')
plt.ylabel('User')
plt.show()
Markdown(
r"The matrix density is $n_{{ratings}}/(n_{{users}} \\times n_{{movies}}) = {:0.3f}$"
.format(np.sum(rating_matrix_binary) / np.prod(rating_matrix.shape))
)
- get_rating_matrix() : 평점 데이터프레임을 기반으로 사용자 - 영화 평점 행렬을 불러옴
- 평점 행렬이 0보다 큰 값을 가져옴
- 평점이 매겨지면 검은색으로 찍힘 / 평점이 안 매겨지면 흰색
- 하얀색 부분이 sparse한 부분
- (np.sum(rating_matrix_binary) : 평점이 매겨진 element의 수 (10만건)
- (np.prod(rating_matrix.shape) : 행렬의 크기로 나눔
- Matrix density : 0.063 —> 대략 0.6이 아닌 정도만 평점이 매겨져 있음
def get_rating_matrix() : 반환값은 2차원 행렬
<aside> 💡 def get_rating_matrix(X): """Function to generate a ratings matrx and mappings for the user and item ids to the row and column indices
Parameters
----------
X : pandas.DataFrame, shape=(n_ratings,>=3)
First 3 columns must be in order of user, item, rating.
Returns
-------
rating_matrix : 2d numpy array, shape=(n_users, n_items)
user_map : pandas Series, shape=(n_users,)
Mapping from the original user id to an integer in the range [0,n_users)
item_map : pandas Series, shape=(n_items,)
Mapping from the original item id to an integer in the range [0,n_items)
"""
user_col, item_col, rating_col = X.columns[:3]
rating = X[rating_col]
user_map = pd.Series(
index=np.unique(X[user_col]),
data=np.arange(X[user_col].nunique()),
name='user_map',
)
item_map = pd.Series(
index=np.unique(X[item_col]),
data=np.arange(X[item_col].nunique()),
name='columns_map',
)
user_inds = X[user_col].map(user_map)
item_inds = X[item_col].map(item_map)
rating_matrix = (
pd.pivot_table( #행은 user, 열은 item로 table을 만들어서 해당 valuse값을 table 안에 넣어줌
data=X,
values=rating_col,
index=user_inds,
columns=item_inds,
)
.fillna(0) #10만건의 값을 채우는데 sparse한 공간은 0으로 채워줌
.values
)
return rating_matrix, user_map, item_map
</aside>
ratings_df['userId'].value_counts(ascending=True)
- 사용자당 평점을 매긴 횟수를 counts함
- 최소 20건 매긴 사용자들에 대해서만 count하고 해당 사용자들을 오름차순으로 정렬 & 평점을 매긴 횟수 counts해서 반환
<code