Como são recomendados os filmes para você? Que tipo de recomendadores são os mais comuns? Porque determinados filmes aparecem como sugestão? Esse é o objetivo desse trabalho, demonstrarei os tipos mais comuns de sistema de recomendação, neste caso, com exemplos de filmes.
Quais são os tipos mais comuns de motores de recomendação?
Nesse projeto, após uma análise exploratória, irei aplicar o Filtro baseado em conteúdo, no qual o usuário poderá informar o nome do filme e com base na descrição do filme serão recomendados os mais similares.
Na análise exploratória testaremos as seguintes hipóteses:
E responderemos às seguintes perguntas:
Serão utilizadas técnicas de Processamento de Linguagem Natural e similaridades por Cosseno.
O conjunto de dados foi extraído do kaggle The Movie Database (conhecido como TMDb) é uma base de dados grátis e de código aberto sobre Filmes e Séries de TV. Criado por Travis Bell em 2008, a diferença para as outras base de dados, é que TMDb é atualizado constantemente pela comunidade. Era conhecido apenas por ser uma base de dados de filmes, mas em 2013 foi adicionado a parte de Séries de TV.
# Para manipulação e visualização
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
plt.style.use('ggplot')
# Para pré-processamento dos textos
import nltk
from nltk.corpus import stopwords
from nltk import word_tokenize
import re
# para extração de features
from sklearn.feature_extraction.text import TfidfVectorizer
# para similaridades
from sklearn.metrics.pairwise import cosine_similarity
import json
from tqdm.notebook import tqdm
from time import sleep
Vamos importar dois conjuntos de dados disponíveis, o primeiro conjunto de dados (tmdb_5000_credits.csv) contém as seguintes informações:
O segundo conjunto de dados (tmdb_5000_movies.csv) contém as seguintes informações:
# importando o conjunto de dados
df_credit = pd.read_csv('data/tmdb_5000_credits.csv')
df_movies = pd.read_csv('data/tmdb_5000_movies.csv')
# convertendo a data em modo datetime
df_movies['release_date'] = pd.to_datetime(df_movies.release_date)
# verificando as primeiras linhas
df_credit.head()
# verificando as primeiras linhas
df_movies.head(2)
# checando as dimensões
print(df_movies.shape)
print(df_credit.shape)
Um dos maiores problemas que temos são os dados faltantes, no qual deveremos tratar de alguma forma, com base no nosso objetivo.
# checando se há dados faltantes
df_credit.isnull().sum()
# checando se há dados faltantes
pd.DataFrame({'faltas_abs':df_movies.isnull().sum(),
'prop_falta':df_movies.isnull().sum()/df_movies.shape[0]})
O primeiro conjunto de dados não constam dados faltantes, já no segundo já há mais dados. Verificamos que a coluna homepage tem aproximadamente 65%, nesse caso não será problema porque não vamos utilizar essa coluna, então poderemos descarta-la.
Agora vamos olhar se os dados foram importados com seus respectivos tipos corretos.
# checando os tipos dos dados
df_credit.dtypes
# checando os tipos dos dados
df_movies.dtypes
O dados estão corretos, então não precisamos fazer nenhuma transformação.
Antes de começar à explorar, vamos fazer algumas extrações para facilitar as análises, como por exemplo, já vimos que a coluna genres contém um dicionário em cada célula e isso não nos dá uma forma otimizada de análise.
# removendo a coluna homepage
df_movies.drop(['homepage'], axis=1, inplace=True)
Vamos agora criar uma coluna de descrição, a coluna tagline possui algumas células em branco, então vamos preencher os dados null com vazios, e ai juntar com a coluna overview. Vamos fazer isso porque a coluna tagline tem alguns valores em branco, juntando com outra não vamos perder informação quando remover.
# preenchendo os vazios
df_movies.tagline.fillna('', inplace=True)
# criando uma coluna de descrição
df_movies['description'] = df_movies['tagline'].map(str) + ' ' + df_movies['overview']
# removendo valores nulos
df_movies = df_movies.dropna().reset_index().drop('index', axis=1)
# checando os dados faltantes
df_movies.isnull().sum()
Podemos ver que agora não temos mais dados faltantes, vamos checar pra ver quantas linhas perdemos nessa transformação.
# checando o dataframe com as novas dimensões
df_movies.shape
Anteriormente tínhamos 4803 linhas e agora temos 4799, não perdemos quase nada.
Vamos querer também analisar por ano e mês de lançamento, então vamos criar mais essas duas colunas.
df_movies['release_year'] = df_movies.release_date.dt.year
df_movies['release_month'] = df_movies.release_date.dt.month
Para extrairmos as colunas que possuem dicionários, vamos criar duas funções utilizando funções json que facilitam as extrações das informações nesse formato.
# função para extrair os nomes em cada linha de determinada coluna
def extract_names(coluna):
# colocando o coluna em uma variável
col_name = f"{coluna}"
# criando um dataframe vazio
df_coluna=pd.DataFrame()
# colocando um valor para iniciar a contagem no loop
index=1
# iterando com um loop na coluna passada como argumento
for i,each in enumerate(df_movies[f'{coluna}']):
coluna_list=json.loads(each)
for coluna in coluna_list:
df_coluna=pd.concat([df_coluna,pd.DataFrame({"coluna":coluna["name"],
"movie_id": df_movies.id[i],
"original_title":df_movies.original_title[i],
"popularity":df_movies.popularity[i],
"revenue":df_movies.revenue[i],
"budget":df_movies.budget[i],
"release_date":df_movies.release_date[i]},index=[index])],axis=0)
index+=1
df_coluna.rename(columns={'coluna':col_name}, inplace=True)
return df_coluna
#=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
# função para extrair os nomes dos dicionarios em cada linha de determinada coluna
def json_decode(data,key):
result = []
data = json.loads(data) #convert to jsonjsonn from string
for item in data: #convert to list from json
result.append(item[key])
return result
Agora vamos criar alguns dataframes somente com as informações pertinentes que queremos analisar, mas se quisermos agregar com informações de outros dataframes criados, podemos juntar através da coluna ID.
# aplicandao a primeira função e extraindo as informações principais de cada dicionário
df_credit['character'] = df_credit.cast.apply(json_decode, key='character')
df_credit['name_character'] = df_credit.cast.apply(json_decode, key='name')
df_credit['department_crew'] = df_credit.crew.apply(json_decode, key='department')
df_credit['job_crew'] = df_credit.crew.apply(json_decode, key='job')
df_credit['name_crew'] = df_credit.crew.apply(json_decode, key='name')
# checando as duas primeiras linhas
df_credit.head(2)
# df_genre = extract_names('genres')
df_production_companies = extract_names('production_companies')
df_production_countries = extract_names('production_countries')
# df_genre.head()
# df_movies['genres'] = df_movies.genres.apply(json_decode, key='name')
df_movies['keywords'] = df_movies.keywords.apply(json_decode, key='name')
df_movies['production_companies'] = df_movies.production_companies.apply(json_decode, key='name')
df_movies['production_countries'] = df_movies.production_countries.apply(json_decode, key='name')
df_movies['spoken_languages'] = df_movies.spoken_languages.apply(json_decode, key='name')
df_movies.head(2)
df_movies_full = pd.merge(df_movies, df_credit[['movie_id', 'character', 'name_character', 'department_crew']], left_on='id', right_on='movie_id').drop('movie_id', axis=1)
df_movies_full['cast_amount'] = df_movies_full.name_character.apply(len)
df_movies_full['crew_amount'] = df_movies_full.department_crew.apply(len)
É uma abordagem que tem por objetivo, resumir suas principais características, frequentemente com métodos visuais. Este método pode ser usado simplesmente para conhecer os dados, responder perguntas ou até mesmo validar hipóteses, mostrando de fato como o negócio é, desmestificando alguns achismos ou crenças do time de Negócios.
Vamos começar fazendo uma análise estatística geral do conjunto, primeiro com dados numéricos e em seguida com os dados categóricos.
# checando as principais estatísticas
df_movies_full.describe()
Podemos ter uma visão geral das principais estatísticas e mais a frente vamos analisar mais especificamente com gráficos.
# checando as principais estatísticas categóricas
df_movies_full.describe(include=['O'])
Agora vamos começar a analisar mais a fundo nosso dataset para validarmos algumas hipóteses e respondendo à outras perguntas.
Vamos começar olhando a distribuição do conjunto de dados numéricos, pra isso vamos criar uma outra variável específica para isso, somente com as variáveis mais relevantes.
Os valores monetários em dólares estão muito extensos, então vamos dividir por um milhão para ficar mais apresentável e facilitar a visualização.
# selecionando somente as colunas numéricas
df_numberic_type = df_movies_full.select_dtypes(np.number).drop(['id','release_year','release_month'], axis=1)
# dividindo em milhão
df_numberic_type["budget (in million $)"] = df_numberic_type.budget/1000000
df_numberic_type["revenue (in million $)"] = df_numberic_type.revenue/1000000
# removendo as colunas originais
df_numberic_type.drop(['budget', 'revenue'], axis=1, inplace=True)
Depois das definições acima, vamos agora configurar o nosso plot, vamos criar 8 gráficos de distribuição normal, que são as 8 colunas numéricas que escolhemos.
# colocando as colunas em uma lista
colunas = df_numberic_type.columns.tolist()
# definindo a área de plotagem
nrow=2
ncol=4
fig, ax = plt.subplots(nrows=nrow, ncols=ncol, figsize=(25,10))
fig.subplots_adjust(hspace=1, wspace=1)
# plotando gráfico de densidade
idx=0
for col in colunas:
idx+=1
plt.subplot(nrow, ncol, idx)
sns.distplot(df_numberic_type[col], color='blue', kde=False)
plt.title(f'Distribuição de {col}', fontsize=15)
plt.tight_layout()
df_numberic_type.vote_average.mean()
Podemos notar que as variáveis runtime and vote_average (que são a média de avaliações) são as que mais se aproximam de uma distribuição normal e podemos concluir algumas coisas:
Então, vamos começar a responder as hipóteses.
# verificando a relação entre despesas e receita
sns.scatterplot(x='budget (in million $)', y='revenue (in million $)', data=df_numberic_type, color='blue')
plt.title('Revenue vs Budget', fontsize=15);
Podemos validar essa hipótese, pois podemos ver o investimento na produção influencia positivamente a receita, para os filmes.
# verificando a relação entre tempo de execução e receita
sns.scatterplot(x='runtime', y='revenue (in million $)', data=df_numberic_type, color='blue')
plt.title('Runtime vs Revenue', fontsize=15);
Podemos notar que não há uma relação significativa entre essas duas variáveis, pois aparentemente os filmes com tempo de execução entre 90 e 120 segundos são os mais rentáveis, portanto, podemos invalidar essa hipótese.
# verificando a relação entre despesas e popularidade
sns.scatterplot(x='budget (in million $)', y='popularity', data=df_numberic_type, color='blue')
plt.title('popularity vs Budget', fontsize=15);
Podemos notar uma correlação moderada, com alguns outliers, mas não necessariamente que tenha uma despesa maior significa que a popularidade será maior.
# verificando a relação entre elenco e receita
sns.scatterplot(x='cast_amount', y='revenue (in million $)', data=df_numberic_type, color='blue')
plt.title('Cast Amount vs Revenue', fontsize=15);
Também podemos notar uma correlação fraca entre essas duas variáveis.
# verificando a relação entre equipe técnica e receita
sns.scatterplot(x='crew_amount', y='revenue (in million $)', data=df_numberic_type, color='blue')
plt.title('Crew Amount vs revenue', fontsize=15);
Podemos notar uma correlação moderada, quanto mais pessoas na equipe técnica, maior o receita. Pode ser que a influência se dê em função de ter mais pessoa a produção ficar melhor.
Analisamos as principais variáveis especificamente, mas podemos verificar as correlações entre elas com um heatmap.
# verificando a relação entre todas as variáveis
plt.figure(figsize=(10,5))
sns.heatmap(df_numberic_type.corr(), vmin=-1, vmax=1, annot=True, cmap='Blues')
A correlação vai de -1 à 1, sendo 0 que significa sem correlação nenhuma. Podemos notar as variáveis mais correlacionadas entre si:
Com essas informações o time, poderá analisar e ir mais a fundo no que mais tem a ver para melhorar o negócio.
Após checarmos as hipóteses, podem ser levantados alguns questionamentos e vamos responder alguns abaixo.
# agrupando os filmes por título e extraindo as informações de despesas
top20_film_budget = pd.DataFrame(df_movies.groupby('title')['budget'].mean().sort_values(ascending=False)/1000000).reset_index().head(20)
# plotando o gráfico
# plt.figure(figsize=(10,5))
sns.barplot(x=top20_film_budget.budget, y=top20_film_budget.title, color='blue')
plt.title('TOP20 filmes mais caros');
O filme Piratas do Caribe foi o mais caro a ser produzido.
# agrupando os filmes por título e extraindo as informações de receita
top20_film_revenue = pd.DataFrame(df_movies.groupby('title')['revenue'].mean().sort_values(ascending=False)/1000000).reset_index().head(20)
# plotando o gráfico
# plt.figure(figsize=(10,5))
sns.barplot(x=top20_film_revenue.revenue, y=top20_film_revenue.title, color='blue')
plt.title('TOP20 filmes mais lucrativos');
O filme Avatar foi o mais lucrativo.
# agrupando os filmes por título e extraindo as informações de popularidade
top20_film_pop = pd.DataFrame(df_movies.groupby('title')['popularity'].mean().sort_values(ascending=False)/1000000).reset_index().head(20)
# plotando o gráfico
# plt.figure(figsize=(10,5))
sns.barplot(x=top20_film_pop.popularity, y=top20_film_pop.title, color='blue')
plt.title('TOP20 filmes mais populares');
Minions é o filme mais popular.
# classificando por ordem as produtoras mais lucrativas
df_production_companies_r = df_production_companies.sort_values(by='revenue', ascending=False).head(10)
# plotando o gráfico
# plt.figure(figsize=(10,5))
sns.barplot(y=df_production_companies_r.production_companies, x=df_production_companies_r.revenue, color='blue')
plt.title('TOP10 produtoras mais lucrativas');
# classificando por ordem as produtoras que mais gastam
df_production_companies_b = df_production_companies.sort_values(by='budget', ascending=False).head(10)
# plotando o gráfico
# plt.figure(figsize=(10,5))
sns.barplot(y=df_production_companies_b.production_companies, x=df_production_companies_b.budget, color='blue')
Após toda essa análise exploratória, partiremos a criar o nosso modelo.
Esse modelo é baseado em conteúdo, utilizando técnicas de NLP e aplicando a função coseno para retornar os filmes mais similares, abaixo descrevo brevemente sobre essa função.
Vamos iniciar selecionando as colunas principais para o nosso modelo.
# mantendo as principais colunas
df1 = df_movies_full[['title', 'tagline', 'overview', 'popularity', 'description']]
Os seguintes passos serão seguidos:
É a tarefa de deixar o texto previsível e analizável, a cada análise que envolva NLP é necessário avaliar as técnicas que serão empregadas, pois não há uma única forma para utilizar em tudo. Por exemplo, se quero analisar as palavras mais comuns que aparecem em determinado documento, se remover stopwords pode ser que eu estarei removendo outras informações importantes assim como utilizar lemmatization ou stemming, há casos em que não são aplicáveis.
Também conhecido como normalização de textos, na célula abaixo trataremos de criar uma função para isso. Com um conhecimento prévio, ou pessoas que conheçam do negócio para te auxiliar, podemos desenvolver e ir testando até chegar no nosso objetivo.
Como o texto está em inglês, removeremos stopwords desse idioma, assim como caracteres especiais e passaremos todas as palavras para minúsculas e também removeremos espaços que estão a mais.
Observe que neste caso não vamos utilizar lemmatization e stemming.
# objeto para stopwords
stop_words = stopwords.words('english')
# função para normalização do texto
def normalize_doc(doc):
# removendo caracteres especiais
doc = re.sub(r'[^a-zA-Z0-9\s]', '', doc, re.I|re.A)
# convertendo em minusculas
doc = doc.lower()
# removendo espaços indesejados no final
doc = doc.strip()
# tokenizando os documentos
tokens = word_tokenize(doc)
# filtrando as stopwords
filtered_tokens = [token for token in tokens if token not in stop_words]
# juntando o tokens com o texto limpo
doc = ' '.join(filtered_tokens)
# resultado final
return doc
Antes de aplicar a função vamos criar uma instância vetorizando a função para aplicar nos documentos, com o método vectorize do numpy, mas o que essa função faz? Ela pega objetos ou matrizes como entrada e retorna uma única matriz numpy, no nosso caso será retornado uma matriz com os textos normalizados.
# instanciando um objeto para vetorizar a função criada
normalize_corpus = np.vectorize(normalize_doc)
Agora vamos executar a função e ver dar uma olhada em sua dimensão.
# executando a função
norm_corpus = normalize_corpus(list(df1['description']))
# olhando o tamanho do corpus
norm_corpus.shape
Eis o resultado da vetorização, ele converteu um objeto de uma coluna do dataframe em um array numpy com os documentos.
O valor TF-IDF, do termo em inglês Term Frequency - Inverse Document Frequency, que significa Frequencia do Termo - inverso da frequencia nos documentos, serve para indicar a importância de uma palavra de um documento em relação a uma coleção de documentos ou em um corpus linguistico.
Esse valor para uma palavra aumenta proporcionalmente à medida que aumenta o número de ocorrências em um documento, no entanto, esse valor é equilibrado pela frequencia da palavra no corpus, com isso auxilia a distinguir o fato da ocorrência de algumas palavras serem geralmente mais comuns que outras.
Vamos iniciar instanciando um objeto com TfidfVectorizer, com alguns parâmetros, que melhor atende nosso objetivo. O resultado será uma matriz com os valores.
# instanciando o tf-idf
tf = TfidfVectorizer(ngram_range=(1, 2), min_df=2)
# treinando e transformando o corpus
tfidf_matrix = tf.fit_transform(norm_corpus)
# checando as dimensões da matriz
tfidf_matrix.shape
A matriz sparsa resultante possui 20659 colunas e a quantidade de linhas se mantém.
Há varias formas de se calcular as similaridades utilizando distâncias como Euclidean, Manhattan, Jaccard ou cosine... No nosso caso vamos empregar o cálculo de similaridade por coseno, pois essa métrica mede a similaridade entre dois vetores, utilizando o cosseno do ângulo entre eles, em tese verifica se estão apontando aproximadamente para a mesma direção, sendo o score de 0 à 1, quando 1 estão de fato indo para a mesma direção. O cálculo se dá pela equação abaixo.
$ similarity = cos(\theta) = {\frac{A.B}{||A|| . ||B||}} = {\frac{\sum \limits _{i=1} ^{n} . A_{i}.B_{i}}{\sqrt{\sum \limits _{i=1} ^{n}.A_{i}^2} . \sqrt{\sum \limits _{i=1} ^{n}.B_{i}^2}}} $
Vamos utilizar a função cosine_similarity do scikit-learn, passando como argumento a matriz tf-idf que calculamos anteriormente.
# calculando as similaridades na matriz
doc_sim = cosine_similarity(tfidf_matrix)
# colocando em um dataframe
doc_sim_df = pd.DataFrame(doc_sim)
# verificando as primeiras linhas
doc_sim_df.head()
Foram calculados a similaridades em cada linha contra cada linha do dataframe, semelhante a uma matriz de correlação, as linhas 0 e 0 por exemplo tem score 1 porque é ela própria e a similaridade tem o valor máximo.
# convertendo a coluna title em uma lista
movies_list = df1.title.values
# olhando a dimensão
movies_list.shape
# ordenando por popularidade (do mais popular)
pop_movies = df1.sort_values('popularity', ascending=False)
pop_movies.head()
# localizando o ID, no nosso caso, do mais popular
movie_idx = np.where(movies_list == 'Minions')[0][0]
movie_idx
# encontrando os filmes similares
movie_similarities = doc_sim_df.iloc[movie_idx].values
movie_similarities
# selecionando os 5 mais populares IDs
similar_movie_idxs = np.argsort(-movie_similarities)[1:6]
similar_movie_idxs
# descrevendo os 5 filmes mais populares
similar_movies = movies_list[similar_movie_idxs]
similar_movies
# criando a função para o recomendador de filmes
def movie_recommender(movie_title, movies=movies_list, doc_sims=doc_sim_df):
# encontrando o ID do filme
movie_idx = np.where(movies == movie_title)[0][0]
# descrevendo os filmes mais similares
movie_similarities = doc_sims.iloc[movie_idx].values
# encontrando os IDs dos 5 primeiros filmes mais similares
similar_movie_idxs = np.argsort(-movie_similarities)[1:6]
# descrevendo os 5 filmes mais similares
similar_movies = movies[similar_movie_idxs]
# retornando com o resultado
return similar_movies
# criando lista dos 10 mais populares do nosso conjunto
popular_movies = pop_movies.title.tolist()[1:11]
# recomendando os filmes para essa lista
for movie in popular_movies:
print('Movie:', movie)
print('Top 5 filmes recomendados:', movie_recommender(movie_title=movie))
print('\n**************************************************************************************')
Essa foi uma demonstração de um simples engine para recomendar filmes com base na descrição do filme, claro que existem outros, porém a adaptação também é simples para testar outros modelos, assim também como inclusão de outras features que refinariam ainda mais o resultado da busca.
Além disso esse modelo pode servir para outros temas como por exemplo, um conjunto de dados de problemas em uma linha de produção, facilmente pode ser adaptado para encontrar, em uma base de dados, problemas similares com base no conteúdo descrito.
https://rstudio-pubs-static.s3.amazonaws.com/369891_b123051c3cb64da5a6d22a8d0b6e0d84.html
https://www.kaggle.com/eaybek/examination-of-genres-through-year-and-month
https://www.kaggle.com/tmdb/tmdb-movie-metadata
https://movile.blog/sistemas-de-recomendacao-com-filtros-colaborativos/
https://realpython.com/build-recommendation-engine-collaborative-filtering/