72 lines
1.9 KiB
Python
72 lines
1.9 KiB
Python
"""Replaceable baseline risk scoring engine."""
|
|
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
|
|
|
|
class RiskLevel(StrEnum):
|
|
LOW = "low"
|
|
MODERATELY_LOW = "moderately_low"
|
|
MEDIUM = "medium"
|
|
HIGH = "high"
|
|
VERY_HIGH = "very_high"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class IndicatorValue:
|
|
name: str
|
|
value: float
|
|
weight: float
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class FactorContribution:
|
|
name: str
|
|
value: float
|
|
weight: float
|
|
contribution: float
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RiskScore:
|
|
score: float
|
|
level: RiskLevel
|
|
factors: tuple[FactorContribution, ...]
|
|
|
|
|
|
class BaselineRiskEngine:
|
|
"""Weighted safety-score baseline following the PRD scoring direction.
|
|
|
|
Input indicator values represent risk intensity (0 = no observed risk,
|
|
100 = maximum risk). The public score is the inverse safety score, so a
|
|
higher result always means a safer assessment.
|
|
"""
|
|
|
|
model_version = "baseline-safety-v2"
|
|
|
|
def predict(self, indicators: list[IndicatorValue]) -> RiskScore:
|
|
total_weight = sum(item.weight for item in indicators)
|
|
factors = tuple(
|
|
FactorContribution(
|
|
name=item.name,
|
|
value=item.value,
|
|
weight=item.weight,
|
|
contribution=round((100 - item.value) * item.weight / total_weight, 2),
|
|
)
|
|
for item in indicators
|
|
)
|
|
score = round(sum(item.contribution for item in factors), 2)
|
|
return RiskScore(score=score, level=self._classify(score), factors=factors)
|
|
|
|
@staticmethod
|
|
def _classify(score: float) -> RiskLevel:
|
|
if score >= 80:
|
|
return RiskLevel.LOW
|
|
if score >= 60:
|
|
return RiskLevel.MODERATELY_LOW
|
|
if score >= 40:
|
|
return RiskLevel.MEDIUM
|
|
if score >= 20:
|
|
return RiskLevel.HIGH
|
|
return RiskLevel.VERY_HIGH
|