Más sobre ciencia de datos: cienciadedatos.net
Pandas permite almacenar variables categóricas en un formato más eficiente que el formato de texto. Para ello dispone del tipo de datos category
. Internamente pandas almacena cada categoría como un entero y mantiene un mapa que relaciona cada entero con la categoría correspondiente. Por ejemplo, para una columna con los valores ['a', 'b', 'c', 'a']
, pandas almacena internamente [0, 1, 2, 0]
y mantiene un mapa que relaciona 0
con a
, 1
con b
y 2
con c
.
Este tipo de almacenamiento es más eficiente en términos de memoria y también puede mejorar el rendimiento en algunas operaciones. Sin embargo, puede causar problemas cuando se utilizan como equivalentes variables que, a pesar de tener los mismos valores, el mapa interno de las categorías es diferente.
En este documento se muestra cómo evitar este problema y cómo impacta en modelos de machine learning que gestionan automaticamente las variables de tipo category
.
⚠ Warning
Las librerías LightGBM, XGBoost y HistGradientBoosting de scikit-learn son capaces de seleccionar automáticamente las variables de tipo `category`. Sin embargo, solo LightGBM y HistGradientBoosting de scikit-learn son capaces de identificar cambios en la codificación entre entrenamiento y nuevos datos, y mantener la coherencia de las categorías. XGBoost no gestiona automáticamente las categorías de forma correcta. Si se utilizan variables de tipocategory
en un modelo de XGBoost, es necesario asegurarse de que el mapa de categorías es el mismo en el conjunto de entrenamiento y en los nuevos datos.
# Librarías
# ==============================================================================
import pandas as pd
import numpy as np
from lightgbm import LGBMRegressor
from sklearn.ensemble import HistGradientBoostingRegressor
from xgboost import XGBRegressor
import sklearn
import lightgbm
import xgboost
print(f"Versión de pandas: {pd.__version__}")
print(f"Versión de scikit-learn: {sklearn.__version__}")
print(f"Versión de lightgbm: {lightgbm.__version__}")
print(f"Versión de xgboost: {xgboost.__version__}")
Supongase que se tiene una serie con los valores ['a', 'b', 'c', 'a']
y se desea almacenarla como una serie de tipo category
. Por defecto, pandas ordena las categorías alfabéticamente, y les asigna un entero en el rango [0, n-1]
donde n
es el número de categorías. En este caso, el mapa de categorías sería {'a': 0, 'b': 1, 'c': 2}
.
variable_1 = pd.Series(["a", "b", "c", "a"], dtype="category")
mapa_variable_1 = dict(enumerate(variable_1.cat.categories))
mapa_variable_1
Se crea ahora otra variable que tiene los mismos valores, pero en un orden diferente: ['b', 'c', 'a', 'b']
. Dado que las categorías se ordenan alfabéticamente, el mapa de categorías sería el mismo que en el caso anterior.
variable_2 = pd.Series(['b', 'c', 'a', 'b'], dtype="category")
mapa_variable_2 = dict(enumerate(variable_2.cat.categories))
mapa_variable_2
Sin embargo, si se crea una variable con un subconjunto de las categorías anteriores, el mapa de categorías será diferente ya que al ordenarlas, las posiciones de las categorías cambian. Por ejemplo, si se crea una variable con los valores ['b', 'c', 'b']
, el mapa de categorías sería {'b': 0, 'c': 1}
.
variable_3 = pd.Series(['b', 'c', 'b'], dtype="category")
mapa_variable_3 = dict(enumerate(variable_3.cat.categories))
mapa_variable_3
Para asegurar que el mapa de categorías es el mismo para todas las variables que representan la misma información, se puede especificar el orden de las categorías al crear la serie de tipo category
.
categorias = ['a', 'b', 'c']
variable_1 = pd.Series(pd.Categorical(
["a", "b", "c", "a"],
categories=categorias,
ordered=False
))
mapa_variable_1 = dict(enumerate(variable_1.cat.categories))
variable_2 = pd.Series(pd.Categorical(
['b', 'c', 'b'],
categories=categorias,
ordered=False
))
mapa_variable_2 = dict(enumerate(variable_2.cat.categories))
print(mapa_variable_1)
print(mapa_variable_2)
Tambien se puede recodificar una serie de tipo category
para que tenga el mismo mapa de categorías que otra serie.
variable_1 = pd.Series(["a", "b", "c", "a"], dtype="category")
variable_2 = pd.Series(['b', 'c', 'b'], dtype="category")
variable_2 = pd.Series(pd.Categorical(
variable_2,
categories=variable_1.cat.categories,
ordered=False
))
mapa_variable_1 = dict(enumerate(variable_1.cat.categories))
mapa_variable_2 = dict(enumerate(variable_2.cat.categories))
print(mapa_variable_1)
print(mapa_variable_2)
Los códigos internos utilizados por pandas para las categorías tienen especial relevancia en modelos de machine learning que gestionan automáticamente las variables de tipo category
, por ejemplo, LightGBM, XGBoost y HistGradientBoosting. Esto es así porque estos modelos utilizan los códigos internos de las categorías, no las categorías en sí. Por lo tanto, es muy importante que al realizar las predicciones, las categorías tengan el mismo mapa de códigos internos que en el momento de entrenar el modelo.
En los siguinetes ejemplos se comprueba si las implementaciones de LightGBM, XGBoost y HistGradientBoosting gestionan automáticamente las categorías de forma correcta.
# Datos
# ==============================================================================
datos = pd.read_csv(
"https://raw.githubusercontent.com/JoaquinAmatRodrigo/"
"Estadistica-machine-learning-python/master/data/california_housing_extended.csv"
)
datos = datos.drop(
columns=['Latitude', 'Longitude', 'postcode', 'ciudad_mas_cercana', 'distancia_ciudad_mas_cercana']
)
print(f"Dimensiones del dataset: {datos.shape}")
datos.head()
# Variables categóricas
# ==============================================================================
datos['county'] = datos['county'].astype('category')
datos.dtypes
# Mapa de categorías
# ==============================================================================
mapa_county = dict(enumerate(datos['county'].cat.categories))
mapa_county
# Modelo LightGBM
# ==============================================================================
modelo = LGBMRegressor(verbose=-1, random_state=6987)
modelo.fit(
X = datos.drop(columns='MedHouseVal'),
y = datos['MedHouseVal'],
categorical_feature='auto'
)
modelo
# Variables categóricas que se han utilizado en el entrenamiento del modelo
# ==============================================================================
cat_index = modelo.booster_.params.get('categorical_column')
if cat_index is not None:
features_in_model = modelo.booster_.feature_name()
cat_features_in_model = [features_in_model[i] for i in cat_index]
cat_features_in_model
Se crean dos observaciones de test, ambas iguales pero con una codificación interna distinta de la variable categorica county
.
test_1 = datos.iloc[0:1, :].drop(columns='MedHouseVal').copy()
display(test_1)
print(f"Código: {test_1['county'][0]} -- {test_1['county'].cat.codes[0]}")
test_2 = pd.DataFrame([{
'MedInc': 8.3252,
'HouseAge': 41.0,
'AveRooms': 6.984126984126984,
'AveBedrms': 1.0238095238095235,
'Population': 322.0,
'AveOccup': 2.555555555555556,
'county': 'Contra Costa County',
}])
test_2['county'] = test_2['county'].astype('category')
display(test_2)
print(f"Código: {test_2['county'][0]} -- {test_2['county'].cat.codes[0]}")
En test_1
la variable county
sigue el mismo mapa de categorías que en el conjunto de entrenamiento, la categoría Contra Costa County se codifica con el valor 6. En test_2
la variable county
sigue un mapa de categorías distinto, la categoría Contra Costa County se codifica con el valor 0.
# Predicciones
# ==============================================================================
prediccion_1 = modelo.predict(test_1)
prediccion_2 = modelo.predict(test_2)
print(f"Predicción 1: {prediccion_1}")
print(f"Predicción 2: {prediccion_2}")
LightGBM identifica cambios en el mapa de categorías y, si es distinto en el conjunto de test que en el conjunto de entrenamiento, utiliza el de test. Para más información ver github issue.
# Modelo HistGradientBoostingRegressor
# ==============================================================================
modelo = HistGradientBoostingRegressor(
categorical_features = 'from_dtype',
random_state=6987
)
modelo.fit(
X = datos.drop(columns='MedHouseVal'),
y = datos['MedHouseVal'],
)
modelo
# Variables categóricas que se han utilizado en el entrenamiento del modelo
# ==============================================================================
modelo.feature_names_in_[modelo.is_categorical_]
# Predicciones
# ==============================================================================
prediccion_1 = modelo.predict(test_1)
prediccion_2 = modelo.predict(test_2)
print(f"Predicción 1: {prediccion_1}")
print(f"Predicción 2: {prediccion_2}")
En vista de los resultados, HistGradientBoosting también gestiona automáticamente las categorías de forma correcta.
# Modelo XGBoost
# ==============================================================================
modelo = XGBRegressor(
enable_categorical=True,
random_state=6987
)
modelo.fit(
X = datos.drop(columns='MedHouseVal'),
y = datos['MedHouseVal'],
)
modelo
# Variables categóricas que se han utilizado en el entrenamiento del modelo
# ==============================================================================
feature_types = np.array(modelo.get_booster().feature_types)
features_in_model = np.array( modelo.get_booster().feature_names)
features_in_model[feature_types == 'c']
# Datos de test
# ==============================================================================
display(test_1)
print(f"Código: {test_1['county'][0]} -- {test_1['county'].cat.codes[0]}")
display(test_2)
print(f"Código: {test_2['county'][0]} -- {test_2['county'].cat.codes[0]}")
# Predicciones
# ==============================================================================
prediccion_1 = modelo.predict(test_1)
prediccion_2 = modelo.predict(test_2)
print(f"Predicción 1: {prediccion_1}")
print(f"Predicción 2: {prediccion_2}")
Las predicciones son distintas. Esto se debe a que, a pesar de que el valor de county
es el mismo en ambos casos, el código interno de la categoría es distinto. XGBoost no gestiona automáticamente las categorías de forma correcta.
Vease lo que ocurre si se recodifica la variable county
de test_2
para que tenga el mismo mapa de categorías que test_1
.
# Recodificación de variables categóricas
# ==============================================================================
test_2['county'] = pd.Categorical(
test_2['county'],
categories=test_1['county'].cat.categories,
ordered=False
)
display(test_2)
print(f"Código: {test_2['county'][0]} -- {test_2['county'].cat.codes[0]}")
# Predicciones
# ==============================================================================
prediccion_1 = modelo.predict(test_1)
prediccion_2 = modelo.predict(test_2)
print(f"Predicción 1: {prediccion_1}")
print(f"Predicción 2: {prediccion_2}")
Las predicciones son ahora iguales.
⚠ Warning
XGBoost no gestiona automáticamente las categorías de forma correcta. Si se utilizan variables de tipo `category` en un modelo de XGBoost, es necesario asegurarse de que el mapa de categorías es el mismo en el conjunto de entrenamiento y en los nuevos datos.import session_info
session_info.show(html=False)
%%html
¿Cómo citar este documento?
Si utilizas este documento o alguna parte de él, te agradecemos que lo cites. ¡Muchas gracias!
Uso de pandas category para codificar variables categóricas en modelos de machine learning por Joaquín Amat Rodrigo, disponible bajo una licencia Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0 DEED) en https://www.cienciadedatos.net/documentos/py55-pandas-category-modelos-machine-learning.html
¿Te ha gustado el artículo? Tu ayuda es importante
Mantener un sitio web tiene unos costes elevados, tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊
Este documento creado por Joaquín Amat Rodrigo tiene licencia Attribution-NonCommercial-ShareAlike 4.0 International.
Se permite:
Compartir: copiar y redistribuir el material en cualquier medio o formato.
Adaptar: remezclar, transformar y crear a partir del material.
Bajo los siguientes términos:
Atribución: Debes otorgar el crédito adecuado, proporcionar un enlace a la licencia e indicar si se realizaron cambios. Puedes hacerlo de cualquier manera razonable, pero no de una forma que sugiera que el licenciante te respalda o respalda tu uso.
NoComercial: No puedes utilizar el material para fines comerciales.
CompartirIgual: Si remezclas, transformas o creas a partir del material, debes distribuir tus contribuciones bajo la misma licencia que el original.