Source code for syne_tune.optimizer.schedulers.searchers.conformal.surrogate.symmetric_conformalized_quantile_regression_model
from dataclasses import dataclass
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from tqdm import tqdm
from syne_tune.optimizer.schedulers.searchers.conformal.surrogate.quantile_regression_model import (
GradientBoostingQuantileRegressor,
QuantileRegressorPredictions,
)
[docs]
@dataclass
class SymmetricConformalQuantileCorrection:
lower_quantile: float
upper_quantile: float
target_coverage: float = None
correction: float = None
def __post_init__(self):
assert abs(1 - self.lower_quantile - self.upper_quantile) < 1e-7, (
f"Upper and lower quantiles must be symmetric and "
f"<{self.lower_quantile}, {self.upper_quantile}> are given"
)
self.target_coverage = 1 - 2 * self.lower_quantile
[docs]
class SymmetricConformalizedGradientBoostingQuantileRegressor(
GradientBoostingQuantileRegressor
):
def __init__(
self,
quantiles: int = 5,
valid_fraction: float = 0.10,
min_samples_to_conformalize: int = 32,
verbose: bool = False,
**kwargs,
):
assert (
type(quantiles) is int
), "This class only accepts the total number of quantiles"
assert quantiles % 2 == 1, "The number of quantiles must be odd"
assert valid_fraction > 0
super().__init__(quantiles, verbose, **kwargs)
self.valid_fraction = valid_fraction
self.min_samples_to_conformalize = min_samples_to_conformalize
quantile_bands_locations = sorted(list(self.quantile_regressors.keys()))
ncorrections = (quantiles - 1) // 2
self.conformal_correction = {
quantile_bands_locations[idx]: SymmetricConformalQuantileCorrection(
lower_quantile=quantile_bands_locations[idx],
upper_quantile=quantile_bands_locations[
len(quantile_bands_locations) - 1 - idx
],
)
for idx in range(ncorrections)
}
[docs]
def fit(self, df_features: np.ndarray, y: np.array, **kwargs):
nsamples = len(np.ravel(y))
if (nsamples > self.min_samples_to_conformalize) or (
self.min_samples_to_conformalize == 0
):
self._fit_conformal(df_features, y, **kwargs)
else:
self._fit_nonconformal(df_features, y, **kwargs)
def _fit_nonconformal(self, df_features: np.ndarray, y: np.array, **kwargs):
for quantile in tqdm(
self.quantile_regressors,
desc="Training Quantile Regression",
disable=not self.verbose,
):
self.quantile_regressors[quantile].fit(df_features, np.ravel(np.ravel(y)))
for alpha, cq in self.conformal_correction.items():
cq.correction = 0
def _fit_conformal(self, df_features: np.ndarray, y: np.array, **kwargs):
x_training, x_validation, y_training, y_validation = train_test_split(
df_features, y, test_size=self.valid_fraction
)
for quantile in tqdm(
self.quantile_regressors,
desc="Training Quantile Regression",
disable=not self.verbose,
):
self.quantile_regressors[quantile].fit(x_training, np.ravel(y_training))
for alpha, cq in self.conformal_correction.items():
residuals_lower = (
self.quantile_regressors[cq.lower_quantile]
.predict(x_validation)
.ravel()
- y_validation.ravel()
)
residuals_upper = (
y_validation.ravel()
- self.quantile_regressors[cq.upper_quantile]
.predict(x_validation)
.ravel()
)
residuals = np.maximum(residuals_lower, residuals_upper)
cq.correction = np.quantile(
residuals, q=cq.target_coverage / (1 + 1 / residuals.size)
)
[docs]
def predict(self, df_test: pd.DataFrame) -> QuantileRegressorPredictions:
quantile_res = {}
for alpha, cq in self.conformal_correction.items():
lower_preds = (
self.quantile_regressors[cq.lower_quantile].predict(df_test)
- cq.correction
)
quantile_res[cq.lower_quantile] = lower_preds
upper_preds = (
self.quantile_regressors[cq.upper_quantile].predict(df_test)
+ cq.correction
)
quantile_res[cq.upper_quantile] = upper_preds
# Predict for 0.5-th quantile since it is not corrected
quantile_res[0.5] = self.quantile_regressors[0.5].predict(df_test)
return QuantileRegressorPredictions.from_quantile_results(
quantiles=self.quantiles,
results=quantile_res,
)