chore: import upstream snapshot with attribution
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:28:58 +08:00
commit ba4be087d5
2316 changed files with 2668701 additions and 0 deletions
@@ -0,0 +1,22 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from scripts.tts_comparison_report.reporting.components.audio_report import prepare_audio_pairs
from scripts.tts_comparison_report.reporting.components.boxplots import BoxPlotsConfig
from scripts.tts_comparison_report.reporting.components.eval_report import prepare_eval_artifacts
__all__ = [
"BoxPlotsConfig",
"prepare_audio_pairs",
"prepare_eval_artifacts",
]
@@ -0,0 +1,98 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import random
import warnings
from scripts.tts_comparison_report.reporting.constants import SEED
from scripts.tts_comparison_report.reporting.models import AudioPair, BucketData, BucketStructure
_RNG = random.Random(SEED)
def _collect_audio_pairs(
benchmark_name: str,
bucket_baseline: BucketData,
bucket_candidate: BucketData,
bucket_structure: BucketStructure,
) -> list[AudioPair]:
baseline_paths = bucket_baseline.get_benchmark_audio_paths(benchmark_name)
candidate_paths = bucket_candidate.get_benchmark_audio_paths(benchmark_name)
baseline_meta = bucket_baseline.get_benchmark_sample_meta(benchmark_name, bucket_structure)
candidate_meta = bucket_candidate.get_benchmark_sample_meta(benchmark_name, bucket_structure)
pairs = []
if set(baseline_paths) != set(candidate_paths):
raise ValueError(f"Audio sample sets differ for benchmark '{benchmark_name}'.")
for name in baseline_paths:
if name not in candidate_paths or name not in baseline_meta or name not in candidate_meta:
raise ValueError(
f"Missing matched sample '{name}' in audio paths or metadata for benchmark '{benchmark_name}'."
)
if baseline_meta[name].sample_id != candidate_meta[name].sample_id:
raise ValueError(
f"Sample id mismatch for '{name}' in benchmark '{benchmark_name}'. "
"Probably you use different versions of buckets."
)
pair = AudioPair(
context_path=baseline_meta[name].context_path,
baseline_path=baseline_paths[name],
candidate_path=candidate_paths[name],
text=baseline_meta[name].gt_text,
)
pairs.append(pair)
pairs.sort(key=lambda p: p.baseline_path.stem)
return pairs
def prepare_audio_pairs(
bucket_baseline: BucketData,
bucket_candidate: BucketData,
bucket_structure: BucketStructure,
used_benchmarks: list[str],
samples_per_benchmark: int,
) -> dict[str, list[AudioPair]]:
"""Prepare audio pairs for the selected benchmarks.
Args:
bucket_baseline: Baseline bucket data.
bucket_candidate: Candidate bucket data.
used_benchmarks: Benchmark names to include in the audio report.
samples_per_benchmark: Maximum number of audio pairs to sample per benchmark.
Returns:
Mapping from benchmark name to sampled baseline/candidate audio pairs.
Raises:
ValueError: If benchmark audio sets or sample metadata are inconsistent.
"""
pairs = {}
for benchmark_name in used_benchmarks:
benchmark_pairs = _collect_audio_pairs(benchmark_name, bucket_baseline, bucket_candidate, bucket_structure)
sampled_pairs = _RNG.sample(benchmark_pairs, k=min(samples_per_benchmark, len(benchmark_pairs)))
if len(sampled_pairs) < samples_per_benchmark:
warnings.warn(
f"\nBenchmark '{benchmark_name}' contains only {len(sampled_pairs)} available paired samples, "
f"but {samples_per_benchmark} were requested.",
stacklevel=2,
)
pairs[benchmark_name] = sampled_pairs
return pairs
@@ -0,0 +1,221 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass, field
from io import BytesIO
from typing import Optional
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes
from matplotlib.patches import PathPatch
from scripts.tts_comparison_report.reporting.metrics import DistributionMetricSpec, DistributionMetricsRegistry
from scripts.tts_comparison_report.reporting.models import BucketData, StatTestResult, Winner
@dataclass
class BoxPlotsConfig:
"""Styling and layout configuration for generated benchmark box plots."""
font_family: str = "sans-serif"
font_list: list[str] = field(default_factory=lambda: ["Arial", "Helvetica", "DejaVu Sans"])
linewidth: float = 0.4
default_model_color: str = "#36454F"
winner_model_color: str = "#7393B3"
box_alpha: float = 0.35
grid_alpha: float = 0.4
fontsize: int = 6
fontsize_title: int = 8
widths: float = 0.6
mean_marker: str = "o"
mean_marker_color: str = "#CD5C5C"
mean_marker_size: float = 4.0
median_color: str = "black"
whisker_color: str = "#666666"
cap_color: str = "#666666"
outlier_color: str = "#708090"
outlier_marker: str = "o"
outlier_markersize: float = 3.0
outlier_alpha: float = 0.5
def _style_boxplot(
bp: dict[str, PathPatch],
metric: DistributionMetricSpec,
winner_lookup: dict[str, Winner],
cfg: BoxPlotsConfig,
) -> None:
for i, patch in enumerate(bp["boxes"]):
winner = winner_lookup[metric.report_name]
if (i == 0 and winner == Winner.baseline) or (i == 1 and winner == Winner.candidate):
color = cfg.winner_model_color
else:
color = cfg.default_model_color
patch.set_facecolor(color)
patch.set_alpha(cfg.box_alpha)
patch.set_edgecolor(color)
patch.set_linewidth(cfg.linewidth)
def _add_mean_ci_labels(
ax: Axes,
baseline: np.ndarray,
candidate: np.ndarray,
metric: DistributionMetricSpec,
cfg: BoxPlotsConfig,
) -> None:
for x, values in [(1, baseline), (2, candidate)]:
mean, median = values.mean(), np.median(values)
sem = values.std(ddof=1) / np.sqrt(len(values)) if len(values) > 1 else 0.0
ci95 = 1.96 * sem
label = f"{mean:.3f} ± {ci95:.3f}"
if metric.plot_range is not None:
range_ = metric.plot_range[1] - metric.plot_range[0]
else:
range_ = values.max() - values.min()
x_offset = 0.02
y_offset = 0.03 * range_
if median > mean and mean - y_offset > 0:
y_offset = -y_offset
ax.text(x + x_offset, mean + y_offset, label, ha="left", va="center", fontsize=cfg.fontsize)
def _configure_boxplot_axis(
ax: Axes,
metric: DistributionMetricSpec,
baseline_name: str,
candidate_name: str,
cfg: BoxPlotsConfig,
) -> None:
ax.set_title(metric.report_name, fontsize=cfg.fontsize_title)
ax.set_xticks([1, 2])
ax.set_xticklabels([baseline_name, candidate_name])
ax.tick_params(axis="x", labelsize=cfg.fontsize)
ax.tick_params(axis="y", labelsize=cfg.fontsize)
ax.grid(True, axis="y", linestyle="dotted", alpha=cfg.grid_alpha)
for spine in ax.spines.values():
spine.set_linewidth(cfg.linewidth)
ax.tick_params(axis="both", width=cfg.linewidth)
if metric.plot_range is not None:
ax.set_ylim(metric.plot_range[0], metric.plot_range[1])
def prepare_boxplots(
bucket_baseline: BucketData,
bucket_candidate: BucketData,
stat_test_results: list[StatTestResult],
cfg: BoxPlotsConfig,
benchmark_name: Optional[str] = None,
) -> BytesIO:
"""Create an in-memory box plot figure for summary or benchmark-level metrics.
Args:
bucket_baseline: Baseline bucket data.
bucket_candidate: Candidate bucket data.
stat_test_results: Statistical test results used to highlight the winning model.
cfg: Plot styling and layout configuration.
benchmark_name: Benchmark name. If omitted, metric samples are aggregated
across all benchmarks.
Returns:
PNG image stored in an in-memory bytes buffer.
"""
baseline_name = bucket_baseline.name
candidate_name = bucket_candidate.name
winner_lookup = {res.metric_name: res.winner for res in stat_test_results}
num_rows = sum(m.add_to_box_plot for m in DistributionMetricsRegistry)
fig_height = max(2.0 * num_rows, 4.5)
with plt.rc_context({"font.family": cfg.font_family, "font.sans-serif": cfg.font_list}):
fig, axs = plt.subplots(num_rows, 1, figsize=(6, fig_height), squeeze=False)
axs = axs.flatten()
plot_idx = 0
for metric in DistributionMetricsRegistry:
if not metric.add_to_box_plot:
continue
baseline = bucket_baseline.get_metric_samples(
metric_name=metric.key,
benchmark_name=benchmark_name,
)
candidate = bucket_candidate.get_metric_samples(
metric_name=metric.key,
benchmark_name=benchmark_name,
)
baseline = np.asarray(baseline, dtype=float)
candidate = np.asarray(candidate, dtype=float)
ax = axs[plot_idx]
plot_idx += 1
bp = ax.boxplot(
[baseline, candidate],
positions=[1, 2],
widths=cfg.widths,
patch_artist=True,
showmeans=True,
meanline=False,
meanprops={
"marker": cfg.mean_marker,
"markerfacecolor": cfg.mean_marker_color,
"markeredgecolor": cfg.mean_marker_color,
"markersize": cfg.mean_marker_size,
},
medianprops={
"color": cfg.median_color,
"linewidth": cfg.linewidth,
},
whiskerprops={
"color": cfg.whisker_color,
"linewidth": cfg.linewidth,
},
capprops={
"color": cfg.cap_color,
"linewidth": cfg.linewidth,
},
boxprops={
"linewidth": cfg.linewidth,
},
flierprops={
"marker": cfg.outlier_marker,
"markerfacecolor": cfg.outlier_color,
"markeredgecolor": cfg.outlier_color,
"markersize": cfg.outlier_markersize,
"alpha": cfg.outlier_alpha,
},
)
_style_boxplot(bp, metric, winner_lookup, cfg)
_add_mean_ci_labels(ax, baseline, candidate, metric, cfg)
_configure_boxplot_axis(ax, metric, baseline_name, candidate_name, cfg)
fig.tight_layout(rect=[0, 0, 1, 0.985])
buffer = BytesIO()
fig.savefig(buffer, format="png", dpi=300, bbox_inches="tight")
plt.close(fig)
buffer.seek(0)
return buffer
@@ -0,0 +1,97 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from scripts.tts_comparison_report.reporting.components.boxplots import BoxPlotsConfig, prepare_boxplots
from scripts.tts_comparison_report.reporting.components.metrics_table import (
prepare_benchmark_metrics_table_rows,
prepare_summary_metrics_table_rows,
)
from scripts.tts_comparison_report.reporting.components.stat_tests import (
prepare_stat_tests_analysis_info,
prepare_stat_tests_table_rows,
run_stat_tests,
)
from scripts.tts_comparison_report.reporting.models import BucketData, EvalArtifacts, EvalResult, ModelConfiguration
def prepare_eval_artifacts(
bucket_baseline: BucketData,
bucket_candidate: BucketData,
box_plots_cfg: BoxPlotsConfig,
) -> EvalArtifacts:
"""Prepare summary and benchmark-level evaluation artifacts for report rendering.
Args:
bucket_baseline: Baseline bucket data.
bucket_candidate: Candidate bucket data.
box_plots_cfg: Configuration used to generate benchmark and summary box plots.
Returns:
Evaluation artifacts containing configuration metadata, summary results,
and per-benchmark results.
"""
baseline_name = bucket_baseline.name
candidate_name = bucket_candidate.name
is_self_comparison = bucket_baseline.path == bucket_candidate.path
metrics_table_row = prepare_summary_metrics_table_rows(bucket_baseline, bucket_candidate)
stat_test_results = run_stat_tests(bucket_baseline, bucket_candidate)
stat_test_table_row = prepare_stat_tests_table_rows(baseline_name, candidate_name, stat_test_results)
stat_tests_analysis_info = prepare_stat_tests_analysis_info(baseline_name, candidate_name, stat_test_results)
box_plots = prepare_boxplots(
bucket_baseline=bucket_baseline,
bucket_candidate=bucket_candidate,
stat_test_results=stat_test_results,
cfg=box_plots_cfg,
)
configuration = ModelConfiguration(
baseline=bucket_baseline.configuration_str,
candidate=bucket_candidate.configuration_str,
)
summary = EvalResult(
metrics_table_row=metrics_table_row,
stat_test_table_row=stat_test_table_row,
stat_tests_analysis_info=stat_tests_analysis_info,
box_plots=box_plots,
)
benchmarks = {}
for benchmark_name in bucket_baseline.benchmarks:
metrics_table_row = prepare_benchmark_metrics_table_rows(benchmark_name, bucket_baseline, bucket_candidate)
stat_test_results = run_stat_tests(bucket_baseline, bucket_candidate, benchmark_name)
stat_test_table_row = prepare_stat_tests_table_rows(baseline_name, candidate_name, stat_test_results)
stat_tests_analysis_info = prepare_stat_tests_analysis_info(baseline_name, candidate_name, stat_test_results)
box_plots = prepare_boxplots(
bucket_baseline=bucket_baseline,
bucket_candidate=bucket_candidate,
stat_test_results=stat_test_results,
cfg=box_plots_cfg,
benchmark_name=benchmark_name,
)
benchmarks[benchmark_name] = EvalResult(
metrics_table_row=metrics_table_row,
stat_test_table_row=stat_test_table_row,
stat_tests_analysis_info=stat_tests_analysis_info,
box_plots=box_plots,
)
return EvalArtifacts(
configuration=configuration,
summary=summary,
benchmarks=benchmarks,
is_self_comparison=is_self_comparison,
)
@@ -0,0 +1,154 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import html
from typing import Optional
import numpy as np
from scripts.tts_comparison_report.reporting.metrics import MetricSpec, MetricsRegistry
from scripts.tts_comparison_report.reporting.models import BucketData
def _metric_comparator(
a: float,
b: float,
lower_is_better: Optional[bool],
) -> Optional[bool]:
if lower_is_better is None:
return None
if lower_is_better:
# If the values are equal, the baseline wins.
return a <= b
return a >= b
def _format_metric_values(
a: float,
b: float,
metric: MetricSpec,
) -> tuple[str, str]:
a, b = metric.multiplier * a, metric.multiplier * b
a_is_better = _metric_comparator(a, b, metric.lower_is_better)
a, b = round(a, metric.round_digits), round(b, metric.round_digits)
a_str, b_str = f"{a}{metric.units}", f"{b}{metric.units}"
a_str = html.escape(a_str)
b_str = html.escape(b_str)
if metric.lower_is_better is not None:
if a_is_better:
a_str = f"<strong>{a_str}</strong>"
else:
b_str = f"<strong>{b_str}</strong>"
return a_str, b_str
def prepare_benchmark_metrics_table_rows(
benchmark_name: str,
bucket_baseline: BucketData,
bucket_candidate: BucketData,
) -> list[list[str]]:
"""Prepare formatted metric rows for one benchmark comparison table.
Args:
benchmark_name: Name of the benchmark to render.
bucket_baseline: Baseline bucket data.
bucket_candidate: Candidate bucket data.
Returns:
Table rows containing metric names and formatted baseline/candidate values.
Raises:
ValueError: If a required metric is missing for the benchmark.
"""
rows = []
for metric in MetricsRegistry:
a = bucket_baseline.get_metric_avg_value(
metric_name=metric.key,
benchmark_name=benchmark_name,
)
b = bucket_candidate.get_metric_avg_value(
metric_name=metric.key,
benchmark_name=benchmark_name,
)
if a is None or b is None:
if metric.optional:
continue
raise ValueError(f"Unknown metric '{metric.key}' for benchmark '{benchmark_name}'.")
a_str, b_str = _format_metric_values(a, b, metric)
rows.append([html.escape(metric.report_name), a_str, b_str])
return rows
def prepare_summary_metrics_table_rows(
bucket_baseline: BucketData,
bucket_candidate: BucketData,
) -> list[list[str]]:
"""Prepare formatted metric rows for the summary comparison table.
Args:
bucket_baseline: Baseline bucket data.
bucket_candidate: Candidate bucket data.
Returns:
Table rows containing metric names and formatted macro-averaged
baseline/candidate values.
Raises:
ValueError: If a required metric is missing for any benchmark included
in the summary.
"""
rows = []
for metric in MetricsRegistry:
if not metric.include_in_summary:
continue
a_vals, b_vals = [], []
skip = False
for benchmark_name in bucket_baseline.benchmarks:
a = bucket_baseline.get_metric_avg_value(
metric_name=metric.key,
benchmark_name=benchmark_name,
)
b = bucket_candidate.get_metric_avg_value(
metric_name=metric.key,
benchmark_name=benchmark_name,
)
if a is None or b is None:
if metric.optional:
skip = True
break
raise ValueError(f"Unknown metric '{metric.key}' for benchmark '{benchmark_name}'.")
a_vals.append(a)
b_vals.append(b)
if skip:
continue
avg_a, avg_b = np.mean(a_vals), np.mean(b_vals)
a_str, b_str = _format_metric_values(avg_a, avg_b, metric)
rows.append([html.escape(metric.report_name), a_str, b_str])
return rows
@@ -0,0 +1,191 @@
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import html
import warnings
from enum import Enum
from typing import Optional
from scipy.stats import mannwhitneyu
from scripts.tts_comparison_report.reporting.constants import P_VAL_ROUND_DIGITS
from scripts.tts_comparison_report.reporting.metrics import DistributionMetricsRegistry
from scripts.tts_comparison_report.reporting.models import BucketData, StatTestAnalysisInfo, StatTestResult, Winner
_SIGNIFICANCE_LEVEL: float = 0.05
class _Alternative(str, Enum):
two_sided = "two-sided"
greater = "greater"
less = "less"
def _run_single_stat_test(
baseline: list[float],
candidate: list[float],
lower_is_better: bool,
) -> tuple[Winner, _Alternative, float]:
if not baseline:
raise ValueError("Baseline sample is empty.")
if not candidate:
raise ValueError("Candidate sample is empty.")
if len(baseline) != len(candidate):
warnings.warn(
"\nBaseline and candidate contain different numbers of samples. "
"This may indicate missing filewise metrics or dataset mismatch.",
stacklevel=2,
)
# First test whether distributions differ at all, then determine direction.
p_val_two_sided = mannwhitneyu(baseline, candidate, alternative="two-sided", method="auto").pvalue
if p_val_two_sided >= _SIGNIFICANCE_LEVEL:
return Winner.tie, _Alternative.two_sided, round(p_val_two_sided, P_VAL_ROUND_DIGITS)
p_val = mannwhitneyu(baseline, candidate, alternative="less", method="auto").pvalue
if p_val < _SIGNIFICANCE_LEVEL:
winner = Winner.baseline if lower_is_better else Winner.candidate
p_val = round(p_val, P_VAL_ROUND_DIGITS)
return winner, _Alternative.less, p_val
p_val = mannwhitneyu(baseline, candidate, alternative="greater", method="auto").pvalue
if p_val < _SIGNIFICANCE_LEVEL:
winner = Winner.candidate if lower_is_better else Winner.baseline
p_val = round(p_val, P_VAL_ROUND_DIGITS)
return winner, _Alternative.greater, p_val
return Winner.tie, _Alternative.two_sided, round(p_val_two_sided, P_VAL_ROUND_DIGITS)
def _map_winner_to_name(
winner: Winner,
baseline_name: str,
candidate_name: str,
) -> str:
if winner == Winner.baseline:
return baseline_name
if winner == Winner.candidate:
return candidate_name
return winner.value
def run_stat_tests(
bucket_baseline: BucketData,
bucket_candidate: BucketData,
benchmark_name: Optional[str] = None,
) -> list[StatTestResult]:
"""Run statistical tests for all distribution metrics.
Args:
bucket_baseline: Baseline bucket data.
bucket_candidate: Candidate bucket data.
benchmark_name: Benchmark name. If omitted, metric samples are aggregated
across all benchmarks.
Returns:
List of StatTestResult instances for configured distribution metrics.
Raises:
ValueError: If metric samples are missing or benchmark data is invalid.
"""
results = []
for metric in DistributionMetricsRegistry:
winner, alternative, p_value = _run_single_stat_test(
baseline=bucket_baseline.get_metric_samples(metric.key, benchmark_name),
candidate=bucket_candidate.get_metric_samples(metric.key, benchmark_name),
lower_is_better=metric.lower_is_better,
)
result = StatTestResult(
metric_name=metric.report_name,
winner=winner,
alternative=alternative.value,
p_value=p_value,
)
results.append(result)
return results
def prepare_stat_tests_table_rows(
baseline_name: str,
candidate_name: str,
stat_test_results: list[StatTestResult],
) -> list[list[str]]:
"""Prepare formatted rows for a statistical test results table.
Args:
baseline_name: Name of the baseline model used in the reports.
candidate_name: Name of the candidate model used in the reports.
stat_test_results: Statistical test results to format.
Returns:
Table rows containing metric name, winner, alternative hypothesis,
and p-value.
"""
rows = []
for res in stat_test_results:
winner = _map_winner_to_name(res.winner, baseline_name, candidate_name)
rows.append(
[
html.escape(res.metric_name),
html.escape(winner),
html.escape(res.alternative),
html.escape(str(res.p_value)),
]
)
return rows
def prepare_stat_tests_analysis_info(
baseline_name: str,
candidate_name: str,
stat_test_results: list[StatTestResult],
) -> StatTestAnalysisInfo:
"""Prepare summary information for the statistical test analysis section.
Args:
baseline_name: Name of the baseline model used in the reports.
candidate_name: Name of the candidate model used in the reports.
stat_test_results: Statistical test results to summarize.
Returns:
Instance of StatTestAnalysisInfo containing the overall winner and its
key advantages. If no statistically significant wins are present, both
fields are set to `None`.
"""
a_wins, b_wins = [], []
for res in stat_test_results:
if res.winner == Winner.baseline:
a_wins.append(res.metric_name)
elif res.winner == Winner.candidate:
b_wins.append(res.metric_name)
if not a_wins and not b_wins:
winner, advantages = None, None
else:
winner, wins = (baseline_name, a_wins) if len(a_wins) >= len(b_wins) else (candidate_name, b_wins)
advantages = ", ".join(wins)
return StatTestAnalysisInfo(
winner=winner,
advantages=advantages,
)