datacubeR

Las visualizaciones no me gustan, así que a reciclar.

Crear Funciones reutilizables para plotear

Hay veces que para presentar modelos te piden siempre los mismos gráficos

picture of me picture of me

En mi nuevo trabajo me dí cuenta que utilizan un cierto tipo de gráfico para mostrar el impacto de una variable en particular sobre el target. Me carga hacer visualizaciones, así que decidí crear automatizar la creación de estos gráficos.

Normalmente tenemos una tasa natural de ocurencia de un evento, que se muestra como la línea segmentada TN. Y normalmente se tiene una variable dividida en categorías/segmentos/tramos que es la tasa de que ocurra ese evento por el bin utilizado.

Dado que este gráfico gusta bastante y se utiliza mucho es que decidí crear una función simple que permita evitar el ajetreo de graficar esto cada vez. Para esto voy a utilzar nuevamente el dataset del Titanic el cual también pueden descargar por Terminal si tienen la API de Kaggle.

$ kaggle competitions download -c titanic
import pandas as pd
df = pd.read_csv('train.csv')
df.head()
PassengerIdSurvivedPclassNameSexAgeSibSpParchTicketFareCabinEmbarkedSigning_date
0103Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNS1911-05-17
1211Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85C1911-07-23
2313Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNS1911-09-08
3411Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123S1911-06-26
4503Allen, Mr. William Henrymale35.0003734508.0500NaNS1911-10-25

Este data set se utiliza normalmente para predecir la tasa de supervivencia de los pasajeros del Titanic. En este caso la variable target es Survived.

df.Survived.value_counts(normalize = True)
0    0.616162
1    0.383838
Name: Survived, dtype: float64

Se puede notar que el 61% de los pasajeros no sobrevivió el desastre del Titanic, pero ¿hay alguna diferencia si tomamos subconjuntos de los datos? Por ejemplo si analizamos la supervivencia por Sexo el resultdo que esperaría sería otro:

df.groupby(['Sex']).Survived.value_counts(normalize = True)
Sex     Survived
female  1           0.742038
        0           0.257962
male    0           0.811092
        1           0.188908
Name: Survived, dtype: float64

En realidad, al hacer el split por Sexo notamos que el 74% de las mujeres sobrevevivieron, mientras que sólo el 18% de los hombre sobrevivió. Entonces, ¿existe alguna buena manera de graficar esto? Nuestro objetivo es mostrar sólo la tasa de supervivencia (porque la tasa de muertes no es más que el complemento) y mostrar como se compara en contra de la tasa natural.

Un truco

Para datos que son binarios (como en el caso de nuestra variable Survived) es posible calcular la tasa de supervivencia de la siguiente forma:

df.groupby('Sex').Survived.mean()
Sex
female    0.742038
male      0.188908
Name: Survived, dtype: float64
import matplotlib.pyplot as plt
df.groupby('Sex').Survived.mean().plot(kind = 'bar')
plt.show()

png

Entonces vamos a crear la función plot_rate_by(). Esto dará flexibilidad a nuestra implementación permitiendo agregar funcionalidades como agregar título, o dejar que funcione para cualquier dataframe.

def plot_rate_by(data,by,Target, TN, title, x, y, x_label = None, rot = 0):
    TN *=100 # converts to percentage
    # plots ading title, and optional label rotation
    ax = (data.groupby(by)[Target].mean()*100).plot(kind = 'bar', title = title, rot = rot) 
    plt.axhline(TN, color = 'r', linestyle = '--') # adds dashed line
    # adds the red text box, in coordinates x and y to avoid overlapping
    plt.text(x,y,f'TN = {TN}%',bbox=dict(facecolor='red', alpha=0.5)) 
    ax.set_xlabel(x_label) # optional Label for the x Axis
    return plt.show()
    
import numpy as np
# Natural Rate for survivors
tn = np.round(df.Survived.value_counts(normalize = True).loc[1],3) 
plot_rate_by(df, by = 'Sex', Target = 'Survived', TN = tn, 
  title = 'Survivors by Sex', x = 0.4, y = 40, x_label = None)

png

Esto se puede aplicar a cualquier variable categórica, por ejemplo, Pclass:

plot_rate_by(df, by = 'Pclass', Target = 'Survived', TN = tn, title = 'Survivors by Pclass', x = 1.6, y = 40, x_label = None)

png

Otra cosa que normalmente pasa en mi trabajo es que quien aplicar una lógica similar pero utilizando variables continuas. EL tema es que esto no lo hacen de manera directa sino que utilizando un binning previo. Esto puede ser realizado relativamente sencillo combinando numpy y pandas. Por lo tanto tratemos de gráficar la tasa de supervivencia por rango de tarifa Fare, para ello crearemos bins para tarifas <10, <100, <300 y >=300.

Para ello haré uso de una función llamada np.select. El nombre puedo ser poco informativo, pero básicamente devuelve un valor dependiendo de una condición. (sí, muy parecido a if/elif, pero bastante más eficiente).

# lista de condiciones
condlist = [df.Fare < 10, df.Fare < 100, df.Fare < 300, df.Fare >= 300]
# lista de elecciones.
choicelist = ['<10','<100','<300','>=300']

df['Fare_binning'] = np.select(condlist, choicelist)
df[['Fare','Fare_binning']]
FareFare_binning
07.2500<10
171.2833<100
27.9250<10
353.1000<100
48.0500<10
.........
88613.0000<100
88730.0000<100
88823.4500<100
88930.0000<100
8907.7500<10

891 rows × 2 columns

Normalmente uno no sabe qué binnings mostrarán de mejor manera los datos previamente, por lo que es necesario que este tipo de operaciones sea sumamente flexible.¿Cómo transformar esto en una función?

vals_l = [10,100,300]

LUego podemos usar un for loop para crear las condiciones.

En pandas es posible intercambiar los operadores $<$ y $\geqslant$ por los métodos .lt() y .ge() lo cual entrega un poco más de flexibilidad al código.

condlist = []
choicelist = []

for v in vals_l:
    condlist.append(df['Fare'].lt(v)) 
    choicelist.append('<'+str(v)) 

condlist.append(df['Fare'].ge(vals_l[-1]))
choicelist.append('>='+str(vals_l[-1]))

choicelist
['<10', '<100', '<300', '>=300']

Una vez que hemos creado todas las categorías de manera correcta, se puede combinar todo dentro de una función:

def convert_to_range(data, field, vals_l):
    vals_l = vals_l
    condlist = []
    choicelist = []
    for v in vals_l:
        condlist.append(data[field].lt(v))
        choicelist.append('<'+str(v))
    
    condlist.append(data[field].ge(vals_l[-1]))
    choicelist.append('>='+str(vals_l[-1]))
    return pd.Categorical(np.select(condlist, choicelist), 
                                    categories=choicelist, ordered = True)

Para asegurar que los tramos salgan en el orden correcto dentro del gráfico, es super importante que los bins creados sean de tipo Categórico y que estén ordenados. (Ver return)

df['binning_function'] = convert_to_range(df,'Fare',vals_l)
df[['Fare_binning','binning_function']]
Fare_binningbinning_function
0<10<10
1<100<100
2<10<10
3<100<100
4<10<10
.........
886<100<100
887<100<100
888<100<100
889<100<100
890<10<10

891 rows × 2 columns

Podemos usar nuestra función anterior plot_rate_by() para graficar:

plot_rate_by(df, by = 'binning_function', Target = 'Survived', TN = tn, title = 'Survivors by Fare Categories', x = -0.3, y = 42, x_label = None)

png

Podemos notar que se obtienen resultados equivalente que al usar variables categóricas y con la misma flexibilidad.

Espero que esto demuestre que el no gustarte algo puede transformarse en algo entretenido, que es automatizar, y no tener que prestarles tanta atención en el futuro.

Go to top