Una de las primeras cosas que hay que hacer cuando empezamos un proyecto de datos es asignar el tipo de datos correcto para cada variable. Aunque esto parece una tarea sencilla, algunos algoritmos funcionan con ciertos tipos de datos. Aquí trataremos de cubrir estas conversiones mientras explicamos con ejemplos las implicaciones en cada caso.
Espiral de Fibonacci
La sucesión de Fibonacci. Una secuencia de números presente en la naturaleza y en los cuerpos humanos.
¿Qué vamos a repasar en este capítulo?
Detección del tipo de datos correcto
Cómo convertir de categórico a numérico
Cómo convertir de numérico a categórico (métodos de discretización)
Aspectos teóricos y prácticos (ejemplos en Python)
Cómo observa las variables numéricas un modelo predictivo
2.1.2 El universo de los tipos de datos
Hay dos tipos principales de datos, numérico y categórico. Otros nombres para categóricos son string y nominal.
Un subconjunto de categórico es el ordinal o, como se lo llama en Python, un Categoricalordenado. Este tipo es relevante cuando se grafican categorías en un orden determinado. Un ejemplo en Python:
# Crear un factor ordinal u ordenadovar_factor = pd.Categorical(["3_high", "2_mid", "1_low"], ordered=False)var_ordered = pd.Categorical(var_factor, ordered=True)print(var_ordered)
No presten demasiada atención a este tipo de datos, ya que los numéricos y categóricos son los más necesarios.
2.1.2.1 Variable binaria: ¿numérica o categórica?
Este libro sugiere utilizar las variables binarias como numéricas cuando 0 es FALSE y 1 es TRUE. Simplifica el análisis matemático de los datos.
2.1.3 Tipos de datos por algoritmo
Algunos algoritmos funcionan de la siguiente manera:
Sólo con datos categóricos
Sólo con datos numéricos
Con ambos tipos
Además, no todos los modelos predictivos pueden manejar valores faltantes.
El libro vivo de ciencia de datos busca cubrir todas estas situaciones.
2.1.4 Convirtiendo variables categóricas en numéricas
Usar la función pd.get_dummies de pandas es una tarea sencilla que convierte cada variable categórica en una variable flag, también conocida como variable dummy.
Si la variable categórica original tiene treinta valores posibles, entonces resultará en 30 nuevas columnas que contengan el valor 0 o 1, donde 1 representa la presencia de esa categoría en la fila.
Con pandas, para esta conversión sólo se necesitan unas pocas líneas de código:
# Detectar variables categóricascat_vars = heart_disease.select_dtypes( include=['object', 'category']).columns.tolist()import textwrap as _tw_cats_str ="Variables categóricas: "+str(cat_vars)print(_tw.fill(_cats_str, width=60))# Convertir categóricas a dummies (0/1 enteros)heart_disease_2 = pd.get_dummies( heart_disease, columns=cat_vars, drop_first=False, dtype=int)# Renombrar: separador _ por . (consistencia con R)for c in cat_vars: heart_disease_2.columns = [ col.replace(f'{c}_', f'{c}.')for col in heart_disease_2.columns]import textwrapprint("\nColumnas del nuevo dataset:")cols_str =str(list(heart_disease_2.columns))print(textwrap.fill(cols_str, width=60))
Los datos originales heart_disease han sido convertidos a heart_disease_2 que no tiene variables categóricas, sólo numéricas y dummy. Observe que cada nueva variable tiene un punto seguido por el valor.
Si comprobamos el antes y el después para el séptimo paciente (fila) en la variable chest_pain que puede tomar los valores 1, 2, 3 o 4, entonces
# Antesprint("Antes:")print(heart_disease.iloc[6]['chest_pain'])# Después (una fila = un paciente)print("\nDespués:")print(heart_disease_2.iloc[[6]][['chest_pain.1', 'chest_pain.2','chest_pain.3', 'chest_pain.4']].to_string())
Habiendo conservado y transformado sólo variables numéricas excluyendo las nominales, los datos heart_disease_2 están listos para ser utilizados.
2.1.5 ¿Es categórica o numérica? Piénsenlo.
Consideren la variable chest_pain, que puede tomar los valores 1, 2, 3, o 4. ¿Es esta variable categórica o numérica?
Si los valores están ordenados, entonces se la puede considerar tan numérica como si exhibiera un orden, es decir, 1 es menos de 2, 2 es menos de 3, y 3 es menos de 4.
Si creamos un modelo de árbol de decisión, entonces podemos encontrar reglas como: “Si el dolor de pecho es > 2.5, entonces…”. ¿Tiene sentido? El algoritmo divide la variable por un valor que no está presente (2.5); sin embargo, la interpretación que hacemos es “si dolor de pecho es igual o superior a 3, entonces…”.
2.1.6 Pensar como un algoritmo
Considere dos variables numéricas de entrada y una variable binaria de destino. El algoritmo verá ambas variables de entrada como puntos en un rectángulo, considerando que hay valores infinitos entre cada número.
Por ejemplo, una Máquina de Soporte Vectorial (SVM) creará varios vectores para separar la clase de la variable de destino. Encontrará regiones basadas en estos vectores. ¿Cómo sería posible encontrar estas regiones basándose en variables categóricas? No es posible y es por eso que el SVM sólo funciona con variables numéricas como en las redes neuronales artificiales.
Máquina de vectores soporte
Image credit: ZackWeinberg
La última imagen muestra tres líneas, que representan tres límites de decisión o regiones diferentes.
Para una rápida introducción a este concepto de SVM, por favor vean este corto video: Demo SVM.
Sin embargo, si el modelo está basado en árboles, como decision trees, random forest o gradient boosting machine, entonces manejan ambos tipos porque su espacio de búsqueda puede ser regiones (igual que SVM) y categorías. Como la regla “si postal_code es AX441AG y tiene más de 55 años, entonces...”.
Volviendo al ejemplo de la enfermedad cardíaca, la variable chest_pain exhibe orden. Debemos aprovechar esto porque si lo convertimos en una variable categórica, entonces estamos perdiendo información y este es un punto importante a la hora de manejar los tipos de datos.
2.1.6.1 ¿Es la solución tratar a todas las variables como categóricas?
No…. Una variable numérica contiene más información que una nominal debido a su orden. En las variables categóricas, los valores no se pueden comparar. Digamos que no es posible hacer una regla como Si el código postal es superior a "AX2004-P".
Los valores de una variable nominal pueden ser comparados si tenemos otra variable para usar como referencia (normalmente un resultado a predecir).
Por ejemplo, el código postal “AX2004-P” es más alto que “MA3942-H” porque hay más personas interesadas en asistir a clases de fotografía.
Además, la alta cardinalidad es un problema en las variables categóricas, por ejemplo, una variable postal code que contiene cientos de valores diferentes. Este libro ha tratado este tema en ambos capítulos: el manejo de variables de alta categorización para estadísticas descriptivas y cuando hacemos modelado predictivo.
De todos modos, pueden hacer la prueba gratis de convertir todas las variables en categóricas y ver qué pasa. Comparen los resultados con las variables numéricas. Recuerden usar alguna buena medida de error para la prueba, como el estadístico Kappa o ROC, y validar los resultados.
2.1.6.2 Tengan cuidado al convertir variables categóricas en numéricas
Imaginemos que tenemos una variable categórica que necesitamos convertir a numérica. Como en el caso anterior, pero intentando una diferente transformación, asignen un número diferente a cada categoría.
Tenemos que tener cuidado al hacer tales transformaciones porque estamos introduciendo orden a la variable.
Considere el siguiente ejemplo de datos con cuatro filas. Las dos primeras variables son visitas y codigo_postal (esto funciona como dos variables de entrada o visitas como entrada y codigo_postal como salida).
El siguiente código mostrará las visitas dependiendo de codigo_postal transformadas según dos criterios:
transformacion_1: Asigna un número de secuencia basado en el orden dado.
transformacion_2: Asigna un número basado en la cantidad de visitas.
Para estar seguros, nadie construye un modelo predictivo usando sólo cuatro filas; sin embargo, la intención de este ejemplo es mostrar cómo la relación cambia de no lineal (transformacion_1) a lineal (transformacion_2). Esto hace las cosas más fáciles para el modelo predictivo y explica la relación.
El efecto es el mismo cuando manejamos millones de filas de datos y el número de variables escala a cientos. Aprender de datos pequeños es un enfoque adecuado en estos casos.
2.1.7 Discretizando variables numéricas
Este proceso convierte los datos en una categoría dividiéndolos en segmentos. Para una definición más sofisticada, podemos citar a Wikipedia: La discretización refiere al proceso de transferir funciones, modelos y ecuaciones continuas a contrapartes discretas.
Los segmentos también se conocen como bins o buckets. Continuemos con los ejemplos.
2.1.7.1 Sobre los datos
Los datos contienen información sobre el porcentaje de niños con retraso en el crecimiento. El valor ideal es cero.
El indicador refleja la proporción de niños menores de 5 años que presentan retraso en el crecimiento. Los niños con retraso en el crecimiento tienen mayor riesgo de enfermedad y muerte.
En primer lugar, tenemos que hacer una rápida preparación de datos. Cada fila representa un par país-año, por lo que tenemos que obtener el indicador más reciente por país.
data_stunting = pd.read_csv("images/share-of-children-""younger-than-5-who-suffer-from-stunting.csv")# Renombrar la métricadata_stunting = data_stunting.rename(columns={'Share of stunted children under 5':'share_stunted_child'})# Realizar la agrupación previamente mencionadad_stunt_grp = (data_stunting .sort_values('Year') .groupby('Entity') .last() .reset_index()[['Entity', 'share_stunted_child']])
Los criterios de segmentación más comunes son:
Igual rango
Igual frecuencia
Segmentos personalizados
Todos están explicados a continuación.
2.1.7.2 Igual rango
El rango se suele encuentrar en los histogramas que estudian la distribución, pero es altamente susceptible a los valores atípicos. Para crear, por ejemplo, cuatro segmentos, dividimos por 4 los valores mínimos y máximos.
# pd.cut con igual rango (equivalente a cut_interval)d_stunt_grp['share_stunted_child_eq_range'] = pd.cut( d_stunt_grp['share_stunted_child'], bins=4)# sort_index() ordena las barras por intervalovc = d_stunt_grp['share_stunted_child_eq_range'].value_counts( ).sort_index()print(vc)vc.plot(kind='bar', color='#009E73', figsize=(5, 3.5))plt.ylabel('Frecuencia')plt.xticks(rotation=45, ha='right')plt.tight_layout()plt.show()
El resultado nos dice que hay cuatro categorías en la variable y, entre paréntesis y corchetes, el número total de casos por categoría. Por ejemplo, la categoría (15.8,30.3] contiene todos los casos que tienen share_stunted_child desde 15.8 (no inclusive) hasta 30.3 (inclusive).
2.1.7.3 Igual frecuencia
Esta técnica agrupa la misma cantidad de observaciones utilizando criterios basados en percentiles. Pueden encontrar más información sobre percentiles en el capítulo: Anexo 1: La magia de los percentiles.
La función equal_freq del paquete funpymodeling (equivalente a funModeling::equal_freq) crea segmentos basándonos en este criterio:
Noten que solo es necesario definir el valor máximo de cada segmento.
Por lo general, no sabemos cuál es el valor mínimo o máximo. En esos casos, podemos utilizar los valores -np.inf e np.inf. De lo contrario, si definimos un valor que está fuera del rango, pd.cut le asignará el valor NaN.
Es una buena práctica asignar el valor mínimo y máximo usando una función. En este caso, la variable es un porcentaje, por lo que sabemos de antemano que su escala es de 0 a 100; sin embargo, Atención:¿qué pasaría si no conociéramos el rango?
La función devolverá NaN para aquellos valores por debajo o por encima de los puntos de corte. Una solución es obtener valores mínimos y máximos variables:
# Obtener el valor mínimo y máximomin_value = d_stunt_grp['share_stunted_child'].min()max_value = d_stunt_grp['share_stunted_child'].max()# Configuren include_lowest=True para incluir el valor# mínimo, de lo contrario será asignado como NaN.# precision=2 redondea los límites de los intervalos.d_stunt_grp['share_stunted_child_custom_2'] = pd.cut( d_stunt_grp['share_stunted_child'], bins=[min_value, 2, 9.4, 29, max_value], include_lowest=True, precision=2)print(d_stunt_grp['share_stunted_child_custom_2'].value_counts( ).sort_index())
Todas estas transformaciones se realizan con un conjunto de datos de práctica basado en las distribuciones de las variables. Tal es el caso de la discretización de igual frecuencia y de igual rango. Pero, ¿qué pasaría si llegaran nuevos datos?
Si aparece un nuevo valor mínimo o máximo, afectará el rango de ubicaciones en el método igual rango. Si llega algún nuevo valor, entonces moverá los puntos basados en percentiles como vimos en el método igual frecuencia.
Para ver qué pasa, imaginemos que añadimos cuatro casos más en el ejemplo propuesto, con los valores 88, 2, 7 y 3:
# Simular que se agregan cuatro valores nuevosupdated_data = pd.concat([ d_stunt_grp['share_stunted_child'], pd.Series([88, 2, 7, 3])], ignore_index=True)# Discretizar por igual frecuenciaupdated_data_eq_freq = equal_freq(updated_data, 4)# Resultados en...print(updated_data_eq_freq.value_counts().sort_index())
¡Todos los segmentos cambiaron! Dado que estas son nuevas categorías, el modelo predictivo fallará a la hora de procesarlas porque son todos valores nuevos.
La solución es conservar los puntos de corte cuando preparamos los datos. Luego, ejecutamos el modelo en producción, utilizamos el segmento de discretización manual y, así, forzamos que cada caso quede en la categoría correspondiente. De esta manera, el modelo predictivo siempre ve lo mismo.
La solución será detallada en la siguiente sección.
2.1.9 Discretización automática de data frames
Las funciones discretize_get_bins y discretize_df del paquete funpymodeling (equivalentes a las de funModeling >= 1.6.6) operan juntas para ayudarnos en la tarea de discretización.
Veamos un ejemplo. Primero, corroboramos los tipos de datos actuales:
st = status(heart_disease)print(st[['variable', 'type', 'q_nan','p_nan']].sort_values('type').to_string())
Tenemos variables object, enteras, y numéricas: ¡una buena mezcla! La transformación tiene dos pasos. Primero, obtiene los valores de corte o de umbral donde comienza cada segmento. El segundo paso es utilizar el umbral para obtener las variables como categóricas.
Discretizaremos dos variables en el siguiente ejemplo: max_heart_rate y oldpeak. Además, agregaremos algunos valores NA a oldpeak para evaluar cómo opera la función con datos faltantes.
# Crear una copia para conservar los datos originalesheart_disease_2b = heart_disease.copy()# Introducir algunos valores faltantes en las primeras# 30 filas de la variable oldpeakheart_disease_2b.loc[:29, 'oldpeak'] = np.nan
Paso 1) Obtener los umbrales de segmento para cada variable de entrada:
discretize_get_bins devuelve un diccionario que necesitaremos para la función discretize_df, que genera el data frame final procesado.
d_bins = discretize_get_bins( data=heart_disease_2b,input=['max_heart_rate', 'oldpeak'], n_bins=5)# Verificar el objeto `d_bins`:for var, bins in d_bins.items():print(f"{var}: {[round(b, 2) for b in bins]}")
data: el data frame que contiene las variables a procesar.
input: lista de strings que contienen los nombres de las variables.
n_bins: la cantidad de segmentos que tendremos en los datos discretizados.
Podemos ver el punto del umbral (o límite superior) para cada variable.
Nota: En R, discretize_get_bins con n_bins=5 para oldpeak produce los umbrales 0.1|0.3|1.1|2|Inf. En Python, pd.qcut con duplicates='drop' puede generar menos segmentos cuando la variable tiene muchos valores repetidos (como oldpeak, que tiene muchos ceros). Esto es esperable y no un error: el algoritmo fusiona segmentos que serían idénticos.
¡Es hora de continuar con el siguiente paso!
Paso 2) Aplicar los umbrales para cada variable:
# Ahora se puede aplicar en el mismo data frame o# en uno nuevo (por ejemplo, en un modelo predictivo# en el que los datos cambian con el tiempo)heart_disease_discretized = discretize_df( data=heart_disease_2b, data_bins=d_bins)
Parámetros:
data: data frame que contiene las variables a procesar.
data_bins: diccionario resultado de discretize_get_bins.
Las variables finales serán categóricas y útiles para graficar.
A veces, no es posible obtener la misma cantidad de casos por segmento al computar por igual frecuencia, como en el caso de la variable oldpeak.
2.1.9.2 Manejo de valores NA
Con respecto a los valores NA, la nueva variable oldpeak tiene cinco categorías: cuatro categorías generadas por la discretización (menos que las cinco solicitadas en n_bins=5 debido a valores repetidos) más el valor NA. Noten el punto al final que indica la presencia de valores faltantes.
2.1.9.3 Más información
discretize_df nunca devolverá un valor NaN sin transformarlo en el string NA..
n_bins configura la cantidad de segmentos para todas las variables.
Solo las variables definidas en input serán procesadas, mientras que las restantes no serán modificadas en absoluto.
discretize_get_bins devuelve un diccionario que puede ser modificado a mano como sea necesario.
2.1.9.4 Discretización con nuevos datos
En nuestros datos, el valor mínimo para max_heart_rate es 71. La preparación de datos debe ser robusta cuando incorporamos nuevos casos; por ejemplo, si llega un nuevo paciente cuya max_heart_rate es 68, entonces el proceso actual lo asignará a la categoría más baja.
En otras funciones de otros paquetes, esta preparación puede devolver un NaN porque está fuera del segmento.
Como señalamos anteriormente, si los nuevos datos llegan a lo largo del tiempo, es probable que obtengan nuevos valores mínimos/máximos. Esto puede romper nuestro proceso. Para resolver esto, discretize_df siempre tendrá como mínimo/máximo los valores -Inf/Inf; por lo tanto, cualquier nuevo valor que caiga por debajo o por encima del mínimo/máximo se añadirá al segmento más bajo o más alto según corresponda.
El diccionario devuelto por discretize_get_bins debe ser guardado para poder aplicarlo a nuevos datos. Si la discretización no está pensada para funcionar con nuevos datos, entonces no tiene sentido tener dos funciones: puede ser solo una. Además, no habría necesidad de guardar los resultados de discretize_get_bins.
Con este enfoque de dos pasos, podemos manejar ambos casos.
2.1.9.5 Conclusiones sobre la discretización de dos pasos
El uso de discretize_get_bins + discretize_df permite una rápida preparación de datos, con un data frame limpio y listo para usar. Dado que muestra claramente dónde empieza y termina cada segmento, resulta indispensable a la hora de realizar informes estadísticos.
La decisión de no fallar a la hora de manejar un nuevo valor mínimo o máximo cuando incorporamos nuevos datos es solo una decisión. En algunos contextos, fracasar puede ser el comportamiento deseado.
La intervención humana: La manera más fácil de discretizar un data frame es seleccionar la misma cantidad de segmentos para aplicar a cada variable, igual que en el ejemplo que vimos. Sin embargo, si es necesario realizar ajustes, entonces algunas variables pueden necesitar un número diferente de segmentos. Por ejemplo, una variable con menos dispersión puede funcionar bien con pocos segmentos.
Los valores más comunes para el número de segmentos suelen ser 3, 5, 10 ó 20 (pero no más). Esta decisión corre por cuenta del científico de datos.
2.1.9.6 Bonus track: El arte del equilibrio
Alta cantidad de segmentos => Más ruido capturado
Baja cantidad de segmentos => Demasiada simplificación, menos varianza.
¿Estos términos les suenan parecidos a otros empleados en el ámbito de machine learning?
La respuesta: ¡Sí! Solo por mencionar un ejemplo: buscar el equilibrio a la hora de agregar o quitar variables en un modelo predictivo.
Más variables: Alerta de sobreajuste (el modelo predictivo es demasiado detallado).
Menos variables: Peligro de subajuste (no hay suficiente información para captar los patrones generales).
Como la filosofía oriental ha señalado durante miles de años, hay un arte en encontrar el equilibrio justo entre un valor y su opuesto.
2.1.10 Reflexiones finales
Como podemos ver, cada decisión tiene su costo en la discretización o preparación de datos. ¿Cómo creen que un sistema automático o inteligente resolverá todas estas situaciones sin la intervención o el análisis humano?
Para estar seguros, podemos delegar algunas tareas a procesos automáticos; sin embargo, los humanos son indispensables en la etapa de preparación de datos, brindando los datos de entrada correctos para procesar.
La asignación de variables como categóricas o numéricas, los dos tipos de datos más utilizados, varía según la naturaleza de los datos y los algoritmos seleccionados, ya que algunos sólo soportan un tipo de datos.
Cuando trabajamos con variables categóricas, podemos cambiar su distribución reorganizando las categorías según una variable objetivo para exponer mejor su relación. Convertir una relación variable no lineal en una lineal.
2.1.11 Bonus track
Volvamos a la sección sobre discretización de variables y grafiquemos todas las transformaciones que hemos visto hasta ahora:
Los datos ingresados son siempre los mismos. Sin embargo, todos estos métodos exhiben diferentes perspectivas de la misma cosa.
Algunas perspectivas son más adecuadas que otras para ciertas situaciones, como el uso de igual frecuencia para modelos predictivos.
Aunque este caso solo considera una variable, el razonamiento es el mismo si tenemos más variables a la vez, es decir, un espacio “N-dimensional”.
Cuando construimos modelos predictivos, describimos el mismo grupo de puntos de diferentes maneras, al igual que cuando distintas personas dan su opinión sobre un objeto.
2.2 Var. de alta cardinalidad en estadística descriptiva
2.2.1 ¿De qué se trata esto?
Una variable de alta cardinalidad es aquella que puede tomar muchos valores diferentes. Por ejemplo, la variable país.
Este capítulo cubrirá la reducción de la cardinalidad basada en la regla de Pareto, usando la función freq_tbl que da una visión rápida sobre dónde se concentran la mayoría de los valores y la distribución de la variable.
2.2.2 Alta cardinalidad en estadística descriptiva
El siguiente ejemplo contiene una encuesta de 910 casos, con 3 columnas: person, country y has_flu, que indica haber tenido gripe en el último mes.
# data_country: dataset original del paquete# funModeling de R, exportado a CSV.data_country = pd.read_csv("images/data_country.csv")
Los datos de data_country provienen del paquete funModeling de R.
Rápido análisis numérico de data_country (primeras 10 filas)
# Graficar las primeras 10 filasprint(data_country.head(10))
person country has_flu
0 478 France no
1 990 Brazil no
2 606 France no
3 575 Philippines no
4 806 France no
5 232 France no
6 422 Poland no
7 347 Romania no
8 858 Finland no
9 704 France no
has_flu frequency percentage cumulative_perc
0 no 827 0.9088 0.9088
1 yes 83 0.0912 1.0000
Análisis de frecuencia de casos con gripe
La última tabla muestra que hay sólo 83 filas en las que has_flu="yes", lo que representa cerca del 9% del total de personas (que tuvieron gripe).
Pero muchos de ellos casi no tienen participación en los datos. Esta es la cola larga, por lo que una técnica para reducir la cardinalidad es conservar aquellas categorías que están presentes en un alto porcentaje de los datos, por ejemplo 70, 80 o 90%, el principio de Pareto.
# freq_tbl ya devuelve frecuencia, porcentaje y# porcentaje acumulado, ordenado por frecuencia.country_freq = freq_tbl(data_country[['country']])# Las primeras 10 filas tienen la mayor participación.print(country_freq.head(10))
country frequency percentage cumulative_perc
0 France 288 0.3165 0.3165
1 Turkey 67 0.0736 0.3901
2 China 65 0.0714 0.4615
3 Uruguay 63 0.0692 0.5307
4 United Kingdom 45 0.0495 0.5802
5 Australia 41 0.0451 0.6253
6 Germany 30 0.0330 0.6583
7 Canada 19 0.0209 0.6792
8 Netherlands 19 0.0209 0.7001
9 Japan 18 0.0198 0.7199
Vemos que 10 representan más del 70% de los casos. Podemos asignar la categoría other a los casos restantes y graficar:
top_10 = country_freq.head(10)['country'].tolist()data_country['country_2'] = data_country['country'].apply(lambda x: x if x in top_10 else'other')c2_freq = freq_tbl(data_country[['country_2']])print(c2_freq)c2_freq.plot.bar(x='country_2', y='frequency', legend=False, figsize=(6, 4), color='steelblue')plt.ylabel('Frecuencia')plt.xticks(rotation=45, ha='right')plt.tight_layout()plt.show()
country_2 frequency percentage cumulative_perc
0 France 288 0.3165 0.3165
1 other 255 0.2802 0.5967
2 Turkey 67 0.0736 0.6703
3 China 65 0.0714 0.7417
4 Uruguay 63 0.0692 0.8109
5 United Kingdom 45 0.0495 0.8604
6 Australia 41 0.0451 0.9055
7 Germany 30 0.0330 0.9385
8 Canada 19 0.0209 0.9594
9 Netherlands 19 0.0209 0.9803
10 Japan 18 0.0198 1.0000
Variable país modificada - análisis de frecuencia
2.2.3 Comentarios finales
Las categorías poco representativas a veces son errores en los datos, como tener: “Egipto”, “Eggipto.”, y pueden dar alguna evidencia de malos hábitos de recolección de datos y/o posibles errores en la recolección de la fuente.
No existe una regla general para reducir los datos, depende de cada caso particular.
Próximo capítulo recomendado: Variables de alta cardinalidad en modelado predictivo
2.3 Variables de alta cardinalidad en modelado predictivo
2.3.1 ¿De qué se trata esto?
Como hemos visto en el capítulo anterior, Alta cardinalidad en estadística descriptiva, conservamos las categorías con la mayor representatividad, pero ¿qué tal si podemos tener otra variable para predecir con ella? Es decir, predecir has_flu basándonos en country.
Utilizar el último método puede destruir la información de la variable, por lo que pierde poder predictivo. En este capítulo iremos más allá en el método descrito anteriormente, utilizando una función de agrupación automática -auto_grouping- y navegando a través de la estructura de la variable para dar algunas ideas sobre cómo optimizar una variable categórica, pero lo más importante: animar al lector a realizar sus propias optimizaciones.
Otros autores han nombrado este reagrupamiento como reducción de la cardinalidad o encoding.
¿Qué vamos a repasar en este capítulo?
Concepto de representatividad de los datos (tamaño de muestra).
Tamaño de muestra con una variable objetivo o de resultado.
De Python: Presentar un método para ayudar a reducir la cardinalidad y analizar numéricamente variables categóricas.
Un ejemplo práctico de antes y después que reduce la cardinalidad y facilita la extracción de ideas.
Cómo diferentes modelos, como un random forest o gradient boosting machine (GBM, en inglés), manejan las variables categóricas.
2.3.2 Pero, ¿es necesario reagrupar la variable?
Depende del caso, pero la respuesta más rápida es sí. En este capítulo veremos un caso en el que esta preparación de datos aumenta la precisión general (medida por área debajo de la curva ROC).
Existe un equilibrio entre la representación de los datos (cuántas filas tiene cada categoría) y cómo se relaciona cada categoría con la variable de resultado. Por ejemplo: algunos países son más propensos a los casos de gripe que otros.
Analizamos numéricamente data_country.
Análisis rápido de data_country (primeras 10 filas)
# Graficar las primeras 10 filasprint(data_country.head(10))
person country has_flu country_2
0 478 France no France
1 990 Brazil no other
2 606 France no France
3 575 Philippines no other
4 806 France no France
5 232 France no France
6 422 Poland no other
7 347 Romania no other
8 858 Finland no other
9 704 France no France
has_flu frequency percentage cumulative_perc
0 no 827 0.9088 0.9088
1 yes 83 0.0912 1.0000
Distribución de la variable has_flu
2.3.3 El caso
El modelo predictivo intentará mapear ciertos valores con ciertos resultados, en nuestro caso la variable objetivo es binaria.
Calcularemos un análisis numérico completo de country con respecto a la variable objetivo has_flu basado en categ_analysis.
Cada fila representa una categoría única de variables input. Y en cada fila podemos encontrar atributos que definen cada categoría en términos de representatividad y probabilidad.
# `categ_analysis` del paquete funpymodeling,# equivalente a funModeling::categ_analysis en R.country_profiling = categ_analysis( data=data_country, input='country', target='has_flu')# Visualizar las primeras 15 filas (países) de 70._ren_cp = {'mean_target': 'mean_tgt','sum_target': 'sum_tgt','perc_target': 'pct_tgt','q_rows': 'rows','perc_rows': 'pct_rows'}print(country_profiling.head(15) .rename(columns=_ren_cp) .to_string(index=False))
country mean_tgt sum_tgt pct_tgt rows pct_rows
Malaysia 1.000 1 0.012 1 0.001
Mexico 0.667 2 0.024 3 0.003
Portugal 0.200 1 0.012 5 0.005
United Kingdom 0.178 8 0.096 45 0.049
Uruguay 0.175 11 0.133 63 0.069
Israel 0.167 1 0.012 6 0.007
Switzerland 0.167 1 0.012 6 0.007
Canada 0.158 3 0.036 19 0.021
France 0.142 41 0.494 288 0.316
Argentina 0.111 1 0.012 9 0.010
Germany 0.100 3 0.036 30 0.033
Australia 0.098 4 0.048 41 0.045
Romania 0.091 1 0.012 11 0.012
Spain 0.091 1 0.012 11 0.012
Sweden 0.083 1 0.012 12 0.013
Nota 1: La primera columna ajusta automáticamente su nombre en base a la variable input
Nota 2: La variable has_flu tiene valores yes y no, categ_analysis asigna internamente el número 1 a la clase menos representativa, yes en este caso, para calcular el promedio, suma y porcentaje.
Estas son las métricas que devuelve categ_analysis:
country: nombre de cada categoría en la variable input.
mean_target: sum_target/q_rows, número promedio de has_flu="yes" para una categoría. Esta es la probabilidad.
sum_target: cantidad de valores has_flu="yes" en cada categoría.
perc_target: lo mismo que sum_target pero expresado como porcentaje, sum_target of each category / total sum_target. Esta columna suma 1.00.
q_rows: cantidad de filas que, más allá de la variable has_flu, cayeron en una categoría. Es la distribución de input. Esta columna suma la cantidad total de filas analizadas.
perc_rows: relacionado con q_rows, representa la porción o porcentaje de cada categoría. Esta columna suma 1.00.
2.3.3.1 ¿Qué conclusiones podemos extraer de esto?
Leyendo como ejemplo la primera fila de France:
41 personas tienen gripe (sum_target=41). Estas 41 personas representan casi el 50% del total de personas con gripe (perc_target=0.494).
La probabilidad de tener gripe en Francia es 14.2% (mean_target=0.142)
Total de filas de Francia=288 -de 910-. Esta es la variable q_rows; perc_rows es el mismo número pero en porcentaje.
Sin considerar el filtro por país, tenemos:
La columna sum_target suma el total de personas con gripe en los datos actuales.
La columna perc_target suma 1.00 -o 100%
La columna q_rows suma el total de filas presentes en el data frame data_country.
La columna perc_rows suma 1.00 o 100%.
2.3.4 Análisis para el modelado predictivo
Cuando desarrollamos modelos predictivos, puede que nos interesen aquellos valores que aumentan la probabilidad de un determinado evento. En nuestro caso:
¿Cuáles son los países que maximizan la probabilidad de encontrar personas con gripe?
Fácil, tomemos country_profiling en orden descendiente según mean_target:
# Ordenar country_profiling por mean_target y luego# tomar los primeros 6 paísesprint(country_profiling.sort_values('mean_target', ascending=False).head(6) .rename(columns=_ren_cp) .to_string(index=False))
country mean_tgt sum_tgt pct_tgt rows pct_rows
Malaysia 1.000 1 0.012 1 0.001
Mexico 0.667 2 0.024 3 0.003
Portugal 0.200 1 0.012 5 0.005
United Kingdom 0.178 8 0.096 45 0.049
Uruguay 0.175 11 0.133 63 0.069
Israel 0.167 1 0.012 6 0.007
¡Genial! Tenemos a Malaysia como el país con mayor probabilidad de tener gripe! El 100% de las personas ahí tienen gripe (mean_target=1.000).
Pero nuestro sentido común nos aconseja que quizás algo anda mal….
¿Cuántas filas tiene Malaysia? Respuesta: 1. -columna: q_rows=1 ¿Cuántos casos positivos tiene Malaysia? Respuesta: 1 -columna: sum_target=1.
Dado que no se puede aumentar la muestra vean que si esta proporción se mantiene alta, contribuirá a un sobreajuste y creará un sesgo en el modelo predictivo.
¿Y qué pasa con Mexico? 2 de cada 3 tienen gripe…. todavía parece baja. Sin embargo, Uruguay tiene un 17,5% de probabilidad -11 de 63 casos- y estos 63 casos representan casi el 7% de la población total (perc_rows=0.069), esta proporción parece más creíble.
A continuación se presentan algunas ideas para tratar esto:
2.3.4.1 Caso 1: Reducción mediante la recategorización de valores menos representativos
Mantengamos todos los casos que tengan al menos un determinado porcentaje de representación en los datos. Supongamos que cambiamos el nombre de los países que tienen menos del 1% de presencia en los datos a others.
country_profiling = categ_analysis( data=data_country, input='country', target='has_flu')countries_high_rep = country_profiling[ country_profiling['perc_rows'] >0.01]['country'].tolist()# Si no pertenece a countries_high_rep entonces lo# asignamos a la categoría `other`data_country['country_new'] = data_country['country'].apply(lambda x: x if x in countries_high_rep else'other')
Hemos reducido drásticamente la cantidad de países -~74% menos- sólo reduciendo la cantidad de países al recategorizar al 1% menos representativo. Quedaron 18 de los 70 países.
La probabilidad de una variable objetivo se ha estabilizado un poco más en la categoría “other”. Ahora cuando el modelo predictivo veaMalaysiano asignará el 100% de la probabilidad, sino el ~4.1% (mean_target del grupo other).
Consejo sobre este último método
Tengan cuidado al aplicar esta técnica a ciegas. A veces, en una predicción objetivo altamente desequilibrada -por ejemplo, detección de anomalías- el comportamiento anormal está presente en menos del 1% de los casos.
# Replicar los datosd_abnormal = data_country.copy()# Simular comportamiento anormal en algunos paísesd_abnormal['abnormal'] = d_abnormal['country'].apply(lambda x: 'yes'if x in ['Brazil', 'Chile']else'no')# Análisis categóricoab_analysis = categ_analysis( d_abnormal, input='country', target='abnormal')# Visualizar sólo los primeros 6 elementosprint(ab_analysis.head(6) .rename(columns=_ren_cp) .to_string(index=False))# Inspeccionar la distribuciónprint("\nDistribución de 'abnormal':")print(d_abnormal['abnormal'].value_counts())
country mean_tgt sum_tgt pct_tgt rows pct_rows
Brazil 1.0 13 0.867 13 0.014
Chile 1.0 2 0.133 2 0.002
Argentina 0.0 0 0.000 9 0.010
New Zealand 0.0 0 0.000 4 0.004
Poland 0.0 0 0.000 13 0.014
Philippines 0.0 0 0.000 7 0.008
Distribución de 'abnormal':
abnormal
no 895
yes 15
Name: count, dtype: int64
¿Cuántos valores anormales hay?
Sólo 15, y representan el 1,65% de los valores totales.
Comprobando la tabla devuelta por categ_analysis, podemos ver que este comportamiento anormal ocurre sólo en categorías con una participación realmente baja: Brazil que está presente en sólo 1,4% de los casos, y Chile con 0,2%.
En este caso, crear una categoría other basada en la distribución no es una buena idea.
Conclusión:
A pesar de que este es un ejemplo preparado, hay algunas técnicas de preparación de datos que pueden ser realmente útiles en términos de precisión, pero necesitan cierta supervisión. Esta supervisión puede ser con ayuda de algoritmos.
2.3.4.2 Caso 2: Reducción mediante agrupación automática
Este procedimiento utiliza la técnica de clustering o agrupamiento kmeans y la tabla devuelta por categ_analysis para crear grupos -clusters- que contienen categorías que muestran un comportamiento similar en términos de:
perc_rows
perc_target
La combinación de ambos nos llevará a encontrar grupos en base a la probabilidad y la representatividad.
Manos a la obra en Python:
Definimos el parámetro n_groups, es el número de grupos deseados. El número es relativo a los datos y a la cantidad de categorías totales. Pero un número general estaría entre 3 y 10.
La función auto_grouping del paquete funpymodeling (equivalente a funModeling::auto_grouping). Por favor noten que el parámetro target sólo funciona para variables binarias (en la versión de R se menciona que funciona para variables no binarias también).
Note: el parámetro seed es opcional, pero al asignarle un número siempre obtendrá los mismos resultados.
country country_rec
Malaysia group_9
Mexico group_9
Portugal group_9
United Kingdom group_3
Uruguay group_8
Israel group_9
Switzerland group_9
Canada group_7
France group_2
Argentina group_9
Germany group_7
Australia group_6
Romania group_9
Spain group_9
Sweden group_9
Netherlands group_1
Turkey group_5
Morocco group_4
New Zealand group_4
Norway group_4
Peru group_4
Montenegro group_4
Moldova, Republic of group_4
Pakistan group_4
Palestinian Territory group_4
Russian Federation group_4
Philippines group_1
Poland group_1
Saudi Arabia group_4
Senegal group_4
Singapore group_1
Slovenia group_4
South Africa group_1
Taiwan group_4
Thailand group_4
Ukraine group_4
Malta group_4
Latvia group_4
Luxembourg group_4
Lithuania group_4
Austria group_4
Bangladesh group_4
Belgium group_1
Bosnia and Herzegovina group_4
Brazil group_1
Bulgaria group_1
Cambodia group_4
Chile group_4
China group_5
Costa Rica group_4
Croatia group_4
Cyprus group_4
Czech Republic group_4
Denmark group_4
Dominican Republic group_4
Egypt group_4
Finland group_4
Ghana group_4
Greece group_4
Honduras group_4
Hong Kong group_1
Indonesia group_4
Iran, Islamic Republic of group_4
Ireland group_4
Isle of Man group_4
Italy group_1
Japan group_1
Korea, Republic of group_4
Asia/Pacific Region group_4
Vietnam group_4
auto_grouping devuelve un diccionario que contiene 3 objetos:
df_equivalence: data frame que contiene una tabla para encontrar las equivalencias entre datos viejos y nuevos.
fit_cluster: modelo k-means que se utiliza para reducir la cardinalidad (los valores se escalan).
recateg_results: data frame que contiene el análisis numérico de cada grupo con respecto a la variable objetivo. La primera columna ajusta su nombre a la variable de entrada. En este caso tenemos: country_rec. Cada grupo corresponde a una o varias categorías de la variable de entrada (como vimos en df_equivalence).
Exploremos cómo se comportan los nuevos grupos, esto es lo que verá el modelo predictivo:
Ahora hacemos las transformaciones adicionales. Identificamos los grupos con probabilidad 0 o muy baja y los agrupamos:
# Identificar grupos con mean_target=0 o muy bajorecateg = country_groups['recateg_results']zero_groups = recateg[ recateg['mean_target'] ==0]['country_rec'].tolist()low_groups = recateg[ (recateg['mean_target'] >0) & (recateg['sum_target'] <=2)]['country_rec'].tolist()# Reemplazardata_country_2['country_rec'] = ( data_country_2['country_rec'].apply(lambda x: 'low_likelihood'if x in zero_groupselse ('low_target_share'if x in low_groupselse x)))
Verificando la agrupación final (variable country_rec):
Cada grupo parece tener un buen tamaño de muestra con respecto a la distribución de sum_target.
Todos los grupos parecen tener una buena representación. Esto se puede comprobar en la variable perc_rows.
Intentar con un número menor de clusters puede ayudar a reducir un poco esta tarea manual. Esto fue sólo una demostración de cómo optimizar una variable que tiene muchas categorías diferentes.
2.3.5 Manejo de nuevas categorías cuando el modelo predictivo está en producción
Imaginemos que aparece un nuevo país, new_country_hello_world, los modelos predictivos fallarán ya que fueron entrenados con valores fijos. Una técnica es asignar un grupo que tenga mean_target=0.
Es similar al caso del último ejemplo. Pero la diferencia está en ciertos grupos: estas categorías encajarían mejor en un grupo de probabilidad media que en un valor completamente nuevo.
Después de un tiempo deberíamos reconstruir el modelo con todos los nuevos valores, de lo contrario estaríamos penalizando a new_country_hello_world si tiene una buena probabilidad.
En otras palabras:
¿Aparece una nueva categoría? Envíenla al grupo menos significativo. Después de un tiempo, vuelvan a analizar su impacto. ¿Tiene una probabilidad media o alta? Cámbienla al grupo más adecuado.
2.3.6 ¿Los modelos predictivos pueden manejan la alta cardinalidad? Parte 1
Sí, y no. Algunos modelos tratan mejor que otros este asunto de la alta cardinalidad. En algunos escenarios, esta preparación de datos puede no ser necesaria. Este libro trata de exponer este tema que, a veces, puede llevar a un mejor modelo.
Ahora, vamos a atravesar este tema construyendo dos modelos predictivos: Máquina de potenciación del gradiente - bastante robusta para muchas entradas de datos diferentes.
El primer modelo no tiene datos tratados, y el segundo ha sido tratado por las funciones del paquete funpymodeling.
Estamos midiendo la precisión basándonos en el área ROC, que oscila entre 0.5 y 1; cuanto más alto sea el número, mejor será el modelo. Vamos a utilizar la validación cruzada para estar seguros del valor. La importancia de la validación cruzada de los resultados se trata en el capítulo Conociendo el error.
# Construir el primer modelo, sin reducir la# cardinalidad.# Preparar los datos: convertir country a dummiesdata_model = data_country_2.copy()data_model['target'] = ( data_model['has_flu'] =='yes').astype(int)# Modelo 1: con variable country original (dummies)X1 = pd.get_dummies(data_model[['country']], dtype=int)y = data_model['target']gbm1 = GradientBoostingClassifier( n_estimators=100, random_state=42)scores1 = cross_val_score(gbm1, X1, y, cv=4, scoring='roc_auc')roc =round(scores1.mean(), 2)print(f"ROC (sin reducir cardinalidad): {roc}")
ROC (sin reducir cardinalidad): 0.65
El área debajo de la curva ROC es: el valor roc mostrado arriba.
Ahora hacemos el mismo modelo con los mismos parámetros, pero aplicando la preparación de datos que hicimos antes.
# Construir el segundo modelo, basándonos en la# variable country_recX2 = pd.get_dummies(data_model[['country_rec']], dtype=int)gbm2 = GradientBoostingClassifier( n_estimators=100, random_state=42)scores2 = cross_val_score(gbm2, X2, y, cv=4, scoring='roc_auc')new_roc =round(scores2.mean(), 2)print(f"ROC (con cardinalidad reducida): {new_roc}")
ROC (con cardinalidad reducida): 0.67
Luego calculamos el porcentaje de mejora con respecto al primer valor de ROC:
Hemos utilizado uno de los modelos más robustos, máquina de potenciación del gradiente, y hemos comparado el rendimiento. Si probamos otro modelo, por ejemplo regresión logística, que es más sensible a los datos sucios, obtendremos una mayor diferencia entre reducir y no reducir la cardinalidad.
En lecturas adicionales hay un punto de referencia de diferentes tratamientos para variables categóricas y cómo cada una aumenta o disminuye la precisión.
2.3.7 ¿Los modelos predictivos pueden manejan la alta cardinalidad? Parte 2
Revisemos cómo algunos modelos lidian con esto:
Árboles de decisión: Tienden a seleccionar variables con alta cardinalidad en la parte superior, dándoles más importancia que a otras, en función de la ganancia de información. En la práctica, es una prueba de que está sobreajustado. Este modelo es bueno para ver la diferencia entre reducir o no una variable de alta cardinalidad.
Random forest: maneja variables categóricas con muchas categorías, pero puede ser limitado. Es muy probable que esta limitación sea para evitar el sobreajuste. Este punto, en conjunción con la naturaleza del algoritmo -crea muchos árboles-, reduce el efecto de un único árbol de decisión al elegir una variable de alta cardinalidad.
Gradient Boosting Machine y Regresión logística: convierten variables categóricas internas en variables flag o dummy. En el ejemplo que vimos sobre los países, implica la creación -interna- de 70 variables flag.
Comprobemos el modelo que creamos antes:
# Verificar el primer modelo...print(f"Cantidad de features: {X1.shape[1]}")print(f"\nImportancia de las variables (top 10):")gbm1.fit(X1, y)importances = pd.Series( gbm1.feature_importances_, index=X1.columns).sort_values(ascending=False)print(importances.head(10))
Cantidad de features: 70
Importancia de las variables (top 10):
country_France 0.239689
country_Mexico 0.178809
country_Uruguay 0.145322
country_Malaysia 0.136391
country_United Kingdom 0.118960
country_Canada 0.039445
country_China 0.036381
country_Portugal 0.019950
country_Australia 0.019723
country_Germany 0.016789
dtype: float64
Eso es: las variables flag representan a los países, pero muchas fueron reportadas como no relevantes para la predicción.
Esto está relacionado con Ingeniería de variables. Además, está relacionado con Selección de las mejores variables. Es una práctica muy recomendable seleccionar primero las variables que contienen más información y luego crear el modelo predictivo.
Conclusión: la reducción de la cardinalidad reducirá la cantidad de variables en estos modelos.
2.3.8 Variable objetivo numérica o multinomial
Hasta ahora, el libro sólo cubrió casos donde la variable objetivo era una variable binaria. Está previsto que en el futuro abarque también variables objetivo numéricas y multi-valor.
Sin embargo, si leyeron hasta aquí, puede que quieran explorar por su cuenta teniendo en mente la misma idea. En las variables numéricas, por ejemplo la previsión de page visits en un sitio web, habrá ciertas categorías de la variable de entrada que estarán más relacionadas con un valor alto en las visitas, mientras que hay otras que están más correlacionadas con valores bajos.
Lo mismo ocurre con la variable de salida multinomial, habrá algunas categorías más relacionadas con ciertos valores. Por ejemplo, prediciendo el grado de epidemia: high, mid o low según la ciudad. Habrá algunas ciudades que se correlacionarán más con un alto nivel epidémico que otras.
2.3.9 ¿Qué beneficio “extra” obtuvimos con la agrupación?
Saber cómo las categorías fueron asignadas a los grupos nos brinda información que -en algunos casos- es bueno registrar. Las categorías que pertenezcan a un mismo grupo van a tener un comportamiento similar -en términos de representatividad y poder predictivo.
Si Argentina y Chile están en el group_1, entonces son iguales, y así es cómo las verá el modelo.
2.3.10 Representatividad o tamaño de muestra
Este concepto aplica al análisis de cualquier variable categórica, pero es un tema muy común en la ciencia de datos y las estadísticas: tamaño de muestra. ¿Cuántos datos necesitamos para ver el patrón bien desarrollado?
En una variable categórica: ¿Cuántos casos de la categoría “X” necesitamos para confiar en la correlación entre el valor “X” y un valor objetivo? Esto es lo que hemos analizado.
En términos generales: cuanto más difícil sea predecir un evento, más casos vamos a necesitar…
Más adelante en este libro abarcaremos este tema desde otros puntos de vista refiriéndonos de vuelta a esta página.
2.3.11 Reflexiones finales
Vimos dos casos para reducir la cardinalidad, al primero no le importa la variable objetivo, lo que puede ser peligroso en un modelo predictivo, mientras que al segundo sí. Crea una nueva variable basada en la afinidad -y representatividad- de cada categoría de entrada con la variable objetivo.
Concepto clave: representatividad de cada categoría respecto a sí misma, y respecto al evento que se va a predecir. Un buen punto a explorar es analizarlo basándonos en pruebas estadísticas.
Lo que se mencionó al principio con respecto a destruir la información en la variable de entrada implica que la agrupación resultante tiene las mismas proporciones entre grupos (en una variable binaria de entrada).
¿Siempre debemos reducir la cardinalidad? Depende, dos pruebas con un simple dato no son suficientes para extrapolar a todos los casos. Esperamos que sea un buen comienzo para que el lector empiece a hacer sus propias optimizaciones cuando lo considere relevante para el proyecto.
El concepto de valores extremos, al igual que otros temas en machine learning, no es un concepto exclusivo de esta área. Lo que hoy es un valor atípico puede que mañana no lo sea. Los límites entre el comportamiento normal y el anormal son difusos; por otro lado, pararse en los extremos es fácil.
¿Qué es un valor atípico? Enfoques filosóficos y prácticos
Valores atípicos por dimensionalidad y tipo de datos (numéricos o categóricos)
Cómo detectar valores atípicos en Python (bottom/top X%, Tukey y Hampel)
Preparación de valores atípicos para análisis numérico en Python
Preparación de valores atípicos para modelado predictivo en Python
2.4.2 La intuición detrás de los valores atípicos
Por ejemplo, consideren la siguiente distribución:
# Crear un conjunto de datos de muestranp.random.seed(31415)df_1 = pd.DataFrame({'var': np.round(10000* np.random.beta(0.15, 2.5, 1000))})# Graficarfig, ax = plt.subplots(figsize=(8, 4))ax.hist(df_1['var'], bins=20, color='steelblue', edgecolor='white')ax.set(xlabel='var', ylabel='Frecuencia')plt.tight_layout()plt.show()
Distribución de muestra con cola larga
La variable está sesgada hacia la izquierda, mostrando algunos puntos atípicos a la derecha. Queremos lidiar con ellos. Entonces, surge la pregunta: ¿Dónde definimos el umbral de lo extremo? Basándonos en la intuición, puede ser el 1% más alto, o podemos analizar cómo cambia el promedio si quitamos el 1% más alto.
Ambos casos podrían estar bien. De hecho, tomar otro número como el umbral (es decir, 2% o 0,1%), también puede ser correcto. Vamos a visualizarlos:
# Calcular los percentiles del 3% y 1% superiorpercentile_var = df_1['var'].quantile( [0.98, 0.99, 0.999])df_p = pd.DataFrame({'value': percentile_var.values,'percentile': ['a_98th', 'b_99th', 'c_99.9th']})# Graficar la misma distribución más los percentilesfig, ax = plt.subplots(figsize=(8, 4))ax.hist(df_1['var'], bins=20, color='steelblue', edgecolor='white', alpha=0.7)colors_p = ['green', 'red', 'purple']for i, row in df_p.iterrows(): ax.axvline(x=row['value'], linestyle='--', color=colors_p[i], label=row['percentile'])ax.legend()ax.set(xlabel='var', ylabel='Frecuencia')plt.tight_layout()plt.show()
Por ahora, seguiremos con el 1% superior (percentil 99) como el umbral para marcar todos los puntos que estén más allá como valores atípicos.
Marcando el 1 porciento superior como atípico
Aquí surge un elemento conceptual interesante: cuando definimos lo anormal (o una anomalía), el concepto de normal emerge como su opuesto.
Este comportamiento “normal” está representado en el área verde:
Mismo umbral, diferente perspectiva
Lo difícil es determinar dónde se separa lo normal de lo anormal. Hay varios enfoques para lidiar con esto. Vamos a repasar algunos de ellos.
2.4.3 ¿Cuál es el límite entre clima cálido y clima frío?
Hagamos esta sección más filosófica. Algunos buenos matemáticos también fueron filósofos, como es el caso de Pitágoras e Isaac Newton.
¿Dónde podemos poner el umbral para indicar que comienza el clima cálido o, a la inversa, que termina el clima frío?
¿Cuál es el punto de corte?
Cerca del Ecuador, una temperatura cerca de los 10ºC (50ºF) probablemente sea un valor extremadamente bajo; sin embargo, en la Antártida, ¡sería un día de playa!
“¡Oh! ¡Pero eso sería tomar un ejemplo extremo con dos locaciones diferentes!”
¡No hay problema! Hagamos zoom a una ciudad, como un fractal, el límite donde una empieza (y otra termina) no tendrá un único valor para determinar lo siguiente: “Ok, el clima cálido empieza en los 25.5ºC (78ºF).”
Es relativo.
Sin embargo, es bastante fácil pararse en los extremos, donde la incertidumbre disminuye a casi cero. Por ejemplo, cuando consideramos una temperatura de 60ºC (140ºF).
“Ok. Pero, ¿cómo se relacionan estos conceptos con machine learning?”
Estamos exponiendo aquí la relatividad que existe al considerar una etiqueta (cálido/frío) como una variable numérica (temperatura). Esto puede ser considerado para cualquier otra variable numérica, como los ingresos económicos y las etiquetas “normal” y “anormal”.
Entender los valores extremos es una de las primeras tareas en análisis exploratorio de datos. Entonces podremos ver cuáles son los valores normales. Esto se trata en el capítulo Análisis numérico, La voz de los números.
Existen varios métodos para marcar valores como valores atípicos. Así como podríamos analizar la temperatura, esta marca es relativa y todos los métodos pueden ser correctos. El método más rápido puede ser tratar el X% superior e inferior como valores atípicos.
Los métodos más robustos consideran las variables de distribución utilizando cuantiles (método de Tukey) o la dispersión de los valores a través de la desviación estándar (método de Hampel).
La definición de estos límites es una de las tareas más comunes en machine learning. ¿Por qué? ¿Cuándo? Señalemos dos ejemplos:
Ejemplo 1: Cuando desarrollamos un modelo predictivo que devuelve una probabilidad de llamar o no llamar a un determinado cliente, necesitamos configurar el umbral para asignar la etiqueta final: “¡sí, llamar!”/“no llamar”. Hay más información sobre esto en el capítulo de Scoring de datos.
Ejemplo 2: Otro ejemplo se da cuando necesitamos discretizar una variable numérica porque necesitamos que sea categórica. Los límites en cada segmento afectarán al resultado general. Hay más información sobre esto en la sección Discretizando variables numéricas
Volviendo al problema original (¿Dónde termina el clima frío?), no todas las preguntas necesitan una respuesta: algunas solamente nos ayudan a pensar.
2.4.4 El impacto de los valores atípicos
2.4.4.1 Construcción de modelos
Algunos modelos, como el bosque aleatorio y las máquinas de potenciación del gradiente, tienden a lidiar mejor con los valores atípicos; sin embargo, el “ruido” puede afectar los resultados de todos modos. El impacto de los valores atípicos en estos modelos es menor que en otros, como las regresiones lineales, las regresiones logísticas, los kmeans y los árboles de decisión.
Un aspecto que contribuye a la disminución del impacto es que ambos modelos crean muchos sub-modelos. Si cualquiera de los modelos toma un valor atípico como información, entonces otros sub-modelos probablemente no lo harán; por lo tanto, el error se cancela. El equilibrio yace en la pluralidad de voces.
2.4.4.2 Comunicar los resultados
Si debemos informar cuáles fueron las variables utilizadas en el modelo, terminaremos quitando los valores atípicos para no mostrar un histograma con una sola barra y/o un sesgo en el promedio.
Es mejor mostrar un número no sesgado que justificar que el modelo podrá lidiar con valores extremos.
2.4.4.3 Tipos de valores atípicos según el tipo de datos
Numéricos: como los que vimos antes:
Variable numérica con valores atípicos
Categóricos: Tener una variable en la que la dispersión de la categorías es bastante alta (alta cardinalidad): por ejemplo, código postal. Hay más información sobre cómo lidiar con valores atípicos en variables categóricas en el capítulo Variables de alta cardinalidad en estadística descriptiva.
country
frequency
percentage
cumulative_perc
0
France
288
0.6874
0.6874
1
China
65
0.1551
0.8425
2
Uruguay
63
0.1504
0.9929
3
Peru
2
0.0048
0.9977
4
Vietnam
1
0.0024
1.0000
Variable categórica con valores atípicos
Peru y Vietnam son valores atípicos en este ejemplo dado que su participación en los datos es inferior al 1%.
2.4.4.4 Tipos de valores atípicos según dimensionalidad
Hasta ahora, hemos observado valores atípicos unidimensionales y univariados. También podemos considerar dos o más variables en simultáneo.
Por ejemplo, tenemos el siguiente conjunto de datos, df_hello_world, con dos variables: v1 y v2. Haciendo el mismo análisis que antes:
v1 frequency percentage cumulative_perc
0 Uruguay 80 0.597 0.597
1 Argentina 54 0.403 1.000
----------------------------------------------------------------
v2 frequency percentage cumulative_perc
0 cat_A 83 0.6194 0.6194
1 cat_B 51 0.3806 1.0000
----------------------------------------------------------------
'Variables processed: v1, v2'
Valores atípicos según dimensionalidad
Por ahora no hay valores atípicos, ¿correcto?
Ahora creamos una tabla de contingencia que nos diga la distribución de ambas variables, una contra la otra:
v2 cat_A cat_B
v1
Argentina 39.55 0.75
Uruguay 22.39 37.31
¡Oh! La combinación de Argentina y cat_B es realmente baja (0.75%) en comparación con los otros valores (menos del 1%), mientras que las otras intersecciones están por encima del 22%.
2.4.4.5 Algunas reflexiones…
Los últimos ejemplos muestran el potencial de los valores extremos o atípicos y están presentados como consideraciones que tenemos que tener en cuenta con un nuevo conjunto de datos.
Mencionamos 1% como un posible umbral para marcar un valor como atípico. Este número podría ser 0.5% o 3%, dependiendo del caso.
Además, la presencia de este tipo de valores atípicos podría no traer problemas.
2.4.5 Cómo lidiar con valores atípicos en Python
La función prep_outliers del paquete funpymodeling (equivalente a funModeling::prep_outliers) puede ayudarnos con esta tarea. Puede manejar de una a ‘N’ variables en simultáneo (especificando el parámetro input).
El núcleo es el siguiente:
Soporta tres métodos diferentes (parámetro method) para considerar un valor como un outlier: bottom_top, Tukey, y Hampel.
Funciona en dos modos (parámetro type) al establecer un valor NaN o al frenar la variable en un valor particular.
2.4.6 Paso 1: Cómo detectar valores atípicos
Los siguientes métodos se implementan en la función prep_outliers. Obtienen diferentes resultados para que el usuario pueda seleccionar los que mejor se ajustan a sus necesidades.
2.4.6.0.1 Método de valores ‘bottom’ y ‘top’
Esto considera valores atípicos tomando los valores del X% inferior y superior, basados en el percentil. Los puntos de corte más utilizados son 0.5%, 1%, 1.5%, 3%, entre otros.
Configurando el parámetro top_percent en 0.01 se tratarán todos los valores del 1% superior.
La misma lógica aplica a los valores más bajos: si se establece el parámetro bottom_percent en 0.01 se marcará como valores atípicos al 1% más bajo de todos los valores.
La función interna utilizada es quantile; si queremos marcar el 1% inferior y el superior, escribimos:
Este método marca valores atípicos utilizando los valores cuartiles, Q1, Q2, y Q3, donde Q1 es esquivalente al percentil 25, Q2 al percentil 50 (también conocido como la mediana), y Q3 es el percentil 75.
El rango intercuartil (IQR por sus siglas en inglés) se calcula haciendo Q3 - Q1.
La fórmula:
El umbral inferior es: Q1 - 3*IQR. Todos los valores que queden por debajo son considerados atípicos.
El umbral superior es: Q3 + 3*IQR. Todos los valores que queden por encima son considerados atípicos.
El valor 3 es para detectar el límite “extremo”. Este método viene del diagrama de caja, donde el multiplicador es 1.5 (no 3). Esto hace que muchos más valores sean marcados como atípicos, lo veremos en la siguiente imagen.
Cómo interpretar un diagrama de caja
Podemos acceder a la función tukey_outlier para calcular el límite de Tukey:
print(tukey_outlier(heart_disease['age']))
{'lower': 9.0, 'upper': 100.0}
Devuelve un diccionario con dos valores; por lo tanto, tenemos el umbral inferior y el superior: todos los valores que estén por debajo de nueve y por encima de 100 serán considerados atípicos.
2.4.6.0.3 Método de Hampel
La fórmula:
El umbral inferior es: median_value - 3*mad_value. Todos los valores que queden por debajo son considerados atípicos.
El umbral superior es: median_value + 3*mad_value. Todos los valores que queden por encima son considerados atípicos.
Podemos acceder a la función hampel_outlier para calcular el límite de Hampel:
print(hampel_outlier(heart_disease['age']))
{'lower': 29.31, 'upper': 82.69}
Devuelve un diccionario con dos valores; por lo tanto, tenemos el umbral inferior y el superior.
Tiene un parámetro llamado k_mad_value, y su valor por defecto es 3. El valor k_mad_value puede ser modificado.
Cuanto más alto sea el valor k_mad_value, más amplio será el rango de los umbrales (los límites se alejarán más de la mediana).
2.4.7 Paso 2: ¿Qué hacemos con los valores atípicos?
Ya detectamos qué puntos son los atípicos. Ahora, la pregunta es ¿Qué hacemos con ellos?
Hay dos escenarios posibles:
Escenario 1: Preparar los valores atípicos para el análisis numérico
Escenario 2: Preparar los valores atípicos para modelado predictivo
Hay un tercer escenario en el que no hacemos nada con los valores atípicos detectados. Simplemente los dejamos ser.
Proponemos recurrir a la función prep_outliers del paquete funpymodeling que nos dará una mano con esta tarea.
Más allá de la función en sí, lo importante aquí es el concepto subyacente y la posibilidad de desarrollar un método superador.
La función prep_outliers abarca estos dos escenarios con el parámetro type:
type = "set_na", para el escenario 1
type = "stop", para el escenario 2
2.4.7.1 Escenario 1: Preparar los valores atípicos para el análisis numérico
El análisis inicial:
En este caso, todos los valores atípicos son convertidos a NaN, por lo que, al aplicar la mayoría de las funciones características (máx, mín, promedio, etc.) obtendremos un valor menos sesgado.
Por ejemplo, consideremos la siguiente variable (la que vimos al principio con algunos valores atípicos):
# Para entender todas estas métricas, por favor# diríjanse al capítulo sobre Análisis numéricoprof_df1 = profiling_num(df_1)print(prof_df1[['variable', 'mean', 'std_dev','variation_coef']].to_string(index=False))print()print(prof_df1[['variable', 'p_01', 'p_05','p_50', 'p_95', 'p_99']].to_string( index=False))print()print(prof_df1[['variable', 'skewness', 'kurtosis','iqr']].to_string(index=False))
variable mean std_dev variation_coef
var 594.378 1215.0494 2.0442
variable p_01 p_05 p_50 p_95 p_99
var 0.0 0.0 29.0 3490.2 5549.53
variable skewness kurtosis iqr
var 2.8079 8.0599 550.5
Aquí podemos ver varios indicadores que nos dan algunas pistas. El desvío estándar std_dev es realmente alto comparado con el promedio mean, y eso se refleja en el coeficiente de variación variation_coef. Además, la curtosis es alta y el valor de p_99 es casi el doble que el de p_95.
Esta última tarea de mirar algunos números y visualizar la distribución de la variable es como imaginar una fotografía por lo que otra persona nos dice: convertimos la voz (que es una señal) en una imagen en nuestro cerebro.
2.4.7.1.1 Utilizar prep_outliers para el análisis numérico
Debemos configurar type="set_na". Esto implica que cada punto marcado como un valor atípico será convertido a NaN.
Usaremos los tres métodos: Tukey, Hampel, y bottom/top X%.
Antes de la transformación, había 0 valores NaN, mientras que después algunos valores fueron marcados como atípicos de acuerdo a la prueba de Tukey y reemplazados por NaN.
for col in ['var', 'var_tukey', 'var_hampel']: q = df_1[col].isna().sum() p =round(100* df_1[col].isna().mean(), 1)print(f"{col}: q_na={q}, p_na={p}%")
Este último método es mucho más severo al identificar valores atípicos, marcando el 36.6% de los valores como atípicos. Es probable que esto se deba a que la variable está bastante sesgada hacia la izquierda.
Método del ‘bottom’ y ‘top’ X%
Por último, podemos probar el método más fácil: quitar el 2% superior.
Comparación de métodos para identificar valores atípicos
Al seleccionar el bottom/top X%, siempre tendremos algunos valores que cumplan con esa condición, mientras que en los otros dos métodos puede que esto no suceda.
2.4.7.1.3 Conclusiones sobre el manejo de valores atípicos en el análisis numérico
La idea es modificar los valores atípicos lo menos posible (por ejemplo, si estamos interesados solamente en describir el comportamiento general).
Para lograr eso -a la hora de crear un informe ad hoc, por ejemplo- podemos usar el promedio. Podríamos elegir el método del 2% superior porque solo afecta al 2% de todos los valores y provoca una disminución drástica en el promedio.
“Modificar o no modificar el conjunto de datos, esa es la cuestión.” William Shakespeare como científico de datos.
El método de Hampel modificó demasiado el promedio. Eso fue tomando el valor estándar de este método, que es 3-MAD (un desvío estándar un tanto robusto).
Por favor tengan en cuenta que esta demostración no significa que Hampel o Tukey sean una mala elección. De hecho, son métodos más robustos porque el umbral puede ser más alto que el valor actual; de hecho, ningun valor es tratado como atípico.
En el otro extremo, podemos considerar, por ejemplo, la variable age de los datos heart_disease. Analicemos sus valores atípicos:
# Obtener el umbral de valores atípicosprint("Tukey:", tukey_outlier(heart_disease['age']))# Obtener los valores mínimos y máximosprint(f"Min: {heart_disease['age'].min()}")print(f"Max: {heart_disease['age'].max()}")
El umbral superior es 100, y el valor máximo es 77.
Ergo: la variable age no tiene valores atípicos.
Si hubiéramos utilizado el método bottom/top X%, entonces los datos dentro de esos porcentajes hubieran sido detectados como valores atípicos.
Todos los ejemplos que vimos hasta ahora tomaron una sola variable a la vez; no obstante, prep_outliers puede manejar varias en simultáneo usando el parámetro input como veremos en la siguiente sección. Todo lo que vimos hasta aquí será equivalente, excepto lo que hacemos una vez que detectamos los valores atípicos, es decir, el método de imputación.
2.4.7.2 Escenario 2: Preparar los valores atípicos para modelado predictivo
El caso anterior da como resultado que los valores atípicos observados se convierten a valores NaN. Esto es un gran problema si estamos construyendo un modelo de machine learning, ya que muchos de ellos no funcionan con valores NaN. Hay más información sobre el manejo de datos faltantes en el capítulo Datos faltantes.
Para lidiar con valores atípicos y poder usar un modelo predictivo, una buena idea es configurar el parámetro type='stop', para que todos los valores marcados como atípicos sean convertidos al valor del umbral.
Algunas cosas a tener en cuenta:
Traten de pensar en el tratamiento (y creación) de las variables como si se lo estuvieran explicando al modelo. Al frenar las variables en un determinado valor, 1% por ejemplo, le estamos diciendo al modelo: Hey, modelo, por favor considera todos los valores extremos como si estuvieran en el percentil 99, dado que este valor ya es lo suficientemente alto. Gracias.
Algunos modelos predictivos son más tolerantes al ruido que otros. Podemos ayudarlos tratando algunos de los valores atípicos. En la práctica, pre-procesar datos tratando los atípicos tiende a producir resultados más precisos cuando estamos en presencia de datos nunca vistos.
2.4.7.3 Imputar valores atípicos para modelado predictivo
Primero, creamos un conjunto de datos con algunos valores atípicos. Ahora el ejemplo tiene dos variables.
Tukey funcionó perfectamente esta vez, exponiendo un promedio más preciso para ambas variables: cercano a 1 para var1 y cercano a 0 para var2.
Observen que esta vez no hay ni un valor NaN. Lo que hizo la función esta vez fue frenar la variable en los valores umbral. Ahora, los valores mínimos y máximos serán los mismos que informó el método de Tukey.
Verificar el umbral para var1:
print(tukey_outlier(df_2['var1']))
{'lower': -4.15, 'upper': 5.79}
Ahora verificamos los valores min/max antes de la transformación:
Comparación de métodos para identificar valores atípicos (modelado predictivo)
Importante: Los puntos que están por encima del valor 100 fueron excluidos, de lo contrario, hubiera sido imposible notar la diferencia entre los métodos.
2.4.8 Reflexiones finales
Hemos abordado el tema de los valores atípicos tanto desde una perspectiva filosófica como técnica, invitando al lector a mejorar sus habilidades de pensamiento crítico a la hora de definir los límites (umbrales). Es fácil pararse en los extremos, pero encontrar el equilibrio es una tarea difícil.
En términos técnicos, cubrimos tres métodos con diferentes bases para identificar valores atípicos:
Bottom/Top X%: Este método siempre marcará valores como atípicos dado que en todas las variables hay un X% inferior y superior.
Tukey: Se basa en el clásico boxplot, que usa cuartiles.
Hampel: Es bastante restrictivo si no modificamos el parámetro por defecto. Se basa en la mediana y el valor del MAD (similar al desvío estándar pero menos sensible a los valores atípicos).
Una vez que identificamos los valores atípicos, el siguiente paso es decidir qué hacer con ellos. En algunos casos no es necesario hacer ningún tratamiento. En conjuntos de datos muy pequeños, podemos identificarlos a simple vista.
La regla de: “Sólo modificar lo que es necesario” (que también puede aplicar a la relación entre el ser humano y la naturaleza), nos dice que no tratemos o excluyamos ciegamente todos los valores atípicos extremos. Con cada acción que hicimos, introducimos algún sesgo. Por eso es tan importante saber cuáles son las implicaciones de cada método. Si es una buena decisión o no depende de la naturaleza de los datos que estamos analizando.
En modelado predictivo, aquellos que tienen algún tipo de técnica de remuestreo interno, o crean varios modelos pequeños para llegar a una predicción final, son más estables con los valores extremos. Hay más información sobre remuestreo y error en el capítulo Conociendo el error.
En algunos casos, cuando el modelo predictivo está ejecutándose en producción, es recomendable reportar o considerar la preparación de cualquier valor extremo nuevo, es decir, un valor que no estaba presente durante la construcción del modelo. Hay más información sobre este tema, pero con una variable categórica, en Variables de alta cardinalidad en modelado predictivo, sección: Manejo de nuevas categorías cuando el modelo predictivo está en producción.
Una buena prueba para que haga el lector es tomar un conjunto de datos, tratar los valores atípicos, y luego comparar algunas métricas de desempeño como Kappa, ROC, Precisión (Accuracy), etc.; ¿La preparación de los datos mejoró alguna de ellas? O, en los reportes, ver cuánto cambia el promedio. Incluso graficando, ¿ahora el gráfico nos dice algo? De esta manera, el lector creará nuevo conocimiento basándose en su experiencia.
2.5 Datos faltantes: Análisis, manejo e imputación
2.5.1 ¿De qué se trata esto?
El análisis de los valores faltantes es la estimación del vacío mismo. Los valores faltantes presentan un obstáculo a la hora de crear modelos predictivos, análisis de clusters, reportes, etc.
En este capítulo, ahondaremos en el concepto y tratamiento de valores nulos. Realizaremos análisis utilizando diferentes enfoques e interpretaremos los distintos resultados.
Si todo sale bien, después de estudiar el capítulo entero, el lector podrá entender conceptos clave del manejo de valores faltantes y podrá tomar mejores abordajes que los que proponemos aquí.
¿Qué vamos a repasar en este capítulo?
¿Qué es un valor nulo, conceptualmente?
Cuándo excluir filas o columnas.
Análisis numérico de valores faltantes.
Transformación e imputación de variables numéricas y categóricas.
Imputar valores: desde enfoques simples a algunos más complejos.
Ejemplificaremos estos temas con un enfoque práctico en Python. Este código busca ser lo suficientemente genérico para que lo puedan aplicar en sus proyectos.
2.5.2 Cuando el valor nulo representa información
Los valores vacíos también aparecen como “NULL” en bases de datos, NaN en Python (o NA en R), o simplemente el string “empty” en programas de hojas de cálculo. También pueden estar representados con algún número, como: 0, -1 o -999.
Por ejemplo, imaginen una agencia de viajes que une dos tablas, una de personas y otra de países. El resultado muestra la cantidad de viajes por persona:
person South_Africa Brazil Costa_Rica
Fotero 1.000000 5.000000 5.000000
Herno NaN NaN NaN
Mamarul 34.000000 40.000000 NaN
En este resultado, Mamarul viajó a South Africa34 veces.
¿Qué representa el valor NaN (o NULL)?
En este caso, NaN debería ser reemplazado por 0, indicando cero viajes en esa intersección persona-país. Después de la conversión, la tabla está lista para usar.
Ejemplo: Reemplazar todos los valores NaN por 0
# Hacer una copiadf_travel_2 = df_travel.copy()# Reemplazar todos los valores NaN con 0df_travel_2 = df_travel_2.fillna(0)print(df_travel_2.to_string(index=False))
person South_Africa Brazil Costa_Rica
Fotero 1.000000 5.000000 5.000000
Herno 0.000000 0.000000 0.000000
Mamarul 34.000000 40.000000 0.000000
El último ejemplo transforma todos los valores NaN en 0. No obstante, en otros escenarios, esta transformación podría no aplicar para todas las columnas.
Ejemplo: Reemplazar los valores NaN por 0 sólo en ciertas columnas
Probablemente el escenario más común sea reemplazar NaN por algún valor -cero en este caso- sólo en algunas columnas. Definimos una lista que contiene todas las variables a reemplazar y luego aplicamos fillna.
# Reemplazar valores NaN con 0 solo en las columnas# seleccionadasvars_to_replace = ['Brazil', 'Costa_Rica']df_travel_3 = df_travel.copy()df_travel_3[vars_to_replace] = ( df_travel_3[vars_to_replace].fillna(0))print(df_travel_3.to_string(index=False))
person South_Africa Brazil Costa_Rica
Fotero 1.000000 5.000000 5.000000
Herno NaN 0.000000 0.000000
Mamarul 34.000000 40.000000 0.000000
Tengan a mano la última función ya que es muy común enfrentarnos a la situación de aplicar una función especificada a un subconjunto de variables y volver a incorporar las variables transformadas y no transformadas al mismo conjunto de datos.
Vamos a un ejemplo más complejo.
2.5.3 Cuando el valor nulo es un valor nulo
En otras ocasiones, tener un valor nulo es correcto, está expresando la ausencia de algo. Debemos tratarlos para poder usar la tabla. Muchos modelos predictivos no pueden manejar tablas de entrada con valores faltantes.
En algunos casos, una variable es medida después de un período de tiempo, por lo que tenemos datos a partir de este momento y NaN en las instancias previas.
A veces hay casos aleatorios, como una máquina que falla al recoletar datos o un usuario que se olvidó de completar algún campo en un formulario, entre otros.
Aquí aparece una pregunta importante: ¿Qué hacemos?
Las siguientes recomendaciones son simplemente eso, recomendaciones. Pueden probar diferentes enfoques para descubrir cuál es la mejor estrategia para los datos que están analizando. No existe un “talle único y universal” en esto.
2.5.4 Excluir la fila entera
Si al menos una columna tiene un valor NaN, excluyan la fila.
Es un método fácil y rápido, ¿no? Lo recomendamos cuando la cantidad de filas con valores faltantes sea baja. Pero, ¿cuán baja es baja? Eso depende de ustedes. Diez casos en 1,000 filas pueden no tener un gran impacto, a menos que esos 10 casos estén vinculados con la predicción de una anomalía; en esta instancia, representan información. Señalamos este tema en Caso 1: Reducción mediante la recategorización de valores menos representativos.
Ejemplo en Python:
Inspeccionemos el conjunto de datos heart_disease con la función status, dado que uno de sus objetivos principales es ayudarnos con este tipo de decisiones.
st = status(heart_disease)print(st[['variable', 'q_nan', 'p_nan']].round(2).sort_values('q_nan', ascending=False).to_string(index=False))
q_nan indica la cantidad de valores NaN y p_nan es el porcentaje. Pueden encontrar toda la información sobre la función status en el capítulo Análisis numérico, La voz de los números.
Dos variables tienen filas con valores NaN, entonces excluimos estas filas:
# dropna devuelve el mismo data frame habiendo excluido# todas las filas que contenían al menos un valor NaNheart_disease_clean = heart_disease.dropna()# número de filas antes de la exclusión:print(f"Filas antes: {len(heart_disease)}")# número de filas después de la exclusión:print(f"Filas después: {len(heart_disease_clean)}")
Filas antes: 303
Filas después: 297
Después de la exclusión, unas pocas filas fueron eliminadas. Este enfoque parece adecuado para este conjunto de datos.
Sin embargo, existen otros escenarios en los que casi todos los casos son valores vacíos, por lo que ¡esta exclusión eliminaría todo el conjunto de datos!
2.5.5 Excluir la columna
En una operación similar al último caso, excluimos la columna. Si aplicamos el mismo razonamiento y la eliminación es sólo de unas pocas columnas y las restantes proveen un resultado final confiable, entonces es aceptable.
Ejemplo en Python:
Estas exclusiones se pueden manejar fácilmente con la función status. El siguiente código va a conservar todos los nombres de las variables cuyo porcentaje de valores NaN es mayor que 0.
# Obtener nombres de variables que contienen valores NAst = status(heart_disease)vars_to_exclude = st[st['p_nan'] >0]['variable'].tolist()# Verificar las variables a excluirprint("Variables a excluir:", vars_to_exclude)# Excluir las variables del conjunto de datos originalheart_disease_clean_2 = heart_disease.drop( columns=vars_to_exclude)
Variables a excluir: ['num_vessels_flour', 'thal']
2.5.6 Tratamiento de valores vacíos en variables categóricas
Abarcaremos diferentes perspectivas tanto para convertir como para tratar valores vacíos en variables nominales.
Los datos del siguiente ejemplo fueron derivados de web_navigation_data, que contiene información estándar sobre cómo llegan los usuarios a un determinado sitio web. Contiene source_page (la página desde la que proviene el usuario), landing_page (primera página visitada en el sitio), y country.
# Cargar los datos de navegación webweb_navigation_data = pd.read_csv("images/web_navigation_data.txt", sep="\t", na_values="")
variable type q_nan p_nan
source_page object 50 0.520000
landing_page object 5 0.050000
country object 3 0.030000
Las tres variables tienen valores vacíos (NaN). Falta casi la mitad de los valores en source_page, mientras que las otras dos variables tienen un porcentaje menor de valores NaN.
2.5.6.2 Caso A: Convertir el valor nulo en un string
En variables categóricas o nominales, el tratamiento más rápido es convertir el valor nulo en el string unknown. Así, el modelo de machine learning va a tomar los valores “vacíos” como otra categoría. Piénsenlo como una regla: “Si variable_X = unknown, entonces el resultado = sí”.
A continuación, proponemos dos métodos para cubrir los escenarios típicos:
Ejemplo en Python:
# Método 1: Convertir una sola variableweb_navigation_data_1 = web_navigation_data.copy()web_navigation_data_1['source_page'] = ( web_navigation_data_1['source_page'].fillna('unknown_source'))# Método 2: Es una situación típica la de aplicar una# función sólo a variables específicas y luego# reingresarlas al data frame original# Imaginen que queremos convertir todas las variables# que tengan menos de 6% de valores NA:stat_nav = status(web_navigation_data)vars_to_process = stat_nav[ stat_nav['p_nan'] <0.06]['variable'].tolist()print("Variables a procesar:", vars_to_process)# Crear un nuevo data frame con las variables# transformadasweb_navigation_data_2 = web_navigation_data.copy()for v in vars_to_process: web_navigation_data_2[v] = ( web_navigation_data_2[v].fillna('other'))
2.5.6.3 Caso B: Asignar la categoría más frecuente
La intuición detrás de este método es agregar más de lo mismo para no afectar la variable. Sin embargo, a veces la afecta. No tendrá el mismo impacto si el valor más común aparece el 90% de las ocasiones que si aparece el 10%; es decir, depende de la distribución.
Hay otros escenarios en los que podemos incorporar nuevos valores faltantes basándonos en modelos predictivos como k-NN. Este enfoque es más apropiado que reemplazar por el valor más frecuente. No obstante, la técnica recomendada es la que vimos en Caso A: Convertir el valor nulo en un string.
2.5.6.4 Caso C: Excluir algunas columnas y transformar otras
El caso sencillo sería que la columna contenga, digamos, 50% de casos NaN, dado que sería altamente probable que los datos no sean confiables.
En el caso que vimos antes, source_page tiene más de la mitad de los valores vacíos. Podríamos excluir esta variable y transformar -como lo hicimos- las otras dos.
El ejemplo está preparado como genérico:
# Configurar el umbralthreshold_to_exclude =0.50# Representa 50%stat_nav = status(web_navigation_data)vars_to_exclude = stat_nav[ stat_nav['p_nan'] >= threshold_to_exclude]['variable'].tolist()vars_to_keep = stat_nav[ stat_nav['p_nan'] < threshold_to_exclude]['variable'].tolist()# Finalmente...print("Variables a excluir:", vars_to_exclude)print("Variables a conservar:", vars_to_keep)# La próxima línea excluirá las variables que queden# por encima del umbral y transformará las demásweb_navigation_data_3 = web_navigation_data.drop( columns=vars_to_exclude).copy()for v in vars_to_keep: web_navigation_data_3[v] = ( web_navigation_data_3[v].fillna('unknown'))# Verificar que no haya valores NaN y que la variable# que estaba por encima del umbral haya desaparecidoprint("\nResultado final:")print(status(web_navigation_data_3)[ ['variable', 'q_nan', 'p_nan']].round(2).to_string( index=False))
Variables a excluir: ['source_page']
Variables a conservar: ['landing_page', 'country']
Resultado final:
variable q_nan p_nan
landing_page 0 0.000000
country 0 0.000000
2.5.6.5 Resumiendo
¿Qué pasa si los datos tienen 40% de valores NaN? Depende del objetivo del análisis y de la naturaleza de los datos.
Lo importante aquí es “salvar” la variable para poder usarla. Es común encontrar muchas variables con valores faltantes. Puede que esas variables incompletas contengan información útil para la predicción cuando tienen un valor, por lo tanto, debemos tratarlas y luego construir un modelo predictivo.
De todas maneras, debemos minimizar el sesgo que estamos introduciendo porque el valor faltante es un valor que “no está ahí”.
Cuando estamos haciendo un informe, la sugerencia es reemplazar NaN con el string empty,
Cuando estamos haciendo un modelo predictivo que se está corriendo en el momento, la recomendación es asignar la categoría que más se repite.
2.5.7 ¿Hay algún patrón en los valores faltantes?
Primero, carguemos datos de ejemplo y hagamos un análisis rápido.
# Crear un dataset similar a HollywoodMovies2011 para# demostrar patrones en valores faltantesnp.random.seed(31415)n_movies =136HollywoodMovies2011 = pd.DataFrame({'Movie': [f'Movie_{i}'for i inrange(n_movies)],'LeadStudio': np.random.choice( ['Warner Bros', 'Fox', 'Paramount', 'Disney','Universal', 'Sony'], n_movies),'Story': np.random.choice( ['Original', 'Sequel', 'Remake', 'Based on Book','Adaptation'], n_movies),'Genre': np.random.choice( ['Action', 'Comedy', 'Drama', 'Horror','Animation', 'Thriller'], n_movies),'TheatersOpenWeek': np.random.normal(2800, 800, n_movies).clip(100, 4100).round(0),'BOAvgOpenWeek': np.random.normal(8000, 3000, n_movies).clip(500, 20000).round(0),'DomesticGross': np.random.normal(80, 60, n_movies).clip(1, 400).round(1),'ForeignGross': np.random.normal(90, 70, n_movies).clip(0, 500).round(1),'WorldGross': np.random.normal(170, 100, n_movies).clip(1, 800).round(1),'Budget': np.random.normal(80, 50, n_movies).clip(5, 300).round(1),'Profitability': np.random.normal(2, 1.5, n_movies).clip(0, 10).round(2),'OpeningWeekend': np.random.normal(25, 15, n_movies).clip(0.1, 100).round(1)})# Introducir NaN con patrónna_idx_1 = np.random.choice(n_movies, 2, replace=False)na_idx_2 = np.random.choice(n_movies, 16, replace=False)for col in ['DomesticGross', 'ForeignGross','WorldGross', 'Profitability']: HollywoodMovies2011.loc[na_idx_1, col] = np.nanfor col in ['BOAvgOpenWeek', 'TheatersOpenWeek','Budget', 'OpeningWeekend']: HollywoodMovies2011.loc[na_idx_2, col] = np.nan# Análisis numéricoprint(status(HollywoodMovies2011)[ ['variable', 'q_nan', 'p_nan']].round(2).to_string( index=False))
Observemos los valores presentes en la columna p_nan. Hay un patrón en los valores faltantes: algunas variables tienen el mismo porcentaje de valores NA. En este caso, no podemos chequear la fuente de los datos; sin embargo, es una buena idea verificar si esos casos tienen un problema en común.
2.5.8 Tratar valores faltantes en variables numéricas
Nuestro primer acercamiento a este punto al principio del capítulo fue convertir todos los valores de NaN a 0.
Una solución es reemplazar los valores vacíos por la media, la mediana u otros criterios. Sin embargo, tenemos que ser conscientes del cambio que esto genera en la distribución.
Si vemos que la variable parece estar correlacionada cuando no está vacía (igual que la categórica), entonces un método alternativo es crear segmentos, convirtiéndola así en categórica.
2.5.8.1 Método 1: Convertir la variable a categórica
La función equal_freq divide la variable en los segmentos deseados. Toma una variable numérica (TheatersOpenWeek) y devuelve una categórica (TheatersOpenWeek_cat), basándose en el criterio de igual frecuencia.
TheatersOpenWeek_cat
frequency
percentage
cumulative_perc
0
(1053.999, 2323.0]
24
0.176500
0.176500
1
(2323.0, 2629.0]
24
0.176500
0.353000
2
(2629.0, 3079.6]
24
0.176500
0.529500
3
(3079.6, 3677.4]
24
0.176500
0.706000
4
(3677.4, 4100.0]
24
0.176500
0.882500
5
NA
16
0.117600
1.000000
Valores faltantes en datos categóricos
Como podemos ver, TheatersOpenWeek_cat contiene cinco segmentos, cada uno representa aproximadamente el ~18-20% de los casos totales. Pero, los valores NaN siguen ahí.
Por último, debemos convertir los NaN en el string empty.
TheatersOpenWeek_cat_fill
frequency
percentage
cumulative_perc
0
(3079.6, 3677.4]
24
0.176500
0.176500
1
(3677.4, 4100.0]
24
0.176500
0.353000
2
(2323.0, 2629.0]
24
0.176500
0.529500
3
(2629.0, 3079.6]
24
0.176500
0.706000
4
(1053.999, 2323.0]
24
0.176500
0.882500
5
empty
16
0.117600
1.000000
Y eso es todo: la variable está lista para usar.
Cortes personalizados:
Si queremos usar tamaños personalizados de segmentos en lugar de los que vienen dados por la igual frecuencia, podemos usar la función pd.cut. En este caso toma la variable numérica TheatersOpenWeek y devuelve TheatersOpenWeek_cat_cust.
# Crear segmentos personalizados, con límites en 1,000,# 2,300, y un máx de 4,100. Los valores por encima de# 4,100 serán asignados a NaN.HollywoodMovies2011['TheatersOpenWeek_cat_cust'] = ( pd.cut(HollywoodMovies2011['TheatersOpenWeek'], bins=[0, 1000, 2300, 4100], include_lowest=True))print(HollywoodMovies2011['TheatersOpenWeek_cat_cust'].value_counts( ).sort_index())
Debemos destacar que la segmentación por igual frecuencia tiende a ser más robusta que la igual distancia que divide la variable, que se basa en tomar el mínimo y el máximo, y la distancia entre cada segmento, sin considerar cuántos casos caen en cada segmento.
La igual frecuencia ubica a los valores atípicos en el primer o último segmento según corresponda. Los valores normales pueden ir desde 3 hasta 20 segmentos. Un alto número de segmentos suele significar más ruido. Para leer más, diríjanse al capítulo de cross_plot.
2.5.8.2 Método 2: Completar el NaN con algún valor
Al igual que con las variables categóricas, podemos reemplazar los valores con un número como el promedio o la mediana.
En este caso, reemplazaremos los valores NaN por el promedio y graficar los resultados del antes y el después lado a lado.
# Completar todos los valores NaN con el promedio de# la variablemean_val = HollywoodMovies2011['TheatersOpenWeek'].mean()HollywoodMovies2011['TheatersOpenWeek_mean'] = ( HollywoodMovies2011['TheatersOpenWeek'].fillna( mean_val))# Graficar la variable original y la transformadafig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))ax1.hist(HollywoodMovies2011['TheatersOpenWeek'].dropna(), bins=15, color='white', edgecolor='black')ax1.set(title='TheatersOpenWeek (original)', ylim=(0, 30))ax2.hist(HollywoodMovies2011['TheatersOpenWeek_mean'], bins=15, color='white', edgecolor='black')ax2.set(title='TheatersOpenWeek_mean (imputado)', ylim=(0, 30))plt.tight_layout()plt.show()
Completando los NA con el valor promedio
Podemos ver un pico alrededor del promedio, que es producto de la transformación. Se introduce un sesgo alrededor de este punto. Si estamos prediciendo algún evento, entonces sería más seguro no tener ningún evento especial cerca de este punto.
Por ejemplo, si estamos prediciendo un evento binario y el evento menos representativo está correlacionado con tener un valor cercano al promedio en TheatersOpenWeek, entonces las probabilidades de tener una Tasa de falsos positivos pueden ser más altas. De nuevo, esto se relaciona con el capítulo de Variables de alta cardinalidad en estadística descriptiva.
Como un comentario extra con respecto a la ultima visualización, fue importante configurar el máximo del eje-y en 30 para que los dos gráficos fueran comparables entre sí.
Como pueden ver, existe una interrelación entre todos los conceptos.
2.5.8.3 Eligiendo el valor adecuado para completar
En el último ejemplo remplazamos los valores NaN con el promedio, ¿pero qué pasa si usamos otros valores? Depende de la distribución de la variable.
La variable que usamos (TheatersOpenWeek) parece tener una distribución normal, que es la razón por la cual utilizamos el promedio. No obstante, si la variable está más sesgada, entonces otra métrica sería más adecuada; por ejemplo, la mediana es menos sensible a los valores atípicos.
2.5.9 Métodos avanzados de imputación
Ahora vamos a hacer un repaso rápido de métodos de imputación más sofisticados en los que creamos un modelo predictivo, con todo lo que eso implica.
2.5.9.1 Método 1: Usando IterativeImputer (equivalente a missForest)
La funcionalidad del IterativeImputer de scikit-learn (similar al paquete missForest en R) se basa en modelos iterativos para completar cada valor faltante, manejando variables numéricas simultáneamente.
from sklearn.experimental import enable_iterative_imputerfrom sklearn.impute import IterativeImputer, SimpleImputer# Copiar los datos (solo columnas numéricas)df_holly = HollywoodMovies2011.select_dtypes( include=[np.number]).copy()# Imputación con IterativeImputer (similar a missForest)iter_imp = IterativeImputer( max_iter=10, random_state=31415)df_imputed_arr = iter_imp.fit_transform(df_holly)df_imputed = pd.DataFrame( df_imputed_arr, columns=df_holly.columns)# Imputación simple con mediana (similar a na.roughfix)simple_imp = SimpleImputer(strategy='median')df_rough_arr = simple_imp.fit_transform(df_holly)df_rough = pd.DataFrame( df_rough_arr, columns=df_holly.columns)
Ahora es momento de comparar las distribuciones de la variable TheatersOpenWeek.
Comparación de métodos de imputación (variable numérica)
Análisis:
La curva naranja muestra la distribución después de la imputación basada en IterativeImputer.
La azul muestra el método de imputación que reemplaza todos los valores NaN por la mediana usando SimpleImputer.
La verde muestra la distribución sin ninguna imputación (por supuesto, los valores NaN no están expuestos).
Reemplazar los valores NaN por la mediana tiende a concentrar, como era de esperarse, todos los valores cerca de un solo punto. Por otro lado, la imputación realizada por IterativeImputer provee una distribución más natural porque no concentra los valores cerca de un solo valor.
¡La curva naranja y la verde son bastante parecidas!
Si deseamos tomar un punto de vista analítico, podemos realizar una prueba estadística para comparar, por ejemplo, los promedios o las varianzas.
2.5.9.2 Método 2: Usar el enfoque MICE
Consejo: Como primer abordaje en la imputación de los valores faltantes, este método es sumamente complejo.
MICE significa “Imputación Multivariada por ecuaciones encadenadas” (“Multivariate Imputation by Chained Equations” en inglés), también se la conoce como “Especificación totalmente condicional” (“Fully Conditional Specification”). Este libro cubre este tema debido a su popularidad.
MICE implica un marco completo para analizar y lidiar con los valores faltantes. Considera las interacciones entre todas las variables al mismo tiempo (multivariado y no una sola) y basa su funcionalidad en un proceso iterativo que utiliza diferentes modelos predictivos para completar cada variable.
Internamente, completa la variable A, basada en B y C. Luego, llena B basada en A y C (A se predice previamente) y la iteración continúa. El nombre “ecuaciones encadenadas” viene del hecho de que podemos especificar el algoritmo por variable para imputar los casos.
En Python, el IterativeImputer de scikit-learn implementa este enfoque MICE. El ejemplo que vimos anteriormente ya utiliza esta técnica internamente.
Habiendo cubierto todas las opciones, podríamos preguntar: ¿cuál es la mejor estrategia? Bueno, depende de cuánto queramos intervenir para manejar los valores faltantes.
Un pequeño repaso de las estrategias posibles:
Excluir las filas y columnas con valores faltantes. Sólo es aplicable si hay pocas filas (o columnas) con valores faltantes, y los datos restantes son suficientes para alcanzar la meta del proyecto. No obstante, cuando excluimos filas con valores faltantes y creamos un modelo predictivo que va a ejecutarse en producción, si llega un caso nuevo que contiene valores faltantes, debemos asignar un valor para procesarlos.
Las estrategias de convertir variables numéricas a categóricas y luego crear el valor “empty” (también aplicable a las variables categóricas), es la opción más rápida -y recomendada- para lidiar con valores nulos. De esta manera incorporamos los valores faltantes al modelo para que pueda manejar la incertidumbre.
Los métodos de imputación como los que cubrimos con IterativeImputer (MICE) son considerablemente más complejos. Con estos métodos, introducimos un sesgo controlado para no tener que excluir filas o columnas.
Es un arte encontrar el equilibrio correcto entre profundizar en estas transformaciones y mantenerlo simple. El tiempo invertido puede no reflejarse en la precisión global.
Independientemente del método, es muy importante analizar el impacto de cada decisión. Hay mucho de prueba y error, así como análisis exploratorio de datos, que conduce al descubrimiento del método más adecuado para sus datos y proyecto.
2.6 Consideraciones que involucran al tiempo
2.6.1 ¿De qué se trata esto?
Todo cambia y nada permanece. - Heráclito, (535 - 475 AC), filósofo griego presocrático.
Lo mismo ocurre con las variables.
Con el paso del tiempo, las variables pueden cambiar sus valores, haciendo que el análisis del tiempo sea crucial a la hora de crear un modelo predictivo. Así evitamos tomar los efectos como causas.
¿Qué vamos a repasar en este capítulo?
Conceptos de filtrado de información antes del evento a predecir.
Cómo analizar y preparar las variables que aumentan -o disminuyen- su valor hasta el infinito (y más allá).
2.6.1.1 No utilicen información del futuro
Imagen de la película: “Volver al futuro” (1985). Robert Zemeckis (Director).
Usar una variable que contiene información después del evento que se está prediciendo es un error común cuando se comienza un nuevo proyecto de modelo predictivo, como jugar a la lotería hoy usando el periódico de mañana.
Imaginemos que necesitamos construir un modelo predictivo para saber qué usuarios es probable que adquieran una suscripción completa en una aplicación web, y este software tiene una funcionalidad ficticia llamada feature_A:
user_id feature_A full_subscription
1 yes yes
2 yes yes
3 yes yes
4 no no
5 yes yes
6 no no
7 no no
8 no no
9 no no
10 no no
Creamos el modelo predictivo, obtuvimos una precisión perfecta, y una inspección arroja el siguiente mensaje: “El 100% de los usuarios que tienen una suscripción completa utiliza la característica Feature A”. Algunos algoritmos predictivos reportan la importancia de las variables; por lo que feature_A estará por encima del resto.
El problema es:feature_A solo está disponible después de que el usuario adquiere la suscripción completa. Por lo tanto, no puede ser utilizada.
El mensaje clave es: No confíen en variables perfectas, ni modelos perfectos.
2.6.1.2 Sean justos con los datos, dejen que desarrollen su comportamiento
Como en la naturaleza, las cosas tienen un tiempo mínimo y máximo para empezar a mostrar cierto comportamiento. Este tiempo oscila de 0 a infinito. En la práctica se recomienda estudiar cuál es el mejor período para analizar, es decir, podemos excluir todo el comportamiento antes y después de este período de observación. Establecer rangos en las variables no es sencillo, ya que puede ser un poco subjetivo.
Imaginen que tenemos una variable numérica que aumenta a medida que pasa el tiempo. Es posible que necesitemos definir una ventana de tiempo de observación para filtrar los datos y alimentar el modelo predictivo.
Configurar el tiempo mínimo: ¿Cuánto tiempo es necesario para empezar a ver el comportamiento?
Configurar el tiempo máximo: ¿Cuánto tiempo es necesario para ver el final del comportamiento?
La solución más fácil es: configurar el mínimo en el principio y el máximo en toda la historia.
Estudio de caso:
Dos personas, Ouro y Borus, son usuarios de una aplicación web que tiene una determinada funcionalidad llamada feature_A, y necesitamos crear un modelo predictivo que pronostique basándose en el uso de feature_A usage -medido en clicks- si una persona va a adquirir full_subscription.
Los datos actuales dicen: Borus tiene full_subscription, mientras que Ouro no.
Tengan cuidado con las consideraciones que involucran el tiempo
El usuario Borus comienza a usar feature_A a partir del día 3, y después de 5 días ella le da más uso -15 clicks vs. 12- a esta función que Ouro, que comenzó a usarla desde el día 0.
Si Borus adquiere full subscription y Ouro no, ¿qué aprenderá el modelo?
Cuando modelamos con la historia completa -days_since_signup = all-, cuanto más alto sea days_since_signup mayor será probabilidad, dado que Borus tiene el número más alto.
Sin embargo, si sólo tomamos la historia de los usuarios de los primeros 5 días desde la suscripción, la conclusión será la opuesta.
¿Por qué conservar los primeros 5 días de la historia?
El comportamiento durante este período inicial (kick-off) puede ser más relevante -con respecto a la precisión de la predicción- que analizar toda la historia. Como dijimos antes, depende de cada caso.
2.6.1.3 Luchar contra el infinito
El número de ejemplos sobre este tema es muy amplio. Mantengamos la esencia de este capítulo en cómo cambian los datos a lo largo del tiempo. A veces es sencillo, como una variable que alcanza su mínimo (o máximo) después de un tiempo fijo. Este caso es fácilmente alcanzable.
Por otro lado, requiere que el ser humano luche contra el infinito.
Consideren el siguiente ejemplo. ¿Cuántas horas se necesitan para alcanzar el valor 0?
¿Qué tal 100 horas?
100 horas
Min value after 100 hours: 0.22
Está cerca de cero, pero ¿qué pasa si esperamos 1000 horas?
1,000 horas
Min value after 1,000 hours: 0.14
¡Hurra! ¡Nos estamos acercando! Pero ¿qué pasa si esperamos 10 veces más? (10,000 horas)
10,000 horas
Min value after 10,000 hours: 0.11
¡Seguimos sin llegar a cero! ¡¿Cuánto tiempo necesitamos?!
Como habrán notado, es probable que lleguemos a cero en el infinito… Estamos ante una Asíntota.
¿Qué debemos hacer? Es momento de pasar a la siguiente sección.
2.6.1.4 Amigarnos con el infinito
En el último ejemplo vimos el análisis de la antigüedad de un consumidor en una compañía. Este valor puede ser infinito.
Por ejemplo, si el objetivo del proyecto es predecir un resultado binario, como buy/don't buy, un análisis útil es calcular la tasa de buy de acuerdo a la antigüedad del usuario. Llegaremos a conclusiones como: En promedio, un consumidor necesita cerca de 6 meses para comprar este producto.
Esta respuesta puede alcanzarse gracias al trabajo conjunto del científico de datos y un experto en la materia.
En este caso, un cero puede considerarse al igual que el valor que tenga el 95% de la población. En términos estadísticos, es el percentil0.95. Este libro abarca extensamente este tema en Anexo 1: La magia de los percentiles. Es un tema clave en el análisis exploratorio de datos.
Un caso relacionado es el de lidiar con valores atípicos, cuando aplicamos percentiles como criterio de corte, como vimos en el capítulo Tratamiento de valores atípicos.
2.6.1.5 Ejemplos en otras áreas
Es muy común encontrar este tipo de variables en muchos conjuntos o proyectos de datos.
En medicina, en los proyectos de análisis de supervivencia, los médicos suelen definir un umbral de, por ejemplo, 3 años para considerar que un paciente sobrevive al tratamiento.
En proyectos de marketing, si un usuario disminuye su actividad dentro de un cierto umbral, digamos:
10-clicks en el sitio web de la compañía en el último mes
No abrir un email después de 1 semana
Él o ella no compra por 30 días
Se lo puede definir como la pérdida de un cliente u oportunidad.
En atención al cliente, un problema puede marcarse como resuelto una vez que la persona pasó 1 semana sin reportar nuevas quejas.
En el análisis de señales cerebrales, si estas señales provienen de la corteza visual en un proyecto en el que, por ejemplo, necesitamos predecir qué tipo de imagen está mirando el paciente, entonces los primeros 40ms de valores no sirven porque es el tiempo que el cerebro necesita para empezar a procesar la señal.
Pero esto también ocurre en “la vida real”, como cuando escribimos un libro de análisis de datos para todas las edades, ¿cuánto tiempo necesitamos para terminarlo? ¿Una cantidad infinita? Probablemente no.
2.6.1.6 Reflexiones finales
Definir un periodo de tiempo para crear un conjunto de entrenamiento y validación, implica un sesgo importante en nuestros datos. Al igual que decidir cómo manejar las variables que cambian con el tiempo. Es por eso que el Análisis exploratorio de datos es importante para entrar en contacto con los datos que estamos analizando.
Los temas están interconectados. Ahora es momento de mencionar la relación de este capítulo con la Validación out-of-time. Cuando predecimos eventos en el futuro, debemos analizar cuánto tiempo es necesario para que la variable objetivo cambie.
El concepto clave aquí es: cómo manejar el tiempo en modelado predictivo. Es una buena oportunidad para preguntar: ¿Cómo sería posible abordar estos problemas del tiempo con sistemas automáticos?
El conocimiento humano es crucial en estos contextos para definir umbrales basándonos en la experiencia, intuición y algunos cálculos.