@@ -512,3 +512,229 @@ def test_primary_golden_fixture_order_invariance():
512512 res_perm = _prepare_probs_distribution_data ({"m1" : probs_perm }, reals_perm , by = 0.20 )
513513
514514 assert res_orig ["rank_bins" ].equals (res_perm ["rank_bins" ])
515+
516+
517+ def test_frozen_r_oracle_ppcr_tied_fixture ():
518+ probs = {"m1" : np .array ([0.00 , 0.15 , 0.30 , 0.50 , 0.50 , 0.50 , 0.65 , 0.80 , 1.00 ])}
519+ reals = np .array ([0 , 1 , 0 , 1 , 0 , 1 , 1 , 0 , 1 ])
520+
521+ perf_df = prepare_performance_data (probs , reals , stratified_by = ("ppcr" ,), by = 0.20 )
522+ dist = _prepare_probs_distribution_data (
523+ probs , reals , stratified_by = ("ppcr" ,), by = 0.20
524+ )
525+ ops = dist ["operating_points" ]
526+
527+ # Independently hardcoded frozen R oracle expected values
528+ expected = [
529+ # req_ppcr, eff_cutoff, realized_ppcr, tp, fp, tn, fn, sens, spec, ppv, npv, lift
530+ (0.00 , 1.00 , 0.00 / 9.0 , 0 , 0 , 4 , 5 , 0.0 , 1.0 , np .nan , 4 / 9 , np .nan ),
531+ (0.20 , 0.71 , 2.00 / 9.0 , 1 , 1 , 3 , 4 , 0.2 , 0.75 , 0.5 , 3 / 7 , 0.9 ),
532+ (0.40 , 0.50 , 3.00 / 9.0 , 2 , 1 , 3 , 3 , 0.4 , 0.75 , 2 / 3 , 0.5 , 1.2 ),
533+ (0.60 , 0.50 , 3.00 / 9.0 , 2 , 1 , 3 , 3 , 0.4 , 0.75 , 2 / 3 , 0.5 , 1.2 ),
534+ (0.80 , 0.24 , 9.00 / 9.0 , 5 , 4 , 0 , 0 , 1.0 , 0.0 , 5 / 9 , np .nan , 1.0 ),
535+ (1.00 , 0.00 , 9.00 / 9.0 , 5 , 4 , 0 , 0 , 1.0 , 0.0 , 5 / 9 , np .nan , 1.0 ),
536+ ]
537+
538+ for (
539+ req_ppcr ,
540+ eff_cutoff ,
541+ real_ppcr ,
542+ tp ,
543+ fp ,
544+ tn ,
545+ fn ,
546+ sens ,
547+ spec ,
548+ ppv ,
549+ npv ,
550+ lift ,
551+ ) in expected :
552+ p_row = perf_df .filter (pl .col ("chosen_cutoff" ) == req_ppcr ).row (0 , named = True )
553+ op_row = ops .filter (pl .col ("value" ) == req_ppcr ).row (0 , named = True )
554+
555+ assert p_row ["ppcr" ] == pytest .approx (req_ppcr )
556+ assert p_row ["chosen_cutoff" ] == pytest .approx (req_ppcr )
557+ assert p_row ["probability_threshold" ] == pytest .approx (eff_cutoff , abs = 1e-4 )
558+
559+ assert op_row ["type" ] == "ppcr"
560+ assert op_row ["value" ] == pytest .approx (req_ppcr )
561+ assert op_row ["cutoff" ] == pytest .approx (eff_cutoff , abs = 1e-4 )
562+ assert op_row ["realized_ppcr" ] == pytest .approx (real_ppcr )
563+
564+ assert p_row ["true_positives" ] == tp
565+ assert p_row ["false_positives" ] == fp
566+ assert p_row ["true_negatives" ] == tn
567+ assert p_row ["false_negatives" ] == fn
568+
569+ assert p_row ["sensitivity" ] == pytest .approx (sens )
570+ assert p_row ["specificity" ] == pytest .approx (spec )
571+
572+ if np .isnan (ppv ):
573+ assert np .isnan (p_row ["ppv" ])
574+ else :
575+ assert p_row ["ppv" ] == pytest .approx (ppv )
576+
577+ if np .isnan (npv ):
578+ assert np .isnan (p_row ["npv" ])
579+ else :
580+ assert p_row ["npv" ] == pytest .approx (npv )
581+
582+ if np .isnan (lift ):
583+ assert np .isnan (p_row ["lift" ])
584+ else :
585+ assert p_row ["lift" ] == pytest .approx (lift )
586+
587+ # Concrete assertion that operating_point.cutoff != operating_point.value for tied PPCR point
588+ op_02 = ops .filter (pl .col ("value" ) == 0.20 ).row (0 , named = True )
589+ assert op_02 ["cutoff" ] != op_02 ["value" ]
590+ assert op_02 ["cutoff" ] == pytest .approx (0.71 )
591+ assert op_02 ["value" ] == pytest .approx (0.20 )
592+
593+
594+ def test_ppcr_repeated_effective_cutoffs_under_ties ():
595+ probs = {"m1" : np .array ([0.00 , 0.15 , 0.30 , 0.50 , 0.50 , 0.50 , 0.65 , 0.80 , 1.00 ])}
596+ reals = np .array ([0 , 1 , 0 , 1 , 0 , 1 , 1 , 0 , 1 ])
597+
598+ dist = _prepare_probs_distribution_data (
599+ probs , reals , stratified_by = ("ppcr" ,), by = 0.20
600+ )
601+ ops = dist ["operating_points" ]
602+
603+ op_04 = ops .filter (pl .col ("value" ) == 0.40 ).row (0 , named = True )
604+ op_06 = ops .filter (pl .col ("value" ) == 0.60 ).row (0 , named = True )
605+
606+ # Different requested value
607+ assert op_04 ["value" ] == pytest .approx (0.40 )
608+ assert op_06 ["value" ] == pytest .approx (0.60 )
609+ assert op_04 ["value" ] != op_06 ["value" ]
610+
611+ # Same effective cutoff, realized PPCR, and confusion matrix
612+ assert op_04 ["cutoff" ] == pytest .approx (op_06 ["cutoff" ]) == pytest .approx (0.50 )
613+ assert (
614+ op_04 ["realized_ppcr" ]
615+ == pytest .approx (op_06 ["realized_ppcr" ])
616+ == pytest .approx (3 / 9 )
617+ )
618+
619+
620+ def test_ppcr_n_less_than_q ():
621+ probs = {"m1" : np .array ([0.10 , 0.50 , 0.90 ])}
622+ reals = np .array ([0 , 1 , 1 ])
623+
624+ res = _prepare_probs_distribution_data (
625+ probs , reals , stratified_by = ("ppcr" ,), by = 0.20
626+ )
627+ rb = res ["rank_bins" ]
628+ ops = res ["operating_points" ]
629+
630+ # Explicit empty rank strata preserved (q = 5)
631+ assert len (rb ) == 5
632+ assert rb ["n_positive" ].to_list () == [0 , 0 , 1 , 0 , 1 ]
633+ assert rb ["n_negative" ].to_list () == [1 , 0 , 0 , 0 , 0 ]
634+
635+ # Verify operating point cutoffs for N < q
636+ cutoffs = ops ["cutoff" ].to_list ()
637+ assert len (cutoffs ) == 6
638+ assert np .allclose (cutoffs , [0.9 , 0.74 , 0.58 , 0.42 , 0.26 , 0.1 ])
639+
640+
641+ def test_ppcr_distinct_predictions_type7_boundaries ():
642+ probs = {"m1" : np .array ([0.10 , 0.30 , 0.70 , 0.90 ])}
643+ reals = np .array ([0 , 1 , 0 , 1 ])
644+
645+ res = _prepare_probs_distribution_data (
646+ probs , reals , stratified_by = ("ppcr" ,), by = 0.25
647+ )
648+ ops = res ["operating_points" ]
649+
650+ # Expected Type-7 linear quantiles at 1.0, 0.75, 0.50, 0.25, 0.0
651+ expected_cutoffs = [0.90 , 0.75 , 0.50 , 0.25 , 0.10 ]
652+ for row , exp_c in zip (ops .iter_rows (named = True ), expected_cutoffs ):
653+ assert row ["cutoff" ] == pytest .approx (exp_c )
654+
655+
656+ def test_ppcr_exact_scores_zero_and_one_endpoints ():
657+ probs = {"m1" : np .array ([0.0 , 0.0 , 0.5 , 1.0 , 1.0 ])}
658+ reals = np .array ([0 , 1 , 0 , 1 , 0 ])
659+
660+ res = _prepare_probs_distribution_data (
661+ probs , reals , stratified_by = ("ppcr" ,), by = 0.20
662+ )
663+ ops = res ["operating_points" ]
664+
665+ op_0 = ops .filter (pl .col ("value" ) == 0.0 ).row (0 , named = True )
666+ assert op_0 ["cutoff" ] == pytest .approx (1.0 )
667+ assert op_0 ["realized_ppcr" ] == pytest .approx (0.0 )
668+
669+ op_1 = ops .filter (pl .col ("value" ) == 1.0 ).row (0 , named = True )
670+ assert op_1 ["cutoff" ] == pytest .approx (0.0 )
671+ assert op_1 ["realized_ppcr" ] == pytest .approx (1.0 )
672+
673+
674+ def test_ppcr_multiple_models_shared_outcomes ():
675+ probs = {
676+ "m1" : np .array ([0.1 , 0.4 , 0.6 , 0.9 ]),
677+ "m2" : np .array ([0.2 , 0.5 , 0.8 , 1.0 ]),
678+ }
679+ reals = np .array ([0 , 1 , 0 , 1 ])
680+
681+ res = _prepare_probs_distribution_data (
682+ probs , reals , stratified_by = ("ppcr" ,), by = 0.5
683+ )
684+ ops = res ["operating_points" ]
685+
686+ ops_m1 = ops .filter (pl .col ("model" ) == "m1" )
687+ ops_m2 = ops .filter (pl .col ("model" ) == "m2" )
688+
689+ assert len (ops_m1 ) == 3
690+ assert len (ops_m2 ) == 3
691+
692+ # Per-model effective cutoffs computed independently
693+ m1_c05 = ops_m1 .filter (pl .col ("value" ) == 0.5 ).row (0 , named = True )["cutoff" ]
694+ m2_c05 = ops_m2 .filter (pl .col ("value" ) == 0.5 ).row (0 , named = True )["cutoff" ]
695+
696+ assert m1_c05 == pytest .approx (0.5 )
697+ assert m2_c05 == pytest .approx (0.65 )
698+
699+
700+ def test_ppcr_multiple_populations_unequal_sample_sizes ():
701+ probs = {
702+ "pop1" : np .array ([0.1 , 0.5 , 0.9 ]),
703+ "pop2" : np .array ([0.2 , 0.4 , 0.6 , 0.8 , 1.0 ]),
704+ }
705+ reals = {
706+ "pop1" : np .array ([0 , 1 , 1 ]),
707+ "pop2" : np .array ([0 , 1 , 0 , 1 , 0 ]),
708+ }
709+
710+ res = _prepare_probs_distribution_data (
711+ probs , reals , stratified_by = ("ppcr" ,), by = 0.5
712+ )
713+ ops = res ["operating_points" ]
714+
715+ p1_ops = ops .filter (pl .col ("population" ) == "pop1" )
716+ p2_ops = ops .filter (pl .col ("population" ) == "pop2" )
717+
718+ assert len (p1_ops ) == 3
719+ assert len (p2_ops ) == 3
720+
721+
722+ def test_ppcr_pre_post_non_regression ():
723+ probs = {"m1" : np .array ([0.00 , 0.15 , 0.30 , 0.50 , 0.50 , 0.50 , 0.65 , 0.80 , 1.00 ])}
724+ reals = np .array ([0 , 1 , 0 , 1 , 0 , 1 , 1 , 0 , 1 ])
725+
726+ perf_df = prepare_performance_data (probs , reals , stratified_by = ("ppcr" ,), by = 0.20 )
727+ dist = _prepare_probs_distribution_data (
728+ probs , reals , stratified_by = ("ppcr" ,), by = 0.20
729+ )
730+
731+ # Rank bins schema and masses frozen
732+ rb = dist ["rank_bins" ]
733+ assert rb ["n_positive" ].to_list () == [1 , 2 , 0 , 1 , 1 ]
734+ assert rb ["n_negative" ].to_list () == [1 , 2 , 0 , 0 , 1 ]
735+
736+ # Metrics frozen
737+ assert perf_df ["true_positives" ].to_list () == [0 , 1 , 2 , 2 , 5 , 5 ]
738+ assert perf_df ["false_positives" ].to_list () == [0 , 1 , 1 , 1 , 4 , 4 ]
739+ assert perf_df ["true_negatives" ].to_list () == [4 , 3 , 3 , 3 , 0 , 0 ]
740+ assert perf_df ["false_negatives" ].to_list () == [5 , 4 , 3 , 3 , 0 , 0 ]
0 commit comments