Created: 27/08/2024


Recommendation System Movie Lens¶

Nama: Maulana Kavaldo

ID_Dicoding: mkavaldo

Import Library¶

In [ ]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

Setup Base¶

In [ ]:
base='/kaggle/input/movielens-20m-dataset/'

Load Dataset¶

In [ ]:
# Load datasets

df_movie = pd.read_csv(base+'movie.csv')
df_rating = pd.read_csv(base+'rating.csv')
df_tags = pd.read_csv(base+'tag.csv')
In [ ]:
df_movie.head()
Out[ ]:
movieId title genres
0 1 Toy Story (1995) Adventure|Animation|Children|Comedy|Fantasy
1 2 Jumanji (1995) Adventure|Children|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama|Romance
4 5 Father of the Bride Part II (1995) Comedy
In [ ]:
df_movie.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 27278 entries, 0 to 27277
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   movieId  27278 non-null  int64 
 1   title    27278 non-null  object
 2   genres   27278 non-null  object
dtypes: int64(1), object(2)
memory usage: 639.5+ KB
In [ ]:
df_movie.isna().sum()
Out[ ]:
movieId    0
title      0
genres     0
dtype: int64
In [ ]:
df_movie.duplicated().sum()
Out[ ]:
0
In [ ]:
df_rating.head()
Out[ ]:
userId movieId rating timestamp
0 1 2 3.5 2005-04-02 23:53:47
1 1 29 3.5 2005-04-02 23:31:16
2 1 32 3.5 2005-04-02 23:33:39
3 1 47 3.5 2005-04-02 23:32:07
4 1 50 3.5 2005-04-02 23:29:40
In [ ]:
print(df_rating.shape)
(20000263, 4)
In [ ]:
df_rating.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20000263 entries, 0 to 20000262
Data columns (total 4 columns):
 #   Column     Dtype  
---  ------     -----  
 0   userId     int64  
 1   movieId    int64  
 2   rating     float64
 3   timestamp  object 
dtypes: float64(1), int64(2), object(1)
memory usage: 610.4+ MB
In [ ]:
df_rating.duplicated().sum()
Out[ ]:
0
In [ ]:
df_rating.isna().sum()
Out[ ]:
userId       0
movieId      0
rating       0
timestamp    0
dtype: int64
In [ ]:
df_tags.head()
Out[ ]:
userId movieId tag timestamp
0 18 4141 Mark Waters 2009-04-24 18:19:40
1 65 208 dark hero 2013-05-10 01:41:18
2 65 353 dark hero 2013-05-10 01:41:19
3 65 521 noir thriller 2013-05-10 01:39:43
4 65 592 dark hero 2013-05-10 01:41:18
In [ ]:
df_tags.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 465564 entries, 0 to 465563
Data columns (total 4 columns):
 #   Column     Non-Null Count   Dtype 
---  ------     --------------   ----- 
 0   userId     465564 non-null  int64 
 1   movieId    465564 non-null  int64 
 2   tag        465548 non-null  object
 3   timestamp  465564 non-null  object
dtypes: int64(2), object(2)
memory usage: 14.2+ MB
In [ ]:
df_tags.isna().sum()
Out[ ]:
userId        0
movieId       0
tag          16
timestamp     0
dtype: int64
In [ ]:
df_tags.duplicated().sum()
Out[ ]:
0

Exploratory Data Analysis¶

Total film yang diproduksi¶

In [ ]:
jumlah_film = len(df_movie.movieId.unique())
print(f"Jumlah total film: {jumlah_film}")
Jumlah total film: 27278

Tahun minimal dan maksimal produksi film¶

In [ ]:
# Ekstrak tahun dari kolom title
df_movie['year'] = df_movie['title'].str.extract(r'\((\d{4})\)')
df_movie['year'] = df_movie['year'].fillna(0).astype(int)

# Menampilkan nilai min dan max dari tahun
year_min = df_movie[df_movie['year'] > 0]['year'].min()
year_max = df_movie['year'].max()

print("Tahun Min:", year_min)
print("Tahun Max:", year_max)
Tahun Min: 1891
Tahun Max: 2015

Terlihat bahwa tercatat film pada dataset ini mulai dari tahun 1891 hingga 2015.

Jumlah User (Konsumen)¶

In [ ]:
jumlah_user = len(df_rating.userId.unique())
print(f"Jumlah total user: {jumlah_user}")
Jumlah total user: 138493

Distribusi Genres¶

In [ ]:
# Plot distribution of movie genres
genre_counts = df_movie['genres'].str.split('|', expand=True).stack().value_counts()
plt.figure(figsize=(8, 4))
sns.barplot(x=genre_counts.index, y=genre_counts.values)
plt.xticks(rotation=90)
plt.title('Distribution of Movie Genres')
plt.xlabel('Genres')
plt.ylabel('Count')
plt.show()
No description has been provided for this image

Produksi film dengan genres drama merupakan genre film terbanyak dengan sekitar 1300 film dan diikuti comedy di posisi kedua sebanyak 8000 lebih film. Genre Drama dan Comedy memiliki perbedaan yang cukup signifikan dibandingkan dengan genre lainnya.

Distribusi Rating¶

In [ ]:
df_rating['timestamp'] = pd.to_datetime(df_rating['timestamp'])
df_rating.replace([float('inf'), float('-inf')], float('nan'), inplace=True)

# Visualisasi Distribusi Rating
plt.figure(figsize=(8, 4))
sns.histplot(df_rating['rating'], bins=5, kde=False)
plt.title('Distribusi Rating')
plt.xlabel('Rating')
plt.ylabel('Jumlah')
plt.show()
/opt/conda/lib/python3.10/site-packages/seaborn/_oldcore.py:1119: FutureWarning: use_inf_as_na option is deprecated and will be removed in a future version. Convert inf values to NaN before operating instead.
  with pd.option_context('mode.use_inf_as_na', True):
No description has been provided for this image

Kebanyakan pengguna memberikan rating dengan rentang 3 hingga 4 star. Mengindikasikan bahwa secara keseluruhan kualitas film sesuai dengan yang diharapkan oleh pengguna (konsumen).

Tren Rating Berdasarkan Waktu¶

In [ ]:
df_rating['year_month'] = df_rating['timestamp'].dt.to_period('M')
rating_trend = df_rating.groupby('year_month').size()
In [ ]:
plt.figure(figsize=(10, 6))
rating_trend.plot(marker='o')
plt.title('Tren Jumlah Rating Berdasarkan Waktu')
plt.xlabel('Waktu (Year-Month)')
plt.ylabel('Jumlah Rating')
plt.show()
No description has been provided for this image

Pemberian rating terbanyak terjadi di tahun 2000 mencapai lebih dari 400.000 rating. Seiring bejalannya waktu mulai dari tahun 2005 pengguna mulai mengalami penurunan dalam pemberian rating.

Data Preprocessing¶

Melakukan penggabungan dataset yang diperlukan untuk Content Based dan Collaborative Filtering.

Content Based Filtering¶

Dataset yang diperlukan yaitu data movie (df_movie) dan tag (df_tags).

In [ ]:
# Memastikan tidak ada data yang kosong agar tidak terjadi error ketika merge
df_movie['genres'] = df_movie['genres'].fillna('Unknown')
df_tags['tag'] = df_tags['tag'].fillna('')

# Menggabungkan semua tag dari satu film menjadi satu string
con_bf = df_tags.groupby('movieId')['tag'].apply(lambda x: ' '.join(x)).reset_index()

# Menggabungkan data movie dengan tag
con_bf = pd.merge(df_movie, con_bf, on='movieId', how='left')
con_bf['tag'] = con_bf['tag'].fillna('')

con_bf.head()
Out[ ]:
movieId title genres tag
0 1 Toy Story (1995) Adventure|Animation|Children|Comedy|Fantasy Watched computer animation Disney animated fea...
1 2 Jumanji (1995) Adventure|Children|Fantasy time travel adapted from:book board game child...
2 3 Grumpier Old Men (1995) Comedy|Romance old people that is actually funny sequel fever...
3 4 Waiting to Exhale (1995) Comedy|Drama|Romance chick flick revenge characters chick flick cha...
4 5 Father of the Bride Part II (1995) Comedy Diane Keaton family sequel Steve Martin weddin...

✅ Penggabungan dataset movie dan tags menjadi satu dataframe berhasil dilakukan.

In [ ]:
# Menggabungkan genres dan tags menjadi satu kolom
con_bf['combined_features'] = con_bf['genres'] + ' ' + con_bf['tag']

con_bf[['title', 'combined_features']].head()
Out[ ]:
title combined_features
0 Toy Story (1995) Adventure|Animation|Children|Comedy|Fantasy Wa...
1 Jumanji (1995) Adventure|Children|Fantasy time travel adapted...
2 Grumpier Old Men (1995) Comedy|Romance old people that is actually fun...
3 Waiting to Exhale (1995) Comedy|Drama|Romance chick flick revenge chara...
4 Father of the Bride Part II (1995) Comedy Diane Keaton family sequel Steve Martin...

✅ Penggabungan string genres dan tags menjadi satu kolom berhasil dilakukan. Namun masih diperlukan pembersihan pada teks yang akan dilakukan pada data preparation.

Collaborative Based Filtering¶

Dataset yang diperlukan yaitu data movie (df_movie) dan rating (df_rating).

In [ ]:
# Merge movies dan ratings
coll_bf = pd.merge(df_rating, df_movie, on='movieId')

# Menghapus kolom timestamp karena tidak diperlukan
coll_bf = coll_bf.drop(columns=['timestamp'])
In [ ]:
coll_bf.head()
Out[ ]:
userId movieId rating year_month title genres year
0 1 2 3.5 2005-04 Jumanji (1995) Adventure|Children|Fantasy 1995
1 1 29 3.5 2005-04 City of Lost Children, The (Cité des enfants p... Adventure|Drama|Fantasy|Mystery|Sci-Fi 1995
2 1 32 3.5 2005-04 Twelve Monkeys (a.k.a. 12 Monkeys) (1995) Mystery|Sci-Fi|Thriller 1995
3 1 47 3.5 2005-04 Seven (a.k.a. Se7en) (1995) Mystery|Thriller 1995
4 1 50 3.5 2005-04 Usual Suspects, The (1995) Crime|Mystery|Thriller 1995

Data Preparation¶

Normalization¶

Pada langkah ini, dilakukan normalisasi data teks agar terhindar dari karakter dan kata-kata yang tidak diperlukan untuk meningkatkan akurasi dan efisiensi dalam proses analisis serta pemodelan selanjutnya.

In [ ]:
import string
import nltk
from nltk.stem import PorterStemmer, WordNetLemmatizer
from nltk.corpus import stopwords

nltk.download('stopwords')
nltk.download('wordnet')
In [ ]:
# Inisialisasi stemmer, lemmatizer, dan stopwords
stemmer = PorterStemmer()
lemmatizer = WordNetLemmatizer()
stop_words = set(stopwords.words('english'))

def normalize_text(text):
    # Mengganti tanda garis tegak dan karakter lain dengan spasi, lalu mengubah menjadi huruf kecil
    text = text.replace('|', ' ').lower()
    
    # Menghapus tanda baca
    text = text.translate(str.maketrans('', '', string.punctuation))
    
    # Menghapus whitespace berlebih
    text = ' '.join(text.split())
    
    # Lemmatization
    text = ' '.join([lemmatizer.lemmatize(word) for word in text.split()])
    
    # Menghapus stopwords
    text = ' '.join([word for word in text.split() if word not in stop_words])
    
    # Mengganti singkatan
    text = text.replace("n't", " not").replace("'re", " are")
    
    return text
In [ ]:
con_bf['combined_clean'] = con_bf['combined_features'].apply(normalize_text)
In [ ]:
con_bf.head()
Out[ ]:
movieId title genres tag combined_features combined_clean
0 1 Toy Story (1995) Adventure|Animation|Children|Comedy|Fantasy Watched computer animation Disney animated fea... Adventure|Animation|Children|Comedy|Fantasy Wa... adventure animation child comedy fantasy watch...
1 2 Jumanji (1995) Adventure|Children|Fantasy time travel adapted from:book board game child... Adventure|Children|Fantasy time travel adapted... adventure child fantasy time travel adapted fr...
2 3 Grumpier Old Men (1995) Comedy|Romance old people that is actually funny sequel fever... Comedy|Romance old people that is actually fun... comedy romance old people actually funny seque...
3 4 Waiting to Exhale (1995) Comedy|Drama|Romance chick flick revenge characters chick flick cha... Comedy|Drama|Romance chick flick revenge chara... comedy drama romance chick flick revenge chara...
4 5 Father of the Bride Part II (1995) Comedy Diane Keaton family sequel Steve Martin weddin... Comedy Diane Keaton family sequel Steve Martin... comedy diane keaton family sequel steve martin...

Melakukan pengecekan kembali untuk memastikan tidak ada missing value dan duplikat

Check missing values

In [ ]:
con_bf.isnull().sum()
Out[ ]:
movieId              0
title                0
genres               0
tag                  0
combined_features    0
combined_clean       0
dtype: int64
In [ ]:
coll_bf.isnull().sum()
Out[ ]:
userId     0
movieId    0
rating     0
title      0
genres     0
dtype: int64

Check duplicate

In [ ]:
con_bf.duplicated().sum()
Out[ ]:
0
In [ ]:
coll_bf.duplicated().sum()
Out[ ]:
0

✅ Sudah berhasil dilakukan normalisasi pada data teks dan tidak ditemukan adanya missing value maupun duplikat. Data siap untuk dilakukan pemodelan.

Model Development dengan Content Based Filtering¶

In [ ]:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

# Mengubah teks menjadi vektor menggunakan TF-IDF
tfidf = TfidfVectorizer()
tfidf_matrix = tfidf.fit_transform(con_bf['combined_clean'])

# Menghitung kesamaan kosinus
cosine_sim = cosine_similarity(tfidf_matrix, tfidf_matrix)
In [ ]:
# Rekomendasi film
film = 'The Dark Knight'
num_recommendations = 10

recommendations = get_recommendations(film, num_recommendations)
if recommendations is not None:
    print(f'{num_recommendations} film rekomendasi yang mirip dengan {film}: \n')
    display(recommendations[['title', 'combined_clean']])
Precision: 1.00

10 film rekomendasi yang mirip dengan The Dark Knight: 

title combined_clean
20307 Batman: The Dark Knight Returns, Part 2 (2013) action animation le 300 rating action packed a...
18090 Batman: Year One (2011) action animation crime batman le 300 rating ba...
21255 Batman Unmasked: The Psychology of the Dark Kn... documentary batman film psychology batman
3126 Batman: Mask of the Phantasm (1993) animation child adapted fromcomic alter ego ba...
12897 Batman: Gotham Knight (2008) action animation crime batman anime batman goo...
21462 Batman: Mystery of the Batwoman (2003) action animation child crime le 300 rating bas...
15566 Batman: Under the Red Hood (2010) action animation animation antihero batman com...
20891 Justice League: Doom (2012) action animation fantasy lauren montgomery bat...
8646 Batman (1966) action adventure comedy adapted fromcomic alte...
21189 LEGO Batman: The Movie - DC Heroes Unite (2013) action adventure animation le 300 rating anima...

✅ Sistem rekomendasi dengan Content Based Filtering berhasil menampilkan 10 rekomendasi film berdasarkan konten dan mendapatkan Precision sebesar 1.0, artinya semua film yang direkomendasikan memiliki kemiripan yang tinggi dengan film dasar dalam hal konten yang dianalisis, sehingga semua rekomendasi dianggap relevan dan sesuai dengan preferensi yang diukur.

Banyaknya film rekomendasi dapat disesuaikan kebutuhan dengan mengubah nilai pada num_recommendations.

Model Development dengan Collaborative Based Filtering¶

In [ ]:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.decomposition import TruncatedSVD
from sklearn.metrics import mean_squared_error
import numpy as np

colbf = coll_bf.head(1500000)

# Membuat matriks pivot user-item
user_movie_matrix = colbf.pivot(index='userId', columns='movieId', values='rating').fillna(0)

# Menggunakan TruncatedSVD untuk dekomposisi matriks
svd = TruncatedSVD(n_components=12, random_state=42)
matrix_svd = svd.fit_transform(user_movie_matrix)

Evaluasi Model dengan RMSE¶

In [ ]:
RMSE: 0.2879955912967949

RMSE (Root Mean Square Error) sebesar 0.288 menunjukkan bahwa rata-rata kesalahan antara prediksi dan nilai sebenarnya (rating yang diberikan pengguna) adalah sekitar 0.288. Menunjukkan bahwa prediksi sistem cukup dekat dengan nilai yang diharapkan dan sudah cukup baik.

In [ ]:
In [ ]:
# Menampilkan hasil rekomendasi
user_id = 379
num_recommendations = 10
recommended_movies_svd = recommend_movies_svd(user_id, num_recommendations)
print(f"Rekomendasi untuk pengguna dengan id {user_id}:")
recommended_movies_svd
Rekomendasi untuk pengguna dengan id 379:
Out[ ]:
title genres
14247 Kinsey (2004) Drama
21704 Zorro, the Gay Blade (1981) Comedy
21968 Toy Soldiers (1991) Action|Drama
22007 Megaforce (1982) Action|Sci-Fi
26818 Sure Thing, The (1985) Comedy|Romance
96607 Hour of the Wolf (Vargtimmen) (1968) Drama|Horror
107778 C.H.U.D. (1984) Horror
181830 Spirits of the Dead (1968) Horror|Mystery
641679 Macabre (1958) Horror|Thriller

✅ Sistem rekomendasi dengan Collaborative Filtering berhasil menampilkan 10 rekomendasi film berdasarkan konten.

Banyaknya film rekomendasi dapat disesuaikan kebutuhan dengan mengubah nilai pada num_recommendations.

  • Sistem rekomendasi dengan Content Based dan Collaborative Filtering sudah berhasil memberikan rekomendasi film sesuai dengan problem statement dan goal yang dicapai dengan evaluasi sebagai berikut:

    • Content-Based Filtering:

      Precission: 1.0

    • Collaborative Filtering:

      RMSE: 0.288

    Menunjukkan bahwa model pada Content Based Filtering sudah baik baik dan Collaborative Filtering juga sudah cukup baik. Namun untuk Collaborative Filtering masih perlu dilakukan eksperimen lebih lanjut agar nilai RMSE menjadi lebih kecil atau mendekati nol.

  • Menggabungkan kedua strategi ini memungkinkan kita untuk mengembangkan sistem rekomendasi yang lebih andal dan serbaguna. Content-Based Filtering berfokus pada rekomendasi yang didasarkan pada karakteristik internal dari item itu sendiri, sedangkan Collaborative Filtering lebih efektif dalam mengidentifikasi pola preferensi pengguna melalui analisis data interaksi yang ada. Dengan memahami manfaat dan keterbatasan dari masing-masing metode, kita dapat memilih pendekatan yang paling cocok untuk memenuhi kebutuhan dan konteks khusus dari sistem rekomendasi yang dikembangkan. Memanfaatkan kedua metode ini secara bersamaan dapat meningkatkan akurasi dan relevansi rekomendasi, sehingga menawarkan pengalaman yang lebih memuaskan bagi pengguna.