Add external report import and fix reliability score consistency

Merge request reports

Loading
+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,
        )
+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}` : ""}
+9 −20
Changes for etsi-dq-frontend/src/pages/MetricDetailPage.jsx: 9 added lines, 20 removed lines.
Original line number Diff line number Diff line
import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { fetchMetricDetail } from "../api/client";
import {
  getReliabilityCvBasis,
  getReliabilityQualityScore,
} from "../utils/reliability";

const METRIC_LABELS = {
  completeness: "Completeness",
@@ -18,22 +22,7 @@ function formatScore(value) {
}

function getReliabilityDisplayScore(detail) {
  const payload = detail?.detail ?? {};
  const firstResult = payload.rel_detail_results?.[0] ?? payload.rel_results?.[0] ?? {};
  const level = firstResult.level;

  if (level === "High Reliability") return 100;
  if (level === "Moderate Reliability") return 80;
  if (level === "Low Reliability") return 40;

  const cv = Number(firstResult.cv ?? detail?.score);
  const highMax = Number(firstResult.high_max ?? 5);
  const moderateMax = Number(firstResult.moderate_max ?? 15);

  if (!Number.isFinite(cv)) return detail?.score;
  if (cv <= highMax) return 100;
  if (cv <= moderateMax) return 80;
  return 40;
  return getReliabilityQualityScore(detail, detail?.score);
}

function getMetricDisplayScore(detail) {
@@ -46,10 +35,10 @@ function getMetricScoreNote(detail) {
  if (!detail) return "/ 100";

  if (detail.metric === "reliability") {
    const payload = detail.detail ?? {};
    const firstResult = payload.rel_detail_results?.[0] ?? payload.rel_results?.[0] ?? {};
    const cv = firstResult.cv ?? detail.score;
    return `Reliability score / 100 · CV basis ${formatScore(cv)}%`;
    const cv = getReliabilityCvBasis(detail, detail.score);
    return cv !== null
      ? `Reliability score / 100 · CV basis ${formatScore(cv)}%`
      : "Reliability score / 100 · CV basis unavailable";
  }

  return "/ 100";
+53 −0
Changes for etsi-dq-frontend/src/utils/reliability.js: 53 added lines, 0 removed lines.
Original line number Diff line number Diff line
const DEFAULT_CV_THRESHOLDS = [5, 15, 30, 50];

export function hasQualityPercentSemantics(result) {
  return result?.score_semantics === "quality_percent";
}

export function getReliabilityCvBasis(result, fallbackCv = null) {
  const summary = result?.summary ?? {};
  const detail = result?.detail ?? {};

  const candidate =
    summary.rel_overall_cv ??
    summary.overall_cv ??
    detail.rel_overall_cv ??
    detail.overall_cv ??
    (hasQualityPercentSemantics(result) ? null : result?.score ?? fallbackCv);

  if (candidate === null || candidate === undefined || candidate === "") return null;
  const cv = Number(candidate);
  return Number.isFinite(cv) ? cv : null;
}

export function cvToReliabilityQualityScore(cvValue, thresholds = DEFAULT_CV_THRESHOLDS) {
  const cv = Math.abs(Number(cvValue));
  if (!Number.isFinite(cv)) return null;

  const parsedThresholds = thresholds.map(Number);
  const usableThresholds =
    parsedThresholds.length === 4 && parsedThresholds.every(Number.isFinite)
      ? parsedThresholds
      : DEFAULT_CV_THRESHOLDS;
  const [t0, t1, t2, t3] = usableThresholds;

  if (cv <= t0) return 100;
  if (cv <= t1) return 80;
  if (cv <= t2) return 60;
  if (cv <= t3) return 40;
  return 20;
}

export function getReliabilityQualityScore(result, fallbackScore = null) {
  if (hasQualityPercentSemantics(result)) {
    const qualityScore = Number(result?.score ?? fallbackScore);
    return Number.isFinite(qualityScore) ? qualityScore : null;
  }

  const detail = result?.detail ?? {};
  const thresholds = detail.rel_cv_thresholds ?? DEFAULT_CV_THRESHOLDS;
  return cvToReliabilityQualityScore(
    getReliabilityCvBasis(result, fallbackScore),
    thresholds
  );
}
Loading
Loading