@@ -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
513564def 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+
717803def test_browser_summary_report_non_regression ():
718804 probs , reals = _inputs ()
719805
0 commit comments