ml_proyecto / 05_ENTRENA_REPARA_DATOS.py
carloscortezasto's picture
Commit final limpio
568241b
# 05_ENTRENA_REPARA_DATOS.py
import os
import joblib
import numpy as np
import pandas as pd
from dotenv import load_dotenv # <<< AÑADIDO
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix
# =====================
# Configuración dinámica de rutas desde .env
# =====================
# 1. Cargar las variables desde el archivo .env
load_dotenv()
# 2. Obtener la ruta base del proyecto
ruta_base = os.getenv("PROYECTO_RUTA_BASE")
# 3. Validar que la ruta se haya cargado correctamente
if not ruta_base:
raise ValueError("La variable 'PROYECTO_RUTA_BASE' no se encontró en el archivo .env.")
print(f"Ruta base del proyecto cargada desde .env: {ruta_base}")
# <<< MODIFICADO: Las rutas ahora se construyen a partir de 'ruta_base' >>>
CARPETA_PROCESADO = os.path.join(ruta_base, "PROCESADO")
ARCHIVO_CSV = os.path.join(CARPETA_PROCESADO, "reporte_mesa_ayuda_COMBINADO.csv")
ARCHIVO_PIPELINE = os.path.join(CARPETA_PROCESADO, "modelo_prioridad_pipeline.joblib")
# =====================
# Carga y limpieza
# =====================
print("📥 Cargando datos desde:", ARCHIVO_CSV)
df = pd.read_csv(ARCHIVO_CSV)
# Columnas que usaremos (usa 'Asunto' si existe para añadir señal)
cat_cols = ['Area_Solicitante', 'Grupo_Asignado', 'Categoria']
text_col = 'Asunto' if 'Asunto' in df.columns else None
# Asegurar existencia de columnas clave
cols_necesarias = cat_cols + ( [text_col] if text_col else [] ) + ['Prioridad']
faltantes = [c for c in cols_necesarias if c not in df.columns]
if faltantes:
raise ValueError(f"Faltan columnas en el CSV para entrenar: {faltantes}")
# Limpiar nulos y espacios
df = df.dropna(subset=['Prioridad'] + cat_cols) # elimina filas sin target o sin categorías
for c in cat_cols + ( [text_col] if text_col else [] ):
if df[c].dtype == 'object':
df[c] = df[c].astype(str).str.strip()
print(f"✅ Dataset tras limpieza: {len(df)} filas, {len(df.columns)} columnas.")
X = df[cat_cols + ([text_col] if text_col else [])]
y = df['Prioridad'].astype(str).str.strip()
# =====================
# Pipeline de preprocesamiento
# =====================
# Categóricas → OneHotEncoder sin drop (NO perder categoría base), ignorar desconocidos
cat_transformer = OneHotEncoder(drop=None, handle_unknown='ignore', sparse_output=True)
# Si hay texto en 'Asunto', lo vectorizamos con TF-IDF (bigrams ayudan)
transformers = [
("cat", cat_transformer, cat_cols)
]
if text_col:
transformers.append(
("txt", TfidfVectorizer(min_df=2, ngram_range=(1, 2)), text_col)
)
preprocess = ColumnTransformer(transformers)
# Modelo con balance de clases para mitigar desbalance
clf = LogisticRegression(
max_iter=2000,
class_weight='balanced',
solver='liblinear' # estable con muchas features dispersas
)
# Pipeline completo
pipe = Pipeline([
("pre", preprocess),
("clf", clf)
])
# =====================
# Split, entrenamiento y evaluación
# =====================
print("📊 Haciendo split estratificado train/test...")
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print("🏋️ Entrenando pipeline...")
pipe.fit(X_train, y_train)
print("\n🧪 Evaluación en test:")
y_pred = pipe.predict(X_test)
print(classification_report(y_test, y_pred, digits=4))
cm = confusion_matrix(y_test, y_pred, labels=sorted(y.unique()))
cm_df = pd.DataFrame(cm, index=[f"real_{c}" for c in sorted(y.unique())],
columns=[f"pred_{c}" for c in sorted(y.unique())])
print("\nMatriz de confusión:")
print(cm_df)
# =====================
# Guardar pipeline
# =====================
joblib.dump(pipe, ARCHIVO_PIPELINE)
print(f"\n💾 Pipeline guardado en:\n{ARCHIVO_PIPELINE}")
# =====================
# Sanity-check con 3 ejemplos
# =====================
def mostrar_pred(nombre, fila_dict):
X_in = pd.DataFrame([fila_dict])
proba = pipe.predict_proba(X_in)[0]
clases = pipe.classes_
pred = pipe.predict(X_in)[0]
print(f"\n🔎 {nombre}")
print(f"🎯 Predicción: {pred}")
print("📊 Probabilidades por clase:")
for cls, p in sorted(zip(clases, proba), key=lambda x: x[0]):
print(f" {cls}: {p:.2f}")
ejemplos = [
("Ticket Crítico (esperable Alta)", {
'Area_Solicitante': 'TI',
'Grupo_Asignado': 'SGD',
'Categoria': 'Incidente',
**({'Asunto': 'No se puede iniciar sesión, error crítico de autenticación'} if text_col else {})
}),
("Ticket Normal (esperable Media)", {
'Area_Solicitante': 'Operaciones',
'Grupo_Asignado': 'APLICATIVO',
'Categoria': 'Requerimiento',
**({'Asunto': 'Solicito acceso a nuevo módulo para el equipo'} if text_col else {})
}),
("Ticket Trivial (esperable Baja)", {
'Area_Solicitante': 'Legal',
'Grupo_Asignado': 'ECASILLA',
'Categoria': 'Orientación',
**({'Asunto': '¿Dónde encuentro el manual y cómo generar reporte?'} if text_col else {})
}),
]
print("\n================= SANITY CHECK =================")
for nombre, fila in ejemplos:
mostrar_pred(nombre, fila)
print("================================================")