La intuición nos puede fallar
En la búsqueda de las características que mejor definan un documento de texto, la intuición se puede cruzar y llevarnos por caminos por los que no queríamos transitar. Por ejemplo, si queremos encontrar las palabras más significativas de un documento, la intuición nos dice que lo lógico sería buscar aquellas que aparecen con más frecuencia, pero esa estrategia puede que nos esté llevando precisamente en la dirección contraria.
Estamos asumiendo que nuestro corpus ya ha sido limpiado y no tiene las ruidosas stop words, así que damos por hecho que las palabras que aparecen con más frecuencia nunca serán del estilo de «el, la, los, la, en, a, …..».
Aún habiendo limpiado el corpus de stop words, la aparición frecuente de una palabra no tiene por qué estar asociada con su importancia. Por ejemplo, la palabra «cambio» es una palabra que podría aparecer con frecuencia en documentos relacionados con el deporte y realmente no nos diga nada relevante entre unos documentos y otros. Si quisiéramos clasificar los documentos por el deporte del que hablan, la palabra «cambio» puede que no sea de utilidad, a pesar de su frecuencia. En el otro extremo, y siguiendo con la analogía de documentos relacionados con deporte, la palabra «esperpéntico» seguramente aparecerá muy rara vez, pero tampoco nos va a indicar nada relevante, ya que es improbable que aparezca de forma consistente. Pero, ¿qué pasaría con la palabra «cuarto»? Es una palabra que puede que no aparezca un gran número de veces por documento, pero que sistemáticamente aparezca en todos (o casi todos) los documentos.
¿Tendremos que buscar entonces las palabras raras? ¿O quizás un punto intermedio?
Por tanto, necesitamos medios para identificar y cuantificar la importancia de las palabras en los documentos.
TF-IDF
Nos vemos en la obligación de tener que recurrir a aspectos más teóricos, pero intentaremos que sea lo menos doloroso posible.
TF-IDF significa Term Frequency times Inverse Document Frequency (es decir, TF x IDF) y es una métrica o score que establece un valor sobre la importancia de una palabra («p») en un documento («p»). Aquí term es análogo a palabra. Es decir, la palabra p para el documento d1 puede tener un score diferente al que tenga para el documento d2.
Esta métrica está compuesta por dos factores, como se puede ver en la fórmula: TF e IDF. Vayamos uno por uno:
- TF recoge la frecuencia de la palabra p en el documento d, normalizado sobre el máximo número de ocurrencias de cualquier palabra en el documento d. Así que hablamos de TFp,d = fp,d / maxk fk,d:
- fp,d indica el número de ocurrencias de la palabra d en el documento p.
- maxk fk,d indica el máximo número de ocurrencias de cualquier palabra (k) en el documento d.
- IDF recoge el número de documentos en los que la palabra p aparece (con independencia del número de veces que aparezca). Así que hablamos de IDFd = log2 (N/nd):
- N es el número total de documentos.
- nd es número de documentos en los que aparece la palabra d.
Por cada palabra p presente en el documento p se hace el cálculo TFp,d x IDFd y las palabras con mejor métrica / score serán las que mejor caracterizarán los temas que se tratan en cada documento.
Un poco de agua para aliviar el susto
Después de esta dosis de teoría, lo mejor es poner los conceptos en práctica. Esta vez, vamos a reproducir completamente el épico diálogo de Groucho Marx en la película Una noche en la ópera. Cada frase es un documento del corpus:
– Haga el favor de poner atención en la primera cláusula porque es muy importante. Dice que… la parte contratante de la primera parte será considerada como la parte contratante de la primera parte. ¿Qué tal, está muy bien, eh?
– No, eso no está bien. Quisiera volver a oírlo.
– Dice que… la parte contratante de la primera parte será considerada como la parte contratante de la primera parte.
– Esta vez creo que suena mejor.
– Si quiere se lo leo otra vez.
– Tan solo la primera parte.
– ¿Sobre la parte contratante de la primera parte?
– No, solo la parte de la parte contratante de la primera parte.
– Oiga, ¿por qué hemos de pelearnos por una tontería como ésta? La cortamos.
– Sí, es demasiado largo. ¿Qué es lo que nos queda ahora?
– Dice ahora… la parte contratante de la segunda parte será considerada como la parte contratante de la segunda parte.
– Eso si que no me gusta nada. Nunca segundas partes fueron buenas. Escuche: ¿por qué no hacemos que la primera parte de la segunda parte contratante sea la segunda parte de la primera parte?»
Aqui también vamos a utilizar una librería de Scikit-learn de Python para tokenizar el corpus, pero la labor de separación en documentos y limpieza la hemos hecho manualmente (para no olvidarnos de cómo hacerlo).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
import re import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer import nltk from nltk.corpus import stopwords nltk.download('stopwords') stops = set(stopwords.words("spanish")) movie_data = "– Haga el favor de poner atención en la primera cláusula porque es muy importante. \ Dice que… la parte contratante de la primera parte será considerada como la parte contratante de la primera parte. \ ¿Qué tal, está muy bien, eh? \ – No, eso no está bien. Quisiera volver a oírlo. \ – Dice que… la parte contratante de la primera parte será considerada como la parte contratante de la primera parte. \ – Esta vez creo que suena mejor. \ – Si quiere se lo leo otra vez. \ – Tan solo la primera parte. \ – ¿Sobre la parte contratante de la primera parte? \ – No, solo la parte de la parte contratante de la primera parte. \ – Oiga, ¿por qué hemos de pelearnos por una tontería como ésta? La cortamos. \ – Sí, es demasiado largo. ¿Qué es lo que nos queda ahora? \ – Dice ahora… la parte contratante de la segunda parte será considerada como la parte contratante de la segunda parte. \ – Eso si que no me gusta nada. Nunca segundas partes fueron buenas. Escuche: ¿por qué no hacemos que la primera parte \ de la segunda parte contratante sea la segunda parte de la primera parte?" movie_data = movie_data.split('–') # Clean up sections movie_data = [line.lower() for line in movie_data] # to lowercase movie_data = [re.sub(r'^https?:\/\/.*[\r\n]*', '', line, flags=re.MULTILINE) for line in movie_data] # no URLs movie_data = [re.sub("(\d)+", '', line) for line in movie_data] # no numbers movie_data = [re.sub("[€º”—“«»>•‘’!""?¿#$%&'()–…*+,-./:;?@[\]^_`{|}~]+", ' ', line) for line in movie_data] # no strange characters movie_data = [re.sub(r'\b\w{1}\b', '', line) for line in movie_data] # no single-character words movie_data = [re.sub('\s\s+', ' ', line) for line in movie_data] # no multiple spaces in the middle movie_data = [line.strip() for line in movie_data] # no extra spaces at the beginning/end movie_data = [line for line in movie_data if len(line) > 0] print("Documentos del corpus:\n") for index, line in enumerate(movie_data): print("Documento {} = {}\n".format(index + 1, line)) vectorizer = TfidfVectorizer(ngram_range = (1,1), analyzer="word", lowercase = True, stop_words = stops ) vec = vectorizer.fit(movie_data) doc_term_mat = vec.transform(movie_data) print("") inv_vocabuly = {v: k for k, v in vec.vocabulary_.items()} print("Palabras con mayor score por documento del corpus:\n") for section in range(len(movie_data)): section_info = doc_term_mat[section,] freq_words_section = [(idx, section_info[0, idx]) for word, idx in vec.vocabulary_.items()] sorted_freq_words_section = sorted(freq_words_section, key = lambda x: x[1], reverse=True) top_freq_words_section = [(inv_vocabuly[word[0]], word[1]) for word in sorted_freq_words_section] top_X_scores = top_freq_words_section[:5] print("Documento {} = ".format(section + 1) + str(top_X_scores)) print("") |
Documentos del corpus:
Documento 1 = haga el favor de poner atención en la primera cláusula porque es muy importante dice que la parte contratante de la primera parte será considerada como la parte contratante de la primera parte qué tal está muy bien eh
Documento 2 = no eso no está bien quisiera volver oírlo
Documento 3 = dice que la parte contratante de la primera parte será considerada como la parte contratante de la primera parte
Documento 4 = esta vez creo que suena mejor
Documento 5 = si quiere se lo leo otra vez
Documento 6 = tan solo la primera parte
Documento 7 = sobre la parte contratante de la primera parte
Documento 8 = no solo la parte de la parte contratante de la primera parte
Documento 9 = oiga por qué hemos de pelearnos por una tontería como ésta la cortamos
Documento 10 = sí es demasiado largo qué es lo que nos queda ahora
Documento 11 = dice ahora la parte contratante de la segunda parte será considerada como la parte contratante de la segunda parte
Documento 12 = eso si que no me gusta nada nunca segundas partes fueron buenas escuche por qué no hacemos que la primera parte de la segunda parte contratante sea la segunda parte de la primera parte
Palabras con mayor score por documento del corpus:
Documento 1 = [(‘parte’, 0.483654381731612), (‘primera’, 0.39534733460602467), (‘contratante’, 0.2635648897373498), (‘haga’, 0.23375166585608662), (‘favor’, 0.23375166585608662)]
Documento 2 = [(‘quisiera’, 0.5172566287016166), (‘volver’, 0.5172566287016166), (‘oírlo’, 0.5172566287016166), (‘bien’, 0.4442260012569211), (‘haga’, 0.0)]
Documento 3 = [(‘parte’, 0.7326850536625904), (‘primera’, 0.39927283340099023), (‘contratante’, 0.39927283340099023), (‘dice’, 0.26864011349325434), (‘considerada’, 0.26864011349325434)]
Documento 4 = [(‘creo’, 0.5172566287016166), (‘suena’, 0.5172566287016166), (‘mejor’, 0.5172566287016166), (‘vez’, 0.4442260012569211), (‘haga’, 0.0)]
Documento 5 = [(‘quiere’, 0.5364329212987224), (‘leo’, 0.5364329212987224), (‘vez’, 0.46069482409390994), (‘si’, 0.46069482409390994), (‘haga’, 0.0)]
Documento 6 = [(‘tan’, 0.656112696286408), (‘solo’, 0.5634772050709476), (‘primera’, 0.3698974076156291), (‘parte’, 0.33938985986097164), (‘haga’, 0.0)]
Documento 7 = [(‘parte’, 0.7920734454138474), (‘primera’, 0.4316362224548798), (‘contratante’, 0.4316362224548798), (‘haga’, 0.0), (‘favor’, 0.0)]
Documento 8 = [(‘parte’, 0.7980248010305959), (‘solo’, 0.4416442933270728), (‘primera’, 0.28991958808582435), (‘contratante’, 0.28991958808582435), (‘haga’, 0.0)]
Documento 9 = [(‘oiga’, 0.4472135954999579), (‘pelearnos’, 0.4472135954999579), (‘tontería’, 0.4472135954999579), (‘ésta’, 0.4472135954999579), (‘cortamos’, 0.4472135954999579)]
Documento 10 = [(‘demasiado’, 0.5172566287016166), (‘largo’, 0.5172566287016166), (‘queda’, 0.5172566287016166), (‘ahora’, 0.4442260012569211), (‘haga’, 0.0)]
Documento 11 = [(‘parte’, 0.6418660148662156), (‘segunda’, 0.5328339335697898), (‘contratante’, 0.34978147996650094), (‘ahora’, 0.2664169667848949), (‘dice’, 0.2353411717888399)]
Documento 12 = [(‘parte’, 0.5084810414242387), (‘segunda’, 0.42210671257335786), (‘primera’, 0.27709404624164236), (‘gusta’, 0.24575046764511696), (‘nunca’, 0.24575046764511696)]
A pesar de que nuestro corpus de documentos es insignificante, se puede apreciar claramente los scores de cada palabra dentro de cada documento.
TF-IDF sobre los programas electorales
Al igual que hicimos con otros métodos, es el momento de utilizar TF-IDF sobre los programas electorales de los partidos políticos que concurren a las elecciones del 10-N, y ver qué score obtienen las palabras más usadas. Hemos seguido el siguiente proceso:
- Aplicar los pasos que se detallan en este post (leer los ficheros de los programas, limpiarlos de caracteres extraños, dividir en palabras y eliminar las stop words).
- Identificar las 5 palabras con mayor score en cada sección del programa de cada partido.
- Identificar las 20 palabras con mayor score considerando el programa completo de cada partido.
- Juntar todas las palabras identificadas y comparar los resultados entre partidos.
Vamos a mirar los binomios de palabras
El método TD-IDF también se puede aplicar sobre binomios de palabras, es decir, sobre todos los pares de palabras que aparecen de forma consecutiva en los documentos de los programas de los partidos. De hecho, realizando este análisis sobre binomios, se pueden ver mayores diferencias entre los diferentes partidos.
Si te interesa el código, aquí lo tienes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
import pandas as pd import string import matplotlib.pyplot as plt %matplotlib inline import seaborn as sns import plotly import plotly.graph_objects as go import os from os import listdir from os.path import isfile, join import spacy from spacy.tokens import Doc import re from sklearn.feature_extraction.text import TfidfVectorizer import nltk from nltk.tokenize import RegexpTokenizer from nltk.corpus import stopwords nltk.download('stopwords') nltk.download('wordnet') # Function to return a list of stop words to consider def create_stop_words(): # We create a stop word list stops = set(stopwords.words("spanish")) # We define individual numbers and letters as stop words all_letters_numbers = string.digits + string.ascii_letters stops = stops.union([]) # add some stopwords stops = stops.union(list(all_letters_numbers)) return stops g_parties_dict = {"Cs": "./Programas/Cs/", "PP": "./Programas/PP/", "PSOE": "./Programas/PSOE/", "UP": "./Programas/UP/", "VOX": "./Programas/VOX/"} g_parties_color_dict = {"Cs": ["#ffd9b3", "#ff8000"], "PP": ["#b3d9ff", "#0080ff"], "PSOE": ["#ffb3b3", "#ff0000"], "UP": ["#e6b3ff", "#aa00ff"], "VOX": ["#c2f0c2", "#33cc33"]} g_nlp = spacy.load('es_core_news_sm') g_stop_words = create_stop_words() | g_nlp.Defaults.stop_words g_not_category_name='Resto de partidos' g_charts_root_dir = './Charts' def list_dirs(directory): """Returns all directories in a given directory """ return [f for f in pathlib.Path(directory).iterdir() if f.is_dir()] def list_files(directory, ext): """Returns all files in a given directory """ onlyfiles = [join(directory, f) for f in listdir(directory) if isfile(join(directory, f)) and f.endswith(ext)] return sorted(onlyfiles) # Function to remove numbers and small words (1 or 2 letters) from a document def num_and_short_word_preprocessor(tokens): # Regular expression for numbers no_numbers = re.sub('(\d)+', '', tokens.lower()) # Regular expression for 1-letter and 2-letter words no_short_words = re.sub(r'\b\w{1,2}\b', '', no_numbers) return no_short_words # Function to tokenize a document wihthout using stopwords def custom_tokenizer(doc): word_tokenizer = RegexpTokenizer(r'\w+') tokens = word_tokenizer.tokenize(doc) return tokens # Function to tokenize a document using stopwords def custom_tokenizer_filtering_stopwords(doc): word_tokenizer = RegexpTokenizer(r'\w+') tokens = word_tokenizer.tokenize(doc) filtered_tokens = [token for token in tokens if token not in g_stop_words] return filtered_tokens def create_Doc(row): doc = Doc(g_nlp.vocab, words=row) doc.is_parsed = True return doc def create_party_programs_corpus_with_sections(): all_party_program_section_name_l = [] all_party_corpus_lc = [] all_party_name = list() for party_name, party_program_location in g_parties_dict.items(): # List of files and their corresponding names that make up the party program party_program_section_file_l = list_files(party_program_location, ".txt") party_program_section_name_l = list(map(lambda x: os.path.split(x)[1], party_program_section_file_l)) # List that contains the program document corpus party_program_corpus = list() # Fill out the corpus for section_file_name in party_program_section_file_l: with open(section_file_name, 'r', encoding="utf8") as section_file: section_file_data = section_file.read().replace('\n', '. ') party_program_corpus.append(section_file_data) all_party_name.append(party_name) # Clean up sections party_corpus_lc = [section.lower() for section in party_program_corpus] # to lowercase party_corpus_lc = [re.sub("(\d)+", '', section) for section in party_corpus_lc] # no numbers party_corpus_lc = [re.sub("[€º”—“«»>•‘’!""#$%&'()*+,-./:;?@[\]^_`{|}~]+", ' ', section) for section in party_corpus_lc] # no strange characters party_corpus_lc = [re.sub('\s\s+', ' ', section) for section in party_corpus_lc] # no multiple spaces party_corpus_lc = [re.sub(r'\b\w{1}\b', '', section) for section in party_corpus_lc] all_party_corpus_lc += party_corpus_lc # Cleanup section names all_party_program_section_name_l += party_program_section_name_l all_party_program_section_name_l = [re.sub(".txt", '', section_name) for section_name in all_party_program_section_name_l] all_party_program_section_name_l = [re.sub("(\d)+[-]", '', section_name) for section_name in all_party_program_section_name_l] token_raw_party_section = [custom_tokenizer(section) for section in all_party_corpus_lc] token_filtered_party_section = [custom_tokenizer_filtering_stopwords(section) for section in all_party_corpus_lc] # Create DataFrame party_corpus_df = pd.DataFrame(data = { "Party": all_party_name, "Section_name": all_party_program_section_name_l, "Section_text": all_party_corpus_lc, "Tokenized_raw_section_text": token_raw_party_section, "Tokenized_filtered_section_text": token_filtered_party_section, }) parsed = party_corpus_df.Tokenized_filtered_section_text.apply(create_Doc) party_corpus_df["Parsed_section_text"] = parsed return party_corpus_df def merge_party_sections(party_corpus_df): corpus_groupby_party = party_corpus_df.groupby("Party", as_index=False) party_aggregated_sections = corpus_groupby_party["Section_text"].apply(lambda x: ' '.join(x)) party_sections_df = pd.DataFrame(data = {"Section_text": party_aggregated_sections.values, "Party": list(corpus_groupby_party["Party"].groups.keys())}) return party_sections_df def create_corpora_metric(corpora, doc_names, metric, stop, ngram = 1, num_freq_words=25, num_freq_words_section = 5, debug = False): if metric == "bow": vectorizer = CountVectorizer(stop_words = stop, ngram_range = (ngram, ngram), preprocessor = num_and_short_word_preprocessor, tokenizer=custom_tokenizer) elif metric =="tf-idf": vectorizer = TfidfVectorizer(stop_words = stop, ngram_range = (ngram, ngram), preprocessor = num_and_short_word_preprocessor, tokenizer=custom_tokenizer) else: raise Exception('Metric not supported') vec = vectorizer.fit(corpora) doc_term_mat = vec.transform(corpora) # START: Calculate the most frequent words per section (using word ID) freq_words_set = set() for section in range(len(corpora)): section_info = doc_term_mat[section,] freq_words_section = [(idx, section_info[0, idx]) for word, idx in vec.vocabulary_.items()] sorted_freq_words_section = sorted(freq_words_section, key = lambda x: x[1], reverse=True) top_freq_words_section = [word[0] for word in sorted_freq_words_section] freq_words_set.update(top_freq_words_section[0:num_freq_words_section]) # END # START: Calculate the most frequest words overall (using word ID) all_sections_info = doc_term_mat.sum(axis=0) all_freq_words_section = [(idx, all_sections_info[0, idx]) for word, idx in vec.vocabulary_.items()] all_sorted_freq_words_section = sorted(all_freq_words_section, key = lambda x: x[1], reverse=True) all_top_freq_words_section = [word[0] for word in all_sorted_freq_words_section] freq_words_set.update(all_top_freq_words_section[0:num_freq_words]) # END freq_words_key_l = list(freq_words_set) # list of most frequent word IDs reverse_dictionary = {v: k for k, v in vec.vocabulary_.items()} # dictionary ID: word freq_words_value_l = [reverse_dictionary[word_key] for word_key in freq_words_key_l] # list of most frequent words #DataFrame including the most frequent words per section and overall freq_words_df = pd.DataFrame((doc_term_mat[:,freq_words_key_l]).todense(), columns=freq_words_value_l, index=doc_names) return freq_words_df def create_metric_dict_by_party(party_corpus_df, metric = "bow", ngram = 1, num_freq_words=10, num_freq_words_section = 5): parties = party_corpus_df.Party.unique() metric_by_party = dict() # METRIC (BOW or TF-IDF) for individual parties for party in parties: party_corpora = party_corpus_df[party_corpus_df.Party == party] party_section_texts = party_corpora.Section_text.tolist() party_section_names = party_corpora.Section_name.tolist() freq_words_df = create_corpora_metric(party_section_texts, party_section_names, metric, g_stop_words, ngram = ngram, num_freq_words=num_freq_words, num_freq_words_section = num_freq_words_section) metric_by_party[party] = freq_words_df # METRIC (BOW or TF-IDF) for the overall corpora including all parties party_sections_df = merge_party_sections(party_corpus_df) overall_section_texts = party_sections_df.Section_text.tolist() overall_section_names = party_sections_df.Party.tolist() overall_freq_words_df = create_corpora_metric(overall_section_texts, overall_section_names, metric, g_stop_words, ngram = ngram, num_freq_words=num_freq_words, num_freq_words_section = num_freq_words_section) metric_by_party["All"] = overall_freq_words_df return metric_by_party def normalize_word_df(word_df): norm_word_df = word_df.copy() total_per_party = norm_word_df.sum(axis=1) grand_total = sum(total_per_party) for party in norm_word_df.index: norm_word_df.loc[party] = (norm_word_df.loc[party] / total_per_party[party]) * 100 return norm_word_df def show_df_as_bubble(word_df, num_words = 2, normalize = False, standard_sizeref = True, title = 'Palabras con mayor score por partido', xtitle = 'Palabras', ytitle = 'Partido', sub_dir = 'bow', file_name = 'bow_bubble', to_file = False): if normalize: word_df = normalize_word_df(word_df) title = title + " (normalizado)" # Keep the top 'num_words' words word_df.reindex(word_df.sum().sort_values(ascending=False).index, axis=1) num_words = min(num_words, word_df.shape[1]) word_df = word_df.iloc[:, :num_words] # Sort in alphabetical order word_df = word_df.reindex(sorted(word_df.columns), axis=1) # Get the word labels word_l = word_df.columns.values num_words = len(word_l) # List that contains every bubble row. Each element is a party. bubble_data = [] xs = list(range(word_df.shape[1])) for num_row in range(len(word_df.values)): index_name = word_df.index[num_row] index_color = g_parties_color_dict[index_name][1] index_values = word_df.values[num_row] if normalize: sizeref = 0.02 * max(index_values) hover_text = ["{:.2f}%".format(index_value) for index_value in index_values] else: if standard_sizeref: sizeref = 1 else: sizeref = 0.02 * max(index_values) hover_text = index_values section_bubble = go.Scatter( x = word_l, y = [num_row + 1] * num_words, name = index_name, showlegend = True, mode = 'markers', hovertext = hover_text, hoverinfo = "x+text", marker_size = index_values, marker = dict(color = index_color, sizeref = sizeref), ) bubble_data.append(section_bubble) fig = go.Figure(data=bubble_data) fig.update_layout( title = dict(text = title, xanchor = 'center', x = 0.5, font = dict(size=20)), yaxis = dict( tickmode = 'array', tickvals = list(range(1, num_words + 1)), ticktext = word_df.index ), legend = dict(itemsizing = 'constant') ) fig.update_xaxes(tickangle=315, title_text=xtitle, title_font=dict(size=18)) fig.update_yaxes(title_text=ytitle, title_font=dict(size=18)) if to_file: complete_file_name = '{}/{}/{}.html'.format(g_charts_root_dir, sub_dir, file_name) plotly.offline.plot(fig, filename = complete_file_name, auto_open=False) fig.show() party_corpus_df = create_party_programs_corpus_with_sections() td_idf_by_party = create_metric_dict_by_party(party_corpus_df, metric = "tf-idf", ngram = 1, num_freq_words=20, num_freq_words_section = 5) two_gram_tf_idf_by_party = create_metric_dict_by_party(party_corpus_df, metric = "tf-idf", ngram = 2, num_freq_words=20, num_freq_words_section = 5) tdidf_all_party_words = td_idf_by_party['All'] tdidf_all_party_2_words = two_gram_tf_idf_by_party['All'] show_df_as_bubble(tdidf_all_party_words, num_words = 100, normalize = False, standard_sizeref = False, title = 'Palabras con mayor score por partido', to_file = True, sub_dir = 'tfidf', file_name = 'tfidf_partidos') show_df_as_bubble(tdidf_all_party_2_words, num_words = 100, normalize = False, standard_sizeref = False, title = 'Binomios de palabras con mayor score por partido', to_file = True, sub_dir = 'tfidf', file_name = 'tfidf_2_partidos') |
Takeaway
Encontrar las palabras que mejor caracterizan un documento no es sencillo, y requiere aplicar métodos que sepan ponderar moderadas apariciones de las palabras de una forma más o menos consistente en los diferentes documentos que componen el corpus. TD-IDF nos aporta mejores resultados que BOW en este respecto, y en este post hemos presentado tanto los aspectos teóricos como su funcionamiento sobre corpus reales.
Nuevamente los gráficos que presentamos ofrecen la oportunidad de interaccionar para que podáis sacar vuestras propias conclusiones.
Ver en Kaggle