""" =============================================================================== FILE: services/scud_etl/anomaly_detector.py PROJECT: SCUD Orion AI (Unified Architecture) MODULE: services / scud_etl ROLE: Детектирование истинных аномалий и конфликтов реестров. (Сотрудники из exceptions.json, удаленка и командировки исключены). =============================================================================== """ import logging from typing import List, Dict, Any import pandas as pd logger = logging.getLogger("SCUD_ANOMALY") def detect_registry_anomalies(df_merged: pd.DataFrame) -> List[Dict[str, Any]]: """ Выявляет реальные аномалии: - Приход в офис во время отпуска или больничного листа. - Ошибки считывателей СКУД (наличие выхода при отсутствии отметки входа). """ anomalies = [] if df_merged is None or df_merged.empty: return anomalies for _, row in df_merged.iterrows(): # Пропускаем системные исключения if row.get("is_excluded", False): continue fio = row.get("fio_clean") or row.get("Сотрудник", "") start_day = str(row.get("Начало_дня", "")).strip() end_day = str(row.get("Конец_дня", "")).strip() reason = str(row.get("причина отсутствия", row.get("Вид_отсутствия", ""))).strip() reason_lower = reason.lower() # Пропускаем технические метки if "исключен" in reason_lower or "овк" in reason_lower: continue # ⭐️ Физический приход в офис: удаленка и командировки разрешены и НЕ являются аномалией if start_day not in ["Нет входа", "—", "", "nan", "None"] and reason and reason != "nan": if not ("удален" in reason_lower or "дистанцион" in reason_lower or "командировк" in reason_lower or "поездк" in reason_lower): anomalies.append({ "fio": fio, "type": "PHYSICAL_PRESENCE_DURING_ABSENCE", "description": f"Сотрудник пришел по СКУД ({start_day}), но в 1С оформлен документ: '{reason}'." }) # Аномалия оборудования (есть выход без утреннего входа) if start_day in ["Нет входа", "—", ""] and end_day not in ["Нет выхода", "—", "", "nan", "None"]: anomalies.append({ "fio": fio, "type": "SCUD_EQUIPMENT_ANOMALY", "description": f"Отсутствует отметка утреннего входа при наличии выхода ({end_day})." }) return anomalies