Data Scientist @ Kernel Analytics
Objetivos de hoy:
mystring = '¡Hola Pyladies Madrid!'
mystring2 = "¡Hola 'Pyladies Madrid'!"
print(mystring2)
¡Hola 'Pyladies Madrid'!
print(str(len(mystring)) + ' longitud de la cadena')
print(str(mystring.index("o")) + ' posición') #primera ocurrencia
print(str(mystring.count("a")) + ' veces') #contar el número de ocurrencias
22 longitud de la cadena 2 posición 3 veces
print(mystring.upper())
print(mystring.lower())
¡HOLA PYLADIES MADRID! ¡hola pyladies madrid!
print(mystring[4])
print(mystring[1:5])
print(mystring[::-1])
a Hola !dirdaM seidalyP aloH¡
Más información: Basic String Operations
Normalizar: el texto consiste en unificar la forma de los textos. Para ello hay distintas operaciones y algoritmos que pueden ayudarnos.
¿Qué es mejor lematizar o hacer stemming? ¿qué algoritmos funcionan mejor?
Operaciones con el texto normalizado:
(las tres soportan multiples idiomas, quizá SpaCy es la más limitada porque lleva menos años disponible, aunque todas soportan el castellano)
conda install -c conda-forge spacy
python -m spacy download es
conda install -c anaconda nltk
conda install -c anaconda gensim
# nltk.download()
Importamos las librerías necesarias
import numpy as np
import pandas as pd
from IPython.display import display, HTML
import nltk
import spacy
import gensim
import re
import os
import codecs
from sklearn import feature_extraction
import seaborn as sns
import matplotlib.pyplot as plt
Cargamos el modelo en español para SpaCy
import spacy
import es_core_news_sm
nlp = es_core_news_sm.load()
Importamos el fichero de texto que contiene la sinopsis para las películas nominadas desde la edición 14 de los premios.
Para obtener esta información he escrapeado la web oficial de los Premios Goya
info_pelis = pd.read_csv("sinopsis_nominados_goya.csv",encoding='latin-1')
Al hacer el scrapping de la web el texto contiene algunas cosas que nos gustaría eliminar antes de comenzar
def clean_Goya(tit):
return(' '.join(tit.split()))
info_pelis['titulo'] = info_pelis.titulo.apply(clean_Goya)
info_pelis['descripcion'] = info_pelis.descripcion.apply(clean_Goya)
Cargamos el fichero que contiene si las películas han sido nominadas o ganadoras en la categoría mejor película o mejor dirección y añadimos esas columnas a nuestro dataframe original
premios = pd.read_csv("titulo_edicion.csv", encoding='latin-1', sep=';')
info_pelis = pd.merge(info_pelis, premios[['titulo','edicion','nom_mejor_pelicula','gana_mejor_pelicula','nom_mejor_direc','gana_mejor_direc']], on = ['titulo', 'edicion'], how='inner')
info_nominadas = info_pelis[(info_pelis.nom_mejor_pelicula == 1) | (info_pelis.nom_mejor_direc==1)]
print(str(len(info_nominadas)) + ' peliculas nominadas a mejor película o a mejor dirección')
82 peliculas nominadas a mejor película o a mejor dirección
display(info_pelis.head())
descripcion | edicion | titulo | url_peli | nom_mejor_pelicula | gana_mejor_pelicula | nom_mejor_direc | gana_mejor_direc | |
---|---|---|---|---|---|---|---|---|
0 | Álvaro quiere ser escritor, pero todo lo que e... | 32 | El autor | https://www.premiosgoya.com/pelicula/el-autor | 1.0 | 0.0 | 1.0 | 0.0 |
1 | Frida, una niña de seis años, afronta el prime... | 32 | Estiu 1993 | https://www.premiosgoya.com/pelicula/estiu-1993 | 1.0 | 0.0 | 0.0 | 0.0 |
2 | Tras haber luchado en la Primera Guerra Carlis... | 32 | Handia | https://www.premiosgoya.com/pelicula/handia | 1.0 | 0.0 | 1.0 | 0.0 |
3 | A finales de los años 50 Florence Green decide... | 32 | La librería | https://www.premiosgoya.com/pelicula/la-libreria | 1.0 | 1.0 | 1.0 | 1.0 |
4 | Madrid, años 90. En plena noche, la policía re... | 32 | Verónica | https://www.premiosgoya.com/pelicula/veronica | 1.0 | 0.0 | 1.0 | 0.0 |
Este es el tipo de información con la que vamos a trabajar:
print(info_nominadas.iloc[0].titulo)
print('............')
print(info_nominadas.iloc[0].descripcion)
El autor ............ Álvaro quiere ser escritor, pero todo lo que escribe es falso, pretencioso, insípido. Trabaja como escribiente en una notaría de Sevilla y su vida es gris, coloreada sólo por sus sueños. Su mujer, Amanda, es todo lo contrario. Siempre ha tenido los pies en la tierra y nunca ha soñado con ser escritora. Sin embargo, es ella la que se pone a escribir y le sale un best-seller. Ironías de la vida. La separación es inevitable. Y en ese momento, Álvaro decide afrontar su sueño: escribir una gran novela. Pero es incapaz; no tiene talento ni imaginación. Guiado por Juan, su profesor de escritura, indaga en los fundamentos de la novela, hasta que un día descubre que la ficción se escribe con la realidad. Álvaro comienza a manipular a sus vecinos y amistades para crear una historia, una historia real que supera a la ficción.
print(info_nominadas.iloc[1].titulo)
print('............')
print(info_nominadas.iloc[1].descripcion)
Estiu 1993 ............ Frida, una niña de seis años, afronta el primer verano de su vida con su nueva família adoptiva, tras la muerte de su madre.
Vamos a localizar entidades mediante SpaCy, de una forma muy sencilla.
doc = nlp(info_nominadas.iloc[1].descripcion)
print([(w.text, w.pos_) for w in doc])
[('Frida', 'PROPN'), (',', 'PUNCT'), ('una', 'DET'), ('niña', 'NOUN'), ('de', 'ADP'), ('seis', 'NUM'), ('años', 'NOUN'), (',', 'PUNCT'), ('afronta', 'VERB'), ('el', 'DET'), ('primer', 'ADJ'), ('verano', 'NOUN'), ('de', 'ADP'), ('su', 'DET'), ('vida', 'NOUN'), ('con', 'ADP'), ('su', 'DET'), ('nueva', 'ADJ'), ('família', 'NOUN'), ('adoptiva', 'ADJ'), (',', 'PUNCT'), ('tras', 'ADP'), ('la', 'DET'), ('muerte', 'NOUN'), ('de', 'ADP'), ('su', 'DET'), ('madre', 'NOUN'), ('.', 'PUNCT')]
También es posible visualizar esta información y las relaciones entre términos mediante displacy
# from spacy import displacy
# displacy.serve(doc, style='dep')
Para cada documento SpaCy nos ofrece mucha otra información:
mytokens = []
for token in doc:
mytokens.append({'text': token.text, 'lemma': token.lemma_, 'pos': token.pos_,'tag':token.tag_,'dep':token.dep_,
'shape':token.shape_, 'is_alpha': token.is_alpha, 'is_stop': token.is_stop})
pd.DataFrame(mytokens)
dep | is_alpha | is_stop | lemma | pos | shape | tag | text | |
---|---|---|---|---|---|---|---|---|
0 | nsubj | True | False | Frida | PROPN | Xxxxx | PROPN___ | Frida |
1 | punct | False | False | , | PUNCT | , | PUNCT__PunctType=Comm | , |
2 | det | True | True | uno | DET | xxx | DET__Definite=Ind|Gender=Fem|Number=Sing|PronT... | una |
3 | appos | True | False | niño | NOUN | xxxx | NOUN__Gender=Fem|Number=Sing | niña |
4 | case | True | True | de | ADP | xx | ADP__AdpType=Prep | de |
5 | nummod | True | True | seis | NUM | xxxx | NUM__Number=Plur|NumType=Card | seis |
6 | nmod | True | False | año | NOUN | xxxx | NOUN__Gender=Masc|Number=Plur | años |
7 | punct | False | False | , | PUNCT | , | PUNCT__PunctType=Comm | , |
8 | ROOT | True | False | afrontar | VERB | xxxx | VERB__Mood=Ind|Number=Sing|Person=3|Tense=Pres... | afronta |
9 | det | True | True | el | DET | xx | DET__Definite=Def|Gender=Masc|Number=Sing|Pron... | el |
10 | amod | True | True | 1 | ADJ | xxxx | ADJ__Gender=Masc|Number=Sing|NumType=Ord | primer |
11 | obj | True | False | verano | NOUN | xxxx | NOUN__Gender=Masc|Number=Sing | verano |
12 | case | True | True | de | ADP | xx | ADP__AdpType=Prep | de |
13 | det | True | True | su | DET | xx | DET__Number=Sing|Person=3|Poss=Yes|PronType=Prs | su |
14 | nmod | True | False | vida | NOUN | xxxx | NOUN__Gender=Fem|Number=Sing | vida |
15 | case | True | True | con | ADP | xxx | ADP__AdpType=Prep | con |
16 | det | True | True | su | DET | xx | DET__Number=Sing|Person=3|Poss=Yes|PronType=Prs | su |
17 | amod | True | True | nuevo | ADJ | xxxx | ADJ__Gender=Fem|Number=Sing | nueva |
18 | obl | True | False | família | NOUN | xxxx | NOUN__Gender=Fem|Number=Sing | família |
19 | amod | True | False | adoptivo | ADJ | xxxx | ADJ__Gender=Fem|Number=Sing | adoptiva |
20 | punct | False | False | , | PUNCT | , | PUNCT__PunctType=Comm | , |
21 | case | True | True | tras | ADP | xxxx | ADP__AdpType=Prep | tras |
22 | det | True | True | lo | DET | xx | DET__Definite=Def|Gender=Fem|Number=Sing|PronT... | la |
23 | obl | True | False | muerte | NOUN | xxxx | NOUN__Gender=Fem|Number=Sing | muerte |
24 | case | True | True | de | ADP | xx | ADP__AdpType=Prep | de |
25 | det | True | True | su | DET | xx | DET__Number=Sing|Person=3|Poss=Yes|PronType=Prs | su |
26 | nmod | True | False | madre | NOUN | xxxx | NOUN__Gender=Fem|Number=Sing | madre |
27 | punct | False | False | . | PUNCT | . | PUNCT__PunctType=Peri | . |
¿Qué entendemos por palabras vacías?
Cargamos las stopwords predefinidas en español en NLTK
stopwords_spa = nltk.corpus.stopwords.words('spanish')
print(len(stopwords_spa))
print(stopwords_spa[0:21])
313 ['de', 'la', 'que', 'el', 'en', 'y', 'a', 'los', 'del', 'se', 'las', 'por', 'un', 'para', 'con', 'no', 'una', 'su', 'al', 'lo', 'como']
Idéntica operación en SpaCy
from spacy.lang.es.stop_words import STOP_WORDS
print(len(STOP_WORDS))
print(list(STOP_WORDS)[0:21])
551 ['trata', 'ya', 'mías', 'intentas', 'mencionó', 'propia', 'vaya', 'parte', 'casi', 'mucho', 'emplean', 'ésos', 'no', 'haces', 'peor', 'ello', 'aun', 'estado', 'alli', 'embargo', 'paìs']
Como primera operación vamos a calcular la frecuencia en la que aparece cada término. Excluyendo aquellos que son stopwords, nombres propios o particulas
from __future__ import print_function, unicode_literals
import spacy
from collections import defaultdict, Counter
pos_counts = defaultdict(Counter)
words = []
for sinop in info_pelis.descripcion:
doc = nlp(sinop)
for token in doc:
if token.is_stop != True and token.is_punct != True and token.pos_ not in ['PROPN', 'CONJ', 'ADP', 'DET']:
words.append(token.lemma_)
word_TF = Counter(words)
word_TF = pd.DataFrame.from_dict(word_TF, orient='index').reset_index()
word_TF = word_TF.rename(index=str, columns={"index": "token", 0: "TF"})
word_TF = word_TF.sort('TF', ascending=False)
/home/claudiag/anaconda3/lib/python3.6/site-packages/ipykernel/__main__.py:15: FutureWarning: sort(columns=....) is deprecated, use sort_values(by=.....)
Las 10 palabras más frecuentes son:
display(word_TF.head(10))
token | TF | |
---|---|---|
8 | vida | 391 |
48 | año | 332 |
207 | vivir | 205 |
130 | joven | 202 |
152 | hijo | 176 |
44 | historia | 171 |
12 | mujer | 155 |
124 | casar | 155 |
131 | llegar | 146 |
64 | mundo | 143 |
display(word_TF[word_TF.token.isin(['cine', 'director', 'español', 'película', 'vida',
'vivir', 'mujer', 'hombre', 'joven', 'hijo', 'madre', 'padre'])])
token | TF | |
---|---|---|
8 | vida | 391 |
207 | vivir | 205 |
130 | joven | 202 |
152 | hijo | 176 |
12 | mujer | 155 |
136 | padre | 130 |
67 | hombre | 109 |
53 | madre | 95 |
271 | película | 81 |
3134 | cine | 36 |
1016 | español | 29 |
654 | director | 28 |
En el contexto en el que estamos trabajando conviene añadir algunas otras palabras muy comunes en las sinopsis
MY_STOP_WORDS = STOP_WORDS.copy()
MY_STOP_WORDS = MY_STOP_WORDS.union(['cine','actor','director','español','película','documental','historia',
'vida','vivir','año','vivir', 'mujer', 'hombre', 'joven', 'hijo', 'madre',
'padre', 'niño','niña', 'hijo', 'hija' ])
NLTK ofrece distintos stemmers y lemmatizers con un rendimiento desigual en español:
from nltk.stem.snowball import SnowballStemmer
stemmer_eng = SnowballStemmer("english")
stemmer = SnowballStemmer("spanish")
print(stemmer_eng.stem('girls'))
print(stemmer.stem('chicas'))
girl chic
from nltk.stem import WordNetLemmatizer
wordnet_lemmatizer = WordNetLemmatizer()
print(wordnet_lemmatizer.lemmatize('girls'))
print(wordnet_lemmatizer.lemmatize('chicas'))
girl chicas
En SpaCy sólo se han desarrollado lematizadores, aquí un interesante hilo en GitHub Built-in stemmer?
doc = nlp('chica')
for token in doc:
print(token.lemma_.lower())
chico
En este apartado vamos a tratar de identificar segmentos de películas agrupando mediante las sinopsis. Esta operación puede apoyarse tanto en las funciones disponibles en NLTK como en SpaCy.
Funciones con NLTK
def tokenize_and_stem(text):
# Primero se extraen los tokens de una frase, despues se extraen las palabras,
# para asegurar que la puntuación toma su propio tokent
tokens = [word for sent in nltk.sent_tokenize(text) for word in nltk.word_tokenize(sent)]
filtered_tokens = []
# Solo tomamos los tokens que contienen letras, para evitar tokens numericos y puntuacion
for token in tokens:
if re.search('[a-zA-Z]', token):
filtered_tokens.append(token)
stems = [stemmer.stem(t) for t in filtered_tokens] # se extrae el tema para cada palabra
return stems
def tokenize_only(text):
tokens = [word.lower() for sent in nltk.sent_tokenize(text) for word in nltk.word_tokenize(sent)]
filtered_tokens = []
for token in tokens:
if re.search('[a-zA-Z]', token):
filtered_tokens.append(token)
return filtered_tokens
Funciones son SpaCy
def tokenize_and_lemm_spacy(text):
doc = nlp(text)
lemmas = []
for token in doc:
if token.is_stop== False: # aprovechamos para eliminar ya las stopwords
if token.is_alpha== True: # Nos quedamos solo con los tokens que contienen letras
if token.pos_ not in ['PROPN', 'CONJ', 'ADP', 'DET']: # eliminamos nombres propios, conjunciones, determinantes
lemmas.append(token.lemma_.lower())
return lemmas
def tokenize_only_spacy(text):
doc = nlp(text)
tokens = []
for token in doc:
if token.is_stop== False :
if token.is_alpha== True:
if token.pos_ not in ['PROPN', 'CONJ', 'ADP', 'DET']:
tokens.append(token.text.lower())
return tokens
print(info_nominadas.iloc[1].descripcion)
print(tokenize_only(info_nominadas.iloc[1].descripcion))
print(tokenize_and_stem(info_nominadas.iloc[1].descripcion))
Frida, una niña de seis años, afronta el primer verano de su vida con su nueva família adoptiva, tras la muerte de su madre. ['frida', 'una', 'niña', 'de', 'seis', 'años', 'afronta', 'el', 'primer', 'verano', 'de', 'su', 'vida', 'con', 'su', 'nueva', 'família', 'adoptiva', 'tras', 'la', 'muerte', 'de', 'su', 'madre'] ['frid', 'una', 'niñ', 'de', 'seis', 'años', 'afront', 'el', 'prim', 'veran', 'de', 'su', 'vid', 'con', 'su', 'nuev', 'famili', 'adopt', 'tras', 'la', 'muert', 'de', 'su', 'madr']
print(info_nominadas.iloc[1].descripcion)
print(tokenize_only_spacy(info_nominadas.iloc[1].descripcion))
print(tokenize_and_lemm_spacy(info_nominadas.iloc[1].descripcion))
Frida, una niña de seis años, afronta el primer verano de su vida con su nueva família adoptiva, tras la muerte de su madre. ['niña', 'años', 'afronta', 'verano', 'vida', 'família', 'adoptiva', 'muerte', 'madre'] ['niño', 'año', 'afrontar', 'verano', 'vida', 'família', 'adoptivo', 'muerte', 'madre']
Vamos a crear una lista con todas nuestras sinopsis
synopses = list(info_pelis['descripcion'])
Así como una lista de todos los términos encontrados:
totalvocab_stemmed = []
totalvocab_tokenized = []
for i in synopses:
allwords_stemmed = tokenize_and_lemm_spacy(i)
totalvocab_stemmed.extend(allwords_stemmed)
allwords_tokenized = tokenize_only_spacy(i)
totalvocab_tokenized.extend(allwords_tokenized)
Con estas dos listas, de tokens y lemas, vamos a crear un DataFrame, que contendrá el lema y en el índice el token. Así podemos rescatar en cualquier momento el token original.
vocab_frame = pd.DataFrame({'words': totalvocab_tokenized}, index = totalvocab_stemmed)
print( str(vocab_frame.shape[0]) + ' en el vocab_frame')
29140 en el vocab_frame
vocab_frame.head(10)
words | |
---|---|
escritor | escritor |
escribir | escribe |
falso | falso |
pretencioso | pretencioso |
insípido | insípido |
trabaja | trabaja |
escribiente | escribiente |
notar | notaría |
vida | vida |
gris | gris |
A continuación definimos los parametros para la función de frecuencia inversa (tf-idf), para poder convertir la lista de sinopsis en una matriz de términos y documentos.
Para alcanzar esta matriz, en primer lugar contamos el número de veces que ocurre una palabra en un documento. Así obtenemos una matriz de términos documentos.
A continuación esta matriz es ponderada por la frecuencia inversa: palabras que ocurren frecuentemente en un documento pero no en el corpus reciben un peso mayor, asumiendo que aportan un mayor significado en el documento.
Sobre los parámetros:
from sklearn.feature_extraction.text import TfidfVectorizer
#define vectorizer parameters
tfidf_vectorizer = TfidfVectorizer(max_df=0.8, max_features=200000,
min_df=3, stop_words=MY_STOP_WORDS,
use_idf=True, tokenizer=tokenize_and_lemm_spacy, ngram_range=(1,3))
%time tfidf_matrix = tfidf_vectorizer.fit_transform(synopses) #fit the vectorizer to synopses
print(tfidf_matrix.shape)
CPU times: user 41 s, sys: 600 ms, total: 41.6 s Wall time: 20.9 s (921, 2432)
terms contiene la lista de features con la que vamos a realizar el cluster
terms = tfidf_vectorizer.get_feature_names()
Calculamos la distancia como 1- similaridad coseno, esto es la medida de similaridad entre nuestros documentos. La resta nos permitirá representarlo en el plano
from sklearn.metrics.pairwise import cosine_similarity
dist = 1 - cosine_similarity(tfidf_matrix)
from sklearn.cluster import KMeans
num_clusters = 3
km = KMeans(n_clusters=num_clusters)
%time km.fit(tfidf_matrix)
clusters = km.labels_.tolist()
CPU times: user 2.68 s, sys: 24 ms, total: 2.71 s Wall time: 1.37 s
from sklearn.externals import joblib
#Salvamos nuestro modelo, o bien lo cargamos si ya lo hemos ejecutado previamente
joblib.dump(km, 'doc_cluster.pkl')
#km = joblib.load('doc_cluster.pkl')
clusters = km.labels_.tolist()
Construimos un pandas dataframe para que sea más cómodo trabajar
films = {'title': list(info_pelis.titulo),
'synopsis': list(info_pelis.descripcion),
'edition': list(info_pelis.edicion),
'cluster': clusters,
'nom_mejor_pelicula': list(info_pelis.nom_mejor_pelicula),
'nom_mejor_dire': list(info_pelis.nom_mejor_direc),
'gana_mejor_pelicula': list(info_pelis.gana_mejor_pelicula)}
frame = pd.DataFrame(films, index = [clusters] , columns = ['title', 'synopsis', 'edition', 'cluster', 'nom_mejor_pelicula','nom_mejor_dire', 'gana_mejor_pelicula'])
distribution_clusters = frame[['cluster', 'title']].groupby('cluster').count().reset_index() #numero de peliculas por cluster
distribution_clusters_nom = frame[frame.nom_mejor_pelicula==1][['cluster', 'title']].groupby('cluster').count().reset_index() #numero de peliculas nominadas a mejor pelicula por cluster
Exploramos el resultado de la clusterización
sns.set_style("whitegrid")
ax = sns.barplot(x="cluster", y="title", data=distribution_clusters)
ax.set(ylabel='# películas', xlabel='Cluster')
ax.set_title('Clusters de películas nominadas a los Goya')
plt.show()
plt.close()
sns.set_style("whitegrid")
ax = sns.barplot(x="cluster", y="title", data=distribution_clusters_nom)
ax.set(ylabel='# películas', xlabel='Cluster')
ax.set_title('Clusters de películas nominadas a mejor película en los Goya')
plt.show()
plt.close()
print('Cluster 0: ')
print(list(frame[(frame.cluster==0) & (frame.nom_mejor_pelicula ==1)].title))
print('Cluster 1: ')
print(list(frame[(frame.cluster==1) & (frame.nom_mejor_pelicula ==1)].title))
print('Cluster 2: ')
print(list(frame[(frame.cluster==2) & (frame.nom_mejor_pelicula ==1)].title))
Cluster 0: ['Estiu 1993', 'Verónica', 'Tarde para la ira', 'Un monstruo viene a verme', 'A cambio de nada', 'Truman', 'Loreak', 'Caníbal', 'La gran familia española', 'La herida', 'Grupo 7', 'Lo imposible', 'La piel que habito', 'No habrá paz para los malvados', 'Balada triste de trompeta', 'Pa negre', 'El secreto de sus ojos', 'La soledad', 'El laberinto del fauno', 'Volver', 'La mala educación', 'Roma', 'Mi vida sin mí', 'El otro lado de la cama', 'Hable con ella', 'Sin noticias de Dios', 'El Bola', 'La comunidad', 'Leo', 'You\x92re the One (Una historia de entonces)', 'La lengua de las mariposas', 'Solas'] Cluster 1: ['El autor', 'Handia', 'La librería', 'Julieta', 'La novia', 'Nadie quiere la noche', 'La isla mínima', 'Magical Girl', 'Relatos salvajes', '15 años y un día', 'Vivir es fácil con los ojos cerrados', 'Blancanieves', 'Agora', 'Celda 211', 'El baile de la Victoria', 'Camino', 'Los crímenes de Oxford', 'Los girasoles ciegos', 'Siete mesas de billar francés', '7 vírgenes', 'Obaba', 'Mar adentro', 'Planta 4', 'Te doy mis ojos', 'En la ciudad sin límites', 'Los lunes al sol', 'Juana la Loca', 'Lucía y el sexo', 'Cuando vuelvas a mi lado', 'Todo sobre mi madre'] Cluster 2: ['El hombre de las mil caras', 'Que Dios nos perdone', 'Un día perfecto', 'El Niño', 'El artista y la modelo', 'Blackthorn. Sin destino', 'La voz dormida', 'Buried \x96 Enterrado', 'También la lluvia', 'Sólo quiero caminar', 'El orfanato', 'Las 13 rosas', 'Alatriste', 'Salvador (Puig Antich)', 'La vida secreta de las palabras', 'Princesas', 'Tiovivo c. 1950', 'Soldados de Salamina', 'Los otros']
# sólo las películas nominadas a mejor película
frame_nom = frame[frame.nom_mejor_pelicula==1]
over_edition = frame[['edition', 'cluster', 'title']].groupby(['edition', 'cluster']).count().reset_index()
sns.set_style("whitegrid")
ax = sns.barplot(x="edition", y="title", hue="cluster", data=over_edition)
ax.set(ylabel='# películas', xlabel='Edición Premios Goya')
ax.set_title('Cluster de películas nominadas a los Goya')
plt.show()
plt.close()
sns.set_style("whitegrid")
ax = sns.barplot(x="edition", y="title", hue="cluster", data=frame[frame.nom_mejor_pelicula==1][['edition', 'cluster', 'title']].groupby(['edition', 'cluster']).count().reset_index())
ax.set(ylabel='# películas', xlabel='Edición Premios Goya')
ax.set_title('Cluster de películas nominadas a los Goya en la candidatura Mejor Película')
plt.show()
plt.close()
# nominadas en cada cluster y pct de ganadoras del premio Goya a mejor película
frame_nom[['cluster', 'gana_mejor_pelicula']].groupby(['cluster']).agg(['count', 'sum', 'mean']).reset_index()
cluster | gana_mejor_pelicula | |||
---|---|---|---|---|
count | sum | mean | ||
0 | 0 | 32 | 9.0 | 0.281250 |
1 | 1 | 30 | 11.0 | 0.366667 |
2 | 2 | 19 | 2.0 | 0.105263 |
from __future__ import print_function
# frame.index = range(len(frame.index))
print("Palabras top por cluster:")
print()
order_centroids = km.cluster_centers_.argsort()[:, ::-1]
for i in range(num_clusters):
print("Palabras Cluster %d :" % i, end='')
for ind in order_centroids[i, :26]: #15 palabras por cluster
print(' %s' % str(vocab_frame.ix[terms[ind].split(' ')].values.tolist()[0][0]), end=',')
print()
print()
# print("Cluster %d titles:" % i, end='')
# for title in frame.ix[i]['title'].values.tolist():
# print(' %s,' % title, end='')
# print() #add whitespace
# print() #add whitespace
Palabras top por cluster: Palabras Cluster 0 : casa, descubre, noche, familia, amigas, amor, relación, cambiarán, llamada, situación, mundo, recibe, barrio, pueblo, muerte, profesor, nan, encontrando, sale, vuelve, música, perdido, inesperada, busca, vecinos, chica, Palabras Cluster 1 : viaje, decide, mundo, ciudad, casa, formará, aventuras, destino, pequeña, chica, encontrando, familia, busca, descubre, cambiarán, crucen, amor, abuela, acaba, amigas, países, novia, soñado, luchado, huida, camino, Palabras Cluster 2 : guerra, llegan, nan, unidos, desaparecido, grupo, dejando, extraños, española, familia, mundial, civiles, descubre, muere, mundo, pueblo, encontrando, casa, guerra, convierte, luchado, nan, político, nan, vieja, pequeña,
import os # for os.path.basename
import matplotlib.pyplot as plt
import matplotlib as mpl
from sklearn.manifold import MDS
MDS()
# Convertimos el resultado en 2 componentes para poder representarlo
mds = MDS(n_components=2, dissimilarity="precomputed", random_state=1)
pos = mds.fit_transform(dist) # shape (n_components, n_samples)
xs, ys = pos[:, 0], pos[:, 1]
#set up colors per clusters using a dict
cluster_colors = {0: '#1b9e77', 1: '#d95f02', 2: '#7570b3', 3: '#e7298a', 4: '#66a61e'}
#set up cluster names using a dict
cluster_names = {0: 'Familia y costumbrismo',
1: 'Amor y aventura',
2: 'Histórico, sucesos y acción '
}
%matplotlib inline
#crear dataframe, con los resultados de la descomposicion y otra información
df = pd.DataFrame(dict(x=xs, y=ys, label=clusters, title=list(frame.title),
nom_mejor_pelicula = list(frame.nom_mejor_pelicula),
gana_mejor_pelicula = list(frame.gana_mejor_pelicula),
))
df = df[df.nom_mejor_pelicula==1]
df.index = range(len(df.index))
#agrupamos por cluster
groups = df.groupby('label')
fig, ax = plt.subplots(figsize=(17, 9)) # set size
ax.margins(0.05) # Optional, just adds 5% padding to the autoscaling
for name, group in groups:
ax.plot(group.x, group.y, marker='o', linestyle='', ms=12,
label=cluster_names[name], color=cluster_colors[name],
mec='none')
ax.set_aspect('auto')
ax.tick_params(\
axis= 'x',
which='both',
bottom='off',
top='off',
labelbottom='off')
ax.tick_params(\
axis= 'y',
which='both',
left='off',
top='off',
labelleft='off')
ax.legend(numpoints=1)
for i in range(len(df)):
ax.text(df.ix[i]['x'],
df.ix[i]['y'],
df.ix[i]['title'], size=10)
plt.show()
plt.close()
El método Ward es un algoritmo de clustering jerárquico. Nos permite en cada etapa identificar el par de clusters con la mínima distancia. Para realizarlo emplearemos la distancia previamente calculada.
Devuelve 2 clusters principales, reemplazando a los 5 anteriores
from scipy.cluster.hierarchy import ward, dendrogram
linkage_matrix = ward(dist) #define the linkage_matrix using ward clustering pre-computed distances
fig, ax = plt.subplots(figsize=(15, 20))
ax = dendrogram(linkage_matrix, orientation="right", labels=list(info_pelis.titulo));
plt.tick_params(\
axis= 'x',
which='both',
bottom='off',
top='off',
labelbottom='off')
plt.tight_layout()
plt.savefig('ward_clusters.png', dpi=500)
plt.close()
Latent Dirichlet Allocation (LDA) es un modelo generativo que permite que conjuntos de observaciones puedan ser explicados por grupos no observados que explican por qué algunas partes de los datos son similares. Por ejemplo, si las observaciones son palabras en documentos, presupone que cada documento es una mezcla de un pequeño número de categorías (también denominados como tópicos/temas) y la aparición de cada palabra en un documento se debe a una de las categorías a las que el documento pertenece.
import string
def strip_proppers(text):
# first tokenize by sentence, then by word to ensure that punctuation is caught as it's own token
tokens = [word for sent in nltk.sent_tokenize(text) for word in nltk.word_tokenize(sent) if word.islower()]
return "".join([" "+i if not i.startswith("'") and i not in string.punctuation else i for i in tokens]).strip()
def strip_proppers_spacy(text):
token_list = []
doc = nlp(text)
for token in doc:
if token.is_stop == False and token.is_alpha == True and token.pos_ not in ['PROPN', 'CONJ', 'ADP', 'DET']:
token_list.append(token.text.lower())
return ' '.join(token_list)
from nltk.tag import pos_tag
def strip_proppers_POS(text):
tagged = pos_tag(text.split())
non_propernouns = [word for word,pos in tagged if pos != 'NNP' and pos != 'NNPS']
return non_propernouns
from gensim import corpora, models, similarities
#quitar los nombres propios
%time preprocess = [strip_proppers_spacy(doc) for doc in synopses]
#tokenizar
%time tokenized_text = [tokenize_and_lemm_spacy(text) for text in preprocess]
#quitar las stopwords
%time texts = [[word for word in text if word not in MY_STOP_WORDS] for text in tokenized_text]
CPU times: user 37.4 s, sys: 400 ms, total: 37.8 s Wall time: 18.9 s CPU times: user 22.2 s, sys: 188 ms, total: 22.4 s Wall time: 11.2 s CPU times: user 12 ms, sys: 0 ns, total: 12 ms Wall time: 4.82 ms
#crear un diccionario de Gemsim a partir de los documentos
dictionary = corpora.Dictionary(texts)
#quitar los extremos ( similar al min max fijado en el tf-idf)
dictionary.filter_extremes(no_below=1, no_above=0.8)
#convertir el diccionario en 'bag of words corpus' para usarlo como referencia
corpus = [dictionary.doc2bow(text) for text in texts]
%time lda = models.LdaModel(corpus, num_topics=3, id2word=dictionary, update_every=5, chunksize=10000, passes=100)
CPU times: user 4min 21s, sys: 392 ms, total: 4min 21s Wall time: 4min 21s
lda.show_topics()
[(0, '0.007*"mundo" + 0.005*"viajar" + 0.004*"amigo" + 0.004*"llegar" + 0.004*"descubrir" + 0.003*"grupo" + 0.003*"casar" + 0.003*"amor" + 0.003*"convertir" + 0.003*"personaje"'), (1, '0.007*"casar" + 0.006*"familia" + 0.006*"descubrir" + 0.006*"decidir" + 0.005*"llegar" + 0.005*"encontrar" + 0.004*"mundo" + 0.004*"amor" + 0.004*"viajar" + 0.003*"poblar"'), (2, '0.005*"llegar" + 0.005*"casar" + 0.005*"noche" + 0.004*"muerte" + 0.004*"pequeño" + 0.004*"mundo" + 0.003*"ciudad" + 0.003*"familia" + 0.003*"poblar" + 0.003*"buscar"')]