Introducción y Contexto
Presentación de la materia, el equipo docente, qué es la Ciencia de Datos y por qué Machine Learning ahora.
Equipo docente
Juan M. Rodríguez
Dr. Ing. — responsable de la cátedra.
Azul Villanueva
Coordinación de trabajos prácticos.
Mateo Suster · Franco Mastelli
Clases teóricas y prácticas.
La ciencia de datos no es un campo aislado — convive y se superpone con la estadística clásica, el datamining, el machine learning y la inteligencia artificial. Entender dónde termina uno y empieza el otro ayuda a elegir la herramienta correcta para cada problema.
El ecosistema de modelos
Regresión lineal · Análisis discriminante · PCA
Métodos clásicos basados en fórmulas y estadística (desde el 1700). Interpretables y reproducibles.
Id3 · K-Means · Bayes Naive
Algoritmos compartidos entre datamining y machine learning. Son la base de muchos sistemas modernos.
Redes Neuronales · SVM
Aprenden patrones complejos a partir de datos, sin reglas programadas explícitamente. Requieren entrenamiento.
Clave conceptual: los modelos de Datamining usan fórmulas y estadística. Los de Machine Learning usan entrenamiento. Muchos modelos (K-Means, Bayes) son usados por ambos campos.
Taxonomía del Machine Learning Clásico
Todos los modelos de ML clásico se dividen en dos grandes grupos según si los datos tienen etiquetas o no:
Clasificación: predecir una categoría. Ej: "¿de qué color es el calcetín?" → divide por color.
Regresión: predecir un número. Ej: "¿qué largo tiene la corbata?" → divide por largo.
Clustering: agrupar por similitud. Ej: separar ropa similar en pilas.
Association: encontrar dependencias ocultas. Ej: qué ropa suelo usar junta.
Dimensionality Reduction: generalizar. Ej: hacer los mejores outfits con la ropa disponible.
La ciencia de datos combina estadística, programación y conocimiento del dominio para extraer conocimiento útil de los datos. El punto de partida es siempre entender qué tipo de datos se tienen y qué tipo de problema se quiere resolver.
Tipos de problemas
Clasificación
El modelo predice a cuál categoría pertenece una observación. Las categorías son finitas y conocidas de antemano.
Regresión
El modelo predice un valor continuo para ciertos datos de entrada. Ej: precio de una casa, temperatura.
Agrupamiento (Clustering)
No hay etiquetas. El modelo descubre grupos naturales en los datos por características similares.
Definiciones
Aprendizaje desde datos
Es la ciencia (y el arte) de programar computadoras para que aprendan a partir de datos.
Experiencia + Tarea + Rendimiento
Un programa aprende de la experiencia E respecto de una tarea T y una medida R, si su rendimiento en T medido por R mejora con la experiencia E.
Sin programación explícita
Campo de estudio que da a las computadoras la capacidad de aprender sin ser programadas de manera explícita.
Herramientas — Pandas y EDA
Manipulación de datos con Pandas, exploración inicial, filtrado, agrupamiento y análisis exploratorio.
Todo modelo trabaja con variables de entrada (independientes) y variables de salida (dependientes). Entender el tipo de cada variable define qué técnicas se pueden aplicar.
Taxonomía según nivel de medición
| Tipo | Subtipo | Característica clave | Ejemplos |
|---|---|---|---|
| Categórica (cualitativa) | Nominal | Las modalidades solo se distinguen por ser diferentes. No hay orden posible entre ellas. | Color de cabello, tipo de auto, sexo |
| Ordinal | Se pueden ordenar las modalidades, pero no medir la distancia entre ellas. | Calificación (A, B, C, D, E), estadío de enfermedad (I, II, III, IV) | |
| Cuantitativa | Discreta | Entre dos valores consecutivos no hay valores intermedios. Asociada al proceso de contar. | Cantidad de hijos, materias aprobadas |
| Continua | Entre dos valores cualesquiera existen infinitos valores intermedios. Asociada al proceso de medir. | Peso, edad, temperatura |
La estadística descriptiva nos permite resumir y entender un conjunto de datos de forma numérica antes de construir cualquier modelo.
Medidas de posición (centro)
| Medida | Definición | Nota |
|---|---|---|
| Media (μ̂) | Promedio aritmético. | Sensible a outliers. |
| Mediana (Q2) | Divide la población en dos mitades. | Robusta ante valores extremos. |
| Q1 | Deja el 25% de los datos por debajo. | Primer cuartil. |
| Q3 | Deja el 75% de los datos por debajo. | Tercer cuartil. |
| IQR | Q3 − Q1 | Robusto ante outliers. |
Varianza y Desviación Estándar
Miden la dispersión de los datos. La varianza muestral usa n−1 (corrección de Bessel) para dar un estimador más preciso de la varianza poblacional.
Correlación de Pearson
Mide la relación lineal entre dos variables. Siempre está entre −1 y 1.
¡Atención! Correlación NO implica causalidad. Que dos variables tengan alto índice de correlación no significa que una cause a la otra.
Pandas (Panel Data System) es la biblioteca principal para manipulación de datos tabulares en Python. Opera de forma vectorizada sobre arrays de NumPy.
Series
Array unidimensional etiquetado. Homogéneo. Tamaño inmutable, contenido mutable.
DataFrame
Estructura bidimensional. Cada columna es una Series. Análogo a una tabla SQL.
El EDA es el proceso de investigar y resumir las características principales de un dataset. Su objetivo es entender la estructura de los datos, detectar anomalías y formular hipótesis antes de modelar.
Flujo típico de EDA
- Cargar y explorar:
info(),shape,head(),describe() - Detectar valores faltantes:
isna().sum() - Detectar duplicados:
duplicated().any() - Análisis univariado: distribuciones,
value_counts - Análisis bivariado: correlaciones,
groupby,crosstab - Visualización: histogramas, boxplots, scatter plots
Acceso por posición y nombre
df.iloc[0] # fila 0 por posición df.iloc[0, 2] # fila 0, columna 2 por posición df.loc[1, 'apellido'] # fila índice 1, columna 'apellido' df.iloc[:, [3, 8]] # todas las filas, columnas 3 y 8
Boolean Indexing — Máscara booleana
# Máscara simple df_cultura = df[df['reparticion'] == 'Ministerio de Cultura'] # Múltiples condiciones (& = AND, | = OR, ~ = NOT) df[(df['mes'] == 2) & (df['reparticion'] == 'SECR Ciencia')]
Funciones de agregación
df['mes'].max()
df['salario'].mean()
df.groupby('reparticion')['salario'].mean()
df.sort_values('salario', ascending=False)
df_tracks = pd.read_csv('tracks.csv')
df_tracks.info()
df_tracks.describe()
# Faltantes y duplicados
df_tracks.isna().sum()
df_tracks.dropna()
df_tracks.drop_duplicates()
# Correlación de Pearson
from scipy.stats import pearsonr
corr, _ = pearsonr(df_1['popularity'], df_1['danceability'])
# Matriz de correlación completa
import seaborn as sns
matriz_corr = df_tracks.select_dtypes(include=np.number).corr(method='pearson')
sns.heatmap(matriz_corr, annot=True, cmap='coolwarm')
Visualización y Falacias
Tipos de gráficos, cuándo usar cada uno, y errores frecuentes al interpretar datos.
La visualización no es decoración: es una herramienta de análisis y comunicación. El Datasaurus demuestra que conjuntos de datos radicalmente distintos pueden tener exactamente los mismos estadísticos (media, desviación, correlación).
Conclusión: los estadísticos resumen son insuficientes. Siempre visualizar los datos antes de sacar conclusiones.
Incluso con datos reales y correctos se pueden sacar conclusiones completamente equivocadas.
Paradoja de Simpson
Una tendencia global puede invertirse o desaparecer al segmentar por subgrupos. Ocurre cuando hay una variable confundidora no controlada. Ejemplo: un tratamiento puede parecer peor globalmente, pero al segmentar por tamaño del cálculo renal, resulta ser mejor en cada subgrupo.
Regla práctica: antes de sacar conclusiones de datos agregados, siempre preguntarse si hay subgrupos relevantes que puedan invertir el resultado.
import seaborn as sns import matplotlib.pyplot as plt # Barras sns.countplot(x='species', data=iris) # Histograma sns.histplot(data=df['Age'], alpha=0.5) # Scatter con colores por clase sns.scatterplot(x='petal_length', y='petal_width', hue='species', data=iris) # Boxplot sns.boxplot(x='species', y='sepal_length', data=iris) # Pairplot completo (EDA inicial) sns.pairplot(iris, hue='species', height=2) # Heatmap de correlación sns.heatmap(publicidad.corr(), annot=True)
Regresión
Regresión lineal simple y múltiple, métricas de error (MSE, RMSE, MAE) y covarianza/correlación.
En regresión se busca predecir un valor continuo dado ciertos valores de entrada. Ejemplos: temperatura de mañana, valor de una propiedad, ventas del próximo mes.
Métricas de regresión
| Métrica | Fórmula | Característica |
|---|---|---|
| RMSE | √( (1/m) · Σ(ŷᵢ − yᵢ)² ) | Mismas unidades que y. Penaliza errores grandes. Sensible a outliers. |
| MAE | (1/m) · Σ|ŷᵢ − yᵢ| | Robusto ante outliers. Penaliza todos los errores por igual. |
| MSE | (1/m) · Σ(ŷᵢ − yᵢ)² | Base del RMSE. Usado internamente en el entrenamiento. |
from sklearn.linear_model import LinearRegression
from sklearn import metrics
modelo_tv = LinearRegression()
tv = publicidad['tv'].values.reshape(-1, 1)
ventas = publicidad['ventas'].values.reshape(-1, 1)
modelo_tv.fit(tv, ventas)
B0 = round(modelo_tv.intercept_[0], 2) # intercepto
B1 = round(modelo_tv.coef_[0][0], 2) # pendiente
print(f"ventas = {B1} * tv + {B0}")
Interpretación: β₀ = ventas promedio cuando no se invierte en TV. β₁ = incremento en ventas por cada millón adicional invertido en TV.
Clasificación y Métricas
Clasificación binaria y múltiple, matriz de confusión, precisión, recall, F1, curva ROC y AUC. Con ejemplos del práctico.
Se busca determinar a cuál categoría del conjunto C pertenece la observación. Las categorías son finitas y conocidas de antemano. El modelo trabaja con un dataset etiquetado (supervisado): cada fila tiene un valor real conocido que el modelo aprende a predecir.
Diferencia con Regresión: en clasificación el resultado es una categoría (spam / no spam, perro / gato / pájaro). En regresión el resultado es un número continuo (precio, temperatura).
Cuando creamos distintos modelos de clasificación nos interesa conocer si el modelo está clasificando correctamente lo que queremos. La matriz de confusión es la herramienta base para entender los errores del modelo.
Trabajamos con modelos de clasificación supervisada: el modelo predice una categoría y la data está pre-categorizada (labeled dataset). Es muy importante tener bien claro qué es lo que estamos midiendo con cada métrica, ya que distintas métricas miden cosas distintas.
Los 4 componentes
| Sigla | Nombre | Descripción |
|---|---|---|
| TP | True Positive | Cantidad de clasificaciones positivas correctas. El modelo predijo positivo y era positivo. ✓ |
| TN | True Negative | Cantidad de clasificaciones negativas correctas. El modelo predijo negativo y era negativo. ✓ |
| FP | False Positive | Cantidad de clasificaciones positivas incorrectas. El modelo predijo positivo pero era negativo. ✗ |
| FN | False Negative | Cantidad de clasificaciones negativas incorrectas. El modelo predijo negativo pero era positivo. ✗ |
Objetivo: queremos que la diagonal principal (TP y TN) sea lo más alta posible — significa que el modelo está acertando. Los cuadros fuera de la diagonal son los errores.
Fórmulas de todas las métricas
Ejemplo numérico del práctico
Supongamos esta matriz de confusión:
Precision = TP/(TP+FP) = 2/(2+1) = 0.667
Recall = TPR = TP/(TP+FN) = 2/(2+1) = 0.667
FPR = FP/(FP+TN) = 1/(1+1) = 0.5
¿Cuándo usar Precisión vs Recall?
| Objetivo | Métrica a maximizar | Ejemplo |
|---|---|---|
| Quiero que lo que predigo positivo sea correcto | Precisión | "Los partidos que apuesto, ganar" |
| Quiero capturar todos los positivos reales | Recall | "No quiero perderme ningún caso de cáncer" |
| Balance entre ambas | F1-score | Cuando ambas importan por igual |
Limitación de la Accuracy: puede ser engañosa con clases desbalanceadas. Si el 95% de los emails son legítimos y el modelo siempre predice "legítimo", tiene 95% de Accuracy pero no detecta ningún spam. En esos casos, Precisión, Recall y F1 son más informativas.
La tabla de confusión no tiene por qué ser fija. Si modificamos el modelo (le cambiamos hiperparámetros) puede obtenerse predicciones diferentes, haciendo que cambie su tabla de confusión. Cada configuración del modelo tiene su propio par (TPR, FPR).
¿Qué es la curva ROC?
Al hacer esto muchas veces para distintas configuraciones y graficar la proporción entre TPR (eje Y) y FPR (eje X) para cada tabla de confusión, nos quedamos con una curva ROC (Receiver Operating Characteristic). Cada punto de la curva corresponde a un umbral de clasificación diferente.
TPR es sinónimo de Recall. Graficar TPR vs FPR para todos los umbrales posibles nos muestra el tradeoff del modelo.
AUC — Área Bajo la Curva
El AUC (Area Under Curve) resume la curva ROC en un solo número:
Clasificador perfecto
Distingue perfectamente todas las clases. La curva pasa por el punto (0, 1) — TPR=1, FPR=0.
Clasificador aleatorio
Equivale a tirar una moneda. La curva es la diagonal. No aporta información.
Clasificador útil
Cuanto más alto el AUC, mejor el modelo en general. Un buen modelo suele tener AUC ≥ 0.8.
Curva Precision-Recall vs ROC: cuando las clases están muy desbalanceadas (ej: muy pocos casos positivos), la curva Precision-Recall es más informativa que la ROC.
Antes de entrenar un modelo es fundamental dividir bien los datos. Si usáramos todos los datos para entrenar, no tendríamos forma de saber si el modelo realmente aprendió o simplemente memorizó.
División del dataset
Entrenamiento
El modelo aprende los parámetros ajustando sus pesos con estos datos. Nunca se usa para evaluar.
Validación
Se usa para ajustar hiperparámetros y comparar modelos durante el desarrollo.
Prueba
Se usa una sola vez al final para medir el rendimiento real.
Validación Cruzada (Cross-Validation)
En vez de dividir los datos una sola vez, la validación cruzada hace múltiples divisiones rotando qué parte es el conjunto de prueba. Esto da una estimación más robusta.
Se divide el dataset en K grupos (folds). En cada iteración, uno de los K grupos actúa como datos de prueba y los K−1 restantes como datos de entrenamiento. Se entrena K veces y se promedia el resultado.
Overfitting y Underfitting
| Problema | ¿Qué ocurre? | Error train | Error test |
|---|---|---|---|
| Underfitting | El modelo es demasiado simple. No captura los patrones del dataset. | Alto | Alto |
| Overfitting | El modelo memorizó los datos de entrenamiento incluyendo el ruido. Funciona perfecto en train pero falla en datos nuevos. | Muy bajo | Alto |
| Balanced | El modelo aprendió los patrones generales sin memorizar el ruido. Generaliza bien. | Bajo | Bajo |
Datasets desbalanceados
Se reducen los ejemplos de la clase mayoritaria hasta igualar la cantidad de la clase minoritaria. Se pierde información pero se balancea el dataset.
Se duplican los ejemplos de la clase minoritaria hasta igualar la clase mayoritaria. No se pierde información pero puede causar overfitting.
from sklearn.metrics import (confusion_matrix, precision_score,
recall_score, f1_score, accuracy_score)
y_true = [1,1,0,1,1,1,1,1,0,1] # resultados reales
y_pred = [1,1,1,1,1,1,1,1,1,1] # predicciones del modelo
cm = confusion_matrix(y_true, y_pred)
print("Matriz de confusión:\n", cm)
precision = precision_score(y_true, y_pred) # 0.80
recall = recall_score(y_true, y_pred) # 1.00
f1 = f1_score(y_true, y_pred) # 0.89
accuracy = accuracy_score(y_true, y_pred) # 0.80
Conclusión práctica: el modelo que siempre dice "True" (Modelo 1) gana en todas las métricas al modelo aleatorio. Esto ilustra que casi siempre un modelo aleatorio es el peor caso posible. La forma de mejorar un modelo es ajustando sus hiperparámetros.
from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import RocCurveDisplay from sklearn.datasets import load_wine from sklearn.model_selection import train_test_split X, y = load_wine(return_X_y=True) y = (y == 2) # clasificador binario: tipo 2 vs resto X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) # Modelo SVC svc = SVC(random_state=42) svc.fit(X_train, y_train) svc_disp = RocCurveDisplay.from_estimator(svc, X_test, y_test) # Comparar con Random Forest rfc = RandomForestClassifier(n_estimators=10, random_state=42) rfc.fit(X_train, y_train) ax = plt.gca() RocCurveDisplay.from_estimator(rfc, X_test, y_test, ax=ax, alpha=0.8) svc_disp.plot(ax=ax, alpha=0.8) plt.show()
Calcular AUC
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
X, y = load_breast_cancer(return_X_y=True)
clf = LogisticRegression(solver='liblinear', random_state=0).fit(X, y)
auc = roc_auc_score(y, clf.predict_proba(X)[:, 1])
print(f'AUC = {auc:.4f}')
El Gradient Descent (GD) es el algoritmo iterativo que usan muchos modelos de ML para encontrar los parámetros óptimos. El objetivo es minimizar una función de pérdida (Loss) — una función que mide qué tan mal se está comportando el modelo.
Tomar un valor aleatorio para el parámetro. Calcular el Loss inicial.
Calcular la derivada de la función de pérdida en ese punto. Indica en qué dirección crece el error.
Moverse en la dirección opuesta al gradiente. Tamaño del paso: learning rate × derivada.
Repetir hasta convergencia: cuando la derivada es cercana a cero (estamos en el mínimo).
¡Cuidado con el learning rate! Si es muy grande podemos saltarnos el mínimo. Si es muy pequeño el entrenamiento es muy lento. 0.1 es un valor estándar de punto de partida.
Usa todos los ejemplos para calcular cada paso. Con millones de datos, cada iteración es muy costosa.
Usa 1 ejemplo al azar (o mini-batch) por paso. Mucho más rápido. El SGDClassifier de sklearn implementa esto.
Regresión Logística
Clasificación binaria y multiclase con función sigmoide, frontera de decisión, interpretación de coeficientes, one-hot encoding, preprocesamiento y validación cruzada.
Es tentador usar regresión lineal para clasificar: asignar 0 a una clase y 1 a la otra, ajustar una recta y luego usar un umbral (ej: si predice ≥ 0.5 → clase 1). Para casos simples puede funcionar, pero tiene problemas fundamentales:
- No genera probabilidades: un modelo lineal solo interpola números entre los puntos. No podemos interpretar la salida como una probabilidad real.
- Predice valores fuera de [0,1]: puede dar valores negativos o mayores a 1, lo que no tiene sentido como probabilidad.
- El umbral pierde sentido: si hay outliers extremos, la recta se desplaza y el punto de corte deja de funcionar.
- Mal con múltiples clases: al etiquetar las clases como 0, 1, 2… se impone un orden numérico artificial que puede no existir en la realidad.
Para resolver estos problemas se usa la regresión logística, que modela directamente la probabilidad de pertenencia a una clase usando una función que siempre da valores en [0, 1].
En lugar de ajustar una línea recta, la regresión logística usa la función logística (sigmoide) para comprimir la salida entre 0 y 1. Esa salida se interpreta como una probabilidad.
Donde z es la combinación lineal de los inputs: z = β₀ + β₁·x₁ + β₂·x₂ + ...
La función sigmoide tiene forma de "S": cuando z → +∞, s(z) → 1; cuando z → −∞, s(z) → 0; cuando z = 0, s(z) = 0.5.
Umbral de decisión: si s(x) ≥ 0.5 → clase 1. Si s(x) < 0.5 → clase 0. El umbral 0.5 es el estándar, pero se puede cambiar según el problema (por ej, en medicina puede preferirse un umbral más bajo para no perder casos).
La Frontera de Decisión
El umbral 0.5 corresponde exactamente a cuando z = 0, es decir: β₀ + β₁·x₁ + β₂·x₂ = 0. Esto define un hiperplano (en 2D, una línea recta) que separa el espacio en dos regiones:
- De un lado: s(x) ≥ 0.5 → clase 1.
- Del otro lado: s(x) < 0.5 → clase 0.
La frontera es: β₀ + β₁·x₁ + β₂·x₂ = 0
Despejando x₂: x₂ = −(β₁/β₂)·x₁ − (β₀/β₂)
La pendiente es −β₁/β₂ y el intercepto es −β₀/β₂. Esta recta divide el espacio de características en dos clases.
La interpretación de los coeficientes en regresión logística es más compleja que en regresión lineal porque la salida no es lineal en probabilidades.
¿Qué son los Log-Odds?
Para interpretar los coeficientes hay que transformar la probabilidad en odds y luego en log-odds:
La regresión logística predice linealmente los log-odds. Esto implica:
- Si una variable Xⱼ aumenta en 1 unidad → el log-odds aumenta en βⱼ.
- En términos de odds: los odds se multiplican por e^βⱼ.
Si β = 0.7, entonces e^0.7 ≈ 2. Esto significa que los odds se duplican al aumentar X en una unidad. Es decir, la probabilidad de la clase positiva aumenta, pero cuánto depende del valor actual de la probabilidad.
Tipos de variables
- Variable numérica: al aumentar 1 unidad, los odds cambian por un factor de e^β.
- Variable categórica binaria: cambiar de la categoría de referencia (0) a la otra (1) cambia los odds por e^β.
- Variable categórica múltiple: se usa one-hot encoding con una categoría de referencia.
No es lineal en probabilidades: aunque el modelo es lineal en log-odds, en términos de probabilidad el efecto de aumentar una variable es diferente si estamos en probabilidades bajas vs probabilidades altas.
Para usar variables categóricas en regresión logística (y muchos otros modelos), es necesario transformarlas. La técnica más usada es el one-hot encoding.
¿Qué es One-Hot Encoding?
Convierte una variable categórica con N categorías en N−1 columnas binarias (0 o 1). Se omite una categoría (la "de referencia") para evitar redundancia.
Se convierte en una columna "Sex_female": 1 si es femenino, 0 si es masculino. La categoría masculino es la "de referencia".
# En pandas se hace con get_dummies ds_trabajo = pd.get_dummies(ds_trabajo, columns=["Pclass", "Sex", "Embarked"]) # Resultado: columnas Pclass_1, Pclass_2, Sex_female, Embarked_C, Embarked_Q, Embarked_S
¿Por qué N−1 y no N columnas?
Si usáramos N columnas, una de ellas sería redundante (la última es siempre la que queda cuando las otras son 0). Esto crea problemas de multicolinealidad perfecta en el modelo. Por eso se descarta una categoría (la de referencia).
Dataset: Titanic. Objetivo: predecir si un pasajero sobrevivió (Survived = 1 o 0).
1. Carga y exploración
ds_train = pd.read_csv('ds_titanic.csv')
ds_trabajo = ds_train.copy()
ds_trabajo.shape # (891, 12)
ds_trabajo.columns.tolist()
ds_trabajo.head()
2. Limpieza de datos
# Eliminar columnas no útiles columnas_eliminar = ['PassengerId', 'Name', 'Ticket'] ds_trabajo.drop(columnas_eliminar, axis='columns', inplace=True) # Ver porcentaje de nulos filas_totales = ds_trabajo.shape[0] print(ds_trabajo.isna().sum() / filas_totales * 100) # Eliminar columnas con demasiados nulos (ej: Cabin tiene ~77%) ds_trabajo.drop(['Cabin'], axis='columns', inplace=True) # Eliminar filas con nulos restantes ds_trabajo = ds_trabajo.dropna()
3. One-Hot Encoding
# Convertir variables categóricas a columnas numéricas ds_trabajo = pd.get_dummies(ds_trabajo, columns=["Pclass", "Sex", "Embarked"]) # Resultado: Pclass_1, Pclass_2, Sex_female, Embarked_C, Embarked_Q, Embarked_S
4. División Train / Test
from sklearn.model_selection import train_test_split
columnas_modelo = ds_trabajo.columns.tolist()
columnas_modelo.remove('Survived') # target
X = ds_trabajo[columnas_modelo]
y = ds_trabajo['Survived']
x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=2)
# 80% entrenamiento, 20% test
5. Modelo 1: Variables Pclass, Sex y Age
from sklearn.linear_model import LogisticRegression columnas_modelo1 = ['Pclass_1', 'Pclass_2', 'Sex_female', 'Age'] modelo1 = LogisticRegression() modelo1.fit(x_train[columnas_modelo1], y_train) # Ver coeficientes coeficientes = pd.DataFrame(modelo1.coef_[0], columnas_modelo1, columns=['coef']) print(coeficientes) # Predecir y_pred = modelo1.predict(x_test[columnas_modelo1]) y_pred_proba = modelo1.predict_proba(x_test[columnas_modelo1])[:, 1]
6. Evaluar con matriz de confusión y ROC
from yellowbrick.classifier import ROCAUC import seaborn as sns, matplotlib.pyplot as plt, pandas as pd # Tabla de contingencia (confusión) ds_cm = pd.DataFrame(zip(y_test, y_pred_proba), columns=["Survived", "Prob"]) ds_cm["Survived_predict"] = (ds_cm["Prob"] >= 0.50).astype(int) tabla = pd.crosstab(ds_cm['Survived'], ds_cm['Survived_predict']) sns.heatmap(tabla, annot=True, cmap='Blues') plt.show() # Curva ROC-AUC visualizer = ROCAUC(modelo1, binary=True) visualizer.fit(x_train[columnas_modelo1], y_train) visualizer.score(x_test[columnas_modelo1], y_test) visualizer.show()
7. Validación cruzada
from sklearn.linear_model import LogisticRegressionCV
# CV=5 folds, métrica AUC
modelo_cv = LogisticRegressionCV(cv=5, scoring='roc_auc')
modelo_cv.fit(x_train[columnas_modelo1], y_train)
# Ver AUC de cada fold
for i, auc_score in enumerate(modelo_cv.scores_[1], 1):
print(f"Fold {i}: AUC = {auc_score:.4f}")
print(f"Promedio AUC: {modelo_cv.scores_[1].mean():.4f}")
print(f"C elegido: {modelo_cv.C_}")
¿Qué es C? Es el parámetro de regularización de la regresión logística. Controla qué tan "estricto" es el modelo. C pequeño → modelo más simple (regularización fuerte). C grande → modelo más complejo (regularización débil). La validación cruzada elige el mejor C automáticamente.
Dataset: Iris (3 clases: setosa, versicolor, virginica). La regresión logística también funciona con múltiples clases.
Carga y exploración visual
from sklearn.datasets import load_iris
iris = load_iris()
nombres_columnas = [x.replace('(cm)','').rstrip().replace(" ","_")
for x in iris['feature_names']]
df_iris = pd.DataFrame(iris['data'], columns=nombres_columnas)
y_iris = iris['target']
# Violin plot por clase
import matplotlib.pyplot as plt
plt.figure(figsize=(20, 6))
for f, f_name in enumerate(iris['feature_names']):
plt.subplot(1, 4, f+1)
data = np.zeros((50, 3))
for k in range(3):
data[:, k] = iris.data[iris.target==k, f]
plt.violinplot(data)
plt.ylabel(f_name)
plt.xticks([1,2,3], labels=iris['target_names'], rotation=70)
plt.show()
Se puede observar que la categoría Setosa se distingue del resto fácilmente usando 2 atributos. Sin embargo Versicolor y Virginica no son tan fácilmente separables.
Modelo con 2 variables
df_iris_2_vars = df_iris[['petal_length', 'petal_width']]
clf_2_vars = LogisticRegression(multi_class='multinomial')
clf_2_vars.fit(df_iris_2_vars, y_iris)
# Visualizar la frontera de decisión (malla de predicciones)
x1_min, x1_max = df_iris.petal_length.min(), df_iris.petal_length.max()
x2_min, x2_max = df_iris.petal_width.min(), df_iris.petal_width.max()
xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, 0.1),
np.arange(x2_min, x2_max, 0.1))
XX = np.c_[xx1.ravel(), xx2.ravel()]
pred = clf_2_vars.predict(XX).reshape(xx1.shape)
plt.contourf(xx1, xx2, pred, cmap=plt.cm.Pastel2)
plt.scatter(df_iris_2_vars.petal_length, df_iris_2_vars.petal_width,
c=colors, marker='.')
plt.show()
Modelo completo con train/test + métricas
X_train, X_test, y_train, y_test = train_test_split(df_iris, y_iris,
test_size=0.2, random_state=1)
clf_iris = LogisticRegressionCV(cv=5, multi_class='multinomial')
clf_iris.fit(X_train, y_train)
y_pred = clf_iris.predict(X_test)
# Métricas multiclase (requieren parámetro average)
from sklearn import metrics
print(f"Accuracy: {metrics.accuracy_score(y_test, y_pred):.4f}")
print(f"Recall: {metrics.recall_score(y_test, y_pred, average='micro'):.4f}")
print(f"Precision: {metrics.precision_score(y_test, y_pred, average='micro'):.4f}")
¿Por qué average='micro'? Para la clasificación multiclase hay que agregar los resultados de todas las clases. micro cuenta todos los TP, FP, FN globalmente. Otras opciones: macro (promedio sin ponderar), weighted (promedio ponderado por cantidad de ejemplos).
Visualizar la matriz de confusión
from sklearn.metrics import confusion_matrix
import seaborn as sns
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, cmap='Blues',
xticklabels=iris['target_names'],
yticklabels=iris['target_names'])
plt.show()
La diagonal de la matriz de confusión multiclase muestra los aciertos por clase. Los valores fuera de la diagonal indican qué clases se confunden entre sí. Típicamente versicolor y virginica se confunden más que setosa (que es muy diferente a las otras dos).
Clustering
Aprendizaje no supervisado: K-Means, método del codo (Elbow), coeficiente de Silhouette, estadístico de Hopkins y ejemplos prácticos.
El clustering es aprendizaje no supervisado: no hay etiquetas. El objetivo es agrupar observaciones para un número predefinido de grupos, de modo que los elementos de cada grupo sean similares entre sí. El significado de los grupos lo interpreta el analista.
Diferencia clave con clasificación: en clasificación tenemos etiquetas (sabemos a qué clase pertenece cada dato y el modelo aprende de eso). En clustering no hay etiquetas — el modelo descubre la estructura por sí solo.
K-Means es el algoritmo de clustering más popular. Agrupa observaciones en K grupos donde el centro de cada grupo es la media aritmética de todas las observaciones pertenecientes a ese grupo.
Pasos del algoritmo
El usuario elige K. K-Means elige K centroides al azar.
Cada punto se asigna al centroide más cercano (basado en distancia euclidiana).
Se recalcula cada centroide como el promedio de todos los puntos de su grupo.
Se reasignan los puntos. Repetir pasos 2 y 3 hasta convergencia (cuando los centroides ya no se mueven).
Problema de inicialización: como los centroides iniciales son al azar, el resultado puede variar entre ejecuciones. Usar random_state fija la semilla para reproducibilidad.
Limitaciones de K-Means
- Hay que definir K antes: no es obvio cuántos grupos hay en los datos. Para esto se usan el método del Codo y Silhouette.
- Fronteras circulares: K-Means tiende a detectar grupos con formas circulares. Falla con formas complejas (como medias lunas o espirales).
- Sensible a la escala: como se basa en distancias, variables con distintas unidades afectan el resultado. Hay que estandarizar antes de aplicar.
En clustering no hay etiquetas reales, entonces ¿cómo sabemos si el resultado es bueno? Se usan métricas que miden la cohesión (qué tan compactos son los grupos) y la separación (qué tan distintos son entre sí).
Método del Codo (Elbow)
Calcula el WCSS (Within-Cluster Sum of Squares) — la suma de distancias al cuadrado de cada punto a su centroide — para diferentes valores de K. A medida que K aumenta, el WCSS disminuye. El punto donde la mejora deja de ser significativa forma un "codo" en el gráfico → ese K es el óptimo.
# Calcular WCSS para K de 1 a 9
sse = []
for k in range(1, 10):
km = KMeans(n_clusters=k)
km.fit(X)
sse.append(km.inertia_) # .inertia_ es el WCSS
plt.plot(range(1, 10), sse, '-o')
plt.xlabel('Cantidad de clusters K')
plt.ylabel('WCSS (Inercia)')
plt.show()
Índice de Silhouette
Evalúa la calidad del clustering midiendo para cada punto:
- a(i): distancia promedio de i a todos los puntos de su propio cluster (cohesión — qué tan bien ajusta).
- b(i): distancia promedio de i a todos los puntos del cluster más cercano distinto al suyo (separación).
El índice oscila en [−1, 1]:
Bien asignado
El punto está bien dentro de su cluster y lejos de los demás. El clustering es bueno.
En la frontera
El punto está en el límite entre dos clusters. Podría pertenecer a cualquiera.
Mal asignado
El punto probablemente pertenece al cluster vecino, no al que se le asignó.
from sklearn.metrics import silhouette_score
for k in range(2, 6):
km = KMeans(n_clusters=k)
preds = km.fit_predict(X)
score = silhouette_score(X, preds)
print(f"K={k} → Silhouette = {score:.4f}")
¿Cómo elegir K? Usar ambas métricas: el Elbow da el K donde la mejora se estabiliza; el Silhouette da el K con mejor calidad de clusters. Si ambos coinciden, es una buena señal.
Antes de aplicar K-Means, conviene verificar si los datos realmente tienen tendencia a formar clusters o si están distribuidos aleatoriamente. Para esto existe el estadístico de Hopkins.
¿Cómo funciona?
- Generar m puntos distribuidos al azar en el espacio muestral.
- Extraer m puntos reales del dataset.
- Para ambos conjuntos, calcular las distancias al vecino más cercano (u y w respectivamente).
- Calcular Hopkins:
Interpretación
Estructura clara
Los datos presentan clusters bien definidos. El clustering tiene sentido.
Distribución aleatoria
Los datos no tienen estructura de clusters. Aplicar K-Means no tiene sentido.
Regularidad
Los datos están distribuidos de forma regular (casi cuadrícula). Poco común.
import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.datasets import make_blobs from sklearn.metrics import silhouette_score # Dataset sintético con 4 grupos naturales X, y = make_blobs(n_samples=300, centers=4, cluster_std=0.60, random_state=0) # Aplicar K-Means kmeans = KMeans(n_clusters=4) kmeans.fit(X) y_kmeans = kmeans.predict(X) # Visualizar grupos y centroides plt.scatter(X[:, 0], X[:, 1], c=y_kmeans, s=50, cmap='viridis') centers = kmeans.cluster_centers_ plt.scatter(centers[:, 0], centers[:, 1], c='black', s=200, alpha=0.5) plt.show()
Método del Codo
sse = []
k_range = range(1, 10)
for k in k_range:
km = KMeans(n_clusters=k)
km.fit(X)
sse.append(km.inertia_)
plt.plot(k_range, sse, '-o')
plt.xlabel('Cantidad de clusters K')
plt.ylabel('WCSS (Inercia)')
plt.show()
# El "codo" se ve en K=4 → ese es el número óptimo
Silhouette
for k in range(2, 6):
km = KMeans(n_clusters=k, random_state=42)
preds = km.fit_predict(X)
score = silhouette_score(X, preds)
print(f"K={k} → Silhouette = {score:.4f}")
Validación externa con Iris
from sklearn.datasets import load_iris iris = load_iris() df_iris = pd.DataFrame(iris['data'], columns=nombres_columnas) y_iris = iris['target'] kmeans = KMeans(3, random_state=0) kmeans.fit(df_iris) clusters = kmeans.predict(df_iris) # Comparar clusters vs etiquetas reales validacion = pd.DataFrame(zip(y_iris, clusters), columns=['real', 'cluster']) pd.crosstab(validacion.real, validacion.cluster)
Validación externa: cuando sí tenemos etiquetas previas, podemos comparar los clusters obtenidos con las etiquetas reales usando un crosstab. No siempre los números de cluster coinciden con las clases (ej: cluster 0 puede ser la clase 2 real), pero la tabla de contingencia revela la correspondencia.
Estandarización antes de clustering
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) X_escalado = scaler.fit_transform(X) # Siempre escalar antes de K-Means para evitar que variables con # unidades grandes dominen el cálculo de distancias