Files
breakpilot-lehrer/backend-lehrer/classroom_engine/analytics.py
Benjamin Admin bd4b956e3c [split-required] Split final 43 files (500-668 LOC) to complete refactoring
klausur-service (11 files):
- cv_gutter_repair, ocr_pipeline_regression, upload_api
- ocr_pipeline_sessions, smart_spell, nru_worksheet_generator
- ocr_pipeline_overlays, mail/aggregator, zeugnis_api
- cv_syllable_detect, self_rag

backend-lehrer (17 files):
- classroom_engine/suggestions, generators/quiz_generator
- worksheets_api, llm_gateway/comparison, state_engine_api
- classroom/models (→ 4 submodules), services/file_processor
- alerts_agent/api/wizard+digests+routes, content_generators/pdf
- classroom/routes/sessions, llm_gateway/inference
- classroom_engine/analytics, auth/keycloak_auth
- alerts_agent/processing/rule_engine, ai_processor/print_versions

agent-core (5 files):
- brain/memory_store, brain/knowledge_graph, brain/context_manager
- orchestrator/supervisor, sessions/session_manager

admin-lehrer (5 components):
- GridOverlay, StepGridReview, DevOpsPipelineSidebar
- DataFlowDiagram, sbom/wizard/page

website (2 files):
- DependencyMap, lehrer/abitur-archiv

Other: nibis_ingestion, grid_detection_service, export-doclayout-onnx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 09:41:42 +02:00

225 lines
8.3 KiB
Python

"""
Analytics-Modul fuer Classroom Engine (Phase 5).
Bietet Statistiken und Auswertungen fuer Unterrichtsstunden:
- Phasen-Dauer Statistiken
- Overtime-Analyse
- Lehrer-Dashboard Daten
- Post-Lesson Reflection
WICHTIG: Keine wertenden Metriken (z.B. "Sie haben 70% geredet").
Fokus auf neutrale, hilfreiche Statistiken.
"""
from datetime import datetime
from typing import Optional, List, Dict, Any
from .analytics_models import (
PhaseStatistics,
SessionSummary,
TeacherAnalytics,
LessonReflection,
)
# Re-export models for backward compatibility
__all__ = [
"PhaseStatistics",
"SessionSummary",
"TeacherAnalytics",
"LessonReflection",
"AnalyticsCalculator",
]
class AnalyticsCalculator:
"""Berechnet Analytics aus Session-Daten."""
PHASE_DISPLAY_NAMES = {
"einstieg": "Einstieg",
"erarbeitung": "Erarbeitung",
"sicherung": "Sicherung",
"transfer": "Transfer",
"reflexion": "Reflexion",
}
@classmethod
def calculate_session_summary(
cls,
session_data: Dict[str, Any],
phase_history: List[Dict[str, Any]]
) -> SessionSummary:
"""Berechnet die Zusammenfassung einer Session."""
session_id = session_data.get("session_id", "")
teacher_id = session_data.get("teacher_id", "")
class_id = session_data.get("class_id", "")
subject = session_data.get("subject", "")
topic = session_data.get("topic")
lesson_started = session_data.get("lesson_started_at")
lesson_ended = session_data.get("lesson_ended_at")
if isinstance(lesson_started, str):
lesson_started = datetime.fromisoformat(lesson_started.replace("Z", "+00:00"))
if isinstance(lesson_ended, str):
lesson_ended = datetime.fromisoformat(lesson_ended.replace("Z", "+00:00"))
total_duration = 0
if lesson_started and lesson_ended:
total_duration = int((lesson_ended - lesson_started).total_seconds())
phase_durations = session_data.get("phase_durations", {})
planned_duration = sum(phase_durations.values()) * 60
phase_stats = []
total_overtime = 0
phases_with_overtime = 0
total_pause_count = 0
total_pause_seconds = 0
phases_completed = 0
for entry in phase_history:
phase = entry.get("phase", "")
if phase in ["not_started", "ended"]:
continue
planned_seconds = phase_durations.get(phase, 0) * 60
actual_seconds = entry.get("duration_seconds", 0) or 0
difference = actual_seconds - planned_seconds
had_overtime = difference > 0
overtime_seconds = max(0, difference)
if had_overtime:
total_overtime += overtime_seconds
phases_with_overtime += 1
pause_count = entry.get("pause_count", 0) or 0
pause_seconds = entry.get("total_pause_seconds", 0) or 0
total_pause_count += pause_count
total_pause_seconds += pause_seconds
if entry.get("ended_at"):
phases_completed += 1
phase_stats.append(PhaseStatistics(
phase=phase,
display_name=cls.PHASE_DISPLAY_NAMES.get(phase, phase),
planned_duration_seconds=planned_seconds,
actual_duration_seconds=actual_seconds,
difference_seconds=difference,
had_overtime=had_overtime,
overtime_seconds=overtime_seconds,
was_extended=entry.get("was_extended", False),
extension_minutes=entry.get("extension_minutes", 0) or 0,
pause_count=pause_count,
total_pause_seconds=pause_seconds,
))
return SessionSummary(
session_id=session_id, teacher_id=teacher_id,
class_id=class_id, subject=subject, topic=topic,
date=lesson_started or datetime.now(),
total_duration_seconds=total_duration,
planned_duration_seconds=planned_duration,
phases_completed=phases_completed, total_phases=5,
phase_statistics=phase_stats,
total_overtime_seconds=total_overtime,
phases_with_overtime=phases_with_overtime,
total_pause_count=total_pause_count,
total_pause_seconds=total_pause_seconds,
)
@classmethod
def calculate_teacher_analytics(
cls,
sessions: List[Dict[str, Any]],
period_start: datetime,
period_end: datetime
) -> TeacherAnalytics:
"""Berechnet aggregierte Statistiken fuer einen Lehrer."""
if not sessions:
return TeacherAnalytics(teacher_id="", period_start=period_start, period_end=period_end)
teacher_id = sessions[0].get("teacher_id", "")
total_sessions = len(sessions)
completed_sessions = sum(1 for s in sessions if s.get("lesson_ended_at"))
total_minutes = 0
for session in sessions:
started = session.get("lesson_started_at")
ended = session.get("lesson_ended_at")
if started and ended:
if isinstance(started, str):
started = datetime.fromisoformat(started.replace("Z", "+00:00"))
if isinstance(ended, str):
ended = datetime.fromisoformat(ended.replace("Z", "+00:00"))
total_minutes += (ended - started).total_seconds() / 60
phase_durations_sum: Dict[str, List[int]] = {
"einstieg": [], "erarbeitung": [], "sicherung": [],
"transfer": [], "reflexion": [],
}
overtime_count = 0
overtime_seconds_total = 0
phase_overtime: Dict[str, int] = {}
pause_counts = []
pause_durations = []
subjects: Dict[str, int] = {}
classes: Dict[str, int] = {}
for session in sessions:
subject = session.get("subject", "")
class_id = session.get("class_id", "")
subjects[subject] = subjects.get(subject, 0) + 1
classes[class_id] = classes.get(class_id, 0) + 1
history = session.get("phase_history", [])
session_has_overtime = False
session_pause_count = 0
session_pause_duration = 0
phase_durations_dict = session.get("phase_durations", {})
for entry in history:
phase = entry.get("phase", "")
if phase in phase_durations_sum:
duration = entry.get("duration_seconds", 0) or 0
phase_durations_sum[phase].append(duration)
planned = phase_durations_dict.get(phase, 0) * 60
if duration > planned:
overtime = duration - planned
overtime_seconds_total += overtime
session_has_overtime = True
phase_overtime[phase] = phase_overtime.get(phase, 0) + overtime
session_pause_count += entry.get("pause_count", 0) or 0
session_pause_duration += entry.get("total_pause_seconds", 0) or 0
if session_has_overtime:
overtime_count += 1
pause_counts.append(session_pause_count)
pause_durations.append(session_pause_duration)
avg_durations = {}
for phase, durations in phase_durations_sum.items():
avg_durations[phase] = round(sum(durations) / len(durations)) if durations else 0
most_overtime_phase = None
if phase_overtime:
most_overtime_phase = max(phase_overtime, key=phase_overtime.get)
return TeacherAnalytics(
teacher_id=teacher_id, period_start=period_start, period_end=period_end,
total_sessions=total_sessions, completed_sessions=completed_sessions,
total_teaching_minutes=int(total_minutes),
avg_phase_durations=avg_durations,
sessions_with_overtime=overtime_count,
avg_overtime_seconds=overtime_seconds_total / max(total_sessions, 1),
most_overtime_phase=most_overtime_phase,
avg_pause_count=sum(pause_counts) / max(len(pause_counts), 1),
avg_pause_duration_seconds=sum(pause_durations) / max(len(pause_durations), 1),
subjects_taught=subjects, classes_taught=classes,
)