Ejemplo de Procesamiento en R y en Python

Previos.

Hace algunos meses en una competencia en Kaggle se presentó un código para un sistema de recomendación. En general para todo sistema de recomendación la parte más pesada suele ser el procesamiento, es quizás la clave de obtener buenas variables para tener un modelo más robusto. En fin, se pueden combinar muchas técnicas de todo tipo para crear un sistema de recomendación, en otro momento escribiré sobre lo poco que se al respecto.

El sistema de recomendación en el fondo terminaba siendo un problema de clasificación. Hubo diversos acercamiento para abordar el problema, todos con ciertos aspectos en común pero otros radicalmente distintos, les recomiendo leer la descripción de las soluciones las cuales las pueden encontrar aquí.

Lo interesante del código que tomé para esta entrada, me parece que es un buen código, por que es  legible, hacía operaciones de procesamiento estándar, un modelo “sencillo” y daba buen rendimiento ante la métrica de la competencia. El reto que pensé y el origen de esta entrada, fue replicar el procesamiento que se realizaba en R pero ahora en el entorno de Python.

En la migración del código respeté todos los pasos seguidos en el original, hice uso de la memoria de manera similar y trate de hacer la eliminación de tablas de manera igual. Esto para tratar de comparar lo mejor posible el uso de los dos entornos.

La lucha, R vs Python.

El debate entré los enamorados de un entorno y otro, suelen tener discusiones de todo tipo, en general no aportan nada y los argumentos a favor o en contra no tienen ni sentido ni valor alguno al momento de abordar o resolver un problema real.

Nunca el lenguaje será lo más importante si tienes buenas ideas, buenos algoritmos, etc. Hay gente que programa sus redes neuronales directo desde cuda con C o C++, sin pasar por tensorflow o pytorch. Así que esta entrada no es para apoyar un lenguaje u otro, es para comparar algunos aspectos:

  • Legibilidad del código.
  • Eficiencia en líneas de código.
  • Uso de la memoria.

Quizás hay otros criterios o detalles que se pueden comparar, pero creo que con esos basta.

Más detalles pueden ser considerados, pero creo que para limitar y mantener la entrada a un nivel intermedio los tres aspectos cubren lo general del código.

Código

Pego todo el código para su lectura. El código original en R fue escrito por Fabien Vavrand y la versión en Python es mía.

###########################################################################################################
#
# Kaggle Instacart competition
# Fabien Vavrand, June 2017
# Simple xgboost starter, score 0.3791 on LB
# Products selection is based on product by product binary classification, with a global threshold (0.21)
#
###########################################################################################################

Codigo en R

library(data.table)
library(dplyr)
library(tidyr)


# Load Data ---------------------------------------------------------------
path <- "../input"

aisles <- fread(file.path(path, "aisles.csv"))
departments <- fread(file.path(path, "departments.csv"))
orderp <- fread(file.path(path, "order_products__prior.csv"))
ordert <- fread(file.path(path, "order_products__train.csv"))
orders <- fread(file.path(path, "orders.csv"))
products <- fread(file.path(path, "products.csv"))


# Reshape data ------------------------------------------------------------
aisles$aisle <- as.factor(aisles$aisle)
departments$department <- as.factor(departments$department)
orders$eval_set <- as.factor(orders$eval_set)
products$product_name <- as.factor(products$product_name)

products <- products %>% 
 inner_join(aisles) %>% inner_join(departments) %>% 
 select(-aisle_id, -department_id)
rm(aisles, departments)

ordert$user_id <- orders$user_id[match(ordert$order_id, orders$order_id)]

orders_products <- orders %>% inner_join(orderp, by = "order_id")

rm(orderp)
gc()


# Products ----------------------------------------------------------------
prd <- orders_products %>%
 arrange(user_id, order_number, product_id) %>%
 group_by(user_id, product_id) %>%
 mutate(product_time = row_number()) %>%
 ungroup() %>%
 group_by(product_id) %>%
 summarise(
 prod_orders = n(),
 prod_reorders = sum(reordered),
 prod_first_orders = sum(product_time == 1),
 prod_second_orders = sum(product_time == 2)
 )

prd$prod_reorder_probability <- prd$prod_second_orders / prd$prod_first_orders
prd$prod_reorder_times <- 1 + prd$prod_reorders / prd$prod_first_orders
prd$prod_reorder_ratio <- prd$prod_reorders / prd$prod_orders

prd <- prd %>% select(-prod_reorders, -prod_first_orders, -prod_second_orders)

rm(products)
gc()

# Users -------------------------------------------------------------------
users <- orders %>%
 filter(eval_set == "prior") %>%
 group_by(user_id) %>%
 summarise(
 user_orders = max(order_number),
 user_period = sum(days_since_prior_order, na.rm = T),
 user_mean_days_since_prior = mean(days_since_prior_order, na.rm = T)
 )

us <- orders_products %>%
 group_by(user_id) %>%
 summarise(
 user_total_products = n(),
 user_reorder_ratio = sum(reordered == 1) / sum(order_number > 1),
 user_distinct_products = n_distinct(product_id)
 )

users <- users %>% inner_join(us)
users$user_average_basket <- users$user_total_products / users$user_orders

us <- orders %>%
 filter(eval_set != "prior") %>%
 select(user_id, order_id, eval_set,
 time_since_last_order = days_since_prior_order)

users <- users %>% inner_join(us)

rm(us)
gc()


# Database ----------------------------------------------------------------
data <- orders_products %>%
 group_by(user_id, product_id) %>% 
 summarise(
 up_orders = n(),
 up_first_order = min(order_number),
 up_last_order = max(order_number),
 up_average_cart_position = mean(add_to_cart_order))

rm(orders_products, orders)

data <- data %>% 
 inner_join(prd, by = "product_id") %>%
 inner_join(users, by = "user_id")

data$up_order_rate <- data$up_orders / data$user_orders
data$up_orders_since_last_order <- data$user_orders - data$up_last_order
data$up_order_rate_since_first_order <- data$up_orders / (data$user_orders - data$up_first_order + 1)

data <- data %>% 
 left_join(ordert %>% select(user_id, product_id, reordered), 
 by = c("user_id", "product_id"))

rm(ordert, prd, users)
gc()


# Train / Test datasets ---------------------------------------------------
train <- as.data.frame(data[data$eval_set == "train",])
train$eval_set <- NULL
train$user_id <- NULL
train$product_id <- NULL
train$order_id <- NULL
train$reordered[is.na(train$reordered)] <- 0

test <- as.data.frame(data[data$eval_set == "test",])
test$eval_set <- NULL
test$user_id <- NULL
test$reordered <- NULL

rm(data)
gc()


# Model -------------------------------------------------------------------
library(xgboost)

params <- list(
 "objective" = "reg:logistic",
 "eval_metric" = "logloss",
 "eta" = 0.1,
 "max_depth" = 6,
 "min_child_weight" = 10,
 "gamma" = 0.70,
 "subsample" = 0.76,
 "colsample_bytree" = 0.95,
 "alpha" = 2e-05,
 "lambda" = 10
)

subtrain <- train %>% sample_frac(0.1)
X <- xgb.DMatrix(as.matrix(subtrain %>% select(-reordered)), label = subtrain$reordered)
model <- xgboost(data = X, params = params, nrounds = 80)

importance <- xgb.importance(colnames(X), model = model)
xgb.ggplot.importance(importance)

rm(X, importance, subtrain)
gc()


# Apply model -------------------------------------------------------------
X <- xgb.DMatrix(as.matrix(test %>% select(-order_id, -product_id)))
test$reordered <- predict(model, X)

test$reordered <- (test$reordered > 0.21) * 1

submission <- test %>%
 filter(reordered == 1) %>%
 group_by(order_id) %>%
 summarise(
 products = paste(product_id, collapse = " ")
 )

missing <- data.frame(
 order_id = unique(test$order_id[!test$order_id %in% submission$order_id]),
 products = "None"
)

submission <- submission %>% bind_rows(missing) %>% arrange(order_id)
write.csv(submission, file = "submit.csv", row.names = F)

¿Largo y confuso?…calma, explicaré lo que me resulta importante.

Pero ahora veamos el mismo código completo en Python.

###########################################################################################################
#
# Kaggle Instacart competition
# Similary to Fabien Vavrand's script , Ago 2017
# Simple xgboost starter, score 0.3791 on LB
# Products selection is based on product by product binary classification, with a global threshold (0.21)
# 
# Daniel Legorreta 
# 
###########################################################################################################

import numpy 
import pandas
from sklearn.preprocessing import LabelEncoder
import xgboost as xgb

# Load Data ---------------------------------------------------------------
path="../input/"

aisles=pd.read_csv(path+"aisles.csv")
departments=pd.read_csv(path+"departments.csv")
orderp=pd.read_csv(path+"order_products__prior.csv")
ordert= pd.read_csv(path+"order_products__train.csv")
orders= pd.read_csv(path+"orders.csv")
products= pd.read_csv(path+"products.csv")

# Reshape data ------------------------------------------------------------

Factor=LabelEncoder()
Factor.fit(aisles.aisle)
aisles['aisle']=Factor.transform(aisles.aisle)
Factor.fit(departments.department)
departments['department']=Factor.transform(departments.department)
Factor.fit(products.product_name)
products['product_name']=Factor.transform(products.product_name)

products=departments.join(aisles\
 .join(products.set_index("aisle_id"),how="inner",on='aisle_id')\
 .set_index("department_id"),how="inner",on='department_id')

del(products['aisle_id'])
del(products['department_id'])
del(aisles,departments)

ordert=pd.merge(ordert,orders[orders.eval_set=='train'][['order_id','user_id']],how='left',on='order_id')
orders_products=pd.merge(orders,orderp, how='inner',on = 'order_id')

del(orderp)

# Products ----------------------------------------------------------------

Aux_4=orders_products[['user_id','order_number','product_id','reordered']]\
 .assign(product_time=orders_products\
 .sort_values(['user_id','order_number','product_id'])\
 .groupby(['user_id','product_id'])\
 .cumcount() + 1)

prd1=Aux_4.groupby('product_id')\
 .apply(lambda x:pd.Series(dict(prod_orders=x.user_id.count(),prod_reorders=x.reordered\
 .sum(),prod_first_orders=(x.product_time == 1).sum(),prod_second_orders=(x.product_time == 2).sum())))

prd1.loc[:,'prod_reorder_probability']=prd1.prod_second_orders/prd1.prod_first_orders
prd1.loc[:,'prod_reorder_times']=1+prd1.prod_reorders/prd1.prod_first_orders
prd1.loc[:,'prod_reorder_ratio']=prd1.prod_reorders/prd1.prod_orders

prd=prd1.drop(['prod_reorders', 'prod_first_orders', 'prod_second_orders'],axis=1)

del(Aux_4,prd1)


# Users -------------------------------------------------------------------


users=orders[orders['eval_set'] == "prior"]\
 .groupby('user_id')\
 .agg({'order_number':'max','days_since_prior_order':['sum','mean']})

users.columns = ["_".join(x) for x in users.columns.ravel()]
users.columns=["user_orders","user_period","user_mean_days_since_prior"]

us=orders_products[['user_id','reordered','order_number','product_id']]\
 .groupby('user_id')\
 .apply(lambda x:pd.Series(dict(
 user_total_products=np.size(x.product_id),user_reorder_ratio = np.divide((x.reordered == 1).sum(),(x.order_number > 1).sum()),user_distinct_products =np.size(x.product_id.unique()))))

users1=pd.merge(users,us, left_index=True, right_index=True,how='inner')
users1.loc[:,'user_average_basket']=users1.user_total_products / users1.user_orders

del(us)

us=orders[orders.eval_set != "prior"][['user_id', 'order_id', 'eval_set','days_since_prior_order']]\
 .rename(index=str, columns={"user_id": "user_id", "order_id": "order_id","eval_set":"eval_set","time_since_last_order":"days_since_prior_order"}) 
users=pd.merge(users1,us,left_index=True,right_on="user_id")

del(us,users1)


# Database ----------------------------------------------------------------

data=orders_products[["user_id", "product_id","order_number","add_to_cart_order"]]\
 .groupby(["user_id", "product_id"])\
 .agg({'order_number':['min','max','size'],'add_to_cart_order':['mean']})

data.columns = ["_".join(x) for x in data.columns.ravel()]
data.reset_index(level=[0,1], inplace=True)
data.columns =["user_id","product_id","up_first_order","up_last_order","up_orders","up_average_cart_position"]

prd.reset_index(level=0, inplace=True)

data=users.join(prd\
 .join(data.set_index("product_id"),on='product_id',how="inner")\
 .set_index("user_id"),on="user_id",how="inner")

data.loc[:,"up_order_rate"]=data.up_orders / data.user_orders
data.loc[:,"up_orders_since_last_order"]=data.user_orders - data.up_last_order
data.loc[:,"up_order_rate_since_first_order"]=data.up_orders / (data.user_orders - data.up_first_order + 1)

data=pd.merge(data,ordert[["user_id", "product_id", "reordered"]],how='left',on=["user_id", "product_id"])

del(ordert,prd,users)


# Train / Test datasets ---------------------------------------------------

train=data[data.eval_set == "train"]
train=train.drop(labels=['eval_set','user_id','product_id','order_id'], axis=1)
train.reordered=train.reordered.fillna(0)

test=data[data.eval_set == "test"]
test=test.drop(labels=['eval_set','user_id','reordered'], axis=1)

del(data)

# Model -------------------------------------------------------------------
import xgboost as xgb


subtrain=train.sample(frac=0.5)

param={'objective':'reg:logistic','eval_metric':'logloss','eta':0.1,'max_depth':6,'min_child_weight':10,
'gamma':0.7,'subsample':0.76,'colsample_bytree':0.95,'alpha':2e-05,'lambda':10}

X_train =xgb.DMatrix(subtrain.drop( "reordered", axis=1), label = subtrain.loc[:,"reordered"])


num_round = 80
model = xgb.train(param, X_train, num_round)

X_test =xgb.DMatrix(test.drop(labels=['order_id','product_id'],axis=1))

test.loc[:,'reordered']=model.predict(X_test)
test.loc[:,'reordered']=np.where(test.reordered>.21,1,0)


test.loc[:,'product_id']=test.product_id.apply(lambda x: str(x))
Submission=test[test.reordered==1][['order_id','product_id']].groupby('order_id')['product_id']\
 .agg(lambda x: ' '.join(x)).reset_index()

missing=pd.DataFrame()
missing.loc[:,'order_id']=np.unique(test.order_id[~test.order_id.isin(Submission.order_id)])
missing.loc[:,'product_id']=None

test_salida = pd.concat([Submission, missing], axis=0)
test_salida.columns=['order_id','products']

#Out writing
test_salida.to_csv("submit/submit4.csv", index=False)

¿Abrumado?…calma, revisemos los detalles que creo que son importantes.

Lo primero a comentar es que los entornos son sencillos, solo requiere unas cuantas bibliotecas para procesar los datos:

  • Por parte de R: data.table, dplyr, tidyr y xgboost.
  • Por parte de Python: numpy, pandas, sklearn y xgboost.

El que sean unas cuantas bibliotecas creo que me parece un buen ejemplo para comparar los dos entornos. Solo se hace uso de lo mínimo en los dos lenguajes, lo estándar para procesar y para construir un modelo de árboles con xgboost. Posiblemente algunos se confundan con algunas partes, si desean entender mejor todas las secciones podría explicar a detalle  pero creo innecesario para esta entrada.

Legibilidad.

Para se un poco justos, considerando que se tienen un nivel básico de R deberían resultar familiares el uso y funcionalidad de los “símbolos”:$,<-,%>%.

En Python debería resultar familiar la asignación con “=”, el uso de “.” y “\”.

Considerando que se tiene ese conocimiento mínimo, uno puede leer el código, pese a que no se conozcan muy bien las funciones y operaciones que se realizan, a mi parecer desde la parte de “#Reshape data” se empieza hacer un poco más difícil de seguir el código de Python.

Pero la situación se agrava cuando pasamos a leer parte del código donde se hace operaciones del tipo Filter+GruopBy+Agragation. Ejemplo en el siguiente extracto de código de la sección “# Users” en R:

users <- orders %>%
 filter(eval_set == "prior") %>%
 group_by(user_id) %>%
 summarise(
 user_orders = max(order_number),
 user_period = sum(days_since_prior_order, na.rm = T),
 user_mean_days_since_prior = mean(days_since_prior_order, na.rm = T)
 )

Para Python:

users=orders[orders['eval_set'] == "prior"]\
 .groupby('user_id')\
 .agg({'order_number':'max','days_since_prior_order':['sum','mean']})

users.columns = ["_".join(x) for x in users.columns.ravel()]
users.columns=["user_orders","user_period","user_mean_days_since_prior"]

Este tipo de operaciones entran en el marco de “split-apply-combine”, sobre este tipo de operaciones hace tiempo escribí unas entradas con ejemplos de como se utilizan y cual es la idea detrás, se pueden leer aquí. Para más detalles de este tipo de operaciones se puede leer los artículos [1,2].

Las operaciones suelen ser muy usadas en sql (SELECT+WHERE+GROUPBY), lo que observo es que pese a que los dos entornos pueden hacer lo mismo , en R se vuelve más legible este tipo de procesamientos y en Python resulta ser menos claras, necesitas tener en mente otro tipo de conceptos como estructuras de datos básicas, diccionarios,funciones lambda, funciones en Numpy, etc.

Si observamos con cuidado, se hacen las siguientes operaciones: Orden->Filtro->GroupBy->Agregación. Existen en pandas la función “filter”, pero no funcionaría similar al filtro condicional que necesitamos. La agrupación es similar en los dos entornos, pero cuando pasamos a crear las nuevas variables o las “agregaciones” resulta fácil entender el código de R pero la parte de Python termina de parecer un poco extraño.

Si bien las dos últimas líneas son para agregar un prefijo y renombrar las columnas, esto podría realizarse de otro modo, pero de igual forma resulta más “oscuro” que el código de R.

El siguiente fracción en R y Python creo muestra otro aspecto que muestra lo confuso que puede ser Python.

us <- orders_products %>%
 group_by(user_id) %>%
 summarise(
 user_total_products = n(),
 user_reorder_ratio = sum(reordered == 1) / sum(order_number > 1),
 user_distinct_products = n_distinct(product_id)
 )

En Python:

us=orders_products[['user_id','reordered','order_number','product_id']]\
 .groupby('user_id')\
 .apply(lambda x:pd.Series(dict(
 user_total_products=np.size(x.product_id),user_reorder_ratio = np.divide((x.reordered == 1).sum(),\
 (x.order_number > 1).sum()),\
 user_distinct_products =np.size(x.product_id.unique()))))

En mi opinión, es fácil pensar en elegir algunas columnas( filter), agrupar los datos con respecto alguna de las variables (groupby) y construir nuevas variables (summarize) que informen aspectos como la cantidad de objetos, la media, etc. Lo cual debería ser fácil traducir la idea a código.

En Python resulta complejo pensar el proceso, debido a que tienes que pensarlo con un nivel de “programación mayor” que el requerido en R. Tampoco es que sean necesarios aspectos sumamente avanzados de programación, pero pensando en la situación de trabajo estándar en R creo que te focalizas más en el proceso y en Python tienes que pensar un poco más en la programación, además de pensar en el proceso.

En varios pasos en Python tienes que pensar en aspectos delicados como los índices de las tablas y aplicar alguna operación sobre ellos. Esto es bueno y malo, puede frustrar en un inicio pero después con algo de práctica son útiles.

En resumen las operaciones son de un tipo estándar en SQL o el manejadores de bases de datos, el entorno de R resulta fácil y te libera para pensar en procesamiento y dejar un poco de lado la programación, en Python creo que te obliga a contar con una idea de programación más demandante que en R.

¿Cuanto de esto es culpa mía? 

Quizás toda, ya que yo escribí la versión en Python, pero confieso que me esforcé en tratar de escribirlo lo más simple y legible y respetar la estructura original del código en R.

Cantidad de Líneas de Código y Eficiencia.

Si copias y pegas el código, veras que aproximadamente es considerablemente más compacto el código en Python, por aproximadamente unas 30 líneas de código.

Podría parecer poco, pero si piensas en que tienes que hacer 10 códigos similares, estamos hablando de 300 líneas menos. Lo cual puede ser bastante ahorro de trabajo.

En buen parte el uso de “%>%” en R y de “\” en Python, simplifican mucho el trabajo, otro aspecto es que Pandas en si hace cierta programación que se asemeja a la programación funcional y el uso de “.” ahorra muchos pequeños pasos.

La eficiencia, pensándola en el uso de la memoria RAM y del tiempo en el procesamiento resultaba considerablemente mejor en Python. Cuando se cargan las tablas y se hace el procesamiento resultaba mucho mejor manejada la memoria, si observas en el código de Python trato de seguir el mismo uso de memoria para que fuera lo más similar posible.

En mi experiencia, Python me da mejor rendimiento que R cuando trabajo con tablas considerablemente grandes y cuando lanzo algún algoritmo suele ser aún más notorio el rendimiento. Esto es mi caso, quizás a otros les resulta mejor el entorno de R.

Un aspecto extra.

Los join, la secuencia de join o merge, pese a que se pueden realizar las mismas operaciones en los dos entornos me sorprendió que cuando hacer una sucesión de estas operaciones, el orden es el contrario en R que en Python. Ejemplo en la siguiente fracción de código:

products <- products %>% 
 inner_join(aisles) %>% inner_join(departments) %>% 
 select(-aisle_id, -department_id)

Para Python:

products=departments.join(aisles\
 .join(products.set_index("aisle_id"),how="inner",on='aisle_id')\
 .set_index("department_id"),how="inner",on='department_id')

El orden de operaciones es Productos -> aisles->departaments, al final es un inner join de las tres tablas. Cuando realizar esta cadena de operaciones haces uso de los indices de cada tabla, para poder combinarlas,

Se observa que el orden es al revés, podría parecer que fue mi culpa, pero al construir y comparar las tablas que se obtienen la secuencia de operaciones debían de seguir ese orden por la relación entre los índices.

Esto yo lo veo del siguiente modo, la cadena de operaciones en R van de afuera hacia dentro, por otro lado en Python para de adentro hacia afuera.

Puede que exagere, pero mi apreciación es que ciertas operaciones en Pandas son pensadas solo para un par de objetos, cuando pasar de esa cantidad se vuelve menos claro y poco intuitivo. Creo que en Pandas puede resultar quizás mentalmente complejo pensar en esas operaciones de manera “natural”.

Creo que en este código se repite lo mismo, el nivel de codificación requerida en Python resulta mayor que en R, que sea bueno o malo, no lo se, depende de nuestra formación y acercamiento ambos entornos.

Conclusión

El ejercicio de hacer el símil de un lenguaje a otro creo que siempre es bueno, ayuda a ver ciertos detalles nos parecen obvios. En lo personal el rendimiento del código en Python me sorprendió, resultaba muchísimo mejor que el código en R. Desconozco los detalles a bajo nivel como para saber si es por el tipo de operaciones entre tablas, el manejo de los índices o si terminar pagando con rendimiento el que sea más legible que se gana en R.

Espero te sirva el ejemplo para comparar el mismo tipo de operaciones y no está de más quizás hacer el ejercicio en un entorno de scala con spark.

Referencias:

  1. The Split-Apply-Combine Strategy for Data Analysis.
  2. Split-Apply-Combine en Pandas.
  3. Los datos se pueden descargar desde aquí.
  4. Los códigos se pueden descargar desde el repositorio dlegor.
Anuncios

Statistical computing with Scala free on-line course

Good!!!

Darren Wilkinson's research blog

I’ve recently delivered a three-day intensive short-course on Scala for statistical computing and data science. The course seemed to go well, and the experience has convinced me that Scala should be used a lot more by statisticians and data scientists for a range of problems in statistical computing. In particular, the simplicity of writing fast efficient parallel algorithms is reason alone to take a careful look at Scala. With a view to helping more statisticians get to grips with Scala, I’ve decided to freely release all of the essential materials associated with the course: the course notes (as PDF), code fragments, complete examples, end-of-chapter exercises, etc. Although I developed the materials with the training course in mind, the course notes are reasonably self-contained, making the course quite suitable for self-study. At some point I will probably flesh out the notes into a proper book, but that will probably take…

Ver la entrada original 113 palabras más

Un introducción al análisis de datos con Python

En la entrada “Algo sobre Python, análisis de datos y machine learning”, comenté en general sobre algunas librerías e instalación de Python, pero no mostré mucho sobre como procesar datos en un sentido amplio y general.

Termina de leer

En esta entrada trato de desarrollar con algunos ejemplos un tono más parecido a lo que uno hace en R project cuando tienen información. Ejemplo, en general cuando se tienen una “tabla” de datos uno revisa ciertos aspectos; si es información que lista para analizar quizás contenga campos sin información (NaN), las variables y el tipo de variables ( categóricas, indicadoras, etc.). Después uno juega con algunos gráficos que ayudan a tener una visión o perspectiva de los datos. Pero hay otro tipo de cosas que uno puede hacer, como “juntar” esa tabla de datos con otra, seleccionar solo ciertas variables, modificar la forma de la tabla para hacer algo más parecido a una tabla pivot,etc.

Este tipo de cosas son en general usuales en R, pero también se pueden hacer en Python( y en Spark), la librería es Pandas. En el sitio correspondiente a la librería se tienen suficiente material y ejemplos para hacer cosas, como seleccionar algunas columnas, seleccionar algunas filas, explorar los Missing Values, hacer operaciones merge, join o concatenación, o por otro lado aplicar la versión local del MapReduce que es Split-Apply-Combine (SAC), etc.

En la página se cuentan con varios manuales breves que pueden ser guías buenas e ilustrativas. La referencia obligada a estudiar o leer para tener un buen acercamiento con este módulo es el libro “Python for Data Analysis”, escrito por el creador del módulo Wes McKinney.

La intención de esta entrada es hacer un recorrido rápido sobre las operaciones básicas, que van desde la carga de datos y la exploración gráfica básica , hasta el procesamiento de los Data.Frame para aplicar técnicas del tipo SAC.

No intento que sea exhaustiva esta entrada, es más una lista de ejemplos breves. Al final de la entrada dejo la liga a un tutorial más amplio ( pero aún así breve) que escribo sobre el uso de Pandas y que dejo alojado en Github.

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Si leíste la entrada hasta aquí, seguro te parecerá raro porqué está marcado buena parte del texto y otro párrafos no. Bueno, después de intentar escribir esta entrada decidí tomarme un poco más en serio el escribir un breve tutorial con ejemplos construidos con datos reales.

Como ya es más o menos escuchado el tema de “datos abiertos” decidí escribir algunos ejemplos de la manipulación de datos con Pandas.

La liga es la siguiente:

https://github.com/dlegor/Tutorial-Pandas-Python

Espero posteriormente remediar lo redactado en esta entrada y terminar el tutorial, pero ya tienen material suficiente para tener un breve pero práctico acercamiento a procesar datos con Python.

—–Pendiente por terminar.

 

 

 

 

Los números de 2015

Los duendes de las estadísticas de WordPress.com prepararon un informe sobre el año 2015 de este blog.

Aquí hay un extracto:

La sala de conciertos de la Ópera de Sydney contiene 2.700 personas. Este blog ha sido visto cerca de 8.800 veces en 2015. Si fuera un concierto en el Sydney Opera House, se se necesitarían alrededor de 3 presentaciones con entradas agotadas para que todos lo vean.

Haz click para ver el reporte completo.

¿Qué es NLP?…procesamiento de lenguaje natural

Las entradas de esta categoría contienen ejemplos de NLP, pero en general la implementación de la librería NLTK de python y su contra parte en tm en R project, pero principalmente los ejemplos son en Python. La idea es compartir de manera breve algunos detalles y ejemplos que en buena parte provienen de la referencia [1] y están disponibles en línea en la referencia [2].

Agrego algunos comentarios o variaciones, la intención con estas entradas es motivar a que se consulten otras fuentes y  comentar más adelante sobre lo avances recientes en Deep Learning en NLP, sobre lo cual se puede consultar la referencia [3].

También comparto unos ejemplos haciendo uso de otros módulo, scikit-learn, para implementar algunas técnicas de Machine Learning.

En caso de estar interesado en investigar sobre una breve introducción teórica y con muchos detalles en explicación,  para mi gusto una buena referencia son los capítulos 22 y 23 de la referencia [4], en el cual se  desarrolla el marco teórico sobre el cual hasta ahora es en parte vigente la investigación en este tipo de temas. Existen nuevas metodologías y nuevas técnicas, sobre material de investigación por la relevancia que se tienen de mejorar los sistemas y técnicas para el procesamiento de lenguaje natural.

Un ejemplo vistoso….qué no me quedó.

Dejo la liga donde puede visualizar un ejemplo de nubes de palabras hechas con el módulo wordcloud de python. Tuve algunos problemas con las fuentes  para poder correrlos en windows y el código para poder hacer que funcione correctamente, así que posteriormente comparto los detalles del ejemplo que tenía pensado compartir. Puede verse el código de ejemplo de Alicia en el mismo repositorio.

alice

Liga del repositorio: https://github.com/amueller/word_cloud

Comentario: Espero posteriormente corregir los problemas del código con las fuentes de windows.

Un ejemplo no tan vistoso

Algunos conceptos que no menciono a detalle son las listas, tuplas y cadenas. Estas son estructuras de datos de python, se cuenta con mucha información sobre cómo se opera con ellas o qué métodos funcionan.

Un ejemplo sencilla para conocer cómo se trabaja con ellas es usar Ipython  es lo siguiente:

#Listas
a=['1','2','3','4']
#type() dice el tipo de datos que es
type(a)
#En Ipython 
a.<taps>
#Despliega una lista de métodos básicos para las listas

#Cadena
b='1234'
type(b)

b.<tabs>
#Despliega una lista de métodos básicos para las cadenas

#Define una tupla
c=("1","2","3","4")
type(c)
c.<tabs> 
#Despliega dos métodos básicos con las tuplas

En las referencias se pueden consultar operaciones con los métodos o con otras funciones de python. Estas estructuras de datos son importantes, debido a que muchos de los textos con cargados como listas o como cadenas  y se opera con ellos mediante sus operaciones naturales.

Debido a que hablar de listas, tuplas y diccionario requiere mucho detalle recomiendo consultar una breve introducción en línea proporcionada por el grupo de Stanford, el cual es breve y preciso respecto a lo que se necesita saber de esos temas:

Liga del tutorial de Stanford: Introducción a Python

Lo siguientes ejemplos pueden ser enriquecidos consultando la referencia [1] y [2], las cuales son sumamente buenas y contienen prácticamente todo lo que uno debe de saber de NLP, sobre todo del manejo de la librería NLTK.

Lo primero es tener el módulo instalado en python, ya que se tenga instalado lo importante es usar los textos o corpus con los que cuenta la librería.

Para instalar el módulo depende de la distribución de python que se instala pero sobre en general se puede usar el módulo pip para instalar cualquier módulo.

Lo básico es lo siguiente:

#NLTK
import nltk
nltk.download()
#Cargar los libros

nltk.book()
*** Introductory Examples for the NLTK Book ***
Loading text1, ..., text9 and sent1, ..., sent9
Type the name of the text or sentence to view it.
Type: 'texts()' or 'sents()' to list the materials.
text1: Moby Dick by Herman Melville 1851
text2: Sense and Sensibility by Jane Austen 1811
text3: The Book of Genesis
text4: Inaugural Address Corpus
text5: Chat Corpus
text6: Monty Python and the Holy Grail
text7: Wall Street Journal
text8: Personals Corpus
text9: The Man Who Was Thursday by G . K . Chesterton 1908

Ya que se tiene esto, se cuenta con 9 textos con los cuales se puede trabajar o aprender a usar las funciones básicas de NLTK.

En la lógica de procesar textos está en medio el concepto central de Corpus, esto en el fondo significa que es un conjunto de textos. La intención en general es estudiar o construir Corpus y analizarlos.

En los libros disponibles en NLTK en algunos casos son un textos y no una colección, pero por ejemplo, el el texto 3, 4 y 8 son Corpus, en otro caso está formado el Corpus por un solo texto. Pero siendo abstracto, un conjunto es no vacío si cuenta por lo menos con un elemento( distinto al vacío, cosa de teoría de conjunto). Pero en resumen el conecto de Corpus en cualquiera de las notaciones del procesamiento de textos significa un conjunto de textos.

Algunas operaciones sencillas son identificar concordancias, cuando una palabra tiene contextos similares o cuando tienen un contexto común. La diferencia entre similar y común, es que en la primera se explora qué otras palabras tienen el mismo contexto y en común se toman dos palabras y se explora si tienen algo en común en el contexto.

Un tema delicado es la definición de “contexto”, esto para un sentido práctica en el procesamiento tienen la idea de identificar al derredor de que palabras aparece la palabra que buscamos.

Entonces como ejemplo, suponiendo que elegimos el Corpus de discursos, aún que están en inglés una palabra que posiblemente aparezca es ‘nation’, así que podemos explorar su concordancia, las palabras similares y eligiendo alguna de las similares podemos explorar si tienen un contexto común.

#Ejemplo
text4.concordance('nation')

#Lo que se vería en la consola es:
#Displaying 25 of 302 matches:
# to the character of an independent nation seems to have been distinguished by
#f Heaven can never be expected on a nation that disregards the eternal rules o
#first , the representatives of this nation , then consisting of little more th.....

#Reviso las palabras similares en el Corpus
text4.similar('nation')

#Lo que se vería es:

#people country government world union time constitu#tion states
#republic law land party earth future other presiden#t strength war
#congress spirit

#Elijo la palabra 'country' para explorar el contexto común con la palabra 'nation'
# Se debe de ingresar en la función como lista , es decir se usa:[]
text4.common_contexts(["nation","country"])

#our_by of_and no_can the_now and_the our_from the_t#he no_has whole_in
#our_that the_in our_i this_to our_today the_for a_w#e our_in the_are
#the_i the_and

Ahora en  breve lo que hacen las funciones anteriores es encontrar el contexto, las palabras que tienen “contexto similar” y compara el contexto común.

Algo que siempre está presente en el análisis de datos es hacer gráficas, entonces una gráfica fácil de hacer es explorar la dispersión de un conjunto de palabras o como se comporta la frecuencia con la cual aparecen ciertas palabras en un Corpus.

El ejemplo lo hago con el Corpus text4 que son discursos.

#Gráfica de dispersión
text4.dispersion_plot(["nation","people","government","law"])

#Gráfica de la frecuencia de las palabras

fdist=FreqDist(text4)
fdist.plot(50,cumulative=True)

Las gráficas que se obtienen son la siguientes:

Dispersión

Frecuencia_de_Palabras

La primera gráfica es similar a la que se construye para hacer una gráfica  “jitter” de un conjunto de datos, de cierto modo muestra las apariciones de las palabras en el texto y la segunda gráfica muestra cuales palabras domina la distribución de la frecuencia de 50 palabras.

Otro aspecto básico cuando se analizan datos, es conocer información numérica, es decir; en este caso sería importante conocer la cantidad de palabras de un texto, cuantas se usaron sin contar repeticiones y quitar las palabras o puntuaciones que puedan ser contadas como caracteres. Esto último está relacionado con el concepto de “tokenización”  del texto.

El código siguiente hace las operaciones anteriores:

#Conteo
len(text4)
145735
#Contando las palabras y símbolos , sin repetición
len(set(text4))
9754
#Porcentaje en el que aparecen las palabras
from __future__ import division

len(text4)/lent(set(text4))

14.941049825712529

#Conteo de apariciones de una palabra en el texto

text4.count('nation')
235

#Porcentaje de apariciones en todo el texto

text4.count('nation')/len(text4)
0.0016125158678423166
text4.count('and')/len(text4)
0.03424709232511065

Esto último no tienen mucha relevancia cuando se piensa en un solo texto, pero pensando que se tienen varios textos que analizar puede resultar interesante como se comporta la densidad de los textos y comparar entre ellos.

Un ejemplo den R.

Para hacer el ejemplo en R uso algunos librerías y un texto; Alicia en el país de las maravillas. Para el ejemplo hago uso de las librerías tm, languageR y ggplot2.

Lo siguiente es más normal a lo que se hace, en el ejemplo en Python ya se contaba con Corpus de ejemplo y se analizaban algunas cosas sobre las palabras, contexto, distribución, etc. En este ejemplo, se construye a partir de un texto un Corpus  y su matriz de términos.También se construye una tabla con información básica de las frecuencias.

No explico a detalle lo que hacen las funciones pero pueden leer un poco respecto a como las uso en las entradas ¿Cuánto se puede saber de los discursos? y Clasificación Binaria.

Las funciones que uso son las siguientes:

library(tm)
library(ggplot2)
library(languageR)

tdm2<-function(doc){
 docCor<-Corpus(VectorSource(doc))
 docs <- tm_map(docCor, stripWhitespace)
 docs <- tm_map(docs, removeWords, stopwords("english"))
 docs <- tm_map(docs, removePunctuation)
 docs <-tm_map(docs,removeNumbers)
 docs <- tm_map(docs,content_transformer(tolower))
 DocsTDM <- TermDocumentMatrix(docs) 
 return(DocsTDM)
}

TablaFreq<-function(TDM){
 docmatrix <- as.matrix(TDM)
 doc.counts <- rowSums(docmatrix)
 doc.df <- data.frame(cbind(names(doc.counts),as.numeric(doc.counts)),stringsAsFactors = FALSE)
 names(doc.df) <- c("Términos", "Frecuencia")
 doc.df$Frecuencia <- as.numeric(doc.df$Frecuencia)
 doc.occurrence <- sapply(1:nrow(docmatrix),
 function(i)
 {
 length(which(docmatrix[i, ] > 0)) / ncol(docmatrix)
 })
 doc.density <- doc.df$Frecuencia / sum(doc.df$Frecuencia)
 
 # Add the term density and occurrence rate
 doc.df <- transform(doc.df,density = doc.density,ocurrencia =doc.occurrence)
 S=head(doc.df[with(doc.df, order(-Frecuencia)),], n=50)
 return(S)
}
 
data(alice)
L=tmd2(alice)
L1=TablaFreq(L)

#Gráfica de frecuencia de palabras.

ggplot(L1,aes(L1$Frecuencia,factor(L1$Términos,levels=L1$Términos)))+geom_point(stat="identity", colour="red")+ggtitle("Tabla de Frecuencias")+xlab("Frecuencia de la palabra")+ylab("Las 50 alabra más frecuentes")

Lo que se hace es construir un Corpus del texto de Alicia en el país de las maravillas, luego se construye una tabla con las frecuencias de las palabras respecto a los que se llama Matriz de términos del texto o corpus, por último se grafica el comportamiento de las 50 palabras más frecuentes.

La gráfica que se obtiene es la siguiente:

Alice_plot

En esta pequeña entrada solo traté de mostrar que existen varias herramientas para el procesamiento del lenguaje natural y de text mining. Si bien no es una entrada muy vistosa, la intención es en las siguientes entradas explicar más detalles y técnicas. En las referencias se encuentra suficiente información para aprender respecto al tema.

Referencias:

1.-http://www.nltk.org/book_1ed/

2.-http://www.nltk.org/book/

3.-http://nlp.stanford.edu/courses/NAACL2013/

4.-http://cs224d.stanford.edu/syllabus.html

5.-http://www.gandhi.com.mx/inteligencia-artificial-un-enfoque-moderno

¿Cuánto se puede saber desde los discursos?

Esto es un juego.

Esta entrada la estuve pensando en la última semana de Septiembre 2015, en especial debido a varios acontecimientos y fechas que se celebran en México. Ejemplo, el 1° de Septiembre se hace entrega del informe anual del gobierno por parte del presidente, el día 15 se celebra el día de la independencia de México, el día 27 de Septiembre del 2014 sucedieron unos hechos lamentables en el estado de Guerrera dónde 43 estudiantes de una Normal Rural fueron “desaparecido”. Pero más aún, con motivo de este último suceso se han realizado manifestaciones y posiblemente se vuelven desde este año actos que se realizarán año con año.

Lo que observé en Facebook, en twitter  y en la prensa, suele ser variado dependiendo del perfil tanto de las personas como de los periódicos. Pero algo que observé fue que las críticas se centraban en los discursos del presidente, sobré algo que decía, sobre lo dicho en la ONU con sede en New York mientras se llevaban acabo manifestaciones en México. Sobre el tipo de palabras, sobre los comentarios, etc.

Algunas frases fueron tomadas como los objetivos centrales de las críticas de dichos discursos. Entiendo que existen un equipo que redacta los discursos del presidente, pero también entiendo que existe un personal que revisa y que verifica el tipo de términos y explicaciones que se dirán. Evito imaginar que se tenga una estrategia para hacer crítica o freno a las aspiraciones políticas de un posible candidato a la presidencia, cuando aún faltan 3 años para la contienda electoral, la entrada la hago sin tener una postura política sobre las conclusiones e interpretaciones que se dieron a los discursos.

Pensé que no sería mala idea jugar con una muestra pequeña de discursos (48) y analizar cosas básicas para tratar de ver cómo ha cambiado la “similaridad” entre los discursos, ver cuáles han sido los tópicos a los largo de los meses y tratar de ver qué se puede saber de manera estadística desde los discursos. Pienso que son otros los capacitados para hacer un análisis de tiempo y espacio donde se dice tal o cual discurso y la relevancia o repercusión de lo dicho.

Esto no desentona en tipo de entradas, ya que no es para nada una manifestación política o una entrada donde trato de mostrar algo negativo del gobierno o donde trato de concluir o deducir algo sobre lo que sucede en el país. Es simplemente jugar con un puñado de datos y ver que se descubre.

Con toda mi ignorancia sobre como interpretar un discurso, me atrevo a suponer de un modo simplista que cada discursos político tienen algo de “localidad” y de “temporalidad”. Con localidad, pienso que depende de donde es emitido, de cuan tan importante es el lugar donde se emite y lo temporal me refiero al momento que rodea a dicho discurso, los acontecimiento cercanos, los sucesos sociales y políticos que acontecen en las fechas en las que se dice dicho discurso. Por otro lado, imagino que otro factor que afecta la “relevancia” de un discurso es gracias a los medios de comunicación, ya que bien un discurso puede ser parte de un evento donde un mandatario comunica algo o puede ser usado como parte de la información que usa y discute un medio de comunicación, el cual termina perneando la opinión publica.

Esto de cierto modo me permite hacer una análisis de textos, clasificar los discursos, detectar tópicos en los discursos, comparar por medio de medidas de similaridad como han cambiado o en qué meses se muestra mayor similaridad, comparar la muestra con respecto al informe y la participación en la ONU, etc. Estos aspectos pueden ser trabajados con una combinación de herramientas y algoritmos, lo cual pensé que sería divertido ver hasta donde se puede saber algo desde los discursos.

Sobre la muestra.

Tomé 48 discursos, 4 por mes desde Septiembre 2014 hasta Septiembre 2015. Los elegí de manera aleatoria, todos fueron guardados en archivos txt. Los  discursos varían, tanto en cantidad de palabras como el tipo de evento en el cual se emitió.

La muestra de discursos fue tratada tanto para hacer un Corpus Global, como uno por cada Mes, y de igual forma se compararon todos los discursos de manera independiente para identificar similaridad.

Lo que decidí hacer.

Escribí unas funciones las cuales me permitían dos cosas, extraer las palabras que aparecen con mayor frecuencia y aplicando LDA tomé 20 palabras del primer tópico detectado. Definí un corpus por año,  por mes y tomé los discursos qué más escuche comentar, el discurso del Informe de Gobierno y el Discursos en la ONU como la pareja inicial a comparar.

Así que use la medida de similaridad de Jaccard, que es una medida muy sencilla para definir entre los discursos cuales eran más cercanos según las palabras más frecuentes y cuales por las 20 palabras detectadas en el primer tópico.

Primero muestro un ejemplo y al final muestro los resultados obtenidos con todos los discursos.

Informe de Gobierno e informe ONU.

El código lo comparto más adelante. Los resultados son los siguientes:

#Comparación entre los discursos de la UNO y el Informe de Gobierno

dir1="C:\\.....ruta.....\\150928_ONU.txt"

dir2="D:\\.....ruta......\\150902_Informe_EPN.txt"

#Extracción del mensaje
Doc1<-msg(dir1)
Doc2<-msg(dir2)

#Contrucción de la matriz de terminos
TMD1<-tdm2(Doc1)
TMD2<-tdm2(Doc2)

#Se contruye su matrix de frecuencias
L1=TablaFreq(TMD1)
L2=TablaFreq(TMD2)

head(L1,5)
#    Términos  Frecuencia density ocurrencia
#356 naciones   15       0.01764706 1
#351 mundo      10       0.01176471 1
#330 méxico      9       0.01058824 1
#391 paz         9       0.01058824 1
#551 unidas      9       0.01058824 1
 
head(L2,5)
#     Términos Frecuencia density ocurrencia
#1597  méxico    92    0.014212884 1
#1603  mil       69    0.010659663 1
#1661  nacional  51    0.007878882 1
#1767  país      51    0.007878882 1
#1596  mexicanos 48    0.007415418 1

Jaccard(head(L1,20),head(L2,20))
#0.1428
#Gráfica
graphFreq(L1)
graphFreq(L2)

#Contruyo el DTM de cada texto
DTM_1=DTM(Doc1)
DTM_2=DTM(Doc2)

#Extraego las priemras 20 palabras asociadas con el primer tópico
top1<-TopicLDA(DTM_1)
top2<-TopicLDA(DTM_2)

top1
[1] "naciones" "derechos" "onu" "organización" "respeto" "agenda" 
[7] "frente" "humanidad" "humanos" "armas" "colectiva" "con" 
[13] "general" "seguridad" "señores" "acción" "clara" "consejo" 
[19] "favor" "futuro" 

top2
[1] "méxico" "con" "mil" "país" "reforma" 
[6] "gobierno" "mayor" "año" "administración" "familias" 
[11] "república" "años" "por" "justicia" "programa" 
[16] "educación" "crecimiento" "condiciones" "partir" "pobreza

Jaccard(top1,top2)
#0.0256

Se observa que hay una similaridad mayor entre los discursos cuando se consideran la palabras con mayor frecuencia o apariciones. La palabra que generó mayor crítica en los medios fue “populismo” y “populistas”. Estas dos palabras hicieron que se criticaran mucho los 2 discursos, lo que muestran los datos es que esas palabras aparecen muy poco, comparado con el efecto que generaron en los medios. De cierto modo son como “palabras singulares” que ejercieron mucho impacto en la apreciación del discurso.

La gráficas de las 50 palabras más citadas son las siguientes:

EPN_ONU

Discurso ONU

EPN_IG

Discurso Informe de Gobierno

Se observa que entre los dos discursos existe una diferencia considerable entre el tipo de palabras que se emplean y la frecuencia, también de la gráfica se puede apreciar que hay cierta “forma” diferente entre las palabras más citadas. Lo cual es notorio que la palabra “méxico” domina el discurso de Informe de Gobierno, por otro lado las palabras “naciones” y “mundo” el discurso de la ONU.

Lo que no muestra gran cambio es el desvanecimiento del color, que representa los cambios de la “densidad” de la frecuencia de las palabras. Concluir algo de estos datos es sutil y posiblemente confuso y atrevido, así que limitándome a lo estadístico se puede decir que hay en estructura diferencias pero muy sutiles, casi no se puede decir nada de esta comparación.

Dándole una interpretación a los tópicos y las mediciones de similaridad, los discursos en cuanto a “estructura”; las palabras más frecuentes, muestran un 14% de similaridad. Pero cuando uno analiza las palabras asociadas al primer tópico detectado, se observa que realmente solo son similares en escaso 2%. Los tópicos me parecen más relevantes, y es notoria la diferencia detectada. Las 20 palabras del primer tópico muestran al discurso de la ONU como algo “global” o “mundial”, y al discurso del informe de gobierno lo muestran como algo “nacional” y de problemas “socio económicos”.

En conclusión; como es de esperar,  se puede decir que los discursos no son tan similares (bajo esta medida de similaridad).

Lo global

Haciendo un corpus con los 48 discursos analizar, puedo comparar con respecto a los otros dos y analizar el comportamiento de la similaridad.

#Procesamiento de los discursos
#Librería para la nube de palabras
library(wordcloud)

dir="D:.....ruta....."
setwd(dir)
#Lista de directorios
filesall<-dir()
#Documentos y corpus
corpusalldoc<-sapply(filesall,function(p)msg(p))
corpusall<-tdm2(corpusalldoc)

#Tabla de frecuencias
Lall=TablaFreq(corpusall)

#Gráfica de frecuencias
graphFreq(Lall)

#Tópicos
DTMall<-DTM(corpusalldoc)
Topic_all<-TopicLDA(DTMall)

#Selección de conjunto de palabras para la nube de palabras
Grupo1<-rowSums(as.matrix(corpusall))
Grupo1<-subset(Grupo1,Grupo1>=30)
m<-as.matrix(Grupo1)
dim(m)
word_freqs = sort(rowSums(m), decreasing=TRUE)
dm = data.frame(word=names(word_freqs), freq=word_freqs)
wordcloud(dm$word, dm$freq, random.order=FALSE, random.color=FALSE,colors=brewer.pal(10,"Dark2"))

La gráfica y la nube de palabras que se obtiene es la siguiente:

Palabras_48 disc

Nube_48disc

Comparando con la métrica de similaridad se tienen lo siguiente:

#Medida de Similaridad
#Palabras más frecuentes
#UNO vs los 48 Discursos
Jaccard(Lall[,1],L1[,1])
#0.123
#Informe de Gobierno vs 48 Discursos
Jaccard(Lall[,1],L2[,1])
#0.282

#Tópicos
#ONU vs los 48 Discursos
Jaccard(Topic_all,top1)
#0.0526
#Informe de Gobiernos vs los 48 Discursos
Jaccard(Topic_all,top2)
#0.1428

Se aprecia que la medida de similaridad es mayor entre el discurso de informe que el de la ONU con respecto al corpus de los discursos. Era de esperar que fuera así, por la naturaleza del discurso y la fecha a la cual corresponde.

Es claro que los dos discursos tienen mayor similaridad con la muestra de discursos en el año, que entre ellos. Las dos preguntas que me hago al observar esto es, ¿cómo se comporta esta medida de similaridad por mes? y ¿cuál discurso muestra mayor similaridad con el de gobierno y el de la ONU?

Por Mes y por Discurso.

Haciendo la comparación por meses, se tienen que a comparar los dos discursos, el de la ONU y el del Informe, se tienen que por corpus construido por mes, se tiene gráficas como las siguientes:

Frec_Meses

El comportamiento por tópicos muestra otro comportamiento, el cual genera la siguiente gráfica:

Topic_Meses

Las gráficas de la métrica por mes muestra lo que uno puede esperar, el discurso de la ONU llega a no tener nada de similaridad en los meses de Marzo y Abril con la frecuencia de palabras, pero peor aún muestra poca similaridad con el primer grupo de tópicos en los meses de Octubre, Diciembre, Enero, Abril, Mayo y Septiembre. Es decir, el discurso dado en la ONU considerando que su primer grupo de tópicos se refiere aspecto mundiales o globales, ese no fue tema en esos meses con respecto a la muestra.

Por otro lado, el discurso del informe uno espera que sea similar al dado cada año o que las palabras que se usan en el mes de Septiembre suelen ser usuales. Eso muestra la primera gráfica, pero además vuelve a ser similar al inicio del año. Por otro lado al considerar los tópicos no muestra el mismo comportamiento, resulta que el mes de Junio es por alguna razón el mes con el cual muestra mayor similaridad. Eso me resulta raro, pero así resultó la medida de similaridad.

Ahora considerando cada uno de los discursos elegidos para analizar, el comportamiento que muestran con la frecuencia de palabras es el siguiente:

Freq_por_discurso

Esta muestra algo un poco más interesante, primero el mes de Septiembre muestra mayor similaridad y me resulta extraño que solo con el mes de Septiembre en el 2014, pero también uno puede observar que el mes de Enero con respecto al informe muestra un comportamiento de alta similaridad. Uno puede pensar que con motivo de inicio del año los discursos suelen ser “alentadores” , “nacionalistas”, “de mejoras” , etc. Esto pienso que puede ser interesante revisar una muestra de varios años y comparar como se comporta conforme pasan los años y quizás muestra estacionalidad la medida de similaridad.

Respecto al informe de la ONU muestra que no es usual que en los discursos se haga uso del mismo tipo de palabras, lo cual uno puede esperarlo ya que no suele decirse mucho del contexto “global”, como en el discursos de la ONU.

La gráfica de los tópicos, muestra el siguiente comportamiento:

 Topic_por_discursoLos tópicos muestran una cosa curiosa, el discurso de la ONU muestra entre los meses de Noviembre-Diciembre una alza, ¿efecto de la navidad para hablar del mundo?..no lo se, esto igual permite que si se hace una muestra mayor analizar si hay algún efecto en el primer grupo de tópicos detectados con la técnicas LDA.

Por otro lado el comportamiento del discurso del Informe muestra una alta similaridad en meses como Enero, Mayo y Junio. De nuevo que el mes de Enero aparezca con valores considerablemente mayores me hace suponer que al inicio del año y a medio año suelen tener este comportamiento de realzar o motivar ciertas cosas “nacionalistas” o “de esperanza” de ser un mejor país. No lo se, solo me hace pensar que teniendo una muestra mayor uno puede empezar hacer cosas más interesantes y jugar a poner algunas hipótesis para experimentar.

 ¿Qué cosas hacer para mejorar esto?

Haciéndome auto-críticas, pienso que hacer una muestra mayor y con discursos de varios años puede resultar más interesante. Por otro lado hacer uso de mejores técnicas o de otras técnicas de medidas de similaridad para explorar como se comportan los discursos con varias medidas. Por último no estaría mal hacer una muestra de otros mandatarios para revisar como evolucionan los tópicos y ver como se comportan ante camios o hechos históricos, cosas de ese estilo.

 Código

Comparto las funciones principales, el resto son muchas líneas de código de loops o de procesar un poco los datos para hacer las gráficas. Por lo cual comparto solo lo más importante del código.

library(tm)
library(NLP)
library(ggplot2)

#######################################
#Mensaje
msg<-function(path){
 con<-file(path,open='rt')
 text<-readLines(con, warn = FALSE, n=-1, encoding = "UCS-2LE")
 close(con)
 return(paste(text, collapse = "\n"))
}

#####################################
#TDM

tdm<-function(doc){
 control<-list(removeWords(stopwords("spanish")),
 removePunctuation,
 removeNumbers,
 content_transformer(tolower),
 minDocFreq=2)
 doc.corpus<-Corpus(VectorSource(doc))
 doc.tdm<-TermDocumentMatrix(doc.corpus,control)
 return(doc.tdm)
}

#######################################
#TDM versión 2
tdm2<-function(doc){
 docCor<-Corpus(VectorSource(doc))
 docs <- tm_map(docCor, stripWhitespace)
 docs <- tm_map(docs, removeWords, stopwords("spanish"))
 docs <- tm_map(docs, removePunctuation)
 docs <-tm_map(docs,removeNumbers)
 docs <- tm_map(docs,content_transformer(tolower))
 DocsTDM <- TermDocumentMatrix(docs) 
 return(DocsTDM)
 }

############################################
#Tabla de frecuencias
TablaFreq<-function(TDM){
 docmatrix <- as.matrix(TDM)
 doc.counts <- rowSums(docmatrix)
 doc.df <- data.frame(cbind(names(doc.counts),as.numeric(doc.counts)),stringsAsFactors = FALSE)
 names(doc.df) <- c("Términos", "Frecuencia")
 doc.df$Frecuencia <- as.numeric(doc.df$Frecuencia)
 doc.occurrence <- sapply(1:nrow(docmatrix),
 function(i)
 {
 length(which(docmatrix[i, ] > 0)) / ncol(docmatrix)
 })
 doc.density <- doc.df$Frecuencia / sum(doc.df$Frecuencia)
 doc.df <- transform(doc.df,density = doc.density,ocurrencia =doc.occurrence)
 S=head(doc.df[with(doc.df, order(-Frecuencia)),], n=50)
 return(S)
 }

##############################################
#Gráfica de frecuencias
graphFreq<-function(L){
 library(ggplot2)
 #Se debe de introducir la matriz con frecuencias
 #Gráfica de palabras y frencia
 p<-ggplot(L,aes(x=factor(Términos, levels=unique(as.character(Términos)) ), y=Frecuencia))+geom_bar(stat = "identity",aes(fill=density))+
 coord_flip()+xlab('Apariciones en el texto')+ylab('Las 50 palabras más frecuentes') 
 return(p)
 }

##########################################
#Función para extraer el Document term Matrix
DTM<-function(Texto){
 docCor<-Corpus(VectorSource(Texto))
 docs <- tm_map(docCor, stripWhitespace)
 docs <- tm_map(docs, removeWords, stopwords("spanish"))
 docs <- tm_map(docs, removePunctuation)
 docs<-tm_map(docs,removeNumbers)
 docs <- tm_map(docs,content_transformer(tolower))
 DocsTDM <- DocumentTermMatrix(docs) 
 return(DocsTDM)
}

################################################
#Topic
TopicLDA<-function(DTMdoc){
 #Regresa las 20 palabras relevantes del primer tópico detectado
 library(topicmodels)
 r.lda=LDA(DTMdoc,method="Gibbs",2)
 L=terms(r.lda,20)
 return(L[,1])
 }

##############################################
#Similaridad de Jaccard
Jaccard<-function(A,B){
 a=length(intersect(A,B))
 b=length(union(A,B))
 a/b
}

Análisis de Grafos

En esta entrada hago una recopilación de varios post de Andrie de Vries. Ya que el ejemplo que realiza se puede replicar sin problema alguno, la muestra de datos es suficientemente grande como para ser interesante y combina varias técnicas para detectar comunidades o clusters, con la finalidad de encontrar los nodos más sobre salientes del grafo.

Una área de investigación importante tanto en matemáticas, en física como en ingeniería; es la investigación en Grafos Grandes o redes largas (Large Graph). Sobre este tema recomiendo la referncia [1]. En lo personal es una área que me gusta y que pese a saber poco, trato de leer y de aprender al respecto, quizás más aspectos teóricos que prácticos; pero suele ser grato tener algunos ejemplos de donde aparecen los objetos matemáticos estudiados.

No puedo, ni pretendo hablar de teoría de grafos o gráficas por que no soy el adecuado para hablar a detalle del tema, pero recomiendo las referencias [2,3] para acercarse al tema desde el lado de algoritmos y desde el punto de vista de matemáticas discretas. Un libro en línea que puede ser una grata introducción a manejar grafos en python se puede ver en la referencia [4].

De que trata el ejemplo.

Hago uso de las idea y parte del código de Andrine Vries para mostrar como procesar un grafo. Este está formado por una muestra de paquetes o librerías de R, así que con más  7000 nodos es interesante aplicar ciertos algoritmos.

Las etapas son las siguientes:

  1. Cargar los datos y explorar parte de ellos.
  2. Aplicar el algoritmos PageRank.
  3. Aplicar técnicas de Cluster para grafos y comparar entre algunos algoritmos.
  4. Mostrar algunas representaciones gráficas sobre el grafo y el impacto de los algoritmos en él.

La idea es explicar el uso de cada librería en los puntos anteriores y el uso de Gephi.

Etapa 1. Generar los datos

Antes de mostrar un ejemplo sencillo de como generar un grafo, la idea de grafo es contar con nodos y con aristas. Donde las aristas conectan a los nodos o se quedan conectando al mismo nodo. Una idea gráfica se puede ver como la siguiente imagen:

usa-7-2

Grafo de libro Fractional Graph Theory

Pero también puede ser algo mucho más extraño, por la cantidad de nodos y la conexión entre las aristas. De modo tal que se pueden ver como:

map-of-internet

Visualización de la Intenet

Entonces la conexión entre redes y grafos pues es natural, prácticamente son lo mismo; pero la perspectiva con la cual la estudian desde el punto de vista de física es distinta a la que se estudia en matemáticas.

En R se tiene la librería miniCRAN con la cual se pueden extraer datos de los paquetes o librerías. Cuando se instala una librería muestra en la mayoría de los casos la solicitud de instalar las librerías con las cuales tiene dependencia en caso de no contar con ellas. Así que hay muchas librerías que tienen dependencia con muchas otras librerías y otras, simplemente con unas cuantas. La mayoría usan las librerías base de R, pero con miniCRAN se pueden ignorar estas y analizar solo las que no son básicas.

#Ejemplo 1
 librería(miniCRAN)

tags<-"ggplot2"

#Se extraen datos de las librerías con las cuales tiene relación de tres modos diferente, considerando o descartando algunas.

pkgDep(tags, suggests=FALSE, enhances=FALSE, includeBasePkgs = TRUE)

pkgDep(tags, suggests=FALSE, enhances=FALSE)

pkgDep(tags, suggests=TRUE, enhances=TRUE)

#Gráfica de las relaciones entre el paquete ggplot2

set.seed(1)

plot(makeDepGraph(tags, includeBasePkgs=FALSE, suggests=TRUE, enhances=TRUE), 
 legendPosEdge = c(-1, 1), legendPosVertex = c(1, 1), vertex.size=30, cex=0.7)

La gráfica que se obtiene es la siguiente:

ggplot2_graphs

Si se replica el código pero para una lista de librerías, se puede pensar en buscar el grafo de ciertas librerías que se usan para tareas relacionadas, ejemplo dplyr y reshape2, ggplot2 y lattice; etc. Pero para mostrar que no todos los paquetes están relacionados se puede considerar en la lista algunas librerías que tienen un uso diferente, como tm y lars.

#Ejemplo 2
library(miniCRAN)

tags<-c("dplyr","reshape2","ggplot2","tm","lattice","data.table","lars")

set.seed(1)

plot(makeDepGraph(tags, includeBasePkgs=FALSE, suggests=TRUE, enhances=TRUE), 
 legendPosEdge = c(-1, 1), legendPosVertex = c(1, 1), vertex.size=15,cex=0.5)

La gráfica que se obtienen es la siguiente:

dplyr-reshape2-data.table-ggplot2-lattice-tm_graphs

Observando la imagen se observa que hay dos nodos; tm y lars, que se muestran “disjuntos” a la red que muestran los otros paquetes. En este caso, se aprecia gráficamente que entre las librerías para procesar datos y generar gráficas existen ciertas relaciones, las cuales pueden ser analizadas.

Entonces la idea es extraer el grafo de todos los paquetes de R hasta hoy 29-09-2015 y aplicar algunos algoritmos para jugar con él. La cantidad de paquetes disponibles es de 7234 hasta hoy, lo cual hace pensar en que una imagen gráfica de dicho grafo es algo complicada para detectar cuales paquetes se aglomeran más que otros, o cuales son casi asilados del resto (como en las imágenes anteriores).

La extracción de los datos se realiza con el siguiente código:

library(miniCRAN)
library(magrittr)

MRAN <- "http://mran.revolutionanalytics.com/snapshot/2015-09-29/"

#Se extrae la matriz con todos los paquetes
pdb <- MRAN %>%
 contrib.url(type = "source") %>%
 available.packages(type="source", filters = NULL)

#Para visualizar el tipo de dato y las primeras 5 filas de la matriz de datos
class(pdb)
head(pdb)

#Se construye un grafo con la matriz

g <- pdb[, "Package"] %>%
 makeDepGraph(availPkgs = pdb, suggests=FALSE, enhances=TRUE, includeBasePkgs = FALSE)

#Se puede ver el tipo de dato
class(g)
head(g)

Etapa 2.Aplicación de PageRank

Por el tamaño del grafo resulta conveniente aplicar algún tipo de algoritmo para detectar algo sobre la relación entre paquetes. La idea de aplicar el algoritmo PageRank para detectar los paquetes que tienen más relevancia en la red.

Para ver detalles sobre el algoritmo les recomiendo la referencia [5]. En breve la idea del algoritmo es asignar un valor numérico a cada paquete y por medio de eso poder definir cuales tienen mayor relevancia según el valor que se le asignó que es un modo de sintetizar el peso o impacto del paquete en los otros paquetes. Esto no le hace el merito adecuado a dicho algoritmo, pero los detalles teóricos se salen de poder explicarse en la entrada.

Aplicando el algoritmo que se tienen implementado en la librería igraph, se obtienen los siguientes resultados:

#Aplicación de pageRank
library(igraph)

#Se aplica el algoritmo
pr <- g %>%
 page.rank(directed = FALSE) %>%
 use_series("vector") %>%
 sort(decreasing = TRUE) %>%
 as.matrix %>%
 set_colnames("page.rank")

head(pr, 10)
# Los 10 paquetes más importantes
#   page.rank
#Rcpp 0.021333171
#MASS 0.019112827
#Matrix 0.009390048
#ggplot2 0.009136545
#mvtnorm 0.008061911
#lattice 0.007902018
#survival 0.007814334
#plyr 0.007025619
#sp 0.004832534
#XML 0.004669243

Haciendo una representación gráfica del orden implementado por el algoritmo PageRank, se obtienen lo siguiente:

#Top 10
set.seed(40)
pr %>%
 head(10) %>%
 rownames %>%
 makeDepGraph(pdb) %>%
 plot(main="Top de paquetes", cex=0.5)

#Top 30
set.seed(40)
pr %>%
 head(30) %>%
 rownames %>%
 makeDepGraph(pdb) %>%
 plot(main="Top 30 de paquetes", cex=0.5)

Las gráficas que se obtienen son las siguientes:

top_10

Top_30

Se observa que el grafo de los primeros 10 es más o menos claro, pero que el de los primeros 30 ya empieza a mostrar cierta complejidad o se muestra más complicado para determinar visualmente algunos aspectos, pese a eso se aprecian en la imagen un par de paquetes asilados.

Etapa 3.-Detectando comunidades.

Los clusters, son parte de las técnicas de aprendizaje no supervidado en Machine Learning. Existen muchas técnicas o algoritmos, algunos al paso del tiempo ya sea por la eficiencia computacional o por el tipo de resultados que han permitido obtener terminan siendo predominantes o persistir ante la aparición de nuevas técnicas o algoritmos.

En la entrada “Análisis de Cluster, un ejemplo sencillo” mostré como usar dicha técnica con unos conjuntos de datos. La situación cambia un poco cuando se trabaja sobre grafos o redes, ya que se tiene en particular un tipo de datos (el grafo) sobre el cual se hace uso de sus propiedades para definir los algoritmos para detectar clusters.

En las referencias [6,7,8,9] se puede leer varios exploraciones experimentales sobre el tipo de algoritmos que más predominan en las investigaciones y al implementar dichos algoritmos. Aplico dos algoritmos para detectar comunidades o clusters en el grafo y mido la similaridad entre las dos comunidades con la medida de similaridad de Jaccard para comparar los resultados obtenidos entre los dos algoritmos.

Siguiendo a Andrie de Vries, solo tomo el 80% de la red de los cuales se eligen los nodos más importantes según el algoritmo PageRank.

#Se aplica al mismo grafo ordenado por PageRank pk
#Se selecciona el 80% de los datos

cutoff <- quantile(pr[, "page.rank"], probs = 0.2)
popular <- pr[pr[, "page.rank"] >= cutoff, ] 
toKeep <- names(popular)

vids <- V(g)[toKeep]
gs <- induced.subgraph(g, vids = toKeep)

#Aplicación del primer algoritmo walktrap community
#Se obtienen 2118 comunidades

cl <- walktrap.community(gs, steps = 3)

#Se ordenas
topClusters <- table(cl$membership) %>% 
 sort(decreasing = TRUE) %>% 
 head(25)

#Se gráfica el comportamiento de los cluster o de las comunidades

plot(topClusters, main="Tamaño Cluster", ylab="Numero de cluster", type="b", lwd=2)

#Se aplica el segundo algoritmo, infoMaps community
#Se obtienen 2280 comunidades

cl1 <- infomap.community(gs)

topClusters1 <- table(cl1$membership) %>% 
 sort(decreasing = TRUE) %>% 
 head(25)

plot(topClusters1, main="Tamaño de Clusters", ylab="Numero de clusters", type="b", lwd=2)

Cluster_walktrap

InfoMaps_clusters

Las gráficas muestran el comportamiento de los cluster o comunidades, lo cual muestra que se pueden elegir los primero 10 clusters como los más importantes. Siguiendo a Andrie de Vries se eligen los 10 clusters más importantes.

Para comparar el comportamiento entre los dos algoritmos; además de que el primero detecta 2118 comunidades y el segundo 2280, comparo los 10 cluster más importantes por medio del índice de similaridad de Jaccard. Para los que tienen nociones de teoría de conjuntos, la idea es muy sencilla es comparar cuantos elementos están en la intersección dividido entre los elementos en la unión. Esto da una medida de similaridad, con la cual comparo entre las 10 comunidades más importantes por algoritmo.

#Eligiendo los 10 clusters más importantes

cluster <- function(i, clusters, pagerank, n=10){
 group <- clusters$names[clusters$membership == i]
 pagerank[group, ] %>% sort(decreasing = TRUE) %>% head(n)
}

#Para walktrap
z1 <- lapply(names(topClusters)[1:10], cluster, clusters=cl, pagerank=pr, n=20)

#Para infoMaps
z2 <- lapply(names(topClusters1)[1:10], cluster, clusters=cl1, pagerank=pr, n=20)

#Comparación entre los cluster de cada algoritmo

Jaccard<-function(A,B){
 a=length(intersect(A,B))
 b=length(union(A,B))
 a/b
 }
s=vector(length=10)

for(i in 1:10){
 L1=names(z1[[i]])
 L2=names(z2[[i]])
 s[i]=Jaccard(L1,L2)
 }

library(ggplot2)
t=1:10
qplot(x=t,y=s,geom=c("line", "point"), main="Similaridad entre los dos algoritmos", xlab="Número de Cluster",ylab="Valor de Similaridad")

Similaridad

Se observa que en los dos algoritmos en el primer cluster se tienen más similaridad, la lista de librerías por algoritmo son las siguientes:

#Para el algoritmo Walktrap
names(z1[[1]])
 [1] "MASS" "Matrix" "mvtnorm" "lattice" 
 [5] "survival" "igraph" "coda" "nlme" 
 [9] "rgl" "boot" "RColorBrewer" "numDeriv" 
[13] "Hmisc" "mgcv" "cluster" "gtools" 
[17] "car" "fields" "lme4" "xtable" 

#Para el algoritmo infoMaps
names(z2[[1]])
 [1] "Rcpp" "plyr" "XML" "stringr" 
 [5] "reshape2" "foreach" "jsonlite" "rJava" 
 [9] "cluster" "car" "fields" "lme4" 
[13] "corpcor" "e1071" "doParallel" "vegan" 
[17] "quadprog" "rpart" "randomForest" "plotrix"

La interpretación o explicación de los resultados puede varias, ejemplo en los conjuntos de librerías anteriores aparece una combinación entre librerías para procesar datos, para graficar, para aplicar modelos predictivos o de clasificación, etc.

Para visualizar el grafo de las librerías, la intención era mostrarlo con Gephi pero tuve un problema con el programa y hasta que después repare o vea que es lo que tiene comparto la visualización de toda la red. Pero con igraph se puede hacer una imagen buena de como se comportan los cluster en la red.

#Uso de igraph, parte de código de Andrei de Vries

#Se carga la librería
library(igraph)

# Se preparan los datos para construir la gráfica

cl$degree <-(degree(gs)[cl$names])
cl$cluster<-unname(ave(cl$degree, cl$membership,FUN=function(x)names(x)[which.max(x)]))
V(gs)$name <- cl$cluster

E(gs)$weight <- 1
V(gs)$weight <- 1

gcon<-contract.vertices(gs, cl$membership,vertex.attr.comb = list(weight = "sum", name = function(x)x[1], "ignore"))

gcon<-simplify(gcon, edge.attr.comb = list(weight = "sum", function(x)length(x)))

gcc<-induced.subgraph(gcon, V(gcon)$weight > 20)

V(gcc)$degree<-unname(degree(gcc))

#construcción de la gráfica
par(mar = rep(0.1, 4)) 
g.layout<-layout.kamada.kawai(gcc)
plot.igraph(gcc, edge.arrow.size = 0.1, layout = g.layout, vertex.size = 0.5 * (V(gcc)$degree))

La imagen de grafo que se obtiene es el siguiente:

Grafo1_vis

Se muestra en la imagen como la librería con círculos mayores tienen mayor relevancia en la parte de la red, por su puesto que no están las 7000 librerías representadas pero esta imagen da idea de cuales son las librerías más importantes o las más usadas, partiendo de la clasificación hecha con el algoritmo walktrap y pagerank.

Espero la entrada de una idea como tratar en general datos que pueden ser analizados como un grafo, cabe mencionar que cuando se analiza un corpus de textos o un texto, este también puede ser analizado y explorado visualizando su matriz de términos como la matriz asociada a un grafo y eso hace que se tengan más herramientas para analizar lo que sucede con dicho texto o la relevancia de sus tópicos.

Referencias:

  1. Large Network and graph limits.Lásló Lovasz
  2. Estructura de datos y algoritmos.
  3. Graph Theory
  4. http://interactivepython.org/runestone/static/pythonds/index.html
  5. http://www.mmds.org/
  6. http://www-personal.umich.edu/~mejn/papers/epjb.pdf
  7. http://arxiv.org/abs/1004.3539
  8. http://deim.urv.cat/~clara.granell/publications/ijcss_clara.pdf
  9. http://www.syssec-project.eu/m/page-media/3/moradi-sea12.pdf