PrERT-CNM-Demo / src /prert /phase2 /scoring.py
JayNightmare's picture
Upload folder using huggingface_hub
64c8e6c verified
Raw
History Blame Contribute Delete
4.73 kB
"""Scoring logic for Phase 2 metric baseline runs."""
from __future__ import annotations
from collections import defaultdict
from typing import Any, Dict, Iterable, List, Tuple
from prert.phase2.metrics import compute_missing_penalty
from prert.phase2.types import MetricSpec, SyntheticObservation
LEVEL_WEIGHTS = {
"user": 0.4,
"system": 0.35,
"organization": 0.25,
}
def score_observations(
metric_specs: Iterable[MetricSpec],
observations: Iterable[SyntheticObservation],
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]]]:
spec_by_id = {spec.metric_id: spec for spec in metric_specs}
metric_rows: List[Dict[str, Any]] = []
level_accumulator: Dict[Tuple[str, str], List[float]] = defaultdict(list)
for obs in observations:
spec = spec_by_id.get(obs.metric_id)
if spec is None:
continue
raw_score = 1.0 - (obs.failure_count / max(obs.total_checks, 1))
normalized_score = _clamp(raw_score)
# B8: pull penalty formula from the centralised metrics module so
# spec.missing_data_handling and scoring stay in lockstep.
missing_penalty = compute_missing_penalty(obs.missing_fields)
confidence_adjusted_score = _clamp(
normalized_score * (1.0 - missing_penalty) * obs.observed_confidence * spec.confidence_weight
)
risk_score = _clamp(1.0 - confidence_adjusted_score)
metric_row = {
"row_type": "metric",
"scenario": obs.scenario,
"level": obs.level,
"entity_id": obs.entity_id,
"metric_id": obs.metric_id,
"control_id": spec.control_id,
"total_checks": obs.total_checks,
"failure_count": obs.failure_count,
"missing_fields": obs.missing_fields,
"raw_score": round(raw_score, 6),
"normalized_score": round(normalized_score, 6),
"confidence_adjusted_score": round(confidence_adjusted_score, 6),
"risk_score": round(risk_score, 6),
"risk_band": _risk_band(risk_score),
}
metric_rows.append(metric_row)
level_accumulator[(obs.scenario, obs.level)].append(confidence_adjusted_score)
level_rows = _build_level_rows(level_accumulator)
scenario_rows = _build_scenario_rows(level_rows)
return metric_rows, level_rows, scenario_rows
def _build_level_rows(level_accumulator: Dict[Tuple[str, str], List[float]]) -> List[Dict[str, Any]]:
rows: List[Dict[str, Any]] = []
for (scenario, level), values in sorted(level_accumulator.items()):
avg_score = sum(values) / max(len(values), 1)
risk_score = _clamp(1.0 - avg_score)
rows.append(
{
"row_type": "level_summary",
"scenario": scenario,
"level": level,
"sample_size": len(values),
"compliance_score": round(avg_score, 6),
"risk_score": round(risk_score, 6),
"risk_band": _risk_band(risk_score),
}
)
return rows
def _build_scenario_rows(level_rows: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
by_scenario: Dict[str, Dict[str, float]] = defaultdict(dict)
for row in level_rows:
scenario = str(row["scenario"])
level = str(row["level"])
by_scenario[scenario][level] = float(row["compliance_score"])
rows: List[Dict[str, Any]] = []
for scenario, score_map in sorted(by_scenario.items()):
weighted_score = 0.0
total_weight = 0.0
for level, weight in LEVEL_WEIGHTS.items():
if level in score_map:
weighted_score += score_map[level] * weight
total_weight += weight
if total_weight == 0:
continue
weighted_score /= total_weight
risk_score = _clamp(1.0 - weighted_score)
rows.append(
{
"row_type": "scenario_summary",
"scenario": scenario,
"composite_method": "weighted_sum_v1",
"level_weights": LEVEL_WEIGHTS,
"compliance_score": round(weighted_score, 6),
"risk_score": round(risk_score, 6),
"risk_band": _risk_band(risk_score),
}
)
return rows
def _risk_band(risk_score: float) -> str:
if risk_score >= 0.67:
return "high"
if risk_score >= 0.34:
return "medium"
return "low"
def _clamp(value: float) -> float:
return max(0.0, min(1.0, value))