|
import pandas as pd |
|
import numpy as np |
|
import matplotlib.pyplot as plt |
|
from ast import literal_eval |
|
import gc |
|
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer |
|
from sklearn.preprocessing import MinMaxScaler |
|
from sklearn.metrics.pairwise import cosine_similarity |
|
|
|
from collections import Counter |
|
|
|
import mlflow |
|
|
|
|
|
def init_mlflow(): |
|
mlflow.set_tracking_uri("http://0.0.0.0:8889") |
|
mlflow.set_experiment("Default") |
|
mlflow.start_run() |
|
mlflow.sklearn.autolog() |
|
|
|
|
|
def load_data(): |
|
credits_df = pd.read_csv('./datasets/credits.csv') |
|
keywords_df = pd.read_csv('./datasets/keywords.csv') |
|
links_df = pd.read_csv('./datasets/links_small.csv') |
|
movies_df = pd.read_csv('./datasets/movies_metadata.csv') |
|
ratings_df = pd.read_csv('./datasets/ratings_small.csv') |
|
return credits_df, keywords_df, links_df, movies_df, ratings_df |
|
|
|
|
|
def draw_adult_movies_pie_chart(movies_df): |
|
plt.figure(figsize=(8, 4)) |
|
plt.scatter(x=[0.5, 1.5], y=[1, 1], s=15000, color=['#06837f', '#fdc100']) |
|
plt.xlim(0, 2) |
|
plt.ylim(0.9, 1.2) |
|
|
|
plt.title('Distribution of Adult and Non Adult Movies', fontsize=18, weight=600, color='#333d29') |
|
plt.text(0.5, 1, '{}\nMovies'.format(str(len(movies_df[movies_df['adult'] == 'True']))), va='center', ha='center', |
|
fontsize=18, weight=600, color='white') |
|
plt.text(1.5, 1, '{}\nMovies'.format(str(len(movies_df[movies_df['adult'] == 'False']))), va='center', ha='center', |
|
fontsize=18, weight=600, color='white') |
|
plt.text(0.5, 1.11, 'Adult', va='center', ha='center', fontsize=17, weight=500, color='#1c2541') |
|
plt.text(1.5, 1.11, 'Non Adult', va='center', ha='center', fontsize=17, weight=500, color='#1c2541') |
|
|
|
plt.axis('off') |
|
|
|
plt.savefig('adult.png') |
|
mlflow.log_artifact('adult.png') |
|
|
|
|
|
def draw_genres_pie_chart(df): |
|
genres_list = [] |
|
for i in df['genres']: |
|
i = i[1:] |
|
i = i[:-1] |
|
genres_list.extend(i.split(', ')) |
|
|
|
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(14, 6)) |
|
|
|
df_plot = pd.DataFrame(Counter(genres_list).most_common(5), columns=['genre', 'total']) |
|
|
|
|
|
|
|
|
|
|
|
df_plot_full = pd.DataFrame([Counter(genres_list)]).transpose().sort_values(by=0, ascending=False) |
|
df_plot.loc[len(df_plot)] = {'genre': 'Others', 'total': df_plot_full[6:].sum()[0]} |
|
plt.title('Percentage Ratio of Movie Genres', fontsize=18, weight=600, color='#333d29') |
|
wedges, texts, autotexts = axes[1].pie(x=df_plot['total'], labels=df_plot['genre'], autopct='%.2f%%', |
|
textprops=dict(fontsize=14), explode=[0, 0, 0, 0, 0, 0.1], |
|
colors=['#06837f', '#02cecb', '#b4ffff', '#f8e16c', '#fed811', '#fdc100']) |
|
|
|
for autotext in autotexts: |
|
autotext.set_color('#1c2541') |
|
autotext.set_weight('bold') |
|
|
|
axes[1].axis('off') |
|
|
|
plt.savefig('genres.png') |
|
mlflow.log_artifact('genres.png') |
|
|
|
|
|
def director(x): |
|
for i in x: |
|
if i["job"] == "Director": |
|
return i["name"] |
|
return "" |
|
|
|
|
|
def writer_screenplay(x): |
|
names = [] |
|
for i in x: |
|
if (i["job"] == "Writer") | (i["job"] == "Screenplay") | (i["job"] == "Author"): |
|
name = i["name"] |
|
names.append(name) |
|
return names |
|
|
|
|
|
def calculate_cosine_similarity(train_df): |
|
cosine_sim = cosine_similarity(train_df) |
|
return cosine_sim |
|
|
|
|
|
def clean_data(credits_df, keywords_df, movies_df): |
|
|
|
|
|
movies_df["id"] = movies_df["id"].apply(pd.to_numeric, errors="ignore") |
|
keywords_df["id"] = keywords_df["id"].apply(int) |
|
credits_df["id"] = credits_df["id"].apply(int) |
|
|
|
|
|
df = movies_df.merge(keywords_df, on="id").merge(credits_df, on="id") |
|
|
|
"""Cleaning our merged data from from duplicated and null values""" |
|
|
|
|
|
df.isnull().sum() |
|
|
|
|
|
df.drop_duplicates(subset=["title", "id"], inplace=True) |
|
|
|
|
|
df = df[df.title.notnull()] |
|
|
|
|
|
(df.vote_count < 30).sum() |
|
|
|
|
|
df = df[df.vote_count > 30] |
|
|
|
|
|
df["release_date"] = pd.to_datetime(df['release_date']) |
|
df["release_year"] = df["release_date"].dt.year |
|
|
|
df.drop("release_date", axis=1, inplace=True) |
|
|
|
|
|
df = df[df["release_year"].notnull()] |
|
df = df[df["runtime"].notnull()] |
|
|
|
|
|
df["vote_average_bins"] = pd.cut(df["vote_average"].astype(float), 10, labels=range(1, 11)) |
|
scaler = MinMaxScaler() |
|
df["vote_average_bins"] = df["vote_average_bins"].astype(int) |
|
df["vote_average_bins"] = scaler.fit_transform(df["vote_average_bins"].values.reshape(-1, 1)) |
|
|
|
df["release_year_bins"] = pd.qcut(df["release_year"].astype(float), q=10, labels=range(1, 11)) |
|
scaler = MinMaxScaler() |
|
df["release_year_bins"] = df["release_year_bins"].astype(int) |
|
df["release_year_bins"] = scaler.fit_transform(df["release_year_bins"].values.reshape(-1, 1)) |
|
|
|
|
|
df.set_index("title", inplace=True) |
|
|
|
|
|
languages = pd.get_dummies(df["original_language"]) |
|
|
|
|
|
df['genres'] = df['genres'].fillna('[]').apply(literal_eval).apply( |
|
lambda x: [i['name'] for i in x] if isinstance(x, list) else "") |
|
df["genres"] = df["genres"].astype(str) |
|
|
|
|
|
|
|
cv = CountVectorizer(lowercase=False) |
|
genres = cv.fit_transform(df["genres"]) |
|
genres_df = pd.DataFrame(genres.todense(), columns=cv.get_feature_names_out()) |
|
genres_df.set_index(df.index, inplace=True) |
|
|
|
|
|
df['keywords'] = df['keywords'].fillna('[]').apply(literal_eval).apply( |
|
lambda x: [i['name'] for i in x] if isinstance(x, list) else "") |
|
df["keywords"] = df["keywords"].astype(str) |
|
df["tagline"].fillna("", inplace=True) |
|
df["overview"].fillna("", inplace=True) |
|
df["keywords"].fillna("", inplace=True) |
|
df["text"] = df["overview"] + df["tagline"] + df["keywords"] |
|
|
|
tfidf = TfidfVectorizer(max_features=5000) |
|
tfidf_matrix = tfidf.fit_transform(df["text"]) |
|
tfidf_df = pd.DataFrame(tfidf_matrix.todense(), columns=tfidf.get_feature_names_out()) |
|
tfidf_df.set_index(df.index, inplace=True) |
|
|
|
|
|
df['cast'] = df['cast'].fillna('[]').apply(literal_eval).apply( |
|
lambda x: [i['name'] for i in x] if isinstance(x, list) else "") |
|
df["cast"] = df["cast"].apply(lambda x: [c.replace(" ", "") for c in x]) |
|
df["cast"] = df["cast"].apply(lambda x: x[:15]) |
|
df["CC"] = df["cast"].astype(str) |
|
cv = CountVectorizer(lowercase=False, min_df=4) |
|
cast = cv.fit_transform(df["CC"]) |
|
cast_df = pd.DataFrame(cast.todense(), columns=cv.get_feature_names_out()) |
|
cast_df.set_index(df.index, inplace=True) |
|
|
|
df["dir"] = df["crew"].apply(literal_eval).apply(director) |
|
directors = pd.get_dummies(df["dir"]) |
|
|
|
df["writer_screenplay"] = df["crew"].apply(literal_eval).apply(writer_screenplay) |
|
df["writer_screenplay"] = df["writer_screenplay"].apply(lambda x: [c.replace(" ", "") for c in x]) |
|
df["writer_screenplay"] = df["writer_screenplay"].apply(lambda x: x[:3]) |
|
df["writer_screenplay"] = df["writer_screenplay"].astype(str) |
|
cv = CountVectorizer(lowercase=False, min_df=2) |
|
writing = cv.fit_transform(df["writer_screenplay"]) |
|
writing_df = pd.DataFrame(writing.todense(), columns=cv.get_feature_names_out()) |
|
writing_df.set_index(df.index, inplace=True) |
|
|
|
gc.collect() |
|
train_df = pd.concat([languages, genres_df, cast_df, writing_df, directors, tfidf_df], axis=1) |
|
train_df = train_df.astype(np.int8) |
|
gc.collect() |
|
|
|
return train_df, df |
|
|
|
|
|
class RecommenderSystem(mlflow.pyfunc.PythonModel): |
|
def load_context(self, context): |
|
credits_df, keywords_df, links_df, movies_df, ratings_df = load_data() |
|
self.train_df, self.df = clean_data(credits_df, keywords_df, movies_df) |
|
self.cosine_sim = calculate_cosine_similarity(self.train_df) |
|
|
|
def predict(self, context, model_input): |
|
return self.recommend(model_input[0], self.cosine_sim) |
|
|
|
def recommend(self, title, cosine_sim): |
|
indices = pd.Series(range(0, len(self.train_df.index)), index=self.train_df.index).drop_duplicates() |
|
number = 10 |
|
|
|
idx = indices[title] |
|
|
|
sim_scores = list(enumerate(cosine_sim[idx])) |
|
|
|
|
|
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True) |
|
|
|
scores_arr = np.array(sim_scores) |
|
scores_mean = np.average(scores_arr, axis=0) |
|
mlflow.log_metric("cosine-total-avg", scores_mean[1]) |
|
|
|
|
|
sim_scores = sim_scores[1:number + 1] |
|
scores_arr = np.array(sim_scores) |
|
scores_mean = np.average(scores_arr, axis=0) |
|
|
|
mlflow.log_metric("cosine-result-avg", scores_mean[1]) |
|
mlflow.log_metric("cosine-result-max", sim_scores[0][1]) |
|
mlflow.log_metric("cosine-result-min", sim_scores[number - 1][1]) |
|
mlflow.log_param("number-of-results", number) |
|
|
|
|
|
movie_indices = [i[0] for i in sim_scores] |
|
|
|
recommendations = pd.DataFrame({"Movies": self.df.iloc[movie_indices].index.tolist(), |
|
"Id": self.df.iloc[movie_indices].imdb_id.tolist(), |
|
"Similarity": [sim[1] for sim in sim_scores]}) |
|
return recommendations |
|
|
|
|
|
if __name__ == '__main__': |
|
mlflow.pyfunc.save_model(path="imdb-recommendation-v2", python_model=RecommenderSystem()) |
|
init_mlflow() |
|
mlflow.pyfunc.log_model("imdb-recommendation-v2", python_model=RecommenderSystem(), registered_model_name="recommendation-model-v2") |
|
loaded_model = mlflow.pyfunc.load_model("imdb-recommendation-v2") |
|
print(loaded_model.predict(["The Dark Knight Rises"])) |
|
mlflow.end_run() |
|
|
|
|