homeroom-copilot / src /risk_engine.py
chand2345's picture
Implement risk assessment engine
b231a27
Raw
History Blame Contribute Delete
5.56 kB
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal, TypedDict
RiskLevel = Literal["Low", "Moderate", "High", "Critical"]
ENGAGEMENT_ATTENDANCE_WEIGHT = 0.4
ENGAGEMENT_HOMEWORK_WEIGHT = 0.4
ENGAGEMENT_GRADE_WEIGHT = 0.2
class RiskAssessment(TypedDict):
"""Structured risk output for a single student."""
attendance_risk: RiskLevel
academic_risk: RiskLevel
homework_risk: RiskLevel
behavior_risk: RiskLevel
engagement_score: float
engagement_risk: RiskLevel
overall_risk: RiskLevel
_RISK_ORDER: dict[RiskLevel, int] = {
"Low": 0,
"Moderate": 1,
"High": 2,
"Critical": 3,
}
@dataclass(frozen=True)
class StudentMetrics:
"""Input metrics used to calculate student risk levels.
Scores are expected on a 0-100 scale, while behavior incidents are a
non-negative count over the relevant reporting period.
"""
attendance: float
grades: float
homework: float
behavior_incidents: int
def validate_percentage(value: float, field_name: str) -> None:
"""Validate that a percentage value is within the inclusive 0-100 range.
Args:
value: Percentage value to validate.
field_name: Name of the field being validated.
Raises:
ValueError: If the value is less than 0 or greater than 100.
"""
if value < 0 or value > 100:
raise ValueError(
f"{field_name} must be between 0 and 100. Got {value}."
)
def attendance_risk(attendance: float) -> RiskLevel:
"""Classify risk from an attendance percentage."""
if attendance >= 95:
return "Low"
if attendance >= 90:
return "Moderate"
if attendance >= 80:
return "High"
return "Critical"
def academic_risk(grades: float) -> RiskLevel:
"""Classify risk from an academic grade percentage."""
if grades >= 90:
return "Low"
if grades >= 75:
return "Moderate"
if grades >= 60:
return "High"
return "Critical"
def homework_risk(homework: float) -> RiskLevel:
"""Classify risk from a homework completion percentage."""
if homework >= 90:
return "Low"
if homework >= 75:
return "Moderate"
if homework >= 50:
return "High"
return "Critical"
def behavior_risk(behavior_incidents: int) -> RiskLevel:
"""Classify risk from the number of behavior incidents."""
if behavior_incidents <= 0:
return "Low"
if behavior_incidents <= 2:
return "Moderate"
if behavior_incidents <= 5:
return "High"
return "Critical"
def engagement_score(attendance: float, homework: float, grades: float) -> float:
"""Calculate the weighted engagement score.
Formula: 0.4 * attendance + 0.4 * homework + 0.2 * grades.
"""
return round(
(ENGAGEMENT_ATTENDANCE_WEIGHT * attendance)
+ (ENGAGEMENT_HOMEWORK_WEIGHT * homework)
+ (ENGAGEMENT_GRADE_WEIGHT * grades),
2,
)
def engagement_risk(score: float) -> RiskLevel:
"""Classify risk from the weighted engagement score.
Engagement risk uses dedicated thresholds:
- Low: score >= 85
- Moderate: score >= 70 and score < 85
- High: score >= 55 and score < 70
- Critical: score < 55
"""
if score >= 85:
return "Low"
if score >= 70:
return "Moderate"
if score >= 55:
return "High"
return "Critical"
def overall_risk(*risks: RiskLevel) -> RiskLevel:
"""Return the highest risk level across all provided risk categories."""
return max(risks, key=lambda risk: _RISK_ORDER[risk])
def assess_student_risk(
attendance: float,
grades: float,
homework: float,
behavior_incidents: int,
) -> RiskAssessment:
"""Create a complete risk assessment for one student.
Args:
attendance: Attendance percentage from 0 to 100.
grades: Academic grade percentage from 0 to 100.
homework: Homework completion percentage from 0 to 100.
behavior_incidents: Count of behavior incidents.
Returns:
A dictionary containing category risk levels, engagement score,
engagement risk, and the overall highest risk level.
"""
validate_percentage(attendance, "attendance")
validate_percentage(grades, "grades")
validate_percentage(homework, "homework")
if behavior_incidents < 0:
raise ValueError("behavior_incidents must be non-negative.")
attendance_level = attendance_risk(attendance)
academic_level = academic_risk(grades)
homework_level = homework_risk(homework)
behavior_level = behavior_risk(behavior_incidents)
score = engagement_score(attendance, homework, grades)
engagement_level = engagement_risk(score)
return {
"attendance_risk": attendance_level,
"academic_risk": academic_level,
"homework_risk": homework_level,
"behavior_risk": behavior_level,
"engagement_score": score,
"engagement_risk": engagement_level,
"overall_risk": overall_risk(
attendance_level,
academic_level,
homework_level,
behavior_level,
engagement_level,
),
}
def assess_metrics(metrics: StudentMetrics) -> RiskAssessment:
"""Create a risk assessment from a StudentMetrics instance."""
return assess_student_risk(
attendance=metrics.attendance,
grades=metrics.grades,
homework=metrics.homework,
behavior_incidents=metrics.behavior_incidents,
)