Pipeline en Pandas.

Introducción.

Pandas para muchos de los que hemos estado jugando con el entorno de PyData (Pandas, Matplotlib,Numpy, SciPy,Scikit-learn,entre otras bibliotecas) es fundamental. Al inicio, cuando llegas a una terminal con Ipython o un notebook de Jupyter y trabajas con Pandas, parece no tener mucho sentido esforzarte tanto en aprender Pandas si puedes hacer lo mismo en Excel o R.

Cuando vienes de este último; R, parece menos intuitivo Pandas y como en otras ocasiones mencioné, parece obligarte a programar más y saber más sobre programación, posiblemente es cierto.

Comentario: Estoy preparando otra breve guía para aprender a manipular datos desde Pandas, con algunos detalles desde lo básico hasta nivel intermedio, en las próximas semanas lo comparto. 

Regresando al tema, Pandas es fundamental para hacer cualquier mini proyecto, para aprender aspectos de machine learning o simplemente para manipular datos desde Python.  Pero como todo proyecto o herramienta de manipulación de datos, no inventa el “hilo negro”; hay aspectos o funcionalidades que permiten replicar lo que se realizar en otros entornos. Así que mucho de lo que se puede hacer en Excel o R, también se puede hacer en Pandas.

Para esta entrada decidí comentar y explicar algo que es desde mi punto de vista, alta mente recomendable aprender y aplicar, pipeline o métodos chaining. Estos, como su nombre lo dice, consisten en aplicar una cadena de operaciones o manipulaciones sobre los datos para llegar aún resultado final.

Es frecuente que los códigos de Python con uso de Pandas suelen definir pasos, tras pasos, tras pasos, esto quizás lo podemos llamar como “Procesamiento de datos Imperativo” (Data Processing Imperative). Pero en la práctica en muchas ocasiones hacer un cambio a este tipo de código puede ser un dolor de cabeza.

La otra opción es hacer “Procesamiento de Datos Declarativo” (Data processing Declarative). Esto trata de abstraer ciertas transformaciones, procesos o funciones que aplicamos a los datos. Reduce el código y una vez entendido su funcionamiento, facilita muchísimo aplicar algún cambio.

Métodos de Encadenamiento (Methods Chaining) o Pipeline.

Si tienes experiencia con Scikit-Learn o  has estado aprendiendo a usar esta biblioteca, es posible que hayas leído la sección Pipeline. La diferencia con respecto a esa sección de Scikit-learn y con lo que comentaré, es que los pipeline en scikit-learn son aplicados para construir flujos de trabajo (methods chaining) donde apliquemos algún conjunto de algoritmos o preprocesamientos necesarios para esos algoritmos.

En el ejemplo de esta entrada solo abordo los ejemplos para trabajar con los Datos desde el entorno de Pandas y aplicar preprocesamientos sobre los DataFrame de pandas (sea o no que aplicaremos un algoritmos de Machine Learning).

Antes de seguir les comparto este ejemplo de código:

#Cargamos la biblioteca
import pandas as pd

#Consideramos un archivo con datos de nombre
# Incidentes.csv

Data=pd.read_csv("Incidentes.csv")

#Eliminamos la columna REGISTRO_INTERNO

Data.drop(labels=['REGISTRO_INTERNO'],axis=1,inplace=True)

#Cambiamos el tipo de datos de la variable FECHA
Data.loc[:,'FECHA']=pd.to_datetime(Data.FECHA)

Como observamos, imaginemos que sabemos que la variable REGISTRO_INTERNO siempre viene en nuestro archivo que nos manda cierto departamento o sistema, luego queremos manipular las fechas pero cargamos los datos sin especificar el tipo de dato. En fin, esas 3 líneas se pueden juntar para hacer un pequeño pipeline o una cadena de métodos, no muy interesante, pero es un primer ejemplo.

# Cargamos los datos y aplicamos las transformaciones
Data=(pd.read_csv("Incidentes.csv")
         .drop(labels=['REGISTRO_INTERNO'],axis=1,inplace=True)
         .assing(FECHA=lambda x:pd.to_datetime(x['FECHA'])))

Observamos en el código que solo lo que hice fue unir las operaciones que hacia por separado en cada paso. ¿Nos ahorramos algo? …realmente no, solo le dimos mejor organización visual a las operaciones que aplicamos y resulta sencillo hacer ligeros cambios.

Entonces, ese breve ejemplo muestra que dentro de Pandas podemos unir operaciones haciendo uso de un conjunto de transformaciones aplicadas al DataFrame dentro de  paréntesis “()” y conectando por uso de “.”, ¿sencillo?.

Comentario: la función “assign” lo que permite es crear una nueva variable en el DataFrame, para el ejemplo solo remplazamos la variable que teníamos por una nueva con el mismo nombre pero con otro tipo de dato.

Ahora un ejemplo menos sencillo y donde usaremos algunos aspectos interesantes de Pandas. Suponemos que tenemos 2 conjuntos de datos; train y test, estos queremos  varias cosas:

  • Unir las tablas.
  • Crear una variable con varias variables que son texto.
  • Queremos algunas medidas de las cadenas de texto, como longitud de la cadena y la razón entre la cantidad de palabras sin repeticiones sobre  el total de palabras en cada cadena.
  • Queremos aplicar una transformación logaritmo.
  • Queremos obtener el día de la semana y el mes de una variable tipo Fecha.

Esto de manera imperativa o definiendo los pasos por líneas, resulta tedioso, hacer paso por paso y estar creando y eliminando columnas del nuestro DataFrame.  Aquí es donde resulta útil aplicar funciones como “pipe” y  “assign” sobre un DataFrame.

Pensando en abstracto, separo lo que necesito hacer sobre sobre alguna variable y aquello donde necesito un proceso sobre el DataFrame.

La lista de cosas que haré sobre los datos las separo como sigue:

  • Unir las tablas (Se puede pensar como una operación entre las tablas).
  • Crear una variable con varias variables que son texto ( necesito una función que se aplique sobre el DataFrame).
  •   Queremos algunas medidas de las cadenas de texto, como longitud de la cadena y la razón entre la cantidad de palabras sin repeticiones sobre  el total de palabras en cada cadena. (No quiero columnas innecesarias, sería bueno aplicar un proceso sobre el DataFrame).
  • Queremos aplicar una transformación logaritmo (solo requiero usar una columna o variable).
  • Queremos obtener el día de la semana y el mes de la Fecha (solo requiero ir usando una columna o variable).

Aquí el trabajo se divide en usar transformaciones o funcionalidades de pandas y la función “assign”. Para todo lo que necesito procesar u operar sobre el DataFrame uso “pipe”.

El código en una versión puede ser este:

#Cargamos pandas
import pandas as pd
import numpy as np

#Cargamos los datos por separado
tr =pd.read_csv("../input/train.csv") 
te =pd.read_csv("../input/test.csv")

#Función para concatenar todas las variables 
# que deseamos como texto.
 
def Concat_Text(df,Columns,Name):
df=df.copy()
df.loc[:,Columns].fillna(" ",inplace=True)
df[Name]=df[Columns[0]].astype('str')
for col in Columns[1:]:
df[Name]=df[Name]+' '+df[col].astype('str')
return df

#Función para agregar el conteo de palabras
# y la razón entre la cantidad de únicas 
#y la cadena como tal.

def Ratio_Words(df):
df=df.copy()
df['description']=df['description'].astype('str')
df['num_words_description']=df['description'].apply(lambda x:len(x.split()))
Unique_Words=df['description'].apply(lambda x: len(set(x.split())))
df['Ratio_Words_description']=Unique_Words/df['num_words_description']
return df

#Pipeline sobre nuestros DataFrame
tr_te=tr[tr.columns.difference(["deal_probability"])].append(te)\
.pipe(Concat_Text,['city','param_1','param_2','param_3'],'txt1')\
.pipe(Concat_Text,['title','description'],'txt2')\
.pipe(Ratio_Words)\
.assign( category_name=lambda x: pd.Categorical(x['category_name']).codes,
user_type=lambda x:pd.Categorical(x['user_type']).codes,
param_1=lambda x:lb.fit_transform(x['param_1'].fillna('-1').astype('str')),
param_2=lambda x:lb.fit_transform(x['param_2'].fillna('-1').astype('str')),
param_3=lambda x:lb.fit_transform(x['param_3'].fillna('-1').astype('str')),
price=lambda x: np.log1p(x['price'].fillna(0)),
mday=lambda x: pd.to_datetime(x['activation_date']).dt.day,
wday=lambda x:pd.to_datetime(x['activation_date']).dt.dayofweek)

Es posible que confunda el código en la primera lectura, pero si observas con cuidado las operaciones realizadas sobre el DataFrame ocurren con diferentes  “pipe” y cuando hace algo sobre alguna variable todo ocurre dentro “assign”. A diferencia de usar “()” para meter una cadena de transformaciones hago uso de “\” en cada salto de línea que necesito y eso me permite conectar la siguiente operación que sigue después de un “.”, así que es otra alternativa para escribir código en Pandas como una cadena de métodos.

¿Cómo sería el código de manera imperativa?..no lo se, pero si con muchas más líneas de código.

Lo que uno debe observar es que resulta fácil de seguir los pasos de lo que se hace, con el código imperativo debe de tratar de entenderse línea por línea.

Algunos detalles a comentar son respecto al uso de “pipe”, funcionalidad nos permite mandar el DataFrame hacer o ejecutar un proceso que regresa un DataFrame como salida. De cierto modo es como si definiéramos nuestros propios métodos sobre los DataFrame.

Si se tuviera que modificar alguna de las funciones dentro de los pipe nos olvidamos del DataFrame y uno se concentra solo en la definición del método, por eso comento que es pensar en abstracto o abstraer las transformaciones que deseamos hacer.

Comparación con R.

Si vienes de R, es posible que extrañes dpyr o la versión extendida de tidyverse, pero observando el código de ejemplo queda en Pandas. Se aprecian similitudes y no son mera casualidad, las funciones “assign” busca hacer algo similar a “mutate” de dplyr y “pipe” busca ser más general y dejar que uno defina nuevas funcionalidades o métodos sobre los DataFrame.  El uso de “%>%” en R es similar a “.” en Pandas, la idea es la misma, hacer más ordenado y legible el código.

Ejemplos comparativos entre R y Python.

En las siguientes ligas agrego un código de Konrad Banachewicz para la competencia Avito en Kaggle. Por mi cuenta hice la versión de ese código en Python, ambos modelos y procesamientos eran iguales y arrojaban el mismo performance al medir sobre los datos test de la competencia.

Mi intención fue efectivamente mostrar a la comunidad que las virtudes que tiene R para organizar el código también se pueden recuperar en Python haciendo uso de Pandas.

El código Konrad Banachewicz era un buen ejemplo, era un buen código, sencillo y elegante. Fácil de leer y estudiar, en mi versión respeté todo, nombres y etapas.

Código Konrad: https://www.kaggle.com/konradb/xgb-text2vec-tfidf-clone-lb-0-226/code

Mi Versión: https://www.kaggle.com/legorreta/python-version-xgb-text2vec-tfidf-lb-0-226

Espero los disfrutes!

Conclusiones.

Hacer de uso común en nuestro trabajo o experimentos este tipo de técnicas en lo personal pienso que te reducen tiempo de mantenimiento y modificación.  Pienso que te ayudan a tener más claridad sobre los datos o por lo menos mentalmente es menos frustrante a la hora de agregar nuevas variables o exploraciones, eso en mi opinión te deja pensar más en como mejorar tu proyecto o modelo y no tanto en todo lo que tienes que cambiar de tu código.

Referencias:

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.

Sobre sistemas de recomendación, ejemplo sencillo.

Sobre los sistemas de recomendación

Los sistemas de recomendación son quizás una de esas cosas con las cuales todos tenemos contacto actualmente, van desde sistemas sofisticados y predictivos, hasta otros menos rebuscados que solo nos hacen una lista fija de sugerencia. Los ejemplo conocidos son youtube, spotify, los sistemas de facebook, amazon, Netflix,etc.

Pese a que no es un tema fijo o algo que aparezca como tema dentro de las referencias importantes de Machine Learning, existen algunas referencias que hablan sobre el tema [2] y en particular un laboratorio reconocido por su investigación en sistemas de recomendación es GroupLens de la Universidad de Minnesota.

Este tipo de sistemas se hicieron famosos gracias a la competencia de Netflix,  en la cual un equipo logró cumplir con el reto de minimizar un indicador estadístico para asegurar que se obtenían mejores sugerencias [3]. Existen mucho artículos respecto a este concurso y sobre el sistema que se desarrollo, pero lo que se debe de resaltar es que el equipo ganador no hizo solo de un algoritmo de Machine Learning, uso varios y la parte más importante fue el pre-procesamiento de los datos [4].

Sobre el ejemplo

El siguiente ejemplo de sistema de recomendación lo presento como se discute el texto Machine Learning for Hackers [1]. En el ejemplo se hace uso de la técnica k-Nearest Neighbor(Knn) para definir las recomendaciones, es un método de clasificación no paramétrico. Antes de explicar un poco sobre Knn hago un ejemplo sencillo sobre series de tiempo y principalmente sobre la estimación de la “regresión de k-vecinos cercanos“.

#Regresión Vecinos Cercanos
Mortalidad<-scan("data/cmort.dat")
t=1:lenght(Mortalidad)
#Usamos la función supsmu
plot(t,Mortalidad,col="4",main="Mortalidad por semanas",xlab="Semanas",ylab="Número de muertes")
lines(supsmu(t,Mortalidad,span=0.5),col="2")
lines(supsmu(t,Mortalidad,span=0.01),col="4")

Ejemplo_Regresión_vecinos_cercanos

En la gráfica se observan dos curvas cercanas a los datos que están en color verde.Observando con detenimiento la curva en rojo se parece mucho a la línea de tendencia de los datos, pero no es tal cual una recta. Por otro lado la curva en azul, parece más cercana a una descripción del comportamiento “real” de los datos. En ambos casos lo que se hace es ir calculando una regresión para cierta cantidad de vecinos cercanos a cada punto, la línea roja se calcula para una cantidad mayor de vecinos en cada punto y la azul para una cantidad menor, por ello la línea roja no es una recta del todo y la azul tiene pequeños picos y a la vista nos parece mejor para describir los datos.

La gráfica anterior dice que cuando se calcula la regresión o el mejor ajuste lineal para una cantidad mayor de vecinos se tiende a seguir la tendencia de los datos. Cuando se calcula para una cantidad menor de vecinos es probable que la curva siga de cerca el comportamiento de los datos. Con este ejemplo solo deseo mostrar que Knn es hasta cierto punto útil para hacer exploración de la información y que si bien este caso es una regresión, la idea en general es la misma de considerar los vecinos cercanos. Esto de que “parezca” muy parecido a los datos tienen un costo  en el sentido estadístico en caso de considerar dichos algoritmos como candidatos para implementación.

Una imagen burda del algoritmo Knn es pensar en que si se está localizado en uno de los datos en color verde  se traza un circulo centrado en ese punto y se cuenta la cantidad k vecinos para luego hacer la regresión. Lo que se debe de resaltar es que trazar un circulo implica hablar de una distancia para comparar qué puntos están dentro o no del circulo, igual que en el análisis de Cluster se requiere la noción de distancia en el algoritmo de Knn.

Hago un ejemplo solo para mostrar como funciona la librería class que contiene la función knn que permite hacer el cálculo de k-vecinos cercanos. Tomo un subconjunto de entrenamiento y uno de prueba, estimo el modelo para el conjunto de datos de entrenamiento y comparo el resultado con los datos de prueba. Hago una comparación de un método lineal paramétrico de clasificación ( regresión logística) con respecto al método Knn.

Los datos empleados se pueden descargar desde github.

#Comparación de métodos
data.path<-file.path("data","example_data.csv")
datos<-read.csv(data.path)
n<-1:nrow(datos)
set.seed(1)

#Tomamos el 50% de los datos
library(class)
indices<-sort(sample(n,50))
training.x<-datos[indices,1:2]
training.y<-datos[indices,3]
test.x<-datos[-indices,1:2]
test.y<-datos[-indices,3]
#Aplicamos el método Knn
prediccion<-knn(training.x,test.x,training.y,k=5)
sum(prediccion!=test.y)
#Aplicamos un método lineal logit
modelo.logit<-glm(Label~ X+Y,data=datos[indices,])
prediccion2<-as.matrix(predict(modelo.logit,newdata=df[-indices,])>0)
sum(prediccion2!=test.y)

Se observa que en el ejemplo el método Knn da como predicción un 88% de coincidencias y el método lineal solo el 60%. Así que para situaciones no lineales se puede usar con mejor eficiencia el método Knn. Cuando digo predicción, me refiero a cuantos de los puntos de prueba el método clasificó correctamente y cuantos no.

Ejemplo de sistema de recomendación

Para el ejemplo se toma un conjunto de datos respecto a los paquetes instalados en R project. La intención de detectar de una lista de paquetes instalados cuando un usuario va  instalar otro paquete sabiendo cuales tiene instalados. Fue parte de una competencia en Kaggle. Lo interesante es ver que con una muestra pequeña se pudo aprender del comportamiento de los usuarios caracterizados por los paquetes instalados.

#Revisión de paquetes instalados
data.path<-file.path("data","isntallations.csv")
instalaciones<-read.csv(data.path)
#Revisamos las cabeceras
head(instalaciones)
#Construimos una matriz de datos
library(reshape)
matriz.de.paquetes<-cast(instalaciones,User~Package,value='Installed')
#Modificamos las filas 
row.names(matriz.de.paquetes) <- matriz.de.paquetes[, 1]
matriz.de.paquetes <- matriz.de.paquetes[, -1]
#Matriz de similitud
matriz.similitud <- cor(matriz.de.paquetes)
#Definimos una distancia
distancia <- -log((matriz.similitud / 2) + 0.5)

#Calculamos los vecinos cercanos
k.nearest.neighbors <- function(i, distancia, k = 25)
{
 return(order(distancia[i, ])[2:(k + 1)])
}

#Función para estimar la probabilidad
prob.instalacion <- function(user, package, matriz.de.paquetes, distancia, k = 25)
{
 neighbors <- k.nearest.neighbors(package, distancia, k = k)
 
 return(mean(sapply(neighbors, function (neighbor) {matriz.de.paquetes[user, neighbor]})))
}

#Función para elegir los paquetes más probables de isntalar
paquetes.mas.prob <- function(user,matriz.de.paquetes, distancia, k = 25)
{
 return(order(sapply(1:ncol(matriz.de.paquetes),
 function (package)
 {
 prob.instalacion(user,package,matriz.de.paquetes,distancia,k = k)
 }),
 decreasing = TRUE))
}

user <- 1

listing <- paquetes.mas.prob(user, matriz.de.paquetes, distancia)

colnames(matriz.de.paquetes)[listing[1:15]]
 [1] "adegenet" "AIGIS" "ConvergenceConcepts" "corcounts" "DBI" "DSpat" "ecodist" 
 [8] "eiPack" "envelope" "fBasics" "FGN" "FinTS" "fma" "fMultivar" 
[15] "FNN" 

Explico en lo general los pasos del código anterior. Primero  se cargan los datos que se encuentran en formato “csv”, luego se transforman a una matriz lo cual se hace con la función cast de la librería reshape. Una vez transformados, se calcula su matriz de correlaciones, pero la observación es que el método Knn no funciona con la matriz de correlaciones, sino con la matriz de distancias o similitudes.

El paso clave es usar el logaritmo y definir una distancia mediante la “normalización” de los valores obtenidos en la matriz de correlaciones, este paso es crucial para poder aplicar el algoritmo. Espero se pueda entender este paso, lo que se hace es considerar que las correlaciones tienen valores entre -1 y 1, suponiendo que no se alcanza el valor -1, entonce el intervalo de posibles valores tienen longitud 2. Si dividimos cada valor de la correlación sobre 2 y después se le sumamos 0.5, obligamos a que los valores estén entre 0 y 1. El logaritmo , de hecho el negativo del logaritmo con valores entre 0 y 1 indicarán que cuando los valores se aproximen a cero indica que son más distantes los paquetes de los usuarios  y que cuando se aproximen a 1 son muy cercanos.

Por último define una funciones que permite estimar la probabilidad mediante el cálculo de Knn. Entonces se obtiene una matriz la cual permite conocer cuales son los paquetes con mayor probabilidad para poder recomendarlos al usuario. La última línea muestra los 15 paquetes con mayor probabilidad de recomendar al usuario 1.

Entonces construir un sistema de recomendaciones es prácticamente tener una versión de este tipo de algoritmos o de este tipo de técnicas en el sistema, pero como lo mencioné al inicio de la entrada no existe un solo algoritmo útil para estos sistemas ya que se puede recurrir a varios para determinar cual funciona de manera adecuada. Ejemplo, si los datos muestran un comportamiento lineal es preferible usar la regresión logística que knn ya que clasificará mejor los datos.

Puede parecer confuso lo que hago en el ejemplo, pero en resumen es lo siguiente:

  1. Se cargaron los datos.
  2. Se construye una matriz de correlaciones con la información de los paquetes instalados en cada usuario.
  3. Se define una matriz de distancia por medio de la matriz de correlaciones.
  4. Se aplica el método Knn, el cual estimará los paquetes más cercanos a cada usuario.

Faltaría diseñar una interfaz agradable para mostrar los resultados.

Referencias:

1.-http://shop.oreilly.com/product/0636920018483.do

2.-http://www.mmds.org/

3.-http://www.wired.com/2009/09/bellkors-pragmatic-chaos-wins-1-million-netflix-prize/

4.-http://www.netflixprize.com/assets/ProgressPrize2008_BigChaos.pdf