@@ -170,25 +170,49 @@ def _compute_likelihood_ratio(
170170
171171
172172def _query_weighted_quantile (
173- scores : np .ndarray , alpha : float , weights : np .ndarray
173+ scores : np .ndarray ,
174+ alpha : float ,
175+ weights : np .ndarray ,
176+ test_weight : float = 0.0 ,
174177) -> float :
175- """Compute weighted quantile of scores.
178+ """Compute the weighted conformal quantile of scores.
179+
180+ Implements the finite-sample correction for weighted conformal prediction
181+ under covariate shift (Tibshirani et al. 2019).
176182
177183 Args:
178- scores: Array of conformity scores
179- alpha: Quantile level (between 0 and 1)
180- weights: Weights for each score
184+ scores: Array of conformity scores (higher = more conforming).
185+ alpha: Quantile level (between 0 and 1).
186+ weights: Un-normalized weights (likelihood ratios) for each score.
187+ test_weight: Un-normalized weight representing the test point.
188+ Reserves ``test_weight / (sum(weights) + test_weight)`` of
189+ probability mass at conformity ``-inf``. Default 0.0 recovers the
190+ old, uncorrected behavior (kept for backward compatibility with
191+ direct callers of this helper).
181192
182193 Returns:
183- The weighted alpha-quantile of scores
194+ The weighted alpha-quantile of scores. Returns ``-inf`` if the
195+ reserved test-point mass alone already meets or exceeds ``alpha``,
196+ since there isn't enough calibration mass to justify a stricter,
197+ finite threshold without risking under-coverage.
184198 """
185- # Sort scores and corresponding weights
186199 sorted_indices = np .argsort (scores )
187200 sorted_scores = scores [sorted_indices ]
188201 sorted_weights = weights [sorted_indices ]
189202
190- # Compute cumulative weights
191- cum_weights = np .cumsum (sorted_weights ) / np .sum (sorted_weights )
203+ total_weight = np .sum (sorted_weights ) + test_weight
204+ if total_weight <= 0 :
205+ return - np .inf
206+
207+ p_test = test_weight / total_weight
208+ if p_test >= alpha :
209+ # Not enough calibration mass to reach the target coverage without
210+ # dipping into the mass reserved for the test point itself: fall
211+ # back to the maximally permissive (safe) threshold.
212+ return - np .inf
213+
214+ # Compute cumulative weights over the reserved-mass-inclusive total.
215+ cum_weights = np .cumsum (sorted_weights ) / total_weight
192216
193217 # Find the index where cumulative weight exceeds alpha
194218 idx = np .searchsorted (cum_weights , alpha , side = "left" )
@@ -197,7 +221,7 @@ def _query_weighted_quantile(
197221 if idx >= len (sorted_scores ):
198222 idx = len (sorted_scores ) - 1
199223
200- return sorted_scores [idx ]
224+ return float ( sorted_scores [idx ])
201225
202226
203227class CovariateLabel (SetPredictor ):
@@ -458,29 +482,29 @@ def calibrate(
458482 self .kde_test , self .kde_cal , X
459483 )
460484
461- # Normalize weights
462- weights = likelihood_ratios / np .sum (likelihood_ratios )
485+ # Keep weights un-normalized here
463486 self ._sum_cal_weights = np .sum (likelihood_ratios )
464487
465488 # Extract conformity scores (probabilities of true class)
466489 conformity_scores = y_prob [np .arange (N ), y_true ]
467490
468491 # Compute weighted quantile thresholds
469492 if isinstance (self .alpha , float ):
470- # Marginal coverage: single threshold
471- t = _query_weighted_quantile (conformity_scores , self .alpha , weights )
493+ test_weight = float (np .mean (likelihood_ratios ))
494+ t = _query_weighted_quantile (
495+ conformity_scores , self .alpha , likelihood_ratios , test_weight
496+ )
472497 else :
473498 # Class-conditional coverage: one threshold per class
474499 t = []
475500 for k in range (K ):
476501 mask = y_true == k
477502 if np .sum (mask ) > 0 :
478503 class_scores = conformity_scores [mask ]
479- class_weights = weights [mask ]
480- # Renormalize class weights
481- class_weights = class_weights / np .sum (class_weights )
504+ class_weights = likelihood_ratios [mask ]
505+ class_test_weight = float (np .mean (class_weights ))
482506 t_k = _query_weighted_quantile (
483- class_scores , self .alpha [k ], class_weights
507+ class_scores , self .alpha [k ], class_weights , class_test_weight
484508 )
485509 else :
486510 # If no calibration examples, use -inf (include all)
0 commit comments