dataframe-learn-2.5.0.0: src/DataFrame/DecisionTree/Regression.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
-- | Variance-reduction (weighted-SSE) regression trees.
module DataFrame.DecisionTree.Regression (
RegTreeConfig (..),
defaultRegTreeConfig,
RegFit (..),
-- | Implementation verb used by the fit\/predict instances and boosting.
fitRegTree,
) where
import Control.Parallel (par, pseq)
import Data.Maybe (maybeToList)
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU
import DataFrame.DecisionTree.Cart (
CartFeature (..),
NullSide (..),
sortIndicesByValue,
splitMidpoint,
)
import DataFrame.DecisionTree.Types (Tree (..))
import DataFrame.Internal.Expression (Expr)
-- | 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
}
data RegFit = RegFit
{ rfTree :: !(Tree Double)
, rfFitted :: !(VU.Vector Double)
}
-- | Fit on pre-extracted features, a target vector and optional weights.
fitRegTree ::
RegTreeConfig ->
V.Vector CartFeature ->
VU.Vector Double ->
Maybe (VU.Vector Double) ->
RegFit
fitRegTree cfg feats y mw = RegFit (toTree root) inSample
where
root = buildNode 0 (VU.enumFromN 0 n) featSorted
inSample = VU.update (VU.replicate n 0) (VU.concat (leafRows root))
leafRows (FLeaf v idxs) = [VU.map (,v) idxs]
leafRows (FBranch _ l r) = leafRows l ++ leafRows r
n = VU.length y
weightAt i = maybe 1 (VU.! i) mw
-- For each feature, row indices sorted by value, with null rows left out.
featSorted = V.map presentSorted feats
presentSorted f = case cfNulls f of
Nothing -> sortIndicesByValue (cfValues f)
Just nulls -> VU.filter (not . (nulls VU.!)) (sortIndicesByValue (cfValues f))
buildNode depth idxs sortedByFeat
| depth >= rtMaxDepth cfg || VU.length idxs < rtMinSamplesSplit cfg = leaf
| otherwise =
maybe leaf (splitNode depth idxs sortedByFeat) (bestSplit idxs sortedByFeat)
where
leaf = FLeaf (weightedMean idxs) idxs
splitNode depth idxs sortedByFeat (fj, thr, side)
| VU.null lefts || VU.null rights = FLeaf (weightedMean idxs) idxs
| otherwise =
forceTree l `par`
(forceTree r `pseq` FBranch (cfSplit feat thr side) l r)
where
feat = feats V.! fj
vals = cfValues feat
goesLeft i = case cfNulls feat of
Just nulls | nulls VU.! i -> side == NullsLeft
_ -> vals VU.! i <= thr
lefts = VU.filter goesLeft idxs
rights = VU.filter (not . goesLeft) idxs
l = buildNode (depth + 1) lefts (V.map (VU.filter goesLeft) sortedByFeat)
r =
buildNode (depth + 1) rights (V.map (VU.filter (not . goesLeft)) sortedByFeat)
weightedMean idxs =
let (w, sy) = VU.foldl' step (0, 0) idxs
step (!a, !b) i = (a + weightAt i, b + weightAt i * (y VU.! i))
in if w == 0 then 0 else sy / w
bestSplit idxs sortedByFeat
| null candidates = Nothing
| red > 0 && red >= rtMinImpurityDecrease cfg = Just split
| otherwise = Nothing
where
nNode = VU.length idxs
(totW, totSY, totSY2) = moments idxs
nodeSSE = sse totSY totSY2 totW
candidates =
concat
[ bestThreshold fj (sortedByFeat V.! fj) nNode totW totSY totSY2 nodeSSE
| fj <- [0 .. V.length feats - 1]
]
(red, split) = maximumByFst candidates
-- Best (threshold, null side) for feature @fj@ at this node.
bestThreshold fj sorted nNode totW totSY totSY2 nodeSSE =
[(r, (fj, thr, dir)) | (thr, dir, r) <- maybeToList (go 0 0 0 0 Nothing)]
where
vals = cfValues (feats V.! fj)
m = VU.length sorted
nNull = nNode - m
(pW, pSY, pSY2) = moments sorted
(nW, nSY, nSY2) = (totW - pW, totSY - pSY, totSY2 - pSY2)
hasNulls = nNull > 0
-- Last split to try; without nulls the final row splits nothing.
lastK = if hasNulls then m - 1 else m - 2
score nl wl syl syl2 =
let wr = totW - wl
ok =
nl >= rtMinLeafSize cfg && nNode - nl >= rtMinLeafSize cfg && wl > 0 && wr > 0
in if ok
then Just (nodeSSE - (sse syl syl2 wl + sse (totSY - syl) (totSY2 - syl2) wr))
else Nothing
go !k !wl !syl !syl2 best
| k > lastK || k >= m = best
| otherwise = go (k + 1) wl' syl' syl2' best'
where
i = sorted VU.! k
wi = weightAt i
yi = y VU.! i
wl' = wl + wi
syl' = syl + wi * yi
syl2' = syl2 + wi * yi * yi
boundary
| k + 1 < m = vals VU.! i /= vals VU.! (sorted VU.! (k + 1))
| otherwise = True
thr
| k + 1 < m = splitMidpoint (vals VU.! i) (vals VU.! (sorted VU.! (k + 1)))
| otherwise = vals VU.! i
right = (,) NullsRight <$> score (k + 1) wl' syl' syl2'
left
| hasNulls =
(,) NullsLeft <$> score (k + 1 + nNull) (wl' + nW) (syl' + nSY) (syl2' + nSY2)
| otherwise = Nothing
consider b (dir, r)
| maybe True (\(_, _, rb) -> r > rb) b = Just (thr, dir, r)
| otherwise = b
best'
| boundary = foldl consider best (maybeToList right ++ maybeToList left)
| otherwise = best
moments = VU.foldl' step (0, 0, 0)
where
step (!w, !sy, !sy2) i =
let wi = weightAt i; yi = y VU.! i
in (w + wi, sy + wi * yi, sy2 + wi * yi * yi)
safeDiv :: Double -> Double -> Double
safeDiv a b = if b == 0 then 0 else a / b
-- | Weighted SSE of a node from its sums and total weight.
sse :: Double -> Double -> Double -> Double
sse sumY sumSq w = sumSq - safeDiv (sumY * sumY) w
data FitNode
= FLeaf !Double !(VU.Vector Int)
| FBranch !(Expr Bool) !FitNode !FitNode
toTree :: FitNode -> Tree Double
toTree (FLeaf v _) = Leaf v
toTree (FBranch c l r) = Branch c (toTree l) (toTree r)
-- | Force a subtree to WHNF throughout; pure and value-preserving.
forceTree :: FitNode -> ()
forceTree (FLeaf v _) = v `seq` ()
forceTree (FBranch _ l r) = forceTree l `seq` forceTree r
maximumByFst :: (Ord a) => [(a, b)] -> (a, b)
maximumByFst = foldr1 (\x@(a, _) y@(b, _) -> if a >= b then x else y)