Phase 3 KPI 상태값 명시
This commit is contained in:
parent
1e9f293fda
commit
f7aae885b2
4 changed files with 42 additions and 21 deletions
|
|
@ -16,25 +16,13 @@ from pathlib import Path
|
||||||
from typing import Any, Iterable, Mapping, Sequence
|
from typing import Any, Iterable, Mapping, Sequence
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
PREPOST_MEASURE_NAMES = (
|
from .phase3_kpi_contract import (
|
||||||
"self_efficacy",
|
KPI_REPORT_PATH,
|
||||||
"skill_proficiency",
|
PHASE3_KPI_METRICS,
|
||||||
"training_satisfaction",
|
PREPOST_CSV_PATH,
|
||||||
|
PREPOST_MEASURE_NAMES,
|
||||||
|
PREPOST_TIMEPOINTS,
|
||||||
)
|
)
|
||||||
PREPOST_TIMEPOINTS = ("pre", "post")
|
|
||||||
PHASE3_KPI_METRICS = (
|
|
||||||
"embedding_consistency",
|
|
||||||
"hallucination_rate",
|
|
||||||
"icc",
|
|
||||||
"inter_rater_kappa",
|
|
||||||
"pilot_completion",
|
|
||||||
"self_efficacy_prepost",
|
|
||||||
"session_completion",
|
|
||||||
"sus",
|
|
||||||
"top1",
|
|
||||||
)
|
|
||||||
PREPOST_CSV_PATH = "02-measures/prepost_measures.csv"
|
|
||||||
KPI_REPORT_PATH = "02-measures/kpi_report.json"
|
|
||||||
|
|
||||||
|
|
||||||
class ParticipantKeys:
|
class ParticipantKeys:
|
||||||
|
|
@ -173,7 +161,7 @@ def build_kpi_report(
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
latest_rows = latest_prepost_rows(rows)
|
latest_rows = latest_prepost_rows(rows)
|
||||||
participants = {str(row.get("learner_id") or row.get("participant_id") or "") for row in latest_rows}
|
participants = {str(row.get("learner_id") or row.get("participant_id") or "") for row in latest_rows}
|
||||||
metrics = {name: _placeholder_metric(name) for name in PHASE3_KPI_METRICS}
|
metrics = {name: _design_pending_metric(name) for name in PHASE3_KPI_METRICS}
|
||||||
|
|
||||||
self_efficacy = paired_prepost_summary(latest_rows, "self_efficacy")
|
self_efficacy = paired_prepost_summary(latest_rows, "self_efficacy")
|
||||||
metrics["self_efficacy_prepost"] = {
|
metrics["self_efficacy_prepost"] = {
|
||||||
|
|
@ -183,6 +171,7 @@ def build_kpi_report(
|
||||||
"numerator": self_efficacy["complete_pairs"],
|
"numerator": self_efficacy["complete_pairs"],
|
||||||
"denominator": max(self_efficacy["participants_with_any_measure"], 0),
|
"denominator": max(self_efficacy["participants_with_any_measure"], 0),
|
||||||
"method": "paired normalized post-pre delta for pilot review; no official pass/fail gate",
|
"method": "paired normalized post-pre delta for pilot review; no official pass/fail gate",
|
||||||
|
"status": "computed_prepost",
|
||||||
"source_files": [PREPOST_CSV_PATH],
|
"source_files": [PREPOST_CSV_PATH],
|
||||||
"mean_pre": self_efficacy["mean_pre"],
|
"mean_pre": self_efficacy["mean_pre"],
|
||||||
"mean_post": self_efficacy["mean_post"],
|
"mean_post": self_efficacy["mean_post"],
|
||||||
|
|
@ -194,12 +183,13 @@ def build_kpi_report(
|
||||||
for measure_name in ("skill_proficiency", "training_satisfaction"):
|
for measure_name in ("skill_proficiency", "training_satisfaction"):
|
||||||
summary = paired_prepost_summary(latest_rows, measure_name)
|
summary = paired_prepost_summary(latest_rows, measure_name)
|
||||||
metrics[f"{measure_name}_prepost"] = {
|
metrics[f"{measure_name}_prepost"] = {
|
||||||
**_placeholder_metric(f"{measure_name}_prepost"),
|
**_design_pending_metric(f"{measure_name}_prepost"),
|
||||||
**summary,
|
**summary,
|
||||||
"value": summary["mean_delta"],
|
"value": summary["mean_delta"],
|
||||||
"numerator": summary["complete_pairs"],
|
"numerator": summary["complete_pairs"],
|
||||||
"denominator": summary["participants_with_any_measure"],
|
"denominator": summary["participants_with_any_measure"],
|
||||||
"method": "paired normalized post-pre delta for pilot review; not a required KPI gate yet",
|
"method": "paired normalized post-pre delta for pilot review; not a required KPI gate yet",
|
||||||
|
"status": "computed_prepost",
|
||||||
"source_files": [PREPOST_CSV_PATH],
|
"source_files": [PREPOST_CSV_PATH],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,13 +231,14 @@ def write_kpi_report(report: Mapping[str, Any], path: Path) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _placeholder_metric(name: str) -> dict[str, Any]:
|
def _design_pending_metric(name: str) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"value": 0.0,
|
"value": 0.0,
|
||||||
"threshold": 0.0,
|
"threshold": 0.0,
|
||||||
"pass": False,
|
"pass": False,
|
||||||
"numerator": 0,
|
"numerator": 0,
|
||||||
"denominator": 0,
|
"denominator": 0,
|
||||||
|
"status": "design_pending",
|
||||||
"method": f"not computed by prepost export scaffold: {name}",
|
"method": f"not computed by prepost export scaffold: {name}",
|
||||||
"source_files": [],
|
"source_files": [],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ class Phase3ArtifactCheckerTests(unittest.TestCase):
|
||||||
"pass": True,
|
"pass": True,
|
||||||
"numerator": 1,
|
"numerator": 1,
|
||||||
"denominator": 1,
|
"denominator": 1,
|
||||||
|
"status": "computed_prepost",
|
||||||
"method": "fixture",
|
"method": "fixture",
|
||||||
"source_files": ["fixture"],
|
"source_files": ["fixture"],
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +196,20 @@ class Phase3ArtifactCheckerTests(unittest.TestCase):
|
||||||
report.errors,
|
report.errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_kpi_metric_status_must_be_known(self) -> None:
|
||||||
|
root = self.make_root()
|
||||||
|
report_path = root / "02-measures" / "kpi_report.json"
|
||||||
|
data = json.loads(report_path.read_text(encoding="utf-8"))
|
||||||
|
data["metrics"]["sus"]["status"] = "maybe_later"
|
||||||
|
report_path.write_text(json.dumps(data), encoding="utf-8")
|
||||||
|
|
||||||
|
report = checker.validate(root, max_scan_rows=100)
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
any("metric 'sus' status 'maybe_later' is invalid" in error for error in report.errors),
|
||||||
|
report.errors,
|
||||||
|
)
|
||||||
|
|
||||||
def test_csv_enum_values_are_validated(self) -> None:
|
def test_csv_enum_values_are_validated(self) -> None:
|
||||||
root = self.make_root()
|
root = self.make_root()
|
||||||
write_text(
|
write_text(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ REQUIRED_METRIC_KEYS = {
|
||||||
"numerator",
|
"numerator",
|
||||||
"pass",
|
"pass",
|
||||||
"source_files",
|
"source_files",
|
||||||
|
"status",
|
||||||
"threshold",
|
"threshold",
|
||||||
"value",
|
"value",
|
||||||
}
|
}
|
||||||
|
|
@ -136,8 +137,10 @@ class Phase3KpiExportTests(unittest.TestCase):
|
||||||
self.assertTrue(REQUIRED_METRIC_KEYS.issubset(metric))
|
self.assertTrue(REQUIRED_METRIC_KEYS.issubset(metric))
|
||||||
self_efficacy = report["metrics"]["self_efficacy_prepost"]
|
self_efficacy = report["metrics"]["self_efficacy_prepost"]
|
||||||
self.assertFalse(self_efficacy["pass"])
|
self.assertFalse(self_efficacy["pass"])
|
||||||
|
self.assertEqual(self_efficacy["status"], "computed_prepost")
|
||||||
self.assertEqual(self_efficacy["value"], 25.0)
|
self.assertEqual(self_efficacy["value"], 25.0)
|
||||||
self.assertEqual(self_efficacy["source_files"], [PREPOST_CSV_PATH])
|
self.assertEqual(self_efficacy["source_files"], [PREPOST_CSV_PATH])
|
||||||
|
self.assertEqual(report["metrics"]["icc"]["status"], "design_pending")
|
||||||
|
|
||||||
def test_writers_create_phase3_evidence_files(self) -> None:
|
def test_writers_create_phase3_evidence_files(self) -> None:
|
||||||
rows = build_prepost_csv_rows(fixture_rows(), participant_keys=ParticipantKeys())
|
rows = build_prepost_csv_rows(fixture_rows(), participant_keys=ParticipantKeys())
|
||||||
|
|
|
||||||
|
|
@ -160,9 +160,14 @@ KPI_METRIC_REQUIRED_KEYS = {
|
||||||
"numerator",
|
"numerator",
|
||||||
"pass",
|
"pass",
|
||||||
"source_files",
|
"source_files",
|
||||||
|
"status",
|
||||||
"threshold",
|
"threshold",
|
||||||
"value",
|
"value",
|
||||||
}
|
}
|
||||||
|
KPI_METRIC_STATUSES = {
|
||||||
|
"computed_prepost",
|
||||||
|
"design_pending",
|
||||||
|
}
|
||||||
|
|
||||||
MANIFEST_KEYS = {
|
MANIFEST_KEYS = {
|
||||||
"agreement",
|
"agreement",
|
||||||
|
|
@ -342,6 +347,13 @@ def validate_kpi_report(root: Path, report: Report) -> None:
|
||||||
report.error(f"{rel_path}: metric '{metric_name}' missing '{key}'")
|
report.error(f"{rel_path}: metric '{metric_name}' missing '{key}'")
|
||||||
if "source_files" in metric and not isinstance(metric["source_files"], list):
|
if "source_files" in metric and not isinstance(metric["source_files"], list):
|
||||||
report.error(f"{rel_path}: metric '{metric_name}' source_files must be a list")
|
report.error(f"{rel_path}: metric '{metric_name}' source_files must be a list")
|
||||||
|
status = metric.get("status")
|
||||||
|
if status is not None and status not in KPI_METRIC_STATUSES:
|
||||||
|
expected = ", ".join(sorted(KPI_METRIC_STATUSES))
|
||||||
|
report.error(
|
||||||
|
f"{rel_path}: metric '{metric_name}' status '{status}' is invalid "
|
||||||
|
f"(expected one of: {expected})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def sha256_file(path: Path) -> str:
|
def sha256_file(path: Path) -> str:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue