Aprendizaje automático
Python
scikit-learn
machine learning
clasificación
cross-validation
principiantes

Tu Primer Modelo de Machine Learning con Scikit-learn

Guía paso a paso para construir tu primer modelo de ML: exploración de datos, preprocesamiento, validación cruzada, ajuste de hiperparámetros y evaluación con scikit-learn.

21 de mayo de 2026·6 min read

Todos empezamos en algún punto. Este artículo te lleva por el flujo completo de construir un modelo de machine learning desde cero — sin asumir experiencia previa en ML, solo Python y ganas de experimentar.

Al terminar habrás entrenado un clasificador, lo habrás validado correctamente, ajustado sus hiperparámetros, y entendido qué significan realmente las métricas. También verás por qué el enfoque clásico tiene límites, lo que prepara el terreno para las técnicas más avanzadas en los otros artículos de este sitio.

El Dataset

Usaremos el clásico dataset Titanic: predecir si un pasajero sobrevivió en base a características como edad, sexo, clase del ticket y tarifa. Es lo suficientemente pequeño para correr localmente en segundos y lo suficientemente grande para exponer desafíos reales de preprocesamiento.

python
import pandas as pd
from sklearn.datasets import fetch_openml

# Cargar Titanic desde OpenML
titanic = fetch_openml("titanic", version=1, as_frame=True)
df = titanic.frame
print(df.shape)        # (1309, 14)
print(df.dtypes)

Paso 1: Análisis Exploratorio

Antes de escribir una sola línea de código de modelo, entiende tus datos. Los valores faltantes, distribuciones sesgadas y el desbalance de clases afectan al modelo.

python
# Revisar valores faltantes
print(df.isnull().sum().sort_values(ascending=False))
# body       1188
# cabin      1014
# boat        823
# age         263
# fare          1

# Distribución del target
print(df['survived'].value_counts(normalize=True))
# 0    0.618  (no sobrevivió)
# 1    0.382  (sobrevivió)

# Resumen numérico
print(df[['age', 'fare', 'pclass']].describe())

Dos observaciones inmediatas: cabin tiene demasiados valores faltantes (mejor descartarla), y el dataset está desbalanceado (62% no sobrevivió — necesitamos la métrica correcta). Nota además que boat y body filtran el target: solo se conocen después del desastre, así que usarlas haría que el modelo se viera excelente y fuera inútil.

Paso 2: Preprocesamiento

Los datos crudos rara vez van directamente a un modelo. Necesitamos manejar valores faltantes, codificar variables categóricas y escalar las numéricas.

python
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OrdinalEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer

# Seleccionar y definir tipos de features
NUMERIC_FEATURES      = ['age', 'fare', 'sibsp', 'parch']
CATEGORICAL_FEATURES  = ['sex', 'embarked', 'pclass']

# Sub-pipelines por tipo
numeric_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler()),
])

categorical_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('encoder', OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1)),
])

# Combinar en un solo transformer
preprocessor = ColumnTransformer([
    ('num', numeric_pipeline, NUMERIC_FEATURES),
    ('cat', categorical_pipeline, CATEGORICAL_FEATURES),
])

Usar Pipeline y ColumnTransformer no es solo código limpio — previene data leakage al asegurar que el imputer y el scaler solo aprenden de los datos de entrenamiento, nunca del conjunto de test.

Paso 3: División de Datos

python
from sklearn.model_selection import train_test_split

X = df[NUMERIC_FEATURES + CATEGORICAL_FEATURES]
y = df['survived'].astype(int)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

print(f"Train: {X_train.shape[0]} muestras")
print(f"Test:  {X_test.shape[0]} muestras")

stratify=y asegura que ambos conjuntos tengan la misma proporción de clases — esencial con datasets desbalanceados.

Paso 4: Tu Primer Modelo

Un Árbol de Decisión es un buen punto de partida: es interpretable, no hace suposiciones distribucionales, y expone el sobreajuste claramente.

python
from sklearn.tree import DecisionTreeClassifier

# Pipeline completo: preprocesamiento + modelo
model = Pipeline([
    ('prep', preprocessor),
    ('clf', DecisionTreeClassifier(random_state=42)),
])

# Entrenar solo con datos de training
model.fit(X_train, y_train)

# Verificación rápida
train_acc = model.score(X_train, y_train)
test_acc  = model.score(X_test,  y_test)
print(f"Accuracy en train: {train_acc:.4f}")
print(f"Accuracy en test:  {test_acc:.4f}")
# Accuracy en train: 0.9809  ← sospechosamente alto
# Accuracy en test:  0.7710  ← mucho menor → sobreajuste

La gran brecha entre train y test es la señal clásica de sobreajuste. Un árbol de decisión sin restricciones memoriza los datos de entrenamiento.

Paso 5: Validación Cruzada

Una sola división train/test es frágil — podrías haber tenido buena (o mala) suerte con esa división. La validación cruzada da una estimación más confiable de la generalización.

python
from sklearn.model_selection import cross_val_score, StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

cv_scores = cross_val_score(
    model, X_train, y_train,
    cv=cv,
    scoring='f1',          # Mejor que accuracy para datos desbalanceados
)

print(f"F1 por fold: {cv_scores.round(3)}")
print(f"F1 medio:    {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")
# F1 por fold: [0.682 0.701 0.694 0.671 0.689]
# F1 medio:    0.6874 ± 0.0106

Usamos F1 (media armónica de precisión y recall) en vez de accuracy porque la accuracy en un dataset desbalanceado es engañosa — un modelo que siempre predice "no sobrevivió" tendría 61.8%.

Paso 6: Ajuste de Hiperparámetros con GridSearchCV

Ahora restringimos el árbol para combatir el sobreajuste. max_depth es el parámetro más importante.

python
from sklearn.model_selection import GridSearchCV

param_grid = {
    'clf__max_depth':        [3, 5, 7, 10, None],
    'clf__min_samples_leaf': [1, 5, 10, 20],
    'clf__criterion':        ['gini', 'entropy'],
}

grid_search = GridSearchCV(
    estimator=model,
    param_grid=param_grid,
    cv=cv,
    scoring='f1',
    n_jobs=-1,
    verbose=1,
)

grid_search.fit(X_train, y_train)

print(f"Mejores params: {grid_search.best_params_}")
print(f"Mejor CV F1:    {grid_search.best_score_:.4f}")
# Mejores params: {'clf__criterion': 'entropy', 'clf__max_depth': 5, 'clf__min_samples_leaf': 10}
# Mejor CV F1:    0.7231

El grid probó 5 × 4 × 2 = 40 combinaciones, cada una evaluada con 5-fold CV — 200 ajustes de modelo en total. Con n_jobs=-1 esto corre en paralelo en todos los núcleos.

Paso 7: Evaluar en el Conjunto de Test

El conjunto de test es tu estimación final y honesta del rendimiento. Lo miras una vez, después de tomar todas las decisiones de ajuste.

python
from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

best_model = grid_search.best_estimator_
y_pred = best_model.predict(X_test)

print(classification_report(y_test, y_pred, target_names=['No sobrevivió', 'Sobrevivió']))
text
               precision  recall  f1-score  support
No sobrevivió     0.82    0.87      0.84      161
Sobrevivió        0.76    0.68      0.72      101
accuracy                             0.80      262
macro avg         0.79    0.78      0.78      262
weighted avg      0.80    0.80      0.80      262
python
# Matriz de confusión
cm = confusion_matrix(y_test, y_pred)
disp = ConfusionMatrixDisplay(cm, display_labels=['No sobrevivió', 'Sobrevivió'])
disp.plot(cmap='Blues')
plt.tight_layout()
plt.savefig('matriz_confusion.png', dpi=150)

El modelo identifica correctamente el 68% de los sobrevivientes reales (recall=0.68). Para un caso de uso médico querrías mayor recall, incluso a costa de precisión.

Paso 8: Importancia de Features

Entender qué features impulsan las predicciones genera confianza en el modelo y guía la ingeniería de features futura.

python
import pandas as pd

# Acceder al árbol dentro del pipeline
tree = best_model.named_steps['clf']
feature_names = NUMERIC_FEATURES + CATEGORICAL_FEATURES

importance_df = (
    pd.DataFrame({'feature': feature_names, 'importance': tree.feature_importances_})
    .sort_values('importance', ascending=False)
)

print(importance_df)
#    feature  importance
# 1      sex    0.3812
# 0      age    0.2134
# 5   pclass    0.1876
# 2    sibsp    0.0943
# ...

Sexo, edad y clase dominan — consistente con los relatos históricos del desastre ("mujeres y niños primero").

Hacia Dónde Ir Desde Aquí

Ahora tienes un modelo funcional y correctamente validado. Pero GridSearchCV tiene una limitación fundamental: agota una grilla predefinida, lo que no escala bien cuando crece el número de parámetros. Una grilla de 5 parámetros con 5 valores cada uno son 5⁵ = 3125 ajustes.

Un enfoque más inteligente es usar algoritmos evolutivos para explorar el espacio de hiperparámetros — la misma idea detrás de sklearn-genetic-opt. Si quieres ver cómo se compara en la práctica, el artículo ¿Todavía Usas Grid Search para Optimización de Hiperparámetros? muestra ambos métodos lado a lado en el mismo dataset.


Sobre el autor

Rodrigo Arenas es arquitecto de software e ingeniero de machine learning en Medellín, Colombia. Construye productos, plataformas y software open source para IA, Data Engineering y Machine Learning — es el creador de Ciaren, sklearn-genetic-opt y PyWorkforce.



Rodrigo Arenas — ML Engineer & Open Source Maintainer

¿Estás trabajando en algo similar? Puedo ayudarte a llevarlo a producción.

¿Vas a construir una plataforma de IA o de datos?

Productos, plataformas y software open source para IA, Data Engineering y Machine Learning.

Ver proyectos
Visión

Construyendo un ecosistema de productos y software open source para IA, Data Engineering y Machine Learning.

Proyectos
sklearn-genetic-optPyWorkforceCiaren

Iterixs (En construcción)

© 2026 Rodrigo Arenas