packages feed

dataframe-learn-2.5.0.0: src-internal/DataFrame/DecisionTree/Tao.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

-- | Tree Alternating Optimization: re-optimize one node at a time, bottom-up.
module DataFrame.DecisionTree.Tao (
    taoOptimize,
    taoOptimizeCV,
    taoIteration,
    taoIterationCV,
    optimizeNode,
    findBestSplitTAO,
) where

import qualified Data.List as L
import DataFrame.DecisionTree.CondVec
import DataFrame.DecisionTree.Linear (bestLinearCandidate)
import DataFrame.DecisionTree.Pool (
    bestDiscreteCandidate,
    candidateParChunk,
    evalWithPenaltyVec,
 )
import DataFrame.DecisionTree.Predict
import DataFrame.DecisionTree.Prune (pruneDead)
import DataFrame.DecisionTree.Types
import DataFrame.Internal.Column (Columnable)
import DataFrame.Internal.DataFrame (DataFrame)
import DataFrame.Internal.Expression (Expr)

import Control.Parallel (par, pseq)
import Control.Parallel.Strategies (parListChunk, rdeepseq, using)
import Data.Function (on)
import Data.List (minimumBy)
import Data.Maybe (catMaybes, mapMaybe)
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU

-- | The constant per-fit context threaded through node optimization.
data TaoEnv = TaoEnv
    { teCache :: !CondCache
    , teCfg :: !TreeConfig
    , teTarget :: !T.Text
    , teConds :: ![CondVec]
    , teDf :: !DataFrame
    }

-- | Public TAO entry point over raw conditions; materializes each once.
taoOptimize ::
    forall a.
    (Columnable a, Ord a) =>
    TreeConfig ->
    T.Text ->
    [Expr Bool] ->
    DataFrame ->
    V.Vector Int ->
    Tree a ->
    Tree a
taoOptimize cfg target conds df =
    taoOptimizeCV @a cfg target (mapMaybe (materializeCondVec df) conds) df

-- | TAO outer loop over pre-evaluated candidates, then prune dead branches.
taoOptimizeCV ::
    forall a.
    (Columnable a, Ord a) =>
    TreeConfig ->
    T.Text ->
    [CondVec] ->
    DataFrame ->
    V.Vector Int ->
    Tree a ->
    Tree a
taoOptimizeCV cfg target condVecs df rootIndices initialTree =
    go 0 initialTree (lossWith baseCache initialTree)
  where
    baseCache = condCacheFromVecs condVecs
    lossWith cache = computeTreeLossCached @a cache target df rootIndices
    go iter tree prevLoss
        | iter >= taoIterations cfg = pruneDead tree
        | prevLoss - newLoss < taoConvergenceTol cfg = pruneDead tree'
        | otherwise = go (iter + 1) tree' newLoss
      where
        cache = addTreeCondsToCache df tree baseCache
        tree' = taoIterationCV @a cache cfg target condVecs df rootIndices tree
        newLoss = lossWith cache tree'

-- | Public single-iteration entry point.
taoIteration ::
    forall a.
    (Columnable a, Ord a) =>
    TreeConfig ->
    T.Text ->
    [Expr Bool] ->
    DataFrame ->
    V.Vector Int ->
    Tree a ->
    Tree a
taoIteration cfg target conds df rootIndices tree =
    let condVecs = mapMaybe (materializeCondVec df) conds
        cache = addTreeCondsToCache df tree (condCacheFromVecs condVecs)
     in taoIterationCV @a cache cfg target condVecs df rootIndices tree

-- | One bottom-to-top sweep: re-optimize every node level by level.
taoIterationCV ::
    forall a.
    (Columnable a, Ord a) =>
    CondCache ->
    TreeConfig ->
    T.Text ->
    [CondVec] ->
    DataFrame ->
    V.Vector Int ->
    Tree a ->
    Tree a
taoIterationCV cache cfg target condVecs df rootIndices tree =
    L.foldl'
        (optimizeDepthLevel env rootIndices)
        tree
        [treeDepth tree, treeDepth tree - 1 .. 0]
  where
    env = TaoEnv cache cfg target condVecs df

optimizeDepthLevel ::
    forall a.
    (Columnable a, Ord a) => TaoEnv -> V.Vector Int -> Tree a -> Int -> Tree a
optimizeDepthLevel env rootIndices tree = optimizeAtDepth @a env rootIndices tree 0

optimizeAtDepth ::
    forall a.
    (Columnable a, Ord a) =>
    TaoEnv -> V.Vector Int -> Tree a -> Int -> Int -> Tree a
optimizeAtDepth env indices tree currentDepth targetDepth
    | currentDepth == targetDepth = optimizeNode @a env indices tree
    | otherwise = case tree of
        Leaf v -> Leaf v
        Branch cond left right -> optimizeChildren @a env indices cond left right currentDepth targetDepth

-- | Optimize the two subtrees over their disjoint index sets in parallel.
optimizeChildren ::
    forall a.
    (Columnable a, Ord a) =>
    TaoEnv -> V.Vector Int -> Expr Bool -> Tree a -> Tree a -> Int -> Int -> Tree a
optimizeChildren env indices cond left right currentDepth targetDepth =
    forceTreeWork left' `par` (forceTreeWork right' `pseq` Branch cond left' right')
  where
    (indicesL, indicesR) = partitionIndicesCached (teCache env) cond (teDf env) indices
    left' = optimizeAtDepth @a env indicesL left (currentDepth + 1) targetDepth
    right' = optimizeAtDepth @a env indicesR right (currentDepth + 1) targetDepth

-- | Force a subtree's optimization work to WHNF for the parallel scheduler.
forceTreeWork :: Tree a -> ()
forceTreeWork (Leaf v) = v `seq` ()
forceTreeWork (Branch c l r) = c `seq` forceTreeWork l `seq` forceTreeWork r

-- | Re-optimize one node: pick its best split, or collapse to a leaf.
optimizeNode ::
    forall a. (Columnable a, Ord a) => TaoEnv -> V.Vector Int -> Tree a -> Tree a
optimizeNode env indices tree
    | V.null indices = tree
    | otherwise = case tree of
        Leaf _ -> leaf
        Branch oldCond left right -> rebuiltBranch env indices oldCond left right leaf
  where
    leaf = Leaf (majorityValueFromIndices @a (teTarget env) (teDf env) indices)

rebuiltBranch ::
    forall a.
    (Columnable a, Ord a) =>
    TaoEnv -> V.Vector Int -> Expr Bool -> Tree a -> Tree a -> Tree a -> Tree a
rebuiltBranch env indices oldCond left right leaf
    | underflows = leaf
    | otherwise = Branch newCond left right
  where
    newCond = findBestSplitTAO @a env indices left right oldCond
    (l, r) = partitionIndicesCached (teCache env) newCond (teDf env) indices
    underflows = V.length l < minLeafSize (teCfg env) || V.length r < minLeafSize (teCfg env)

-- | The lowest-penalty replacement condition for a node.
findBestSplitTAO ::
    forall a.
    (Columnable a) =>
    TaoEnv -> V.Vector Int -> Tree a -> Tree a -> Expr Bool -> Expr Bool
findBestSplitTAO env indices leftTree rightTree currentCond
    | V.null indices || null carePoints = currentCond
    | pureReplacementLinear cfg
    , Just c <- linearCandidate
    , isValidAtNode cfg (teDf env) indices c =
        c
    | otherwise = bestOfPool penaltyCV currentCond pool
  where
    cfg = teCfg env
    carePoints =
        identifyCarePointsCached @a
            (teCache env)
            (teTarget env)
            (teDf env)
            indices
            leftTree
            rightTree
    penaltyCV = evalWithPenaltyVec cfg carePoints
    linearCandidate = bestLinearCandidate cfg (teTarget env) (teDf env) carePoints
    valid = filterValidCandidates cfg indices (teConds env)
    pool =
        candidatePool
            env
            indices
            currentCond
            (bestDiscreteCandidate cfg penaltyCV valid)
            linearCandidate

bestOfPool :: (CondVec -> (Int, Int)) -> Expr Bool -> [CondVec] -> Expr Bool
bestOfPool _ currentCond [] = currentCond
bestOfPool penaltyCV _ pool = cvExpr (minimumBy (compare `on` penaltyCV) pool)

-- | Candidates valid for a node: both children keep the minimum leaf size.
filterValidCandidates :: TreeConfig -> V.Vector Int -> [CondVec] -> [CondVec]
filterValidCandidates cfg indices condVecs = map snd (filter fst (zip validity condVecs))
  where
    validity =
        map (validAtNode cfg indices) condVecs
            `using` parListChunk candidateParChunk rdeepseq

validAtNode :: TreeConfig -> V.Vector Int -> CondVec -> Bool
validAtNode cfg indices cv = nTrue >= minLeaf && (V.length indices - nTrue) >= minLeaf
  where
    minLeaf = minLeafSize cfg
    nTrue =
        V.foldl'
            (\ !acc i -> if cvVec cv VU.! i then acc + 1 else acc)
            (0 :: Int)
            indices

-- | The candidate pool to minimize over, each kept only if valid.
candidatePool ::
    TaoEnv ->
    V.Vector Int ->
    Expr Bool ->
    Maybe CondVec ->
    Maybe (Expr Bool) ->
    [CondVec]
candidatePool env indices currentCond discreteCV linearCandidate =
    filter
        (isValidAtNode (teCfg env) (teDf env) indices . cvExpr)
        (catMaybes [currentCV, discreteCV, linearCV])
  where
    currentCV = CondVec currentCond <$> lookupCondVec (teCache env) (teDf env) currentCond
    linearCV = linearCandidate >>= materializeCondVec (teDf env)