diff --git a/packages/data-designer-config/src/data_designer/config/analysis/column_profilers.py b/packages/data-designer-config/src/data_designer/config/analysis/column_profilers.py index 9b1ccec38..9bd3fc3e4 100644 --- a/packages/data-designer-config/src/data_designer/config/analysis/column_profilers.py +++ b/packages/data-designer-config/src/data_designer/config/analysis/column_profilers.py @@ -139,11 +139,12 @@ class JudgeScoreProfilerResults(ColumnProfilerResults): def create_report_section(self) -> Panel: layout = Table.grid(Column(), expand=True, padding=(2, 0)) + histograms = {} if isinstance(self.score_distributions, MissingValue) else self.score_distributions.histograms for score_name in self.summaries.keys(): layout.add_row( create_judge_score_summary_table( score_name=score_name, - histogram=self.score_distributions.histograms[score_name], + histogram=histograms.get(score_name, MissingValue.CALCULATION_FAILED), summary=self.summaries[score_name].summary, ) ) diff --git a/packages/data-designer-config/src/data_designer/config/analysis/utils/reporting.py b/packages/data-designer-config/src/data_designer/config/analysis/utils/reporting.py index a4131483b..436e06f00 100644 --- a/packages/data-designer-config/src/data_designer/config/analysis/utils/reporting.py +++ b/packages/data-designer-config/src/data_designer/config/analysis/utils/reporting.py @@ -15,6 +15,7 @@ from rich.table import Column, Table from rich.text import Text +from data_designer.config.analysis.column_statistics import MissingValue from data_designer.config.analysis.utils.errors import AnalysisReportError from data_designer.config.column_types import ( DataDesignerColumnType, @@ -163,15 +164,20 @@ def generate_analysis_report( def create_judge_score_summary_table( score_name: str, - histogram: CategoricalHistogramData, + histogram: CategoricalHistogramData | MissingValue, summary: str, accent_style: str = ACCENT_STYLE, summary_border_style: str = "dim", ) -> Table: layout = Table.grid(Column(), Column(), expand=True, padding=(0, 2)) + histogram_data = ( + {} + if isinstance(histogram, MissingValue) + else {str(s): c for s, c in zip(histogram.categories, histogram.counts)} + ) histogram_table = create_rich_histogram_table( - {str(s): c for s, c in zip(histogram.categories, histogram.counts)}, + histogram_data, ("score", "count"), name_style=HIST_NAME_STYLE, value_style=HIST_VALUE_STYLE, diff --git a/packages/data-designer-config/src/data_designer/config/utils/visualization.py b/packages/data-designer-config/src/data_designer/config/utils/visualization.py index 2bd9772ab..ad13cd248 100644 --- a/packages/data-designer-config/src/data_designer/config/utils/visualization.py +++ b/packages/data-designer-config/src/data_designer/config/utils/visualization.py @@ -262,11 +262,14 @@ def create_rich_histogram_table( table.add_column(column_names[0], justify="right", style=name_style) table.add_column(column_names[1], justify="left", style=value_style) - max_count = max(data.values()) + max_count = max(data.values(), default=0) for name, value in data.items(): bar = "" if max_count <= 0 else "█" * int((value / max_count) * 20) table.add_row(str(name), f"{bar} {value:.1f}") + if not data: + table.add_row("[dim]no data[/dim]", "") + return table diff --git a/packages/data-designer-config/tests/config/analysis/test_dataset_profiler_results.py b/packages/data-designer-config/tests/config/analysis/test_dataset_profiler_results.py index 1c5d7ab00..94c03ba51 100644 --- a/packages/data-designer-config/tests/config/analysis/test_dataset_profiler_results.py +++ b/packages/data-designer-config/tests/config/analysis/test_dataset_profiler_results.py @@ -1,13 +1,57 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +from copy import deepcopy from pathlib import Path from unittest.mock import patch +import pytest + from data_designer.config.analysis.column_statistics import GeneralColumnStatistics, MissingValue from data_designer.config.analysis.dataset_profiler import DatasetProfilerResults from data_designer.config.utils.constants import EPSILON +_ISSUE_DOCUMENT = { + "num_records": 1, + "target_num_records": 1, + "column_statistics": [ + { + "column_type": "general", + "column_name": "text", + "num_records": 1, + "num_null": 0, + "num_unique": 1, + "pyarrow_dtype": "string", + "simple_dtype": "str", + } + ], + "column_profiles": [ + { + "column_name": "quality", + "summaries": {"helpfulness": {"score_name": "helpfulness", "summary": "ok", "score_samples": []}}, + "score_distributions": { + "scores": {"helpfulness": [5]}, + "reasoning": {"helpfulness": ["ok"]}, + "distribution_types": {"helpfulness": "categorical"}, + "distributions": {"helpfulness": "--"}, + "histograms": {"helpfulness": {"categories": [5], "counts": [1]}}, + }, + } + ], +} + +_DISTS = _ISSUE_DOCUMENT["column_profiles"][0]["score_distributions"] + + +def _dists(histograms: dict) -> dict: + return {**_DISTS, "histograms": histograms} + + +def _judge_profile_document(score_distributions: object) -> dict: + document = deepcopy(_ISSUE_DOCUMENT) + document["column_profiles"][0]["score_distributions"] = score_distributions + return document + def test_dataset_profiler_results_creation(sample_dataset_profiler_results): """Test that DatasetProfilerResults can be created with valid data.""" @@ -158,3 +202,24 @@ def test_dataset_profiler_results_from_dict(): assert result.target_num_records == 200 assert len(result.column_statistics) == 1 assert result.column_statistics[0].column_name == "test_col" + + +@pytest.mark.parametrize( + "score_distributions", + [ + _dists({"helpfulness": {"categories": [], "counts": []}}), + _dists({"helpfulness": "--"}), + "--", + _dists({}), + _DISTS, + ], + ids=["empty", "missing-value", "no-distributions", "no-histogram", "populated"], +) +def test_to_report_renders_degenerate_judge_histograms(score_distributions: object, tmp_path: Path) -> None: + """Test that to_report renders judge profiles whose histogram data is empty or missing.""" + results = DatasetProfilerResults.model_validate(_judge_profile_document(score_distributions)) + report = tmp_path / "report.html" + + results.to_report(report) + + assert report.stat().st_size > 0 diff --git a/packages/data-designer-config/tests/config/utils/test_visualization.py b/packages/data-designer-config/tests/config/utils/test_visualization.py index 0042d61f9..32d7c4dc6 100644 --- a/packages/data-designer-config/tests/config/utils/test_visualization.py +++ b/packages/data-designer-config/tests/config/utils/test_visualization.py @@ -18,6 +18,7 @@ WithRecordSamplerMixin, apply_html_post_processing, convert_to_row_element, + create_rich_histogram_table, display_sample_record, get_truncated_list_as_string, mask_api_key, @@ -336,3 +337,17 @@ def __init__(self, dataset: pd.DataFrame, config_builder: DataDesignerConfigBuil with pytest.raises(DatasetSampleDisplayError, match="out of bounds"): results.display_sample_record(index=999) + + +@pytest.mark.parametrize( + ("data", "expected_rows", "expected_bars"), + [({}, 1, [""]), ({"a": 0, "b": 0}, 2, ["", ""]), ({"a": 1, "b": 3}, 2, ["\u2588" * 6, "\u2588" * 20])], + ids=["empty", "all-zero", "populated"], +) +def test_create_rich_histogram_table_handles_empty_and_zero_counts( + data: dict[str, int], expected_rows: int, expected_bars: list[str] +) -> None: + """Test that histogram rendering survives an empty mapping and all-zero counts.""" + table = create_rich_histogram_table(data, ("score", "count")) + assert table.row_count == expected_rows + assert [str(cell).split(" ")[0] for cell in table.columns[1].cells] == expected_bars