Árboles de decisión y Random Forest
De una tabla a preguntas encadenadas, y de un árbol a un bosque que no memoriza
Un árbol de decisión es una cadena de preguntas que reparte los datos hasta dejarlos en grupos puros. Aprenderás a medir esa pureza con Gini, a controlar la profundidad para no memorizar y a combinar muchos árboles en un Random Forest que generaliza mejor.
- Calcular la impureza Gini de un nodo y la ganancia de una división
- Interpretar un árbol como reglas if/else y seguirlo para un caso nuevo
- Elegir `max_depth` y otras podas para controlar el sobreajuste
- Explicar cómo el bagging construye un Random Forest
- Leer la importancia de variables sin sobreinterpretarla
Antes necesitas: Fundamentos de Machine Learning · Regresión lineal y logística
En esta lección
- ¿Qué es un árbol de decisión?
- ¿Para qué sirve?
- ¿Cómo funciona por dentro? Impureza y ganancia
- Sintaxis: dos modelos, un mismo bosque
- Ejemplo básico: la impureza de un nodo
- Visualización: los datos de la tabla
- Traza: una decisión paso a paso
- Ejemplo intermedio: entrenar, medir y podar
- Random Forest: muchos árboles con bagging
- Árbol y bosque frente a frente
- Caso práctico: el árbol es un if/else
- Errores comunes
- Buenas prácticas
- Ejercicios
- Soluciones explicadas
- Resumen
- Evaluación
Si has jugado a «adivina quién», ya conoces la idea: haces preguntas que partan el grupo en dos y eliges la que deja menos dudas. Un árbol de decisión hace exactamente eso, pero elige las preguntas con una medida de impureza en lugar de intuición.
Su gran ventaja es que se puede leer en voz alta: «si la asistencia supera 67 %, aprueba». Su gran riesgo es aprenderse la tabla de memoria. Esta lección cubre las dos caras y termina con el bosque que corrige la segunda.
¿Qué es un árbol de decisión?
Un árbol de decisión es un modelo que divide el espacio de características con cortes sucesivos. Cada nodo interno pregunta por una variable y un umbral; cada rama es la respuesta; cada hoja devuelve una predicción: una clase o un número. Para clasificar, la hoja devuelve la clase mayoritaria de los ejemplos que llegaron a ella.
El árbol se construye de arriba abajo. En cada nodo prueba todos los cortes posibles de todas las variables, elige el que más reduce la impureza y repite el proceso en cada hijo hasta que se cumple una condición de parada: profundidad máxima, mínimo de ejemplos por hoja o pureza total.
¿Para qué sirve?
- Decisiones explicables: aprobar un crédito, asignar una beca o priorizar una inspección y poder justificar el porqué.
- Datos tabulares mezclados: números y categorías sin necesidad de escalar, algo que la regresión logística agradece pero no exige.
- Relaciones no lineales: umbrales del tipo «a partir de 67 % de asistencia cambia la decisión».
- Selección de variables: la importancia que calcula el bosque orienta qué medir mejor.
¿Cómo funciona por dentro? Impureza y ganancia
La impureza Gini mide cuán mezclado está un nodo. Si p es la proporción de la clase positiva, Gini = 1 − (p² + (1 − p)²). Un nodo puro tiene Gini 0; uno con 50/50 llega a 0.5, el máximo en clasificación binaria. La entropía mide lo mismo con logaritmos y su máximo es 1 bit, pero la conclusión práctica es idéntica.
La ganancia de una división es la impureza del padre menos la impureza de los hijos ponderada por su tamaño. El árbol elige el corte con mayor ganancia. Ese cálculo es voraz: cada nodo decide lo mejor para sí mismo, sin ver el árbol completo, y por eso no garantiza el óptimo global.
Sintaxis: dos modelos, un mismo bosque
Si aún no tienes la biblioteca, instálala una vez; después basta con crear el modelo, fit y predict:
python -m pip install scikit-learnfrom sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
X = [[1, 50], [2, 55], [2, 60], [6, 60], [3, 65], [4, 70], [2, 85], [1, 90]]
y = [0, 0, 0, 1, 0, 1, 1, 1]
# Un árbol: fácil de leer, fácil de sobreajustar
arbol = DecisionTreeClassifier(max_depth=3, random_state=0)
arbol.fit(X, y)
# Un bosque: muchos árboles con muestras distintas
bosque = RandomForestClassifier(n_estimators=200, random_state=0)
bosque.fit(X, y)
print("árboles en el bosque:", len(bosque.estimators_))max_depth limita cuántas preguntas se pueden encadenar. n_estimators es cuántos árboles forman el bosque. random_state fija el azar de la inicialización y del muestreo. Los dos modelos comparten fit, predict y predict_proba, así que puedes intercambiarlos sin reescribir el resto del código.
Ejemplo básico: la impureza de un nodo
Ocho estudiantes, cuatro aprueban y cuatro no: el nodo raíz está tan mezclado como puede estar. Si lo divides por asistencia en 67 %, el grupo de la izquierda queda con cuatro fallos y un aprobado, y el de la derecha con tres aprobados.
def gini(etiquetas):
p = sum(etiquetas) / len(etiquetas)
return 1 - (p ** 2 + (1 - p) ** 2)
raiz = [0, 0, 0, 0, 1, 1, 1, 1]
izquierda = [0, 0, 0, 1, 0] # asistencia <= 67.5
derecha = [1, 1, 1] # asistencia > 67.5
print("raíz:", round(gini(raiz), 2))
print("izquierda:", round(gini(izquierda), 2))
print("derecha:", round(gini(derecha), 2))
ponderado = (5 / 8) * gini(izquierda) + (3 / 8) * gini(derecha)
print("ganancia:", round(gini(raiz) - ponderado, 4))La salida es raíz: 0.5, izquierda: 0.32, derecha: 0.0 y ganancia: 0.3. Dividir por horas en 3.5 daría 0.1667: también buena, pero menor. Con esa diferencia el árbol elige la asistencia, y esa es la razón por la que dos algoritmos con los mismos datos pueden producir árboles distintos.
Visualización: los datos de la tabla
asistencia <= 67.5: estas cinco filas van a la izquierda. Cuatro fallan y una aprueba, así que el nodo no es puro.Recorre los pasos para ver cómo cambian las celdas.
La matriz deja ver por qué hacen falta dos preguntas: ninguna columna ordena las etiquetas por sí sola. La asistencia separa el grupo grande y las horas resuelven el pequeño. Los árboles no necesitan que las columnas estén en la misma escala, porque cada pregunta usa una sola columna.
Traza: una decisión paso a paso
Un estudiante nuevo estudió 5 horas pero asistió solo al 62 %. Recorre el árbol como lo haría el modelo:
horas = 5asistencia = 62if asistencia > 67.5: pred = "aprueba"elif horas > 4.5: pred = "aprueba"else: pred = "no aprueba"print(pred)horas = 5, el valor de la primera característica.—
Avanza paso a paso: observa la línea resaltada, las variables y la salida.
Cambia la asistencia a 90 y el recorrido termina en la primera pregunta: la rama derecha es pura y no hace falta mirar las horas. Esa es la ventaja del árbol: cada caso recorre solo unas pocas preguntas, aunque el modelo tenga cientos de nodos.
Ejemplo intermedio: entrenar, medir y podar
Sin límite de profundidad, un árbol crece hasta que cada hoja tiene un solo ejemplo. Con estos ocho datos, eso significa memorizar y acertar el 100 % en entrenamiento. Limitar la profundidad a 2 lo obliga a generalizar:
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
X = [[1, 50], [2, 55], [2, 60], [6, 60], [3, 65], [4, 70], [2, 85], [1, 90]]
y = [0, 0, 0, 1, 0, 1, 1, 1]
for d in [1, 2]:
arbol = DecisionTreeClassifier(max_depth=d, random_state=0)
arbol.fit(X, y)
pred = arbol.predict(X)
print(d, round(accuracy_score(y, pred), 2))La salida es 1 0.88 y 2 1.0. Con una sola pregunta el árbol acierta 7 de 8: deja un aprobado en el grupo de asistencia baja. Con la segunda, separa ese caso por horas. Aquí la profundidad 2 es la receta exacta para estos datos, pero en un problema real ese 1.0 en entrenamiento es una señal de alarma, no una meta.
Un árbol sin poda tiene varianza alta: si cambias diez filas, la estructura entera puede reorganizarse, porque cada división depende de las anteriores. Los parámetros que controlan ese crecimiento son max_depth, min_samples_leaf (mínimo de ejemplos por hoja), min_samples_split (mínimo para intentar dividir) y ccp_alpha (podado por coste-complejidad).
Random Forest: muchos árboles con bagging
Un solo árbol es inestable. Un Random Forest entrena cientos de árboles, cada uno con una muestra aleatoria con reemplazo (bagging) y, en cada división, con un subconjunto aleatorio de variables. Después promedia las probabilidades o vota. Al promediar modelos con errores distintos, la varianza baja y el resultado se estabiliza.
El costo es la interpretabilidad: ya no hay un árbol que puedas dibujar. A cambio ganas robustez y una medida de importancia por variable, que se calcula sumando cuánto reduce la impureza cada variable en todos los árboles y normalizando para que sume 1.
from sklearn.ensemble import RandomForestClassifier
X = [[1, 50], [2, 55], [2, 60], [6, 60], [3, 65], [4, 70], [2, 85], [1, 90]]
y = [0, 0, 0, 1, 0, 1, 1, 1]
bosque = RandomForestClassifier(n_estimators=100, random_state=0)
bosque.fit(X, y)
horas, asistencia = bosque.feature_importances_
print("horas:", round(horas, 2))
print("asistencia:", round(asistencia, 2))
print("suma:", round(horas + asistencia, 2))La tercera línea imprime suma: 1.0, porque las importancias se normalizan. Compara las dos primeras: la que se acerque más a 1 concentra las decisiones del bosque. En este conjunto, la asistencia separa cinco filas de golpe y suele llevarse el peso mayor.
Árbol y bosque frente a frente
| Aspecto | Árbol de decisión | Random Forest |
|---|---|---|
| Interpretación | reglas legibles | caja negra con importancias |
| Sobreajuste | alto si no se poda | más resistente por el promedio |
| Estabilidad | cambia con pocas filas | estable entre ejecuciones |
| Coste | entrenamiento rápido | cien veces más árboles |
| Uso típico | explicar una decisión | predecir con datos tabulares |
Caso práctico: el árbol es un if/else
Una vez entrenado y podado, el árbol se puede traducir a una función. Eso permite incrustarlo en cualquier sistema sin cargar la biblioteca, y también revisar si las reglas tienen sentido para el negocio.
def aprobo(horas, asistencia):
if asistencia > 67.5:
return "aprueba"
if horas > 4.5:
return "aprueba"
return "no aprueba"
casos = [(5, 62), (3, 65), (2, 80)]
for horas, asistencia in casos:
print(horas, asistencia, "->", aprobo(horas, asistencia))def aprobo(horas, asistencia):
if asistencia > 67.5:
return True
return horas > 4.5#include <iostream>
bool aprobo(double horas, double asistencia) {
if (asistencia > 67.5) return true;
return horas > 4.5;
}
int main() {
std::cout << aprobo(5, 62) << '\n';
}La lógica es idéntica; cambian la declaración de tipos y la forma de imprimir. Esto explica por qué un árbol pequeño se puede portar a producción en cualquier lenguaje, sin depender de scikit-learn.
La salida del ejemplo es 5 62 -> aprueba, 3 65 -> no aprueba y 2 80 -> aprueba: el tercer caso ni siquiera consulta las horas. Si esta regla decide una beca, conviene preguntarse si tiene sentido que 5 horas con 62 % de asistencia basten para aprobar. El árbol encontró un patrón; aprobarlo es tu decisión.
Errores comunes
- Dejar crecer el árbol sin límite: memoriza, se vuelve enorme y falla fuera de la tabla.
- Podar mirando el test: elige la profundidad con validación, nunca con el conjunto que vas a reportar.
- Interpretar una hoja con dos ejemplos como una ley: con pocos casos, la regla es ruido.
- Olvidar
random_stateen el bosque: dos ejecuciones dan importancias distintas y la comparación se vuelve imposible. - Usar árboles con miles de columnas dispersas: los bosques rinden mejor con variables densas y con sentido.
Buenas prácticas
- Empieza con un árbol de profundidad 2 o 3 para ver la estructura antes de complicarla.
- Elige la profundidad con validación cruzada y grafica el error de train y de validación juntos.
- Exige un mínimo de ejemplos por hoja (
min_samples_leaf) si vas a explicar las reglas a alguien. - Compara siempre con la regresión logística: si el árbol no mejora, prefiere el modelo simple.
- Guarda el bosque con
jobliby la versión de scikit-learn: los modelos no siempre son compatibles entre versiones.
Ejercicios
Compara dos divisiones
horas <= 3.5, que deja [0, 0, 0, 0, 1, 1] a la izquierda y [1, 1] a la derecha. Imprime la impureza ponderada y la ganancia con cuatro decimales.Pista
Pondera cada Gini por su peso: 6/8 y 2/8. La ganancia es 0.5 menos el resultado.
El precio de una pregunta
max_depth=1 y otro con max_depth=3, sobre los ocho estudiantes. Imprime la exactitud de entrenamiento de cada uno redondeada a dos decimales y explica qué cambia.Pista
Reutiliza las listas del ejemplo. Con max_depth=3 el árbol puede memorizar los ocho casos.
def gini(etiquetas): p = sum(etiquetas) / len(etiquetas) return 1 - (p ** 2 + ( - p) ** 2) nodo = [1, 1, 1, 0] print(round(gini(nodo), 3))
La segunda proporción es la de la clase negativa.
Soluciones explicadas
En el primer ejercicio, la comparación importa más que el resultado: 0.1667 frente a 0.3 explica por qué el árbol prefiere la asistencia. Si los datos fueran otros, las horas podrían ganar por un margen igual de pequeño, y el árbol cambiaría por completo. Esa fragilidad es la razón por la que un bosque promedia cien versiones.
En el segundo, la exactitud de entrenamiento sube con la profundidad por construcción: cada pregunta adicional puede separar más casos. Para saber si esa mejora sirve, compara la exactitud de validación. Si la de entrenamiento sube y la de validación baja, la pregunta extra solo está memorizando.
Resumen
- Un árbol divide el espacio con preguntas binarias y predice con la hoja alcanzada.
- Gini mide la mezcla de un nodo: 0 es puro y 0.5 es el máximo binario.
- La ganancia es la reducción de impureza ponderada; el árbol elige el corte con mayor ganancia.
- Sin poda, el árbol memoriza;
max_depthymin_samples_leafcontrolan el crecimiento. - Random Forest combina árboles entrenados con muestras y variables aleatorias para reducir la varianza.
- La importancia de variables reparte el peso y suma 1; no demuestra causalidad.
Evaluación
Un nodo contiene 6 ejemplos de la clase 1 y 2 de la clase 0. ¿Cuál es su Gini?
¿Qué hace el bagging en un Random Forest?
Tu árbol acierta 1.0 en entrenamiento y 0.68 en validación. ¿Qué harías primero?
¿Ya lo entendiste?
Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.