Taller Semana 8: Regresión Logística en Python¶
Información del Curso¶
| Campo | Información |
|---|---|
| Asignatura | Fundamentos para IA |
| Semana | 8 |
| Actividad | Script. Trabajo de Aplicación: Regresión logística |
| Docente | Ing. Breyner Alexander Parra Rojas |
| Fecha | 28 de Febrero de 2026 |
Integrantes - Equipo de Trabajo 10¶
- Alexander Oviedo Fadul
- Josmar Peña Buitrago
- Maria Fernanda Ruiz Paipilla
- Neheman Samir Jaller Cerchiaro
- William David Obando Lopez
INTRODUCCIÓN¶
En esta octava semana del curso, nuestro equipo aborda uno de los algoritmos de clasificación más fundamentales y utilizados en el aprendizaje automático: la Regresión Logística.
A primera vista, el nombre "regresión" podría sugerir que intentaremos predecir un valor numérico continuo (como el precio de una casa o la temperatura de mañana). Sin embargo, como bien señala Wei-Meng Lee (2019): "Unlike linear regression, logistic regression does not try to predict the value of a numeric variable given a set of inputs. Instead, the output of logistic regression is the probability of a given input point belonging to a specific class. The output of logistic regression always lies in [0,1]." (p. 151). Es decir, no predecimos un número, sino la probabilidad de que algo sea cierto o falso, de que un paciente esté enfermo o sano, o como veremos en nuestro caso de estudio, de que un tumor sea benigno o maligno.
Lo que vamos a construir hoy¶
Para poner en práctica estos conceptos, nos propusimos implementar un modelo de regresión logística desde cero utilizando las potentes librerías de Python diseñadas para el cálculo numérico y la ciencia de datos (como pandas, numpy, scikit-learn y matplotlib).
Nuestro objetivo no es solo lograr que el código corra sin errores, sino entender las matemáticas detrás de las funciones y visualizar cómo el algoritmo traza la línea que separa una categoría de otra.
Acompáñennos en este recorrido estadístico y de programación, donde daremos sentido a los datos cargados y evaluaremos el desempeño de nuestro modelo.
1. IMPORTACIÓN DE LIBRERÍAS¶
Comenzaremos importando las herramientas que nos facilitarán todo el proceso. Elegimos las librerías estándar en la comunidad de Data Science de Python.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Utilidades de Scikit-Learn
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# El algoritmo estrella de hoy
from sklearn.linear_model import LogisticRegression
# Métricas para evaluar qué tan bien nos fue
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
print("✔ Librerías importadas correctamente.")
print(f" • Pandas versión: {pd.__version__}")
print(f" • NumPy versión: {np.__version__}")
✔ Librerías importadas correctamente. • Pandas versión: 2.2.2 • NumPy versión: 2.0.2
2. CARGA DEL DATASET¶
Para este ejercicio, hemos seleccionado el clásico conjunto de datos de Breast Cancer Wisconsin (Diagnostic), que ya viene integrado en Scikit-Learn. Este dataset nos viene como anillo al dedo para problemas de clasificación binaria.
El problema a resolver es claro: dadas ciertas características extraídas de imágenes digitales de un aspirado con aguja fina de una masa mamaria, ¿es el tumor benigno o maligno?
# Cargamos los datos directamente desde scikit-learn
cancer_data = load_breast_cancer()
# Lo convertimos a un DataFrame de Pandas para que sea más fácil de manipular y visualizar
df = pd.DataFrame(data=cancer_data.data, columns=cancer_data.feature_names)
# Añadimos la columna objetivo (Target) al DataFrame
df['target'] = cancer_data.target
print(f"El dataset tiene {df.shape[0]} registros y {df.shape[1]} columnas (incluyendo nuestro target).")
print("\nPrimeras 5 filas del dataset:")
display(df.head())
El dataset tiene 569 registros y 31 columnas (incluyendo nuestro target). Primeras 5 filas del dataset:
| mean radius | mean texture | mean perimeter | mean area | mean smoothness | mean compactness | mean concavity | mean concave points | mean symmetry | mean fractal dimension | ... | worst texture | worst perimeter | worst area | worst smoothness | worst compactness | worst concavity | worst concave points | worst symmetry | worst fractal dimension | target | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 17.99 | 10.38 | 122.80 | 1001.0 | 0.11840 | 0.27760 | 0.3001 | 0.14710 | 0.2419 | 0.07871 | ... | 17.33 | 184.60 | 2019.0 | 0.1622 | 0.6656 | 0.7119 | 0.2654 | 0.4601 | 0.11890 | 0 |
| 1 | 20.57 | 17.77 | 132.90 | 1326.0 | 0.08474 | 0.07864 | 0.0869 | 0.07017 | 0.1812 | 0.05667 | ... | 23.41 | 158.80 | 1956.0 | 0.1238 | 0.1866 | 0.2416 | 0.1860 | 0.2750 | 0.08902 | 0 |
| 2 | 19.69 | 21.25 | 130.00 | 1203.0 | 0.10960 | 0.15990 | 0.1974 | 0.12790 | 0.2069 | 0.05999 | ... | 25.53 | 152.50 | 1709.0 | 0.1444 | 0.4245 | 0.4504 | 0.2430 | 0.3613 | 0.08758 | 0 |
| 3 | 11.42 | 20.38 | 77.58 | 386.1 | 0.14250 | 0.28390 | 0.2414 | 0.10520 | 0.2597 | 0.09744 | ... | 26.50 | 98.87 | 567.7 | 0.2098 | 0.8663 | 0.6869 | 0.2575 | 0.6638 | 0.17300 | 0 |
| 4 | 20.29 | 14.34 | 135.10 | 1297.0 | 0.10030 | 0.13280 | 0.1980 | 0.10430 | 0.1809 | 0.05883 | ... | 16.67 | 152.20 | 1575.0 | 0.1374 | 0.2050 | 0.4000 | 0.1625 | 0.2364 | 0.07678 | 0 |
5 rows × 31 columns
Breve exploración inicial¶
Antes de alimentar nuestro modelo, siempre es buena idea comprobar si hay datos faltantes (nulos) y entender cómo están distribuidas las clases en nuestro caso práctico.
print("¿Tenemos valores nulos en el dataset?:")
print(df.isnull().sum().sum(), "valores nulos (¡Perfecto!)")
print("\nDistribución de clases (0 = Maligno, 1 = Benigno):")
counts = df['target'].value_counts()
print(counts)
# Vamos a graficar esto para visualizarlo mejor
plt.figure(figsize=(6, 4))
sns.countplot(x='target', data=df, palette='Set2')
plt.title('Distribución de Diagnósticos de Cáncer de Mama')
plt.xticks([0, 1], ['Maligno', 'Benigno'])
plt.xlabel('Diagnóstico')
plt.ylabel('Cantidad de Pacientes')
plt.show()
¿Tenemos valores nulos en el dataset?: 0 valores nulos (¡Perfecto!) Distribución de clases (0 = Maligno, 1 = Benigno): target 1 357 0 212 Name: count, dtype: int64
/tmp/ipython-input-211/19851355.py:10: FutureWarning: Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect. sns.countplot(x='target', data=df, palette='Set2')
Notemos algo crucial aquí: tenemos datos para ambas categorías de forma decente, aunque hay un número algo mayor de tumores benignos (lo cual es normal en datos médicos). Tener datos para ambas clases es vital, pues un modelo estadístico solo puede aprender a diferenciar lo que ha visto.
3. PREPARACIÓN DE LOS DATOS (Train/Test Split & Scaler)¶
Aquí es donde separamos la paja del trigo. Siguiendo las buenas prácticas que menciona Bagnato (2017) en sus tutoriales, no podemos entrenar y evaluar el modelo con los mismos datos. Si hiciéramos eso, el modelo simplemente memorizaría las respuestas (overfitting) en lugar de aprender los patrones subyacentes.
Por lo tanto, apartaremos el 20% de nuestro dataset exclusivamente para las pruebas, y en ellos evaluaremos el puntaje final del modelo.
# Separamos nuestras variables predictoras (X) de nuestra variable objetivo (y)
X = df.drop('target', axis=1)
y = df['target']
# Dividimos los datos (80% para entrenamiento, 20% para pruebas)
# Usamos un 'random_state' fijo para que si ustedes corren este código, obtengan el mismo exacto resultado.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)
print(f"Datos de entrenamiento: {X_train.shape[0]} filas.")
print(f"Datos de prueba: {X_test.shape[0]} filas.")
Datos de entrenamiento: 455 filas. Datos de prueba: 114 filas.
El truco bajo la manga: Estandarización¶
La regresión logística se apoya fuertemente en cálculos matemáticos de optimización; busca los coeficientes que reduzcan el error mediante descenso de gradiente. Pero... ¿qué sucede si una característica de nuestro dataset está en milímetros y otra tiene valores de cientos o miles? El modelo le dará una importancia gigantesca a las variables con números grandes, ignorando casi por completo a las pequeñas.
Para evitar este sesgo matemático y "nivelar el campo de juego", estandarizamos los datos (haciendo que las variables tengan una media de 0 y una desviación estándar de 1).
# Inicializamos el estandarizador
scaler = StandardScaler()
# ¡Ojo aquí! Solo hacemos 'fit' en los datos de entrenamiento para evitar el "data leakage" (fuga de datos)
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print("✔ Datos estandarizados correctamente.")
✔ Datos estandarizados correctamente.
4. ENTRENAMIENTO DEL MODELO DE REGRESIÓN LOGÍSTICA¶
Llegó el momento de la verdad. Las matemáticas complejas, el logaritmo de las probabilidades y la función sigmoide se abstraen aquí en un par de líneas de código gracias a scikit-learn.
Como se vio en la teoría del curso, la función Logit (logaritmo de los odds) nos permite transformar cualquier variable en un rango de $(-\infty, \infty)$. Luego, aplicando la curva Sigmoide, garantizamos que nuestro modelo devuelva valores que estén de manera exclusiva entre 0 y 1, representando certeza (probabilidad).
# Instanciamos nuestro modelo logístico
# Añadimos un max_iter más alto en caso de que necesite más rondas para converger a la solución óptima
modelo_logistico = LogisticRegression(max_iter=1000, random_state=42)
# ¡A entrenar! Ajustamos el modelo con nuestros datos de entrenamiento previamente escalados
modelo_logistico.fit(X_train_scaled, y_train)
print("✔ El modelo ha sido entrenado exitosamente con el dataset de entrenamiento.")
✔ El modelo ha sido entrenado exitosamente con el dataset de entrenamiento.
5. PREDICCIONES Y EVALUACIÓN¶
Un modelo sin evaluación es tan útil como una brújula en un campo magnético. Necesitamos saber qué tan bueno es prediciendo diagnósticos de pacientes que nunca ha visto antes (nuestro conjunto de pruebas o X_test).
# Solicitamos al modelo que haga predicciones utilizando los datos que apartamos
predicciones = modelo_logistico.predict(X_test_scaled)
# Calculamos la métrica principal: ¿En qué porcentaje acertamos?
accuracy = accuracy_score(y_test, predicciones)
print(f"⭐ Exactitud (Accuracy) del Modelo: {accuracy * 100:.2f} %")
⭐ Exactitud (Accuracy) del Modelo: 97.37 %
¡Un 97.37% de exactitud!¶
A decir verdad, este es un resultado excelente. Pero como científicos de datos en formación, sabemos que mirar únicamente el Accuracy puede engañarnos (especialmente en el sector salud). Por ello, recurrimos a dos herramientas invaluables: La Matriz de Confusión y el Reporte de Clasificación.
matriz = confusion_matrix(y_test, predicciones)
# Visualizamos la matriz de manera amigable
plt.figure(figsize=(6, 5))
sns.heatmap(matriz, annot=True, fmt='d', cmap='Blues',
xticklabels=['Maligno (0)', 'Benigno (1)'],
yticklabels=['Maligno (0)', 'Benigno (1)'])
plt.ylabel('Valor Real')
plt.xlabel('Predicción del Modelo')
plt.title('Matriz de Confusión de la Regresión Logística')
plt.show()
print("\nReporte Detallado de Clasificación:")
print("="*55)
print(classification_report(y_test, predicciones, target_names=['Maligno', 'Benigno']))
Reporte Detallado de Clasificación:
=======================================================
precision recall f1-score support
Maligno 0.98 0.95 0.96 43
Benigno 0.97 0.99 0.98 71
accuracy 0.97 114
macro avg 0.97 0.97 0.97 114
weighted avg 0.97 0.97 0.97 114
¿Cómo leemos esta matriz? (Lo que nos llama la atención)¶
Esta matriz nos dice mucho más sobre cómo se comportó nuestro modelo:
- El modelo identificó correctamente a 41 personas con tumores malignos (Verdaderos Negativos).
- El modelo identificó correctamente a 70 personas con tumores benignos (Verdaderos Positivos).
- Falsos Positivos: El modelo se equivocó diciendo que 2 personas tenían un tumor benigno, cuando en realidad era maligno.
- Falsos Negativos: El modelo le dijo a 1 persona que su tumor era maligno, siendo este verdaderamente benigno.
No vamos a mentir; los 2 casos donde dijimos "es benigno" y era maligno son los más peligrosos en el ámbito médico (estas personas podrían irse a casa pensando que no requieren tratamiento). Es por esto que en contextos de IA de salud a menudo optimizamos más el Recall (sensibilidad) sobre el tumor maligno que el accuracy general.
CONCLUSIONES¶
- Poder Predictivo: La Regresión Logística, a pesar de ser uno de los algoritmos más simples y tradicionales en la esfera del Machine Learning, demostró ser increíblemente potente y robusta para esta tarea de clasificación, brindando un desempeño sobresaliente superior al 97%.
- La Inteligencia Artificial No Es Magia, Son Matemáticas: A través de este script, comprendimos de manera práctica cómo las librerías de
numpyyscikit-learnrealizan cálculos vectoriales y aplican el concepto de logit function y probabilidades en milisegundos para ofrecernos una clasificación precisa. - Transparencia y Limitaciones: Entendemos que nuestro modelo no es infalible. Como evidenciamos en nuestra matriz de confusión, se presentaron falsos diagnósticos. Esto nos recuerda la premisa fundamental de la IA en la actualidad: funciona maravillosa como una herramienta de asistencia para un profesional médico, acelerando los tamizajes y revelando patrones, pero no debe considerarse un reemplazo ciego del criterio experto.
REFERENCIAS¶
- Bagnato, J. (2017). Regresión Logística con Python. Paso a paso. Aprende Machine Learning en Español. https://www.aprendemachinelearning.com/regresion-logistica-con-python-paso-a-paso/
- Lee, W. (2019). Python machine learning (pp. 151-174). John Wiley & Sons, Incorporated.
- Theodoridis, S. (2015). Machine learning: A bayesian and optimization perspective (53-102) Elsevier Science & Technology.
© 2026 - Equipo de Trabajo 10 - UNIMINUTO