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

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.

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