Loading README.md +8 −2 Changes for README.md: 8 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -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": { Loading api/schemas/analysis.py +2 −0 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 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/package.json +1 −0 Changes for etsi-dq-frontend/package.json: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", "test": "node tests/reliability-utils.test.mjs", "lint": "eslint .", "preview": "vite preview" }, Loading etsi-dq-frontend/src/pages/AnalysisPage.jsx +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 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 Loading
README.md +8 −2 Changes for README.md: 8 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -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": { Loading
api/schemas/analysis.py +2 −0 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 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/package.json +1 −0 Changes for etsi-dq-frontend/package.json: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", "test": "node tests/reliability-utils.test.mjs", "lint": "eslint .", "preview": "vite preview" }, Loading
etsi-dq-frontend/src/pages/AnalysisPage.jsx +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 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