docs: update CHANGELOG and ROADMAP with section 10 (svodka/otchet split, Y-23:59:59)
This commit is contained in:
@@ -1,32 +1,44 @@
|
||||
"""
|
||||
===============================================================================
|
||||
FILE: services/scud_etl/anomaly_detector.py
|
||||
PROJECT: SCUD Orion AI (Unified Architecture)
|
||||
MODULE: services / scud_etl
|
||||
ROLE: Детектирование истинных аномалий и конфликтов реестров.
|
||||
(Сотрудники из exceptions.json, удаленка и командировки исключены).
|
||||
ROLE: Детектирование аномалий СКУД, дубликатов пропусков и несоответствий с 1С.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Any
|
||||
import pandas as pd
|
||||
from config import normalize_fio
|
||||
|
||||
logger = logging.getLogger("SCUD_ANOMALY")
|
||||
|
||||
|
||||
def detect_registry_anomalies(df_merged: pd.DataFrame) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Выявляет реальные аномалии:
|
||||
- Приход в офис во время отпуска или больничного листа.
|
||||
- Ошибки считывателей СКУД (наличие выхода при отсутствии отметки входа).
|
||||
"""
|
||||
def detect_registry_anomalies(df_merged: pd.DataFrame, df_raw_scud: pd.DataFrame = None) -> List[Dict[str, Any]]:
|
||||
anomalies = []
|
||||
if df_merged is None or df_merged.empty:
|
||||
return anomalies
|
||||
|
||||
# 1. ⭐️ Поиск дубликатов учеток/пропусков в сыром СКУД
|
||||
if df_raw_scud is not None and not df_raw_scud.empty:
|
||||
df_raw = df_raw_scud.copy()
|
||||
if 'fio_clean' not in df_raw.columns:
|
||||
f_col = 'Сотрудник' if 'Сотрудник' in df_raw.columns else 'ФИО'
|
||||
df_raw['fio_clean'] = df_raw[f_col].apply(normalize_fio)
|
||||
|
||||
counts = df_raw['fio_clean'].value_counts()
|
||||
duplicate_fios = counts[counts > 1].index.tolist()
|
||||
|
||||
for dup_fio in duplicate_fios:
|
||||
sub = df_raw[df_raw['fio_clean'] == dup_fio]
|
||||
departments = ", ".join(sub['Подразделение'].astype(str).unique())
|
||||
anomalies.append({
|
||||
"fio": dup_fio,
|
||||
"type": "DUPLICATE_SCUD_CARD",
|
||||
"description": f"Сотрудник заведен в СКУД {len(sub)} раза (отделы: {departments}). События входа и выхода объединены автоматически."
|
||||
})
|
||||
|
||||
# 2. Поиск кадровых аномалий и сбоев оборудования
|
||||
for _, row in df_merged.iterrows():
|
||||
# Пропускаем системные исключения
|
||||
if row.get("is_excluded", False):
|
||||
continue
|
||||
|
||||
@@ -36,25 +48,24 @@ def detect_registry_anomalies(df_merged: pd.DataFrame) -> List[Dict[str, Any]]:
|
||||
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}'."
|
||||
"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})."
|
||||
"description": f"Зафиксирован выход ({end_day}) при отсутствии отметки утреннего входа."
|
||||
})
|
||||
|
||||
return anomalies
|
||||
Reference in New Issue
Block a user