Curso de ML EN

Capítulo 2 de 37 · básico

Convertir datos en features

Qué cubre este capítulo

Un modelo nunca ve tus datos. Ve una matriz de números, y alguien tuvo que convertir la columna de color, la columna de talla, la bandera de sí/no en esa matriz antes de que el algoritmo siquiera corriera. Esa traducción no es un trámite. Es una decisión de modelado, y es uno de los pocos lugares donde una sola línea descuidada cambia en silencio lo que el modelo tiene permitido aprender — sin error, sin warning, solo una respuesta peor que después no vas a poder explicar.

Este capítulo trata de esa traducción. Empezamos con los cuatro tipos de columna que te encuentras en la práctica — un número continuo, un conteo discreto, una categoría sin orden, una categoría con un orden real, un sí/no — y construimos a mano, en NumPy puro, los encoders que convierten cada una en números: one-hot para categorías nominales, enteros ordinales para las que sí tienen un orden genuino, y una sola columna 0/1 para la bandera binaria. Luego OneHotEncoder y OrdinalEncoder de scikit-learn hacen el mismo trabajo, y confirmamos que llegan a una matriz idéntica. El punto no es ningún encoder en particular. Es el error que se esconde entre dos de ellos: darle a una categoría nominal un orden numérico falso — rojo es 0, verde es 1, azul es 2 — y ver a un modelo lineal creerse todas las mentiras que ese orden cuenta.

Los datos son una pequeña tabla sintética de productos, con semilla fija para que las codificaciones sean legibles y el efecto sea imposible de pasar por alto. Y la recompensa es una animación: la misma columna de color, ajustada de dos maneras, donde el accuracy de un modelo lineal salta de un volado a algo genuinamente útil en el momento en que dejamos de fingir que los colores tienen un orden.

Un poco de historia

La idea de que una categoría siquiera pertenece a una regresión viene del análisis de varianza. R. A. Fisher, trabajando en Rothamsted en los años veinte, necesitaba una forma de preguntar si el fertilizante o la variedad de trigo cambiaban el rendimiento — y esas son etiquetas, no números. Su respuesta, expuesta en "Statistical Methods for Research Workers" en 1925, fue comparar la varianza entre grupos con la varianza dentro de ellos. Lo que hizo la idea portable al resto de la estadística fue darse cuenta de que ANOVA es solo regresión sobre columnas indicadoras: reemplazas una categoría de k niveles con un conjunto de columnas 0/1, una por nivel, y los coeficientes de la regresión se vuelven los efectos de grupo. Daniel Suits escribió la versión para practicantes en 1957, en un artículo titulado sin rodeos "Use of Dummy Variables in Regression Equations", y "variable dummy" sigue siendo el nombre que un econometrista le da a lo que un ingeniero de machine learning llama one-hot.

El otro nombre viene del hardware. En lógica digital es común guardar el estado de una máquina en un registro donde exactamente un bit está en alto y todos los demás en bajo — un registro one-hot, llamado así porque un solo cable está "caliente" a la vez. Desperdicia bits y es maravilloso para circuitos: decodificar el estado es trivial porque cada estado tiene su propia línea dedicada, sin aritmética que relacione uno con el siguiente. Esa es exactamente la propiedad que queremos para un feature nominal. Una columna por categoría, exactamente una encendida, y nada en la codificación dice que alguna categoría sea más grande, más cercana o más que cualquier otra. Dos tradiciones, un estadístico contando parcelas de cosecha y un ingeniero cableando una máquina de estados, llegaron al mismo truco por la misma razón: algunas cosas son nombres, y los nombres no tienen aritmética.

La intuición

Aquí está el problema completo en una oración. Una categoría es un nombre; un número tiene un orden y un espaciado; y en el momento en que escribes un nombre como número acabas de inventar un orden y un espaciado que el nombre nunca tuvo.

Digamos que el color de un producto es rojo, verde o azul. Ordénalos alfabéticamente en inglés y reparte enteros — blue es 0, green es 1, red es 2 — y le dijiste a cualquier modelo que los lee como números tres cosas que nunca quisiste decir. Que el verde está entre el azul y el rojo. Que el rojo está el doble de lejos del azul que el verde. Que el paso de azul a verde mide lo mismo que el paso de verde a rojo. Nada de eso es cierto de los colores. Lo inventaste al alfabetizar. Un modelo lineal, que no hace más que sumar valores de features con pesos, te toma completamente la palabra: ajusta un solo peso sobre ese entero, y un solo peso solo puede ordenar las categorías en línea recta. Si la verdad es que rojo y azul se comportan igual y el verde es el raro, ninguna línea recta que pase por 0, 1, 2 puede decirlo.

La codificación one-hot se niega a inventar el orden. En lugar de una columna con un número falso adentro, haces una columna por categoría, cada una con una bandera 0/1 de "¿es esta?". Ahora rojo, verde y azul viven en tres ejes separados, perpendiculares y equidistantes, y el modelo ajusta un peso independiente para cada uno. No hay "entre", no hay "el doble de lejos", no hay nada que creerse equivocadamente. El costo es el ancho — cien ciudades se vuelven cien columnas — pero para una categoría sin orden real es la representación honesta, y aquí la honestidad sale barata.

La matemática

Una variable nominal toma uno de KK valores c1,,cKc_1, \dots, c_K, y la codificación one-hot mapea un valor xx a un vector de indicadores de longitud KK:

onehot(x)=(1[x=c1], 1[x=c2], , 1[x=cK])\operatorname{onehot}(x) = \big(\mathbb{1}[x = c_1],\ \mathbb{1}[x = c_2],\ \dots,\ \mathbb{1}[x = c_K]\big)

Aquí 1[]\mathbb{1}[\cdot] vale 1 cuando la condición se cumple y 0 en caso contrario, así que el vector es puros ceros excepto por un único 1 en la posición de la categoría de xx. Cada categoría se vuelve un eje propio; ninguna par está más cerca que cualquier otra.

Contrasta eso con la codificación por etiqueta (u ordinal), que le asigna a cada categoría un entero code(x){0,1,,K1}\operatorname{code}(x) \in \{0, 1, \dots, K-1\} y se queda con una sola columna. La contribución de esa columna en un modelo lineal es un peso por ese entero:

η=wcode(x)+b\eta = w \cdot \operatorname{code}(x) + b

Como η\eta es lineal en el código, el efecto predicho tiene que avanzar de forma monótona conforme el código crece — el modelo literalmente no puede hacer que la categoría de en medio sea la excepción. Peor aún, cualquier método que mida distancias ahora lee la brecha entre dos categorías como

code(ci)code(cj)\big\lvert \operatorname{code}(c_i) - \operatorname{code}(c_j) \big\rvert

así que las categorías codificadas como 0 y 2 se tratan como si estuvieran el doble de lejos que 0 y 1. Esa métrica impuesta está bien cuando los códigos reflejan un orden real con espaciado uniforme — una columna genuinamente ordinal como chico, mediano, grande — y es una fabricación cuando no. La matemática es la misma en ambos casos; solo tu conocimiento de la columna decide si es una descripción o una mentira.

En qué es bueno, en qué no

La virtud del one-hot es que no asume nada. Nunca cuela un orden en el modelo, funciona para cualquier columna nominal, y los pesos ajustados se leen directamente como efectos por categoría. Su vicio es el ancho. Una columna con mil valores distintos — IDs de usuario, códigos postales, SKUs de producto — se vuelve mil columnas casi vacías, lo que infla la memoria, hace más lento el ajuste y le entrega a un modelo lineal regularizado mil perillas diminutas para hacer overfitting. Ese es el momento de dejar el one-hot atrás y usar un target encoder (reemplaza cada categoría con un promedio suavizado del target) o un hashing encoder (mapea las categorías a un número fijo de columnas y tolera las colisiones). La alta cardinalidad es el límite real del one-hot, y llega más rápido de lo que la gente espera.

Los enteros ordinales son el trato opuesto. Son compactos — una columna, siempre — y llevan exactamente la información que quieres cuando la categoría tiene un orden genuino con espaciado con significado. Chico, mediano, grande; frío, tibio, caliente; las estrellas de una reseña. Usa enteros ahí y le ayudaste al modelo, porque el orden es real y un término lineal puede aprovecharlo. La falla es usar esa misma codificación compacta en una columna nominal porque era una línea menos de código, momento en el cual fabricaste un orden y el modelo se lo cree. Toda la habilidad está en distinguir los dos casos, y no es el código el que te lo dice — es si la columna tiene un orden en el mundo real.

Los datos

Construí una tabla sintética pequeña a propósito, porque un dataset real esconde el mecanismo bajo el ruido y yo quiero el efecto al desnudo. Trescientas filas de un listado de productos inventado, una columna de cada tipo que le importa a este capítulo:

  • price — un número continuo, dólares, muestreado de una normal alrededor de 50.
  • reviews — un conteo discreto, una Poisson alrededor de 20.
  • color — una categoría nominal, red / green / blue, sin orden alguno.
  • size — una categoría ordinal, S / M / L, un orden real.
  • on_sale — una bandera binaria, sí / no.
  • converted — el target, 0 o 1, si el visitante compró.

El generador está armado para que la señal en color sea no monótona en cualquier código entero: rojo y azul convierten bien, verde no. Sobre la tabla completa la tasa de conversión es 0.71 para azul, 0.07 para verde y 0.75 para rojo — los dos extremos altos, el centro bajo. Ese es el patrón con forma de XOR que ningún peso único sobre un eje entero puede ordenar correctamente, que es la razón entera por la que la elección de codificación va a importar aquí. El efecto de size, en cambio, es monótono — lo más grande vende más — así que los enteros ordinales en size son honestos. Una columna es una trampa, la otra está bien, y la diferencia es conocimiento sobre la columna, no el código.

Aquí está el mecanismo en sí. A la izquierda, ocho filas de la columna color cruda — un valor categórico cada una. A la derecha, las mismas ocho filas después del one-hot: tres columnas indicadoras, exactamente una encendida por fila.

Lee una fila de corrido. Un producto rojo se vuelve 0, 0, 1; uno verde se vuelve 0, 1, 0. La celda categórica única se desplegó en una fila de una matriz donde exactamente una entrada está encendida. Nada tiene orden, nada está más cerca de nada. Esta es la imagen que describía la sección de historia — la variable dummy y el registro one-hot, el mismo truco dibujado como cuadrícula.

Constrúyelo, una función a la vez

Cada encoder es pequeño, y esa pequeñez es el punto — no hay dónde esconder un bug sutil. One-hot primero. Hacer fit significa aprender el vocabulario de categorías; hacer transform significa encender una columna por fila:

def one_hot_fit(col):
    """Learn the category vocabulary of a nominal column.

    col is a length-N array of category labels. We record the sorted set of
    distinct values; that fixed, ordered list is what pins each category to a
    specific output column so train and test line up. Sorting (not first-seen
    order) is what makes this match scikit-learn's OneHotEncoder exactly.
    """
    return sorted(set(col))


def one_hot_transform(col, categories):
    """Expand a nominal column into one 0/1 indicator column per category.

    Returns an (N, K) matrix where column k is 1 exactly on the rows whose value
    is categories[k], and 0 everywhere else. No category is closer to any other:
    every level sits on its own orthogonal axis, which is the whole point.
    """
    col = np.asarray(col)
    out = np.zeros((len(col), len(categories)), dtype=float)
    for k, cat in enumerate(categories):
        out[:, k] = (col == cat).astype(float)
    return out

Ordeno las categorías en lugar de tomarlas en el orden en que aparecen por una razón aburrida pero importante: es lo que hace que train y test coincidan en cuál columna es cuál, y es lo que hace que esto coincida después con el encoder de scikit-learn, para poder demostrar que los dos son idénticos. La codificación ordinal es el uso honesto de los enteros — tú le entregas el orden real y ella lo respeta:

def ordinal_encode(col, order):
    """Map a genuinely ordered column to integers that respect that order.

    `order` is the domain-knowledge ranking you supply, e.g. ["S", "M", "L"].
    Each value becomes its position in that list, so S->0, M->1, L->2. This is
    only honest when the column really is ordered: the integers assert that L is
    above M is above S, and that the step S->M is the same size as M->L.
    """
    rank = {cat: i for i, cat in enumerate(order)}
    return np.array([rank[v] for v in col], dtype=float)

Fíjate que ordinal_encode exige un argumento order. No puedes llamarla sin declarar, en voz alta, cuál es el ranking — S antes que M antes que L. Esa fricción es un feature. Te fuerza a hacer el único juicio que importa. Ahora la trampa, la función que se ve casi igual y significa algo completamente distinto:

def label_encode(col):
    """Assign each category an integer by sorted order — no meaning attached.

    This is the mechanically identical twin of ordinal encoding, but with the
    order chosen alphabetically instead of by any real ranking. On a truly
    ordered column it happens to be fine; on a nominal column (color, city) it
    is the trap the chapter is about, because it invents an order the data never
    had: blue < green < red, with red twice as far from blue as green is.
    """
    categories = sorted(set(col))
    rank = {cat: i for i, cat in enumerate(categories)}
    return np.array([rank[v] for v in col], dtype=float), categories

label_encode es ordinal_encode con el orden elegido por ti, alfabéticamente, por nadie que conozca los datos. En una columna genuinamente ordenada eso es suerte inofensiva. En una columna nominal es el bug completo: inventa blue < green < red y le entrega esa ficción al modelo como si fuera algo medido. Los mismos enteros, la misma aritmética — la única diferencia es si el orden que afirma existe en el mundo. Por último la bandera binaria, que necesita exactamente una columna, no dos:

def binary_encode(col, positive):
    """Collapse a two-valued column to a single 0/1 column.

    `positive` is the label that maps to 1 (everything else maps to 0). A binary
    variable needs exactly one column, not two — one indicator already carries
    all the information, and a second would be a perfect mirror of the first.
    """
    col = np.asarray(col)
    return (col == positive).astype(float)

Un sí/no ya es un bit de información; una sola columna 0/1 lo carga todo, y una segunda columna sería solo su imagen espejo, redundante y, para un modelo lineal, colineal. Una columna, listo.

Míralo trabajar

Esta es la recompensa, y es la demostración más limpia del libro de una mala codificación haciendo daño medible. Tomamos la columna color sola, ajustamos un modelo lineal para predecir la conversión a partir de ella, y mostramos el ajuste de dos maneras. Los tres anillos huecos son la verdad — la tasa de conversión real de cada color, fija, sin moverse nunca: azul alto, verde pegado al piso, rojo alto. Los diamantes son las predicciones del modelo, y la línea gris que los atraviesa es la respuesta del modelo a lo largo del eje de color.

El primer frame es la codificación por etiqueta. El color quedó aplanado sobre un solo eje entero — azul en 0, verde en 1, rojo en 2 — así que la respuesta del modelo lineal tiene que ser una curva más o menos recta, y una curva recta que pase por esos tres puntos no puede hundirse en el medio. Mira lo que le hace al verde: la tasa de conversión real del verde es 0.07, en el piso, pero el modelo, obligado a mantener su respuesta monótona, arrastra la predicción del verde hasta 0.52, a media gráfica, completamente equivocada. No es una falla de tuning. La codificación hizo inalcanzable la respuesta correcta.

Dale play y el orden falso se relaja, frame a frame, hasta llegar al one-hot. Mientras eso pasa, el diamante del verde se sale de la línea y cae hacia su tasa real de 0.07, aterrizando en 0.10 una vez que cada color tiene su propio eje. Azul y rojo también se asientan sobre sus tasas reales. La línea gris, que empezó como un arrastre monótono casi plano, termina doblada para coincidir con la realidad. Y el número en la leyenda — accuracy, la fracción de las 300 filas que este modelo de solo-color acierta — sube de 0.52, apenas mejor que adivinar, a 0.79. La misma columna, el mismo modelo, los mismos datos. Lo único que cambió es que dejamos de decirle al modelo que el verde estaba entre el azul y el rojo.

Reinicia y avánzalo frame por frame. La predicción no se teletransporta; se desliza, porque estamos mezclando dos modelos realmente ajustados — el de codificación por etiqueta y el one-hot — y viendo cómo el compromiso forzado del modelo lineal se disuelve conforme la restricción se levanta. El momento en que el verde cruza por debajo de 0.5 es el momento en que el accuracy salta, porque es cuando el modelo por fin empieza a decir que los productos verdes "no van a convertir", que es lo que son.

La implementación completa

El archivo de encoders entero, sin librería — las cuatro transformaciones exactamente como las usaron las verificaciones y la animación:

"""Turning columns into features, built from scratch.

A model only ever sees a matrix of numbers. Everything upstream of that matrix
is a decision about how a raw column — a color, a size, a yes/no flag — becomes
those numbers, and the decision is not free: it tells the model what kind of
thing the column is. This file builds the four encoders you reach for by hand,
in pure NumPy (+ pandas for loading), so the mechanics are in front of you:

  one_hot   nominal categories  -> one indicator column each
  ordinal   truly ordered levels -> integers that respect the order
  label     categories           -> sorted integers (the trap on nominal data)
  binary    a two-valued column  -> a single 0/1 column

Every function below appears in the chapter one step at a time (the
`# region:` markers are what the book's include directives pull in).
"""

import numpy as np
import pandas as pd


# region: onehot
def one_hot_fit(col):
    """Learn the category vocabulary of a nominal column.

    col is a length-N array of category labels. We record the sorted set of
    distinct values; that fixed, ordered list is what pins each category to a
    specific output column so train and test line up. Sorting (not first-seen
    order) is what makes this match scikit-learn's OneHotEncoder exactly.
    """
    return sorted(set(col))


def one_hot_transform(col, categories):
    """Expand a nominal column into one 0/1 indicator column per category.

    Returns an (N, K) matrix where column k is 1 exactly on the rows whose value
    is categories[k], and 0 everywhere else. No category is closer to any other:
    every level sits on its own orthogonal axis, which is the whole point.
    """
    col = np.asarray(col)
    out = np.zeros((len(col), len(categories)), dtype=float)
    for k, cat in enumerate(categories):
        out[:, k] = (col == cat).astype(float)
    return out
# endregion


# region: ordinal
def ordinal_encode(col, order):
    """Map a genuinely ordered column to integers that respect that order.

    `order` is the domain-knowledge ranking you supply, e.g. ["S", "M", "L"].
    Each value becomes its position in that list, so S->0, M->1, L->2. This is
    only honest when the column really is ordered: the integers assert that L is
    above M is above S, and that the step S->M is the same size as M->L.
    """
    rank = {cat: i for i, cat in enumerate(order)}
    return np.array([rank[v] for v in col], dtype=float)
# endregion


# region: label
def label_encode(col):
    """Assign each category an integer by sorted order — no meaning attached.

    This is the mechanically identical twin of ordinal encoding, but with the
    order chosen alphabetically instead of by any real ranking. On a truly
    ordered column it happens to be fine; on a nominal column (color, city) it
    is the trap the chapter is about, because it invents an order the data never
    had: blue < green < red, with red twice as far from blue as green is.
    """
    categories = sorted(set(col))
    rank = {cat: i for i, cat in enumerate(categories)}
    return np.array([rank[v] for v in col], dtype=float), categories
# endregion


# region: binary
def binary_encode(col, positive):
    """Collapse a two-valued column to a single 0/1 column.

    `positive` is the label that maps to 1 (everything else maps to 0). A binary
    variable needs exactly one column, not two — one indicator already carries
    all the information, and a second would be a perfect mirror of the first.
    """
    col = np.asarray(col)
    return (col == positive).astype(float)
# endregion


def load_data(path="../data/products.csv"):
    """The synthetic product table: one column of every type we care about.

    Returns the DataFrame as-is. Columns:
      price     continuous numeric   (float)
      reviews   discrete numeric     (int count)
      color     nominal category     (red / green / blue)
      size      ordinal category     (S < M < L)
      on_sale   binary               (yes / no)
      converted target               (0 / 1 — did the visitor buy)
    """
    return pd.read_csv(path)

La versión de librería

Nadie escribe estos a mano en producción, y una vez que viste las versiones desde cero sabes por qué las de librería son confiables: hacen exactamente lo mismo. El módulo preprocessing de scikit-learn trae OneHotEncoder y OrdinalEncoder como objetos fit/transform con la misma lógica de categorías que usamos — vocabulario ordenado, una columna por nivel para el one-hot, un orden explícito para el ordinal:

def sklearn_one_hot(col):
    """scikit-learn's OneHotEncoder on one nominal column.

    sparse_output=False gives a dense (N, K) array; categories default to the
    sorted unique values, so the column order matches our one_hot exactly.
    """
    enc = OneHotEncoder(sparse_output=False, categories="auto")
    return enc.fit_transform(np.asarray(col).reshape(-1, 1)), list(enc.categories_[0])


def sklearn_ordinal(col, order):
    """scikit-learn's OrdinalEncoder with an explicit category order.

    Passing categories=[order] pins S->0, M->1, L->2 the way our ordinal_encode
    does. With the default (sorted) order this same object is 'label' encoding.
    """
    enc = OrdinalEncoder(categories=[list(order)])
    return enc.fit_transform(np.asarray(col).reshape(-1, 1)).ravel()

El checkpoint del capítulo verifica que nuestro one-hot desde cero coincida con la salida de OneHotEncoder celda por celda — mismo orden de columnas, mismos valores 0/1, cada fila sumando exactamente uno — y que nuestra codificación ordinal coincida con la de OrdinalEncoder cuando se le da el mismo orden S/M/L, antes de dibujar cualquier gráfica. Si un encoder se desvía, la corrida falla ruidosamente en lugar de publicar una imagen equivocada. Los dos modelos que usamos para calificar todo se mantienen fijos, así que lo único que cambia entre corridas es cómo se codificó color frente a ellos — un modelo lineal que suma columnas con pesos, y un árbol que parte por umbrales:

def logistic_fit_score(Xtr, ytr, Xte, yte):
    """Test accuracy of a fixed logistic-regression classifier.

    A linear model: it can only add up a weighted sum of the feature columns.
    That linearity is exactly what makes it believe an ordinal integer's fake
    distances, which is what we want to expose.
    """
    clf = LogisticRegression(max_iter=2000, random_state=0)
    clf.fit(Xtr, ytr)
    return float(clf.score(Xte, yte))


def tree_fit_score(Xtr, ytr, Xte, yte):
    """Test accuracy of a fixed decision-tree classifier.

    A tree splits one column at a time on thresholds, so it can carve a single
    integer axis into per-category bands (code <= 0.5, code <= 1.5, ...). It is
    far less fooled by a bad encoding than the linear model is.
    """
    clf = DecisionTreeClassifier(max_depth=4, random_state=0)
    clf.fit(Xtr, ytr)
    return float(clf.score(Xte, yte))

Ese par es deliberado. El modelo lineal es al que el orden falso engaña; el árbol es el control que muestra que el daño es específico de los modelos que hacen aritmética sobre sus entradas.

Desde cero contra librería

El duelo de siempre es un poco distinto en un capítulo de codificación, porque los encoders desde cero y los de librería son idénticos por construcción — el checkpoint lo demuestra, así que graficar "nuestro one-hot contra el one-hot de sklearn" serían dos barras de la misma altura. La comparación que vale la pena es la que el capítulo prometió: lo que la elección de codificación le hace al accuracy en datos no vistos de un modelo real. La misma tabla sintética, un split de 225 filas de train / 75 de test, cuatro corridas — el modelo lineal y el árbol, cada uno alimentado con color como entero de etiqueta (mal) y como columnas one-hot (bien):

Lee los dos pares. Para el modelo lineal, codificar color por etiqueta da 0.573 de accuracy en test — la fracción de los 75 visitantes reservados que etiqueta correctamente — y cambiar a one-hot lo sube a 0.760, una ganancia de 0.187, catorce visitantes extra acertados de setenta y cinco, por nada más que un cambio de codificación. Sin features nuevos, sin tuning, sin reentrenar el modelo de otra forma. Solo dejamos de mentirle sobre color, y un clasificador de volado se volvió uno útil. Ese 0.187 es el capítulo entero, impreso como una barra.

Ahora mira el árbol. Con etiqueta, saca 0.747; con one-hot, 0.733 — una variación de 0.013, ruido estadístico. Al árbol apenas le importa, y vale la pena entender por qué, porque es la misma razón por la que no le importó el escalado el capítulo pasado. Un árbol parte una columna a la vez por umbrales, así que incluso con el eje entero falso puede cortarlo en bandas por categoría — código menor a 0.5 es azul, menor a 1.5 es verde, el resto es rojo — y recuperar las categorías que el modelo lineal no pudo. La lección no es que la codificación nunca importe; es que importa para los modelos que hacen aritmética sobre sus entradas y mucho menos para los que hacen preguntas de sí/no sobre ellas. Si solo corrieras árboles podrías volverte descuidado aquí. El día que pongas un modelo lineal o uno basado en distancias detrás de ese mismo entero de etiqueta, el descuido te cuesta 0.187.

Conclusiones

Empareja la codificación con lo que la variable realmente es, no con lo que requiere menos tecleo. Si una categoría no tiene orden — color, ciudad, línea de producto, navegador — es nominal, y quiere one-hot: una columna por nivel, sin aritmética que las relacione, nada que un modelo lineal pueda malinterpretar. Si una categoría tiene un orden real y con espaciado uniforme — chico/mediano/grande, la escala de desacuerdo-a-acuerdo de una encuesta, un grado de severidad — entonces los enteros ordinales no solo son aceptables, son la decisión correcta, porque el orden es información y los enteros se la entregan al modelo gratis. El modo de falla es exactamente una sustitución: agarrar la codificación compacta de enteros en una columna nominal porque era una línea menos de código. Esa es la trampa de "rojo es 0, verde es 1, azul es 2", y este capítulo midió lo que cuesta — 0.187 de accuracy en un modelo lineal, la diferencia entre un modelo que funciona y uno que no.

Dos advertencias que cargo a cada proyecto. Primero, el one-hot tiene un techo: la alta cardinalidad. Unas cuantas docenas de categorías están bien; unos cuantos miles de IDs de usuario son un desastre que come memoria y hace overfitting, y ahí es donde el target encoding o un hashing encoder se ganan su lugar. Conoce el punto de cambio antes de toparte con él. Segundo, el modelo detrás de la codificación decide cuánto importa todo esto — los modelos lineales, los basados en distancias y los de gradient descent se toman tu orden fabricado al pie de la letra y sufren, mientras que los árboles y los ensambles de árboles casi ni se inmutan. Así que la pregunta nunca es solo "cómo codifico esta columna", es "cómo codifico esta columna para el modelo al que se la voy a dar". Acierta el tipo, respeta los órdenes reales y niégate a inventar falsos, y cuida la cardinalidad. Esa es toda la disciplina, y ocurre antes de que el algoritmo por el que realmente viniste llegue a correr.