diff --git a/ProyectoIA_Gestion/detect.py b/ProyectoIA_Gestion/detect.py index 56079d9..72e5474 100644 --- a/ProyectoIA_Gestion/detect.py +++ b/ProyectoIA_Gestion/detect.py @@ -13,7 +13,6 @@ def insertar_alerta_en_db(cursor, tipo_alerta, id_entidad, entidad, mensaje, fec VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0) """ try: - # Asegurarse de que los valores numéricos opcionales sean None si no se proporcionan p_dev = float(porc_devolucion) if porc_devolucion is not None else None c_env = int(cant_enviada) if cant_enviada is not None else None c_dev = int(cant_devuelta) if cant_devuelta is not None else None @@ -31,6 +30,7 @@ DB_DATABASE = 'SistemaGestion' CONNECTION_STRING = f'DRIVER={{ODBC Driver 18 for SQL Server}};SERVER={DB_SERVER};DATABASE={DB_DATABASE};Trusted_Connection=yes;TrustServerCertificate=yes;' MODEL_INDIVIDUAL_FILE = 'modelo_anomalias.joblib' MODEL_SISTEMA_FILE = 'modelo_sistema_anomalias.joblib' +MODEL_DIST_FILE = 'modelo_anomalias_dist.joblib' # << NUEVO: Nombre del modelo de distribuidores # --- 2. Determinar Fecha --- if len(sys.argv) > 1: @@ -46,11 +46,12 @@ except Exception as e: print(f"CRITICAL: No se pudo conectar a la base de datos. Error: {e}") exit() -# --- 3. DETECCIÓN INDIVIDUAL (CANILLITAS) --- +# --- FASE 1: Detección de Anomalías Individuales (Canillitas) --- print("\n--- FASE 1: Detección de Anomalías Individuales (Canillitas) ---") if not os.path.exists(MODEL_INDIVIDUAL_FILE): print(f"ADVERTENCIA: Modelo individual '{MODEL_INDIVIDUAL_FILE}' no encontrado.") else: + # ... (esta sección se mantiene exactamente igual que antes) ... model_individual = joblib.load(MODEL_INDIVIDUAL_FILE) query_individual = f""" SELECT esc.Id_Canilla AS id_canilla, esc.Fecha AS fecha, esc.CantSalida AS cantidad_enviada, esc.CantEntrada AS cantidad_devuelta, c.NomApe AS nombre_canilla @@ -81,15 +82,16 @@ else: cant_devuelta=row['cantidad_devuelta'], porc_devolucion=row['porcentaje_devolucion']) else: - print("INFO: No se encontraron anomalías individuales significativas.") + print("INFO: No se encontraron anomalías individuales significativas en canillitas.") else: print("INFO: No hay datos de canillitas para analizar en la fecha seleccionada.") -# --- 4. DETECCIÓN DE SISTEMA --- +# --- FASE 2: Detección de Anomalías de Sistema --- print("\n--- FASE 2: Detección de Anomalías de Sistema ---") if not os.path.exists(MODEL_SISTEMA_FILE): print(f"ADVERTENCIA: Modelo de sistema '{MODEL_SISTEMA_FILE}' no encontrado.") else: + # ... (esta sección se mantiene exactamente igual que antes) ... model_sistema = joblib.load(MODEL_SISTEMA_FILE) query_agregada = f""" SELECT CAST(Fecha AS DATE) AS fecha_dia, DATEPART(weekday, Fecha) as dia_semana, @@ -128,7 +130,59 @@ else: mensaje=mensaje, fecha_anomalia=target_date.date()) -# --- 5. Finalización --- +# --- FASE 3: Detección de Anomalías Individuales (Distribuidores) --- +print("\n--- FASE 3: Detección de Anomalías Individuales (Distribuidores) ---") +if not os.path.exists(MODEL_DIST_FILE): + print(f"ADVERTENCIA: Modelo de distribuidores '{MODEL_DIST_FILE}' no encontrado.") +else: + model_dist = joblib.load(MODEL_DIST_FILE) + query_dist = f""" + SELECT + es.Id_Distribuidor AS id_distribuidor, + d.Nombre AS nombre_distribuidor, + CAST(es.Fecha AS DATE) AS fecha, + SUM(CASE WHEN es.TipoMovimiento = 'Salida' THEN es.Cantidad ELSE 0 END) as cantidad_enviada, + SUM(CASE WHEN es.TipoMovimiento = 'Entrada' THEN es.Cantidad ELSE 0 END) as cantidad_devuelta + FROM + dist_EntradasSalidas es + JOIN + dist_dtDistribuidores d ON es.Id_Distribuidor = d.Id_Distribuidor + WHERE + CAST(es.Fecha AS DATE) = '{target_date.strftime('%Y-%m-%d')}' + GROUP BY + es.Id_Distribuidor, d.Nombre, CAST(es.Fecha AS DATE) + HAVING + SUM(CASE WHEN es.TipoMovimiento = 'Salida' THEN es.Cantidad ELSE 0 END) > 0 + """ + df_dist_new = pd.read_sql(query_dist, cnxn) + + if not df_dist_new.empty: + df_dist_new['porcentaje_devolucion'] = (df_dist_new['cantidad_devuelta'] / df_dist_new['cantidad_enviada']).fillna(0) * 100 + df_dist_new['dia_semana'] = pd.to_datetime(df_dist_new['fecha']).dt.dayofweek + features_dist = ['id_distribuidor', 'porcentaje_devolucion', 'dia_semana'] + X_dist_new = df_dist_new[features_dist] + df_dist_new['anomalia'] = model_dist.predict(X_dist_new) + + anomalias_dist_detectadas = df_dist_new[df_dist_new['anomalia'] == -1] + + if not anomalias_dist_detectadas.empty: + for index, row in anomalias_dist_detectadas.iterrows(): + mensaje = f"Devolución inusual del {row['porcentaje_devolucion']:.2f}% para el distribuidor '{row['nombre_distribuidor']}'." + insertar_alerta_en_db(cursor, + tipo_alerta='DevolucionAnomalaDist', + id_entidad=row['id_distribuidor'], + entidad='Distribuidor', + mensaje=mensaje, + fecha_anomalia=row['fecha'], + cant_enviada=row['cantidad_enviada'], + cant_devuelta=row['cantidad_devuelta'], + porc_devolucion=row['porcentaje_devolucion']) + else: + print("INFO: No se encontraron anomalías individuales significativas en distribuidores.") + else: + print("INFO: No hay datos de distribuidores para analizar en la fecha seleccionada.") + +# --- Finalización --- cnxn.commit() cnxn.close() print("\n--- DETECCIÓN COMPLETA ---") \ No newline at end of file diff --git a/ProyectoIA_Gestion/modelo_anomalias_dist.joblib b/ProyectoIA_Gestion/modelo_anomalias_dist.joblib new file mode 100644 index 0000000..8f01a7f Binary files /dev/null and b/ProyectoIA_Gestion/modelo_anomalias_dist.joblib differ diff --git a/ProyectoIA_Gestion/train_distribuidores.py b/ProyectoIA_Gestion/train_distribuidores.py new file mode 100644 index 0000000..007cc8c --- /dev/null +++ b/ProyectoIA_Gestion/train_distribuidores.py @@ -0,0 +1,69 @@ +import pandas as pd +from sklearn.ensemble import IsolationForest +import joblib +import pyodbc +from datetime import datetime, timedelta + +print("--- INICIANDO SCRIPT DE ENTRENAMIENTO (DISTRIBUIDORES) ---") + +# --- 1. Configuración --- +DB_SERVER = 'TECNICA3' +DB_DATABASE = 'SistemaGestion' +CONNECTION_STRING = f'DRIVER={{ODBC Driver 18 for SQL Server}};SERVER={DB_SERVER};DATABASE={DB_DATABASE};Trusted_Connection=yes;TrustServerCertificate=yes;' + +MODEL_FILE = 'modelo_anomalias_dist.joblib' # <-- Nuevo nombre de archivo para el modelo +CONTAMINATION_RATE = 0.01 # Un 1% de contaminación es un buen punto de partida + +# --- 2. Carga de Datos desde SQL Server --- +try: + print(f"Conectando a la base de datos '{DB_DATABASE}' en '{DB_SERVER}'...") + cnxn = pyodbc.connect(CONNECTION_STRING) + + fecha_limite = datetime.now() - timedelta(days=365) + + # << CAMBIO IMPORTANTE: Nueva consulta para agrupar Salidas y Entradas por distribuidor y día >> + query = f""" + SELECT + Id_Distribuidor AS id_distribuidor, + CAST(Fecha AS DATE) AS fecha, + SUM(CASE WHEN TipoMovimiento = 'Salida' THEN Cantidad ELSE 0 END) as cantidad_enviada, + SUM(CASE WHEN TipoMovimiento = 'Entrada' THEN Cantidad ELSE 0 END) as cantidad_devuelta + FROM + dist_EntradasSalidas + WHERE + Fecha >= '{fecha_limite.strftime('%Y-%m-%d')}' + GROUP BY + Id_Distribuidor, CAST(Fecha AS DATE) + HAVING + SUM(CASE WHEN TipoMovimiento = 'Salida' THEN Cantidad ELSE 0 END) > 0 + """ + print("Ejecutando consulta para obtener datos de entrenamiento de distribuidores...") + df = pd.read_sql(query, cnxn) + cnxn.close() + +except Exception as e: + print(f"Error al conectar o consultar la base de datos: {e}") + exit() + +if df.empty: + print("No se encontraron datos de entrenamiento de distribuidores en el último año. Saliendo.") + exit() + +# --- 3. Preparación de Datos --- +print(f"Preparando {len(df)} registros para el entrenamiento del modelo de distribuidores...") +# Se usa (df['cantidad_enviada'] + 0.001) para evitar división por cero +df['porcentaje_devolucion'] = (df['cantidad_devuelta'] / (df['cantidad_enviada'] + 0.001)) * 100 +df.fillna(0, inplace=True) +df['porcentaje_devolucion'] = df['porcentaje_devolucion'].clip(0, 100) +df['dia_semana'] = pd.to_datetime(df['fecha']).dt.dayofweek + +features = ['id_distribuidor', 'porcentaje_devolucion', 'dia_semana'] # <-- Característica clave: id_distribuidor +X = df[features] + +# --- 4. Entrenamiento y Guardado --- +print(f"Entrenando el modelo con tasa de contaminación de {CONTAMINATION_RATE}...") +model = IsolationForest(n_estimators=100, contamination=CONTAMINATION_RATE, random_state=42) +model.fit(X) +joblib.dump(model, MODEL_FILE) + +print(f"--- ENTRENAMIENTO DE DISTRIBUIDORES COMPLETADO. Modelo guardado en '{MODEL_FILE}' ---") \ No newline at end of file