---
title: "CART: árboles binarios, índice de Gini y poda"
pagetitle: "CART: árboles binarios de clasificación, índice de Gini y poda de costo-complejidad"
open-graph:
description: "El algoritmo CART: índice de Gini, divisiones binarias para atributos categóricos y numéricos, regiones rectangulares, importancia de variables y poda de costo-complejidad con el eslabón más débil, con desarrollo manual, laboratorio interactivo y Python."
twitter-card:
description: "El algoritmo CART: índice de Gini, divisiones binarias para atributos categóricos y numéricos, regiones rectangulares, importancia de variables y poda de costo-complejidad con el eslabón más débil, con desarrollo manual, laboratorio interactivo y Python."
description: "El algoritmo CART: índice de Gini, divisiones binarias para atributos categóricos y numéricos, regiones rectangulares, importancia de variables y poda de costo-complejidad con el eslabón más débil, con desarrollo manual, laboratorio interactivo y Python."
author: "Diego Villalba"
lang: es
format:
html:
toc: true
toc-depth: 3
toc-title: "Contenido"
code-fold: true
code-tools: true
code-summary: "Mostrar código"
theme: cosmo
highlight-style: github
execute:
echo: true
warning: false
message: false
bibliography: [referencias_fundamentos_clasificacion.bib, referencias_cart.bib, ../../references.bib]
jupyter: python3
---
{{< include ../preprocesamiento/_labkit.qmd >}}
CART (*Classification and Regression Trees*), propuesto por @breiman1984, es la versión de los árboles de decisión que domina la práctica: es el algoritmo de `scikit-learn` y el bloque de construcción de los bosques aleatorios y del *gradient boosting*. Comparte con ID3 la estrategia voraz y recursiva del capítulo anterior, pero cambia cuatro decisiones de diseño:
| | ID3 / C4.5 | CART |
|---|---|---|
| Forma de las divisiones | Multivía (una rama por valor) | **Siempre binaria** |
| Impureza | Entropía, razón de ganancia | **Índice de Gini** (o entropía) |
| Variables numéricas | C4.5: cortes binarios | Cortes binarios, reutilizables |
| Control de complejidad | Poda pesimista | **Poda de costo-complejidad** con validación |
| Regresión | No | Sí (error cuadrático) |
: Diferencias principales entre ID3/C4.5 y CART. {#tbl-id3-cart}
La restricción binaria parece una limitación, pero tiene ventajas: evita el sesgo hacia atributos con muchos valores (cada pregunta parte en solo dos grupos), permite agrupar categorías parecidas y produce árboles que, en el plano, son **particiones en rectángulos**. Este capítulo construye CART a mano con un ejemplo categórico, lo lleva al plano con variables numéricas y desarrolla su contribución más importante: la poda de costo-complejidad.
## Intuición: ¿quién compra una laptop?
Catorce clientes de una tienda de electrónica, descritos por cuatro atributos categóricos [@han2011]:
| Id | Edad | Ingreso | Estudiante | Crédito | ¿Compra? |
|---:|---|---|---|---|---|
| 1 | joven | alto | no | favorable | no |
| 2 | joven | alto | no | excelente | no |
| 3 | adulto | alto | no | favorable | sí |
| 4 | mayor | medio | no | favorable | sí |
| 5 | mayor | bajo | sí | favorable | sí |
| 6 | mayor | bajo | sí | excelente | no |
| 7 | adulto | bajo | sí | excelente | sí |
| 8 | joven | medio | no | favorable | no |
| 9 | joven | bajo | sí | favorable | sí |
| 10 | mayor | medio | sí | favorable | sí |
| 11 | joven | medio | sí | excelente | sí |
| 12 | adulto | medio | no | excelente | sí |
| 13 | adulto | alto | sí | favorable | sí |
| 14 | mayor | medio | no | excelente | no |
: Catorce clientes, 9 compran y 5 no. {#tbl-laptop}
CART no puede preguntar «¿qué edad tiene?» con tres respuestas. Debe preguntar algo de sí o no: «¿es adulto?», «¿es joven?» o «¿es joven o mayor?». La pregunta adecuada es la que deja los dos grupos más puros.
## Construcción paso a paso
### Paso 1. La impureza de Gini en la raíz
El **índice de Gini** de un nodo con proporciones de clase $p_1, \ldots, p_K$ es
$$
G = 1 - \sum_{k=1}^{K} p_k^2.
$$ {#eq-gini}
Es la probabilidad de equivocarse si se etiqueta un ejemplo al azar según las proporciones del nodo[^gini-prob]. En la raíz, con 9 «sí» y 5 «no»:
$$
G_{\text{raíz}} = 1 - \left(\tfrac{9}{14}\right)^2 - \left(\tfrac{5}{14}\right)^2 = 1 - 0.413 - 0.128 = 0.459.
$$
### Paso 2. Las preguntas binarias de un atributo
Edad tiene tres valores, así que admite $2^{3-1} - 1 = 3$ preguntas binarias distintas. Para «¿edad = adulto?»: los 4 adultos compran todos (Gini 0) y los otros 10 se reparten 5 y 5 (Gini 0.5). La impureza ponderada es
$$
\tfrac{4}{14}(0) + \tfrac{10}{14}(0.5) = 0.357, \qquad \Delta G = 0.459 - 0.357 = 0.102.
$$
| Pregunta | Sí cumple (sí/no) | No cumple (sí/no) | Gini ponderado | Reducción $\Delta G$ |
|---|---|---|---:|---:|
| ¿edad = adulto? | 4 / 0 | 5 / 5 | 0.357 | **0.102** |
| ¿edad = joven? | 2 / 3 | 7 / 2 | 0.394 | 0.066 |
| ¿edad = mayor? | 3 / 2 | 6 / 3 | 0.457 | 0.002 |
: Las tres preguntas binarias para la edad. {#tbl-preguntas-edad}
### Paso 3. Comparar atributos
Repitiendo el cálculo para cada atributo y quedándonos con su mejor pregunta:
| Atributo | Mejor pregunta | Gini ponderado | Reducción |
|---|---|---:|---:|
| **Edad** | ¿adulto? | **0.357** | **0.102** |
| Estudiante | ¿estudiante? | 0.367 | 0.092 |
| Crédito | ¿excelente? | 0.429 | 0.031 |
| Ingreso | ¿alto? | 0.443 | 0.016 |
: Mejor pregunta de cada atributo en la raíz. {#tbl-mejores-preguntas}
La raíz pregunta «¿edad = adulto?». La rama «sí» es pura (4 compras) y se vuelve hoja; la rama «no» (10 clientes, 5 y 5) se vuelve a dividir con el mismo procedimiento, donde **la edad puede reaparecer** con otra pregunta («¿joven?»), algo imposible en ID3.
### Paso 4. Variables numéricas: cortes
Con una variable numérica, la pregunta es «¿$x \le t$?». Se ordenan los valores y se prueban los puntos medios entre valores consecutivos. Con cuatro clientes de edades 20 (sí), 25 (sí), 30 (no) y 40 (no):
| Corte $t$ | Izquierda | Derecha | Gini ponderado |
|---:|---|---|---:|
| 22.5 | 1 sí | 1 sí, 2 no | $\frac{3}{4}\big(1 - \frac{1}{9} - \frac{4}{9}\big) = 0.333$ |
| **27.5** | **2 sí** | **2 no** | **0** |
| 35.0 | 2 sí, 1 no | 1 no | $\frac{3}{4}\big(1 - \frac{4}{9} - \frac{1}{9}\big) = 0.333$ |
: Cortes candidatos para una variable numérica. {#tbl-cortes-numericos}
Con dos variables numéricas, cada corte es una recta vertical u horizontal, y el árbol completo parte el plano en **rectángulos**, cada uno con la clase mayoritaria de los puntos que contiene. El laboratorio de este capítulo lo muestra con 60 puntos.
### Paso 5. Podar: el eslabón más débil
Un árbol que crece hasta tener hojas puras sobreajusta. En los datos del laboratorio, el árbol completo tiene 18 hojas, acierta el 100 % del entrenamiento y solo el 71.7 % de 300 puntos de prueba. CART propone crecer el árbol completo y después **podarlo**, cambiando cada subárbol por una hoja cuando la mejora que aporta no compensa su tamaño.
Para medir la mejora, sea $R(t) = \frac{n_t}{n} G_t$ la impureza ponderada del nodo $t$ si fuera una hoja, y $R(T_t)$ la suma de esa cantidad sobre las hojas del subárbol que cuelga de $t$. El **costo por hoja** de conservar el subárbol es
$$
g(t) = \frac{R(t) - R(T_t)}{|T_t| - 1},
$$
la reducción de impureza que aporta cada hoja adicional. Tomemos el árbol de cuatro hojas que aparece en la secuencia de poda del laboratorio:
| Nodo | Pregunta | Ejemplos (0/1) | $G_t$ | $R(t)$ | $R(T_t)$ | Hojas | $g(t)$ |
|---|---|---|---:|---:|---:|---:|---:|
| raíz | $x_2 \le 5.70$ | 33 / 27 | 0.495 | 0.495 | 0.224 | 4 | **0.090** |
| izquierdo | $x_1 \le 4.05$ | 15 / 19 | 0.493 | 0.279 | 0.164 | 2 | 0.115 |
| derecho | $x_1 \le 2.30$ | 18 / 8 | 0.426 | 0.185 | 0.060 | 2 | 0.125 |
: Costo por hoja de cada nodo interno del árbol de cuatro hojas. {#tbl-eslabon}
Por ejemplo, para la raíz, las cuatro hojas tienen 13, 21, 6 y 20 puntos con impurezas 0.260, 0.308, 0 y 0.180, así que $R(T) = \frac{13}{60}0.260 + \frac{21}{60}0.308 + 0 + \frac{20}{60}0.180 = 0.224$ y $g = (0.495 - 0.224)/3 = 0.090$.
El nodo con el menor $g(t)$ es el **eslabón más débil**: el primero que conviene podar cuando se penaliza el tamaño. Sorprendentemente, aquí es la raíz: con una penalización mayor que 0.090 por hoja, el árbol pasa de cuatro hojas a una sola. Ese valor de $g$ es el $\alpha$ al que ocurre la poda.
## Formalización
### Impureza y reducción de impureza
Para un nodo $t$ con $n_t$ ejemplos que una pregunta $s$ divide en $t_L$ y $t_R$, CART elige
$$
s^{*} = \arg\max_{s} \; \Delta G(s, t) = G_t - \frac{n_{t_L}}{n_t} G_{t_L} - \frac{n_{t_R}}{n_t} G_{t_R}.
$$ {#eq-reduccion-gini}
`scikit-learn` permite `criterion="gini"` o `"entropy"`; como se vio en el capítulo anterior, ambas son estrictamente cóncavas y casi siempre eligen la misma pregunta. Gini es algo más rápido de calcular porque no usa logaritmos.
**Atributos categóricos.** Un atributo con $m$ valores admite $2^{m-1} - 1$ preguntas binarias, número que explota con muchas categorías. Para dos clases, @breiman1984 demostraron un atajo: si se ordenan las categorías por su proporción de la clase positiva, la partición óptima es uno de los $m - 1$ cortes de ese orden. En el ejemplo, las proporciones de compra son joven 0.40, mayor 0.60 y adulto 1.00; los cortes del orden son {joven} | {mayor, adulto} y {joven, mayor} | {adulto}, y el segundo es el óptimo de la @tbl-preguntas-edad.
**Atributos numéricos.** Tras ordenar los $n_t$ valores, todos los cortes se evalúan en una pasada actualizando los conteos de cada lado. Una variable puede volver a usarse más abajo con otro corte. Como solo importa el orden de los valores, el árbol es **invariante a transformaciones monótonas** (logaritmos, escalamiento): no hace falta estandarizar.
### Importancia de las variables
Sumando, para cada variable, las reducciones de impureza ponderadas $\frac{n_t}{n}\Delta G$ de todos los nodos donde se usa, y normalizando para que sumen 1, se obtiene la **importancia por disminución de impureza** (MDI), el atributo `feature_importances_` de `scikit-learn`. Es gratuita, pero sesgada a favor de variables numéricas o con muchas categorías, que ofrecen más cortes posibles. La importancia por permutación y los valores SHAP, que se estudian con los ensambles, son alternativas más fiables.
### Poda de costo-complejidad
Para $\alpha \ge 0$ se define el costo-complejidad de un subárbol $T$ del árbol completo $T_0$:
$$
R_\alpha(T) = R(T) + \alpha\,|T|,
$$ {#eq-costo-complejidad}
donde $R(T)$ es la impureza ponderada total de sus hojas (en la formulación original de @breiman1984, la tasa de error) y $|T|$ su número de hojas. Con $\alpha = 0$ gana el árbol completo; al crecer $\alpha$, conviene ir cambiando subárboles por hojas. Un subárbol $T_t$ deja de compensar justo cuando $R(t) + \alpha = R(T_t) + \alpha|T_t|$, es decir, cuando $\alpha = g(t)$.
Breiman et al. demostraron que los árboles óptimos para cada $\alpha$ forman una **secuencia anidada** $T_0 \supset T_1 \supset \cdots \supset \{\text{raíz}\}$, que se obtiene podando repetidamente el eslabón más débil. Los valores $0 = \alpha_0 < \alpha_1 < \cdots$ en que cambia el árbol son la **ruta de poda**. Para elegir $\alpha$ se usa validación cruzada: se evalúa cada $\alpha$ de la ruta y se elige el de menor error (o el más grande dentro de un error estándar del mínimo).
### Árboles de regresión
Con una respuesta numérica, la hoja predice el promedio de sus ejemplos y la impureza es la varianza (o la suma de cuadrados): CART elige el corte que minimiza $\sum_{i \in t_L}(y_i - \bar{y}_L)^2 + \sum_{i \in t_R}(y_i - \bar{y}_R)^2$. Los árboles de regresión se desarrollan en el capítulo de ensambles, donde son la base del *bagging* y del *boosting*.
## Laboratorio: CART en el plano
El laboratorio entrena CART con 60 puntos de dos clases y lo evalúa con 300 puntos de prueba generados por la misma regla: la clase 1 es probable en dos zonas rectangulares (por eso un árbol es el modelo natural) y con un 12 % de etiquetas invertidas al azar. Con $\alpha = 0.02$ se obtiene el árbol de la @tbl-eslabon. Cuando dos cortes producen exactamente la misma reducción de Gini, el laboratorio elige el primero en un orden fijo, mientras que `scikit-learn` decide al azar; por eso el árbol completo puede diferir en algún corte profundo (exactitud en prueba 0.723 frente a 0.717), aunque los árboles podados coinciden.
::: {.content-visible when-format="html"}
```{=html}
<div class="plab" id="cart-lab">
<p class="plab-eyebrow">Laboratorio · CART</p>
<h3 class="plab-title">¿Cuánto hay que podar?</h3>
<p class="plab-sub">Las regiones sombreadas son las hojas del árbol. Limita su crecimiento con la profundidad o el mínimo por hoja, o pódalo con α. Observa la ruta de poda y la exactitud en prueba.</p>
<div class="plab-controls">
<label>Profundidad máxima: <b id="cart-d-v"></b><input type="range" id="cart-d" min="1" max="10" step="1" value="10"></label>
<label>Mínimo de ejemplos por hoja: <b id="cart-m-v"></b><input type="range" id="cart-m" min="1" max="12" step="1" value="1"></label>
<label>Penalización α: <b id="cart-a-v"></b><input type="range" id="cart-a" min="0" max="0.11" step="0.001" value="0"></label>
<div class="plab-field">Mostrar
<div class="plab-seg" id="cart-show"><button data-value="tr" aria-pressed="true">Entrenamiento</button><button data-value="te">Prueba</button></div>
</div>
</div>
<div class="plab-stats">
<div class="plab-stat"><span>Hojas</span><strong id="cart-leaves"></strong></div>
<div class="plab-stat"><span>Exactitud entrenamiento</span><strong id="cart-tr"></strong></div>
<div class="plab-stat"><span>Exactitud prueba</span><strong id="cart-te"></strong></div>
<div class="plab-stat"><span>Primer corte</span><strong id="cart-root"></strong></div>
</div>
<div class="plab-grid">
<div class="plab-panel">
<h4>Regiones del árbol</h4>
<svg id="cart-svg" role="img" aria-label="Partición rectangular del plano producida por el árbol, con los puntos de entrenamiento o prueba"></svg>
<div class="plab-legend"><span><i class="dot" style="background:var(--lab-s1)"></i>clase 1</span><span><i class="dot" style="background:var(--lab-s2)"></i>clase 0</span><span><i class="dot" style="background:transparent;border:2px solid var(--lab-text)"></i>mal clasificado</span><span><i style="background:var(--lab-s3)"></i>zonas reales</span></div>
</div>
<div class="plab-panel">
<h4>Ruta de poda</h4>
<svg id="cart-path" role="img" aria-label="Exactitud de entrenamiento y de prueba y número de hojas en función de alfa"></svg>
<div class="plab-legend"><span><i style="background:var(--lab-s1)"></i>entrenamiento</span><span><i style="background:var(--lab-s4)"></i>prueba</span><span><i style="background:var(--lab-axis)"></i>hojas (eje derecho)</span><span><i class="dot" style="background:var(--lab-s2)"></i>α actual</span></div>
</div>
</div>
<div class="plab-grid" style="margin-top:1rem">
<div class="plab-panel">
<h4>Búsqueda del primer corte</h4>
<svg id="cart-split" role="img" aria-label="Reducción de Gini en la raíz para cada umbral de x1 y de x2"></svg>
<div class="plab-legend"><span><i style="background:var(--lab-s1)"></i>cortes en x₁</span><span><i style="background:var(--lab-s5)"></i>cortes en x₂</span><span><i class="dot" style="background:var(--lab-s2)"></i>elegido</span></div>
</div>
<div class="plab-panel"><h4>Nodos internos y su costo por hoja g(t)</h4><div id="cart-tab" class="plab-scroll"></div></div>
</div>
<p class="plab-note" id="cart-note" aria-live="polite"></p>
</div>
<script>
(() => {
const K = window.LabKit, root = document.getElementById('cart-lab');
const $ = s => root.querySelector(s);
const D = {"tr":[[6.3,9.0,1],[7.8,2.3,1],[3.0,8.7,0],[0.1,8.2,1],[8.0,4.7,1],[3.0,2.8,0],[2.5,4.5,0],[5.0,5.5,1],[10.0,7.9,0],[6.2,9.9,0],[2.2,1.6,0],[6.1,0.4,1],[0.4,5.1,0],[4.7,9.2,0],[6.3,5.1,0],[5.0,2.5,1],[0.1,1.9,0],[6.9,2.0,1],[3.7,0.0,0],[8.3,1.5,0],[2.7,8.8,0],[5.1,8.5,0],[6.4,7.4,0],[0.9,5.4,0],[5.1,8.7,0],[3.6,6.0,0],[0.6,3.9,1],[3.2,1.5,0],[8.2,3.8,1],[9.8,5.9,0],[6.1,6.4,0],[6.8,1.5,1],[4.4,2.4,1],[4.0,1.0,0],[9.7,2.2,1],[6.7,3.0,0],[8.7,6.6,0],[1.3,8.5,1],[9.4,9.0,0],[5.7,1.5,1],[1.9,9.3,1],[5.5,1.8,0],[8.8,6.4,0],[5.7,3.8,1],[4.1,2.4,1],[0.4,8.8,1],[4.7,5.5,1],[3.2,7.5,0],[0.3,3.7,0],[0.3,1.2,0],[9.7,6.6,0],[4.3,5.2,1],[8.7,3.4,1],[5.9,6.8,0],[3.6,5.2,1],[7.7,9.1,0],[1.5,9.3,1],[0.1,7.5,1],[8.1,1.4,1],[4.2,8.2,1]],"te":[[5.4,3.4,1],[3.7,3.7,1],[9.9,6.3,0],[6.7,3.3,1],[6.8,1.2,1],[0.5,8.5,1],[0.1,9.8,1],[8.3,7.9,0],[0.5,2.1,0],[8.5,4.3,1],[6.3,1.2,1],[1.9,5.0,0],[7.6,5.4,1],[1.1,8.7,1],[1.4,4.4,0],[5.9,3.2,1],[4.2,8.9,0],[4.5,5.4,1],[9.7,2.8,0],[8.9,3.6,1],[3.1,8.8,0],[0.7,8.7,1],[8.3,8.9,0],[0.6,2.4,0],[7.8,4.4,1],[0.1,6.9,0],[8.0,9.7,0],[1.5,2.5,0],[8.4,0.9,1],[2.7,4.5,0],[4.2,5.4,1],[8.7,6.3,0],[6.5,5.9,1],[2.1,4.5,0],[9.9,9.1,0],[7.7,5.3,1],[8.7,6.3,0],[4.0,5.3,0],[2.0,5.7,0],[3.1,7.8,0],[8.3,9.9,0],[8.3,0.6,1],[7.2,8.1,0],[8.7,4.9,1],[4.8,5.3,1],[1.5,9.6,1],[9.9,2.6,0],[9.6,2.1,1],[0.8,7.3,0],[8.2,2.0,1],[9.5,4.0,0],[1.9,3.0,0],[1.1,7.7,1],[2.9,6.9,0],[4.6,1.2,1],[5.9,9.7,0],[1.8,9.6,1],[1.2,5.8,0],[1.7,10.0,1],[5.5,8.7,0],[7.1,4.5,1],[2.6,8.7,0],[8.9,7.8,1],[8.1,0.4,1],[2.2,5.5,0],[0.8,5.9,0],[9.5,3.5,1],[6.9,2.0,1],[8.0,9.0,0],[8.2,4.4,1],[3.5,0.8,1],[8.5,9.0,0],[5.6,7.6,0],[2.3,7.6,1],[2.5,7.3,0],[0.1,3.8,0],[2.4,0.4,0],[2.6,9.2,0],[3.0,1.2,0],[6.7,7.7,0],[3.4,9.2,0],[7.0,5.4,1],[9.0,8.1,0],[6.0,2.5,1],[5.2,6.2,0],[9.0,9.4,0],[0.6,5.3,0],[9.4,4.3,1],[7.5,2.3,0],[8.7,1.7,0],[2.6,2.5,1],[1.4,6.7,0],[8.4,1.6,1],[2.0,4.0,0],[3.0,6.4,0],[1.2,5.8,0],[5.0,1.2,1],[4.5,3.7,1],[0.7,3.3,1],[6.4,8.8,0],[9.5,7.4,1],[4.1,1.5,0],[6.8,5.7,1],[8.6,2.6,1],[6.6,0.9,1],[3.1,4.3,0],[8.1,3.5,1],[6.2,9.9,0],[9.5,4.8,0],[2.1,6.2,0],[7.9,5.9,1],[7.5,2.1,1],[2.9,1.6,0],[0.6,4.6,0],[7.7,3.7,1],[9.5,3.5,1],[3.8,8.5,0],[6.6,5.3,1],[2.4,0.9,0],[1.6,1.7,0],[4.2,3.3,1],[5.5,2.6,1],[6.8,9.5,0],[8.2,5.8,1],[2.6,7.9,1],[8.4,0.9,1],[8.5,9.2,0],[2.0,1.8,0],[7.3,4.8,1],[0.7,6.1,0],[8.3,7.3,0],[0.5,0.4,0],[5.2,6.6,0],[9.2,7.1,0],[6.8,8.8,0],[4.1,1.7,1],[1.1,3.6,0],[8.6,5.5,1],[0.4,9.1,1],[8.2,0.6,1],[7.9,1.8,1],[0.5,4.0,0],[7.1,2.7,1],[4.3,8.4,0],[1.2,6.4,0],[3.2,0.6,0],[7.6,6.7,0],[4.6,4.7,1],[3.9,6.8,0],[0.1,1.1,0],[0.9,1.1,0],[5.5,4.2,1],[6.6,5.2,1],[7.8,7.0,0],[0.3,0.9,0],[6.6,8.5,0],[8.3,0.9,1],[6.0,1.5,1],[3.0,8.5,0],[6.2,8.4,0],[6.0,9.0,0],[2.7,4.8,0],[9.4,7.6,0],[0.5,2.1,0],[3.7,6.4,1],[2.9,8.4,0],[8.1,4.2,0],[2.5,2.4,1],[2.3,5.0,0],[5.4,8.9,0],[5.0,5.6,1],[2.3,9.8,1],[4.8,1.5,1],[2.9,7.3,0],[2.7,9.9,0],[5.6,7.9,1],[6.0,6.9,0],[3.9,8.1,0],[8.1,8.0,0],[6.3,8.0,1],[8.0,2.3,1],[0.5,9.2,1],[3.9,0.8,0],[8.7,8.4,0],[6.1,1.0,1],[8.7,4.9,1],[0.9,1.8,0],[1.4,8.7,1],[8.9,9.3,0],[3.2,5.8,0],[6.2,3.6,1],[0.2,0.6,0],[0.8,4.5,0],[4.1,0.3,1],[0.6,3.8,0],[6.5,5.0,1],[7.5,5.6,1],[0.9,1.1,0],[3.6,6.0,0],[8.7,8.8,0],[9.6,8.6,0],[4.0,5.6,1],[0.6,9.4,1],[5.9,0.4,1],[8.9,7.2,0],[9.9,1.0,1],[4.2,1.1,1],[9.4,1.5,0],[7.2,6.8,0],[6.0,4.8,1],[4.3,8.0,0],[6.0,3.4,1],[7.3,6.7,0],[4.4,7.0,0],[1.7,4.3,0],[7.8,7.9,0],[7.5,10.0,0],[3.9,9.1,1],[7.8,7.6,0],[3.2,0.9,0],[7.7,0.6,1],[6.6,0.2,1],[1.9,4.4,0],[8.3,3.0,1],[9.2,9.4,0],[7.5,3.5,1],[4.5,9.7,0],[5.7,5.9,1],[5.7,1.6,1],[9.9,5.9,1],[6.8,8.1,0],[4.9,2.5,1],[9.4,1.8,1],[3.7,7.0,0],[4.4,5.9,1],[2.2,8.5,0],[8.6,4.4,1],[9.2,8.7,0],[6.5,2.3,1],[0.3,9.3,1],[9.3,6.1,0],[5.4,7.3,0],[4.0,3.2,0],[3.5,3.5,0],[5.4,0.1,1],[2.2,5.6,1],[8.9,5.2,1],[2.8,9.4,0],[8.6,2.7,1],[9.9,7.0,0],[2.6,6.1,0],[3.3,9.2,0],[6.1,0.5,1],[5.2,5.5,1],[8.2,9.4,0],[6.8,5.8,1],[5.3,1.7,1],[7.4,5.0,1],[4.0,7.5,0],[3.6,7.0,0],[3.3,1.9,1],[5.6,9.2,0],[1.9,1.5,0],[3.2,8.6,0],[4.6,3.8,1],[3.7,3.3,0],[5.5,4.1,1],[1.5,7.4,1],[1.5,5.5,0],[4.9,0.2,1],[2.5,8.0,0],[8.4,8.6,0],[1.4,7.5,0],[3.5,2.3,0],[1.9,8.8,1],[9.8,5.5,1],[9.4,1.4,0],[0.1,4.6,0],[8.9,5.1,1],[2.6,9.2,0],[6.4,3.5,1],[5.8,6.5,0],[0.8,5.5,0],[6.0,0.8,1],[1.8,0.3,0],[2.2,0.3,0],[5.9,8.2,0],[1.1,8.8,1],[2.4,0.0,0],[4.2,3.0,1],[3.6,3.2,0],[7.8,3.7,1],[8.4,6.8,0],[6.8,7.7,0],[1.7,9.3,1],[5.7,0.2,1],[2.0,9.1,1],[6.4,0.0,1],[1.0,9.6,1],[2.2,4.5,1]]}, TR = D.tr, TE = D.te, N = TR.length;
const gini = c => { const n = c[0] + c[1]; return n ? 1 - (c[0] / n) ** 2 - (c[1] / n) ** 2 : 0; };
const cnt = idx => { const c = [0, 0]; idx.forEach(i => c[TR[i][2]]++); return c; };
// Mejor división binaria de un nodo (como scikit-learn: cortes en puntos medios, mínimo por hoja).
function bestSplit(idx, minLeaf, curves) {
const c = cnt(idx), n = idx.length, g = gini(c); let best = null;
[0, 1].forEach(f => {
const s = [...idx].sort((a, b) => TR[a][f] - TR[b][f]), L = [0, 0];
for (let k = 0; k < n - 1; k++) {
L[TR[s[k]][2]]++;
const v = TR[s[k]][f], w = TR[s[k + 1]][f];
if (v === w || k + 1 < minLeaf || n - k - 1 < minLeaf) continue;
const R = [c[0] - L[0], c[1] - L[1]], dec = g - (k + 1) / n * gini(L) - (n - k - 1) / n * gini(R);
const thr = (v + w) / 2;
if (curves) curves[f].push([thr, dec]);
if (!best || dec > best.dec + 1e-12) best = { f, thr, dec };
}
});
return best;
}
function grow(idx, depth, maxD, minLeaf) {
const c = cnt(idx), node = { idx, c, n: idx.length, g: gini(c), cls: c[1] > c[0] ? 1 : 0 };
if (!c[0] || !c[1] || depth >= maxD || idx.length < 2 * minLeaf) return node;
const s = bestSplit(idx, minLeaf);
if (!s) return node;
node.f = s.f; node.thr = s.thr;
node.L = grow(idx.filter(i => TR[i][s.f] <= s.thr), depth + 1, maxD, minLeaf);
node.R = grow(idx.filter(i => TR[i][s.f] > s.thr), depth + 1, maxD, minLeaf);
return node;
}
const isLeaf = t => !t.L;
const leaves = t => isLeaf(t) ? [t] : [...leaves(t.L), ...leaves(t.R)];
const Rn = t => t.n / N * t.g;
const RT = t => K.sum(leaves(t).map(Rn));
const internals = t => isLeaf(t) ? [] : [t, ...internals(t.L), ...internals(t.R)];
const gfun = t => (Rn(t) - RT(t)) / (leaves(t).length - 1);
// Poda del eslabón más débil mientras g(t) ≤ α. Devuelve la ruta (α, hojas) si se pide.
function prune(tree, alpha, path) {
const copy = t => isLeaf(t) ? { ...t } : { ...t, L: copy(t.L), R: copy(t.R) };
const T = copy(tree);
if (path) path.push([0, leaves(T).length, copy(T)]);
for (;;) {
const ins = internals(T); if (!ins.length) break;
let w = ins[0], gw = gfun(w); ins.forEach(t => { const gt = gfun(t); if (gt < gw - 1e-12) { w = t; gw = gt; } });
if (!path && gw > alpha + 1e-12) break;
delete w.L; delete w.R; delete w.f; delete w.thr;
if (path) path.push([gw, leaves(T).length, copy(T)]);
}
return T;
}
const predict = (t, x) => { while (!isLeaf(t)) t = x[t.f] <= t.thr ? t.L : t.R; return t.cls; };
const acc = (t, set) => set.filter(p => predict(t, p) === p[2]).length / set.length;
const showSeg = K.segmented($('#cart-show'), render);
function render() {
const maxD = +$('#cart-d').value, minLeaf = +$('#cart-m').value, alpha = +$('#cart-a').value;
$('#cart-d-v').textContent = maxD === 10 ? 'sin límite' : maxD; $('#cart-m-v').textContent = minLeaf; $('#cart-a-v').textContent = K.fmt(alpha, 3);
const full = grow(TR.map((_, i) => i), 0, maxD === 10 ? 99 : maxD, minLeaf);
const path = []; prune(full, 0, path);
const T = prune(full, alpha);
const lv = leaves(T), aTr = acc(T, TR), aTe = acc(T, TE);
$('#cart-leaves').textContent = lv.length;
$('#cart-tr').textContent = K.fmt(aTr, 3); $('#cart-te').textContent = K.fmt(aTe, 3);
$('#cart-root').textContent = isLeaf(T) ? '—' : `x${T.f === 0 ? '₁' : '₂'} ≤ ${K.fmt(T.thr, 2)}`;
// Regiones: rectángulos de las hojas.
const c = K.chart($('#cart-svg'), { width: 380, height: 380, x: [0, 10], y: [0, 10], margin: { left: 40, right: 12, top: 12, bottom: 40 } });
(function boxes(t, b) {
if (isLeaf(t)) { const r = c.rect(b[0], b[2], b[1], b[3], t.cls ? 's1' : 's2'); r.style.fillOpacity = 0.17; r.style.stroke = 'var(--lab-bg)'; r.style.strokeWidth = 1.5; return; }
if (t.f === 0) { boxes(t.L, [b[0], t.thr, b[2], b[3]]); boxes(t.R, [t.thr, b[1], b[2], b[3]]); }
else { boxes(t.L, [b[0], b[1], b[2], t.thr]); boxes(t.R, [b[0], b[1], t.thr, b[3]]); }
})(T, [0, 10, 0, 10]);
c.axes({ xTicks: 5, yTicks: 5, xLabel: 'x₁', yLabel: 'x₂', grid: false, xFmt: v => K.fmt(v, 0) });
[[4, 10, 0, 6], [0, 2.5, 7, 10]].forEach(z => c.path([[z[0], z[2]], [z[1], z[2]], [z[1], z[3]], [z[0], z[3]], [z[0], z[2]]], 's3 line dash thin'));
const pts = showSeg.get() === 'tr' ? TR : TE;
pts.forEach(p => { const d = c.dot(p[0], p[1], pts === TR ? 4.5 : 3, (p[2] ? 's1' : 's2') + ' dot');
if (predict(T, p) !== p[2]) { d.style.stroke = 'var(--lab-text)'; d.style.strokeWidth = 1.8; } });
// Ruta de poda: exactitudes y hojas frente a α.
const amax = 0.11, steps = path.map(([a, n, t]) => ({ a, n, tr: acc(t, TR), te: acc(t, TE) }));
const stepPath = key => { const pts = []; steps.forEach((s, i) => { const a1 = i + 1 < steps.length ? Math.min(steps[i + 1].a, amax) : amax; if (s.a <= amax) { pts.push([s.a, s[key]], [a1, s[key]]); } }); return pts; };
const nmax = steps[0].n;
const cp = K.chart($('#cart-path'), { width: 380, height: 260, x: [0, amax], y: [0.4, 1.02], margin: { left: 44, right: 36, bottom: 40 } });
cp.axes({ xTicks: 5, yTicks: 6, xLabel: 'α', yLabel: 'exactitud', xFmt: v => K.fmt(v, 2), yFmt: v => K.fmt(v, 1) });
const leafPts = []; steps.forEach((s, i) => { const a1 = i + 1 < steps.length ? Math.min(steps[i + 1].a, amax) : amax; if (s.a <= amax) leafPts.push([s.a, 0.4 + 0.62 * s.n / nmax], [a1, 0.4 + 0.62 * s.n / nmax]); });
cp.path(leafPts, 'muted line thin');
cp.text(amax, 1.02, `${nmax} hojas`, { 'text-anchor': 'end', dy: -2 });
cp.path(stepPath('tr'), 's1 line'); cp.path(stepPath('te'), 's4 line');
cp.line(alpha, 0.4, alpha, 1.02, 'muted line dash thin');
cp.dot(alpha, aTe, 6, 's2 dot');
// Búsqueda del primer corte.
const curves = [[], []], rootBest = bestSplit(TR.map((_, i) => i), minLeaf, curves);
const ymax = Math.max(0.05, ...curves[0].map(p => p[1]), ...curves[1].map(p => p[1])) * 1.1;
const cs = K.chart($('#cart-split'), { width: 380, height: 220, x: [0, 10], y: [0, ymax], margin: { left: 48, bottom: 40 } });
cs.axes({ xTicks: 5, yTicks: 4, xLabel: 'umbral t', yLabel: 'reducción de Gini', xFmt: v => K.fmt(v, 0), yFmt: v => K.fmt(v, 2) });
cs.path(curves[0], 's1 line thin'); cs.path(curves[1], 's5 line thin');
if (rootBest) { cs.dot(rootBest.thr, rootBest.dec, 6, 's2 dot'); cs.text(rootBest.thr, rootBest.dec, `x${rootBest.f ? '₂' : '₁'} ≤ ${K.fmt(rootBest.thr, 2)}: ΔG = ${K.fmt(rootBest.dec, 3)}`, { dx: 8, dy: -6, class: 'strong' }); }
// Tabla de nodos internos.
const ins = internals(T);
$('#cart-tab').innerHTML = ins.length ? '<table class="plab-table"><thead><tr><th>Pregunta</th><th>n (0/1)</th><th>G</th><th>R(t)</th><th>R(Tₜ)</th><th>Hojas</th><th>g(t)</th></tr></thead><tbody>' +
(() => { let gmin = Math.min(...ins.map(gfun)); return ins.map(t => `<tr${Math.abs(gfun(t) - gmin) < 1e-12 ? ' class="hl"' : ''}><td>x${t.f ? '₂' : '₁'} ≤ ${K.fmt(t.thr, 2)}</td><td>${t.n} (${t.c[0]}/${t.c[1]})</td><td>${K.fmt(t.g, 3)}</td><td>${K.fmt(Rn(t), 3)}</td><td>${K.fmt(RT(t), 3)}</td><td>${leaves(t).length}</td><td>${K.fmt(gfun(t), 4)}</td></tr>`).join(''); })() +
'</tbody></table><p class="plab-sub" style="margin:.4rem 0 0">Resaltado: el eslabón más débil, el siguiente nodo que se podaría al subir α.</p>' : '<p class="plab-sub">El árbol es una sola hoja.</p>';
const best = steps.reduce((b, s) => s.te > b.te ? s : b, steps[0]), note = $('#cart-note');
if (lv.length > 12) { note.className = 'plab-note warn'; note.textContent = `Con ${lv.length} hojas el árbol acierta ${K.fmt(aTr, 3)} en entrenamiento pero ${K.fmt(aTe, 3)} en prueba: hay hojas diminutas alrededor de puntos con la etiqueta invertida. Sube α o el mínimo por hoja.`; }
else if (lv.length === 1) { note.className = 'plab-note bad'; note.textContent = 'El árbol se podó hasta la raíz: responde siempre la clase mayoritaria. α es demasiado grande.'; }
else { note.className = 'plab-note good'; note.textContent = `${lv.length} hojas: entrenamiento ${K.fmt(aTr, 3)}, prueba ${K.fmt(aTe, 3)}. En esta ruta, el mejor árbol en prueba tiene ${best.n} hojas (${K.fmt(best.te, 3)}); en la práctica α se elige con validación cruzada, no con la prueba.`; }
}
root.querySelectorAll('input[type=range]').forEach(el => el.addEventListener('input', render));
render();
})();
</script>
```
:::
::: {.content-visible when-format="pdf"}
En la versión web, este laboratorio entrena CART en el plano y lo poda con α. Valores de referencia: sin restricciones el árbol tiene 18 hojas, exactitud de entrenamiento 1 y de prueba cercana a 0.72; el primer corte es $x_2 \le 5.70$. La ruta de poda cambia de árbol en α ≈ 0.0141 (14 hojas), 0.0148 (12), 0.0152 (10), 0.0172 (7), 0.0188 (4 hojas, prueba 0.853) y 0.0902 (1 hoja). Con α entre 0.019 y 0.090 se obtiene el árbol de cuatro hojas de la @tbl-eslabon.
:::
**Guía del laboratorio.**
1. Sin restricciones, cuenta las hojas y localiza las que rodean a un solo punto. ¿Qué exactitud tienen en prueba?
2. En el panel de búsqueda del primer corte, comprueba que el máximo de la curva roja está en $x_2 = 5.70$. ¿Cuál es el mejor corte en $x_1$ y por qué pierde?
3. Predice antes de mover: ¿con qué α aparece el árbol de cuatro hojas? Compruébalo en la ruta de poda y verifica la @tbl-eslabon en la tabla de nodos.
4. Sube α poco a poco desde 0.02 hasta 0.09. ¿Por qué el árbol no cambia en todo ese intervalo y luego salta a una sola hoja?
5. Con α = 0, usa solo el mínimo por hoja (por ejemplo 5) o la profundidad máxima (2). ¿Llegas a un árbol parecido al podado? ¿Qué ventaja tiene la poda sobre la pre-poda?
6. Cambia a *Prueba* y compara las regiones con los rectángulos verdes (las zonas reales). ¿Qué parte de la estructura recuperó el árbol podado?
## Implementación en Python
### El ejemplo categórico
Las funciones siguientes enumeran todas las preguntas binarias de cada atributo y reproducen las @tbl-preguntas-edad y @tbl-mejores-preguntas.
```{python}
#| label: cart-laptop
import numpy as np
import pandas as pd
from itertools import combinations
laptop = pd.DataFrame({
"edad": ["joven", "joven", "adulto", "mayor", "mayor", "mayor", "adulto", "joven", "joven", "mayor", "joven", "adulto", "adulto", "mayor"],
"ingreso": ["alto", "alto", "alto", "medio", "bajo", "bajo", "bajo", "medio", "bajo", "medio", "medio", "medio", "alto", "medio"],
"estudiante": ["no", "no", "no", "no", "sí", "sí", "sí", "no", "sí", "sí", "sí", "no", "sí", "no"],
"crédito": ["favorable", "excelente", "favorable", "favorable", "favorable", "excelente", "excelente", "favorable", "favorable", "favorable", "excelente", "excelente", "favorable", "excelente"],
"compra": ["no", "no", "sí", "sí", "sí", "no", "sí", "no", "sí", "sí", "sí", "sí", "sí", "no"],
}, index=range(1, 15))
def gini(etiquetas):
p = pd.Series(etiquetas).value_counts(normalize=True)
return float(1 - (p ** 2).sum())
def preguntas_binarias(valores):
"""Todos los subconjuntos S con S y su complemento contados una sola vez."""
valores = sorted(valores)
vistos, salida = set(), []
for r in range(1, len(valores)):
for S in combinations(valores, r):
clave = frozenset([frozenset(S), frozenset(valores) - frozenset(S)])
if clave not in vistos:
vistos.add(clave)
salida.append(set(S))
return salida
def evaluar(datos, atributo, objetivo="compra"):
filas, g0, n = [], gini(datos[objetivo]), len(datos)
for S in preguntas_binarias(datos[atributo].unique()):
m = datos[atributo].isin(S)
gp = m.sum() / n * gini(datos.loc[m, objetivo]) + (~m).sum() / n * gini(datos.loc[~m, objetivo])
filas.append({"atributo": atributo, "pregunta": f"¿{atributo} ∈ {sorted(S)}?", "gini ponderado": gp, "reducción": g0 - gp})
return pd.DataFrame(filas)
print(f"Gini en la raíz: {gini(laptop['compra']):.3f}")
todas = pd.concat([evaluar(laptop, a) for a in ["edad", "ingreso", "estudiante", "crédito"]])
todas.sort_values("reducción", ascending=False).round(3).head(8)
```
`DecisionTreeClassifier` no acepta categorías directamente: hay que codificarlas. Con *one-hot*, cada pregunta es «¿categoría = valor?», que en este ejemplo basta para encontrar la misma raíz:
```{python}
#| label: cart-laptop-sklearn
from sklearn.tree import DecisionTreeClassifier, export_text
X_oh = pd.get_dummies(laptop.drop(columns="compra"), prefix_sep="=").astype(int)
arbol = DecisionTreeClassifier(criterion="gini", random_state=0).fit(X_oh, laptop["compra"])
print(export_text(arbol, feature_names=list(X_oh.columns)))
```
### El ejemplo numérico y la ruta de poda
Generamos los mismos datos del laboratorio: dos zonas rectangulares donde la clase 1 tiene probabilidad 0.88 y, fuera de ellas, 0.12.
```{python}
#| label: cart-datos
def zonas(X):
return ((X[:, 0] > 4) & (X[:, 1] < 6)) | ((X[:, 0] < 2.5) & (X[:, 1] > 7))
def generar(n, semilla):
rng = np.random.default_rng(semilla)
X = rng.uniform(0, 10, size=(n, 2)).round(1)
y = (rng.uniform(size=n) < np.where(zonas(X), 0.88, 0.12)).astype(int)
return X, y
X_tr, y_tr = generar(60, 7)
X_te, y_te = generar(300, 6)
completo = DecisionTreeClassifier(random_state=0).fit(X_tr, y_tr)
print(f"Árbol completo: {completo.get_n_leaves()} hojas, entrenamiento {completo.score(X_tr, y_tr):.3f}, prueba {completo.score(X_te, y_te):.3f}")
ruta = completo.cost_complexity_pruning_path(X_tr, y_tr)
filas = []
for a in ruta.ccp_alphas:
m = DecisionTreeClassifier(random_state=0, ccp_alpha=a).fit(X_tr, y_tr)
filas.append({"α": a, "hojas": m.get_n_leaves(), "entrenamiento": m.score(X_tr, y_tr), "prueba": m.score(X_te, y_te)})
pd.DataFrame(filas).round(4)
```
El árbol de cuatro hojas y el costo por hoja de sus nodos internos (@tbl-eslabon) se obtienen de la estructura interna `tree_`:
```{python}
#| label: cart-eslabon
podado = DecisionTreeClassifier(random_state=0, ccp_alpha=0.02).fit(X_tr, y_tr)
print(export_text(podado, feature_names=["x1", "x2"], show_weights=True))
t, n = podado.tree_, podado.tree_.n_node_samples[0]
def hojas(i):
return [i] if t.children_left[i] == -1 else hojas(t.children_left[i]) + hojas(t.children_right[i])
R = lambda i: t.n_node_samples[i] / n * t.impurity[i]
filas = []
for i in range(t.node_count):
if t.children_left[i] != -1:
h = hojas(i)
filas.append({"nodo": i, "pregunta": f"x{t.feature[i] + 1} <= {t.threshold[i]:.2f}", "G": t.impurity[i],
"R(t)": R(i), "R(T_t)": sum(R(j) for j in h), "hojas": len(h),
"g(t)": (R(i) - sum(R(j) for j in h)) / (len(h) - 1)})
pd.DataFrame(filas).round(4)
```
### Elegir α con validación cruzada
En la práctica no hay un conjunto de prueba grande para elegir α: se usa validación cruzada sobre el entrenamiento con los valores de la ruta.
```{python}
#| label: fig-cart-cv
#| fig-cap: "Exactitud de entrenamiento, de validación cruzada (10 pliegues estratificados, repetida 5 veces) y de prueba a lo largo de la ruta de poda. La validación cruzada favorece árboles pequeños; el árbol completo (α = 0) es el peor en prueba."
import plotly.graph_objects as go
from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=5, random_state=0)
alfas = ruta.ccp_alphas[:-1] # el último deja solo la raíz
cv_media = [cross_val_score(DecisionTreeClassifier(random_state=0, ccp_alpha=a), X_tr, y_tr, cv=cv).mean() for a in alfas]
tabla = pd.DataFrame([
{"α": a,
"entrenamiento": DecisionTreeClassifier(random_state=0, ccp_alpha=a).fit(X_tr, y_tr).score(X_tr, y_tr),
"validación cruzada": c,
"prueba": DecisionTreeClassifier(random_state=0, ccp_alpha=a).fit(X_tr, y_tr).score(X_te, y_te)}
for a, c in zip(alfas, cv_media)])
fig = go.Figure()
for col, color in [("entrenamiento", "#2563eb"), ("validación cruzada", "#ea580c"), ("prueba", "#7c3aed")]:
fig.add_trace(go.Scatter(x=tabla["α"], y=tabla[col], mode="lines+markers", name=col, line=dict(color=color, shape="hv")))
fig.update_layout(xaxis_title="α", yaxis_title="exactitud", height=360, margin=dict(l=10, r=10, t=20, b=40))
fig.show()
mejor = tabla.loc[tabla["validación cruzada"].idxmax()]
print(f"α elegido por validación cruzada: {mejor['α']:.4f} → prueba {mejor['prueba']:.3f}")
```
### Importancia de las variables
```{python}
#| label: cart-importancia
pd.DataFrame({"árbol completo": completo.feature_importances_, "árbol podado": podado.feature_importances_},
index=["x1", "x2"]).round(3)
```
En el árbol podado, $x_1$ acumula casi el 90 % de la importancia aunque la primera pregunta sea sobre $x_2$: ese primer corte reduce poco la impureza ponderada ($0.495 - 0.279 - 0.185 = 0.031$), porque solo separa las dos mitades del plano, y son los dos cortes en $x_1$ los que aíslan las zonas (0.115 y 0.125). La importancia mide cuánto contribuye cada variable, no qué pregunta va primero.
## Complejidad, ventajas y limitaciones
Con $p$ variables numéricas ordenadas una vez al inicio, encontrar el mejor corte de un nodo con $n_t$ ejemplos cuesta $O(p\, n_t)$; como cada nivel reparte los $n$ ejemplos, el árbol cuesta $O(p\, n \log n)$ si está balanceado (profundidad $\approx \log n$) y hasta $O(p\, n^2)$ en el peor caso. La ruta de poda se calcula después en tiempo proporcional al número de nodos.
**Ventajas.** Las del capítulo anterior (interpretabilidad, variables mixtas, sin escalamiento, interacciones automáticas), más un tratamiento uniforme de variables numéricas y categóricas, la poda con fundamento estadístico y la extensión directa a regresión.
**Limitaciones.**
- **Alta varianza**: pequeños cambios en los datos cambian los cortes de arriba y, con ellos, todo el árbol. Es el problema que resuelven los bosques aleatorios.
- Fronteras en escalones paralelos a los ejes: una frontera diagonal se aproxima con muchos cortes.
- Las categorías con muchos niveles generan muchas preguntas posibles y favorecen el sobreajuste; conviene agruparlas o usar codificaciones por frecuencia u objetivo.
- La importancia MDI está sesgada; para interpretar conviene usar permutación o SHAP.
## Resumen
- CART construye árboles **binarios**: preguntas «¿$A \in S$?» para categorías y «¿$x \le t$?» para números, elegidas por la mayor reducción del índice de Gini.
- Un atributo categórico con $m$ valores admite $2^{m-1} - 1$ preguntas; para dos clases basta ordenar las categorías por su proporción de positivos y probar $m - 1$ cortes.
- En el plano, un árbol es una partición en rectángulos. Crecido hasta hojas puras, rodea el ruido.
- La **poda de costo-complejidad** minimiza $R(T) + \alpha|T|$; el **eslabón más débil**, $\min_t g(t)$, define la ruta de poda, una secuencia anidada de árboles. $\alpha$ se elige con validación cruzada.
- La importancia por disminución de impureza es un subproducto útil pero sesgado.
[^gini-prob]: Si en un nodo hay una proporción $p_k$ de cada clase y se etiqueta un ejemplo al azar con la clase $k$ con probabilidad $p_k$, la probabilidad de acertar es $\sum_k p_k^2$ y la de equivocarse, $1 - \sum_k p_k^2$. Es también la varianza de la variable indicadora de clase sumada sobre las clases.
## Referencias {.unnumbered}
::: {#refs}
:::