packages feed

dataframe-learn-1.0.2.0: src/DataFrame/DecisionTree/Tao.hs

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

-- | Tree Alternating Optimization: hold the tree fixed and re-optimize one node
-- at a time, bottom-up, minimizing care-point misroutes. Sibling subtrees at a
-- depth level are independent and optimized in parallel.
module DataFrame.DecisionTree.Tao (
    taoOptimize,
    taoOptimizeCV,
    taoIteration,
    taoIterationCV,
    optimizeNode,
    findBestSplitTAO,
) where

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 (foldl', 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 the node-optimization
-- recursion (the cache is rebuilt each iteration).
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: iterate until the iteration
-- budget or convergence tolerance is reached, 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 =
    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, scoring the left
-- in parallel with the right (the cache is read-only, so this is pure).
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 so the parallel scheduler has
-- something substantial to evaluate; pure and value-preserving.
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 when the
-- node is empty or the chosen split underflows 'minLeafSize'.
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, falling back to the
-- current condition when no valid candidate beats it.
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 (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)

-- | Validity-filtered candidates the node could split on: both children must
-- keep at least 'minLeafSize'. Scored in parallel chunks, order preserved.
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: the current condition, the best
-- discrete candidate, and the linear candidate, 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)