diff --git a/ProyectoIA_Gestion/detect.py b/ProyectoIA_Gestion/detect.py index 72e5474..4d1b1af 100644 --- a/ProyectoIA_Gestion/detect.py +++ b/ProyectoIA_Gestion/detect.py @@ -30,7 +30,9 @@ 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 +MODEL_DIST_FILE = 'modelo_anomalias_dist.joblib' +MODEL_DANADAS_FILE = 'modelo_danadas.joblib' +MODEL_MONTOS_FILE = 'modelo_montos.joblib' # --- 2. Determinar Fecha --- if len(sys.argv) > 1: @@ -182,6 +184,108 @@ else: else: print("INFO: No hay datos de distribuidores para analizar en la fecha seleccionada.") + # --- FASE 4: Detección de Anomalías en Bobinas Dañadas --- + print("\n--- FASE 4: Detección de Anomalías en Bobinas Dañadas ---") + if not os.path.exists(MODEL_DANADAS_FILE): + print(f"ADVERTENCIA: Modelo de bobinas dañadas '{MODEL_DANADAS_FILE}' no encontrado.") + else: + model_danadas = joblib.load(MODEL_DANADAS_FILE) + query_danadas = f""" + SELECT + h.Id_Planta as id_planta, + p.Nombre as nombre_planta, + DATEPART(weekday, h.FechaMod) as dia_semana, + COUNT(DISTINCT h.Id_Bobina) as cantidad_danadas + FROM + bob_StockBobinas_H h + JOIN + bob_dtPlantas p ON h.Id_Planta = p.Id_Planta + WHERE + h.Id_EstadoBobina = 3 -- Asumiendo ID 3 para 'Dañada' + AND h.TipoMod = 'Estado: Dañada' + AND CAST(h.FechaMod AS DATE) = '{target_date.strftime('%Y-%m-%d')}' + GROUP BY + h.Id_Planta, p.Nombre, DATEPART(weekday, h.FechaMod) + """ + df_danadas_new = pd.read_sql(query_danadas, cnxn) + + if not df_danadas_new.empty: + for index, row in df_danadas_new.iterrows(): + features_danadas = ['id_planta', 'dia_semana', 'cantidad_danadas'] + X_danadas_new = row[features_danadas].to_frame().T + + prediction = model_danadas.predict(X_danadas_new) + + if prediction[0] == -1: + mensaje = f"Se registraron {row['cantidad_danadas']} bobina(s) dañada(s) en la Planta '{row['nombre_planta']}', un valor inusualmente alto." + insertar_alerta_en_db(cursor, + tipo_alerta='ExcesoBobinasDañadas', + id_entidad=row['id_planta'], + entidad='Planta', + mensaje=mensaje, + fecha_anomalia=target_date.date()) + print(f"INFO: Análisis de {len(df_danadas_new)} planta(s) con bobinas dañadas completado.") + else: + print("INFO: No se registraron bobinas dañadas en la fecha seleccionada.") + +# --- FASE 5: Detección de Anomalías en Montos Contables --- + print("\n--- FASE 5: Detección de Anomalías en Montos Contables ---") + if not os.path.exists(MODEL_MONTOS_FILE): + print(f"ADVERTENCIA: Modelo de montos contables '{MODEL_MONTOS_FILE}' no encontrado.") + else: + model_montos = joblib.load(MODEL_MONTOS_FILE) + + # Consulta unificada para obtener todas las transacciones del día + query_transacciones = f""" + SELECT 'Distribuidor' AS entidad, p.Id_Distribuidor AS id_entidad, d.Nombre as nombre_entidad, p.Id_Empresa as id_empresa, p.Fecha as fecha, p.TipoMovimiento as tipo_transaccion, p.Monto as monto + FROM cue_PagosDistribuidor p JOIN dist_dtDistribuidores d ON p.Id_Distribuidor = d.Id_Distribuidor + WHERE CAST(p.Fecha AS DATE) = '{target_date.strftime('%Y-%m-%d')}' + + UNION ALL + + SELECT + CASE WHEN cd.Destino = 'Distribuidores' THEN 'Distribuidor' ELSE 'Canillita' END AS entidad, + cd.Id_Destino AS id_entidad, + COALESCE(d.Nombre, c.NomApe) as nombre_entidad, + cd.Id_Empresa as id_empresa, + cd.Fecha as fecha, + cd.Tipo as tipo_transaccion, + cd.Monto as monto + FROM cue_CreditosDebitos cd + LEFT JOIN dist_dtDistribuidores d ON cd.Id_Destino = d.Id_Distribuidor AND cd.Destino = 'Distribuidores' + LEFT JOIN dist_dtCanillas c ON cd.Id_Destino = c.Id_Canilla AND cd.Destino = 'Canillas' + WHERE CAST(cd.Fecha AS DATE) = '{target_date.strftime('%Y-%m-%d')}' + """ + + df_transacciones_new = pd.read_sql(query_transacciones, cnxn) + + if not df_transacciones_new.empty: + # Aplicar exactamente el mismo pre-procesamiento que en el entrenamiento + df_transacciones_new['tipo_transaccion_cat'] = pd.Categorical(df_transacciones_new['tipo_transaccion']).codes + df_transacciones_new['dia_semana'] = pd.to_datetime(df_transacciones_new['fecha']).dt.dayofweek + + features = ['id_entidad', 'id_empresa', 'tipo_transaccion_cat', 'dia_semana', 'monto'] + X_new = df_transacciones_new[features] + + df_transacciones_new['anomalia'] = model_montos.predict(X_new) + anomalias_detectadas = df_transacciones_new[df_transacciones_new['anomalia'] == -1] + + if not anomalias_detectadas.empty: + for index, row in anomalias_detectadas.iterrows(): + tipo_alerta = 'MontoInusualPago' if row['tipo_transaccion'] in ['Recibido', 'Realizado'] else 'MontoInusualNota' + mensaje = f"Se registró un '{row['tipo_transaccion']}' de ${row['monto']:,} para '{row['nombre_entidad']}', un valor atípico." + + insertar_alerta_en_db(cursor, + tipo_alerta=tipo_alerta, + id_entidad=row['id_entidad'], + entidad=row['entidad'], + mensaje=mensaje, + fecha_anomalia=row['fecha'].date()) + else: + print("INFO: No se encontraron anomalías en los montos contables registrados.") + else: + print("INFO: No hay transacciones contables para analizar en la fecha seleccionada.") + # --- Finalización --- cnxn.commit() cnxn.close() diff --git a/ProyectoIA_Gestion/train_danadas.py b/ProyectoIA_Gestion/train_danadas.py new file mode 100644 index 0000000..91a5a10 --- /dev/null +++ b/ProyectoIA_Gestion/train_danadas.py @@ -0,0 +1,67 @@ +import pandas as pd +from sklearn.ensemble import IsolationForest +import joblib +import pyodbc +from datetime import datetime, timedelta + +print("--- INICIANDO SCRIPT DE ENTRENAMIENTO (BOBINAS DAÑADAS) ---") + +# --- 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_danadas.joblib' +CONTAMINATION_RATE = 0.02 # Un 2% de los días podrían tener una cantidad anómala de bobinas dañadas (ajustable) + +# --- 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 contar bobinas marcadas como "Dañada" por día y planta >> + # Asumimos que el estado "Dañada" tiene Id_EstadoBobina = 3 y el historial lo registra + query = f""" + SELECT + CAST(h.FechaMod AS DATE) as fecha, + DATEPART(weekday, h.FechaMod) as dia_semana, + h.Id_Planta as id_planta, + COUNT(DISTINCT h.Id_Bobina) as cantidad_danadas + FROM + bob_StockBobinas_H h + WHERE + h.Id_EstadoBobina = 3 -- Asumiendo que 3 es el ID del estado 'Dañada' + AND h.TipoMod = 'Estado: Dañada' -- Filtra por el evento específico del cambio de estado + AND h.FechaMod >= '{fecha_limite.strftime('%Y-%m-%d')}' + GROUP BY + CAST(h.FechaMod AS DATE), + DATEPART(weekday, h.FechaMod), + h.Id_Planta + """ + print("Ejecutando consulta para obtener historial de bobinas dañadas...") + 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 bobinas dañadas en el último año. Saliendo.") + exit() + +# --- 3. Preparación de Datos --- +print(f"Preparando {len(df)} registros agregados para el entrenamiento...") +# Las características serán la planta, el día de la semana y la cantidad de bobinas dañadas ese día +features = ['id_planta', 'dia_semana', 'cantidad_danadas'] +X = df[features] + +# --- 4. Entrenamiento y Guardado --- +print(f"Entrenando el modelo de bobinas dañadas 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 BOBINAS DAÑADAS COMPLETADO. Modelo guardado en '{MODEL_FILE}' ---") \ No newline at end of file diff --git a/ProyectoIA_Gestion/train_montos.py b/ProyectoIA_Gestion/train_montos.py new file mode 100644 index 0000000..836e8a4 --- /dev/null +++ b/ProyectoIA_Gestion/train_montos.py @@ -0,0 +1,92 @@ +import pandas as pd +from sklearn.ensemble import IsolationForest +import joblib +import pyodbc +from datetime import datetime, timedelta + +print("--- INICIANDO SCRIPT DE ENTRENAMIENTO (MONTOS CONTABLES) ---") + +# --- 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_montos.joblib' +CONTAMINATION_RATE = 0.01 # Asumimos que el 1% de las transacciones podrían ser anómalas (ajustable) + +# --- 2. Carga de Datos de Múltiples Tablas --- +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=730) # Usamos 2 años de datos para tener más contexto financiero + + # Query para Pagos a Distribuidores + query_pagos = f""" + SELECT + 'Distribuidor' AS entidad, + Id_Distribuidor AS id_entidad, + Id_Empresa AS id_empresa, + Fecha AS fecha, + TipoMovimiento AS tipo_transaccion, + Monto AS monto + FROM + cue_PagosDistribuidor + WHERE + Fecha >= '{fecha_limite.strftime('%Y-%m-%d')}' + """ + + # Query para Notas de Crédito/Débito + query_notas = f""" + SELECT + CASE + WHEN Destino = 'Distribuidores' THEN 'Distribuidor' + WHEN Destino = 'Canillas' THEN 'Canillita' + ELSE 'Desconocido' + END AS entidad, + Id_Destino AS id_entidad, + Id_Empresa AS id_empresa, + Fecha AS fecha, + Tipo AS tipo_transaccion, + Monto AS monto + FROM + cue_CreditosDebitos + WHERE + Fecha >= '{fecha_limite.strftime('%Y-%m-%d')}' + """ + + print("Ejecutando consultas para obtener datos de pagos y notas...") + df_pagos = pd.read_sql(query_pagos, cnxn) + df_notas = pd.read_sql(query_notas, cnxn) + + cnxn.close() + +except Exception as e: + print(f"Error al conectar o consultar la base de datos: {e}") + exit() + +# --- 3. Unificación y Preparación de Datos --- +if df_pagos.empty and df_notas.empty: + print("No se encontraron datos de entrenamiento en el período seleccionado. Saliendo.") + exit() + +# Combinamos ambos dataframes +df = pd.concat([df_pagos, df_notas], ignore_index=True) +print(f"Preparando {len(df)} registros contables para el entrenamiento...") + +# Feature Engineering: Convertir textos a números categóricos +# Esto ayuda al modelo a entender "Recibido", "Credito", etc., como categorías distintas. +df['tipo_transaccion_cat'] = pd.Categorical(df['tipo_transaccion']).codes +df['dia_semana'] = df['fecha'].dt.dayofweek + +# Las características para el modelo serán el contexto de la transacción y su monto +features = ['id_entidad', 'id_empresa', 'tipo_transaccion_cat', 'dia_semana', 'monto'] +X = df[features] + +# --- 4. Entrenamiento y Guardado --- +print(f"Entrenando el modelo de montos contables 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 MONTOS COMPLETADO. Modelo guardado en '{MODEL_FILE}' ---") \ No newline at end of file