251 lines
7.6 KiB
Python
251 lines
7.6 KiB
Python
# Authors: Fabian Pedregosa <fabian@fseoane.net>
|
|
# Alexandre Gramfort <alexandre.gramfort@inria.fr>
|
|
# Nelle Varoquaux <nelle.varoquaux@gmail.com>
|
|
# License: BSD Style.
|
|
|
|
import numpy as np
|
|
from scipy import interpolate
|
|
from ..base import BaseEstimator, TransformerMixin, RegressorMixin
|
|
from ..utils import as_float_array, check_arrays
|
|
|
|
|
|
def isotonic_regression(y, weight=None, y_min=None, y_max=None):
|
|
"""Solve the isotonic regression model:
|
|
|
|
min sum w[i] (y[i] - y_[i]) ** 2
|
|
|
|
subject to y_min = y_[1] <= y_[2] ... <= y_[n] = y_max
|
|
|
|
where:
|
|
- y[i] are inputs (real numbers)
|
|
- y_[i] are fitted
|
|
- w[i] are optional strictly positive weights (default to 1.0)
|
|
|
|
Parameters
|
|
----------
|
|
y: iterable of floating-point values
|
|
The data
|
|
|
|
weight: iterable of floating-point values, optional, default: None
|
|
Weights on each point of the regression.
|
|
If None, weight is set to 1 (equal weights)
|
|
|
|
y_min: optional, default: None
|
|
if not None, set the lowest value of the fit to y_min
|
|
|
|
y_max: optional, default: None
|
|
if not None, set the highest value of the fit to y_max
|
|
|
|
Returns
|
|
-------
|
|
y_: list of floating-point values
|
|
Isotonic fit of y
|
|
|
|
References
|
|
----------
|
|
"Active set algorithms for isotonic regression; A unifying framework"
|
|
by Michael J. Best and Nilotpal Chakravarti, section 3.
|
|
"""
|
|
if weight is None:
|
|
weight = np.ones(len(y), dtype=y.dtype)
|
|
if y_min is not None or y_max is not None:
|
|
y = np.copy(y)
|
|
weight = np.copy(weight)
|
|
C = np.dot(weight, y * y) * 10 # upper bound on the cost function
|
|
if y_min is not None:
|
|
y[0] = y_min
|
|
weight[0] = C
|
|
if y_max is not None:
|
|
y[-1] = y_max
|
|
weight[-1] = C
|
|
|
|
active_set = [(weight[i] * y[i], weight[i], [i, ])
|
|
for i in range(len(y))]
|
|
current = 0
|
|
|
|
while current < len(active_set) - 1:
|
|
value0, value1, value2 = 0, 0, np.inf
|
|
weight0, weight1, weight2 = 1, 1, 1
|
|
while value0 * weight1 <= value1 * weight0 and \
|
|
current < len(active_set) - 1:
|
|
value0, weight0, idx0 = active_set[current]
|
|
value1, weight1, idx1 = active_set[current + 1]
|
|
if value0 * weight1 <= value1 * weight0:
|
|
current += 1
|
|
|
|
if current == len(active_set) - 1:
|
|
break
|
|
|
|
# merge two groups
|
|
value0, weight0, idx0 = active_set.pop(current)
|
|
value1, weight1, idx1 = active_set.pop(current)
|
|
active_set.insert(current,
|
|
(value0 + value1,
|
|
weight0 + weight1, idx0 + idx1))
|
|
while value2 * weight0 > value0 * weight2 and current > 0:
|
|
value0, weight0, idx0 = active_set[current]
|
|
value2, weight2, idx2 = active_set[current - 1]
|
|
if weight0 * value2 >= weight2 * value0:
|
|
active_set.pop(current)
|
|
active_set[current - 1] = (value0 + value2,
|
|
weight0 + weight2,
|
|
idx0 + idx2)
|
|
current -= 1
|
|
|
|
solution = np.empty(len(y))
|
|
for value, weight, idx in active_set:
|
|
solution[idx] = value / weight
|
|
return solution
|
|
|
|
|
|
class IsotonicRegression(BaseEstimator, TransformerMixin, RegressorMixin):
|
|
"""Isotonic regression model.
|
|
|
|
The isotonic regression optimization problem is defined by::
|
|
min sum w_i (y[i] - y_[i]) ** 2
|
|
|
|
subject to y_[i] <= y_[j] whenever X[i] <= X[j]
|
|
and min(y_) = y_min, max(y_) = y_max
|
|
|
|
where:
|
|
- y[i] are inputs (real numbers)
|
|
- y_[i] are fitted
|
|
- X specifies the order. If X is non-decreasing then y_ is non-decreasing.
|
|
- w[i] are optional strictly positive weights (default to 1.0)
|
|
|
|
Parameters
|
|
----------
|
|
y_min: optional, default: None
|
|
If not None, set the lowest value of the fit to y_min.
|
|
|
|
y_max: optional, default: None
|
|
If not None, set the highest value of the fit to y_max.
|
|
|
|
Attributes
|
|
----------
|
|
`X_`: ndarray (n_samples, )
|
|
A copy of the input X.
|
|
|
|
`y_`: ndarray (n_samples, )
|
|
Isotonic fit of y.
|
|
|
|
References
|
|
----------
|
|
Isotonic Median Regression: A Linear Programming Approach
|
|
Nilotpal Chakravarti
|
|
Mathematics of Operations Research
|
|
Vol. 14, No. 2 (May, 1989), pp. 303-308
|
|
"""
|
|
def __init__(self, y_min=None, y_max=None):
|
|
self.y_min = y_min
|
|
self.y_max = y_max
|
|
|
|
def _check_fit_data(self, X, y, weight=None):
|
|
if len(X.shape) != 1:
|
|
raise ValueError("X should be a vector")
|
|
|
|
def fit(self, X, y, weight=None):
|
|
"""Fit the model using X, y as training data.
|
|
|
|
Parameters
|
|
----------
|
|
X: array-like, shape=(n_samples,)
|
|
training data
|
|
|
|
y: array-like, shape=(n_samples,)
|
|
training target
|
|
|
|
weight: array-like, shape=(n_samples,), optional, default: None
|
|
weights. If set to None, all weights will be set to 1 (equal
|
|
weights)
|
|
|
|
Returns
|
|
-------
|
|
self: object
|
|
returns an instance of self
|
|
|
|
Note
|
|
----
|
|
X is stored for future use, as `transform` needs X to interpolate
|
|
new input data.
|
|
"""
|
|
X, y, weight = check_arrays(X, y, weight, sparse_format='dense')
|
|
y = as_float_array(y)
|
|
self._check_fit_data(X, y, weight)
|
|
order = np.argsort(X)
|
|
self.X_ = as_float_array(X[order], copy=False)
|
|
self.y_ = isotonic_regression(y[order], weight, self.y_min, self.y_max)
|
|
return self
|
|
|
|
def transform(self, T):
|
|
"""Transform new data by linear interpolation
|
|
|
|
Parameters
|
|
----------
|
|
T: array-like, shape=(n_samples,)
|
|
data to transform
|
|
|
|
Returns
|
|
-------
|
|
T_: array, shape=(n_samples,)
|
|
The transformed data
|
|
"""
|
|
T = as_float_array(T)
|
|
if len(T.shape) != 1:
|
|
raise ValueError("X should be a vector")
|
|
|
|
f = interpolate.interp1d(self.X_, self.y_, kind='linear',
|
|
bounds_error=True)
|
|
return f(T)
|
|
|
|
def fit_transform(self, X, y, weight=None):
|
|
"""Fit model and transform y by linear interpolation.
|
|
|
|
Parameters
|
|
----------
|
|
X: array-like, shape=(n_samples,)
|
|
training data
|
|
|
|
y: array-like, shape=(n_samples,)
|
|
training target
|
|
|
|
weight: array-like, shape=(n_samples,), optional, default: None
|
|
weights. If set to None, all weights will be equal to 1 (equal
|
|
weights)
|
|
|
|
Returns
|
|
-------
|
|
y_: array, shape=(n_samples,)
|
|
The transformed data
|
|
|
|
Note
|
|
----
|
|
X doesn't influence the result of `fit_transform`. It is however stored
|
|
for future use, as `transform` needs X to interpolate new input
|
|
data.
|
|
"""
|
|
X, y, weight = check_arrays(X, y, weight, sparse_format='dense')
|
|
y = as_float_array(y)
|
|
self._check_fit_data(X, y, weight)
|
|
order = np.argsort(X)
|
|
order_inv = np.zeros(len(y), dtype=np.int)
|
|
order_inv[order] = np.arange(len(y))
|
|
self.X_ = as_float_array(X[order], copy=False)
|
|
self.y_ = isotonic_regression(y[order], weight, self.y_min, self.y_max)
|
|
return self.y_[order_inv]
|
|
|
|
def predict(self, T):
|
|
"""Predict new data by linear interpolation.
|
|
|
|
Parameters
|
|
----------
|
|
T: array-like, shape=(n_samples,)
|
|
Data to transform.
|
|
|
|
Returns
|
|
-------
|
|
T_: array, shape=(n_samples,)
|
|
Transformed data.
|
|
"""
|
|
return self.transform(T)
|