## Ridge Regression
   
## This code is written by Davide Albanese, <albanese@fbk.eu>.
## (C) 2010 Fondazione Bruno Kessler - Via Santa Croce 77, 38100 Trento, ITALY.

## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.

## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.

## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.

__all__ = ["RidgeRegression", "KernelRidgeRegression"]

import numpy as np

class RidgeRegression(object):
    """Ridge Regression and Ordinary Least Squares (OLS).
    """
    
    def __init__(self, alpha=0.0):
        """Initialization.

        :Parameters:
          alpha : float (>= 0.0)
                regularization (0.0: OLS)
        """

        self.alpha = alpha
        self.__beta = None
        self.__beta0 = None
                
    def learn(self, x, y):
        """Compute the regression coefficients.

        :Parameters:
          x : numpy 2d array (n x p)
            matrix of regressors
          y : numpy 1d array (n)
            response
        """
        
        if not isinstance(x, np.ndarray):
            raise ValueError("x must be an numpy 2d array")

        if not isinstance(y, np.ndarray):
            raise ValueError("y must be an numpy 1d array")

        if x.ndim > 2:
            raise ValueError("x must be an 2d array")
        
        if x.shape[0] != y.shape[0]:
            raise ValueError("x and y are not aligned")

        xm = x - np.mean(x)
        
        n = x.shape[0]
        p = x.shape[1]

        if n < p:
            xd = np.dot(xm, xm.T)
            if self.alpha:
                xd += self.alpha * np.eye(n)
            xdi = np.linalg.pinv(xd)
            self.__beta = np.dot(np.dot(xm.T, xdi), y)
        else:
            xd = np.dot(xm.T, xm)
            if self.alpha:
                xd += self.alpha * np.eye(p)
            xdi = np.linalg.pinv(xd)
            self.__beta = np.dot(xdi, np.dot(xm.T, y))
        
        self.__beta0 = np.mean(y) - np.dot(self.__beta, np.mean(x, axis=0))
   
    def pred(self, x):
        """Compute the predicted response.
        
        :Parameters:
          x : numpy 2d array (nxp)
            matrix of regressors
        
        :Returns:
          yp : 1d ndarray
             predicted response
        """

        if not isinstance(x, np.ndarray):
            raise ValueError("x must be an numpy 2d array")
        
        if x.ndim > 2:
            raise ValueError("x must be an 2d array")
        
        if x.shape[1] != self.__beta.shape[0]:
            raise ValueError("x and beta are not aligned")
        
        p = np.dot(x, self.__beta) + self.__beta0
        
        return p
    
    def selected(self):
        """Returns the regressors ranking.
        """
        
        if self.__beta == None:
            raise ValueError("regression coefficients are not computed. "
                             "Run RidgeRegression.learn(x, y)")

        sel = np.argsort(np.abs(self.__beta))[::-1]
        
        return sel

    def beta(self):
        """Return b_1, ..., b_p.
        """

        return self.__beta

    def beta0(self):
        """Return b_0.
        """

        return self.__beta0


class KernelRidgeRegression(object):
    """Ridge Regression and Ordinary Least Squares (OLS).
    """
    
    def __init__(self, kernel, alpha):
        """Initialization.

        :Parameters:
          alpha : float (> 0.0)
        """

        self.alpha = alpha
        self.__kernel = kernel
        self.__x = None
        self.__c = None
        
    def learn(self, x, y):
        """Compute the regression coefficients.
        
        :Parameters:
          x : numpy 2d array (n x p)
            matrix of regressors
          y : numpy 1d array (n)
            response
        """
        
        if not isinstance(x, np.ndarray):
            raise ValueError("x must be an numpy 2d array")

        if not isinstance(y, np.ndarray):
            raise ValueError("y must be an numpy 1d array")

        if x.ndim > 2:
            raise ValueError("x must be an 2d array")
        
        if x.shape[0] != y.shape[0]:
            raise ValueError("x and y are not aligned")
       
        n = x.shape[0]
        p = x.shape[1]
        
        K = self.__kernel.matrix(x)
        
        tmp = np.linalg.inv(K + (self.alpha * np.eye(n)))
        self.__c = np.dot(y, tmp)
        self.__x = x.copy()        
   
    def pred(self, x):
        """Compute the predicted response.
        
        :Parameters:
          x : numpy 2d array (n x p)
            matrix of regressors
        
        :Returns:
          yp : 1d ndarray
             predicted response
        """

        if not isinstance(x, np.ndarray):
            raise ValueError("x must be an numpy 2d array")

        if x.ndim > 2:
            raise ValueError("x must be an 2d array")
       
        if x.shape[1] != self.__x.shape[1]:
            raise ValueError("x is not aligned")
        
        y = np.empty(x.shape[0])
        
        for i in range(x.shape[0]):
            k =  self.__kernel.vector(x[i], self.__x)
            y[i] = np.sum(self.__c * k)
            
        return y


