feat(etl): stable pipeline, exception registry in SQLite, multi-pass aggregation and db_cli
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
===============================================================================
|
||||
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
|
||||
Reference in New Issue
Block a user