Cuando empezamos en Machine Learning, suele parecer suficiente dividir los datos en dos partes:
Esa idea es correcta como punto de partida, pero se vuelve insuficiente cuando además queremos comparar modelos o ajustar parámetros. En ese caso, si miramos demasiadas veces el conjunto de prueba, terminamos adaptando nuestras decisiones a ese conjunto y dejamos de medir de forma objetiva.
La separación clásica en tres grupos ayuda a resolver ese problema:
La idea central es esta: el conjunto de prueba debe mantenerse “virgen” hasta el final. Si lo usamos antes, dejamos de tener una medida confiable del rendimiento real.
Imaginemos que estás preparando un examen:
Si usaras las preguntas exactas del examen real para decidir cómo prepararte, la nota final dejaría de ser una evaluación honesta. En Machine Learning pasa algo parecido.
Vamos a usar un clasificador KNN. Este algoritmo necesita un parámetro importante: n_neighbors, es decir, cuántos vecinos considerar. En lugar de elegirlo al azar, vamos a probar varios valores y usar el conjunto de validación para decidir.
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
datos = pd.DataFrame({
"horas_estudio": [1, 2, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10],
"practicas": [0, 1, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6],
"aprobo": [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
})
X = datos[["horas_estudio", "practicas"]]
y = datos["aprobo"]
# 1) Separación inicial: entrenamiento+validación / prueba
X_temp, X_test, y_temp, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y
)
# 2) Separación interna: entrenamiento / validación
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp, test_size=0.33, random_state=42, stratify=y_temp
)
print("Entrenamiento:", X_train.shape, y_train.shape)
print("Validación:", X_val.shape, y_val.shape)
print("Prueba:", X_test.shape, y_test.shape)
mejor_k = None
mejor_score = -1
for k in [1, 3, 5]:
modelo = KNeighborsClassifier(n_neighbors=k)
modelo.fit(X_train, y_train)
y_val_pred = modelo.predict(X_val)
score = accuracy_score(y_val, y_val_pred)
print(f"Exactitud con k={k} en validación: {score}")
if score > mejor_score:
mejor_score = score
mejor_k = k
print("\nMejor valor de k:", mejor_k)
# 3) Evaluación final con el mejor k sobre prueba
modelo_final = KNeighborsClassifier(n_neighbors=mejor_k)
modelo_final.fit(X_train, y_train)
y_test_pred = modelo_final.predict(X_test)
print("Exactitud final en prueba:", accuracy_score(y_test, y_test_pred))
nuevo_estudiante = pd.DataFrame({
"horas_estudio": [7],
"practicas": [4]
})
prediccion = modelo_final.predict(nuevo_estudiante)[0]
print("Predicción para el nuevo estudiante:", prediccion)
Salida resumida esperada:
Entrenamiento: ...
Validación: ...
Prueba: ...
Exactitud con k=1 en validación: ...
Exactitud con k=3 en validación: ...
Exactitud con k=5 en validación: ...
Mejor valor de k: ...
Exactitud final en prueba: ...
Predicción para el nuevo estudiante: 1
El flujo del programa sigue una lógica muy importante:
k usando solo validación;Eso evita elegir el modelo mirando directamente el rendimiento en prueba.
X_temp, X_test, y_temp, y_test: crea una primera división donde el conjunto de prueba queda reservado.X_train, X_val, y_train, y_val: divide el bloque restante en entrenamiento y validación.for k in [1, 3, 5]: recorre distintas configuraciones del algoritmo.accuracy_score(y_val, y_val_pred): mide cuál configuración funciona mejor en validación.mejor_k: guarda la opción que logró el mejor resultado.modelo_final: entrena el modelo con el valor elegido y lo evalúa finalmente en prueba.El conjunto de validación no existe para “decorar” el flujo. Existe para que podamos tomar decisiones sin contaminar la evaluación final.
Si cambias el modelo, los parámetros, las variables o el preprocesamiento mirando el conjunto de prueba, ya no estás evaluando: estás optimizando contra ese conjunto. En consecuencia, el valor final deja de representar cómo responderá el modelo frente a datos realmente nuevos.
La separación en entrenamiento, validación y prueba es muy útil cuando:
En datasets pequeños, a veces esta división deja muy pocos datos para aprender. En esos casos, suele usarse validación cruzada, que veremos más adelante.