AulaML Python · C++
Python intermedio · 45 min · Python para Machine Learning

Á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.

Lección 3 de 7 del módulo
Al terminar esta lección podrás
  • 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
  1. ¿Qué es un árbol de decisión?
  2. ¿Para qué sirve?
  3. ¿Cómo funciona por dentro? Impureza y ganancia
  4. Sintaxis: dos modelos, un mismo bosque
  5. Ejemplo básico: la impureza de un nodo
  6. Visualización: los datos de la tabla
  7. Traza: una decisión paso a paso
  8. Ejemplo intermedio: entrenar, medir y podar
  9. Random Forest: muchos árboles con bagging
  10. Árbol y bosque frente a frente
  11. Caso práctico: el árbol es un if/else
  12. Errores comunes
  13. Buenas prácticas
  14. Ejercicios
  15. Soluciones explicadas
  16. Resumen
  17. 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:

SHELL Instalación
python -m pip install scikit-learn
PYTHON arbol_y_bosque.py
from 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.

PYTHON gini.py
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

Mapa de datos ·Ocho estudiantes: horas, asistencia y resultado
8 × 3 rango [0, 90] El nodo raíz usa asistencia <= 67.5: estas cinco filas van a la izquierda. Cuatro fallan y una aprueba, así que el nodo no es puro.
horas
asistencia (%)
aprueba
e1
1
50
0
e2
2
55
0
e3
2
60
0
e4
6
60
1
e5
3
65
0
e6
4
70
1
e7
2
85
1
e8
1
90
1

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:

Ejecución visual ·Recorrido del árbol para un caso nuevo
1 / 6
1 horas = 5
2 asistencia = 62
3
4 if asistencia > 67.5:
5 pred = "aprueba"
6 elif horas > 4.5:
7 pred = "aprueba"
8 else:
9 pred = "no aprueba"
10
11 print(pred)
Entra el caso nuevo con horas = 5, el valor de la primera característica.
Variables
horas = 5
Salida
—

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:

PYTHON profundidad.py
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.

PYTHON bosque.py
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ónRandom Forest
Interpretaciónreglas legiblescaja negra con importancias
Sobreajustealto si no se podamás resistente por el promedio
Estabilidadcambia con pocas filasestable entre ejecuciones
Costeentrenamiento rápidocien veces más árboles
Uso típicoexplicar una decisiónpredecir 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.

PYTHON regla_de_negocio.py
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))
La misma regla en los dos lenguajes
Python
PYTHON regla.py
def aprobo(horas, asistencia):
    if asistencia > 67.5:
        return True
    return horas > 4.5
C++
CPP regla.cpp
#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_state en 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 joblib y la versión de scikit-learn: los modelos no siempre son compatibles entre versiones.

Ejercicios

Ejercicio 1

Compara dos divisiones

En el mismo nodo raíz de ocho estudiantes (cuatro y cuatro), calcula la ganancia de dividir por 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.

Ejercicio 2

El precio de una pregunta

Entrena dos árboles, uno con 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.

Completar código Completa la impureza
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_depth y min_samples_leaf controlan 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

Evaluación 1

Un nodo contiene 6 ejemplos de la clase 1 y 2 de la clase 0. ¿Cuál es su Gini?

Evaluación 2

¿Qué hace el bagging en un Random Forest?

Evaluación 3

Tu árbol acierta 1.0 en entrenamiento y 0.68 en validación. ¿Qué harías primero?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.