Preprocesamiento, feature selections, limpieza de datos…cuánto importan para un modelo?

Hace una semanas terminó la competencia “Pover-T Tests: Predicting Proverty”. Esta fue propuesta y soportada por el Banco Mundial y se desarrollo sobre la plataforma drivendata.com.

El problema de fondo es interesante por ciertos aspectos que mencionaré más adelante. Mi posición final fue 330 de 2310 participantes. No es una posición para presumir, pero decidí contar un poco lo que no hice y lo poco que hice La moraleja; por decirlo de algún modo, “lo simple suele dar buenos resultados”.

La competencia

Para ver un detalle sobre los datos y la descripción se puede visitar la página de drivedata.com. Para el fines de esta entrada los aspectos interesantes que tenían los datos eran:

  • Muchas variables, casi todas variables categóricas.
  • Un problema de clasificación binaria, que es algo que se puede considerar estándar y sencillo.
  • El target o variable respuesta para los 3 países era no balanceada.
  • Algunos Missing Values, nada abrumador.
  • Se contaba con información agregada a nivel país y otra desde una muestra a nivel individuo. Al final, integrar la información hacía tener un problema con muchas más variables.

En el fondo era casi un mismo problema para los 3 países que se modelaron, los países fueron etiquetados como países: A, B y C.

El país con mayor cantidad de variables fue B, seguido de C y por último A. Todos tenían más de 200 variables, la pocas variables que no eran directamente categóricas eran numéricas que representaban categorías (a mi parecer). Este era un problema parecido a la mayoría de problemas que uno afronta en la vida laboral, es mucho mas natural en los negocios tener variables categóricas que variables continuas.

Pero, ¿qué hacer con tantas categorías?…transformar todas en binarias( one-hot encoding), hacer binarización, hacer hasher…enmbeding desde una red neuronal, autoencoding, hacer proyecciones desde el target…dejar solo los niveles como etiquetas numéricas, aplicar cat2vector…en fin, sobran opciones.

Algo que creo es normal, es que mentalmente se le ocurren a uno mil y un maneras de hacer algo, recuerda alguna técnica que leyó hace poco o algo que vio sobre como tratar tal o cual tipo de datos, pero siempre es más sencillo pensar que hacerlo, así que cuando uno caen en consciencia querer revisar todo lo pensado se vuelve tarea interminable.

Otro problema, inmediato uno se podía preguntar,¿todas las variables importan?…por lo general no..pero depende del modelo, pero también podemos correr un modelo sin considerar eliminar ninguna variable y dejar que el modelo en “automático” no considere importante esa variable. Pero de nuevo las técnicas para elegir variables son más de una y todas tienen pros y contras, la lista mental de posibilidades entre la selección y el preprocesamiento se vuelve ya una tarea grande…y lo importante, ¿qué modelo usar?…será mejor para este tipo de datos…tal o cual familia de modelos…una regresión logística, o un SVM, o un ensamble de árboles,…etc.

Así que juntando todo lo que posiblemente podemos hacer de preprocesamiento, selección de variables y test de modelos..ya es una trabajo que requiere su tiempo, pero falta cosas importantes como limpieza de los datos ( tratamiento de los missing data) , la estrategia de validación cruzada, selección de parámetros, peor aún recuerdo que son 3 países diferentes con conjuntos de variables diferentes. En fin, la cantidad de trabajo ya es cosa de locos.

Y por cierto,  ¿no tendría que hacer una exploración previa?,¿buscar patrones que me “clarifique” el problema?…o sea, más tareas por hacer.

Cuando pienso en todo lo que ya tienen uno en la lista de cosas por hacer para un problema y pensar en aplicarlo en 3 escenarios diferentes; por los 3 países, resulta terrible como trabajo para dedicarle solo unas horas. ¿Qué hacer?

Simple, pero no más simple.

Exploración, Data Cleaning y Preprocesamiento

A menudo cuando lees o escuchas a gente que habla de machine Learning o aprendizaje estadístico, de ciencia de datos o de Inteligencia artificial, hace ver todo como si fuera sumamente complejo. Hacen ver como si cada etapa fuera algo casi mágico o donde solo algunos cuantos pueden hacerlo “bien”. Yo le llamo ” postura purista”, por que suena correcto desde un punto de vista académico.

A mi no me gusta esa pedantería, ni siquiera me gusta el nombre de científico de datos, pero en lo siguiente cuento las cosas que hice de tres etapas: exploración, limpieza y preprocesamiento.

Exploración

Gastar mucho tiempo explorando la relación entre las variables con respecto al target o entre ellas puede llegar a ser meramente perdida de tiempo. Si bien es lo más “bonito”, por que es muy interesante hacer gráficos grandiosos, ¿cuánto ayudan al momento de construir un modelo?

Siempre ayuda a tener alguna idea sobre el problema haciendo alguna exploración, algunas gráficas, pero en ocasiones veo más esfuerzo en la exploración gráfica de lo que creo puede ayudar,  buscar usar gráficos complejos solo hacen ver interesante los datos y el problema, no suele ayudar a resolver el problema. Ejemplo, podrías tener 1000 series de tiempo y explorar y graficar la relación entre ellas, tomar diferentes periodos de tiempo y ver cosas como los “picos” u outliers, ver la relación entre la correlación con un mapa de calor, etc. Pero es posible que todos esos gráficos sean tan bonitos como inútiles a la hora de construir un modelo.

Así que no está mal hacer gráficos, pero en ocasiones lo más simple te puede decir casi lo más importante de tus variables y del problema. Al final un modelo no se construye con gráficas.

En este caso, solo hice algunas exploraciones menores. Explorar el balance de las variables binarias, del target y de las categorías. Explorar la relación entre las variables con respecto al target con chi2 y nada más.

Limpieza o Data Cleaning

“Data Cleaning” suele llamarse a las técnicas para tratar los missing values o valores ausentes, suelen decir que lo recomendable es …blablablabla, ¿cuánto importante es aplicar tantas técnicas de data cleaning?…casi nada. No digo que no importe, pero en general las cosas simples y el sentido común ayudan más que alguna técnica robusta.

Esto no implica que todas las investigaciones que se realizan sobre ese problema no aporten, pero al momento de hacer algo práctico resultan no siempre aportar mucho.

En ocasiones, lo más simple y obvio es lo más adecuado. La mayoría de técnicas suelen enfocarse a datos continuos o variables que son del tipo 1.2,3.5.4.5,..ect. Entonces te dicen algunos que “mad” es lo mejor, pero otros dicen que la media o la mediana o el máximo o el mínimo…, pero en ocasiones basta con tomar para esos valores 0 o -1, o -999999, etc. En realidad, hay técnicas mucho más robustas que sustituir por un valor, dan buenos resultados pero conlleva a varios costos.

El primero, complejidad en el modelo y segundo “agregamos” datos que posiblemente nos afecta ocasionando sobre estimación al modelo.

Para este caso, lo más simple fue lo mejor, sustituir esos valores ausentes por cero…nada más, nada especial.

Preprocesamiento o feature engineering

La tarea después de hacer exploración y limpieza es pensar en como preprocesar o procesar. Cuando tenemos más de una fuente de datos necesitaremos integrar toda la información o consolidar en una base que sea viable para construir el modelo.

La mayoría de operaciones al integrar son operaciones usuales en SQL, agrupaciones, join, etc.

Para el procesamiento se puede pensar en como tratar las variables o qué transformaciones hacer con ellas.

Como preprocesamiento hay mucho por hacer, desde construir nuevas variables hasta “limpiar” los datos. En especial esta muestra de datos tenían algunos missing values..pero,¿qué hacer con tantas variables?

En general muchos científicos de datos tienen sus sesgo por tal o cual técnica, pero realmente en mi experiencia hacer o definir preprocesamiento e ingeniería de variables (feature engineering) es mucho más arte que ciencia. Pese a las miles de técnicas no hay nada que aseguré que tal o cual técnica es la mejor, siempre depende del problema y en lo personal como regla pienso que cuando las variables que estas construyendo son muy complejas, es muy probable que ayuden poco al modelo, así que casi siempre las técnicas más simples funcionan mejor.

En conclusión, lo simple suele dar buenos resultados, quizás no los mejores pero si resultados competitivos con una inversión de tiempo mucho menor. Al final, lo mejor es experimentar, jugar con diferentes técnicas pero no cegarnos esperando que la nueva técnica será la mejor para nuestro problema.

Selección de Variables y Modelos

Como si no fuera suficiente tener que hacer todas las etapas previas, cuando uno busca técnicas sobre selección de variables y de modelos la cantidad de recomendaciones sobran.

Pero,¿cuál elegir?…yo en lo personal desde hace un tiempo decidí leer las nuevas recomendaciones pero siempre considerar las técnicas más sencillas y experimentar con 2 o 3 técnicas, si tengo un poco más de tiempo invertir en alguna nueva técnicas o método.

Para este problema use dos métodos sencillos para seleccionar variables: Boruta y Selección desde ExtraTree. Los dos métodos o algoritmos requieren poca inversión de tiempo y considerablemente poco costo de computo.

Modelos

La lista de posibles modelos para un problema de clasificación es una mega lista, desde la regresión logística hasta alguna red neuronal..sin olvidar pensar en ensamblar modelos o usar un meta modelo tipo Adaboosting, etc. En fin, para este problema decidí experimentar con pocos modelos, todos basados en árboles.

Cuando empecé a ver el problema se me ocurrió hacer mil y un cosas para probar diferentes modelos, pero recordé un caso que me resulta curioso. En el 2014 se publicó una competencia en Kaggle sobre datos del experimento Atlas que tiene era la búsqueda del Higgs, en dicha competencia los 3 ganadores usaron redes y árboles. El primer lugar usó más de 80 redes y árboles, el tercer lugar otra cantidad considerable de redes y árboles, pero el segundo lugar usó solo 6 modelos. Si, solo 6 y un ensamble de ellos. Eso me sorprendió muchísimo, pensar en la inversión de esfuerzo de hacer 80 modelos hacer solo 6 es increíble, sobra decir que la persona que desarrolló esta solución hoy esta con el equipo de OpenAI.

Esos 6 modelos fueron construidos usando “regularized greedy forest” que es otra alternativa al tipo de modelos de ensamble de árboles. Había probado antes este modelo con algunos datos, lo que en especial me parece mejor que xgboosting es que la cantidad de parámetros e hiperparámetros en menor y esto al momento de desarrollar un modelo ayuda mucho. Ya que modelos tipo xgboosting y Lightgbm requieren tener cuidado con la selección de parámetros.

Hice pruebas con dos modelos y decidí ensamblar de manera simple, media aritmética. Para comparar este tipo de modelos con respecto a otro algoritmo de árboles, usé ExtraTree el cual con respecto a RandomForest presenta ciertos beneficios tanto en tiempo de computo como en rendimiento.

Para el caso del modelo ExtraTree decidí probar la selección de parámetros con una búsqueda exhaustiva, pero como era de esperarse el tiempo requerido era considerable y aborté ese acercamiento por una búsqueda aleatoria de parámetros (Randomized search). El tiempo requerido era de esperarse, disminuyó muchísimo y el performance del modelo fue bueno.

Como al final eran 3 diferentes países, en el fondo era hacer por lo menos 3 diferentes modelos. Así que la selección de variables, la selección de parámetros y la estimación de los modelos resulto requerir poco tiempo.

Y de nueva cuenta, lo simple puede dar buenos resultado, no gané pero la posición no creo que fuera mala con respecto a lo que hice. Al ser tantas variables y tener “una señal tenue”, los dos modelos hicieron desde mi punto de vista un buen trabajo, el código era breve y el tiempo de computo también.

Seguro haciendo una experimentación minuciosa y cuidadosa con diferentes técnicas y diferentes modelos quizás el resultado hubiera sido mejor, pero eso es exactamente lo que no quería hacer.

Quizás lo más importante

La validación cruzada creo en lo más importante sea cual sea el camino que decidimos tomar para hacer todo el proceso de construcción de un modelo, en este punto no esta mal no escatimar en el tiempo invertido y el esfuerzo mental y de computo necesario para hacer una buena validación cruzada.

Al final podemos elegir parámetros mediante un proceso de validación, pero también podemos hacer una revisión mediante validación cruzada del rendimiento y estabilidad de nuestro modelo antes de lanzarnos a estimar todo.

La estrategia básica es la que casi todos recomiendan, separar los datos de entrenamiento y dejar un parte para validación. Pero no es la única ni tampoco la que mejor, al final depende el problema.

En mi caso use kfolders, en varias etapas y al final para ver la estabilidad del modelo con respecto a los datos. Probé considerar 5 y 7 folder, al final lo que obtenía era que los modelos se mostraban estables y el resultado en la competencia lo mostró ya que al finalizar dicha competencia se ejecuta con el resto de data test y no vario mi posición mucho, al contrario aumente posiciones.

Conclusión:

Después de mucho tiempo de pensar en los mil y un detalles que todos los científicos de datos nos dicen  que debemos de hacer para volvernos un verdadero científico de datos (sobre todo los que se vuelven influencer en linkenId, facebook o instagram ), en la práctica pocas veces esos científicos de datos nos muestran lo que hacen y eso me hace desconfiar de lo que dicen, que posiblemente digan algunas cosas ciertas pero que en la practica no funcionan como las dicen.

Así que mi recomendación es no creer del todo y aprender y practicar con lo simple e ir experimentando paso a paso. Al final el camino para aprender algo es seguir estudiándolo y experimentar con ello la mayor cantidad de veces posibles.

Promesa: prometo compartir el código en un futuro, necesito revisarlo y probarlo para que lo ejecuten sin problema.

Anuncios

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.

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
}