Implementación AnomalIA - Fix de dropdowns y permisos.
All checks were successful
Optimized Build and Deploy / remote-build-and-deploy (push) Successful in 5m17s

This commit is contained in:
2025-06-30 15:26:14 -03:00
parent 95aa09d62a
commit c96d259892
59 changed files with 1430 additions and 337 deletions

View File

@@ -0,0 +1,134 @@
import pandas as pd
import joblib
import os
import pyodbc
from datetime import datetime, timedelta
import sys
def insertar_alerta_en_db(cursor, tipo_alerta, id_entidad, entidad, mensaje, fecha_anomalia, cant_enviada=None, cant_devuelta=None, porc_devolucion=None):
"""Función centralizada para insertar en la nueva tabla Sistema_Alertas."""
insert_query = """
INSERT INTO Sistema_Alertas
(TipoAlerta, IdEntidad, Entidad, Mensaje, FechaAnomalia, CantidadEnviada, CantidadDevuelta, PorcentajeDevolucion, Leida)
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
cursor.execute(insert_query, tipo_alerta, id_entidad, entidad, mensaje, fecha_anomalia, c_env, c_dev, p_dev)
print(f"INFO: Alerta '{tipo_alerta}' para '{entidad}' ID {id_entidad} registrada.")
except Exception as e:
print(f"ERROR: No se pudo insertar la alerta para '{entidad}' ID {id_entidad}. Error: {e}")
print("--- INICIANDO SCRIPT DE DETECCIÓN COMPLETO ---")
# --- 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_INDIVIDUAL_FILE = 'modelo_anomalias.joblib'
MODEL_SISTEMA_FILE = 'modelo_sistema_anomalias.joblib'
# --- 2. Determinar Fecha ---
if len(sys.argv) > 1:
target_date = datetime.strptime(sys.argv[1], '%Y-%m-%d')
else:
target_date = datetime.now() - timedelta(days=1)
print(f"--- FECHA DE ANÁLISIS: {target_date.date()} ---")
try:
cnxn = pyodbc.connect(CONNECTION_STRING)
cursor = cnxn.cursor()
except Exception as e:
print(f"CRITICAL: No se pudo conectar a la base de datos. Error: {e}")
exit()
# --- 3. DETECCIÓN INDIVIDUAL (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:
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
FROM dist_EntradasSalidasCanillas esc
JOIN dist_dtCanillas c ON esc.Id_Canilla = c.Id_Canilla
WHERE CAST(Fecha AS DATE) = '{target_date.strftime('%Y-%m-%d')}' AND CantSalida > 0
"""
df_new = pd.read_sql(query_individual, cnxn)
if not df_new.empty:
df_new['porcentaje_devolucion'] = (df_new['cantidad_devuelta'] / df_new['cantidad_enviada']).fillna(0) * 100
df_new['dia_semana'] = pd.to_datetime(df_new['fecha']).dt.dayofweek
features = ['id_canilla', 'porcentaje_devolucion', 'dia_semana']
X_new = df_new[features]
df_new['anomalia'] = model_individual.predict(X_new)
anomalias_detectadas = df_new[df_new['anomalia'] == -1]
if not anomalias_detectadas.empty:
for index, row in anomalias_detectadas.iterrows():
mensaje = f"Devolución del {row['porcentaje_devolucion']:.2f}% para '{row['nombre_canilla']}'."
insertar_alerta_en_db(cursor,
tipo_alerta='DevolucionAnomala',
id_entidad=row['id_canilla'],
entidad='Canillita',
mensaje=mensaje,
fecha_anomalia=row['fecha'].date(),
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.")
else:
print("INFO: No hay datos de canillitas para analizar en la fecha seleccionada.")
# --- 4. DETECCIÓN 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:
model_sistema = joblib.load(MODEL_SISTEMA_FILE)
query_agregada = f"""
SELECT CAST(Fecha AS DATE) AS fecha_dia, DATEPART(weekday, Fecha) as dia_semana,
COUNT(DISTINCT Id_Canilla) as total_canillitas_activos,
SUM(CantSalida) as total_salidas, SUM(CantEntrada) as total_devoluciones
FROM dist_EntradasSalidasCanillas
WHERE CAST(Fecha AS DATE) = '{target_date.strftime('%Y-%m-%d')}' AND CantSalida > 0
GROUP BY CAST(Fecha AS DATE), DATEPART(weekday, Fecha)
"""
df_system = pd.read_sql(query_agregada, cnxn)
if not df_system.empty and df_system['total_salidas'].iloc[0] > 0:
df_system['ratio_devolucion'] = (df_system['total_devoluciones'] / df_system['total_salidas']).fillna(0)
df_system['salidas_por_canillita'] = (df_system['total_salidas'] / df_system['total_canillitas_activos']).fillna(0)
features_system = ['dia_semana', 'total_salidas', 'ratio_devolucion', 'salidas_por_canillita']
X_system = df_system[features_system]
df_system['anomalia_sistema'] = model_sistema.predict(X_system)
if df_system['anomalia_sistema'].iloc[0] == -1:
ratio_hoy = df_system['ratio_devolucion'].iloc[0] * 100
mensaje = f"El ratio de devolución global fue del {ratio_hoy:.2f}%, un valor atípico para este día de la semana."
insertar_alerta_en_db(cursor,
tipo_alerta='ComportamientoSistema',
id_entidad=0,
entidad='Sistema',
mensaje=mensaje,
fecha_anomalia=target_date.date())
else:
print("INFO: El comportamiento agregado del sistema fue normal.")
else:
mensaje = f"ALERTA GRAVE: No se registraron movimientos de salida para ningún canillita en la fecha {target_date.date()}."
insertar_alerta_en_db(cursor,
tipo_alerta='FaltaDeDatos',
id_entidad=0,
entidad='Sistema',
mensaje=mensaje,
fecha_anomalia=target_date.date())
# --- 5. Finalización ---
cnxn.commit()
cnxn.close()
print("\n--- DETECCIÓN COMPLETA ---")

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,65 @@
import pandas as pd
from sklearn.ensemble import IsolationForest
import joblib
import os
import pyodbc
from datetime import datetime, timedelta
print("--- INICIANDO SCRIPT DE ENTRENAMIENTO (CONEXIÓN BD) ---")
# --- 1. Configuración de Conexión y Parámetros ---
DB_SERVER = 'TECNICA3' # O el nombre de tu instancia, ej: '.\SQLEXPRESS'
DB_DATABASE = 'SistemaGestion' # El nombre de tu base de datos
# Para autenticación de Windows, usa la siguiente línea:
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.joblib'
CONTAMINATION_RATE = 0.001 # Tasa de contaminación del 0.013% (ajustable según tus necesidades)
# --- 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)
# Tomamos el último año de datos para el entrenamiento
fecha_limite = datetime.now() - timedelta(days=365)
query = f"""
SELECT
Id_Canilla AS id_canilla,
Fecha AS fecha,
CantSalida AS cantidad_enviada,
CantEntrada AS cantidad_devuelta
FROM
dist_EntradasSalidasCanillas
WHERE
Fecha >= '{fecha_limite.strftime('%Y-%m-%d')}'
"""
print("Ejecutando consulta para obtener datos de entrenamiento...")
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 en el último año. Saliendo.")
exit()
# --- 3. Preparación de Datos (sin cambios) ---
print(f"Preparando {len(df)} registros para el entrenamiento...")
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'] = df['fecha'].dt.dayofweek
features = ['id_canilla', 'porcentaje_devolucion', 'dia_semana']
X = df[features]
# --- 4. Entrenamiento y Guardado (sin cambios) ---
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 COMPLETADO. Modelo guardado en '{MODEL_FILE}' ---")

View File

@@ -0,0 +1,71 @@
import pandas as pd
from sklearn.ensemble import IsolationForest
import joblib
import os
import pyodbc
from datetime import datetime
print("--- INICIANDO SCRIPT DE ENTRENAMIENTO DE SISTEMA ---")
# --- 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_sistema_anomalias.joblib'
CONTAMINATION_RATE = 0.02 # Tasa de contaminación del 0.2% (ajustable según tus necesidades)
# --- 2. Carga y Agregación de Datos desde SQL Server ---
try:
print("Conectando a la base de datos...")
cnxn = pyodbc.connect(CONNECTION_STRING)
# Consulta para agregar los datos por día
query = """
SELECT
CAST(Fecha AS DATE) AS fecha_dia,
DATEPART(weekday, Fecha) as dia_semana, -- 1=Domingo, 2=Lunes...
COUNT(DISTINCT Id_Canilla) as total_canillitas_activos,
SUM(CantSalida) as total_salidas,
SUM(CantEntrada) as total_devoluciones
FROM
dist_EntradasSalidasCanillas
WHERE CantSalida > 0 -- Solo considerar días con actividad de salida
GROUP BY
CAST(Fecha AS DATE), DATEPART(weekday, Fecha)
"""
print("Ejecutando consulta de agregación de datos históricos...")
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 históricos para entrenar el modelo de sistema. Saliendo.")
exit()
# --- 3. Feature Engineering para el modelo de sistema ---
print(f"Preparando {len(df)} registros agregados para el entrenamiento...")
# El ratio de devolución es una característica muy potente
df['ratio_devolucion'] = (df['total_devoluciones'] / df['total_salidas']).fillna(0)
# Ratio de salidas por canillita activo
df['salidas_por_canillita'] = (df['total_salidas'] / df['total_canillitas_activos']).fillna(0)
# Seleccionamos las características que el modelo usará
features = ['dia_semana', 'total_salidas', 'ratio_devolucion', 'salidas_por_canillita']
X = df[features]
# --- 4. Entrenamiento del Modelo ---
print(f"Entrenando el modelo IsolationForest de sistema con tasa de contaminación de {CONTAMINATION_RATE}...")
model = IsolationForest(
n_estimators=100,
contamination=CONTAMINATION_RATE,
random_state=42
)
model.fit(X)
# --- 5. Guardado del Modelo ---
joblib.dump(model, MODEL_FILE)
print("--- ENTRENAMIENTO DE SISTEMA COMPLETADO ---")
print(f"Modelo de sistema guardado exitosamente como '{MODEL_FILE}'")