Skip to content

Commit 9bcc3d9

Browse files
authored
Merge pull request #420 from uriahf/feat/align-summary-report-prediction-distribution-placement-1091887902099766664
Align Python browser summary report prediction distribution placement
2 parents d4c7721 + df998a8 commit 9bcc3d9

2 files changed

Lines changed: 150 additions & 46 deletions

File tree

‎src/rtichoke/summary_report/summary_report.py‎

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,34 @@ def _create_browser_summary_report(
481481
}
482482
],
483483
},
484+
{
485+
"id": "prediction-distribution",
486+
"title": "Prediction Distribution",
487+
"groups": [
488+
{
489+
"id": "prediction-distribution-probability-threshold",
490+
"title": "By Probability Threshold",
491+
"components": [
492+
{
493+
"id": "prediction-distribution",
494+
"title": "Prediction Distribution",
495+
"spec": threshold_prediction_distribution,
496+
}
497+
],
498+
},
499+
{
500+
"id": "prediction-distribution-ppcr",
501+
"title": "By Predicted Positives Condition Rate (PPCR)",
502+
"components": [
503+
{
504+
"id": "prediction-distribution-2",
505+
"title": "Prediction Distribution",
506+
"spec": ppcr_prediction_distribution,
507+
}
508+
],
509+
},
510+
],
511+
},
484512
{
485513
"id": "calibration",
486514
"title": "Calibration",
@@ -512,11 +540,6 @@ def _create_browser_summary_report(
512540
"id": "discrimination-probability-threshold",
513541
"title": "By Probability Threshold",
514542
"components": [
515-
{
516-
"id": "prediction-distribution",
517-
"title": "Prediction Distribution",
518-
"spec": threshold_prediction_distribution,
519-
},
520543
{"id": "roc", "title": "ROC", "spec": roc_thresh_spec},
521544
{
522545
"id": "precision-recall",
@@ -535,11 +558,6 @@ def _create_browser_summary_report(
535558
"id": "discrimination-ppcr",
536559
"title": "By PPCR",
537560
"components": [
538-
{
539-
"id": "prediction-distribution-2",
540-
"title": "Prediction Distribution",
541-
"spec": ppcr_prediction_distribution,
542-
},
543561
{"id": "roc-2", "title": "ROC", "spec": roc_ppcr_spec},
544562
{
545563
"id": "precision-recall-2",

‎tests/test_summary_report_browser.py‎

Lines changed: 122 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def test_browser_summary_report_is_opt_in_and_uses_real_canonical_components(
190190
section_ids = [section["id"] for section in report["sections"]]
191191
assert section_ids == [
192192
"prevalence",
193+
"prediction-distribution",
193194
"calibration",
194195
"discrimination",
195196
"utility",
@@ -262,8 +263,8 @@ def test_browser_summary_report_preserves_component_local_identity(tmp_path):
262263
assert "horizon" not in report
263264

264265
prev_spec = report["sections"][0]["items"][0]["spec"]
265-
calib_smooth_spec = report["sections"][1]["items"][0]["spec"]
266-
calib_discrete_spec = report["sections"][1]["items"][1]["spec"]
266+
calib_smooth_spec = report["sections"][2]["items"][0]["spec"]
267+
calib_discrete_spec = report["sections"][2]["items"][1]["spec"]
267268

268269
assert prev_spec["schemaVersion"] == "1.0"
269270
assert prev_spec["type"] == "summary_metrics"
@@ -405,11 +406,18 @@ def test_browser_summary_report_prediction_distribution_components_render(tmp_pa
405406
page.on("pageerror", lambda err: errors.append(str(err)))
406407

407408
page.goto(f"{base_url}/{output.name}")
408-
page.wait_for_selector("#discrimination")
409409

410-
# 1. Activate probability threshold group tab and verify prediction-distribution
410+
# 1. Activate top-level Prediction Distribution section tab
411+
pred_dist_section_tab = page.locator(
412+
"button[aria-controls='prediction-distribution']"
413+
)
414+
if pred_dist_section_tab.is_visible():
415+
pred_dist_section_tab.click()
416+
page.wait_for_selector("#prediction-distribution")
417+
418+
# 2. Activate probability threshold group tab and verify prediction-distribution
411419
thresh_tab = page.locator(
412-
"button[aria-controls='discrimination-probability-threshold']"
420+
"button[aria-controls='prediction-distribution-probability-threshold']"
413421
)
414422
thresh_tab.click()
415423

@@ -424,8 +432,8 @@ def test_browser_summary_report_prediction_distribution_components_render(tmp_pa
424432
assert bbox_thresh["width"] > 0
425433
assert bbox_thresh["height"] > 0
426434

427-
# 2. Activate PPCR group tab and verify prediction-distribution-2
428-
ppcr_tab = page.locator("button[aria-controls='discrimination-ppcr']")
435+
# 3. Activate PPCR group tab and verify prediction-distribution-2
436+
ppcr_tab = page.locator("button[aria-controls='prediction-distribution-ppcr']")
429437
ppcr_tab.click()
430438

431439
comp_ppcr = page.locator("[data-component-id='prediction-distribution-2']")
@@ -450,51 +458,90 @@ def test_browser_summary_report_structure_and_component_counts(tmp_path):
450458

451459
report = _embedded_report(output.read_text(encoding="utf-8"))
452460

453-
# 5 sections
461+
# Exactly 6 sections
454462
sections = report["sections"]
455-
assert len(sections) == 5
463+
assert len(sections) == 6
456464
section_ids = [s["id"] for s in sections]
457465
assert section_ids == [
458466
"prevalence",
467+
"prediction-distribution",
459468
"calibration",
460469
"discrimination",
461470
"utility",
462471
"performance-table",
463472
]
473+
section_titles = [s["title"] for s in sections]
474+
assert section_titles == [
475+
"Prevalence",
476+
"Prediction Distribution",
477+
"Calibration",
478+
"Discrimination",
479+
"Utility",
480+
"Performance Table",
481+
]
464482

465-
# Discrimination section
466-
disc_sec = sections[2]
467-
assert disc_sec["id"] == "discrimination"
483+
# Section 1: Prediction Distribution section
484+
pd_sec = sections[1]
485+
assert pd_sec["id"] == "prediction-distribution"
486+
assert pd_sec["title"] == "Prediction Distribution"
468487

469-
items = disc_sec["items"]
470-
assert len(items) == 3
471-
assert items[0]["id"] == "auroc"
488+
pd_items = pd_sec["items"]
489+
assert len(pd_items) == 2
472490

473491
# Group 1: By Probability Threshold
474-
grp_thresh = items[1]
475-
assert grp_thresh["id"] == "discrimination-probability-threshold"
476-
thresh_comps = grp_thresh["components"]
477-
assert [c["id"] for c in thresh_comps] == [
478-
"prediction-distribution",
492+
pd_thresh = pd_items[0]
493+
assert pd_thresh["id"] == "prediction-distribution-probability-threshold"
494+
assert pd_thresh["title"] == "By Probability Threshold"
495+
thresh_pd_comps = pd_thresh["components"]
496+
assert len(thresh_pd_comps) == 1
497+
assert thresh_pd_comps[0]["id"] == "prediction-distribution"
498+
assert thresh_pd_comps[0]["title"] == "Prediction Distribution"
499+
assert thresh_pd_comps[0]["spec"]["type"] == "prediction_distribution"
500+
assert (
501+
thresh_pd_comps[0]["spec"]["operatingPoint"]["dimension"]
502+
== "probability_threshold"
503+
)
504+
505+
# Group 2: By PPCR
506+
pd_ppcr = pd_items[1]
507+
assert pd_ppcr["id"] == "prediction-distribution-ppcr"
508+
assert pd_ppcr["title"] == "By Predicted Positives Condition Rate (PPCR)"
509+
ppcr_pd_comps = pd_ppcr["components"]
510+
assert len(ppcr_pd_comps) == 1
511+
assert ppcr_pd_comps[0]["id"] == "prediction-distribution-2"
512+
assert ppcr_pd_comps[0]["title"] == "Prediction Distribution"
513+
assert ppcr_pd_comps[0]["spec"]["type"] == "prediction_distribution"
514+
assert ppcr_pd_comps[0]["spec"]["operatingPoint"]["dimension"] == "ppcr"
515+
516+
# Section 3: Discrimination section
517+
disc_sec = sections[3]
518+
assert disc_sec["id"] == "discrimination"
519+
520+
disc_items = disc_sec["items"]
521+
assert len(disc_items) == 3
522+
assert disc_items[0]["id"] == "auroc"
523+
524+
# Discrimination Group 1: By Probability Threshold
525+
disc_grp_thresh = disc_items[1]
526+
assert disc_grp_thresh["id"] == "discrimination-probability-threshold"
527+
disc_thresh_comps = disc_grp_thresh["components"]
528+
assert [c["id"] for c in disc_thresh_comps] == [
479529
"roc",
480530
"precision-recall",
481531
"gains",
482532
"lift",
483533
]
484-
assert thresh_comps[0]["spec"]["type"] == "prediction_distribution"
485534

486-
# Group 2: By PPCR
487-
grp_ppcr = items[2]
488-
assert grp_ppcr["id"] == "discrimination-ppcr"
489-
ppcr_comps = grp_ppcr["components"]
490-
assert [c["id"] for c in ppcr_comps] == [
491-
"prediction-distribution-2",
535+
# Discrimination Group 2: By PPCR
536+
disc_grp_ppcr = disc_items[2]
537+
assert disc_grp_ppcr["id"] == "discrimination-ppcr"
538+
disc_ppcr_comps = disc_grp_ppcr["components"]
539+
assert [c["id"] for c in disc_ppcr_comps] == [
492540
"roc-2",
493541
"precision-recall-2",
494542
"gains-2",
495543
"lift-2",
496544
]
497-
assert ppcr_comps[0]["spec"]["type"] == "prediction_distribution"
498545

499546
# Collect all component IDs across report
500547
all_comp_ids = []
@@ -508,6 +555,10 @@ def test_browser_summary_report_structure_and_component_counts(tmp_path):
508555

509556
assert len(all_comp_ids) == 18
510557
assert len(set(all_comp_ids)) == 18
558+
assert "prediction-distribution" in all_comp_ids
559+
assert "prediction-distribution-2" in all_comp_ids
560+
assert all_comp_ids.count("prediction-distribution") == 1
561+
assert all_comp_ids.count("prediction-distribution-2") == 1
511562

512563

513564
def test_browser_summary_report_default_grid_and_standalone_identity(tmp_path):
@@ -517,9 +568,9 @@ def test_browser_summary_report_default_grid_and_standalone_identity(tmp_path):
517568

518569
report = _embedded_report(output.read_text(encoding="utf-8"))
519570

520-
disc_sec = report["sections"][2]
521-
embedded_thresh_spec = disc_sec["items"][1]["components"][0]["spec"]
522-
embedded_ppcr_spec = disc_sec["items"][2]["components"][0]["spec"]
571+
pd_sec = report["sections"][1]
572+
embedded_thresh_spec = pd_sec["items"][0]["components"][0]["spec"]
573+
embedded_ppcr_spec = pd_sec["items"][1]["components"][0]["spec"]
523574

524575
# Grid check 0.00, 0.01, ..., 1.00
525576
expected_grid = [round(x, 2) for x in np.linspace(0.0, 1.0, 101)]
@@ -640,7 +691,7 @@ def test_browser_summary_report_multiple_evaluations(tmp_path):
640691
)
641692
report_multi_model = _embedded_report(out_multi_model.read_text(encoding="utf-8"))
642693

643-
thresh_spec = report_multi_model["sections"][2]["items"][1]["components"][0]["spec"]
694+
thresh_spec = report_multi_model["sections"][1]["items"][0]["components"][0]["spec"]
644695
evals = thresh_spec["evaluations"]
645696
assert len(evals) == 2
646697
assert evals[0]["id"] == "evaluation-1"
@@ -664,7 +715,7 @@ def test_browser_summary_report_multiple_evaluations(tmp_path):
664715
)
665716
report_multi_pop = _embedded_report(out_multi_pop.read_text(encoding="utf-8"))
666717

667-
thresh_spec_pop = report_multi_pop["sections"][2]["items"][1]["components"][0][
718+
thresh_spec_pop = report_multi_pop["sections"][1]["items"][0]["components"][0][
668719
"spec"
669720
]
670721
evals_pop = thresh_spec_pop["evaluations"]
@@ -700,20 +751,55 @@ def test_browser_summary_report_authoritative_schema_validation(tmp_path):
700751
report_errors = list(report_validator.iter_errors(report_spec))
701752
assert not report_errors, f"Report schema validation errors: {report_errors}"
702753

703-
disc_sec = report_spec["sections"][2]
704-
thresh_pred_dist = disc_sec["items"][1]["components"][0]["spec"]
754+
pd_sec = report_spec["sections"][1]
755+
thresh_pred_dist = pd_sec["items"][0]["components"][0]["spec"]
705756
thresh_errors = list(v2_validator.iter_errors(thresh_pred_dist))
706757
assert not thresh_errors, (
707758
f"Threshold prediction distribution v2 schema errors: {thresh_errors}"
708759
)
709760

710-
ppcr_pred_dist = disc_sec["items"][2]["components"][0]["spec"]
761+
ppcr_pred_dist = pd_sec["items"][1]["components"][0]["spec"]
711762
ppcr_errors = list(v2_validator.iter_errors(ppcr_pred_dist))
712763
assert not ppcr_errors, (
713764
f"PPCR prediction distribution v2 schema errors: {ppcr_errors}"
714765
)
715766

716767

768+
def test_browser_summary_report_does_not_duplicate_producer_calls(
769+
monkeypatch, tmp_path
770+
):
771+
probs, reals = _inputs()
772+
output = tmp_path / "producer_counts.html"
773+
774+
perf_calls = []
775+
orig_perf = summary_report_module.prepare_performance_data
776+
777+
def spy_perf(*args, **kwargs):
778+
perf_calls.append(kwargs.get("stratified_by"))
779+
return orig_perf(*args, **kwargs)
780+
781+
dist_calls = []
782+
orig_dist = summary_report_module._prepare_probs_distribution_data
783+
784+
def spy_dist(*args, **kwargs):
785+
dist_calls.append(kwargs.get("stratified_by"))
786+
return orig_dist(*args, **kwargs)
787+
788+
def fail_create_probs_histogram(*args, **kwargs):
789+
raise AssertionError("report must not call public create_probs_histogram")
790+
791+
monkeypatch.setattr(summary_report_module, "prepare_performance_data", spy_perf)
792+
monkeypatch.setattr(
793+
summary_report_module, "_prepare_probs_distribution_data", spy_dist
794+
)
795+
monkeypatch.setattr(rtichoke, "create_probs_histogram", fail_create_probs_histogram)
796+
797+
create_summary_report(probs, reals, renderer="browser", output_file=output)
798+
799+
assert perf_calls == [("probability_threshold",), ("ppcr",)]
800+
assert dist_calls == [("probability_threshold",), ("ppcr",)]
801+
802+
717803
def test_browser_summary_report_non_regression():
718804
probs, reals = _inputs()
719805

0 commit comments

Comments
 (0)