<jupyter_start><jupyter_text>En esta primera sección hacemos una limpieza de datos para obtener matrices de frecuencias (Term-Document Matrixes) a partir de los discursos de estado de la nación, lo que será necesario para una buena visualización. Estas matrices son la pieza fundamental para el análisis de texto, y consisten en matrices que guardan la frecuencia en la cual cada palabra es mencionada en un texto.<jupyter_code># Cargamos librerías para leer los documentos y crear las matrices de frecuencias
import glob 
import os
import scipy
import numpy as np
import pandas as pd<jupyter_output><empty_output><jupyter_text>Los datos crudos son un conjunto de 89 archivos de texto con los discursos de estado de la nación entre 1926 y 2015, con excepción de 1970.  Después de muchos problemas con la lectura de datos, se observa que hasta el año 1989 tienen encoding utf8. Desde 1990 el encoding no es reconocido pero se puede reconocer con ISO-8859-1.<jupyter_code>#speeches are in corpus folder
filepath = '..\\corpus\\*'
file_list = glob.glob(filepath)

corpus = []

for file_path in file_list:
    with open(file_path, encoding = "ISO-8859-1") as f_input:
        corpus.append(f_input.read())<jupyter_output><empty_output><jupyter_text>We should have 90 speeches since 1926<jupyter_code>len(corpus)<jupyter_output><empty_output><jupyter_text>Primero obtenemos un set de stopwords en español, las que deben ser removidas de los discursos. Si vemos el primero, notamos que hay muchas de estas palabras como 'del', 'y', 'la', que deben ser removidas.<jupyter_code>corpus[0]
#se puede comentar afuera todo este bloque de código, no es parte del análisis final. Pero sirve para notar que las stopwords
#del módulo stop_words no son suficientes para nuestro análisis

#from stop_words import get_stop_words 
#stop_words = get_stop_words('spanish')
#len(stop_words)<jupyter_output><empty_output><jupyter_text>La cantidad de palabras en stopwords de la libraría stop_words es 308, lo que es algo bajo y al utilizarla en el procesamiento de datos se obtienen muchas palabras que entregan poca información como números. Por esto se lee un archivo con una mejor versión de stopwords en español.<jupyter_code>stop_words_es = open('../data/stopwords.txt', encoding = 'ISO-8859-1') 
stop_words_es = [line.split() for line in stop_words_es.readlines()]
len(stop_words_es)<jupyter_output><empty_output><jupyter_text>Como se puede ver, hay 673 palabras en este último set, lo que permitirá obtener mejores resultados. Como el formato no es el requerido, lo cambiamos a una lista de python.<jupyter_code>stop_es = []
for i in range(0,len(stop_words_es)):
    stop_es.append(stop_words_es[i][0])<jupyter_output><empty_output><jupyter_text>Agregamos algunas palabras que se usan en discursos de estado de la nación que no aportan para efectos del análisis usado en esta etapa.<jupyter_code>extra_words = ['uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis', 'siete', 'ocho', 'nueve']
stop_es.extend(extra_words)<jupyter_output><empty_output><jupyter_text>También removemos todos los números escritos como números (1-9) del corpus ya que tampoco nos dan información relevante para los algoritmos que aplicaremos.<jupyter_code>for i in range(0, 90): corpus[i] = ''.join(c for c in corpus[i] if not c.isnumeric())<jupyter_output><empty_output><jupyter_text>Ahora pasamos a tokenizar los textos (separar por palabras), obtener el vocabulario completo usado y las matrices de frecuencia. Para esto usamos la función CountVectorizer de sklearn.<jupyter_code>from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(input=corpus, encoding = "utf-8", strip_accents='unicode', stop_words= stop_es)

dtm = vectorizer.fit_transform(corpus)  # matriz de frecuencia para todos los discursos

vocab = vectorizer.get_feature_names()  # lista de vocabulario usado
type(dtm)<jupyter_output><empty_output><jupyter_text>Como se puede ver, la matriz de frecuencia es un objeto de clase csr_matrix de scipy. Para trabajar con los datos será más conveniente convertir la matriz a un array de numpy, lo que se hace a continuación.<jupyter_code>dtm = dtm.toarray()
vocab = np.array(vocab)
type(dtm)<jupyter_output><empty_output><jupyter_text>También construimos un arreglo sumando los discursos para cada periodo presidencial. Esto concluye la parte de data cleaning.  Ahora identificamos los discursos por número de presidente<jupyter_code>(years, vocabulary_length) = dtm.shape
dtm_presidents = np.zeros((18, vocabulary_length))
dtm_presidents[0] = dtm[0]+dtm[1] #26-27
dtm_presidents[1] = dtm[2]+dtm[5] #28-31
dtm_presidents[2] = dtm[6] #32
dtm_presidents[3] = dtm[7]+dtm[12] #33-38
dtm_presidents[4] = dtm[13]+dtm[15] #39-41
dtm_presidents[5] = dtm[16]+dtm[20] #42-46
dtm_presidents[6] = dtm[21]+dtm[26] #47-52
dtm_presidents[7] = dtm[27]+dtm[32] #53-58
dtm_presidents[8] = dtm[33]+dtm[38] #59-64
dtm_presidents[9] = dtm[39]+dtm[44] #65-70 
dtm_presidents[10] = dtm[45]+dtm[47] #71-73
dtm_presidents[11] = dtm[48]+dtm[63] #74-89
dtm_presidents[12] = dtm[64]+dtm[67] #90-93
dtm_presidents[13] = dtm[68]+dtm[73] #94-99
dtm_presidents[14] = dtm[74]+dtm[79] #00-05
dtm_presidents[15] = dtm[80]+dtm[83] #06-09
dtm_presidents[16] = dtm[84]+dtm[87] #10-13
dtm_presidents[17] = dtm[88]+dtm[89] #14-15<jupyter_output><empty_output><jupyter_text>A continuación como análisis inicial buscamos una medida de distancia entre discursos. La similitud del coseno es ideal como medida de similitud vectorial. Esto permitirá ubicar cada discurso en un plano cartesiano y hacer comparaciones de acuerdo a la frecuencia de palabras utilizadas.<jupyter_code>from sklearn.metrics.pairwise import cosine_similarity
dist = 1 - cosine_similarity(dtm)<jupyter_output><empty_output><jupyter_text>Ubicamos los discursos en un plano para visualizar la distancia entre ellos.<jupyter_code>import matplotlib.pyplot as plt
from sklearn.manifold import MDS

mds = MDS(n_components=2, dissimilarity="precomputed", random_state=1)
pos = mds.fit_transform(dist)  
xs, ys = pos[:, 0], pos[:, 1]
filepath = '..\\corpus\\*'
file_list = glob.glob(filepath)
names = [os.path.basename(fn).replace('*', '') for fn in file_list]

for x, y, name in zip(xs, ys, sorted(names)):
    color = 'skyblue'
    plt.scatter(x, y, c=color)
    plt.text(x, y, name)


plt.savefig('../results/distance.png', dpi = 600)
plt.show()<jupyter_output><empty_output><jupyter_text>Como tenemos demasiada información en la figura, hacemos lo mismo para periodos presidenciales. Para esto necesitamos una lista con los nombres de los presidentes, la que tenemos en presidents_names.txt<jupyter_code>aux = open('../data/presidents_names.txt', encoding = 'ISO-8859-1') 
aux = [line.split() for line in aux.readlines()]
names_presidents = []
for i in range(0,len(aux)):
    names_presidents.append(aux[i][0])
names_presidents
dist_2 = 1 - cosine_similarity(dtm_presidents)
mds = MDS(n_components=2, dissimilarity="precomputed", random_state=1)
pos = mds.fit_transform(dist_2)
xs, ys = pos[:, 0], pos[:, 1]

for x, y, name in zip(xs, ys, names_presidents):
    color = 'skyblue'
    plt.scatter(x, y, c=color)
    plt.text(x, y, name)


plt.savefig('../results/distanceByPresident.png', dpi = 600)
plt.show()<jupyter_output><empty_output><jupyter_text>Es importante no considerar la posición respecto a un eje como una medida de tendencia política, lo único que nos dice el gráfico es la similitud entre presidentes según la frecuencia de las palabras usadas. Las que evidentemente pueden usarse en contextos muy distintos. Interesantemente, se puede ver un cluster de los presidentes desde la vuelta a la democracia en el sector sur este de la figura.   Convertimos el arreglo en un data frame de pandas, lo que permitira hacer un mayor análisis.<jupyter_code>dtm = dtm.transpose()
dtm_pandas = pd.DataFrame(dtm)
index_corpus = []
for x in range(1926, 2016):
    index_corpus.append(x)
dtm_pandas.columns = index_corpus
vocab_df = pd.DataFrame(vocab)
dtm_pandas['words'] = vocab_df
dtm_pandas.to_csv('../results/dtm.csv') #guardamos el dataframe 
dtm_pandas.head(5)<jupyter_output><empty_output><jupyter_text>Veamos algunos estadísticos descriptivos del dataframe. Eliminamos los que no dicen mucho en este caso, por ejemplo, todos los discursos tienen observaciones para cada palabra, aunque esta sea de frecuencia cero. Por lo que no dice nada el conteo de observaciones por variable.<jupyter_code>stats = dtm_pandas.describe()
stats = stats.drop(stats.index[[0,3,4,5,6]])
stats<jupyter_output><empty_output><jupyter_text>Contemos la cantidad total de palabras por discurso para estudiar en que periodos se hicieron discursos más largos.<jupyter_code>%matplotlib inline
import matplotlib.pyplot as plt
stats = stats.append((dtm_pandas.sum().to_frame().transpose().drop('words', 1).rename(index={0:'total words'})))
fig = plt.figure();
stats.iloc[3].plot.line()
fig.suptitle('Word count State of the Union', fontsize=14, fontweight='bold')

ax = fig.add_subplot(111)
fig.subplots_adjust(top=0.85)
ax.set_xlabel('Year')
ax.set_ylabel('Word count')<jupyter_output>C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:5: MatplotlibDeprecationWarning: Adding an axes using the same arguments as a previous axes currently reuses the earlier instance.  In a future version, a new instance will always be created and returned.  Meanwhile, this warning can be suppressed, and the future behavior ensured, by passing a unique label to each axes instance.
  """<jupyter_text>El conteo de algunas palabras podría ser interesante. Por ejemplo, veamos como evolucionan palabras como 'mercado', 'social', 'familia', 'estado', y 'mujer'.<jupyter_code>word_1 = dtm_pandas.loc[dtm_pandas['words'] == 'mercado'].drop('words', 1)
word_2 = dtm_pandas.loc[dtm_pandas['words'] == 'social'].drop('words', 1)
word_3 = dtm_pandas.loc[dtm_pandas['words'] == 'familia'].drop('words', 1)
word_4 = dtm_pandas.loc[dtm_pandas['words'] == 'gobierno'].drop('words', 1)
word_5 = dtm_pandas.loc[dtm_pandas['words'] == 'mujer'].drop('words', 1)

fig_1 = plt.figure();
word_1.iloc[0].plot.line()
fig_1.suptitle('Word count "mercado"', fontsize=14, fontweight='bold')

ax = fig_1.add_subplot(111)
fig_1.subplots_adjust(top=0.85)
ax.set_xlabel('Year')
ax.set_ylabel('Word count')


fig_2 = plt.figure();
word_2.iloc[0].plot.line()
fig_2.suptitle('Word count "social"', fontsize=14, fontweight='bold')

ax = fig_2.add_subplot(111)
fig_2.subplots_adjust(top=0.85)
ax.set_xlabel('Year')
ax.set_ylabel('Word count')

fig_3 = plt.figure();
word_3.iloc[0].plot.line()
fig_3.suptitle('Word count "familia"', fontsize=14, fontweight='bold')

ax = fig_3.add_subplot(111)
fig_3.subplots_adjust(top=0.85)
ax.set_xlabel('Year')
ax.set_ylabel('Word count')

fig_4 = plt.figure();
word_4.iloc[0].plot.line()
fig_4.suptitle('Word count "gobierno"', fontsize=14, fontweight='bold')

ax = fig_4.add_subplot(111)
fig_4.subplots_adjust(top=0.85)
ax.set_xlabel('Year')
ax.set_ylabel('Word count')

fig_5 = plt.figure();
word_5.iloc[0].plot.line()
fig_5.suptitle('Word count "mujer"', fontsize=14, fontweight='bold')

ax = fig_5.add_subplot(111)
fig_5.subplots_adjust(top=0.85)
ax.set_xlabel('Year')
ax.set_ylabel('Word count')
plt.savefig('../results/wordcount_mujer.png', dpi = 600)
plt.tight_layout()<jupyter_output>C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:11: MatplotlibDeprecationWarning: Adding an axes using the same arguments as a previous axes currently reuses the earlier instance.  In a future version, a new instance will always be created and returned.  Meanwhile, this warning can be suppressed, and the future behavior ensured, by passing a unique label to each axes instance.
  # This is added back by InteractiveShellApp.init_path()
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:21: MatplotlibDeprecationWarning: Adding an axes using the same arguments as a previous axes currently reuses the earlier instance.  In a future version, a new instance will always be created and returned.  Meanwhile, this warning can be suppressed, and the future behavior ensured, by passing a unique label to each axes instance.
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:30: MatplotlibDeprecationWarning: Adding an axes using the same arguments as a prev[...]<jupyter_text>Hacemos un ejercicio de clusterización:<jupyter_code>from scipy.cluster.hierarchy import ward, dendrogram

linkage_matrix = ward(dist)

dendrogram(linkage_matrix, orientation="right", labels=names)
plt.tight_layout() 

plt.savefig('../results/clustering.png', dpi = 600)
plt.show()<jupyter_output><empty_output><jupyter_text>Al igual que en el primer gráfico de distancia entre discursos, tenemos demasiada información en este gráfico. Nuevamente repetimos por presidente.<jupyter_code>linkage_matrix = ward(dist_2)

dendrogram(linkage_matrix, orientation="right", labels=names_presidents)
plt.tight_layout()
plt.savefig('../results/clustering_presidents.png', dpi = 600)
plt.show()<jupyter_output><empty_output><jupyter_text>La clusterización es interesante porque entrega resultados similares al gráfico de distancia entre discursos. Como se puede ver, en un cluster tenemos los 4 últimos presidentes.   Topic Modeling Para concluir hacemos un ejercicio de clasificación para identificar los temas que más se repiten en los discursos. El modelo usado es LDA (Latent-Dirichlet Allocation).<jupyter_code>import lda
#este método es intensivo en operación, por lo que este bloque tarda alrededor de 1 minuto en mi laptop
model = lda.LDA(n_topics=10, n_iter=1000, random_state=1)

model.fit(dtm.transpose())

topic_word = model.topic_word_
n_top_words = 10

for i, topic_dist in enumerate(topic_word):
    topic_words = np.array(vocab)[np.argsort(topic_dist)][:-(n_top_words+1):-1]
    print('Topic {}: {}'.format(i, ' '.join(topic_words)))<jupyter_output>INFO:lda:n_documents: 90
INFO:lda:vocab_size: 35736
INFO:lda:n_words: 625259
INFO:lda:n_topics: 10
INFO:lda:n_iter: 1000
INFO:lda:<0> log likelihood: -7273326
INFO:lda:<10> log likelihood: -6188744
INFO:lda:<20> log likelihood: -5896438
INFO:lda:<30> log likelihood: -5800448
INFO:lda:<40> log likelihood: -5753256
INFO:lda:<50> log likelihood: -5724590
INFO:lda:<60> log likelihood: -5707256
INFO:lda:<70> log likelihood: -5694368
INFO:lda:<80> log likelihood: -5685263
INFO:lda:<90> log likelihood: -5677635
INFO:lda:<100> log likelihood: -5669082
INFO:lda:<110> log likelihood: -5663285
INFO:lda:<120> log likelihood: -5659231
INFO:lda:<130> log likelihood: -5657872
INFO:lda:<140> log likelihood: -5655476
INFO:lda:<150> log likelihood: -5652969
INFO:lda:<160> log likelihood: -5651772
INFO:lda:<170> log likelihood: -5651005
INFO:lda:<180> log likelihood: -5650185
INFO:lda:<190> log likelihood: -5649065
INFO:lda:<200> log likelihood: -5649017
INFO:lda:<210> log likelihood: -5647911
INFO:lda:<[...]<jupyter_text>Se pueden identificar los topic principales, y también podemos ver la evolución de estos, información que está guardada en el objeto 'model'. Grafiquemos la evolución del topic 3 por mencionar palabras como 'democracia', 'futuro', 'desarrollo', y 'empresas'.<jupyter_code>plt.plot(sorted(names), model.doc_topic_[:,2])
plt.savefig('../results/topic2.png', dpi = 600)
plt.show()<jupyter_output>INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.
INFO:matplotlib.category:Using categorical units to plot a list of strings that are all parsable as floats or dates. If these strings should be plotted as numbers, cast to the appropriate data type before plotting.<jupyter_text>Leemos los datos de pib per cápita<jupyter_code>gdppc = pd.read_csv('../data/gdppc1926.csv', names = ['gdppc'])
gdppc = gdppc.drop(gdppc.index[[0]])
gdppc['gdppc'] = gdppc['gdppc'].astype(float)
gdppc.head()
gdppc.plot(kind='line')
plt.show()
import seaborn as sns
gdppc['Topic 2'] = model.doc_topic_[:,2]
sns.regplot(x = gdppc['Topic 2'], y = gdppc['gdppc'] )
plt.savefig('../results/reg_plot.png', dpi = 600)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11208709637510825, 'fraction_numerical': 0.03897067920326611, 'mean_word_length': 6.272604588394062, 'pattern_counts': {'":': 0, '<': 112, '<?xml version=': 0, '>': 111, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '23', 'dir': 'jupyter-structured-clean-dedup', 'id': '663587', 'license': 'MIT', 'n_tokens_mistral': 6264, 'n_tokens_neox': 5854, 'n_words': 1618, 'path': 'scripts/LDA State of the Union.ipynb', 'repo_name': 'diodz/chile-state-of-the-union-LDA', 'size': '16166'}