Loading api/schemas/analysis.py +2 −0 Viewed Changes for api/schemas/analysis.py: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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] = {} # 지표별 요약 데이터 Loading @@ -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 # 샘플 데이터 api/services/analysis_service.py +17 −0 Viewed Changes for api/services/analysis_service.py: 17 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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 {}, Loading Loading @@ -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", {}), Loading Loading @@ -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 Loading @@ -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, ) Loading etsi-dq-frontend/src/pages/AnalysisPage.jsx +18 −16 Viewed Changes for etsi-dq-frontend/src/pages/AnalysisPage.jsx: 18 added lines, 16 removed lines. Original line number Diff line number Diff line Loading @@ -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", Loading Loading @@ -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); Loading Loading @@ -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(", ") || "-")] ); } Loading Loading @@ -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(", ") || "-")] ); } Loading Loading @@ -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); Loading @@ -481,6 +478,7 @@ export default function AnalysisPage() { label: METRIC_LABELS[metricKey] ?? metricKey, score, rawScore, cvBasis, status, rawStatus, reason, Loading Loading @@ -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 && ( Loading Loading @@ -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 etsi-dq-frontend/src/pages/MetricDetailPage.jsx +9 −20 Viewed 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", Loading @@ -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) { Loading @@ -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"; Loading etsi-dq-frontend/src/utils/reliability.js 0 → 100644 +53 −0 Viewed 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
api/schemas/analysis.py +2 −0 Viewed Changes for api/schemas/analysis.py: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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] = {} # 지표별 요약 데이터 Loading @@ -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 # 샘플 데이터
api/services/analysis_service.py +17 −0 Viewed Changes for api/services/analysis_service.py: 17 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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 {}, Loading Loading @@ -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", {}), Loading Loading @@ -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 Loading @@ -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, ) Loading
etsi-dq-frontend/src/pages/AnalysisPage.jsx +18 −16 Viewed Changes for etsi-dq-frontend/src/pages/AnalysisPage.jsx: 18 added lines, 16 removed lines. Original line number Diff line number Diff line Loading @@ -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", Loading Loading @@ -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); Loading Loading @@ -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(", ") || "-")] ); } Loading Loading @@ -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(", ") || "-")] ); } Loading Loading @@ -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); Loading @@ -481,6 +478,7 @@ export default function AnalysisPage() { label: METRIC_LABELS[metricKey] ?? metricKey, score, rawScore, cvBasis, status, rawStatus, reason, Loading Loading @@ -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 && ( Loading Loading @@ -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
etsi-dq-frontend/src/pages/MetricDetailPage.jsx +9 −20 Viewed 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", Loading @@ -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) { Loading @@ -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"; Loading
etsi-dq-frontend/src/utils/reliability.js 0 → 100644 +53 −0 Viewed 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 ); }