Projeto: Recomendador de filmes

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?

  • Filtros colaborativos: São baseados em informações e comportamentos de outros usuários parecidos com os seus.
  • Filtros baseados em conteúdo: São baseados em informações das preferências do perfil do usuário, em itens similares ao já adquiridos anteriormente.
  • Sistemas híbridos: São utilizados combinando os dois sistemas juntos, colaborativo e baseado em conteudo sendo mais efetivo em alguns casos.

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:

  • Quanto maior sua despesa, maior é a receita arrecadada pelo filme.
  • Quanto maior o tempo de execução do filme, maior é a receita arrecadada.
  • Quanto maior o gasto na produção do filmes, maior é sua popularidade.
  • Quanto maior a quantidade de atores, mais o filme arrecada.
  • Quanto maior mais equipe técnica, mais o filme arrecada.

E responderemos às seguintes perguntas:

  • Quais são os top 20 filmes mais caros para ser produzido? Qual o nome do que mais gastou?
  • Quais são os top 20 filmes mais lucrativos? Qual o nome do que tem a receita mais alta?
  • Qual o filme mais popular?
  • Qual produtora foi a mais lucrativa?
  • Qual produtora foi a mais gastam?

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.

Importando as bibliotecas

In [1]:
# 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

Conhecendo o conjunto de dados

Vamos importar dois conjuntos de dados disponíveis, o primeiro conjunto de dados (tmdb_5000_credits.csv) contém as seguintes informações:

  • movie_id: Um identificador único para cada filme.
  • cast: O nome dos atores principais e coadjuvantes.
  • crew: O nome do Diretor, Editor, Compositor, Escritor etc.

O segundo conjunto de dados (tmdb_5000_movies.csv) contém as seguintes informações:

  • budget: O orçamento em que o filme foi feito.
  • genre: O gênero do filme, ação, comédia, suspense etc.
  • homepage: Um link para a página inicial do filme.
  • id: Este é, de fato, o movie_id como no primeiro conjunto de dados.
  • keywords: As palavras-chave ou tags relacionadas ao filme.
  • original_language: O idioma em que o filme foi feito.
  • original_title: O título do filme antes da tradução ou adaptação.
  • overview: Uma breve descrição do filme.
  • popularity: Uma quantidade numérica que especifica a popularidade do filme.
  • production_companies: A casa de produção do filme.
  • production_countries: O país em que foi produzido.
  • release_date: A data em que foi lançado.
  • revenue: A receita mundial gerada pelo filme.
  • runtime: O tempo de execução do filme em minutos.
  • status: "Lançado" ou "Rumor".
  • tagline: Slogan do filme.
  • title: Título do filme.
  • vote_average: classificações médias que o filme recebeu.
  • vote_count: a contagem dos votos recebidos.
In [2]:
# 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()
Out[2]:
movie_id title cast crew
0 19995 Avatar [{"cast_id": 242, "character": "Jake Sully", "... [{"credit_id": "52fe48009251416c750aca23", "de...
1 285 Pirates of the Caribbean: At World's End [{"cast_id": 4, "character": "Captain Jack Spa... [{"credit_id": "52fe4232c3a36847f800b579", "de...
2 206647 Spectre [{"cast_id": 1, "character": "James Bond", "cr... [{"credit_id": "54805967c3a36829b5002c41", "de...
3 49026 The Dark Knight Rises [{"cast_id": 2, "character": "Bruce Wayne / Ba... [{"credit_id": "52fe4781c3a36847f81398c3", "de...
4 49529 John Carter [{"cast_id": 5, "character": "John Carter", "c... [{"credit_id": "52fe479ac3a36847f813eaa3", "de...
In [3]:
# verificando as primeiras linhas
df_movies.head(2)
Out[3]:
budget genres homepage id keywords original_language original_title overview popularity production_companies production_countries release_date revenue runtime spoken_languages status tagline title vote_average vote_count
0 237000000 [{"id": 28, "name": "Action"}, {"id": 12, "nam... http://www.avatarmovie.com/ 19995 [{"id": 1463, "name": "culture clash"}, {"id":... en Avatar In the 22nd century, a paraplegic Marine is di... 150.437577 [{"name": "Ingenious Film Partners", "id": 289... [{"iso_3166_1": "US", "name": "United States o... 2009-12-10 2787965087 162.0 [{"iso_639_1": "en", "name": "English"}, {"iso... Released Enter the World of Pandora. Avatar 7.2 11800
1 300000000 [{"id": 12, "name": "Adventure"}, {"id": 14, "... http://disney.go.com/disneypictures/pirates/ 285 [{"id": 270, "name": "ocean"}, {"id": 726, "na... en Pirates of the Caribbean: At World's End Captain Barbossa, long believed to be dead, ha... 139.082615 [{"name": "Walt Disney Pictures", "id": 2}, {"... [{"iso_3166_1": "US", "name": "United States o... 2007-05-19 961000000 169.0 [{"iso_639_1": "en", "name": "English"}] Released At the end of the world, the adventure begins. Pirates of the Caribbean: At World's End 6.9 4500
In [4]:
# checando as dimensões
print(df_movies.shape)
print(df_credit.shape)
(4803, 20)
(4803, 4)

Um dos maiores problemas que temos são os dados faltantes, no qual deveremos tratar de alguma forma, com base no nosso objetivo.

In [5]:
# checando se há dados faltantes
df_credit.isnull().sum()
Out[5]:
movie_id    0
title       0
cast        0
crew        0
dtype: int64
In [6]:
# checando se há dados faltantes
pd.DataFrame({'faltas_abs':df_movies.isnull().sum(),
              'prop_falta':df_movies.isnull().sum()/df_movies.shape[0]})
Out[6]:
faltas_abs prop_falta
budget 0 0.000000
genres 0 0.000000
homepage 3091 0.643556
id 0 0.000000
keywords 0 0.000000
original_language 0 0.000000
original_title 0 0.000000
overview 3 0.000625
popularity 0 0.000000
production_companies 0 0.000000
production_countries 0 0.000000
release_date 1 0.000208
revenue 0 0.000000
runtime 2 0.000416
spoken_languages 0 0.000000
status 0 0.000000
tagline 844 0.175724
title 0 0.000000
vote_average 0 0.000000
vote_count 0 0.000000

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.

In [7]:
# checando os tipos dos dados
df_credit.dtypes
Out[7]:
movie_id     int64
title       object
cast        object
crew        object
dtype: object
In [8]:
# checando os tipos dos dados
df_movies.dtypes
Out[8]:
budget                           int64
genres                          object
homepage                        object
id                               int64
keywords                        object
original_language               object
original_title                  object
overview                        object
popularity                     float64
production_companies            object
production_countries            object
release_date            datetime64[ns]
revenue                          int64
runtime                        float64
spoken_languages                object
status                          object
tagline                         object
title                           object
vote_average                   float64
vote_count                       int64
dtype: object

O dados estão corretos, então não precisamos fazer nenhuma transformação.

Extraindo features

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.

In [9]:
# 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.

In [10]:
# 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()
Out[10]:
budget                  0
genres                  0
id                      0
keywords                0
original_language       0
original_title          0
overview                0
popularity              0
production_companies    0
production_countries    0
release_date            0
revenue                 0
runtime                 0
spoken_languages        0
status                  0
tagline                 0
title                   0
vote_average            0
vote_count              0
description             0
dtype: int64

Podemos ver que agora não temos mais dados faltantes, vamos checar pra ver quantas linhas perdemos nessa transformação.

In [11]:
# checando o dataframe com as novas dimensões
df_movies.shape
Out[11]:
(4799, 20)

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.

In [12]:
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.

In [13]:
# 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.

In [14]:
# 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')
In [15]:
# checando as duas primeiras linhas
df_credit.head(2)
Out[15]:
movie_id title cast crew character name_character department_crew job_crew name_crew
0 19995 Avatar [{"cast_id": 242, "character": "Jake Sully", "... [{"credit_id": "52fe48009251416c750aca23", "de... [Jake Sully, Neytiri, Dr. Grace Augustine, Col... [Sam Worthington, Zoe Saldana, Sigourney Weave... [Editing, Art, Sound, Sound, Production, Sound... [Editor, Production Design, Sound Designer, Su... [Stephen E. Rivkin, Rick Carter, Christopher B...
1 285 Pirates of the Caribbean: At World's End [{"cast_id": 4, "character": "Captain Jack Spa... [{"credit_id": "52fe4232c3a36847f800b579", "de... [Captain Jack Sparrow, Will Turner, Elizabeth ... [Johnny Depp, Orlando Bloom, Keira Knightley, ... [Camera, Directing, Production, Writing, Writi... [Director of Photography, Director, Producer, ... [Dariusz Wolski, Gore Verbinski, Jerry Bruckhe...
In [16]:
# df_genre = extract_names('genres')
df_production_companies = extract_names('production_companies')
df_production_countries = extract_names('production_countries')
In [17]:
# df_genre.head()
In [18]:
# 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')
In [19]:
df_movies.head(2)
Out[19]:
budget genres id keywords original_language original_title overview popularity production_companies production_countries ... runtime spoken_languages status tagline title vote_average vote_count description release_year release_month
0 237000000 [{"id": 28, "name": "Action"}, {"id": 12, "nam... 19995 [culture clash, future, space war, space colon... en Avatar In the 22nd century, a paraplegic Marine is di... 150.437577 [Ingenious Film Partners, Twentieth Century Fo... [United States of America, United Kingdom] ... 162.0 [English, Español] Released Enter the World of Pandora. Avatar 7.2 11800 Enter the World of Pandora. In the 22nd centur... 2009 12
1 300000000 [{"id": 12, "name": "Adventure"}, {"id": 14, "... 285 [ocean, drug abuse, exotic island, east india ... en Pirates of the Caribbean: At World's End Captain Barbossa, long believed to be dead, ha... 139.082615 [Walt Disney Pictures, Jerry Bruckheimer Films... [United States of America] ... 169.0 [English] Released At the end of the world, the adventure begins. Pirates of the Caribbean: At World's End 6.9 4500 At the end of the world, the adventure begins.... 2007 5

2 rows × 22 columns

In [20]:
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)
In [21]:
df_movies_full['cast_amount'] = df_movies_full.name_character.apply(len)
In [22]:
df_movies_full['crew_amount'] = df_movies_full.department_crew.apply(len)

Análise Exploratória de Dados

É 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.

In [23]:
# checando as principais estatísticas
df_movies_full.describe()
Out[23]:
budget id popularity revenue runtime vote_average vote_count release_year release_month cast_amount crew_amount
count 4.799000e+03 4799.000000 4799.000000 4.799000e+03 4799.000000 4799.000000 4799.000000 4799.000000 4799.000000 4799.000000 4799.000000
mean 2.906593e+07 56899.920192 21.509884 8.232920e+07 106.903105 6.094186 690.789123 2002.461138 6.794332 22.138987 27.000000
std 4.073251e+07 88236.500208 31.824074 1.629076e+08 22.561305 1.188340 1234.941795 12.414480 3.423371 19.582407 31.633445
min 0.000000e+00 5.000000 0.000372 0.000000e+00 0.000000 0.000000 0.000000 1916.000000 1.000000 0.000000 0.000000
25% 8.000000e+05 9012.500000 4.685547 0.000000e+00 94.000000 5.600000 54.000000 1999.000000 4.000000 11.000000 8.000000
50% 1.500000e+07 14623.000000 12.929525 1.918402e+07 103.000000 6.200000 236.000000 2005.000000 7.000000 17.000000 16.000000
75% 4.000000e+07 58461.500000 28.350728 9.295652e+07 118.000000 6.800000 737.500000 2011.000000 10.000000 25.000000 32.000000
max 3.800000e+08 447027.000000 875.581305 2.787965e+09 338.000000 10.000000 13752.000000 2017.000000 12.000000 224.000000 435.000000

Podemos ter uma visão geral das principais estatísticas e mais a frente vamos analisar mais especificamente com gráficos.

In [24]:
# checando as principais estatísticas categóricas
df_movies_full.describe(include=['O'])
Out[24]:
genres keywords original_language original_title overview production_companies production_countries spoken_languages status tagline title description character name_character department_crew
count 4799 4799 4799 4799 4799 4799 4799 4799 4799 4799 4799 4799 4799 4799 4799
unique 1175 4220 37 4797 4799 3695 469 530 3 3945 4796 4799 4746 4759 4129
top [{"id": 18, "name": "Drama"}] [] en Batman Five years after a zombie outbreak, the men an... [] [United States of America] [English] Released The Host A power beyond measure requires a protector wi... [] [] [Directing, Writing]
freq 369 410 4503 2 1 349 2977 3170 4791 840 2 1 41 41 196

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.

In [25]:
# 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.

In [26]:
# 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()
In [27]:
df_numberic_type.vote_average.mean()
Out[27]:
6.094186288810169

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:

  • O tempo de execução dos filmes tem em média 106 segundos, aproximadamente.
  • A nota média de avaliações é aproximadamente 6.

Então, vamos começar a responder as hipóteses.

Quanto maior sua despesa, maior é a receita arrecadada pelo filme.

In [28]:
# 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.

Quanto maior o tempo de execução do filme, maior é a receita arrecadada.

In [29]:
# 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.

Quanto maior o gasto na produção do filmes, maior é sua popularidade.

In [30]:
# 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.

Quanto maior a quantidade de atores, mais o filme arrecada.

In [31]:
# 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.

Quanto maior mais equipe técnica, mais o filme arrecada.

In [32]:
# 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.

In [33]:
# 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')
Out[33]:
<matplotlib.axes._subplots.AxesSubplot at 0x1bbcad12948>

A correlação vai de -1 à 1, sendo 0 que significa sem correlação nenhuma. Podemos notar as variáveis mais correlacionadas entre si:

  • Contagem de votos com popularidade (0.78);
  • Contagem de votos com receita (0.78);
  • Despesas com receita (0.73), analisamos visualmente.

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.

Quais são os top 20 filmes mais caros para ser produzido? Qual o nome do que mais gastou?

In [34]:
# 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.

Quais são os top 20 filmes mais lucrativos? Qual o nome do que tem a receita mais alta?

In [35]:
# 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.

Qual o filme mais popular?

In [36]:
# 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.

Qual produtora foi a mais lucrativa?

In [37]:
# 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');

Qual produtora foi a mais gastam?

In [38]:
# 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')
Out[38]:
<matplotlib.axes._subplots.AxesSubplot at 0x1bbc05e2d08>

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.

In [39]:
# mantendo as principais colunas
df1 = df_movies_full[['title', 'tagline', 'overview', 'popularity', 'description']]

Construindo o Sistema Recomendador de filmes

Os seguintes passos serão seguidos:

  1. Pré-processamento do texto
  2. Engenharia de features
  3. Computar as similaridades dos documentos
  4. Encontrar os tops filmes similares
  5. Construir a função para recomendação de filmes

1. Pré-processamento do texto

É 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.

In [40]:
# 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.

In [41]:
# 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.

In [42]:
# executando a função
norm_corpus = normalize_corpus(list(df1['description']))

#  olhando o tamanho do corpus
norm_corpus.shape
Out[42]:
(4799,)

Eis o resultado da vetorização, ele converteu um objeto de uma coluna do dataframe em um array numpy com os documentos.

2. Engenharia de features com TF-IDF

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.

In [43]:
# 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
Out[43]:
(4799, 20659)

A matriz sparsa resultante possui 20659 colunas e a quantidade de linhas se mantém.

3. Computando as similaridades

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.

In [44]:
# 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()
Out[44]:
0 1 2 3 4 5 6 7 8 9 ... 4789 4790 4791 4792 4793 4794 4795 4796 4797 4798
0 1.000000 0.010701 0.00000 0.019029 0.028685 0.0249 0.000000 0.026516 0.000000 0.007419 ... 0.009701 0.0 0.023336 0.033549 0.000000 0.000000 0.0 0.006889 0.000000 0.000000
1 0.010701 1.000000 0.01189 0.000000 0.041622 0.0000 0.014562 0.027121 0.034687 0.007614 ... 0.009955 0.0 0.004817 0.000000 0.000000 0.012592 0.0 0.022382 0.013724 0.000000
2 0.000000 0.011890 1.00000 0.000000 0.000000 0.0000 0.000000 0.022242 0.015853 0.004891 ... 0.042615 0.0 0.000000 0.000000 0.016519 0.000000 0.0 0.011677 0.000000 0.003999
3 0.019029 0.000000 0.00000 1.000000 0.008792 0.0000 0.015973 0.023171 0.027451 0.073607 ... 0.000000 0.0 0.009666 0.000000 0.000000 0.000000 0.0 0.028343 0.021783 0.027732
4 0.028685 0.041622 0.00000 0.008792 1.000000 0.0000 0.022909 0.028676 0.000000 0.023536 ... 0.014799 0.0 0.000000 0.000000 0.000000 0.010760 0.0 0.010509 0.000000 0.000000

5 rows × 4799 columns

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.

In [45]:
# convertendo a coluna title em uma lista
movies_list = df1.title.values

# olhando a dimensão
movies_list.shape
Out[45]:
(4799,)

4 Encontrando os mais similares com base em uma amostra

In [46]:
# ordenando por popularidade (do mais popular)
pop_movies = df1.sort_values('popularity', ascending=False)
pop_movies.head()
Out[46]:
title tagline overview popularity description
546 Minions Before Gru, they had a history of bad bosses Minions Stuart, Kevin and Bob are recruited by... 875.581305 Before Gru, they had a history of bad bosses M...
95 Interstellar Mankind was born on Earth. It was never meant ... Interstellar chronicles the adventures of a gr... 724.247784 Mankind was born on Earth. It was never meant ...
788 Deadpool Witness the beginning of a happy ending Deadpool tells the origin story of former Spec... 514.569956 Witness the beginning of a happy ending Deadpo...
94 Guardians of the Galaxy All heroes start somewhere. Light years from Earth, 26 years after being a... 481.098624 All heroes start somewhere. Light years from E...
127 Mad Max: Fury Road What a Lovely Day. An apocalyptic story set in the furthest reach... 434.278564 What a Lovely Day. An apocalyptic story set in...
In [47]:
# localizando o ID, no nosso caso, do mais popular
movie_idx = np.where(movies_list == 'Minions')[0][0]
movie_idx
Out[47]:
546
In [48]:
# encontrando os filmes similares
movie_similarities = doc_sim_df.iloc[movie_idx].values
movie_similarities
Out[48]:
array([0.01045346, 0.01072743, 0.        , ..., 0.00690641, 0.        ,
       0.        ])
In [49]:
# selecionando os 5 mais populares IDs
similar_movie_idxs = np.argsort(-movie_similarities)[1:6]
similar_movie_idxs
Out[49]:
array([506, 614, 241, 813, 154], dtype=int64)
In [50]:
# descrevendo os 5 filmes mais populares
similar_movies = movies_list[similar_movie_idxs]
similar_movies
Out[50]:
array(['Despicable Me 2', 'Despicable Me',
       'Teenage Mutant Ninja Turtles: Out of the Shadows', 'Superman',
       'Rise of the Guardians'], dtype=object)

5. Construindo um recomendador de filmes

In [51]:
# 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
In [52]:
# criando lista dos 10 mais populares do nosso conjunto
popular_movies = pop_movies.title.tolist()[1:11]
In [53]:
# 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**************************************************************************************')
Movie: Interstellar
Top 5 filmes recomendados: ['Gattaca' 'Space Pirate Captain Harlock' 'Space Cowboys'
 'Starship Troopers' 'Final Destination 2']

**************************************************************************************
Movie: Deadpool
Top 5 filmes recomendados: ['Silent Trigger' 'Underworld: Evolution' 'Bronson' 'Shaft' 'Don Jon']

**************************************************************************************
Movie: Guardians of the Galaxy
Top 5 filmes recomendados: ['Chasing Mavericks' 'E.T. the Extra-Terrestrial' 'American Sniper'
 'The Amazing Spider-Man 2' 'Hoop Dreams']

**************************************************************************************
Movie: Mad Max: Fury Road
Top 5 filmes recomendados: ['The 6th Day' 'Star Trek Beyond' 'Kites' 'The Orphanage'
 'The Water Diviner']

**************************************************************************************
Movie: Jurassic World
Top 5 filmes recomendados: ['Jurassic Park' 'The Lost World: Jurassic Park' 'The Nut Job'
 "National Lampoon's Vacation" 'Vacation']

**************************************************************************************
Movie: Pirates of the Caribbean: The Curse of the Black Pearl
Top 5 filmes recomendados: ["Pirates of the Caribbean: Dead Man's Chest" 'The Pirate'
 'Pirates of the Caribbean: On Stranger Tides'
 'The Pirates! In an Adventure with Scientists!' 'Joyful Noise']

**************************************************************************************
Movie: Dawn of the Planet of the Apes
Top 5 filmes recomendados: ['Battle for the Planet of the Apes' 'Groove' 'The Other End of the Line'
 'Chicago Overcoat' 'Definitely, Maybe']

**************************************************************************************
Movie: The Hunger Games: Mockingjay - Part 1
Top 5 filmes recomendados: ['The Hunger Games: Catching Fire' 'The Hunger Games: Mockingjay - Part 2'
 'John Carter' 'For Greater Glory - The True Story of Cristiada'
 'The Proposition']

**************************************************************************************
Movie: Big Hero 6
Top 5 filmes recomendados: ['Wreck-It Ralph' 'A Home at the End of the World' 'Phat Girlz' 'Splice'
 'U.F.O.']

**************************************************************************************
Movie: Terminator Genisys
Top 5 filmes recomendados: ['Terminator 2: Judgment Day' 'Terminator Salvation'
 'Terminator 3: Rise of the Machines' 'Mad Max'
 'X-Men: Days of Future Past']

**************************************************************************************

Conclusão

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.

Referências