dataframe-learn-2.0.0.0: src/DataFrame/LinearModel/Regression.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{- | Linear regression with the standard penalties: OLS (QR), ridge (Cholesky),
and lasso\/elastic net (FISTA). 'fit' produces a 'LinearRegressor'; 'predict'
compiles it to an @Expr Double@ over the raw feature columns.
-}
module DataFrame.LinearModel.Regression (
module DataFrame.Model,
Penalty (..),
LinearConfig (..),
defaultLinearConfig,
LinearRegressor (..),
) where
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU
import DataFrame.Featurize.Internal (
affineExpr,
featureNames,
numericMatrix,
targetDoubles,
)
import DataFrame.Internal.Expression (Expr)
import DataFrame.LinearAlgebra (Matrix, dot, gram, tMatVec)
import DataFrame.LinearAlgebra.Solve (choleskySolve, qrLeastSquares)
import DataFrame.LinearSolver (
LinearModel (..),
SolverConfig (..),
defaultSolverConfig,
fitProx,
)
import DataFrame.LinearSolver.Loss (squaredLoss)
import DataFrame.Model
-- | Regularization choice. @alpha@ is the penalty strength; @l1Ratio@ mixes L1/L2.
data Penalty
= OLS
| Ridge !Double
| Lasso !Double
| ElasticNet !Double !Double
deriving (Eq, Show)
-- | Hyperparameters for linear regression: the penalty and the FISTA solver config.
data LinearConfig = LinearConfig
{ lcPenalty :: !Penalty
, lcSolver :: !SolverConfig
}
deriving (Eq, Show)
defaultLinearConfig :: LinearConfig
defaultLinearConfig = LinearConfig{lcPenalty = OLS, lcSolver = defaultSolverConfig}
{- | A fitted linear regressor. @regCoef@ and @regIntercept@ are sklearn's
@coef_@ / @intercept_@ in raw feature space.
-}
data LinearRegressor = LinearRegressor
{ regCoef :: !(VU.Vector Double)
, regIntercept :: !Double
, regFeatureNames :: !(V.Vector T.Text)
, regPenalty :: !Penalty
}
deriving (Eq, Show)
instance Fit LinearConfig (Expr Double) where
type ModelOf LinearConfig (Expr Double) = LinearRegressor
type FrameReq LinearConfig (Expr Double) = 'AllDoubleFrame
fit (LinearConfig penalty cfg) target df =
case penalty of
OLS -> closedForm (olsSolve mat y)
Ridge alpha -> closedForm (ridgeSolve alpha mat y)
Lasso alpha -> proxFit alpha 1.0
ElasticNet alpha l1r -> proxFit alpha l1r
where
names = featureNames target df
(nameVec, mat) = numericMatrix names df
y = targetDoubles target df
closedForm (coef, intercept) =
LinearRegressor coef intercept nameVec penalty
proxFit alpha l1r =
let proxCfg =
cfg{scL1Lambda = alpha * l1r, scL2Lambda = alpha * (1 - l1r)}
m = fitProx squaredLoss proxCfg mat y nameVec
in LinearRegressor (lmWeights m) (lmIntercept m) nameVec penalty
instance Predict LinearRegressor where
type Prediction LinearRegressor = Expr Double
predict m =
affineExpr
(regIntercept m)
(zip (VU.toList (regCoef m)) (V.toList (regFeatureNames m)))
-- | OLS via QR on the intercept-augmented design matrix.
olsSolve :: Matrix -> VU.Vector Double -> (VU.Vector Double, Double)
olsSolve mat y =
case qrLeastSquares augmented y of
Right sol -> (VU.drop 1 sol, sol VU.! 0)
Left _ -> ridgeSolve 1e-8 mat y
where
augmented = V.map (VU.cons 1) mat
{- | Ridge via Cholesky on @(XcᵀXc + αI) w = Xcᵀ yc@ over centred data; the
intercept is recovered from the column/target means.
-}
ridgeSolve :: Double -> Matrix -> VU.Vector Double -> (VU.Vector Double, Double)
ridgeSolve alpha mat y =
case choleskySolve a rhs of
Just w -> (w, meanY - dot w meansX)
Nothing -> (VU.replicate d 0, meanY)
where
n = V.length mat
d = if n == 0 then 0 else VU.length (V.head mat)
meansX =
VU.generate d $ \j ->
sum [(mat V.! i) VU.! j | i <- [0 .. n - 1]] / fromIntegral n
meanY = VU.sum y / fromIntegral n
centered = V.map (\row -> VU.zipWith (-) row meansX) mat
yc = VU.map (subtract meanY) y
a = addDiag alpha (gram centered)
rhs = tMatVec centered yc
-- | Add @alpha@ to the diagonal of a square matrix.
addDiag :: Double -> Matrix -> Matrix
addDiag alpha = V.imap (\i row -> row VU.// [(i, row VU.! i + alpha)])
-- | Predict the target for each row of a feature matrix.
predictLinear :: LinearRegressor -> Matrix -> VU.Vector Double
predictLinear m = VU.convert . V.map (\x -> regIntercept m + dot (regCoef m) x)