Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Covariate Shift Conformal Regressor #151

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 67 additions & 11 deletions mapie/conformity_scores.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import numpy as np

from .density_ratio import DensityRatioEstimator
from ._machine_precision import EPSILON
from ._typing import NDArray, ArrayLike

Expand All @@ -18,6 +19,7 @@ def __init__(
self,
sym: bool,
consistency_check: bool = True,
compute_score_weights: bool = False,
eps: np.float64 = np.float64(1e-8),
):
"""
Expand All @@ -30,6 +32,7 @@ def __init__(
- get_estimation_distribution and
- get_signed_conformity_scores
by default True.
compute_score_weights : TODO
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to define what is compute_score_weights

eps : float, optional
Threshold to consider when checking the consistency between the
following methods:
Expand All @@ -43,12 +46,13 @@ def __init__(
by default sys.float_info.epsilon.
"""
self.sym = sym
self.eps = eps
self.consistency_check = consistency_check
self.compute_score_weights = compute_score_weights
self.eps = eps

@abstractmethod
def get_signed_conformity_scores(
self, y: ArrayLike, y_pred: ArrayLike,
self, y: ArrayLike, y_pred: ArrayLike, conformity_scores: ArrayLike
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a mistake: conformity_scores should not be an argument to get_signed_conformity_scores.

) -> NDArray:
"""
Placeholder for get_signed_conformity_scores.
Expand Down Expand Up @@ -95,7 +99,10 @@ def get_estimation_distribution(
"""

def check_consistency(
self, y: ArrayLike, y_pred: ArrayLike, conformity_scores: ArrayLike
self,
y: NDArray,
y_pred: NDArray,
conformity_scores: NDArray,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no reason to change the style (for consistency).

) -> None:
"""
Check consistency between the following methods:
Expand All @@ -112,15 +119,15 @@ def check_consistency(
Observed values.
y_pred : NDArray
Predicted values.
conformity_scores: NDArray
Conformity scores.

Raises
------
ValueError
If the two methods are not consistent.
"""
score_distribution = self.get_estimation_distribution(
y_pred, conformity_scores
)
score_distribution = self.get_estimation_distribution(y_pred, conformity_scores)
abs_conformity_scores = np.abs(np.subtract(score_distribution, y))
max_conf_score = np.max(abs_conformity_scores)
if max_conf_score > self.eps:
Expand All @@ -136,9 +143,23 @@ def check_consistency(
"sure that the two methods are consistent."
)

def get_conformity_scores(
self, y: ArrayLike, y_pred: ArrayLike
) -> NDArray:
def get_weights(self, X: NDArray) -> NDArray:
"""
Compute weights for conformity scores on calibration samples.

Parameters
----------
X : NDArray
Dataset used for computing the weights

Returns
-------
NDArray
Estimated weights
"""
return np.ones(shape=(len(X) + 1))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand it, the get_weights method is only used in the CovariateShiftConformityScore class. It is not used in the already implemented ConformityScore classes. From this observation:

  • I suggest you implement a new WeightedConformalScore class (in a new file weighted_conformity_scores.py), which inherits from ConformalScore with all your current changes.
  • Thus the CovariateShiftConformityScore class will inherit from WeightedConformalScore and will be the only class that benefits from your changes without impacting the others.

I can confirm that this new structure will have an impact on the future features we plan to implement. Indeed, we plan to implement conditional conformal predictions, and weighted conformal scores will be the first step in this direction, thanks to you.


def get_conformity_scores(self, y: ArrayLike, y_pred: ArrayLike) -> NDArray:
"""
Get the conformity score considering the symmetrical property if so.

Expand Down Expand Up @@ -177,7 +198,9 @@ def __init__(self) -> None:
super().__init__(sym=True, consistency_check=True)

def get_signed_conformity_scores(
self, y: ArrayLike, y_pred: ArrayLike,
self,
y: ArrayLike,
y_pred: ArrayLike,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no reason to change the style (for consistency).

) -> NDArray:
"""
Compute the signed conformity scores from the predicted values
Expand Down Expand Up @@ -237,7 +260,9 @@ def _all_strictly_positive(self, y: ArrayLike) -> bool:
return True

def get_signed_conformity_scores(
self, y: ArrayLike, y_pred: ArrayLike,
self,
y: ArrayLike,
y_pred: ArrayLike,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no reason to change the style (for consistency).

) -> NDArray:
"""
Compute samples of the estimation distribution from the predicted
Expand All @@ -259,3 +284,34 @@ def get_estimation_distribution(
"""
self._check_predicted_data(y_pred)
return np.multiply(y_pred, np.add(1, conformity_scores))


class CovariateShiftConformityScore(ConformityScore):
def __init__(
self,
density_ratio_estimator: DensityRatioEstimator,
) -> None:
super().__init__(
sym=True,
consistency_check=False,
compute_score_weights=True,
)
self.density_ratio_estimator = density_ratio_estimator

def get_signed_conformity_scores(self, y: NDArray, y_pred: NDArray) -> NDArray:
scores = np.subtract(y, y_pred)
return scores

def get_weights(self, X: NDArray) -> NDArray:
dre_test = self.density_ratio_estimator.predict(X) # n_test
dre_calib = self.density_ratio_estimator.calib_dr_estimates_ # n_calib
denom = dre_calib.sum() + dre_test # n_test
calib_weights = dre_calib[:, np.newaxis] / denom # (n_calib, n_test)
test_weights = dre_test / denom # n_test
weights_stacked = np.vstack([calib_weights, test_weights]).T
return weights_stacked # (n_test, n_calib+1)

def get_estimation_distribution(
self, y_pred: NDArray, conformity_scores: NDArray
) -> NDArray:
return np.add(y_pred, conformity_scores)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said before, I suggest you to move it to a new file weighted_conformity_scores.py.

Loading