Árboles de decisión desde cero: cómo se realizan realmente las divisiones
La última vez, Sam comparó XGBoost, LightGBM y CatBoost en un conjunto de datos de práctica. Sin embargo, antes de confiar en un ensamble de cientos de árboles, quiere entender lo que realmente hace un árbol individual.
1. El problema del ‘cuarto desordenado’: por qué necesitamos una regla para el caos
¿Alguna vez has entrado a una habitación donde todo está en el lugar equivocado? Calcetines en el librero, libros en la canasta de la ropa sucia, un zapato perdido sobre la encimera de la cocina. No simplemente moverías las cosas al azar para ordenar. Agruparías las cosas hasta que cada caja contuviera solo un tipo de artículo.
Los datos de práctica de autos usados de Sam se sienten como esa habitación: autos caros y baratos mezclados como calcetines en un librero. Necesita clasificarlos en pilas limpias y separadas antes de poder confiar en un modelo para hacer predicciones.
Piensa en un Árbol de Decisión como un organizador profesional. Su trabajo es tomar una pila desordenada de datos y dividirla en pilas más pequeñas que sean lo más ‘limpias’ posible. En la ciencia de datos, una pila perfectamente limpia es ‘pura’. Una caja que contiene solo calcetines azules es pura. Una caja que contiene calcetines, juguetes y correo es ‘impura’.
Para construir un árbol, necesitamos una forma de medir qué tan desordenado es un grupo. Una regla para el caos. Si no podemos medir el desorden, no podemos saber si una regla ‘si-entonces’ específica (como ‘¿El kilometraje es mayor a 50,000?’) realmente hizo que nuestros datos estuvieran más limpios. Así es como se ve un punto de partida desordenado en Python.
# Let's represent our 'items' as labels
# 1 = 'Expensive Car', 0 = 'Cheap Car'
group_a = [1, 1, 1, 1, 1] # Perfectly clean (all expensive)
group_b = [1, 0, 1, 0, 1] # Very messy (mixed)
group_c = [0, 0, 0, 0, 0] # Perfectly clean (all cheap)
print(f"Group A: {group_a} - This looks very organized.")
print(f"Group B: {group_b} - This is pure chaos.")
- Los comentarios establecen el esquema de codificación:
1significa “Auto Caro” y0significa “Auto Barato” — las etiquetas binarias son estándar para problemas de clasificación de dos clases. group_a = [1, 1, 1, 1, 1]representa un grupo perfectamente limpio donde cada auto es caro — esto es lo que un árbol quiere que sus nodos hoja parezcan.group_b = [1, 0, 1, 0, 1]es el grupo desordenado: tres autos caros y dos baratos mezclados — esto es con lo que el árbol comienza y trata de separar.group_c = [0, 0, 0, 0, 0]es el otro extremo perfectamente limpio — todos baratos, sin mezcla.- Las f-strings (
f"Group A: {group_a} ...") usan la interpolación de cadenas de Python para incrustar los valores de la lista directamente en el texto de salida, para que puedas ver los datos de un vistazo.
Lo que realmente buscamos es la división que crea los subgrupos más limpios. Pero para encontrarla, necesitamos una forma matemática de convertir el ‘caos’ en un número.
2. Impureza de Gini: el puntaje de ‘suposición errónea’
Imagina una bolsa que contiene todos los elementos de group_b — tres 1s y dos 0s. Metes la mano, sacas uno y adivinas qué es. ¿Qué tan probable es que te equivoques?
Esa probabilidad de una suposición errónea es lo que mide la Impureza de Gini. Es la probabilidad de etiquetar incorrectamente un elemento elegido al azar si las etiquetas se asignan según la distribución del conjunto.
- Si la bolsa es 100% manzanas, adivinas ‘manzana’ cada vez. Te equivocas el 0% de las veces. Puntaje Gini: 0.
- Si la bolsa es 50% manzanas y 50% naranjas, te equivocas mucho. Alta impureza.
Sin embargo, Gini es solo un atajo. La fórmula: 1 - sum(squared probabilities of each label).
def calculate_gini(labels):
if not labels: return 0
counts = {}
for l in labels:
counts[l] = counts.get(l, 0) + 1
impurity = 1
for l in counts:
prob_of_label = counts[l] / len(labels)
impurity -= prob_of_label**2
return impurity
print(f"Gini for Group A (Pure): {calculate_gini(group_a):.2f}")
print(f"Gini for Group B (Messy): {calculate_gini(group_b):.2f}")
def calculate_gini(labels):define una función que toma una lista de etiquetas de clase (como[1, 0, 1, 0, 1]) y devuelve un único puntaje de impureza.if not labels: return 0es una cláusula de guarda — por convención, una lista vacía tiene cero impureza, y esto evita una división por cero más adelante.counts = {}inicializa un diccionario vacío para contar cuántas veces aparece cada etiqueta en la lista.counts[l] = counts.get(l, 0) + 1es una expresión clásica de Python:.get(l, 0)devuelve el recuento actual para la etiquetal(o 0 si aún no se ha visto), luego suma 1 — esto construye un diccionario de frecuencias en una sola pasada.impurity = 1inicia el Gini en 1 y resta cada probabilidad al cuadrado — la fórmula es .prob_of_label = counts[l] / len(labels)calcula la proporción del total que representa cada etiqueta.impurity -= prob_of_label**2resta la probabilidad al cuadrado — elevar al cuadrado penaliza menos a las clases dominantes que a las clases minoritarias, razón por la cual Gini favorece las divisiones que aíslan a la mayoría.- El f-string
{calculate_gini(group_a):.2f}formatea el flotante devuelto a 2 decimales para una salida más limpia.
Un puntaje Gini de 0.48 para el Grupo B significa que el conjunto es aproximadamente 48% ‘desordenado’. Más cerca de 0 significa una división más limpia.
3. Entropía: el factor ‘sorpresa’
Otra forma de medir la impureza es la Entropía. Gini rastrea la probabilidad de equivocarse; la Entropía rastrea la información, o la sorpresa.
Una moneda justa cae en cara — no te sorprende, porque había incertidumbre real. Pero si te digo que una moneda con dos caras cayó en cara, no te he dado nada nuevo. Ya sabías que iba a pasar.
Esta es la parte más difícil: la Entropía usa logaritmos (específicamente log2), ya que los logaritmos cuentan bits de información. Un grupo perfectamente puro tiene Entropía 0. Una división perfecta de 50/50 da Entropía 1.0.
import math
def calculate_entropy(labels):
if not labels: return 0
counts = {}
for l in labels:
counts[l] = counts.get(l, 0) + 1
entropy = 0
for l in counts:
p = counts[l] / len(labels)
entropy -= p * math.log2(p)
return entropy
print(f"Entropy for Group B: {calculate_entropy(group_b):.2f}")
import mathincorpora el módulo math de Python para acceder amath.log2— la función de logaritmo en base 2 que requiere la Entropía.- El bucle que construye el diccionario
countses idéntico al de la función Gini — mismo recuento de frecuencias, misma expresión. entropy = 0comienza en cero y suma cada término (mediante-=), mientras que Gini comienza en 1 y resta — ambos acumulan pero desde diferentes líneas base.p = counts[l] / len(labels)calcula la probabilidad de cada etiqueta, exactamente igual que en Gini.entropy -= p * math.log2(p)es la diferencia clave con Gini: en lugar de elevar la probabilidad al cuadrado, la Entropía multiplica cada probabilidad por su propio logaritmo en base 2 — esto mide el “contenido de información” en bits. Cuandopes pequeño,log2(p)es un número negativo grande, por lo que el productop * log2(p)todavía contribuye con un pequeño término positivo (los dos negativos se cancelan).math.log2(p)fallaría sipfuera 0, pero la guardaif not labelsy el bucle del diccionario (que solo visita las etiquetas que existen) previenen ese caso límite.
Impureza de Gini — la probabilidad de clasificar erróneamente un elemento elegido al azar si fuera etiquetado según la distribución de clases:
Entropía — la “sorpresa” o contenido de información de un conjunto, medido en bits:
Ganancia de información — la reducción de impureza después de dividir el conjunto según el atributo :
| Lenguaje simple | Símbolo estadístico | Equivalente en Python |
|---|---|---|
| Probabilidad de la etiqueta | counts[l] / len(labels) | |
| Impureza de Gini del conjunto | 1 - sum(prob**2) | |
| Entropía del conjunto | -sum(p * math.log2(p)) | |
| Impureza ponderada de los hijos después de la división | $\sum \frac{ | S_v |
| Ganancia de información de la división | gini(parent) - weighted_child_impurity |
¿Por qué usarla en lugar de Gini? La Entropía es ligeramente más sensible a los cambios en el medio de la distribución. En la práctica, usualmente llevan al mismo árbol, pero la Entropía es la forma clásica de medir la ‘Ganancia de información’.
| Criterio | Impureza de Gini | Entropía |
|---|---|---|
| Costo computacional | Menor (sin cálculo de logaritmos) | Mayor (log₂ por término) |
| Sensibilidad cerca de divisiones 50/50 | Menos sensible | Más sensible |
| Rango (binario) | 0 a 0.5 | 0 a 1.0 |
| Resultado típico | Mismo árbol que Entropía | Mismo árbol que Gini |
Regla práctica: Usa Gini (el valor predeterminado en la mayoría de librerías) cuando busques velocidad; recurre a la Entropía cuando necesites un poco más de sensibilidad a las divisiones balanceadas o cuando estés siguiendo la literatura clásica de Ganancia de información.
4. Ganancia de información: la prueba de ‘¿vale la pena?’
Ahora tenemos una regla. ¿Cómo la usa el árbol en realidad? Usa Ganancia de información.
Antes de una división, el árbol mide el desorden del grupo ‘Padre’. Luego simula una división (por ejemplo, ‘Mileage < 50k’) y calcula el desorden promedio de los dos grupos ‘Hijo’ resultantes.
Ganancia de información = (Desorden antes) - (Desorden después)
Una ganancia alta significa que la división realmente ayudó a limpiar el desorden. Una ganancia de cero significa que no logró nada. Así que vamos a calcularla.
def information_gain(parent, left_child, right_child):
# We weight the children by their size
weight_l = len(left_child) / len(parent)
weight_r = len(right_child) / len(parent)
gain = calculate_gini(parent) - (weight_l * calculate_gini(left_child) + weight_r * calculate_gini(right_child))
return gain
# Example: Splitting a group of 4 cars
parent_cars = [1, 0, 1, 0]
left_split = [1, 1] # All expensive
right_split = [0, 0] # All cheap
gain = information_gain(parent_cars, left_split, right_split)
print(f"Information Gain of this split: {gain:.2f}")
def information_gain(parent, left_child, right_child):toma el grupo original y los dos subgrupos creados por una división candidata — la función funciona con cualquier medida de impureza (Gini o Entropía) ya que llama acalculate_giniinternamente.weight_l = len(left_child) / len(parent)calcula qué fracción de las filas del padre terminó en el hijo izquierdo — un hijo más grande tiene más peso, por lo que su impureza importa más para la puntuación general.weight_r = len(right_child) / len(parent)hace lo mismo para el hijo derecho; ten en cuenta queweight_l + weight_rsiempre es igual a 1 (asumiendo que cada fila va a un lado o al otro).gain = calculate_gini(parent) - (weight_l * calculate_gini(left_child) + weight_r * calculate_gini(right_child))es la fórmula de la Ganancia de información en una sola línea: la impureza del padre menos el promedio ponderado por tamaño de las impurezas de los hijos.parent_cars = [1, 0, 1, 0]es una división 50/50 — máximamente impuro para un problema de dos clases, por lo que Gini es 0.5 (el peor caso para uno binario).left_split = [1, 1]yright_split = [0, 0]son ambos perfectamente puros (Gini = 0), por lo que la ganancia es0.5 - (0.5*0 + 0.5*0) = 0.5— la máxima posible para una división binaria.
El resultado es 0.50. Dado que el Gini máximo para dos clases es 0.50, pasamos de un desastre total a una limpieza perfecta. Una victoria clara.
5. Construyendo la mejor división: una guía paso a paso
Un árbol de decisión es solo un contador rápido. Escanea cada columna y cada valor posible para encontrar la división con la mayor Ganancia de Información.
Tomemos el pequeño conjunto de datos de autos de la Parte 4. Queremos predecir si un auto es ‘Caro’ (1) o ‘Barato’ (0).
# Data: [Mileage, Is_Electric (1/0)], Label: [Is_Expensive (1/0)]
data = [
[50000, 0, 0], # High mile, gas, cheap
[10000, 1, 1], # Low mile, electric, expensive
[80000, 0, 0], # High mile, gas, cheap
[5000, 0, 1], # Low mile, gas, expensive
]
# Let's test two possible splits:
# Split 1: Is the car Electric?
# Split 2: Is Mileage < 20,000?
parent_labels = [0, 1, 0, 1]
# Split 1 results (Electric vs Gas)
# Electric cars: [1] | Gas cars: [0, 0, 1]
electric_gain = information_gain(parent_labels, [1], [0, 0, 1])
# Split 2 results (Mileage < 20k)
# Low miles: [1, 1] | High miles: [0, 0]
mileage_gain = information_gain(parent_labels, [1, 1], [0, 0])
print(f"Gain from 'Is Electric': {electric_gain:.2f}")
print(f"Gain from 'Low Mileage': {mileage_gain:.2f}")
if mileage_gain > electric_gain:
print("The tree chooses Mileage as the first branch!")
- Cada fila en
dataes[Mileage, Is_Electric, Is_Expensive]— los dos primeros valores son características y el tercero es la etiqueta que el árbol intenta predecir. parent_labels = [0, 1, 0, 1]extrae solo las etiquetas de las cuatro filas — dos baratos (0) y dos caros (1), por lo que el nodo padre está completamente mezclado.electric_gain = information_gain(parent_labels, [1], [0, 0, 1])simula la división por “¿Es eléctrico?”: el grupo eléctrico[1]es puro (un auto caro), pero el grupo de gasolina[0, 0, 1]sigue siendo mixto (dos baratos, uno caro), por lo que la ganancia es solo parcial.mileage_gain = information_gain(parent_labels, [1, 1], [0, 0])simula la división por “Kilometraje < 20,000”: ambos nodos hijos son perfectamente puros — los autos de bajo kilometraje son todos caros, los de alto kilometraje son todos baratos — por lo que la ganancia es máxima.if mileage_gain > electric_gain:es la lógica de decisión real del árbol: la división candidata que tenga la mayor Ganancia de Información gana y se convierte en la regla de división del nodo.- Las sentencias de impresión revelan qué división elegiría el árbol — en este caso, el kilometraje gana porque separa las clases perfectamente, mientras que la división eléctrica deja desordenado al grupo de gasolina.
La división de ‘Bajo Kilometraje’ produce dos grupos perfectamente puros: ganancia máxima. El árbol la elige y luego continúa.
Entonces, ¿qué aprendimos hoy?
- Impureza simplemente significa que un grupo está mezclado.
- Gini y Entropía son dos formas de medir ese desorden.
- La Ganancia de Información te dice si una división realmente organizó los datos.
- El Árbol es un bucle que prueba cada división y se queda con la de mayor Ganancia.
Esa es la conclusión. Incluso modelos complejos como XGBoost o CatBoost (cubiertos en la Parte 4) se construyen sobre estas mismas piezas. Solo lo hacen más rápido, con más árboles.
Un solo árbol es fácil de leer pero frágil—Sam quiere ver qué pasa cuando combinas muchos de ellos.
A continuación: Bosques Aleatorios vs. Gradient Boosting—equipos de árboles votando sobre los listados de Sam para lograr un efecto de ‘sabiduría de la multitud’. Pruébalo con tus propios datos y comprueba si puedes identificar los grupos puros por ti mismo.
Comprueba tu comprensión
Las preguntas a continuación van desde recuerdo simple hasta diseño abierto, siguiendo aproximadamente la Taxonomía de Bloom.
Recordar ¿Qué significa una puntuación de Impureza de Gini de 0, y qué significa una puntuación de 0.5 para un problema de dos clases?
Comprender
Con tus propias palabras, explica qué mide la “Ganancia de información”, usando la fórmula del artículo (Messiness Before - Messiness After).
Aplicar
Usando la fórmula calculate_gini del artículo (1 - sum(squared probabilities of each label)), calcula la Impureza de Gini para un grupo de 5 autos donde 4 son “Caros” (1) y 1 es “Barato” (0).
Analizar
El ejemplo de la Sección 5 del artículo muestra que la división “Bajo Kilometraje” alcanza una ganancia de 0.50 (hijos perfectamente limpios) mientras que la división “¿Es Eléctrico?” alcanza una ganancia menor (hijos impuros — el grupo de gasolina [0, 0, 1] aún mezcla autos baratos y caros). Explica paso a paso por qué un algoritmo de árbol que revisa “cada valor posible en cada columna” tiene la garantía de encontrar la división que maximiza la ganancia, incluso antes de evaluar un solo candidato.
Evaluar El artículo dice que Gini y Entropía “generalmente llevan al mismo árbol” y llama a la Entropía “ligeramente más sensible a cambios en el medio de la distribución”. Critica la práctica de elegir uno sobre el otro por defecto (por ejemplo, siempre usar Gini porque es más rápido de calcular, sin comprobar la Entropía): ¿cuándo podría esa diferencia de sensibilidad realmente cambiar qué división elige un árbol?
Crear Diseña un conjunto de datos de juguete de 4 filas (como el ejemplo de kilometraje/eléctrico del artículo) con dos divisiones candidatas donde la ganancia basada en Entropía y la ganancia basada en Gini no estuvieran de acuerdo sobre qué división es mejor. No necesitas calcular números exactos — solo describe la distribución de etiquetas en los hijos de cada división candidata que crearía ese desacuerdo.
Artículos relacionados
- P04: XGBoost vs. LightGBM vs. CatBoost: Cómo elegir realmente sin un título en matemáticas — donde Sam comparó las tres principales bibliotecas de gradient boosting en un conjunto de datos de práctica de autos usados antes de abordar el modelo real de HomeMatch.
- P06: Random Forests vs. Gradient Boosting: Por qué los equipos de árboles ganan — el siguiente paso para Sam: combinar muchos árboles individuales en ensambles que votan sobre si un anuncio se venderá rápido.
Referencias y lecturas adicionales
- Breiman, L., Friedman, J., Olshen, R., & Stone, C. (1984). Classification and Regression Trees. Belmont, CA: Wadsworth International Group. — el texto fundacional de CART que formalizó la Impureza de Gini como criterio de división y estableció el algoritmo recursivo de construcción de árboles que se sigue utilizando en la actualidad.
- Quinlan, J. R. (1986). “Induction of Decision Trees.” Machine Learning, 1(1), 81–106. — introdujo el algoritmo ID3 y popularizó la Ganancia de Información basada en Entropía como criterio de división para la construcción de árboles de decisión.
- Kaggle: Precios de Viviendas — Técnicas Avanzadas de Regresión — una competencia donde los modelos basados en árboles (bosques aleatorios, gradient boosting) dominan la tabla de posiciones para la predicción de viviendas con datos tabulares — el mismo dominio en el que Sam trabaja en HomeMatch.
- scikit-learn: documentación de Árboles de Decisión — documentación oficial que cubre tanto Gini como Entropía (parámetro
criterion), el control de la profundidad del árbol y consejos prácticos para evitar el sobreajuste.
Esta traducción fue generada automáticamente y puede contener errores. Si el idioma inglés es tu preferencia, puedes leer el artículo original en inglés .
«Aplica lo que aprendiste» es para suscriptores Supporter e Insider.
Suscríbete para desbloquear los ejercicios de este artículo.
Ver planesArtículos relacionados
- Aprendizaje Automático En revisión
¿Qué es la validación cruzada y cómo evitar hacerla mal?
Aprende a hacer validación cruzada de la manera correcta: evita el sobreajuste, previene la fuga de datos con pipelines, lee la desviación estándar y maneja las series de tiempo correctamente.
- Aprendizaje Automático En revisión
Random Forests vs. Gradient Boosting: Por qué ganan los equipos de árboles
Compara Random Forests frente a Gradient Boosting y aprende por qué los equipos de árboles de decisión vencen a los árboles individuales reduciendo la varianza y el sesgo mediante el ensamblaje.
- Aprendizaje Automático En revisión
P02: Cómo funciona realmente el descenso de gradiente — y las variantes que lo hacen práctico
Aprende cómo el descenso de gradiente minimiza el error del modelo al sentir la pendiente de tu función de pérdida, y compara Batch, SGD, Mini-Batch y Adam con Python puro.
- Aprendizaje Automático En revisión
Validación cruzada anidada: por qué tu puntaje de validación te miente
El ajuste de hiperparámetros infla los puntajes de validación debido al sesgo de optimización: aprende cómo la validación cruzada anidada con Optuna te ofrece estimaciones honestas.
¿Buscas otra cosa?
Busca en todos los artículos por título, resumen o tema.