@@ -399,6 +399,220 @@ def test_join_failures():
399399 _prediction_distribution_v2_spec_from_performance_data (probs , reals , by = 0.5 )
400400
401401
402+ def test_authoritative_schema_validation_probability_threshold_and_ppcr ():
403+ from jsonschema import Draft202012Validator
404+
405+ schema_json = {
406+ "type" : "object" ,
407+ "required" : [
408+ "schemaVersion" ,
409+ "type" ,
410+ "evaluations" ,
411+ "bins" ,
412+ "operatingPoints" ,
413+ ],
414+ "properties" : {
415+ "schemaVersion" : {"const" : "2.0" , "type" : "string" },
416+ "type" : {"const" : "prediction_distribution" , "type" : "string" },
417+ "evaluations" : {
418+ "minItems" : 1 ,
419+ "type" : "array" ,
420+ "items" : {
421+ "type" : "object" ,
422+ "required" : ["id" , "population" ],
423+ "properties" : {
424+ "id" : {"type" : "string" },
425+ "model" : {"type" : "string" },
426+ "population" : {"type" : "string" },
427+ },
428+ },
429+ },
430+ "operatingPoint" : {
431+ "type" : "object" ,
432+ "required" : ["dimension" ],
433+ "properties" : {
434+ "dimension" : {"enum" : ["probability_threshold" , "ppcr" ]}
435+ },
436+ },
437+ "bins" : {
438+ "minItems" : 1 ,
439+ "type" : "array" ,
440+ "items" : {
441+ "type" : "object" ,
442+ "required" : [
443+ "evaluationId" ,
444+ "lower" ,
445+ "upper" ,
446+ "includeLower" ,
447+ "includeUpper" ,
448+ "nPositive" ,
449+ "nNegative" ,
450+ ],
451+ "properties" : {
452+ "evaluationId" : {"type" : "string" },
453+ "lower" : {"minimum" : 0 , "maximum" : 1 , "type" : "number" },
454+ "upper" : {"minimum" : 0 , "maximum" : 1 , "type" : "number" },
455+ "includeLower" : {"type" : "boolean" },
456+ "includeUpper" : {"type" : "boolean" },
457+ "nPositive" : {"minimum" : 0 , "type" : "integer" },
458+ "nNegative" : {"minimum" : 0 , "type" : "integer" },
459+ },
460+ },
461+ },
462+ "rankBins" : {
463+ "type" : "array" ,
464+ "items" : {
465+ "type" : "object" ,
466+ "required" : [
467+ "evaluationId" ,
468+ "rankLower" ,
469+ "rankUpper" ,
470+ "positiveMass" ,
471+ "negativeMass" ,
472+ ],
473+ "properties" : {
474+ "evaluationId" : {"type" : "string" },
475+ "rankLower" : {"minimum" : 0 , "maximum" : 1 , "type" : "number" },
476+ "rankUpper" : {"minimum" : 0 , "maximum" : 1 , "type" : "number" },
477+ "positiveMass" : {"minimum" : 0 , "type" : "number" },
478+ "negativeMass" : {"minimum" : 0 , "type" : "number" },
479+ },
480+ },
481+ },
482+ "operatingPoints" : {
483+ "minItems" : 1 ,
484+ "type" : "array" ,
485+ "items" : {
486+ "type" : "object" ,
487+ "required" : [
488+ "evaluationId" ,
489+ "type" ,
490+ "value" ,
491+ "cutoff" ,
492+ "realizedPpcr" ,
493+ ],
494+ "properties" : {
495+ "evaluationId" : {"type" : "string" },
496+ "type" : {"enum" : ["probability_threshold" , "ppcr" ]},
497+ "value" : {"minimum" : 0 , "maximum" : 1 , "type" : "number" },
498+ "cutoff" : {"minimum" : 0 , "maximum" : 1 , "type" : "number" },
499+ "realizedPpcr" : {"minimum" : 0 , "maximum" : 1 , "type" : "number" },
500+ "performance" : {
501+ "type" : "array" ,
502+ "items" : {
503+ "type" : "object" ,
504+ "required" : ["metricId" , "estimate" ],
505+ "properties" : {
506+ "metricId" : {"type" : "string" },
507+ "estimate" : {"type" : ["number" , "null" ]},
508+ },
509+ },
510+ },
511+ },
512+ },
513+ },
514+ },
515+ }
516+ validator = Draft202012Validator (schema_json )
517+
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+ # 1. Probability Threshold mode schema validation
522+ chart_thresh = rtichoke .create_probs_histogram (
523+ probs = probs , reals = reals , by = 0.20 , stratified_by = ("probability_threshold" ,)
524+ )
525+ errors_thresh = list (validator .iter_errors (chart_thresh .spec ))
526+ assert not errors_thresh , (
527+ f"Schema validation errors in probability_threshold mode: { errors_thresh } "
528+ )
529+
530+ # 2. PPCR mode schema validation
531+ chart_ppcr = rtichoke .create_probs_histogram (
532+ probs = probs , reals = reals , by = 0.20 , stratified_by = ("ppcr" ,)
533+ )
534+ errors_ppcr = list (validator .iter_errors (chart_ppcr .spec ))
535+ assert not errors_ppcr , f"Schema validation errors in PPCR mode: { errors_ppcr } "
536+
537+
538+ def test_stratified_by_boundary_validation ():
539+ probs = {"m1" : np .array ([0.1 , 0.5 , 0.9 ])}
540+ reals = np .array ([0 , 1 , 1 ])
541+
542+ # Plain string error
543+ with pytest .raises (ValueError , match = "plain string" ):
544+ rtichoke .create_probs_histogram (
545+ probs , reals , stratified_by = "probability_threshold"
546+ )
547+
548+ # Empty sequence error
549+ with pytest .raises (ValueError , match = "contain exactly one element" ):
550+ rtichoke .create_probs_histogram (probs , reals , stratified_by = ())
551+
552+ # Sequence with > 1 elements
553+ with pytest .raises (ValueError , match = "contain exactly one element" ):
554+ rtichoke .create_probs_histogram (
555+ probs , reals , stratified_by = ("probability_threshold" , "ppcr" )
556+ )
557+
558+ # Unsupported dimension key error
559+ with pytest .raises (ValueError , match = "Unsupported stratification key" ):
560+ rtichoke .create_probs_histogram (
561+ probs , reals , stratified_by = ("unsupported_key" ,)
562+ )
563+
564+
565+ def test_negative_contract_failures ():
566+ probs = {"m1" : np .array ([0.1 , 0.5 , 0.9 ])}
567+ reals = np .array ([0 , 1 , 1 ])
568+
569+ dist_data = _prepare_probs_distribution_data (probs , reals , by = 0.5 )
570+ perf_data = prepare_performance_data (probs , reals , by = 0.5 )
571+ eval_meta = _build_evaluation_metadata (probs , reals , np .array ([]))
572+
573+ # 1. Unknown reference group in bins
574+ bad_bins = dist_data ["bins" ].with_columns (
575+ pl .lit ("unknown_group" ).alias ("evaluation" )
576+ )
577+ bad_dist = dict (dist_data , bins = bad_bins )
578+ with pytest .raises (ValueError , match = "Unknown reference group in bins" ):
579+ _prediction_distribution_v2_spec (bad_dist , perf_data , eval_meta )
580+
581+ # 2. Unknown reference group in rank bins
582+ bad_rank_bins = dist_data ["rank_bins" ].with_columns (
583+ pl .lit ("unknown_group" ).alias ("evaluation" )
584+ )
585+ bad_dist_rank = dict (dist_data , rank_bins = bad_rank_bins )
586+ with pytest .raises (ValueError , match = "Unknown reference group in rank bins" ):
587+ _prediction_distribution_v2_spec (bad_dist_rank , perf_data , eval_meta )
588+
589+ # 3. Unknown reference group in operating points
590+ bad_ops = dist_data ["operating_points" ].with_columns (
591+ pl .lit ("unknown_group" ).alias ("evaluation" )
592+ )
593+ bad_dist_op = dict (dist_data , operating_points = bad_ops )
594+ with pytest .raises (ValueError , match = "Unknown reference group in operating points" ):
595+ _prediction_distribution_v2_spec (bad_dist_op , perf_data , eval_meta )
596+
597+ # 4. Non-finite requested operating-point value
598+ bad_op_value = dist_data ["operating_points" ].with_columns (
599+ pl .when (pl .col ("value" ) == 0.0 )
600+ .then (float ("nan" ))
601+ .otherwise (pl .col ("value" ))
602+ .alias ("value" )
603+ )
604+ bad_dist_nan = dict (dist_data , operating_points = bad_op_value )
605+ with pytest .raises (ValueError , match = "Non-finite operating point value" ):
606+ _prediction_distribution_v2_spec (bad_dist_nan , perf_data , eval_meta )
607+
608+ # 5. Incomplete evaluation coverage
609+ partial_meta = {"m1" : eval_meta ["m1" ], "m2" : eval_meta ["m1" ]}
610+ with pytest .raises (
611+ ValueError , match = "Missing performance row|Incomplete evaluation coverage"
612+ ):
613+ _prediction_distribution_v2_spec (dist_data , perf_data , partial_meta )
614+
615+
402616def test_producer_owned_performance_wins ():
403617 probs = {"m1" : np .array ([0.1 , 0.5 , 0.9 ])}
404618 reals = np .array ([0 , 1 , 1 ])
0 commit comments