추천시스템

Week 09. MovieLens 100K DataSet

yooni825 2024. 5. 7. 21:01

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