División de un dataframe en bins definidos por una característica

El objetivo en este escenario es, partiendo de un dataframe, generar bins (bloques) definidos por los valores que tome una de las características y, a continuación, dividir el dataframe según estos bins.

Veamos un ejemplo muy sencillo: Supongamos que estamos trabajando con el dataset Iris y queremos dividirlo en N grupos en función de los valores que tome la característica "sepal_length". Tendríamos que crear N bins basados en los valores de sepal_length y, a continuación, asignar cada registro del dataframe a uno de los bins.

Para llevar esto a la práctica vamos a trabajar con apenas 5 registros del mencionado dataset Iris, y vamos a crear solo 2 bins: el primero cubrirá desde el valor más bajo de sepal_length hasta un valor intermedio de dicha columna, y el segundo desde el valor intermedio hasta el valor más elevado.

Si los bins se generan con el mismo tamaño, ese valor intermedio sería -en nuestro caso- el valor medio. Es decir, si partimos de los valores 1, 1, 1 y 2, los dos bins serían [1, 1.5] y [1.5, 2] (olvidémonos por un momento de si el valor 1.5 pertenecería al primer o al segundo bin).

Empecemos, en primer lugar, cargando el dataset iris y quedándonos solo con los primeros 5 registros:

import numpy as np
import pandas as pd
import seaborn as sns

iris = sns.load_dataset("iris")
iris = iris.head()
iris.head()

5 primeros registros de Iris

Mostramos -para poder hacer una comprobación visual- los valores mínimo y máximo de la columna sepal_length:

print("min: ", min(iris.sepal_length))
print("max: ", max(iris.sepal_length))

Valores mínimo y máximo de sepal_length

Ahora vamos a crear los bins. Para ello utilizaremos la función numpy.linspace(), función que nos permite crear un conjunto de números equiespaciados entre dos dados. En nuestro ejemplo, los valores extremos vendrían definidos por el valor mínimo y máximo que acabamos de calcular:

bins = np.linspace(iris.sepal_length.min(), iris.sepal_length.max(), 3)
bins

Bins creados con np.linspace()

Es decir, el primer bin vendría definido por el intervalo [4.6, 4.85] y el segundo por el intervalo [4.85, 5.1]. La cuestión de si el valor intermedio pertenece a uno u otro bin dependerá de cómo asignemos los registros a cada uno de los bins. Una buena opción es utilizar la función numpy.digitize(), que devuelve los índices de los bins a los que pertenece cada uno de los registros: Es decir, a esta función habría que pasar los valores a repartir en bins (la columna sepal_length en nuestro caso) y los bins (que hemos almacenado en la variable homónima), y devolvería una lista de índices, uno para cada registro, indicando a cuál de los bins pertenece cada uno. Hagamos una primera prueba:

i = np.digitize(iris.sepal_length, bins)
i

Índices generados

Vemos, sin embargo, que se han asignado los registros no a 2 bins, sino a 3. La respuesta a esto la encontramos en la documentación de numpy.digitize()

Documentación de numpy.digitize()

El menor valor (o el mayor, pues esto es configurable mediante el parámetro right) siempre va a tener un bin propio (pues los bins son cerrados o por la derecha o por la izquierda), que no es lo que queremos: queremos generar solo 2 bins, y que todos los registros se incluyan en estos bins.

La solución es evitar que los valores mínimo y máximo a partir de los que se generan los bins coincidan con los valores mínimo y máximo de los datos a repartir en bins. Por ejemplo:

alpha = 0.0001
bins = np.linspace(iris.sepal_length.min() - alpha, iris.sepal_length.max() + alpha, 3)
bins

Generación de los bins

Ahora ninguno de los valores a repartir en los bins va a coincidir con los valores usados para generar los bins, por lo que todos deberían caer dentro de los bins. Comprobémoslo:

i = np.digitize(iris.sepal_length, bins)
i

Índices a los bins

Efectivamente, todos los índices apuntan a los dos bins que queríamos crear. Podemos comprobar visualmente que el primer valor de la columna sepal_length (5.1) que antes se incluía en su propio bin por ser el valor máximo ahora pertenece al bin 2 (el bin formado por los valores mayores o iguales a 4.85 y menores que 5.1001).

Y ahora que hemos generado el índice del bin al que pertenece cada registro, podemos dividir el dataframe usando la función pandas.groupby() para, por ejemplo, calcular el número de registros en cada bin:

iris.groupby(by = i).species.count()

Número de registros en cada bin

O calcular el valor medio del ancho del sépalo en cada bin:

iris.groupby(by = i).sepal_width.mean()

Valor medio del ancho del sépalo en cada bin

Obsérvese que, en este caso, no estamos pasando al parámetro by el nombre de un campo (que es, probablemente, lo más frecuente) sino una lista de valores que pandas asocia a cada registro para determinar los grupos.

Categoría
Submitted by admin on Tue, 08/13/2019 - 09:54