nyaggle/nyaggle/ensemble/averaging.py

145 lines
5.4 KiB
Python

from typing import Callable, List, Optional, Tuple
import numpy as np
import pandas as pd
import scipy.stats as stats
from scipy.optimize import minimize
from nyaggle.ensemble.common import EnsembleResult
def averaging(test_predictions: List[np.ndarray],
oof_predictions: Optional[List[np.ndarray]] = None,
y: Optional[pd.Series] = None,
weights: Optional[List[float]] = None,
eval_func: Optional[Callable] = None,
rank_averaging: bool = False) -> EnsembleResult:
"""
Perform averaging on model predictions.
Args:
test_predictions:
List of predicted values on test data.
oof_predictions:
List of predicted values on out-of-fold training data.
y:
Target value
weights:
Weights for each predictions
eval_func:
Evaluation metric used for calculating result score. Used only if ``oof_predictions`` and ``y`` are given.
rank_averaging:
If ``True``, predictions will be converted to rank before averaging.
Returns:
Namedtuple with following members
* test_prediction:
numpy array, Average prediction on test data.
* oof_prediction:
numpy array, Average prediction on Out-of-Fold validation data. ``None`` if ``oof_predictions`` = ``None``.
* score:
float, Calculated score on Out-of-Fold data. ``None`` if ``eval_func`` is ``None``.
"""
if weights is None:
weights = np.ones((len(test_predictions))) / len(test_predictions)
if rank_averaging:
test_predictions, oof_predictions = _to_rank(test_predictions, oof_predictions)
def _weighted_average(predictions: List[np.ndarray], weights: List[float]):
if len(predictions) != len(weights):
raise ValueError('len(predictions) != len(weights)')
average = np.zeros_like(predictions[0])
for i, weight in enumerate(weights):
if predictions[i].shape != average.shape:
raise ValueError('predictions[{}].shape != predictions[0].shape'.format(i))
average += predictions[i] * weight
return average
average_test = _weighted_average(test_predictions, weights)
if oof_predictions is not None:
average_oof = _weighted_average(oof_predictions, weights)
score = eval_func(y, average_oof) if eval_func is not None else None
else:
average_oof = None
score = None
return EnsembleResult(average_test, average_oof, score)
def averaging_opt(test_predictions: List[np.ndarray],
oof_predictions: Optional[List[np.ndarray]],
y: Optional[pd.Series],
eval_func: Optional[Callable[[np.ndarray, np.ndarray], float]],
higher_is_better: bool,
weight_bounds: Tuple[float, float] = (0.0, 1.0),
rank_averaging: bool = False,
method: Optional[str] = None) -> EnsembleResult:
"""
Perform averaging with optimal weights using scipy.optimize.
Args:
test_predictions:
List of predicted values on test data.
oof_predictions:
List of predicted values on out-of-fold training data.
y:
Target value
eval_func:
Evaluation metric f(y_true, y_pred) used for calculating result score.
Used only if ``oof_predictions`` and ``y`` are given.
higher_is_better:
Determine the direction of optimize ``eval_func``.
weight_bounds:
Specify lower/upper bounds of each weight.
rank_averaging:
If ``True``, predictions will be converted to rank before averaging.
method:
Type of solver. If ``None``, SLSQP will be used.
Returns:
Namedtuple with following members
* test_prediction:
numpy array, Average prediction on test data.
* oof_prediction:
numpy array, Average prediction on Out-of-Fold validation data. ``None`` if ``oof_predictions`` = ``None``.
* score:
float, Calculated score on Out-of-Fold data. ``None`` if ``eval_func`` is ``None``.
"""
def _minimize(weights):
prediction = np.zeros_like(oof_predictions[0])
for weight, oof in zip(weights, oof_predictions):
prediction += weight * oof
oof_score = eval_func(y, prediction)
return -oof_score if higher_is_better else oof_score
weights = np.ones((len(test_predictions))) / len(test_predictions)
if rank_averaging:
test_predictions, oof_predictions = _to_rank(test_predictions, oof_predictions)
method = method or 'SLSQP'
if method in ['COBYLA', 'SLSQP', 'trust-constr']:
cons = ({'type': 'eq', 'fun': lambda w: 1 - sum(w)})
else:
cons = None
bounds = [weight_bounds] * len(test_predictions)
result = minimize(_minimize, weights, method=method, constraints=cons, bounds=bounds)
return averaging(test_predictions, oof_predictions, y, result['x'], eval_func)
def _to_rank(test_predictions: List[np.ndarray], oof_predictions: Optional[List[np.ndarray]]):
if oof_predictions is not None:
oof_predictions = [stats.rankdata(oof) / len(oof) for oof in oof_predictions]
test_predictions = [stats.rankdata(test) / len(test) for test in test_predictions]
return test_predictions, oof_predictions