lct-hack/backend/app/scoring/group.py
2026-09-21 19:56:13 +03:00

69 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Воспроизводимая аналитика учебной группы по сохранённым оценкам.
Рекомендации здесь основаны на кодах ошибок, а не выдаются за вывод ИИ.
Повторные попытки учитываются в среднем балле, но один курсант не может
увеличить долю группы повторением одной и той же ошибки.
"""
from collections import Counter, defaultdict
from dataclasses import dataclass
from uuid import UUID
from app.domain.taxonomy import ERRORS, ErrorCode
RECOMMENDATIONS: dict[str, str] = {
"E1": "Повторить опросную карту и обязательные уточняющие вопросы.",
"E2": "Отработать классификацию происшествия и маршрутизацию по ЕКП.",
"E3": "Провести тренировку по временным нормативам.",
"E4": "Разобрать управление разговором и коммуникацию в стрессе.",
"E5": "Потренировать полноту заполнения карточки 112.",
"E6": "Повторить проверку карточки перед завершением и передачей.",
"D1": "Отработать первичную отметку ДДС в течение 30 секунд.",
"D2": "Сверять статусы реагирования с фактическими докладами.",
"D3": "Разобрать профильность обращений и допустимость отказа.",
"D4": "Тренировать обоснование отказа в комментарии.",
"D5": "Тренировать полноту комментария: действие и передача информации.",
"D6": "Повторить цепочку статусов выезда, прибытия и работ.",
}
@dataclass(frozen=True)
class ScoredAttempt:
trainee_id: UUID | None
score: float
codes: dict[str, int]
def summarize(attempts: list[ScoredAttempt], enrolled: int) -> dict:
"""Сводка только по завершённым попыткам с оценкой."""
active = {item.trainee_id for item in attempts if item.trainee_id is not None}
affected: dict[str, set[UUID]] = defaultdict(set)
occurrences: Counter[str] = Counter()
for attempt in attempts:
for code, count in attempt.codes.items():
if code not in RECOMMENDATIONS or not isinstance(count, int) or count <= 0:
continue
occurrences[code] += count
if attempt.trainee_id is not None:
affected[code].add(attempt.trainee_id)
errors = []
for code in occurrences:
errors.append({
"code": code,
"title": ERRORS[ErrorCode(code)].title,
"affected_trainees": len(affected[code]),
"occurrences": occurrences[code],
"rate_percent": round(100 * len(affected[code]) / len(active), 1) if active else 0.0,
"recommendation": RECOMMENDATIONS[code],
})
errors.sort(key=lambda item: (-item["affected_trainees"], -item["occurrences"], item["code"]))
return {
"enrolled_trainees": enrolled,
"active_trainees": len(active),
"scored_attempts": len(attempts),
"average_score": round(sum(item.score for item in attempts) / len(attempts), 1)
if attempts else None,
"errors": errors,
}