Spaces:
Sleeping
Sleeping
| """Summarize an in-house SOAP review export without reading note content.""" | |
| from __future__ import annotations | |
| import argparse | |
| import csv | |
| import json | |
| from datetime import datetime | |
| from pathlib import Path | |
| REQUIRED_COLUMNS = ( | |
| "note_id", | |
| "clinician_id", | |
| "completeness", | |
| "hallucination", | |
| "terminology", | |
| "reviewed_at", | |
| "status", | |
| ) | |
| SCORE_LIMITS = {"completeness": (1, 5), "hallucination": (0, 3), "terminology": (1, 5)} | |
| STATUSES = {"accepted", "needs_correction", "unsafe"} | |
| def summarize_ratings(rows: list[dict[str, str]]) -> dict[str, object]: | |
| if not rows: | |
| raise ValueError("rating export has no rows") | |
| notes: set[str] = set() | |
| totals = {field: 0 for field in SCORE_LIMITS} | |
| unsafe = 0 | |
| serious_hallucinations = 0 | |
| for number, row in enumerate(rows, start=2): | |
| if set(row) != set(REQUIRED_COLUMNS): | |
| raise ValueError(f"row {number}: rating export columns do not match the approved schema") | |
| if any(not row[column].strip() for column in REQUIRED_COLUMNS): | |
| raise ValueError(f"row {number}: required rating value is empty") | |
| try: | |
| datetime.fromisoformat(row["reviewed_at"]) | |
| except ValueError as exc: | |
| raise ValueError(f"row {number}: reviewed_at must be ISO 8601") from exc | |
| notes.add(row["note_id"].strip()) | |
| for field, (low, high) in SCORE_LIMITS.items(): | |
| try: | |
| value = int(row[field]) | |
| except ValueError as exc: | |
| raise ValueError(f"row {number}: {field} must be an integer") from exc | |
| if not low <= value <= high: | |
| raise ValueError(f"row {number}: {field} must be between {low} and {high}") | |
| totals[field] += value | |
| if row["status"] not in STATUSES: | |
| raise ValueError(f"row {number}: status must be one of {sorted(STATUSES)}") | |
| unsafe += row["status"] == "unsafe" | |
| serious_hallucinations += int(row["hallucination"]) >= 2 | |
| count = len(rows) | |
| return { | |
| "rating_rows": count, | |
| "unique_notes": len(notes), | |
| "mean_completeness": round(totals["completeness"] / count, 2), | |
| "mean_hallucination": round(totals["hallucination"] / count, 2), | |
| "mean_terminology": round(totals["terminology"] / count, 2), | |
| "serious_hallucinations": serious_hallucinations, | |
| "unsafe_notes": unsafe, | |
| } | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--input", required=True, type=Path) | |
| args = parser.parse_args() | |
| with args.input.open(encoding="utf-8", newline="") as stream: | |
| summary = summarize_ratings(list(csv.DictReader(stream))) | |
| print(json.dumps(summary, ensure_ascii=False, indent=2)) | |
| if __name__ == "__main__": | |
| main() | |