Machine Learning con Python y scikit-learn
scikit-learn es la biblioteca de machine learning más utilizada en Python. Ofrece una API consistente para preprocesamiento, entrenamiento, evaluación y despliegue de modelos, integrándose perfectamente con NumPy y pandas.
Instalación
pip install scikit-learn
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.preprocessing import StandardScaler, LabelEncoder, OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report, confusion_matrix, mean_squared_error
Flujo de trabajo típico
Datos brutos → Preprocesamiento → División train/test → Modelo → Evaluación → Ajuste
Cargar y preparar datos
from sklearn.datasets import load_breast_cancer, load_boston, fetch_california_housing
import pandas as pd
# Dataset de ejemplo: clasificación binaria (cáncer de mama)
data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = pd.Series(data.target, name='target') # 0=maligno, 1=benigno
print(f"Muestras: {X.shape[0]}, Características: {X.shape[1]}")
print(f"Distribución de clases: {y.value_counts().to_dict()}")
# División train/test
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2, # 20% para test
random_state=42, # reproducibilidad
stratify=y # mantener proporción de clases
)
print(f"Train: {len(X_train)} | Test: {len(X_test)}")
Preprocesamiento
from sklearn.preprocessing import (
StandardScaler, MinMaxScaler, RobustScaler,
LabelEncoder, OneHotEncoder,
PolynomialFeatures
)
from sklearn.impute import SimpleImputer
# Escalado numérico
scaler = StandardScaler() # media=0, std=1 — para SVM, regresión logística
# MinMaxScaler() # rango [0,1] — para redes neuronales
# RobustScaler() # robusto a outliers (usa mediana/IQR)
X_train_scaled = scaler.fit_transform(X_train) # fit + transform en train
X_test_scaled = scaler.transform(X_test) # solo transform en test (¡nunca fit!)
# Imputación de valores faltantes
imputer = SimpleImputer(strategy='median') # opciones: mean, median, most_frequent, constant
X_imputed = imputer.fit_transform(X_train)
# Encoding de variables categóricas
df_cat = pd.DataFrame({'color': ['rojo', 'azul', 'verde', 'azul']})
# LabelEncoder — para target (enteros ordinales)
le = LabelEncoder()
y_encoded = le.fit_transform(df_cat['color'])
# OneHotEncoder — para features categóricas
ohe = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
X_encoded = ohe.fit_transform(df_cat[['color']])
print(ohe.get_feature_names_out()) # ['color_azul', 'color_rojo', 'color_verde']
Clasificación
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report, roc_auc_score
# Regresión logística (buena baseline)
log_reg = LogisticRegression(max_iter=1000, random_state=42)
log_reg.fit(X_train_scaled, y_train)
y_pred = log_reg.predict(X_test_scaled)
print(classification_report(y_test, y_pred, target_names=data.target_names))
# Random Forest — robusto, maneja valores faltantes, importancia de features
rf = RandomForestClassifier(
n_estimators=100,
max_depth=10,
min_samples_split=5,
random_state=42,
n_jobs=-1, # usar todos los núcleos
)
rf.fit(X_train, y_train) # RF no necesita escalado
y_pred_rf = rf.predict(X_test)
print(f"Random Forest accuracy: {rf.score(X_test, y_test):.3f}")
print(f"AUC-ROC: {roc_auc_score(y_test, rf.predict_proba(X_test)[:,1]):.3f}")
# Importancia de características
importancias = pd.Series(rf.feature_importances_, index=data.feature_names)
print(importancias.sort_values(ascending=False).head(5))
Regresión
from sklearn.linear_model import Ridge, Lasso, ElasticNet
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_squared_error, r2_score
import numpy as np
# Dataset de regresión: precios de casas en California
housing = fetch_california_housing()
X_r = pd.DataFrame(housing.data, columns=housing.feature_names)
y_r = housing.target # precio medio en cientos de miles
X_r_train, X_r_test, y_r_train, y_r_test = train_test_split(
X_r, y_r, test_size=0.2, random_state=42
)
# Ridge (regularización L2)
ridge = Ridge(alpha=1.0)
ridge.fit(X_r_train, y_r_train)
y_r_pred = ridge.predict(X_r_test)
print(f"Ridge R²: {r2_score(y_r_test, y_r_pred):.3f}")
print(f"Ridge RMSE: {np.sqrt(mean_squared_error(y_r_test, y_r_pred)):.3f}")
# Lasso (regularización L1, selección de features)
lasso = Lasso(alpha=0.01)
lasso.fit(X_r_train, y_r_train)
coefs_no_cero = (lasso.coef_ != 0).sum()
print(f"Lasso: {coefs_no_cero}/{X_r.shape[1]} features seleccionadas")
# Gradient Boosting — generalmente el mejor resultado para tabular
gb = GradientBoostingRegressor(
n_estimators=200,
learning_rate=0.05,
max_depth=4,
random_state=42,
)
gb.fit(X_r_train, y_r_train)
print(f"GBM R²: {r2_score(y_r_test, gb.predict(X_r_test)):.3f}")
Pipeline — preprocesamiento + modelo encadenado
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
# Supongamos un DataFrame con features mixtas
from sklearn.datasets import fetch_openml
# Pipeline básico (features numéricas)
pipeline = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler()),
('clf', RandomForestClassifier(n_estimators=100, random_state=42)),
])
pipeline.fit(X_train, y_train)
score = pipeline.score(X_test, y_test)
print(f"Pipeline accuracy: {score:.3f}")
# ColumnTransformer — transformaciones distintas por tipo de columna
numeric_features = ['edad', 'ingresos', 'antiguedad']
categorical_features = ['ciudad', 'categoria']
preprocessor = ColumnTransformer(transformers=[
('num', Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler()),
]), numeric_features),
('cat', Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('ohe', OneHotEncoder(handle_unknown='ignore')),
]), categorical_features),
])
pipeline_completo = Pipeline(steps=[
('prep', preprocessor),
('clf', GradientBoostingClassifier(random_state=42)),
])
Evaluación y validación cruzada
from sklearn.model_selection import cross_val_score, StratifiedKFold
# cross_val_score — k-fold automático
cv_scores = cross_val_score(
rf, X, y,
cv=5, # 5-fold
scoring='f1', # métricas: accuracy, f1, roc_auc, r2, neg_rmse...
n_jobs=-1,
)
print(f"F1 medio: {cv_scores.mean():.3f} ± {cv_scores.std():.3f}")
# StratifiedKFold para clasificación con clases desbalanceadas
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)):
X_fold_train, X_fold_val = X.iloc[train_idx], X.iloc[val_idx]
y_fold_train, y_fold_val = y.iloc[train_idx], y.iloc[val_idx]
rf.fit(X_fold_train, y_fold_train)
score = rf.score(X_fold_val, y_fold_val)
print(f" Fold {fold+1}: {score:.3f}")
# Matriz de confusión
from sklearn.metrics import ConfusionMatrixDisplay
import matplotlib.pyplot as plt
cm = confusion_matrix(y_test, y_pred_rf)
disp = ConfusionMatrixDisplay(cm, display_labels=data.target_names)
disp.plot()
plt.tight_layout()
plt.savefig('confusion_matrix.png', dpi=120)
Búsqueda de hiperparámetros
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from scipy.stats import randint, uniform
# GridSearchCV — búsqueda exhaustiva
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [None, 5, 10],
'min_samples_split': [2, 5, 10],
}
grid_search = GridSearchCV(
RandomForestClassifier(random_state=42),
param_grid,
cv=5,
scoring='f1',
n_jobs=-1,
verbose=1,
)
grid_search.fit(X_train, y_train)
print(f"Mejores parámetros: {grid_search.best_params_}")
print(f"Mejor F1: {grid_search.best_score_:.3f}")
# RandomizedSearchCV — más eficiente para espacios grandes
param_dist = {
'n_estimators': randint(50, 500),
'max_depth': randint(3, 20),
'learning_rate': uniform(0.01, 0.3),
}
rand_search = RandomizedSearchCV(
GradientBoostingClassifier(random_state=42),
param_dist,
n_iter=30, # número de combinaciones a probar
cv=5,
scoring='roc_auc',
random_state=42,
n_jobs=-1,
)
rand_search.fit(X_train, y_train)
print(f"Mejores parámetros: {rand_search.best_params_}")
Clustering
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt
# Generar datos de ejemplo
from sklearn.datasets import make_blobs
X_cl, y_true = make_blobs(n_samples=300, centers=4, cluster_std=0.8, random_state=0)
# KMeans — clustering por partición
kmeans = KMeans(n_clusters=4, random_state=42, n_init='auto')
labels = kmeans.fit_predict(X_cl)
print(f"Silhouette Score: {silhouette_score(X_cl, labels):.3f}") # [-1,1], >0.5 bueno
# Encontrar número óptimo de clusters (método del codo)
inercias = [KMeans(n_clusters=k, random_state=42, n_init='auto').fit(X_cl).inertia_
for k in range(2, 11)]
plt.plot(range(2, 11), inercias, 'bo-')
plt.xlabel('k')
plt.ylabel('Inercia')
plt.title('Método del codo')
plt.savefig('elbow.png', dpi=120)
# DBSCAN — clusters de forma arbitraria, robusto a outliers
dbscan = DBSCAN(eps=0.5, min_samples=5)
labels_db = dbscan.fit_predict(X_cl)
n_clusters = len(set(labels_db)) - (1 if -1 in labels_db else 0)
n_outliers = (labels_db == -1).sum()
print(f"DBSCAN: {n_clusters} clusters, {n_outliers} outliers")
Guardar y cargar modelos
import joblib # más eficiente que pickle para NumPy/sklearn
# Guardar pipeline completo
joblib.dump(pipeline, 'modelo_cancer.pkl', compress=3)
# Cargar y predecir
modelo = joblib.load('modelo_cancer.pkl')
nuevas_muestras = X_test.iloc[:5]
predicciones = modelo.predict(nuevas_muestras)
probabilidades = modelo.predict_proba(nuevas_muestras)[:, 1]
print("Predicciones:", predicciones)
print("Probabilidades:", probabilidades.round(3))
Buenas prácticas
- Siempre aplica
fit_transformen train y solotransformen test — hacerfiten test filtra información del futuro (data leakage). - Usa Pipelines para encadenar preprocesamiento y modelo: garantiza que la misma transformación se aplica en train y producción.
- Cross-validation sobre train set, nunca uses el test set para seleccionar hiperparámetros — guárdalo solo para la evaluación final.
random_state=42en todo lo que tenga aleatoriedad para reproducibilidad.- Empieza con modelos simples (regresión logística, árbol de decisión) como baseline antes de pasar a GBM o redes neuronales.
n_jobs=-1en Random Forest y GridSearchCV para usar todos los núcleos del procesador automáticamente.
Conversiones relacionadas
Conversiones frecuentes del catálogo: