Commit 78b41d73 authored by o's avatar o
Browse files

fix: keep imported reliability scores consistent

parent 660ba3c8
Loading
Loading
Loading
Loading
+8 −2
Changes for README.md: 8 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -220,11 +220,17 @@ metric results. The endpoint stores the imported result, returns a new
`analysis_id`, and makes the existing summary, PDF, and Excel endpoints
available without rerunning the analysis pipeline.

Every top-level `results[].score`, including Reliability, is a higher-is-better
quality score. With `score_scale: "ratio"`, the API converts `0..1` to
`0..100`. For Reliability, keep the raw CV percentage in
`detail.rel_overall_cv`; dashboard views use it only as the displayed CV basis
and do not grade the imported quality score a second time.

```json
{
  "source": "luc",
  "source": "ngsild-wrapper",
  "dataset_id": "stellio-air-quality-001",
  "report_name": "Luc data quality report",
  "report_name": "NGSI-LD data quality report",
  "score_scale": "ratio",
  "weighting_method": "custom",
  "metric_weights": {
+2 −0
Changes for api/schemas/analysis.py: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ class MetricSummary(BaseModel):
    score: Optional[float] = None
    status: str                               # "evaluated", "n/a", "error"
    reason: Optional[str] = None              # N/A인 경우 사유
    score_semantics: Optional[str] = None     # e.g. quality_percent for imported scores
    summary: dict[str, Any] = {}              # 지표별 요약 데이터


@@ -51,6 +52,7 @@ class MetricDetailResponse(BaseModel):
    analysis_id: str
    metric: str
    score: Optional[float] = None
    score_semantics: Optional[str] = None
    detail: dict[str, Any] = {}               # 지표별 상세 데이터
    rules: Optional[list[dict]] = None        # 규칙별 결과 (accuracy, consistency)
    samples: Optional[list[dict]] = None      # 샘플 데이터
+17 −0
Changes for api/services/analysis_service.py: 17 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -277,6 +277,9 @@ class AnalysisService:

            imported_results[metric] = {
                "score": score,
                # Imported top-level scores are normalized to 0..100 quality
                # scores. Reliability's raw CV remains in rel_overall_cv.
                "score_semantics": "quality_percent",
                "status": item.get("status", "evaluated"),
                "reason": item.get("reason"),
                "summary": item.get("summary", {}) or {},
@@ -900,11 +903,18 @@ class AnalysisService:
        if not stored:
            return None

        imported_quality_scores = bool(
            (stored.get("config") or {}).get("__scores_are_quality_percent")
        )
        summaries = []
        for metric, data in stored["results"].items():
            summaries.append(MetricSummary(
                metric=metric,
                score=data.get("score"),
                score_semantics=(
                    data.get("score_semantics")
                    or ("quality_percent" if imported_quality_scores else None)
                ),
                status=data.get("status", "evaluated"),
                reason=data.get("reason"),
                summary=data.get("summary", {}),
@@ -935,6 +945,9 @@ class AnalysisService:

        data = stored["results"][metric]
        detail = data.get("detail", {})
        imported_quality_scores = bool(
            (stored.get("config") or {}).get("__scores_are_quality_percent")
        )

        # 규칙별 결과 추출 (accuracy, consistency)
        rules = None
@@ -947,6 +960,10 @@ class AnalysisService:
            analysis_id=analysis_id,
            metric=metric,
            score=data.get("score"),
            score_semantics=(
                data.get("score_semantics")
                or ("quality_percent" if imported_quality_scores else None)
            ),
            detail=detail,
            rules=rules,
        )
+1 −0
Changes for etsi-dq-frontend/package.json: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -6,6 +6,7 @@
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "node tests/reliability-utils.test.mjs",
    "lint": "eslint .",
    "preview": "vite preview"
  },
+18 −16
Changes for etsi-dq-frontend/src/pages/AnalysisPage.jsx: 18 added lines, 16 removed lines.
Original line number Diff line number Diff line
@@ -3,6 +3,10 @@ import { useNavigate, useSearchParams } from "react-router-dom";
import Card from "../components/ui/Card";
import PageHeader from "../components/layout/PageHeader";
import { fetchAnalysisSummary } from "../api/client";
import {
  getReliabilityCvBasis,
  getReliabilityQualityScore,
} from "../utils/reliability";

const METRIC_LABELS = {
  completeness: "Completeness",
@@ -79,17 +83,6 @@ function getMetricScore(metricData) {
  );
}

function getReliabilityQualityScore(cvValue) {
  const cv = Number(cvValue);
  if (!Number.isFinite(cv)) return 0;

  if (cv <= 5) return 100;
  if (cv <= 15) return 80;
  if (cv <= 30) return 60;
  if (cv <= 50) return 40;
  return 20;
}

function formatMetricValue(metric) {
  if (metric.rawStatus === "n/a") return "N/A";
  return formatScore(metric.score);
@@ -343,7 +336,7 @@ function getMetricDetailRows(metric, detailResponse = null) {
      ["Evaluated Total", summary?.cons_checked_total ?? summary?.evaluated_total ?? "-"],
      ["Passed Total", summary?.cons_passed_total ?? summary?.passed_total ?? "-"],
      ["Violations", summary?.cons_violations_total ?? summary?.violations_total ?? "-"],
      ["Mode", summary?.cons_mode_used ?? (summary?.modes_used ?? []).join(", ") || "-"]
      ["Mode", summary?.cons_mode_used ?? ((summary?.modes_used ?? []).join(", ") || "-")]
    );
  }

@@ -380,7 +373,7 @@ function getMetricDetailRows(metric, detailResponse = null) {
      ["Duplicate Rate", summary?.uniq_duplicate_rate !== undefined ? `${formatScore(Number(summary.uniq_duplicate_rate) * 100)}%` : summary?.duplicate_rate !== undefined ? `${formatScore(Number(summary.duplicate_rate) * 100)}%` : "-"],
      ["Mode", summary?.uniq_mode_label ?? "-"],
      ["Basis", summary?.uniq_basis_label ?? summary?.basis ?? "-"],
      ["Key Columns", summary?.uniq_key_caption ?? (summary?.keys ?? []).join(", ") || "-"]
      ["Key Columns", summary?.uniq_key_caption ?? ((summary?.keys ?? []).join(", ") || "-")]
    );
  }

@@ -471,8 +464,12 @@ export default function AnalysisPage() {
      const rawScore = rawStatus === "n/a" ? 0 : getMetricScore(metricData);
      const score =
        metricKey === "reliability"
          ? getReliabilityQualityScore(rawScore)
          ? (getReliabilityQualityScore(metricData, rawScore) ?? 0)
          : rawScore;
      const cvBasis =
        metricKey === "reliability"
          ? getReliabilityCvBasis(metricData, rawScore)
          : null;
      const status = getMetricDisplayStatus(metricData, score, metricKey);
      const reason = getMetricReason(metricData);

@@ -481,6 +478,7 @@ export default function AnalysisPage() {
        label: METRIC_LABELS[metricKey] ?? metricKey,
        score,
        rawScore,
        cvBasis,
        status,
        rawStatus,
        reason,
@@ -633,7 +631,9 @@ export default function AnalysisPage() {
                )}
                {metric.key === "reliability" && metric.rawStatus !== "n/a" && (
                  <p className="mt-2 text-[10px] text-slate-400">
                    Calculated using CV basis: {formatScore(metric.rawScore)}%
                    {metric.cvBasis !== null
                      ? `Calculated using CV basis: ${formatScore(metric.cvBasis)}%`
                      : "CV basis unavailable"}
                  </p>
                )}
                {metric.reason && (
@@ -693,7 +693,9 @@ export default function AnalysisPage() {
              <div className="rounded-xl bg-slate-50 p-3">
                <p className="text-xs font-semibold text-slate-500">Reliability Basis</p>
                <p className="mt-1 text-xl font-bold text-slate-900">
                  {formatScore(metricMap.reliability.rawScore)}%
                  {metricMap.reliability.cvBasis !== null
                    ? `${formatScore(metricMap.reliability.cvBasis)}%`
                    : "-"}
                </p>
                <p className="mt-1 text-xs text-slate-400">
                  CV basis{reliabilitySummary?.representative_column ? ` · ${reliabilitySummary.representative_column}` : ""}
Loading