packages feed

dataframe-learn-1.1.0.0: src/DataFrame/DecisionTree/Regression.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}

{- | Variance-reduction (weighted-SSE) regression trees, reusing the CART
feature machinery. Leaves predict the (weighted) mean of their rows. The
matrix-level 'fitRegTreeOn' lets gradient boosting refit on residuals without
re-extracting features each round.
-}
module DataFrame.DecisionTree.Regression (
    RegTreeConfig (..),
    defaultRegTreeConfig,
    fitRegTreeOn,
) where

import Data.Function (on)
import Data.Maybe (maybeToList)
import qualified Data.Vector as V
import qualified Data.Vector.Algorithms.Merge as VA
import qualified Data.Vector.Unboxed as VU

import DataFrame.DecisionTree.Cart (CartFeature (..))
import DataFrame.DecisionTree.Types (Tree (..))

-- | Stopping criteria for the regression tree.
data RegTreeConfig = RegTreeConfig
    { rtMaxDepth :: !Int
    , rtMinSamplesSplit :: !Int
    , rtMinLeafSize :: !Int
    , rtMinImpurityDecrease :: !Double
    }
    deriving (Eq, Show)

defaultRegTreeConfig :: RegTreeConfig
defaultRegTreeConfig =
    RegTreeConfig
        { rtMaxDepth = 3
        , rtMinSamplesSplit = 2
        , rtMinLeafSize = 1
        , rtMinImpurityDecrease = 0.0
        }

{- | Fit on pre-extracted features, a target vector, and optional per-row
weights (length @n@). Used by gradient boosting on residual targets.
-}
fitRegTreeOn ::
    RegTreeConfig ->
    V.Vector CartFeature ->
    VU.Vector Double ->
    Maybe (VU.Vector Double) ->
    Tree Double
fitRegTreeOn cfg feats y mw = go 0 (VU.enumFromN 0 n)
  where
    n = VU.length y
    wt i = maybe 1 (VU.! i) mw
    go depth idxs
        | VU.length idxs < rtMinSamplesSplit cfg
            || depth >= rtMaxDepth cfg =
            Leaf (nodeMean idxs)
        | otherwise = case bestSplit idxs of
            Nothing -> Leaf (nodeMean idxs)
            Just (fj, thr) ->
                let vals = cfValues (feats V.! fj)
                    (lefts, rights) = VU.partition (\i -> vals VU.! i <= thr) idxs
                 in if VU.null lefts || VU.null rights
                        then Leaf (nodeMean idxs)
                        else
                            Branch
                                (cfPred (feats V.! fj) thr)
                                (go (depth + 1) lefts)
                                (go (depth + 1) rights)
    nodeMean idxs =
        let (sw, sy) = VU.foldl' (\(!a, !b) i -> (a + wt i, b + wt i * (y VU.! i))) (0, 0) idxs
         in if sw == 0 then 0 else sy / sw
    bestSplit idxs =
        let (totW, totSY, totSY2) = moments idxs
            nodeSSE = totSY2 - safeDiv (totSY * totSY) totW
            candidates =
                [ (red, fj, thr)
                | fj <- [0 .. V.length feats - 1]
                , (thr, red) <- featureSplits idxs fj totW totSY totSY2 nodeSSE
                ]
         in case candidates of
                [] -> Nothing
                _ ->
                    let (red, fj, thr) = maximumByFst candidates
                     in if red >= rtMinImpurityDecrease cfg && red > 0
                            then Just (fj, thr)
                            else Nothing
    featureSplits idxs fj totW totSY totSY2 nodeSSE =
        let vals = cfValues (feats V.! fj)
            sorted = sortByVal vals idxs
         in sweep sorted vals totW totSY totSY2 nodeSSE
    sweep sorted vals totW totSY totSY2 nodeSSE = go0 0 0 0 0 Nothing
      where
        m = VU.length sorted
        go0 !k !wl !syl !syl2 best
            | k >= m - 1 = maybeToList best
            | otherwise =
                let i = sorted VU.! k
                    wi = wt i
                    yi = y VU.! i
                    wl' = wl + wi
                    syl' = syl + wi * yi
                    syl2' = syl2 + wi * yi * yi
                    vCur = vals VU.! i
                    vNext = vals VU.! (sorted VU.! (k + 1))
                    nl = k + 1
                    nr = m - nl
                    wr = totW - wl'
                    valid =
                        vCur /= vNext
                            && nl >= rtMinLeafSize cfg
                            && nr >= rtMinLeafSize cfg
                            && wl' > 0
                            && wr > 0
                    red =
                        nodeSSE
                            - ( (syl2' - safeDiv (syl' * syl') wl')
                                    + ( (totSY2 - syl2')
                                            - safeDiv ((totSY - syl') * (totSY - syl')) wr
                                      )
                              )
                    thr = (vCur + vNext) / 2
                    best' =
                        if valid && maybe True (\(_, b) -> red > b) best
                            then Just (thr, red)
                            else best
                 in go0 (k + 1) wl' syl' syl2' best'
    moments =
        VU.foldl'
            ( \(!w, !sy, !sy2) i ->
                let wi = wt i; yi = y VU.! i
                 in (w + wi, sy + wi * yi, sy2 + wi * yi * yi)
            )
            (0, 0, 0)

safeDiv :: Double -> Double -> Double
safeDiv a b = if b == 0 then 0 else a / b

sortByVal :: VU.Vector Double -> VU.Vector Int -> VU.Vector Int
sortByVal vals = VU.modify (VA.sortBy (compare `on` (vals VU.!)))

maximumByFst :: (Ord a) => [(a, b, c)] -> (a, b, c)
maximumByFst = foldr1 (\x@(a, _, _) y@(b, _, _) -> if a >= b then x else y)