Integrantes: Alan Acevedo, Camila Labarca, Franco Miranda, Julia Paredes
Grupo: 4
Profesores: Andrés Abeliuk, Hernán Sarmiento
Auxiliares: Alison Fernández, Cinthia Sánchez
Fecha de entrega: 20 de diciembre de 2021
En la última década, el mercado de las películas y series ha crecido enormemente gracias a los avances tecnológicos y a plataformas online como Netflix, Hulu, Amazon, Disney+, entre otras. Este tema puede ser bastante interesante de analizar ya que prácticamente todas las personas están familiarizadas con éste ámbito.
El objetivo de este proyecto es aplicar distintas técnicas de minería de datos para realizar un estudio sobre un dataset obtenido de la Internet Movie Database que contiene información sobre películas. Los resultados le podrían ser de bastante utilidad a aspirantes en el mundo del cine, pues les daría una amplia imagen de la industria cinematográfica, y junto con ello entender su comportamiento.
Las mejoras que se realizarán al Hito 1 son principalmente 2. La primera, es que no solo se tendrá en cuenta la información de las películas, sino que también de los actores, directores y escritores que participaron en ellas, por lo que a la tabla que se había obtenido en el Hito 1 se le agregará esta información. Y la segunda, es que habiendo actualizado el dataset se refinarán las preguntas planteadas al final del Hito 1.
Se trabajó con el dataset de IMDB, el cual está distribuido en 7 tablas en formato TSV, de las cuales se ocuparán cinco.
La tabla title_basics contiene varios atributos de interés sobre títulos:
titleType: tipo/formato del título (película, cortometraje, documental, etc.)
primaryTitle: el nombre más popular del título
startYear: año de lanzamiento original
runtimeMinutes: duración en minutos
genres: arreglo de hasta tres géneros asociados al título
La tabla title_ratings contiene dos atributos interesantes:
averageRating: promedio de todas las valoraciones de los usuarios de IMDB que ha recibido el título
numVotes: número de valoraciones que el título ha recibido
La tabla title_crew contiene dos atributos de interés:
directors: id de los directores
writers: id de los escritores
La tabla title_principals tiene cinco atributos relevantes:
ordering: número para identificar filas para un mismo titleId
nconst: identificador único de la persona
category: la categoría del trabajo de la persona
job: trabajo específico de la persona
characters: el nombre del personaje que se actuó, si es aplicable a la persona
La tabla name_basics contiene cinco atributos importantes:
primaryName: nombre de la persona
birthYear: fecha de nacimiento
deathYear: fecha de muerte, si es aplicable
primaryProfession: top 3 profesiones de la persona
knownForTitles: títulos por los que la persona es conocida
Se importan las librerías necesarias.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import re
import seaborn as sns
from sklearn import preprocessing
from sklearn.dummy import DummyClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.neighbors import NearestNeighbors
from sklearn.naive_bayes import GaussianNB
from sklearn.metrics import f1_score, recall_score, precision_score
from sklearn.model_selection import train_test_split
from sklearn.cluster import KMeans
from sklearn.cluster import AgglomerativeClustering
from sklearn.cluster import DBSCAN
from sklearn.decomposition import PCA
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.metrics import silhouette_score
from sklearn.metrics.pairwise import euclidean_distances
pd.options.mode.chained_assignment = None
Se importan los datasets necesarios.
title_basics = pd.read_csv("./datasets/title_basics.tsv", sep='\t', encoding="UTF-8")
title_ratings = pd.read_csv("./datasets/title_ratings.tsv", sep='\t', encoding="UTF-8")
title_crew = pd.read_csv("./datasets/title_crew.tsv", sep='\t', encoding="UTF-8")
name_basics = pd.read_csv("./datasets/name_basics.tsv", sep='\t', encoding="UTF-8")
title_principals = pd.read_csv("./datasets/title_principals.tsv", sep='\t', encoding="UTF-8")
C:\Users\franc\anaconda3\lib\site-packages\IPython\core\interactiveshell.py:3165: DtypeWarning: Columns (4,5) have mixed types.Specify dtype option on import or set low_memory=False. has_raised = await self.run_ast_nodes(code_ast.body, cell_name,
Luego, los atributos que correspondan se cambian a categóricos o a númericos
title_basics["runtimeMinutes"] = pd.to_numeric(title_basics["runtimeMinutes"], errors='coerce')
title_basics["genres"] = title_basics["genres"].astype(object)
title_basics["startYear"] = pd.to_numeric(title_basics["startYear"], errors='coerce')
title_basics["titleType"] = title_basics["titleType"].astype(object)
title_basics["isAdult"] = title_basics["isAdult"].astype(object)
title_ratings["averageRating"] = pd.to_numeric(title_ratings["averageRating"], errors='coerce')
title_ratings["numVotes"] = pd.to_numeric(title_ratings["numVotes"], errors='coerce')
title_basics.shape[0]
8246400
title_basics tiene 8.246.400 filas.
Para ver cuántos elementos sin valores asignados hay en las tablas se usa la siguiente línea:
title_basics.isna().sum(axis=0)
tconst 0 titleType 0 primaryTitle 9 originalTitle 9 isAdult 0 startYear 971886 endYear 0 runtimeMinutes 5920699 genres 10 dtype: int64
La columna runtimeMinutes tiene 5.920.699 valores no asignados, lo que corresponde a un gran porcentaje del total de filas. También hay 971.886 valores nulos en startYear. Estos atributos son de interés y podrían ser usados en el estudio, por lo que se hará una limpieza del dataset.
Se realizará un estudio de los datos relacionados a películas, por lo que se filtrará para quitar los títulos de tipo documental o cortometraje, y además solo se considerarán las películas con géneros válidos.
only_movies = title_basics[(title_basics.genres != '\\N') & (title_basics.titleType == "movie")]
only_movies.shape[0]
516492
El nuevo dataframe tiene 516.492 filas.
Ahora se puede volver a ver cuantos elementos tienen valores no asignados.
only_movies.isna().sum(axis=0)
tconst 0 titleType 0 primaryTitle 0 originalTitle 0 isAdult 0 startYear 70672 endYear 0 runtimeMinutes 170153 genres 0 dtype: int64
only_movies = only_movies.dropna()
only_movies.shape[0]
340916
Después de la limpieza de valores nulos, title_basics tiene 340.916 filas. Sigue siendo un número importante de datos que pueden ser estudiados por lo que no es necesario considerar los valores nulos.
Como ya se filtró según la columna titleType, ésta se elimina del dataframe. Además, endYear es siempre \N para las películas, por lo que no entregan información y se eliminan del dataframe. Por último se elimina la columna originalTitle ya que no es de interés (es el nombre de la película en el idioma original).
only_movies.drop('endYear', axis=1, inplace=True)
only_movies.drop('titleType', axis=1, inplace=True)
only_movies.drop('originalTitle', axis=1, inplace=True)
Se hace un join de las tablas only_movies y title_ratings para que cada película tenga su valoración promedio y cantidad de valoraciones.
movies = pd.merge(only_movies, title_ratings, on='tconst')
Se crea una nueva tabla que será el join de movies y title_crew por la variable tconst, el identificador de cada película.
movies_2 = pd.merge(movies, title_crew, on='tconst')
movies_2
| tconst | primaryTitle | isAdult | startYear | runtimeMinutes | genres | averageRating | numVotes | directors | writers | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | tt0000574 | The Story of the Kelly Gang | 0 | 1906.0 | 70.0 | Action,Adventure,Biography | 6.1 | 692 | nm0846879 | nm0846879 |
| 1 | tt0000591 | The Prodigal Son | 0 | 1907.0 | 90.0 | Drama | 5.2 | 16 | nm0141150 | nm0141150 |
| 2 | tt0000679 | The Fairylogue and Radio-Plays | 0 | 1908.0 | 120.0 | Adventure,Fantasy | 5.4 | 65 | nm0877783,nm0091767 | nm0000875,nm0877783 |
| 3 | tt0001184 | Don Juan de Serrallonga | 0 | 1910.0 | 58.0 | Adventure,Drama | 3.4 | 19 | nm0063413,nm0550220 | nm0049370 |
| 4 | tt0001258 | The White Slave Trade | 0 | 1910.0 | 45.0 | Drama | 5.7 | 132 | nm0088881 | \N |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 235204 | tt9916170 | The Rehearsal | 0 | 2019.0 | 51.0 | Drama | 7.2 | 5 | nm5412267 | nm5412267,nm6743460,nm3245789 |
| 235205 | tt9916190 | Safeguard | 0 | 2020.0 | 90.0 | Action,Adventure,Thriller | 3.6 | 222 | nm7308376 | nm7308376 |
| 235206 | tt9916270 | Il talento del calabrone | 0 | 2020.0 | 84.0 | Thriller | 5.8 | 1163 | nm1480867 | nm1480867,nm10538402 |
| 235207 | tt9916362 | Coven | 0 | 2020.0 | 92.0 | Adventure,Drama,History | 6.3 | 3829 | nm1893148 | nm1893148,nm3471432 |
| 235208 | tt9916538 | Kuambil Lagi Hatiku | 0 | 2019.0 | 123.0 | Drama | 8.3 | 6 | nm4457074 | nm4843252,nm4900525,nm2679404 |
235209 rows × 10 columns
Se tiene que trabajar un poco para que aparezcan los nombres de los directores.
movDir = movies_2[['tconst', 'directors']]
# Se crea una lista de los directores
movDir["directors"] = movDir["directors"].str.split(",")
# Se separa cada uno de los directores por su película
md = movDir.explode("directors")
# Se mapea name_basics a la columna de directores
md["directors"] = md["directors"].map(name_basics.set_index("nconst").squeeze()['primaryName']).fillna("")
# Los nombres de los directores se agrupan para cada película
res = md.groupby("tconst").agg(",".join).reset_index()
# Se quitan las comas innecesarias de la nueva columna
res["directors"] = res["directors"].str.strip(",")
res
| tconst | directors | |
|---|---|---|
| 0 | tt0000574 | Charles Tait |
| 1 | tt0000591 | Michel Carré |
| 2 | tt0000679 | Otis Turner,Francis Boggs |
| 3 | tt0001184 | Ricardo de Baños,Alberto Marro |
| 4 | tt0001258 | August Blom |
| ... | ... | ... |
| 235204 | tt9916170 | Tamar Guimaraes |
| 235205 | tt9916190 | Fraser Precious |
| 235206 | tt9916270 | Giacomo Cimini |
| 235207 | tt9916362 | Pablo Agüero |
| 235208 | tt9916538 | Azhar Kinoi Lubis |
235209 rows × 2 columns
Como se obtuvieron los nombres de los directores para cada película, basta unir esta tabla con la original.
movies_3 = pd.merge(movies, res, on='tconst')
movies_3
| tconst | primaryTitle | isAdult | startYear | runtimeMinutes | genres | averageRating | numVotes | directors | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | tt0000574 | The Story of the Kelly Gang | 0 | 1906.0 | 70.0 | Action,Adventure,Biography | 6.1 | 692 | Charles Tait |
| 1 | tt0000591 | The Prodigal Son | 0 | 1907.0 | 90.0 | Drama | 5.2 | 16 | Michel Carré |
| 2 | tt0000679 | The Fairylogue and Radio-Plays | 0 | 1908.0 | 120.0 | Adventure,Fantasy | 5.4 | 65 | Otis Turner,Francis Boggs |
| 3 | tt0001184 | Don Juan de Serrallonga | 0 | 1910.0 | 58.0 | Adventure,Drama | 3.4 | 19 | Ricardo de Baños,Alberto Marro |
| 4 | tt0001258 | The White Slave Trade | 0 | 1910.0 | 45.0 | Drama | 5.7 | 132 | August Blom |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 235204 | tt9916170 | The Rehearsal | 0 | 2019.0 | 51.0 | Drama | 7.2 | 5 | Tamar Guimaraes |
| 235205 | tt9916190 | Safeguard | 0 | 2020.0 | 90.0 | Action,Adventure,Thriller | 3.6 | 222 | Fraser Precious |
| 235206 | tt9916270 | Il talento del calabrone | 0 | 2020.0 | 84.0 | Thriller | 5.8 | 1163 | Giacomo Cimini |
| 235207 | tt9916362 | Coven | 0 | 2020.0 | 92.0 | Adventure,Drama,History | 6.3 | 3829 | Pablo Agüero |
| 235208 | tt9916538 | Kuambil Lagi Hatiku | 0 | 2019.0 | 123.0 | Drama | 8.3 | 6 | Azhar Kinoi Lubis |
235209 rows × 9 columns
Se hace el mismo proceso anterior para los escritores.
movWri = movies_2[['tconst', 'writers']]
# Se crea una lista de los escritores
movWri["writers"] = movWri["writers"].str.split(",")
# Se separa cada uno de los escritores por su película
mw = movWri.explode("writers")
# Se mapea name_basics a la columna de escritores
mw["writers"] = mw["writers"].map(name_basics.set_index("nconst").squeeze()['primaryName']).fillna("")
# Los nombres de los escritores se agrupan para cada película
res2 = mw.groupby("tconst").agg(",".join).reset_index()
# Se quitan las comas innecesarias de la nueva columna
res2["writers"] = res2["writers"].str.strip(",")
res2
| tconst | writers | |
|---|---|---|
| 0 | tt0000574 | Charles Tait |
| 1 | tt0000591 | Michel Carré |
| 2 | tt0000679 | L. Frank Baum,Otis Turner |
| 3 | tt0001184 | Víctor Balaguer |
| 4 | tt0001258 | |
| ... | ... | ... |
| 235204 | tt9916170 | Tamar Guimaraes,Lillah Halla,Melissa de Raaf |
| 235205 | tt9916190 | Fraser Precious |
| 235206 | tt9916270 | Giacomo Cimini,Lorenzo Collalti |
| 235207 | tt9916362 | Pablo Agüero,Katell Guillou |
| 235208 | tt9916538 | Arief Ash Siddiq,Rino Sarjono,Salman Aristo |
235209 rows × 2 columns
Se unen las tablas y se elimina una columna innecesaria
movies_4 = pd.merge(movies_3, res2, on='tconst')
movies_4
| tconst | primaryTitle | isAdult | startYear | runtimeMinutes | genres | averageRating | numVotes | directors | writers | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | tt0000574 | The Story of the Kelly Gang | 0 | 1906.0 | 70.0 | Action,Adventure,Biography | 6.1 | 692 | Charles Tait | Charles Tait |
| 1 | tt0000591 | The Prodigal Son | 0 | 1907.0 | 90.0 | Drama | 5.2 | 16 | Michel Carré | Michel Carré |
| 2 | tt0000679 | The Fairylogue and Radio-Plays | 0 | 1908.0 | 120.0 | Adventure,Fantasy | 5.4 | 65 | Otis Turner,Francis Boggs | L. Frank Baum,Otis Turner |
| 3 | tt0001184 | Don Juan de Serrallonga | 0 | 1910.0 | 58.0 | Adventure,Drama | 3.4 | 19 | Ricardo de Baños,Alberto Marro | Víctor Balaguer |
| 4 | tt0001258 | The White Slave Trade | 0 | 1910.0 | 45.0 | Drama | 5.7 | 132 | August Blom | |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 235204 | tt9916170 | The Rehearsal | 0 | 2019.0 | 51.0 | Drama | 7.2 | 5 | Tamar Guimaraes | Tamar Guimaraes,Lillah Halla,Melissa de Raaf |
| 235205 | tt9916190 | Safeguard | 0 | 2020.0 | 90.0 | Action,Adventure,Thriller | 3.6 | 222 | Fraser Precious | Fraser Precious |
| 235206 | tt9916270 | Il talento del calabrone | 0 | 2020.0 | 84.0 | Thriller | 5.8 | 1163 | Giacomo Cimini | Giacomo Cimini,Lorenzo Collalti |
| 235207 | tt9916362 | Coven | 0 | 2020.0 | 92.0 | Adventure,Drama,History | 6.3 | 3829 | Pablo Agüero | Pablo Agüero,Katell Guillou |
| 235208 | tt9916538 | Kuambil Lagi Hatiku | 0 | 2019.0 | 123.0 | Drama | 8.3 | 6 | Azhar Kinoi Lubis | Arief Ash Siddiq,Rino Sarjono,Salman Aristo |
235209 rows × 10 columns
Falta vincular los actores con sus películas. Para ello, se debe trabajar de una forma distinta pues el formato de la tabla en donde se encuentran los actores es distinto al resto.
# Se seleccionan las columnas tconst y nconst, los identificadores de las películas y de cada persona, donde
# la persona asociada a cada nconst sea un actor o actriz.
actors_title = title_principals[title_principals['category'].isin(['actor', 'actress'])][['tconst', 'nconst']]
# Se mapea name_basics a la columna de nconst
actors_title['nconst'] = actors_title["nconst"].map(name_basics.set_index("nconst").squeeze()['primaryName']).fillna("")
# Se renombra la columna nconst como actors
actors_title = actors_title.rename(columns={'nconst':'actors'})
# Se consideran solo las películas que estén en la tabla creada anteriormente
actors_title = actors_title[actors_title['tconst'].isin(movies_4['tconst'])]
# Los nombres de los actores se agrupan para cada película
actors_title = actors_title.groupby("tconst").agg(",".join).reset_index()
# Se quitan las comas innecesarias
actors_title["actors"] = actors_title["actors"].str.strip(",")
actors_title
| tconst | actors | |
|---|---|---|
| 0 | tt0000574 | Elizabeth Tait,John Tait,Norman Campbell,Bella... |
| 1 | tt0000591 | Georges Wague,Henri Gouget,Christiane Mandelys... |
| 2 | tt0000679 | L. Frank Baum,Frank Burns,George E. Wilson,Wal... |
| 3 | tt0001184 | Dolores Puchol,Cecilio Rodríguez de la Vega |
| 4 | tt0001258 | Aage Lorentzen,Ellen Diedrich,Victor Fabian,Ju... |
| ... | ... | ... |
| 207478 | tt9916170 | Julia Ianina,Pablo Lafuente,Kelner Macêdo,Germ... |
| 207479 | tt9916190 | Patrick Gallagher,Akie Kotabe,Takayuki Suzuki,... |
| 207480 | tt9916270 | Sergio Castellitto,Lorenzo Richelmy,Anna Fogli... |
| 207481 | tt9916362 | Amaia Aberasturi,Alex Brendemühl,Daniel Fanego... |
| 207482 | tt9916538 | Lala Karmela,Cut Mini Theo,Sahil Shah,Ria Irawan |
207483 rows × 2 columns
Se muestran todos los actores para una película en concreto, por lo tanto, hay que juntar las tablas, eliminar las filas con algún NA (ya sea de director, escritor o actor) y con esto se tiene toda la información.
movies_5 = pd.merge(movies_4, actors_title, on='tconst')
movies_5.dropna()
movies_5
| tconst | primaryTitle | isAdult | startYear | runtimeMinutes | genres | averageRating | numVotes | directors | writers | actors | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | tt0000574 | The Story of the Kelly Gang | 0 | 1906.0 | 70.0 | Action,Adventure,Biography | 6.1 | 692 | Charles Tait | Charles Tait | Elizabeth Tait,John Tait,Norman Campbell,Bella... |
| 1 | tt0000591 | The Prodigal Son | 0 | 1907.0 | 90.0 | Drama | 5.2 | 16 | Michel Carré | Michel Carré | Georges Wague,Henri Gouget,Christiane Mandelys... |
| 2 | tt0000679 | The Fairylogue and Radio-Plays | 0 | 1908.0 | 120.0 | Adventure,Fantasy | 5.4 | 65 | Otis Turner,Francis Boggs | L. Frank Baum,Otis Turner | L. Frank Baum,Frank Burns,George E. Wilson,Wal... |
| 3 | tt0001184 | Don Juan de Serrallonga | 0 | 1910.0 | 58.0 | Adventure,Drama | 3.4 | 19 | Ricardo de Baños,Alberto Marro | Víctor Balaguer | Dolores Puchol,Cecilio Rodríguez de la Vega |
| 4 | tt0001258 | The White Slave Trade | 0 | 1910.0 | 45.0 | Drama | 5.7 | 132 | August Blom | Aage Lorentzen,Ellen Diedrich,Victor Fabian,Ju... | |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 207478 | tt9916170 | The Rehearsal | 0 | 2019.0 | 51.0 | Drama | 7.2 | 5 | Tamar Guimaraes | Tamar Guimaraes,Lillah Halla,Melissa de Raaf | Julia Ianina,Pablo Lafuente,Kelner Macêdo,Germ... |
| 207479 | tt9916190 | Safeguard | 0 | 2020.0 | 90.0 | Action,Adventure,Thriller | 3.6 | 222 | Fraser Precious | Fraser Precious | Patrick Gallagher,Akie Kotabe,Takayuki Suzuki,... |
| 207480 | tt9916270 | Il talento del calabrone | 0 | 2020.0 | 84.0 | Thriller | 5.8 | 1163 | Giacomo Cimini | Giacomo Cimini,Lorenzo Collalti | Sergio Castellitto,Lorenzo Richelmy,Anna Fogli... |
| 207481 | tt9916362 | Coven | 0 | 2020.0 | 92.0 | Adventure,Drama,History | 6.3 | 3829 | Pablo Agüero | Pablo Agüero,Katell Guillou | Amaia Aberasturi,Alex Brendemühl,Daniel Fanego... |
| 207482 | tt9916538 | Kuambil Lagi Hatiku | 0 | 2019.0 | 123.0 | Drama | 8.3 | 6 | Azhar Kinoi Lubis | Arief Ash Siddiq,Rino Sarjono,Salman Aristo | Lala Karmela,Cut Mini Theo,Sahil Shah,Ria Irawan |
207483 rows × 11 columns
Se puede ver que efectivamente no hay filas con algún valor nulo.
movies_5.isna().sum(axis=0)
tconst 0 primaryTitle 0 isAdult 0 startYear 0 runtimeMinutes 0 genres 0 averageRating 0 numVotes 0 directors 0 writers 0 actors 0 dtype: int64
No hay nulos en ninguna columna, por lo que el dataframe está completamente limpio.
Luego de haber limpiado el dataset, se puede hacer un summary para analizar si hay algún dato inconsistente.
movies_5.describe()
| startYear | runtimeMinutes | averageRating | numVotes | |
|---|---|---|---|---|
| count | 207483.000000 | 207483.000000 | 207483.000000 | 2.074830e+05 |
| mean | 1991.278119 | 95.541558 | 6.001914 | 4.363440e+03 |
| std | 26.041480 | 103.329849 | 1.275619 | 3.730290e+04 |
| min | 1906.000000 | 1.000000 | 1.000000 | 5.000000e+00 |
| 25% | 1973.000000 | 83.000000 | 5.300000 | 2.500000e+01 |
| 50% | 2000.000000 | 92.000000 | 6.100000 | 8.800000e+01 |
| 75% | 2013.000000 | 104.000000 | 6.900000 | 4.410000e+02 |
| max | 2021.000000 | 43200.000000 | 10.000000 | 2.455855e+06 |
En startYear se muestran años desde 1906 hasta el año actual, no existen películas en años futuros por lo que está correcta esta columna. En runtimeMinutes existen películas con 1 minuto de duración y con más de 43 mil minutos de duración, estos valores posiblemente sean outliers y entonces se pueden trabajar sin problemas. Los valores de la columna averageRating se mueven entre 1 y 10 lo que es consistente. Por último, la columna numVotes se mueve entre valores positivos y entonces es consistente.
A partir de ahora solo se trabajará con esta tabla, por lo cual se reemplazará la variable movies y se liberará la memoria ocupada por la importación de las otras tablas.
movies = movies_5
title_basics = None
title_ratings = None
title_crew = None
name_basics = None
title_principals = None
# Para exportar el dataset
#movies_5.to_csv("movies_final.tsv", sep='\t')
En la tabla movies hay una columna “genres” que lista hasta 3 géneros de una misma película, éstos se pueden separar para hacer más sencillo el estudio. Se considerará el primer género como el principal.
movies_alt = pd.read_csv("./datasets/movies_final.tsv", sep='\t')
movies_alt["Genero1"], movies_alt["Genero2"], movies_alt["Genero3"] = movies_alt["genres"].str.split(',', 2).str
movies_alt.drop('genres', axis=1, inplace=True)
movies_alt
<ipython-input-23-ac2338081543>:2: FutureWarning: Columnar iteration over characters will be deprecated in future releases.
movies_alt["Genero1"], movies_alt["Genero2"], movies_alt["Genero3"] = movies_alt["genres"].str.split(',', 2).str
| Unnamed: 0 | tconst | primaryTitle | isAdult | startYear | runtimeMinutes | averageRating | numVotes | directors | writers | actors | Genero1 | Genero2 | Genero3 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | tt0000574 | The Story of the Kelly Gang | 0 | 1906.0 | 70.0 | 6.1 | 692 | Charles Tait | Charles Tait | Elizabeth Tait,John Tait,Norman Campbell,Bella... | Action | Adventure | Biography |
| 1 | 1 | tt0000591 | The Prodigal Son | 0 | 1907.0 | 90.0 | 5.2 | 16 | Michel Carré | Michel Carré | Georges Wague,Henri Gouget,Christiane Mandelys... | Drama | NaN | NaN |
| 2 | 2 | tt0000679 | The Fairylogue and Radio-Plays | 0 | 1908.0 | 120.0 | 5.4 | 65 | Otis Turner,Francis Boggs | L. Frank Baum,Otis Turner | L. Frank Baum,Frank Burns,George E. Wilson,Wal... | Adventure | Fantasy | NaN |
| 3 | 3 | tt0001184 | Don Juan de Serrallonga | 0 | 1910.0 | 58.0 | 3.4 | 19 | Ricardo de Baños,Alberto Marro | Víctor Balaguer | Dolores Puchol,Cecilio Rodríguez de la Vega | Adventure | Drama | NaN |
| 4 | 4 | tt0001258 | The White Slave Trade | 0 | 1910.0 | 45.0 | 5.7 | 132 | August Blom | NaN | Aage Lorentzen,Ellen Diedrich,Victor Fabian,Ju... | Drama | NaN | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 207478 | 207478 | tt9916170 | The Rehearsal | 0 | 2019.0 | 51.0 | 7.2 | 5 | Tamar Guimaraes | Tamar Guimaraes,Lillah Halla,Melissa de Raaf | Julia Ianina,Pablo Lafuente,Kelner Macêdo,Germ... | Drama | NaN | NaN |
| 207479 | 207479 | tt9916190 | Safeguard | 0 | 2020.0 | 90.0 | 3.6 | 222 | Fraser Precious | Fraser Precious | Patrick Gallagher,Akie Kotabe,Takayuki Suzuki,... | Action | Adventure | Thriller |
| 207480 | 207480 | tt9916270 | Il talento del calabrone | 0 | 2020.0 | 84.0 | 5.8 | 1163 | Giacomo Cimini | Giacomo Cimini,Lorenzo Collalti | Sergio Castellitto,Lorenzo Richelmy,Anna Fogli... | Thriller | NaN | NaN |
| 207481 | 207481 | tt9916362 | Coven | 0 | 2020.0 | 92.0 | 6.3 | 3829 | Pablo Agüero | Pablo Agüero,Katell Guillou | Amaia Aberasturi,Alex Brendemühl,Daniel Fanego... | Adventure | Drama | History |
| 207482 | 207482 | tt9916538 | Kuambil Lagi Hatiku | 0 | 2019.0 | 123.0 | 8.3 | 6 | Azhar Kinoi Lubis | Arief Ash Siddiq,Rino Sarjono,Salman Aristo | Lala Karmela,Cut Mini Theo,Sahil Shah,Ria Irawan | Drama | NaN | NaN |
207483 rows × 14 columns
Ahora se analizarán las películas exitosas y películas muy malas. Para ello, se mostrarán los deciles que corresponden a rating y número de votos. Con ello, se definirá una película exitosa la que esté sobre el decil 9 en ambas categorías, y una película muy mala será aquella bajo el decil 1 para rating.
movies.sort_values(by="averageRating").quantile(0.9)
startYear 2018.0 runtimeMinutes 121.0 averageRating 7.5 numVotes 2481.0 Name: 0.9, dtype: float64
movies.sort_values(by="numVotes").quantile(0.9)
startYear 2018.0 runtimeMinutes 121.0 averageRating 7.5 numVotes 2481.0 Name: 0.9, dtype: float64
movies.sort_values(by="numVotes").quantile(0.1)
startYear 1951.0 runtimeMinutes 70.0 averageRating 4.3 numVotes 12.0 Name: 0.1, dtype: float64
Vistos los deciles, se define una película exitosa como toda película que tiene un rating promedio de al menos 7.5 y una cantidad de votos mayor o igual a 2481. También, se define una película muy mala como toda película que tenga un rating inferior o igual a 4.3
best_movies = movies[(movies["averageRating"] >= 7.5) & (movies["numVotes"] >= 2481)]
worst_movies = movies[movies["averageRating"] <= 4.3]
A partir de esto, se puede ver qué porcentaje de películas del total puede considerarse exitosa.
round(best_movies.shape[0] / movies.shape[0] * 100, 2)
1.67
Solo el 1.67% de películas puede considerarse exitosa, que corresponde a un porcentaje muy bajo, lo que puede hablar de que es muy difícil crear una película y que ésta sea exitosa.
También, se puede ver qué porcentaje del total de películas corresponde a películas muy malas.
round(worst_movies.shape[0] / movies.shape[0] * 100, 2)
10.77
Un 10.77% de las películas es muy mala, un porcentaje bastante mayor al de películas exitosas, lo que habla de que es mucho más sencillo crear una película muy mala a una que tenga éxito.
Se puede analizar cuantas películas exitosas y cuantas muy malas tienen contenido explícito:
(best_movies[best_movies["isAdult"] == 1].shape[0], worst_movies[worst_movies["isAdult"] == 1].shape[0])
(0, 487)
Hay muchas películas malas en comparación a las exitosas que tienen contenido explícito, lo que puede significar que las películas con contenido explícito tienen mayor probabilidad de no tener éxito. Esto puede deberse a la misma razón mencionada anteriormente, es decir, que estas películas están hechas para un público más pequeño y específico, por lo que no serán bien recibidas por todos y todas.
Luego de realizar el análisis anterior, se puede consultar sobre un top 10 de películas mejor y peor calificadas a partir de 1970, esto último para solo considerar películas contemporáneas.
best_movies1970 = best_movies[best_movies["startYear"] >= 1970]
best_movies1970.sort_values(by="averageRating", ascending=False).head(10)
| tconst | primaryTitle | isAdult | startYear | runtimeMinutes | genres | averageRating | numVotes | directors | writers | actors | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 139637 | tt14923112 | Methagu | 0 | 2021.0 | 100.0 | Biography,History | 9.6 | 8416 | Kittu | Muthu Cheziyan,Kittu,Thirukumaran | Lizzie Antony,Eshwar Basha,Chandrasekar,Kutti ... |
| 86360 | tt0252487 | The Chaos Class | 0 | 1975.0 | 87.0 | Comedy,Drama | 9.3 | 38992 | Ertem Egilmez | Umur Bugay,Rifat Ilgaz | Kemal Sunal,Münir Özkul,Halit Akçatepe,Tarik Akan |
| 56429 | tt0111161 | The Shawshank Redemption | 0 | 1994.0 | 142.0 | Drama | 9.3 | 2455855 | Frank Darabont | Stephen King,Frank Darabont | Tim Robbins,Morgan Freeman,Bob Gunton,William ... |
| 33891 | tt0068646 | The Godfather | 0 | 1972.0 | 175.0 | Crime,Drama | 9.2 | 1699389 | Francis Ford Coppola | Mario Puzo,Francis Ford Coppola | Marlon Brando,Al Pacino,James Caan,Diane Keaton |
| 127660 | tt11737772 | Chal Mera Putt 2 | 0 | 2020.0 | 124.0 | Drama | 9.2 | 2617 | Janjot Singh | Rakesh Dhawan | Amrinder Gill,Simi Chahal,Nasir Chinyoti,Garry... |
| 103705 | tt0383177 | Aguner Poroshmoni | 0 | 1994.0 | 123.0 | Drama,War | 9.1 | 2991 | Humayun Ahmed | Humayun Ahmed | Bipasha Hayat,Asaduzzaman Noor,Abul Hayat,Doll... |
| 184903 | tt5354160 | Mirror Game | 0 | 2016.0 | 147.0 | Crime,Mystery,Thriller | 9.1 | 24835 | Amitabh Reza Chowdhury | Syed Gaosul Alam Shaon,Anam Biswas,Adnan Adib ... | Chanchal Chowdhury,Masuma Rahman Nabila,Partha... |
| 161918 | tt2592910 | CM101MMXI Fundamentals | 0 | 2013.0 | 139.0 | Comedy,Documentary | 9.1 | 44645 | Murat Dündar | Cem Yilmaz | Bilal Kaya,Caner Tüfek |
| 120243 | tt10189514 | Soorarai Pottru | 0 | 2020.0 | 153.0 | Drama | 9.1 | 89377 | Sudha Kongara | Sudha Kongara,Shalini Ushadevi,Vijay Kumar,Aal... | Suriya,Paresh Rawal,Aparna Balamurali,Prakash ... |
| 122101 | tt10534500 | #Home | 0 | 2021.0 | 158.0 | Drama | 9.1 | 7949 | Rojin Thomas | Rojin Thomas | Indrans,Sreenath Bhasi,Manju Pillai,Naslen |
worst_movies1970 = worst_movies[worst_movies["startYear"] >= 1970]
worst_movies1970.sort_values(by="averageRating").head(10)
| tconst | primaryTitle | isAdult | startYear | runtimeMinutes | genres | averageRating | numVotes | directors | writers | actors | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 141178 | tt1538949 | Play in the Gray | 0 | 2009.0 | 85.0 | Biography,Comedy,Documentary | 1.0 | 132 | Kaitlin Meelia | Ian McFarland | Katie Allen,Julee Antonellis,Kate Bornstein,Le... |
| 142980 | tt1611056 | Hito no sabaku | 0 | 2010.0 | 121.0 | Drama | 1.0 | 475 | Hiroshi Gokan,Shinsuke Kurimoto,Paul Young,Dai... | Kôtarô Sawaki,Akiko Amy Kanda,Takashi Hattori,... | Masashi Arifuku,Yôjin Hino,Ben Hiura,Renji Ish... |
| 150620 | tt1945118 | In a Lonely Planet | 0 | 2011.0 | 94.0 | Romance | 1.0 | 459 | Takefumi Tsutsui | Daisuke Miyazaki | Yui Asano,Gô Ayano,Mickey Curtis,Takaaki Ichiyama |
| 120737 | tt1028548 | Konjaku monogatari: The new edition | 0 | 2007.0 | 88.0 | Comedy,Drama | 1.0 | 500 | Mikio Ohkado,Shô Tsukikawa,Yûko Watanabe,Naoki... | Naoki Katô,Michiko Ohishi,Sachiko Tanaka,Kiyoh... | Akino,Azusa,Shusaku Fujiwara,Yinling of Joytoy |
| 153666 | tt2100624 | Good Morning to the World!! | 0 | 2010.0 | 81.0 | Comedy | 1.0 | 458 | Satoru Hirohara | Satoru Hirohara | Miho Arai,Mitsunori Izumi,Shotaro Kaneyama,Kôi... |
| 133641 | tt13232730 | Hearts Are Trump | 0 | 2020.0 | 112.0 | Drama | 1.0 | 21 | Donald James Parker | Donald James Parker | Danielle Superior,William Row,John Goad,Juli T... |
| 199889 | tt7923374 | Badang | 0 | 2018.0 | 105.0 | Action,Comedy,Fantasy | 1.0 | 742 | A. Razak Mohaideen | A. Razak Mohaideen,Azlan Syah,Azhari Mohd Zain | Aliff Syukr,Fasha Sandha,Azhari Mohd Zain,Fauz... |
| 158948 | tt2370036 | On BorrowedTime | 0 | 2012.0 | 60.0 | Documentary | 1.0 | 12 | Emilio Roso | Emilio Roso | Emilio Roso,John Jacobs |
| 134262 | tt13351868 | Bootleg Death Tape III | 0 | 2020.0 | 45.0 | Horror | 1.0 | 8 | Sean Murray,Jamie Robert MacDougall,Ian Boyd,T... | Ian Boyd,Avery Crumley,Valeria Henry,Jamie Rob... | Tony Newton,Avery Crumley,Jamie Robert MacDoug... |
| 202271 | tt8476266 | Teambuilding | 0 | 2018.0 | 83.0 | Comedy | 1.0 | 13 | Ján Novák | Daniela Choderová,Ján Novák | Jan Adámek,Alice Bendová,Hynek Chmelar,Daniela... |
Podemos notar que tanto en las mejores como en las peores hay muchas películas de los últimos años. Esto podría deberse a que en los últimos años se han votado las películas que han salido y esos datos se han guardado en el dataset de forma correcta, lo que puede no haber sido así para películas más antiguas.
A continuación se definen funciones que pueden ser útiles para el análisis de los datos.
# str -> double
# Entrega el porcentaje de películas del género que pertenecen a las mejores películas.
def best_movies_por_genero(genre):
genero = best_movies[pd.Series(best_movies["genres"]).str.contains(genre).tolist()]
return round(genero.shape[0] / best_movies.shape[0] * 100, 2)
# str -> double
# Entrega el porcentaje de películas del género que pertenecen a las peores películas
def worst_movies_por_genero(genre):
genero = worst_movies[pd.Series(worst_movies["genres"]).str.contains(genre).tolist()]
return round(genero.shape[0] / worst_movies.shape[0] * 100, 2)
# str -> boxplot
# Crea un boxplot de los rating de un género
def boxplot_rating_por_genero(genre):
toplox = pd.DataFrame()
genero = movies[pd.Series(movies["genres"]).str.contains(genre).tolist()]
toplox['primaryTitle'] = genero['primaryTitle']
toplox["0"] = (genero[genero["isAdult"] == 0])['averageRating']
toplox["1"] = (genero[genero["isAdult"] == 1])['averageRating']
(toplox[["0", "1"]]).plot(kind="box")
plt.xlabel("¿Es para adultos? (0 = No, 1 = Sí)")
plt.ylabel('Rating')
plt.title(f"Boxplot de ratings para género {genre}")
plt.show()
# str -> boxplot
# Crea un boxplot con la distribución de ratings para una película con género principal genre
# Género principal es el primer género en la lista de géneros
def boxplot_rating_genero_principal(main_genre):
toplox = pd.DataFrame()
toplox['primaryTitle'] = movies_alt[(movies_alt["Genero1"] == main_genre)]['primaryTitle']
toplox["0"] = (movies_alt[(movies_alt["Genero1"] == main_genre) & (movies_alt["isAdult"] == 0)])['averageRating']
toplox["1"] = (movies_alt[(movies_alt["Genero1"] == main_genre) & (movies_alt["isAdult"] == 1)])['averageRating']
(toplox[["0", "1"]]).plot(kind="box")
plt.xlabel("¿Es para adultos? (0 = No, 1 = Sí)")
plt.ylabel('Rating')
plt.title(f"Boxplot de ratings para género principal {main_genre}")
plt.show()
# str -> plt
# Crea un gráfico de dispersión que muestra la relación entre rating y duración para películas de un género
def dispersion_rating_duracion(genre):
toplox = pd.DataFrame()
genero = movies[pd.Series(movies["genres"]).str.contains(genre).tolist()]
toplox['primaryTitle'] = genero['primaryTitle']
toplox["D"] = (movies[movies["genres"] == genre])['runtimeMinutes']
toplox["R"] = (movies[movies["genres"] == genre])['averageRating']
(toplox[["D", "R"]]).plot(kind="scatter", x="D", y="R")
plt.xlabel("Duración")
plt.ylabel('Rating')
plt.title(f"Dispersión entre rating y duración para el género {genre}")
plt.xlim(0, 600)
plt.show()
# str -> plt
# Crea un gráfico de dispersión que muestra la relación entre rating y cantidad de votos para películas de un género
def dispersion_rating_votos(genre):
toplox = pd.DataFrame()
genero = movies[pd.Series(movies["genres"]).str.contains(genre).tolist()]
toplox['primaryTitle'] = genero['primaryTitle']
toplox["V"] = (movies[movies["genres"] == genre])['numVotes']
toplox["R"] = (movies[movies["genres"] == genre])['averageRating']
(toplox[["V", "R"]]).plot(kind="scatter", x="V", y="R")
plt.xlabel("Número de votos")
plt.ylabel('Rating')
plt.title(f"Dispersión entre rating y votos para género {genre}")
plt.show()
Luego de haber definido las funciones, se pueden realizar algunas consultas interesantes, como ver qué porcentaje de películas exitosas o muy malas corresponde a cierto género.
Porcentaje de películas de un género que son exitosas.
print(f'Action: {best_movies_por_genero("Action")}%')
print(f'Drama: {best_movies_por_genero("Drama")}%')
print(f'Crime: {best_movies_por_genero("Crime")}%')
print(f'Comedy: {best_movies_por_genero("Comedy")}%')
print(f'Romance: {best_movies_por_genero("Romance")}%')
print(f'Mystery: {best_movies_por_genero("Mystery")}%')
print(f'Family: {best_movies_por_genero("Family")}%')
print(f'Adult: {best_movies_por_genero("Adult")}%')
print(f'Sport: {best_movies_por_genero("Sport")}%')
print(f'Sci-Fi: {best_movies_por_genero("Sci-Fi")}%')
Action: 15.46% Drama: 74.78% Crime: 17.74% Comedy: 27.93% Romance: 18.97% Mystery: 6.48% Family: 3.97% Adult: 0.0% Sport: 1.87% Sci-Fi: 2.79%
Porcentaje de películas de un género que son muy malas.
print(f'Action: {worst_movies_por_genero("Action")}%')
print(f'Drama: {worst_movies_por_genero("Drama")}%')
print(f'Crime: {worst_movies_por_genero("Crime")}%')
print(f'Comedy: {worst_movies_por_genero("Comedy")}%')
print(f'Romance: {worst_movies_por_genero("Romance")}%')
print(f'Mystery: {worst_movies_por_genero("Mystery")}%')
print(f'Family: {worst_movies_por_genero("Family")}%')
print(f'Adult: {worst_movies_por_genero("Adult")}%')
print(f'Sport: {worst_movies_por_genero("Sport")}%')
print(f'Sci-Fi: {worst_movies_por_genero("Sci-Fi")}%')
Action: 17.86% Drama: 30.66% Crime: 8.43% Comedy: 30.29% Romance: 8.42% Mystery: 4.82% Family: 3.2% Adult: 2.01% Sport: 0.65% Sci-Fi: 7.14%
Comparando ambos resultados, lo primero que se destaca es que no hay películas para adultos exitosas y sí lo hay en películas muy malas, lo que confirma lo dicho en un punto anterior. También, en ambos casos el género “Drama” es el que más aparece, seguido por “Comedy”, y por "Romance" en las películas exitosas y “Action” en las películas malas, esto puede dar a entender que se realizan muchas películas de estos géneros porque son géneros populares, y por ende hay muchas que son tanto exitosas como muy malas. Se puede notar que hay un alto porcentaje de películas del género “Romance” que son exitosas, sin embargo este número disminuye bastante en las películas muy malas, lo mismo ocurre con “Crime”.
Se realizará un gráfico de densidad que mostrará cómo se distribuyen los rating para todas las películas.
pd.DataFrame(movies['averageRating']).plot(kind='density')
plt.ylabel('Rating')
plt.title(f"Densidad de rating para todas las películas")
plt.show()
# (promedio, desviación estándar)
(movies['averageRating'].mean(), movies['averageRating'].std())
(6.001914373707608, 1.275618572859004)
Analizando el gráfico junto con el promedio y desviación estándar para el rating, se puede notar que el gráfico se asemeja bastante a una distribución normal con media 6 y desviación estándar 1.28. El valor de la densidad aumenta levemente hasta llegar a su máximo, para luego caer rápidamente, esto significa que hay muy poca cantidad de películas con rating sobre el promedio en comparación a películas con rating bajo el promedio.
Se crearán boxplots de ratings para géneros utilizando dos métodos. Primero, se creará un boxplot si es que la película es de un único género, y segundo, en caso de que el título esté asociado a más de un género, se creará un boxplot solo considerando el primer género de la lista, esto para ver si hay mayores diferencias a cuando el género es único o no. Se analizará para los géneros “Action”, “Drama” y “Comedy” y se diferenciará por contenido explícito.
Boxplots para “Action”
boxplot_rating_por_genero("Action")
boxplot_rating_genero_principal("Action")
En este caso, no se ven cambios respecto a cuando “Action” es el género principal de una película o no.
Boxplots para “Drama”
boxplot_rating_por_genero("Drama")
boxplot_rating_genero_principal("Drama")
En este caso, si la película no es para adultos no se ven cambios, pero de serlo, la distribución cambia, ésta se traslada un poco hacia abajo, lo que significa que el primer, segundo y tercer cuartil de ratings son más bajos.
Boxplots para “Comedy”
boxplot_rating_por_genero("Comedy")
boxplot_rating_genero_principal("Comedy")
En este último caso se ven cambios tanto si la película es para adultos como si no. Cuando no es para adultos, el tercer cuartil se desplaza un poco para abajo y hay más outliers superiores, mientras que cuando es para adultos, el boxplot se desplaza bastante hacia abajo, y se puede concluir lo mismo que para el género anterior.
Se pueden realizar gráficos de dispersión para analizar la relación entre ciertas variables. En particular, se analizará la relación entre rating y duración, y rating y cantidad de votos para los géneros “Action”, “Drama”, “Comedy”.
Gráficos de dispersión entre rating y duración.
dispersion_rating_duracion("Action")
dispersion_rating_duracion("Drama")
dispersion_rating_duracion("Comedy")
Luego de realizar el gráfico de dispersión entre rating y duración de las películas para los 3 géneros, se puede notar que los 3 son bastante similiares, aunque en el caso de "Drama" hay varios puntos con un rating relativamente alto que tienen mayor duración. Para ninguno de los 3 casos parece haber una relación clara entre las variables.
Gráficos de duración entre rating y cantidad de votos.
dispersion_rating_votos("Action")
dispersion_rating_votos("Drama")
dispersion_rating_votos("Comedy")
Con los gráficos de dispersión entre rating y cantidad de votos se puede ver una clara tendencia, a partir del rating 5.0 aproximadamente comienza a haber un mayor número de votos, lo cual hace sentido, pues si una película es bien calificada más gente se verá interesada en seguir votándola.
Con los valores numéricos del dataset se puede crear una matriz de correlación. Para ver si el título de la película tiene alguna correlación con otra variable se usará el largo del título.
df = pd.DataFrame()
df["length"] = movies["primaryTitle"].apply(len)
df["startYear"] = movies["startYear"]
df["runtimeMinutes"] = movies["runtimeMinutes"]
df["averageRating"] = movies["averageRating"]
df["numVotes"] = movies["numVotes"]
corr_df = df.corr()
plt.title("Correlaciones para la tabla 'movies'")
sns.heatmap(corr_df, annot=True)
plt.show()
La mayor correlación que existe es entre el rating y el número de votos, lo que es consistente con lo dicho anteriormente, pero aun así sigue siendo un valor extremadamente pequeño (ni siquiera 0.1). Entonces, se puede decir que todas las variables son prácticamente independientes entre sí.
Luego de haber agregado las columnas correspondientes a directores, escritores y actores para cada una de las películas, ahora se reformularán las preguntas y problemas. Para estas preguntas se definió éxito como la valoración y popularidad como la cantidad de votos.
¿Es posible predecir el éxito de una película al resto de sus atributos?
¿Existe una relación entre el género y los clusters formados a partir de los atributos de la base de datos?
¿Es posible predecir la popularidad de una película en base a la presencia de un actor en específico y el resto de sus atributos?
Antes de responder las preguntas, se definirán algunas funciones que serán de utilidad.
Para las funciones de etiquetado se utilizarán los cuartiles de cada atributo. Por ejemplo, el cuartil 1 del rating es 5.3, el cuartil 2 es 6.1 y el cuartil 3 es 6.9. De esta forma, quedan 4 clases bastante equilibradas y por lo tanto no es necesario realizar over/sub sampling.
La función run_classifier corre un clasificador cierto número de veces y va guardando sus resultados en un arreglo.
La función sim_matrix crea la matriz de similitud.
La función plot será de utilidad para graficar la matriz de similitud.
def etiquetarRating(rating):
# Películas horribles
if rating <= 5.3:
return 0
# Películas malas
elif rating <= 6.1:
return 1
# Películas promedio
elif rating <= 6.9:
return 2
# Películas excelentes
else:
return 3
def etiquetarDuracion(minutos):
# Películas muy cortas
if minutos <= 83:
return 0
# Películas cortas
elif minutos <= 92:
return 1
# Películas promedio
elif minutos <= 104:
return 2
# Películas largas
else:
return 3
def etiquetarAnio(anio):
# Películas antiguas
if anio <= 1973:
return 0
# Películas clásicas
elif anio <= 2000:
return 1
# Películas actuales
elif anio <= 2013:
return 2
# Películas nuevas
else:
return 3
def etiquetarPopularidad(numVotos):
# Películas nada conocidas
if numVotos <= 30:
return 0
# Películas más o menos conocidas
elif numVotos <= 120:
return 1
# Películas conocidas
elif numVotos <= 700:
return 2
# Películas famosas
else:
return 3
def etiquetarLargoTitulo(titulo):
# Películas de nombre corto
if titulo <= 10:
return 0
# Películas de nombre normal
elif titulo <= 14:
return 1
# Películas de nombre largo
elif titulo <= 20:
return 2
# Películas de nombre muy largo
else:
return 3
def run_classifier(clf, X, y, num_tests=10, test_size=.33):
metrics = {"precision": [], "recall": [], "f1-score": []}
for _ in range(num_tests):
print(f"{_}/{num_tests}", end="\r")
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size)
clf.fit(X_train, y_train.values.ravel())
predictions = clf.predict(X_test)
metrics["y_pred"] = predictions
metrics["precision"].append(precision_score(y_test, predictions, average='macro', zero_division=0))
metrics["recall"].append(recall_score(y_test, predictions, average='macro'))
metrics["f1-score"].append(f1_score(y_test, predictions, average='macro'))
return metrics
def sim_matrix(features, labels):
useful_labels = labels >= 0
indices = np.argsort(labels[useful_labels])
sorted_features = features[useful_labels][indices]
d = euclidean_distances(sorted_features, sorted_features)
return d
def plot(data, model):
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20,5))
fig.suptitle(f"{model.__class__.__name__}")
ax1.scatter(data[:,0], data[:,1], c=model.labels_)
dist = sim_matrix(data, model.labels_)
im = ax2.imshow(dist, cmap="jet")
fig.colorbar(im, ax=ax2)
Se etiquetan los directores y géneros. Para ello, se crea una tabla en donde, si una película tiene m directores y n géneros, esa fila se separará en m*n filas en la nueva tabla.
# Etiqueta rating
movies["etiquetaRating"] = movies["averageRating"].apply(etiquetarRating)
etiquetas = movies[["tconst", "directors", "genres", "numVotes", "primaryTitle", "runtimeMinutes"]]
# Etiqueta directores
etiquetas["directors"] = etiquetas["directors"].str.split(",")
etiquetas = etiquetas.explode("directors")
etiquetaDirector = preprocessing.LabelEncoder()
etiquetaDirector.fit(etiquetas.directors)
etiquetas["etiquetaDirector"] = etiquetaDirector.transform(etiquetas.directors)
# Etiqueta géneros
etiquetas["genres"] = etiquetas["genres"].str.split(",")
etiquetas = etiquetas.explode("genres")
etiquetaGenero = preprocessing.LabelEncoder()
etiquetaGenero.fit(etiquetas.genres)
etiquetas["etiquetaGenero"] = etiquetaGenero.transform(etiquetas.genres)
etiquetas = pd.merge(etiquetas, movies[['tconst', 'etiquetaRating']], on='tconst')
Luego, se agregan las etiquetas de popularidad, largo del título y duración para cada fila.
etiquetas["etiquetaPopularidad"] = etiquetas["numVotes"].apply(etiquetarPopularidad)
etiquetas["largoTitulo"] = etiquetas["primaryTitle"].apply(lambda x: etiquetarLargoTitulo(len(x)))
etiquetas["etiquetaDuracion"] = etiquetas["runtimeMinutes"].apply(etiquetarDuracion)
Luego de haber etiquetado los datos necesarios, se clasificarán los datos utilizando varios métodos y calculando su precision, recall y F1-Score. Si alguno de estos valores entrega un valor mayor o igual a un 75% se dirá que sí se puede predecir.
La idea a seguir será realizar un cierto número de tests con distintos training set, y el valor de precision, recall y F1-Score será el promedio de todos los valores calculados.
Con lo siguiente se puede ver que las clases de las últimas 3 etiquetas asignadas están bien balanceadas:
etiquetas["etiquetaPopularidad"].value_counts()
0 107700 2 106410 1 104221 3 104153 Name: etiquetaPopularidad, dtype: int64
etiquetas["largoTitulo"].value_counts()
2 121903 0 104048 3 101196 1 95337 Name: largoTitulo, dtype: int64
etiquetas["etiquetaDuracion"].value_counts()
3 109678 0 105917 1 103866 2 103023 Name: etiquetaDuracion, dtype: int64
Se realizará la predicción del éxito usando las etiquetas del director, del género, de duración, del largo del título y de la popularidad. No se utilizan ni actores ni escritores porque no son relevantes para el estudio, y tampoco si la película tiene contenido para adultos o no, porque hay un gran desbalance entre ambas clases.
Se utilizará un Dummy Classifier, un Decision Tree, K-Nearest Neighbors y Gaussian Naive-Bayes para realizar la predicción.
X = etiquetas[['etiquetaDirector', 'etiquetaGenero', "etiquetaDuracion", "largoTitulo", "numVotes"]]
y = etiquetas[['etiquetaRating']]
c0 = ("Base Dummy", DummyClassifier(strategy="stratified"))
c1 = ("Decision Tree", DecisionTreeClassifier(max_depth=5))
c2 = ("K-Nearest Neighbors", KNeighborsClassifier(n_neighbors=10))
c3 = ("Gaussian Naive Bayes", GaussianNB())
classifiers = [c0, c1, c2, c3]
for name, clf in classifiers:
metrics = run_classifier(clf, X, y)
print(f"{name}")
print("--------------------------")
print(f"Precision promedio: {np.array(metrics['precision']).mean()}")
print(f"Recall promedio: {np.array(metrics['recall']).mean()}")
print(f"F1-Score promedio: {np.array(metrics['f1-score']).mean()}")
print("--------------------------\n\n")
Base Dummy -------------------------- Precision promedio: 0.2497415496254519 Recall promedio: 0.24973822997939532 F1-Score promedio: 0.24973697972920728 -------------------------- Decision Tree -------------------------- Precision promedio: 0.3058635706634917 Recall promedio: 0.351645145897277 F1-Score promedio: 0.30723483667345886 -------------------------- K-Nearest Neighbors -------------------------- Precision promedio: 0.41114579638428583 Recall promedio: 0.40817228610803147 F1-Score promedio: 0.40760386128253545 -------------------------- Gaussian Naive Bayes -------------------------- Precision promedio: 0.33063289364352494 Recall promedio: 0.26753650336066415 F1-Score promedio: 0.14936150184571495 --------------------------
Luego de probar con distintos clasificadores, es evidente que no es posible predecir el éxito de una película en base a sus atributos, pues el mejor resultado es solo un 41%.
Para generar los clusters, se etiquetaron el año, la duración, los géneros, el rating y la cantidad de votos. La nueva tabla quedó con estos atributos y con todos los datos etiquetados.
movies_p2 = movies.copy()
movies_p2 = movies.drop(columns=['tconst','primaryTitle','directors', 'writers', 'actors'])
movies_p2["startYear"] = movies_p2["startYear"].apply(etiquetarAnio)
movies_p2["runtimeMinutes"] = movies_p2["runtimeMinutes"].apply(etiquetarDuracion)
# Etiqueta géneros
movies_p2["genres"] = movies_p2["genres"].str.split(",")
movies_p2 = movies_p2.explode("genres")
etiquetaGenero = preprocessing.LabelEncoder()
etiquetaGenero.fit(movies_p2.genres)
movies_p2["genres"] = etiquetaGenero.transform(movies_p2.genres)
# Etiqueta de rating y de votos
movies_p2["averageRating"] = movies_p2["averageRating"].apply(etiquetarRating)
movies_p2["numVotes"] = movies_p2["numVotes"].apply(etiquetarPopularidad)
size = movies_p2.shape[0]
Ahora con los datos etiquetados se procedió a generar los clusters usando K-means. Para esto, se hizo el gráfico de SSE de 1 a 15 clusters, y con ello usar el método del codo para elegir la cantidad de clusters óptima.
X = movies_p2
sse = []
clusters = list(range(1, 16))
for k in clusters:
kmeans = KMeans(n_clusters=k).fit(X)
sse.append(kmeans.inertia_)
plt.plot(clusters, sse, marker="o")
plt.title("Método del codo de 1 a 15 clusters")
plt.grid(True)
plt.show()
Al ver el gráfico del método del codo se puede notar que el número óptimo de clusters es 2, por lo que se generaron los 2 clusters usando K-means.
new_X = PCA(n_components=2, random_state=0).fit_transform(X)
kmeans = KMeans(n_clusters=2, random_state=20).fit(movies_p2)
kmeans.fit(X)
kpredict = kmeans.predict(X)
plt.scatter(new_X[:, 0], new_X[:, 1], c=kmeans.labels_)
plt.title("Representación de clusters, en 2 dimensiones")
plt.show()
Visualmente no se ve ningún cluster claro, aunque tampoco parece muy correcta la división en 2 clusters.
Finalmente se usó el coeficiente de Silhouette para evaluar los clusters generados.
print("Dataset X K-Means 2\t", silhouette_score(X, kmeans.labels_))
Dataset X K-Means 2 0.5886217357191276
Según este resultado y a partir del gráfico se puede concluir que K-means no es el mejor método para generar los clusters.
Para la siguiente parte se trataron de generar los clusters usando clustering jerárquico aglomerativo. Sin embargo, dadas las dimensiones del dataframe no se logró hacer esto con todos los datos, ya que se caía con un MemoryError. Entonces, se decidió evaluar con solo una parte de los datos.
Luego de probar con particiones más grandes y fallar con el mismo error anterior se decidió generar los clusters con un 1% de los datos. A continuación se muestran los resultados:
Y = movies_p2.sample(int(size*0.01))
complete = linkage(Y, method="complete")
single = linkage(Y, method="single")
average = linkage(Y, method="average")
ward = linkage(Y, method="ward")
dendrogram(complete)
plt.title("Linkage: Complete")
plt.show()
dendrogram(single)
plt.title("Linkage: Single")
plt.show()
dendrogram(average)
plt.title("Linkage: Average")
plt.show()
dendrogram(ward)
plt.title("Linkage: Ward")
plt.show()
A partir de estos gráficos se generaron los distintos clusters y se evaluaron con la matriz de similitud, excepto para linkage single ya que en este caso no se pueden apreciar los grupos correctamente.
new_Y = PCA(n_components=2, random_state=0).fit_transform(Y)
agc_complete = AgglomerativeClustering(n_clusters=None,linkage="complete",distance_threshold=8).fit(Y)
print("Aglomerative clustering, complete linkage: clusters generated=", agc_complete.n_clusters_)
print(pd.DataFrame(agc_complete.labels_).stack().value_counts())
complete = AgglomerativeClustering(linkage="complete", n_clusters=8).fit(new_Y)
plot(new_Y, complete)
plt.show()
Aglomerative clustering, complete linkage: clusters generated= 7 0 1305 1 890 5 510 4 329 2 318 6 274 3 154 dtype: int64
agc_average = AgglomerativeClustering(n_clusters=None,linkage="average",distance_threshold=4).fit(Y)
print("Aglomerative clustering, average linkage: clusters generated=", agc_average.n_clusters_)
print(pd.DataFrame(agc_average.labels_).stack().value_counts())
average = AgglomerativeClustering(linkage="average", n_clusters=9).fit(new_Y)
plot(new_Y, average)
plt.show()
Aglomerative clustering, average linkage: clusters generated= 11 5 1328 0 890 3 374 1 327 2 254 4 185 6 132 8 114 10 96 9 77 7 3 dtype: int64
agc_ward = AgglomerativeClustering(n_clusters=None,linkage="ward",distance_threshold=50).fit(Y)
print("Aglomerative clustering, ward linkage: clusters generated=", agc_ward.n_clusters_)
print(pd.DataFrame(agc_ward.labels_).stack().value_counts())
ward = AgglomerativeClustering(linkage="ward", n_clusters=7).fit(new_Y)
plot(new_Y, ward)
plt.show()
Aglomerative clustering, ward linkage: clusters generated= 8 0 872 1 488 6 455 3 455 7 435 2 428 5 327 4 320 dtype: int64
A partir de los resultados de clustering jerárquico aglomerativo se puede concluir que este método tampoco fue capaz de generar los clusters correctamente. A pesar de que los clusters se separan claramente en general, como se puede ver en la matriz de similitud, no se puede apreciar ningún patrón entre los clusters generados. Vale recordar que puede ser que este resultado se vea afectado por solo estar considerando el 1% de los datos y no el total.
Finalmente, se usará el método DBSCAN para generar los clusters. Por el mismo problema anterior esto se hace con solo un 10% de los datos y nuevamente se evaluará con el coeficiente de silhouette.
Y = movies_p2.sample(int(size*0.1))
new_Y = PCA(n_components=2, random_state=0).fit_transform(Y)
nbrs = NearestNeighbors(n_neighbors=5).fit(new_Y)
distances, indices = nbrs.kneighbors(new_Y)
distances = np.sort(distances, axis=0)
distances = distances[:,1]
plt.axhline(y=0.01, color='r', linestyle='--') #Ajuste el valor para "y" en esta línea
plt.ylim(0,0.0000002)
plt.plot(distances)
[<matplotlib.lines.Line2D at 0x2939cb60490>]
Como se puede notar, el eps es 0, por lo que podemos inferir que los datos están repartidos sin orden alguno, y es por esto que no hay clusters en este dataset. De igual manera se continuará con DBSCAN y un eps muy pequeño.
Y = movies_p2.sample(int(size*0.1))
new_Y = PCA(n_components=2, random_state=0).fit_transform(Y)
eps = 0.001
min_samples = 10
dbscan = DBSCAN(eps=eps, min_samples=min_samples).fit(new_Y)
plt.scatter(new_Y[:, 0], new_Y[:, 1], c=dbscan.labels_)
plt.show()
_filter_label = dbscan.labels_ >= 0
print("Dataset Y DBSCAN\t", silhouette_score(new_Y[_filter_label], dbscan.labels_[_filter_label]))
Dataset Y DBSCAN 0.9999998470064091
Este resultado se produjo tras elegir un eps muy pequeño, con el cual se interpreta como que cada punto es su propio cluster, y que por ende en realidad no hay ningún cluster, por lo que el hecho de que este coeficiente tienda a 1 significa que los clusters están bien asignados, y entonces los datos debiesen estar distribuidos de manera aleatoria. También, hay que considerar que solo se tomó el 10% de los datos, por lo que puede ser que este resultado no sea fiable completamente.
Para realizar el pre-procesamiento necesario para esta pregunta se define la función etiquetarPresenciaActor, que lo que hace es etiquetar si un actor está presente en una película.
Para ello, se pasa el nombre del actor, el año en que inició su carrera y el año en que la terminó, ambos parámetros opcionales. Las películas anteriores a cuando comenzó su carrera y posteriores a cuando terminó su carrera directamente se sabe que el actor no está, entonces luego solo basta recorrer las que quedaron viendo si el actor está o no. Todo este proceso se hace por razones de eficiencia.
def etiquetarPresenciaActor(nombre, inicio_carrera=1800, fin_carrera=3000):
def etiquetarAnioPorActor(anio):
return 1 if inicio_carrera <= anio and anio <= fin_carrera else 0
moviesActor = movies.copy()
moviesActor["actors"] = moviesActor["actors"].astype(str)
moviesActor["etiquetaActor"] = moviesActor["startYear"]
moviesActor["etiquetaActor"] = moviesActor["etiquetaActor"].apply(etiquetarAnioPorActor)
moviesActor = moviesActor.loc[moviesActor['etiquetaActor'] == 1]
actorsDf = moviesActor[["tconst", "actors"]]
actorsDf["etiquetaActor"] = actorsDf["actors"].apply(lambda x: nombre in x)
actorsDf = actorsDf.drop(columns=["actors"])
moviesCopy = movies.copy()
moviesActor = pd.merge(moviesCopy, actorsDf, on='tconst', how="left")
moviesActor['etiquetaActor'] = moviesActor["etiquetaActor"].fillna(False)
moviesActor["largoTitulo"] = moviesActor["primaryTitle"].apply(lambda x: etiquetarLargoTitulo(len(x)))
moviesActor["etiquetaDuracion"] = moviesActor["runtimeMinutes"].apply(etiquetarDuracion)
moviesActor["etiquetaAnio"] = moviesActor["startYear"].apply(etiquetarAnio)
moviesActor["etiquetaPopularidad"] = moviesActor["numVotes"].apply(etiquetarPopularidad)
return moviesActor
Se puede analizar para cualquier nombre de actor, a modo de ejemplo, se hace con Brad Pitt que empezó su carrera en el año 1987 y a día de hoy no ha terminado.
Se usarán los mismos clasificadores que en la pregunta 1 para realizar la predicción. Si alguno de los valores es mayor o igual a 75% entonces se dirá que se puede predecir, en caso contrario no lo será.
Se utilizará la etiqueta de la presencia de un actor junto a la etiquetas del largo del título, del año, de la duración y del éxito.
tabla = etiquetarPresenciaActor("Brad Pitt", 1987)
X = tabla[['etiquetaActor', 'largoTitulo', "etiquetaAnio", "etiquetaDuracion", "etiquetaRating"]]
y = tabla[['etiquetaPopularidad']]
c0 = ("Base Dummy", DummyClassifier(strategy="stratified"))
c1 = ("Decision Tree", DecisionTreeClassifier(max_depth=5))
c2 = ("K-Nearest Neighbors", KNeighborsClassifier(n_neighbors=10))
c3 = ("Gaussian Naive Bayes", GaussianNB())
classifiers = [c0, c1, c2, c3]
for name, clf in classifiers:
metrics = run_classifier(clf, X, y)
print(f"{name}")
print("--------------------------")
print(f"Precision promedio: {np.array(metrics['precision']).mean()}")
print(f"Recall promedio: {np.array(metrics['recall']).mean()}")
print(f"F1-Score promedio: {np.array(metrics['f1-score']).mean()}")
print("--------------------------\n\n")
Base Dummy -------------------------- Precision promedio: 0.2499382938572004 Recall promedio: 0.2499420547042679 F1-Score promedio: 0.24992896131545192 -------------------------- Decision Tree -------------------------- Precision promedio: 0.3595157716316776 Recall promedio: 0.36752153327103304 F1-Score promedio: 0.351533932493619 -------------------------- K-Nearest Neighbors -------------------------- Precision promedio: 0.3253361100656407 Recall promedio: 0.32243938896406255 F1-Score promedio: 0.31893289295092125 -------------------------- Gaussian Naive Bayes -------------------------- Precision promedio: 0.42018996357229615 Recall promedio: 0.29509764811042855 F1-Score promedio: 0.21064428689119316 --------------------------
Luego de haber probado con los distintos clasificadores, es claro que no se puede saber si la presencia de Brad Pitt junto a otros atributos es relevante a la hora de predecir la popularidad de una película, pues el mejor resultado fue de un 42%.
Al obtener el dataset, no hubieron mayores problemas a la hora de hacer la limpieza de los datos ni la exploración, pues los métodos de pandas para limpiar, obtener un resumen de los datos, y a partir de ahí explorar, facilitaban mucho el trabajo. La exploración de datos aporta mucha información sobre cómo es el dataset y qué datos contiene.
Por otro lado, al momento de realizar los experimentos, existió un problema que fue crucial. Como son más de 200 mil tuplas, para realizar el Clustering Jerárquico Aglomerativo y DBSCAN no se pudo trabajar con los datos completos, sino solo con samples muy pequeños (1% y 10% de los datos, respectivamente), lo que hace que los resultados obtenidos muy posiblemente no sean los esperados.
Se realizó un buen trabajo de etiquetado de los datos para obtener clases balanceadas y no verse en la necesidad de usar over/sub sampling, pues al utilizar los cuartiles de cada uno de los atributos a etiquetar, todas las clases quedaron con aproximadamente la misma cantidad.
Los mejor podría haber sido comenzar realizando la limpieza de los datos y el etiquetado, para luego quitar filas de cada una de las clases y de esa forma obtener un dataset final más pequeño, con todas las clases balanceadas, para poder correr todos los algoritmos de clustering con todo el dataset y obtener resultados más fiables.
Si se contara con más tiempo, se podría explorar el rendimiento de los modelos haciendo una selección más prudente de los atributos, en vez de utilizar la mayor cantidad, con el fin de obtener mejores resultados que sean más cercanos a la realidad. Tambien, en este proyecto se consideraron sólo aquellas filas correspondientes a películas. Sería interesante también trabajar con otros tipos de metrajes que se encuentran en el dataset y que no fueron considerados, como documentales, series, etc. Con esto, se podría explorar preguntas relacionadas al tipo de metraje, como por ejemplo si para cierto género suele ser más atractivo un tipo u otro, o comparar el rendimiento entre directores que crean un sólo tipo de metraje versus los que crean de varios.
Hito 1: Obtención y limpieza de datos. Presentación.
Hito 2: Preprocesamiento de los datos. Propuestas experimentales. Corregir hito 1 según feedback.
Hito 3: Etiquetado de datos. Desarrollo de las preguntas 1 y 3. Planificación futura. Presentación.
Hito 1: Gráficos de dispersión, largo de titulo para matriz de correlación, formulación de preguntas y problemas. Presentación.
Hito 2: Preprocesamiento de los datos. Propuestas experimentales. Corregir hito 1 según feedback.
Hito 3: Etiquetado de datos. Desarrollo pregunta 2. Presentación.
Hito 1: Análisis de mejores y peores películas . Funciones para gráficos. Preguntas y problemas. Presentación.
Hito 2: Preprocesamiento de los datos. Diseño de clasificadores. Propuestas experimentales. Corregir hito 1 según feedback.
Hito 3: Etiquetado de datos. Desarrollo de las preguntas 1 y 3. Conclusiones generales. Presentación. Mejoras a página web.
Hito 1: Análisis y exploración de datos, formulación de preguntas y problemas. Presentación.
Hito 2: Traducción preprocesamiento de datos hito 1 de R a python. Diapositivas. Propuestas experimentales. Corregir hito 1 según feedback.
Hito 3: Etiquetado de datos. Desarrollo pregunta 2. Diapositivas y presentación.