dataframe-learn 2.2.0.0 → 2.3.0.0
raw patch · 16 files changed
+197/−89 lines, 16 filesdep ~dataframe-coredep ~dataframe-operationsPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: dataframe-core, dataframe-operations
API changes (from Hackage documentation)
+ DataFrame.Synthesis: [synMaxAllocBytes] :: SynthesisConfig -> !Int
- DataFrame.Synthesis: SynthesisConfig :: !Int -> !Int -> !LossFunction -> !Int -> SynthesisConfig
+ DataFrame.Synthesis: SynthesisConfig :: !Int -> !Int -> !LossFunction -> !Int -> !Int -> SynthesisConfig
Files
- README.md +1/−1
- dataframe-learn.cabal +11/−11
- src-internal/DataFrame/DecisionTree/Cart.hs +25/−5
- src-internal/DataFrame/DecisionTree/Fit.hs +7/−3
- src-internal/DataFrame/DecisionTree/Linear.hs +18/−11
- src-internal/DataFrame/DecisionTree/Predict.hs +3/−1
- src-internal/DataFrame/DecisionTree/Tao.hs +1/−1
- src-internal/DataFrame/Featurize/Internal.hs +4/−2
- src/DataFrame/Boosting/AdaBoost.hs +6/−2
- src/DataFrame/Boosting/GBM.hs +22/−4
- src/DataFrame/DecisionTree/Model.hs +7/−2
- src/DataFrame/Metrics.hs +2/−1
- src/DataFrame/SVM/RFF.hs +7/−1
- src/DataFrame/Segmented.hs +4/−1
- src/DataFrame/Synthesis.hs +52/−5
- tests-internal/DecisionTree.hs +27/−38
README.md view
@@ -57,7 +57,7 @@ import qualified DataFrame.Typed as T import Data.Maybe (fromJust) -salesT = T.unsafeFreeze @'[T.Column "x" Double, T.Column "y" Double] sales+salesT = T.unsafeFreeze @'[ '("x", Double), '("y", Double) ] sales typedModel = fit defaultLinearConfig (T.col @"y") salesT scored = T.derive @"prediction" (predict typedModel) salesT
dataframe-learn.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: dataframe-learn-version: 2.2.0.0+version: 2.3.0.0 synopsis: Interpretable, expression-returning machine learning for the dataframe ecosystem. description: A small scikit-learn-style ML library where every model returns both an@@ -61,8 +61,8 @@ containers >= 0.6.7 && < 0.10, parallel >= 3.3 && < 4, random >= 1.2 && < 2,- dataframe-core >= 2.2 && < 2.3,- dataframe-operations >= 2.2 && < 2.3,+ dataframe-core >= 2.3 && < 2.4,+ dataframe-operations >= 2.3 && < 2.4, text >= 2.1 && < 3, vector >= 0.13 && < 0.15, vector-algorithms >= 0.9 && < 0.11@@ -105,10 +105,10 @@ containers >= 0.6.7 && < 0.10, parallel >= 3.3 && < 4, random >= 1.2 && < 2,- dataframe-core >= 2.2 && < 2.3,- dataframe-core >= 2.2 && < 2.3,- dataframe-operations >= 2.2 && < 2.3,- dataframe-operations >= 2.2 && < 2.3,+ dataframe-core >= 2.3 && < 2.4,+ dataframe-core >= 2.3 && < 2.4,+ dataframe-operations >= 2.3 && < 2.4,+ dataframe-operations >= 2.3 && < 2.4, dataframe-expr-serializer >= 1.2.0.1 && < 1.3, dataframe-learn:internal, text >= 2.1 && < 3,@@ -140,13 +140,13 @@ aeson >= 0.11.0.0 && < 3, bytestring >= 0.11 && < 0.14, containers >= 0.6.7 && < 0.10,- dataframe-core >= 2.2 && < 2.3,- dataframe-core >= 2.2 && < 2.3,+ dataframe-core >= 2.3 && < 2.4,+ dataframe-core >= 2.3 && < 2.4, dataframe-csv >= 2.3 && < 2.4, dataframe-learn, dataframe-learn:internal,- dataframe-operations >= 2.2 && < 2.3,- dataframe-operations >= 2.2 && < 2.3,+ dataframe-operations >= 2.3 && < 2.4,+ dataframe-operations >= 2.3 && < 2.4, HUnit >= 1.6 && < 1.8, QuickCheck >= 2 && < 3, random >= 1 && < 2,
src-internal/DataFrame/DecisionTree/Cart.hs view
@@ -18,15 +18,22 @@ ) where import DataFrame.DecisionTree.Types (Tree (..), TreeConfig (..))+import DataFrame.Errors (DataFrameException (..), TypeErrorContext (..)) import qualified DataFrame.Functions as F import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (DataFrame, columnNames, unsafeGetColumn)+import DataFrame.Internal.DataFrame (+ DataFrame,+ columnNames,+ getColumn,+ unsafeGetColumn,+ ) import DataFrame.Internal.Expression (Expr (..)) import DataFrame.Internal.Interpreter (interpret) import DataFrame.Internal.Types import DataFrame.Operations.Core (nRows) import DataFrame.Operators +import Control.Exception (throw) import Data.Either (fromRight) import Data.Function (on) import Data.List (foldl')@@ -37,7 +44,7 @@ import qualified Data.Vector as V import qualified Data.Vector.Algorithms.Merge as VA import qualified Data.Vector.Unboxed as VU-import Type.Reflection (typeRep)+import Type.Reflection (TypeRep, typeRep) {- | A one-hot feature column: per-row Double values plus the sklearn LEFT predicate (@x <= threshold@) over the ORIGINAL DataFrame.@@ -89,12 +96,25 @@ (maxTreeDepth cfg) (max 1 (minLeafSize cfg)) +{- | Read the target column at the type the tree is being fitted at. Names the+column and both types on failure: a bare @fromIntegral@ defaults to 'Integer'+and lands here, and the old message said only that something went wrong.+-} cartLabels :: forall a. (Columnable a) => DataFrame -> T.Text -> V.Vector a cartLabels df target = case interpret @a df (Col target) of- Right (TColumn column) -> fromRight err (toVector @a column)- _ -> err+ Right (TColumn column) -> fromRight (throw err) (toVector @a column)+ Left e -> throw e where- err = error "buildCartTree: cannot interpret target column"+ err =+ TypeMismatchException+ ( MkTypeErrorContext+ (Right (typeRep @a))+ ( Left (maybe "missing" columnTypeString (getColumn target df)) ::+ Either String (TypeRep a)+ )+ (Just (T.unpack target))+ (Just "buildCartTree")+ ) cartClasses :: (Ord a) => V.Vector a -> V.Vector a cartClasses = V.fromList . Set.toList . Set.fromList . V.toList
src-internal/DataFrame/DecisionTree/Fit.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -38,6 +39,7 @@ import DataFrame.DecisionTree.Prune (pruneDead, pruneExpr) import DataFrame.DecisionTree.Tao (taoOptimize, taoOptimizeCV) import DataFrame.DecisionTree.Types (Tree (..), TreeConfig (..))+import DataFrame.Errors (DataFrameException (..)) import qualified DataFrame.Functions as F import DataFrame.Internal.Column (Columnable, TypedColumn (..), toVector) import DataFrame.Internal.DataFrame (DataFrame)@@ -69,7 +71,8 @@ condVecs = candidatePool @a cfg target df initialTree = buildCartTree @a cfg target df indices = V.enumFromN 0 (nRows df)-fitDecisionTree _ expr _ = error ("Cannot create tree for compound expression: " ++ show expr)+fitDecisionTree _ expr _ =+ throw (NonColumnReferenceException ("fitDecisionTree: " <> T.pack (show expr))) -- | The deduplicated numeric + discrete candidate pool for a target column. candidatePool ::@@ -116,7 +119,7 @@ majorityValue :: forall a. (Columnable a, Ord a) => T.Text -> DataFrame -> a majorityValue target df- | M.null counts = error "Empty DataFrame in leaf"+ | M.null counts = throw (EmptyDataSetException "majorityValue (tree leaf)") | otherwise = fst (maximumBy (compare `on` snd) (M.toList counts)) where counts = getCounts @a target df@@ -197,7 +200,8 @@ pruned = pruneDead (taoOptimize @a cfg target conds df indices (buildCartTree @a cfg target df))-fitProbTree _ expr _ = error ("Cannot create prob tree for compound expression: " ++ show expr)+fitProbTree _ expr _ =+ throw (NonColumnReferenceException ("fitProbTree: " <> T.pack (show expr))) -- | Convert a 'ProbTree' into one @Expr Double@ per class. probExprs ::
src-internal/DataFrame/DecisionTree/Linear.hs view
@@ -15,7 +15,7 @@ materializeFeatureForCare, ) where -import DataFrame.DecisionTree.Numeric (NumExpr (..), numericCols)+import DataFrame.DecisionTree.Numeric (NumExpr (..), numExprCols, numericCols) import DataFrame.DecisionTree.Types ( CarePoint (..), Direction (..),@@ -33,27 +33,34 @@ import qualified Data.Vector.Unboxed as VU {- | Best oblique candidate, or 'Nothing' when the linear path is disabled or-there are too few care points to fit on.+there are too few care points to fit on. The target column is named so it can+be kept out of the feature set. -} bestLinearCandidate ::- TreeConfig -> DataFrame -> [CarePoint] -> Maybe (Expr Bool)-bestLinearCandidate cfg df carePoints+ TreeConfig -> T.Text -> DataFrame -> [CarePoint] -> Maybe (Expr Bool)+bestLinearCandidate cfg target df carePoints | not (useLinearSolver cfg) = Nothing | length carePoints < minCarePointsForLinear cfg = Nothing- | otherwise = fitLinearCandidate cfg df carePoints+ | otherwise = fitLinearCandidate cfg target df carePoints {- | Fit an L1 logistic regression to the care points and convert the resulting hyperplane to a condition, or 'Nothing' when no numeric features exist or the fitted model is all-zero or degenerate. -} fitLinearCandidate ::- TreeConfig -> DataFrame -> [CarePoint] -> Maybe (Expr Bool)-fitLinearCandidate cfg df carePoints = case materializedFeatures df carePoints of- [] -> Nothing- mats -> linearFromFeatures cfg carePoints mats+ TreeConfig -> T.Text -> DataFrame -> [CarePoint] -> Maybe (Expr Bool)+fitLinearCandidate cfg target df carePoints =+ case materializedFeatures target df carePoints of+ [] -> Nothing+ mats -> linearFromFeatures cfg carePoints mats -materializedFeatures :: DataFrame -> [CarePoint] -> [(T.Text, VU.Vector Double)]-materializedFeatures df carePoints = mapMaybe (materializeFeatureForCare df carePoints) (numericCols df)+materializedFeatures ::+ T.Text -> DataFrame -> [CarePoint] -> [(T.Text, VU.Vector Double)]+materializedFeatures target df carePoints =+ mapMaybe (materializeFeatureForCare df carePoints) (featureCols target df)++featureCols :: T.Text -> DataFrame -> [NumExpr]+featureCols target df = filter (notElem target . numExprCols) (numericCols df) linearFromFeatures :: TreeConfig -> [CarePoint] -> [(T.Text, VU.Vector Double)] -> Maybe (Expr Bool)
src-internal/DataFrame/DecisionTree/Predict.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -32,6 +33,7 @@ Tree (..), TreeConfig (..), )+import DataFrame.Errors (DataFrameException (..)) import DataFrame.Internal.Column (Columnable, TypedColumn (..), toVector) import DataFrame.Internal.DataFrame (DataFrame) import DataFrame.Internal.Expression (Expr (..))@@ -183,7 +185,7 @@ majorityOf :: M.Map a Int -> a majorityOf counts- | M.null counts = error "Empty indices in majorityValueFromIndices"+ | M.null counts = throw (EmptyDataSetException "majorityValueFromIndices") | otherwise = fst (maximumBy (compare `on` snd) (M.toList counts)) computeTreeLoss ::
src-internal/DataFrame/DecisionTree/Tao.hs view
@@ -213,7 +213,7 @@ leftTree rightTree penaltyCV = evalWithPenaltyVec cfg carePoints- linearCandidate = bestLinearCandidate cfg (teDf env) carePoints+ linearCandidate = bestLinearCandidate cfg (teTarget env) (teDf env) carePoints valid = filterValidCandidates cfg indices (teConds env) pool = candidatePool
src-internal/DataFrame/Featurize/Internal.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -166,7 +167,8 @@ -- | The column name behind a @Col@ feature expression. columnExprName :: Expr Double -> T.Text columnExprName (Col n) = n-columnExprName e = error ("expected a column expression, got " ++ show e)+columnExprName e =+ throw (NonColumnReferenceException ("columnExprName: " <> T.pack (show e))) -- | Interpret a @Col@ (or numeric) expression to a @Double@ vector. materializeColumn :: DataFrame -> Expr Double -> VU.Vector Double@@ -197,7 +199,7 @@ argExtreme :: (Columnable a) => (Expr Double -> Expr Double -> Expr Bool) -> [(a, Expr Double)] -> Expr a-argExtreme _ [] = error "argExtreme: no classes"+argExtreme _ [] = throw (EmptyDataSetException "argExtreme") argExtreme _ [(c, _)] = Lit c argExtreme cmp ((c, sc) : rest) = If
src/DataFrame/Boosting/AdaBoost.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-}@@ -19,10 +20,13 @@ AdaBoostModel (..), ) where +import Control.Exception (throw) import Data.List (sort) import Data.Maybe (fromMaybe, maybeToList)+import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU+import DataFrame.Errors (DataFrameException (..)) import DataFrame.DecisionTree.Cart ( CartFeature (..),@@ -101,7 +105,7 @@ clamp e = max 1e-10 (min (1 - 1e-10) e) normalize v = let s = VU.sum v in if s == 0 then v else VU.map (/ s) v fitAdaBoost _ expr _ =- error ("fitAdaBoost: target must be a column, got " ++ show expr)+ throw (NonColumnReferenceException ("fitAdaBoost: " <> T.pack (show expr))) predictCodes :: forall a.@@ -113,7 +117,7 @@ preds :: [a] preds = case interpret df (treeToExpr stump) of Right (TColumn c) -> either (const []) V.toList (toVector @a @V.Vector c)- Left e -> error (show e)+ Left e -> throw e toCode v = fromMaybe 0 (V.findIndex (== v) classesV) -- | A depth-bounded weighted classification tree (weighted Gini splits).
src/DataFrame/Boosting/GBM.hs view
@@ -23,11 +23,13 @@ gbDecisionExpr, ) where +import Control.Exception (throw) import Data.Either (fromRight) import qualified Data.Map.Strict as M import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU+import DataFrame.Errors (DataFrameException (..)) import DataFrame.DecisionTree.Cart (cartFeatures) import DataFrame.DecisionTree.Fit (treeToExpr)@@ -118,16 +120,32 @@ boost !m fScores ts ss usageAcc | m >= gbNEstimators cfg = (ts, ss, usageAcc) | otherwise =- let grad = negGradient (gbLoss cfg) y fScores- tree = fitRegTreeOn rtCfg feats grad Nothing+ let (target', weights) = newtonStep (gbLoss cfg) y fScores+ tree = fitRegTreeOn rtCfg feats target' weights pred = predictTree df tree fScores' = VU.zipWith (\f p -> f + lr * p) fScores pred score = lossValue (gbLoss cfg) y fScores' usage' = foldr (\c -> M.insertWith (+) c 1) usageAcc (treeColumns tree) in boost (m + 1) fScores' (tree : ts) (score : ss) usage' fitGBM _ expr _ =- error ("fitGBM: target must be a column, got " ++ show expr)+ throw (NonColumnReferenceException ("fitGBM: " <> T.pack (show expr))) +newtonStep ::+ GBLoss ->+ VU.Vector Double ->+ VU.Vector Double ->+ (VU.Vector Double, Maybe (VU.Vector Double))+newtonStep SquaredError y f = (negGradient SquaredError y f, Nothing)+newtonStep LogisticDeviance y f = (z, Just h)+ where+ p = VU.map sigmoid f+ h = VU.map (\pi' -> max hFloor (pi' * (1 - pi'))) p+ z = VU.zipWith3 (\yi pi' hi -> (yi - pi') / hi) y p h++-- | Floor on the Hessian, so a saturated row cannot produce an unbounded step.+hFloor :: Double+hFloor = 1e-6+ negGradient :: GBLoss -> VU.Vector Double -> VU.Vector Double -> VU.Vector Double negGradient SquaredError y f = VU.zipWith (-) y f@@ -159,7 +177,7 @@ predictTree :: DataFrame -> Tree Double -> VU.Vector Double predictTree df t = case interpret @Double df (treeToExpr t) of Right (TColumn c) -> fromRight VU.empty (toVector @Double @VU.Vector c)- Left e -> error (show e)+ Left e -> throw e treeColumns :: Tree Double -> [T.Text] treeColumns = getColumns . treeToExpr
src/DataFrame/DecisionTree/Model.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} @@ -17,8 +18,10 @@ DecisionTreeRegressor (..), ) where +import Control.Exception (throw) import qualified Data.Map.Strict as M import qualified Data.Text as T+import DataFrame.Errors (DataFrameException (..)) import qualified Data.Vector as V @@ -83,8 +86,10 @@ (targetDoubles target df) Nothing _ ->- error- ("fit @DecisionTreeRegressor: target must be a column, got " ++ show target)+ throw+ ( NonColumnReferenceException+ ("fit @DecisionTreeRegressor: " <> T.pack (show target))+ ) e = treeToExpr t instance Predict DecisionTreeRegressor where
src/DataFrame/Metrics.hs view
@@ -38,6 +38,7 @@ f1Of, ) where +import Control.Exception (throw) import Data.Either (fromRight) import Data.List (nub, sort, sortBy) import Data.Ord (comparing)@@ -70,7 +71,7 @@ columnOf :: DataFrame -> Expr Double -> VU.Vector Double columnOf df e = case interpret @Double df e of Right (TColumn c) -> fromRight VU.empty (toVector @Double @VU.Vector c)- Left err -> error (show err)+ Left err -> throw err n2 :: VU.Vector Double -> Double n2 = fromIntegral . VU.length
src/DataFrame/SVM/RFF.hs view
@@ -18,10 +18,12 @@ RFFSVMModel (..), ) where +import Control.Exception (throw) import Data.List (sort) import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU+import DataFrame.Errors (DataFrameException (..)) import DataFrame.Featurize.Internal (featureNames, numericMatrix, targetValues) import qualified DataFrame.Functions as F@@ -100,7 +102,11 @@ fitRFFSVM cfg target df = case classes of [neg, pos] -> build neg pos- _ -> error "fitRFFSVM: binary classification only (got /= 2 classes)"+ _ ->+ throw+ ( InternalException+ "fitRFFSVM: binary classification only, but the target has /= 2 classes"+ ) where names = featureNames target df (nameVec, mat) = numericMatrix names df
src/DataFrame/Segmented.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-}@@ -23,6 +24,7 @@ SegmentFit (..), ) where +import Control.Exception (throw) import Data.List (foldl', (\\)) import qualified Data.Map.Strict as M import Data.Maybe (isJust)@@ -30,6 +32,7 @@ import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Unboxed as VU+import DataFrame.Errors (DataFrameException (..)) import DataFrame.Featurize.Internal (featureNames, numericMatrix, targetDoubles) import DataFrame.Internal.Column (@@ -302,7 +305,7 @@ where names = case dfs of (d0 : _) -> featureNames target d0- [] -> error "shrinkLinear: no segments"+ [] -> throw (EmptyDataSetException "shrinkLinear") d = length names mats = [snd (numericMatrix names dframe) | dframe <- dfs] ys = [targetDoubles target dframe | dframe <- dfs]
src/DataFrame/Synthesis.hs view
@@ -1,18 +1,16 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {- | Feature synthesis by bottom-up enumerative search with observational equivalence — the canonical enumerative method from Solar-Lezama's-/Introduction to Program Synthesis/, hardened for a numeric, examples-only-setting.+/Introduction to Program Synthesis/. Given a frame and a numeric target column, it searches for a small, interpretable-arithmetic expression over the other columns whose values track the target. The-specification is purely the example rows; there is no SMT solver and no logical-spec. Deterministic and pure.+arithmetic expression over the other columns whose values track the target. The engine: @@ -48,6 +46,7 @@ synthesizeFeatures, ) where +import Control.Exception (throw) import Data.Bits (xor) import Data.Either (fromRight) import Data.List (sortBy)@@ -59,6 +58,7 @@ import Data.Word (Word64) import GHC.Float (castDoubleToWord64) +import DataFrame.Errors (DataFrameException (..)) import DataFrame.Featurize.Internal (featureNames) import qualified DataFrame.Functions as F import DataFrame.Internal.DataFrame (DataFrame)@@ -91,6 +91,8 @@ , synLoss :: !LossFunction , synTopK :: !Int -- ^ How many ranked features to return in the bank.+ , synMaxAllocBytes :: !Int+ -- ^ Refuse a search whose largest layer would allocate beyond this. } deriving (Eq, Show) @@ -101,6 +103,7 @@ , synBankCap = 500 , synLoss = PearsonCorrelation , synTopK = 16+ , synMaxAllocBytes = 8 * 1024 * 1024 * 1024 } {- | A synthesized feature. 'sfExpr' is the best-scoring expression and 'sfFeatures'@@ -134,6 +137,7 @@ SynthesisConfig -> Expr Double -> DataFrame -> SynthesizedFeature synthesizeFeatures cfg target df | null leaves || VU.null tgt = SynthesizedFeature (Lit 0) (negate (1 / 0)) []+ | Just err <- oversizedSearch cfg (length leaves) n = throw err | otherwise = SynthesizedFeature best bestScore ranked where feats = featureNames target df@@ -276,6 +280,49 @@ absorb cfg tgt seen0 cands = (capLayer cfg tgt fresh, seen') where (fresh, seen') = dedupProgs seen0 cands++oversizedSearch :: SynthesisConfig -> Int -> Int -> Maybe DataFrameException+oversizedSearch cfg nLeaves n+ | synMaxSize cfg <= 3 = Nothing+ | estimate <= budget = Nothing+ | otherwise =+ Just+ ( InternalException+ ( "synthesizeFeatures: a search to synMaxSize="+ <> T.pack (show (synMaxSize cfg))+ <> " over "+ <> T.pack (show nLeaves)+ <> " leaves and "+ <> T.pack (show n)+ <> " rows would allocate about "+ <> T.pack (show (estimate `div` (1024 * 1024 * 1024)))+ <> " GiB, past the "+ <> T.pack (show (budget `div` (1024 * 1024 * 1024)))+ <> " GiB synMaxAllocBytes budget. Lower synMaxSize to "+ <> T.pack (show largestFittingSize)+ <> ", narrow the column set, or raise synMaxAllocBytes."+ )+ )+ where+ budget = synMaxAllocBytes cfg+ bytesPerProg = 8 * max 1 n+ -- 4 commutative ops over unordered pairs plus sub and div over ordered+ -- pairs, all quadratic in the bank; unaries and powers are lower order.+ binaryOpCount = 4 :: Int+ -- Layer k pairs the capped bank with itself over the binary operators.+ candidatesAt k+ | k <= 2 = nLeaves+ | otherwise =+ binaryOpCount * min (synBankCap cfg) (candidatesAt (k - 1)) ^ (2 :: Int)+ estimate = sum [candidatesAt k * bytesPerProg | k <- [2 .. synMaxSize cfg]]+ largestFittingSize =+ last+ ( 3+ : [ k+ | k <- [3 .. synMaxSize cfg]+ , sum [candidatesAt j * bytesPerProg | j <- [2 .. k]] <= budget+ ]+ ) -- | When a layer has more distinct programs than the cap, keep the best-scoring. capLayer :: SynthesisConfig -> Output -> [Prog] -> [Prog]
tests-internal/DecisionTree.hs view
@@ -441,7 +441,6 @@ 0.0 finalLoss --- Shared setup for C2 (a) and (b): axis-aligned pool only, oblique label. obliqueAxisAlignedFixture :: (D.DataFrame, V.Vector Int, [Expr Bool], Tree T.Text) obliqueAxisAlignedFixture =@@ -463,8 +462,6 @@ Tree T.Text in (df, indices, axisConds, initTree) --- C2 (a): with the linear solver OFF, axis-aligned pool cannot recover the--- oblique decision boundary. Preserves the original guarantee of the test. taoAxisAlignedInsufficientForObliqueDiscreteOnly :: Test taoAxisAlignedInsufficientForObliqueDiscreteOnly = TestCase $ do let (df, indices, axisConds, initTree) = obliqueAxisAlignedFixture@@ -481,8 +478,6 @@ "axis-aligned stump cannot recover oblique label without linear solver (loss > 0.1)" (finalLoss > 0.1) --- C2 (b): with the linear solver ON, the L1-LR fit discovers the oblique--- (x + y) hyperplane even though only axis-aligned conditions are in the pool. taoLinearRecoversObliqueFromAxisAlignedPool :: Test taoLinearRecoversObliqueFromAxisAlignedPool = TestCase $ do let (df, indices, axisConds, initTree) = obliqueAxisAlignedFixture@@ -501,10 +496,6 @@ 0.0 finalLoss ---------------------------------------------------------------------------- Nullable numeric feature tests-------------------------------------------------------------------------- -- Cleanly separable nullable column (no actual nulls): Just 1..6 -> "pos", -- Just 7..12 -> "neg". Exercises the nullable numeric path. nullableSepDF :: D.DataFrame@@ -633,10 +624,6 @@ "combined exprs include NMaybeDouble (nullable arithmetic)" (any (\case NMaybeDouble _ -> True; _ -> False) exprs) ---------------------------------------------------------------------------- Probability tree tests-------------------------------------------------------------------------- -- probsFromIndices: counts correct on a 3-row slice probsFromIndicesBasic :: Test probsFromIndicesBasic = TestCase $ do@@ -780,13 +767,6 @@ ) indices ---------------------------------------------------------------------------- C4-C9 / D-series: linear solver integration tests----------------------------------------------------------------------------- C4: Nested oblique recovery without oblique hints; label set by two oblique--- boundaries but only axis-aligned thresholds in the pool. The linear solver--- should learn both splits and reach zero loss. taoRecoversNestedObliqueWithoutHint :: Test taoRecoversNestedObliqueWithoutHint = TestCase $ do let labelExpr =@@ -828,9 +808,6 @@ 0.0 finalLoss --- C5: Monotone loss across iterations with the linear solver enabled.--- Resolves Issue 1 from the prior plan (currentCond included in the--- competition pool). taoMonotoneWithLinear :: Test taoMonotoneWithLinear = TestCase $ do let indices = V.enumFromN 0 20@@ -868,8 +845,6 @@ 0.0 finalLoss --- C8: Linear solver respects the L1 penalty and produces sparse hyperplanes--- on data where only some features are informative. taoLinearProducesSparsity :: Test taoLinearProducesSparsity = TestCase $ do let n = 50 :: Int@@ -911,7 +886,32 @@ ) ("a" `elem` rootCols || "b" `elem` rootCols) --- C9: Determinism — same training data produces an equal (eqExpr) tree.+taoLinearDoesNotUseTarget :: Test+taoLinearDoesNotUseTarget = TestCase $ do+ let n = 40 :: Int+ as = [fromIntegral (i `div` 4) :: Double | i <- [0 .. n - 1]]+ bs = [fromIntegral (i `mod` 3) :: Double | i <- [0 .. n - 1]]+ targets = [if even i then 1.0 else 0.0 :: Double | i <- [0 .. n - 1]]+ df =+ D.fromNamedColumns+ [ ("target", DI.fromList targets)+ , ("a", DI.fromList as)+ , ("b", DI.fromList bs)+ ]+ cfg =+ defaultTreeConfig+ { maxTreeDepth = 1+ , taoIterations = 10+ , minLeafSize = 1+ , useLinearSolver = True+ , minCarePointsForLinear = 2+ }+ result = fitDecisionTree @Double cfg (Col "target") df+ rootCols = getColumns result+ assertBool+ ("target must not appear in the fitted Expr (got " ++ show result ++ ")")+ ("target" `notElem` rootCols)+ taoLinearDeterministic :: Test taoLinearDeterministic = TestCase $ do let cfg =@@ -945,13 +945,6 @@ "skipping linear solver yields same expression as linear-off baseline" (eqExpr result resultOff) ---------------------------------------------------------------------------- Categorical-condition generator tests (Phase 1-2 of the plan)----------------------------------------------------------------------------- A binary-target DataFrame with a 5-level Text column whose levels have--- monotonically-increasing positive rates. Breiman's algorithm should--- enumerate the 4 contiguous-prefix splits in that exact rate order. breimanBinaryDF :: D.DataFrame breimanBinaryDF = let n = 100 :: Int@@ -1085,11 +1078,6 @@ feats' = filter (\c -> feat `elem` getColumns c) conds assertEqual "Breiman prefixes on nullable column ignore nulls" 2 (length feats') ---------------------------------------------------------------------------- PR 2 extended: threshold-consolidation rewrite in combineAndVec /--- combineOrVec. Positive cases, negative cases, semantic-preservation check.-------------------------------------------------------------------------- -- A small synthetic DataFrame to materialize CondVecs against. threshFixtureDF :: D.DataFrame threshFixtureDF =@@ -1329,6 +1317,7 @@ , TestLabel "C5 taoMonotoneWithLinear" taoMonotoneWithLinear , TestLabel "C6 taoLinearVsDiscreteCompetition" taoLinearVsDiscreteCompetition , TestLabel "C8 taoLinearProducesSparsity" taoLinearProducesSparsity+ , TestLabel "C8b taoLinearDoesNotUseTarget" taoLinearDoesNotUseTarget , TestLabel "C9 taoLinearDeterministic" taoLinearDeterministic , TestLabel "D1 taoLinearTinyCareSet" taoLinearTinyCareSet , TestLabel "E1 categoricalBreimanBinary" testCategoricalBreimanBinary