dataframe-learn 1.0.2.0 → 1.1.0.0
raw patch · 46 files changed
+5735/−791 lines, 46 filesdep +randomdep ~dataframe-coredep ~dataframe-operationsdep ~parallelPVP ok
version bump matches the API change (PVP)
Dependencies added: random
Dependency ranges changed: dataframe-core, dataframe-operations, parallel, text
API changes (from Hackage documentation)
- DataFrame.Synthesis: BeamConfig :: Int -> Int -> LossFunction -> Bool -> BeamConfig
- DataFrame.Synthesis: F1 :: LossFunction
- DataFrame.Synthesis: [beamLength] :: BeamConfig -> Int
- DataFrame.Synthesis: [includeConditionals] :: BeamConfig -> Bool
- DataFrame.Synthesis: [lossFunction] :: BeamConfig -> LossFunction
- DataFrame.Synthesis: [searchDepth] :: BeamConfig -> Int
- DataFrame.Synthesis: beamSearch :: DataFrame -> BeamConfig -> TypedColumn Double -> [Expr Double] -> [Expr Bool] -> [Expr Double] -> Maybe (Expr Double)
- DataFrame.Synthesis: data BeamConfig
- DataFrame.Synthesis: deduplicate :: Columnable a => DataFrame -> [Expr a] -> [(Expr a, TypedColumn a)]
- DataFrame.Synthesis: defaultBeamConfig :: BeamConfig
- DataFrame.Synthesis: equivalent :: DataFrame -> Expr Double -> Expr Double -> Bool
- DataFrame.Synthesis: f1FromBinary :: Vector Double -> Vector Double -> Maybe Double
- DataFrame.Synthesis: f1FromCounts :: Int -> Int -> Int -> Maybe Double
- DataFrame.Synthesis: fitClassifier :: Text -> Int -> Int -> DataFrame -> Either String (Expr Int)
- DataFrame.Synthesis: fitRegression :: Text -> Int -> Int -> DataFrame -> Either String (Expr Double)
- DataFrame.Synthesis: generateConditions :: TypedColumn Double -> [Expr Bool] -> [Expr Double] -> DataFrame -> [Expr Bool]
- DataFrame.Synthesis: generatePrograms :: Bool -> [Expr Bool] -> [Expr Double] -> [Expr Double] -> [Expr Double] -> [Expr Double]
- DataFrame.Synthesis: getLossFunction :: LossFunction -> Vector Double -> Vector Double -> Maybe Double
- DataFrame.Synthesis: isConditional :: Expr a -> Bool
- DataFrame.Synthesis: isLiteral :: Expr a -> Bool
- DataFrame.Synthesis: percentiles :: DataFrame -> [Expr Double]
- DataFrame.Synthesis: pickTopN :: DataFrame -> TypedColumn Double -> BeamConfig -> [(Expr Double, TypedColumn a)] -> [Expr Double]
- DataFrame.Synthesis: pickTopNBool :: DataFrame -> TypedColumn Double -> [(Expr Bool, TypedColumn Bool)] -> [Expr Bool]
- DataFrame.Synthesis: roundTo2SigDigits :: Double -> Double
- DataFrame.Synthesis: roundToSigDigits :: Int -> Double -> Double
- DataFrame.Synthesis: satisfiesExamples :: DataFrame -> TypedColumn Double -> Expr Double -> Bool
- DataFrame.Synthesis: synthesizeFeatureExpr :: Text -> BeamConfig -> DataFrame -> Either String (Expr Double)
+ DataFrame.Boosting.AdaBoost: AdaBoostConfig :: !Int -> !Int -> AdaBoostConfig
+ DataFrame.Boosting.AdaBoost: AdaBoostModel :: !Vector Double -> !Vector (Tree a) -> !Vector a -> AdaBoostModel a
+ DataFrame.Boosting.AdaBoost: [abAlphas] :: AdaBoostModel a -> !Vector Double
+ DataFrame.Boosting.AdaBoost: [abClasses] :: AdaBoostModel a -> !Vector a
+ DataFrame.Boosting.AdaBoost: [abMaxDepth] :: AdaBoostConfig -> !Int
+ DataFrame.Boosting.AdaBoost: [abNEstimators] :: AdaBoostConfig -> !Int
+ DataFrame.Boosting.AdaBoost: [abStumps] :: AdaBoostModel a -> !Vector (Tree a)
+ DataFrame.Boosting.AdaBoost: data AdaBoostConfig
+ DataFrame.Boosting.AdaBoost: data AdaBoostModel a
+ DataFrame.Boosting.AdaBoost: defaultAdaBoostConfig :: AdaBoostConfig
+ DataFrame.Boosting.AdaBoost: instance (DataFrame.Internal.Column.Columnable a, GHC.Classes.Ord a) => DataFrame.Model.Fit DataFrame.Boosting.AdaBoost.AdaBoostConfig (DataFrame.Internal.Expression.Expr a) (DataFrame.Boosting.AdaBoost.AdaBoostModel a)
+ DataFrame.Boosting.AdaBoost: instance (DataFrame.Internal.Column.Columnable a, GHC.Classes.Ord a) => DataFrame.Model.Predict (DataFrame.Boosting.AdaBoost.AdaBoostModel a) a
+ DataFrame.Boosting.AdaBoost: instance GHC.Classes.Eq DataFrame.Boosting.AdaBoost.AdaBoostConfig
+ DataFrame.Boosting.AdaBoost: instance GHC.Show.Show DataFrame.Boosting.AdaBoost.AdaBoostConfig
+ DataFrame.Boosting.AdaBoost: instance GHC.Show.Show a => GHC.Show.Show (DataFrame.Boosting.AdaBoost.AdaBoostModel a)
+ DataFrame.Boosting.GBM: GBConfig :: !GBLoss -> !Int -> !Double -> !Int -> !Int -> GBConfig
+ DataFrame.Boosting.GBM: GBModel :: !Double -> !Vector (Tree Double) -> !Double -> !GBLoss -> !Vector Double -> !Map Text Int -> GBModel
+ DataFrame.Boosting.GBM: LogisticDeviance :: GBLoss
+ DataFrame.Boosting.GBM: SquaredError :: GBLoss
+ DataFrame.Boosting.GBM: [gbFeatureUsage] :: GBModel -> !Map Text Int
+ DataFrame.Boosting.GBM: [gbInit] :: GBModel -> !Double
+ DataFrame.Boosting.GBM: [gbLearningRate] :: GBConfig -> !Double
+ DataFrame.Boosting.GBM: [gbLoss] :: GBConfig -> !GBLoss
+ DataFrame.Boosting.GBM: [gbMaxDepth] :: GBConfig -> !Int
+ DataFrame.Boosting.GBM: [gbModelLoss] :: GBModel -> !GBLoss
+ DataFrame.Boosting.GBM: [gbNEstimators] :: GBConfig -> !Int
+ DataFrame.Boosting.GBM: [gbRate] :: GBModel -> !Double
+ DataFrame.Boosting.GBM: [gbSeed] :: GBConfig -> !Int
+ DataFrame.Boosting.GBM: [gbTrainScore] :: GBModel -> !Vector Double
+ DataFrame.Boosting.GBM: [gbTrees] :: GBModel -> !Vector (Tree Double)
+ DataFrame.Boosting.GBM: data GBConfig
+ DataFrame.Boosting.GBM: data GBLoss
+ DataFrame.Boosting.GBM: data GBModel
+ DataFrame.Boosting.GBM: defaultGBConfig :: GBConfig
+ DataFrame.Boosting.GBM: gbDecisionExpr :: GBModel -> Expr Bool
+ DataFrame.Boosting.GBM: gbExprAtStage :: Int -> GBModel -> Maybe (Expr Double)
+ DataFrame.Boosting.GBM: gbProbaExpr :: GBModel -> Expr Double
+ DataFrame.Boosting.GBM: instance DataFrame.Model.Fit DataFrame.Boosting.GBM.GBConfig (DataFrame.Internal.Expression.Expr GHC.Types.Double) DataFrame.Boosting.GBM.GBModel
+ DataFrame.Boosting.GBM: instance DataFrame.Model.Predict DataFrame.Boosting.GBM.GBModel GHC.Types.Double
+ DataFrame.Boosting.GBM: instance GHC.Classes.Eq DataFrame.Boosting.GBM.GBConfig
+ DataFrame.Boosting.GBM: instance GHC.Classes.Eq DataFrame.Boosting.GBM.GBLoss
+ DataFrame.Boosting.GBM: instance GHC.Show.Show DataFrame.Boosting.GBM.GBConfig
+ DataFrame.Boosting.GBM: instance GHC.Show.Show DataFrame.Boosting.GBM.GBLoss
+ DataFrame.Boosting.GBM: instance GHC.Show.Show DataFrame.Boosting.GBM.GBModel
+ DataFrame.DBSCAN: DBSCANConfig :: !Double -> !Int -> DBSCANConfig
+ DataFrame.DBSCAN: DBSCANModel :: !Vector Int -> !Vector Int -> !Int -> DBSCANModel
+ DataFrame.DBSCAN: [dbCoreSampleIndices] :: DBSCANModel -> !Vector Int
+ DataFrame.DBSCAN: [dbEps] :: DBSCANConfig -> !Double
+ DataFrame.DBSCAN: [dbLabels] :: DBSCANModel -> !Vector Int
+ DataFrame.DBSCAN: [dbMinSamples] :: DBSCANConfig -> !Int
+ DataFrame.DBSCAN: [dbNClusters] :: DBSCANModel -> !Int
+ DataFrame.DBSCAN: data DBSCANConfig
+ DataFrame.DBSCAN: data DBSCANModel
+ DataFrame.DBSCAN: dbscanSurrogateExpr :: TreeConfig -> [Expr Double] -> DBSCANModel -> DataFrame -> Expr Int
+ DataFrame.DBSCAN: defaultDBSCANConfig :: DBSCANConfig
+ DataFrame.DBSCAN: instance DataFrame.Model.Fit DataFrame.DBSCAN.DBSCANConfig [DataFrame.Internal.Expression.Expr GHC.Types.Double] DataFrame.DBSCAN.DBSCANModel
+ DataFrame.DBSCAN: instance GHC.Classes.Eq DataFrame.DBSCAN.DBSCANConfig
+ DataFrame.DBSCAN: instance GHC.Classes.Eq DataFrame.DBSCAN.DBSCANModel
+ DataFrame.DBSCAN: instance GHC.Show.Show DataFrame.DBSCAN.DBSCANConfig
+ DataFrame.DBSCAN: instance GHC.Show.Show DataFrame.DBSCAN.DBSCANModel
+ DataFrame.DecisionTree.Model: DecisionTreeClassifier :: !Expr a -> !Int -> !Int -> !Map Text Int -> DecisionTreeClassifier a
+ DataFrame.DecisionTree.Model: DecisionTreeRegressor :: !Tree Double -> !Expr Double -> !Int -> !Int -> !Map Text Int -> DecisionTreeRegressor
+ DataFrame.DecisionTree.Model: [dtcDepth] :: DecisionTreeClassifier a -> !Int
+ DataFrame.DecisionTree.Model: [dtcExpr] :: DecisionTreeClassifier a -> !Expr a
+ DataFrame.DecisionTree.Model: [dtcFeatureUsage] :: DecisionTreeClassifier a -> !Map Text Int
+ DataFrame.DecisionTree.Model: [dtcNLeaves] :: DecisionTreeClassifier a -> !Int
+ DataFrame.DecisionTree.Model: [dtrDepth] :: DecisionTreeRegressor -> !Int
+ DataFrame.DecisionTree.Model: [dtrExpr] :: DecisionTreeRegressor -> !Expr Double
+ DataFrame.DecisionTree.Model: [dtrFeatureUsage] :: DecisionTreeRegressor -> !Map Text Int
+ DataFrame.DecisionTree.Model: [dtrNLeaves] :: DecisionTreeRegressor -> !Int
+ DataFrame.DecisionTree.Model: [dtrTree] :: DecisionTreeRegressor -> !Tree Double
+ DataFrame.DecisionTree.Model: data DecisionTreeClassifier a
+ DataFrame.DecisionTree.Model: data DecisionTreeRegressor
+ DataFrame.DecisionTree.Model: instance (DataFrame.Internal.Column.Columnable a, GHC.Classes.Ord a) => DataFrame.Model.Fit DataFrame.DecisionTree.Types.TreeConfig (DataFrame.Internal.Expression.Expr a) (DataFrame.DecisionTree.Model.DecisionTreeClassifier a)
+ DataFrame.DecisionTree.Model: instance DataFrame.Model.Fit DataFrame.DecisionTree.Regression.RegTreeConfig (DataFrame.Internal.Expression.Expr GHC.Types.Double) DataFrame.DecisionTree.Model.DecisionTreeRegressor
+ DataFrame.DecisionTree.Model: instance DataFrame.Model.Predict (DataFrame.DecisionTree.Model.DecisionTreeClassifier a) a
+ DataFrame.DecisionTree.Model: instance DataFrame.Model.Predict DataFrame.DecisionTree.Model.DecisionTreeRegressor GHC.Types.Double
+ DataFrame.DecisionTree.Model: instance GHC.Show.Show DataFrame.DecisionTree.Model.DecisionTreeRegressor
+ DataFrame.DecisionTree.Model: instance GHC.Show.Show a => GHC.Show.Show (DataFrame.DecisionTree.Model.DecisionTreeClassifier a)
+ DataFrame.DecisionTree.Regression: RegTreeConfig :: !Int -> !Int -> !Int -> !Double -> RegTreeConfig
+ DataFrame.DecisionTree.Regression: [rtMaxDepth] :: RegTreeConfig -> !Int
+ DataFrame.DecisionTree.Regression: [rtMinImpurityDecrease] :: RegTreeConfig -> !Double
+ DataFrame.DecisionTree.Regression: [rtMinLeafSize] :: RegTreeConfig -> !Int
+ DataFrame.DecisionTree.Regression: [rtMinSamplesSplit] :: RegTreeConfig -> !Int
+ DataFrame.DecisionTree.Regression: data RegTreeConfig
+ DataFrame.DecisionTree.Regression: defaultRegTreeConfig :: RegTreeConfig
+ DataFrame.DecisionTree.Regression: fitRegTreeOn :: RegTreeConfig -> Vector CartFeature -> Vector Double -> Maybe (Vector Double) -> Tree Double
+ DataFrame.DecisionTree.Regression: instance GHC.Classes.Eq DataFrame.DecisionTree.Regression.RegTreeConfig
+ DataFrame.DecisionTree.Regression: instance GHC.Show.Show DataFrame.DecisionTree.Regression.RegTreeConfig
+ DataFrame.Featurize.Internal: Features :: ![Text] -> ![Vector Double] -> !Matrix -> !Int -> !Int -> Features
+ DataFrame.Featurize.Internal: [ftCols] :: Features -> ![Vector Double]
+ DataFrame.Featurize.Internal: [ftD] :: Features -> !Int
+ DataFrame.Featurize.Internal: [ftN] :: Features -> !Int
+ DataFrame.Featurize.Internal: [ftNames] :: Features -> ![Text]
+ DataFrame.Featurize.Internal: [ftRows] :: Features -> !Matrix
+ DataFrame.Featurize.Internal: affineExpr :: Double -> [(Double, Text)] -> Expr Double
+ DataFrame.Featurize.Internal: argMaxExpr :: Columnable a => [(a, Expr Double)] -> Expr a
+ DataFrame.Featurize.Internal: argMinExpr :: Columnable a => [(a, Expr Double)] -> Expr a
+ DataFrame.Featurize.Internal: columnExprName :: Expr Double -> Text
+ DataFrame.Featurize.Internal: data Features
+ DataFrame.Featurize.Internal: extractFeatures :: [Expr Double] -> DataFrame -> Features
+ DataFrame.Featurize.Internal: featureNames :: Expr a -> DataFrame -> [Text]
+ DataFrame.Featurize.Internal: materializeColumn :: DataFrame -> Expr Double -> Vector Double
+ DataFrame.Featurize.Internal: numericMatrix :: [Text] -> DataFrame -> (Vector Text, Matrix)
+ DataFrame.Featurize.Internal: targetDoubles :: Expr Double -> DataFrame -> Vector Double
+ DataFrame.Featurize.Internal: targetValues :: Columnable a => Expr a -> DataFrame -> Vector a
+ DataFrame.GMM: DiagCov :: CovType
+ DataFrame.GMM: FullCov :: CovType
+ DataFrame.GMM: GMMConfig :: !Int -> !CovType -> !Int -> !Double -> !Double -> !Int -> GMMConfig
+ DataFrame.GMM: GMMModel :: !Vector Double -> !Vector (Vector Double) -> !Vector Matrix -> !Bool -> !Int -> !Double -> !Int -> !Vector Text -> GMMModel
+ DataFrame.GMM: [gmmConverged] :: GMMModel -> !Bool
+ DataFrame.GMM: [gmmCovType] :: GMMConfig -> !CovType
+ DataFrame.GMM: [gmmCovariances] :: GMMModel -> !Vector Matrix
+ DataFrame.GMM: [gmmFeatureNames] :: GMMModel -> !Vector Text
+ DataFrame.GMM: [gmmK] :: GMMConfig -> !Int
+ DataFrame.GMM: [gmmLogLikelihood] :: GMMModel -> !Double
+ DataFrame.GMM: [gmmMaxIter] :: GMMConfig -> !Int
+ DataFrame.GMM: [gmmMeans] :: GMMModel -> !Vector (Vector Double)
+ DataFrame.GMM: [gmmNIter] :: GMMModel -> !Int
+ DataFrame.GMM: [gmmNObs] :: GMMModel -> !Int
+ DataFrame.GMM: [gmmRegCovar] :: GMMConfig -> !Double
+ DataFrame.GMM: [gmmSeed] :: GMMConfig -> !Int
+ DataFrame.GMM: [gmmTol] :: GMMConfig -> !Double
+ DataFrame.GMM: [gmmWeights] :: GMMModel -> !Vector Double
+ DataFrame.GMM: data CovType
+ DataFrame.GMM: data GMMConfig
+ DataFrame.GMM: data GMMModel
+ DataFrame.GMM: defaultGMMConfig :: GMMConfig
+ DataFrame.GMM: gmmAIC :: GMMModel -> Double
+ DataFrame.GMM: gmmBIC :: GMMModel -> Double
+ DataFrame.GMM: gmmLogDensityExprs :: GMMModel -> Map Int (Expr Double)
+ DataFrame.GMM: instance DataFrame.Model.Fit DataFrame.GMM.GMMConfig [DataFrame.Internal.Expression.Expr GHC.Types.Double] DataFrame.GMM.GMMModel
+ DataFrame.GMM: instance DataFrame.Model.Predict DataFrame.GMM.GMMModel GHC.Types.Int
+ DataFrame.GMM: instance GHC.Classes.Eq DataFrame.GMM.CovType
+ DataFrame.GMM: instance GHC.Classes.Eq DataFrame.GMM.GMMConfig
+ DataFrame.GMM: instance GHC.Classes.Eq DataFrame.GMM.GMMModel
+ DataFrame.GMM: instance GHC.Show.Show DataFrame.GMM.CovType
+ DataFrame.GMM: instance GHC.Show.Show DataFrame.GMM.GMMConfig
+ DataFrame.GMM: instance GHC.Show.Show DataFrame.GMM.GMMModel
+ DataFrame.KMeans: KMeansConfig :: !Int -> !Int -> !Int -> !Double -> !Int -> KMeansConfig
+ DataFrame.KMeans: KMeansModel :: !Vector (Vector Double) -> !Vector Int -> !Double -> !Int -> !Vector Text -> KMeansModel
+ DataFrame.KMeans: [kmCenters] :: KMeansModel -> !Vector (Vector Double)
+ DataFrame.KMeans: [kmFeatureNames] :: KMeansModel -> !Vector Text
+ DataFrame.KMeans: [kmInertia] :: KMeansModel -> !Double
+ DataFrame.KMeans: [kmK] :: KMeansConfig -> !Int
+ DataFrame.KMeans: [kmLabels] :: KMeansModel -> !Vector Int
+ DataFrame.KMeans: [kmMaxIter] :: KMeansConfig -> !Int
+ DataFrame.KMeans: [kmNInit] :: KMeansConfig -> !Int
+ DataFrame.KMeans: [kmNIter] :: KMeansModel -> !Int
+ DataFrame.KMeans: [kmSeed] :: KMeansConfig -> !Int
+ DataFrame.KMeans: [kmTol] :: KMeansConfig -> !Double
+ DataFrame.KMeans: data KMeansConfig
+ DataFrame.KMeans: data KMeansModel
+ DataFrame.KMeans: defaultKMeansConfig :: KMeansConfig
+ DataFrame.KMeans: instance DataFrame.Model.Fit DataFrame.KMeans.KMeansConfig [DataFrame.Internal.Expression.Expr GHC.Types.Double] DataFrame.KMeans.KMeansModel
+ DataFrame.KMeans: instance DataFrame.Model.Predict DataFrame.KMeans.KMeansModel GHC.Types.Int
+ DataFrame.KMeans: instance GHC.Classes.Eq DataFrame.KMeans.KMeansConfig
+ DataFrame.KMeans: instance GHC.Classes.Eq DataFrame.KMeans.KMeansModel
+ DataFrame.KMeans: instance GHC.Show.Show DataFrame.KMeans.KMeansConfig
+ DataFrame.KMeans: instance GHC.Show.Show DataFrame.KMeans.KMeansModel
+ DataFrame.KMeans: kmeansDistanceExprs :: KMeansModel -> [(Text, Expr Double)]
+ DataFrame.KMeans: kmeansTransform :: KMeansModel -> Transform
+ DataFrame.LinearAlgebra: axpy :: Double -> Vector Double -> Vector Double -> Vector Double
+ DataFrame.LinearAlgebra: dot :: Vector Double -> Vector Double -> Double
+ DataFrame.LinearAlgebra: epsNeighbors :: Double -> Matrix -> Int -> Vector Int
+ DataFrame.LinearAlgebra: gram :: Matrix -> Matrix
+ DataFrame.LinearAlgebra: identityM :: Int -> Matrix
+ DataFrame.LinearAlgebra: logSumExp :: Vector Double -> Double
+ DataFrame.LinearAlgebra: matVec :: Matrix -> Vector Double -> Vector Double
+ DataFrame.LinearAlgebra: nearestCenter :: Vector (Vector Double) -> Vector Double -> (Int, Double)
+ DataFrame.LinearAlgebra: scaleV :: Double -> Vector Double -> Vector Double
+ DataFrame.LinearAlgebra: sqDist :: Vector Double -> Vector Double -> Double
+ DataFrame.LinearAlgebra: tMatVec :: Matrix -> Vector Double -> Vector Double
+ DataFrame.LinearAlgebra: transposeM :: Matrix -> Matrix
+ DataFrame.LinearAlgebra: type Matrix = Vector Vector Double
+ DataFrame.LinearAlgebra.Eigen: jacobiEigenSym :: Matrix -> (Vector Double, Matrix)
+ DataFrame.LinearAlgebra.Eigen: powerIterTop :: Int -> Matrix -> (Double, Vector Double)
+ DataFrame.LinearAlgebra.Solve: backSubst :: Matrix -> Vector Double -> Vector Double
+ DataFrame.LinearAlgebra.Solve: cholesky :: Matrix -> Maybe Matrix
+ DataFrame.LinearAlgebra.Solve: choleskySolve :: Matrix -> Vector Double -> Maybe (Vector Double)
+ DataFrame.LinearAlgebra.Solve: forwardSubst :: Matrix -> Vector Double -> Vector Double
+ DataFrame.LinearAlgebra.Solve: logDetFromChol :: Matrix -> Double
+ DataFrame.LinearAlgebra.Solve: qrLeastSquares :: Matrix -> Vector Double -> Either [Int] (Vector Double)
+ DataFrame.LinearModel.Logistic: LogisticConfig :: SolverConfig -> LogisticConfig
+ DataFrame.LinearModel.Logistic: LogisticModel :: !Vector a -> !Vector LinearModel -> LogisticModel a
+ DataFrame.LinearModel.Logistic: [lgClasses] :: LogisticModel a -> !Vector a
+ DataFrame.LinearModel.Logistic: [lgModels] :: LogisticModel a -> !Vector LinearModel
+ DataFrame.LinearModel.Logistic: [lgSolver] :: LogisticConfig -> SolverConfig
+ DataFrame.LinearModel.Logistic: data LogisticModel a
+ DataFrame.LinearModel.Logistic: defaultLogisticConfig :: LogisticConfig
+ DataFrame.LinearModel.Logistic: instance (DataFrame.Internal.Column.Columnable a, GHC.Classes.Ord a) => DataFrame.Model.Fit DataFrame.LinearModel.Logistic.LogisticConfig (DataFrame.Internal.Expression.Expr a) (DataFrame.LinearModel.Logistic.LogisticModel a)
+ DataFrame.LinearModel.Logistic: instance (DataFrame.Internal.Column.Columnable a, GHC.Classes.Ord a) => DataFrame.Model.Predict (DataFrame.LinearModel.Logistic.LogisticModel a) a
+ DataFrame.LinearModel.Logistic: instance GHC.Classes.Eq DataFrame.LinearModel.Logistic.LogisticConfig
+ DataFrame.LinearModel.Logistic: instance GHC.Classes.Eq a => GHC.Classes.Eq (DataFrame.LinearModel.Logistic.LogisticModel a)
+ DataFrame.LinearModel.Logistic: instance GHC.Show.Show DataFrame.LinearModel.Logistic.LogisticConfig
+ DataFrame.LinearModel.Logistic: instance GHC.Show.Show a => GHC.Show.Show (DataFrame.LinearModel.Logistic.LogisticModel a)
+ DataFrame.LinearModel.Logistic: logisticMarginExprs :: (Columnable a, Ord a) => LogisticModel a -> Map a (Expr Double)
+ DataFrame.LinearModel.Logistic: logisticProbExprs :: (Columnable a, Ord a) => LogisticModel a -> Map a (Expr Double)
+ DataFrame.LinearModel.Logistic: newtype LogisticConfig
+ DataFrame.LinearModel.Regression: ElasticNet :: !Double -> !Double -> Penalty
+ DataFrame.LinearModel.Regression: Lasso :: !Double -> Penalty
+ DataFrame.LinearModel.Regression: LinearConfig :: !Penalty -> !SolverConfig -> LinearConfig
+ DataFrame.LinearModel.Regression: LinearRegressor :: !Vector Double -> !Double -> !Vector Text -> !Penalty -> LinearRegressor
+ DataFrame.LinearModel.Regression: OLS :: Penalty
+ DataFrame.LinearModel.Regression: Ridge :: !Double -> Penalty
+ DataFrame.LinearModel.Regression: [lcPenalty] :: LinearConfig -> !Penalty
+ DataFrame.LinearModel.Regression: [lcSolver] :: LinearConfig -> !SolverConfig
+ DataFrame.LinearModel.Regression: [regCoef] :: LinearRegressor -> !Vector Double
+ DataFrame.LinearModel.Regression: [regFeatureNames] :: LinearRegressor -> !Vector Text
+ DataFrame.LinearModel.Regression: [regIntercept] :: LinearRegressor -> !Double
+ DataFrame.LinearModel.Regression: [regPenalty] :: LinearRegressor -> !Penalty
+ DataFrame.LinearModel.Regression: data LinearConfig
+ DataFrame.LinearModel.Regression: data LinearRegressor
+ DataFrame.LinearModel.Regression: data Penalty
+ DataFrame.LinearModel.Regression: defaultLinearConfig :: LinearConfig
+ DataFrame.LinearModel.Regression: instance DataFrame.Model.Fit DataFrame.LinearModel.Regression.LinearConfig (DataFrame.Internal.Expression.Expr GHC.Types.Double) DataFrame.LinearModel.Regression.LinearRegressor
+ DataFrame.LinearModel.Regression: instance DataFrame.Model.Predict DataFrame.LinearModel.Regression.LinearRegressor GHC.Types.Double
+ DataFrame.LinearModel.Regression: instance GHC.Classes.Eq DataFrame.LinearModel.Regression.LinearConfig
+ DataFrame.LinearModel.Regression: instance GHC.Classes.Eq DataFrame.LinearModel.Regression.LinearRegressor
+ DataFrame.LinearModel.Regression: instance GHC.Classes.Eq DataFrame.LinearModel.Regression.Penalty
+ DataFrame.LinearModel.Regression: instance GHC.Show.Show DataFrame.LinearModel.Regression.LinearConfig
+ DataFrame.LinearModel.Regression: instance GHC.Show.Show DataFrame.LinearModel.Regression.LinearRegressor
+ DataFrame.LinearModel.Regression: instance GHC.Show.Show DataFrame.LinearModel.Regression.Penalty
+ DataFrame.LinearModel.Regression: predictLinear :: LinearRegressor -> Matrix -> Vector Double
+ DataFrame.LinearSolver: columnStats :: Vector (Vector Double) -> (Vector Double, Vector Double, Vector Double)
+ DataFrame.LinearSolver: fitProx :: SmoothLoss -> SolverConfig -> Vector (Vector Double) -> Vector Double -> Vector Text -> LinearModel
+ DataFrame.LinearSolver.Loss: SmoothLoss :: !Text -> (Double -> Double -> Double) -> !Double -> SmoothLoss
+ DataFrame.LinearSolver.Loss: [slCurvBound] :: SmoothLoss -> !Double
+ DataFrame.LinearSolver.Loss: [slGradZ] :: SmoothLoss -> Double -> Double -> Double
+ DataFrame.LinearSolver.Loss: [slName] :: SmoothLoss -> !Text
+ DataFrame.LinearSolver.Loss: data SmoothLoss
+ DataFrame.LinearSolver.Loss: logisticLoss :: SmoothLoss
+ DataFrame.LinearSolver.Loss: sigmoid :: Double -> Double
+ DataFrame.LinearSolver.Loss: sqHingeLoss :: SmoothLoss
+ DataFrame.LinearSolver.Loss: squaredLoss :: SmoothLoss
+ DataFrame.Metrics: Binary :: Double -> Average
+ DataFrame.Metrics: Macro :: Average
+ DataFrame.Metrics: Micro :: Average
+ DataFrame.Metrics: Weighted :: Average
+ DataFrame.Metrics: accuracy :: Metric
+ DataFrame.Metrics: classCounts :: Vector Double -> Vector Double -> [(Double, (Int, Int, Int, Int))]
+ DataFrame.Metrics: columnOf :: DataFrame -> Expr Double -> Vector Double
+ DataFrame.Metrics: data Average
+ DataFrame.Metrics: evaluate :: Metric -> Expr Double -> Expr Double -> DataFrame -> Double
+ DataFrame.Metrics: f1 :: Average -> Vector Double -> Vector Double -> Double
+ DataFrame.Metrics: f1Of :: (Int, Int, Int, Int) -> Double
+ DataFrame.Metrics: instance GHC.Classes.Eq DataFrame.Metrics.Average
+ DataFrame.Metrics: instance GHC.Show.Show DataFrame.Metrics.Average
+ DataFrame.Metrics: logLoss :: Metric
+ DataFrame.Metrics: mae :: Metric
+ DataFrame.Metrics: mse :: Metric
+ DataFrame.Metrics: precOf :: (Int, Int, Int, Int) -> Double
+ DataFrame.Metrics: precision :: Average -> Vector Double -> Vector Double -> Double
+ DataFrame.Metrics: predictColumn :: Text -> Expr Double -> DataFrame -> DataFrame
+ DataFrame.Metrics: r2 :: Metric
+ DataFrame.Metrics: recOf :: (Int, Int, Int, Int) -> Double
+ DataFrame.Metrics: recall :: Average -> Vector Double -> Vector Double -> Double
+ DataFrame.Metrics: rmse :: Metric
+ DataFrame.Metrics: rocAuc :: Vector Double -> Vector Double -> Double
+ DataFrame.Metrics: type Metric = Vector Double -> Vector Double -> Double
+ DataFrame.Metrics.Report: ClassStats :: !Double -> !Double -> !Double -> !Int -> ClassStats
+ DataFrame.Metrics.Report: ClassificationReport :: ![(Double, ClassStats)] -> !Double -> !Double -> !Double -> ClassificationReport
+ DataFrame.Metrics.Report: ConfusionMatrix :: ![Double] -> ![[Int]] -> ConfusionMatrix
+ DataFrame.Metrics.Report: RegressionReport :: !Double -> !Double -> !Double -> !Double -> RegressionReport
+ DataFrame.Metrics.Report: [cmClasses] :: ConfusionMatrix -> ![Double]
+ DataFrame.Metrics.Report: [cmCounts] :: ConfusionMatrix -> ![[Int]]
+ DataFrame.Metrics.Report: [crAccuracy] :: ClassificationReport -> !Double
+ DataFrame.Metrics.Report: [crMacroF1] :: ClassificationReport -> !Double
+ DataFrame.Metrics.Report: [crPerClass] :: ClassificationReport -> ![(Double, ClassStats)]
+ DataFrame.Metrics.Report: [crWeightedF1] :: ClassificationReport -> !Double
+ DataFrame.Metrics.Report: [csF1] :: ClassStats -> !Double
+ DataFrame.Metrics.Report: [csPrecision] :: ClassStats -> !Double
+ DataFrame.Metrics.Report: [csRecall] :: ClassStats -> !Double
+ DataFrame.Metrics.Report: [csSupport] :: ClassStats -> !Int
+ DataFrame.Metrics.Report: [rrMAE] :: RegressionReport -> !Double
+ DataFrame.Metrics.Report: [rrMSE] :: RegressionReport -> !Double
+ DataFrame.Metrics.Report: [rrR2] :: RegressionReport -> !Double
+ DataFrame.Metrics.Report: [rrRMSE] :: RegressionReport -> !Double
+ DataFrame.Metrics.Report: classificationReport :: Vector Double -> Vector Double -> ClassificationReport
+ DataFrame.Metrics.Report: classificationReportExpr :: Expr Double -> Expr Double -> DataFrame -> ClassificationReport
+ DataFrame.Metrics.Report: confusionMatrix :: Vector Double -> Vector Double -> ConfusionMatrix
+ DataFrame.Metrics.Report: confusionMatrixExpr :: Expr Double -> Expr Double -> DataFrame -> ConfusionMatrix
+ DataFrame.Metrics.Report: data ClassStats
+ DataFrame.Metrics.Report: data ClassificationReport
+ DataFrame.Metrics.Report: data ConfusionMatrix
+ DataFrame.Metrics.Report: data RegressionReport
+ DataFrame.Metrics.Report: instance GHC.Classes.Eq DataFrame.Metrics.Report.ClassStats
+ DataFrame.Metrics.Report: instance GHC.Classes.Eq DataFrame.Metrics.Report.ClassificationReport
+ DataFrame.Metrics.Report: instance GHC.Classes.Eq DataFrame.Metrics.Report.ConfusionMatrix
+ DataFrame.Metrics.Report: instance GHC.Classes.Eq DataFrame.Metrics.Report.RegressionReport
+ DataFrame.Metrics.Report: instance GHC.Show.Show DataFrame.Metrics.Report.ClassStats
+ DataFrame.Metrics.Report: instance GHC.Show.Show DataFrame.Metrics.Report.ClassificationReport
+ DataFrame.Metrics.Report: instance GHC.Show.Show DataFrame.Metrics.Report.ConfusionMatrix
+ DataFrame.Metrics.Report: instance GHC.Show.Show DataFrame.Metrics.Report.RegressionReport
+ DataFrame.Metrics.Report: regressionReport :: Vector Double -> Vector Double -> RegressionReport
+ DataFrame.Metrics.Report: regressionReportExpr :: Expr Double -> Expr Double -> DataFrame -> RegressionReport
+ DataFrame.Model: class Fit cfg input model | cfg input -> model
+ DataFrame.Model: class Predict model r | model -> r
+ DataFrame.Model: fit :: Fit cfg input model => cfg -> input -> DataFrame -> model
+ DataFrame.Model: predict :: Predict model r => model -> Expr r
+ DataFrame.Model: selectFeatures :: [Text] -> Expr a -> DataFrame -> DataFrame
+ DataFrame.ModelSelection: GridSearchResult :: !c -> !Double -> ![(c, Double)] -> GridSearchResult c
+ DataFrame.ModelSelection: [gsAll] :: GridSearchResult c -> ![(c, Double)]
+ DataFrame.ModelSelection: [gsBestScore] :: GridSearchResult c -> !Double
+ DataFrame.ModelSelection: [gsBest] :: GridSearchResult c -> !c
+ DataFrame.ModelSelection: crossValScore :: Int -> Int -> (DataFrame -> DataFrame -> Double) -> DataFrame -> [Double]
+ DataFrame.ModelSelection: crossValidate :: Int -> Int -> Metric -> Expr Double -> (DataFrame -> Expr Double) -> DataFrame -> [Double]
+ DataFrame.ModelSelection: data GridSearchResult c
+ DataFrame.ModelSelection: gridSearch :: Int -> Int -> [c] -> (c -> DataFrame -> DataFrame -> Double) -> DataFrame -> GridSearchResult c
+ DataFrame.ModelSelection: instance GHC.Show.Show c => GHC.Show.Show (DataFrame.ModelSelection.GridSearchResult c)
+ DataFrame.ModelSelection: trainTestSplit :: Double -> Int -> DataFrame -> (DataFrame, DataFrame)
+ DataFrame.PCA: NComp :: !Int -> NComponents
+ DataFrame.PCA: PCAConfig :: !NComponents -> !Bool -> PCAConfig
+ DataFrame.PCA: PCAModel :: !Vector (Vector Double) -> !Vector Double -> !Vector Double -> !Vector Double -> !Maybe (Vector Double) -> !Vector Text -> PCAModel
+ DataFrame.PCA: VarianceCovered :: !Double -> NComponents
+ DataFrame.PCA: [pcaComponents] :: PCAModel -> !Vector (Vector Double)
+ DataFrame.PCA: [pcaExplainedVarianceRatio] :: PCAModel -> !Vector Double
+ DataFrame.PCA: [pcaExplainedVariance] :: PCAModel -> !Vector Double
+ DataFrame.PCA: [pcaFeatureNames] :: PCAModel -> !Vector Text
+ DataFrame.PCA: [pcaMean] :: PCAModel -> !Vector Double
+ DataFrame.PCA: [pcaNComponents] :: PCAConfig -> !NComponents
+ DataFrame.PCA: [pcaScale] :: PCAModel -> !Maybe (Vector Double)
+ DataFrame.PCA: [pcaStandardize] :: PCAConfig -> !Bool
+ DataFrame.PCA: data NComponents
+ DataFrame.PCA: data PCAConfig
+ DataFrame.PCA: data PCAModel
+ DataFrame.PCA: defaultPCAConfig :: PCAConfig
+ DataFrame.PCA: instance DataFrame.Model.Fit DataFrame.PCA.PCAConfig [DataFrame.Internal.Expression.Expr GHC.Types.Double] DataFrame.PCA.PCAModel
+ DataFrame.PCA: instance GHC.Classes.Eq DataFrame.PCA.NComponents
+ DataFrame.PCA: instance GHC.Classes.Eq DataFrame.PCA.PCAConfig
+ DataFrame.PCA: instance GHC.Classes.Eq DataFrame.PCA.PCAModel
+ DataFrame.PCA: instance GHC.Show.Show DataFrame.PCA.NComponents
+ DataFrame.PCA: instance GHC.Show.Show DataFrame.PCA.PCAConfig
+ DataFrame.PCA: instance GHC.Show.Show DataFrame.PCA.PCAModel
+ DataFrame.PCA: pcaExprs :: PCAModel -> [(Text, Expr Double)]
+ DataFrame.PCA: pcaTransform :: PCAModel -> Transform
+ DataFrame.PCA.Kernel: KernelPCAConfig :: !Int -> !Maybe Double -> !Int -> !Int -> KernelPCAConfig
+ DataFrame.PCA.Kernel: KernelPCAModel :: !Vector (Vector Double) -> !Vector (Vector Double) -> !Vector Double -> !Vector Double -> !Double -> !Vector Text -> KernelPCAModel
+ DataFrame.PCA.Kernel: [kpcaBetas] :: KernelPCAModel -> !Vector (Vector Double)
+ DataFrame.PCA.Kernel: [kpcaConsts] :: KernelPCAModel -> !Vector Double
+ DataFrame.PCA.Kernel: [kpcaEigenvalues] :: KernelPCAModel -> !Vector Double
+ DataFrame.PCA.Kernel: [kpcaFeatureNames] :: KernelPCAModel -> !Vector Text
+ DataFrame.PCA.Kernel: [kpcaGammaUsed] :: KernelPCAModel -> !Double
+ DataFrame.PCA.Kernel: [kpcaGamma] :: KernelPCAConfig -> !Maybe Double
+ DataFrame.PCA.Kernel: [kpcaLandmarks] :: KernelPCAModel -> !Vector (Vector Double)
+ DataFrame.PCA.Kernel: [kpcaNComponents] :: KernelPCAConfig -> !Int
+ DataFrame.PCA.Kernel: [kpcaNLandmarks] :: KernelPCAConfig -> !Int
+ DataFrame.PCA.Kernel: [kpcaSeed] :: KernelPCAConfig -> !Int
+ DataFrame.PCA.Kernel: data KernelPCAConfig
+ DataFrame.PCA.Kernel: data KernelPCAModel
+ DataFrame.PCA.Kernel: defaultKernelPCAConfig :: KernelPCAConfig
+ DataFrame.PCA.Kernel: instance DataFrame.Model.Fit DataFrame.PCA.Kernel.KernelPCAConfig [DataFrame.Internal.Expression.Expr GHC.Types.Double] DataFrame.PCA.Kernel.KernelPCAModel
+ DataFrame.PCA.Kernel: instance GHC.Classes.Eq DataFrame.PCA.Kernel.KernelPCAConfig
+ DataFrame.PCA.Kernel: instance GHC.Classes.Eq DataFrame.PCA.Kernel.KernelPCAModel
+ DataFrame.PCA.Kernel: instance GHC.Show.Show DataFrame.PCA.Kernel.KernelPCAConfig
+ DataFrame.PCA.Kernel: instance GHC.Show.Show DataFrame.PCA.Kernel.KernelPCAModel
+ DataFrame.PCA.Kernel: kernelPCAExprs :: KernelPCAModel -> [(Text, Expr Double)]
+ DataFrame.PCA.Kernel: kernelPcaTransform :: KernelPCAModel -> Transform
+ DataFrame.Random: gaussianPair :: Gen -> ((Double, Double), Gen)
+ DataFrame.Random: gaussianVector :: Int -> Gen -> (Vector Double, Gen)
+ DataFrame.Random: mkGen :: Int -> Gen
+ DataFrame.Random: nextDouble :: Gen -> (Double, Gen)
+ DataFrame.Random: nextIntR :: (Int, Int) -> Gen -> (Int, Gen)
+ DataFrame.Random: nextWord64 :: Gen -> (Word64, Gen)
+ DataFrame.Random: sampleIndices :: Int -> Int -> Gen -> (Vector Int, Gen)
+ DataFrame.Random: shuffleInts :: Int -> Gen -> (Vector Int, Gen)
+ DataFrame.Random: splitGen :: Gen -> (Gen, Gen)
+ DataFrame.Random: type Gen = StdGen
+ DataFrame.SVM: LinearSVCModel :: !Vector a -> !Vector LinearModel -> LinearSVCModel a
+ DataFrame.SVM: SVCConfig :: !Double -> !Int -> !Double -> SVCConfig
+ DataFrame.SVM: [svcC] :: SVCConfig -> !Double
+ DataFrame.SVM: [svcClasses] :: LinearSVCModel a -> !Vector a
+ DataFrame.SVM: [svcMaxIter] :: SVCConfig -> !Int
+ DataFrame.SVM: [svcModels] :: LinearSVCModel a -> !Vector LinearModel
+ DataFrame.SVM: [svcTol] :: SVCConfig -> !Double
+ DataFrame.SVM: data LinearSVCModel a
+ DataFrame.SVM: data SVCConfig
+ DataFrame.SVM: defaultSVCConfig :: SVCConfig
+ DataFrame.SVM: instance (DataFrame.Internal.Column.Columnable a, GHC.Classes.Ord a) => DataFrame.Model.Fit DataFrame.SVM.SVCConfig (DataFrame.Internal.Expression.Expr a) (DataFrame.SVM.LinearSVCModel a)
+ DataFrame.SVM: instance (DataFrame.Internal.Column.Columnable a, GHC.Classes.Ord a) => DataFrame.Model.Predict (DataFrame.SVM.LinearSVCModel a) a
+ DataFrame.SVM: instance GHC.Classes.Eq DataFrame.SVM.SVCConfig
+ DataFrame.SVM: instance GHC.Classes.Eq a => GHC.Classes.Eq (DataFrame.SVM.LinearSVCModel a)
+ DataFrame.SVM: instance GHC.Show.Show DataFrame.SVM.SVCConfig
+ DataFrame.SVM: instance GHC.Show.Show a => GHC.Show.Show (DataFrame.SVM.LinearSVCModel a)
+ DataFrame.SVM: svcMarginExprs :: (Columnable a, Ord a) => LinearSVCModel a -> Map a (Expr Double)
+ DataFrame.SVM.RFF: RFFConfig :: !Int -> !Double -> !Double -> !Int -> !Double -> !Int -> RFFConfig
+ DataFrame.SVM.RFF: RFFSVMModel :: !Vector (Vector Double) -> !Vector Double -> !Vector Double -> !Double -> !Double -> !a -> !a -> !Vector Text -> RFFSVMModel a
+ DataFrame.SVM.RFF: [rffB] :: RFFSVMModel a -> !Vector Double
+ DataFrame.SVM.RFF: [rffC] :: RFFConfig -> !Double
+ DataFrame.SVM.RFF: [rffCoef] :: RFFSVMModel a -> !Vector Double
+ DataFrame.SVM.RFF: [rffD] :: RFFConfig -> !Int
+ DataFrame.SVM.RFF: [rffFeatureNames] :: RFFSVMModel a -> !Vector Text
+ DataFrame.SVM.RFF: [rffGamma] :: RFFConfig -> !Double
+ DataFrame.SVM.RFF: [rffIntercept] :: RFFSVMModel a -> !Double
+ DataFrame.SVM.RFF: [rffMaxIter] :: RFFConfig -> !Int
+ DataFrame.SVM.RFF: [rffNegClass] :: RFFSVMModel a -> !a
+ DataFrame.SVM.RFF: [rffPosClass] :: RFFSVMModel a -> !a
+ DataFrame.SVM.RFF: [rffScale] :: RFFSVMModel a -> !Double
+ DataFrame.SVM.RFF: [rffSeed] :: RFFConfig -> !Int
+ DataFrame.SVM.RFF: [rffTol] :: RFFConfig -> !Double
+ DataFrame.SVM.RFF: [rffW] :: RFFSVMModel a -> !Vector (Vector Double)
+ DataFrame.SVM.RFF: data RFFConfig
+ DataFrame.SVM.RFF: data RFFSVMModel a
+ DataFrame.SVM.RFF: defaultRFFConfig :: RFFConfig
+ DataFrame.SVM.RFF: instance (DataFrame.Internal.Column.Columnable a, GHC.Classes.Ord a) => DataFrame.Model.Fit DataFrame.SVM.RFF.RFFConfig (DataFrame.Internal.Expression.Expr a) (DataFrame.SVM.RFF.RFFSVMModel a)
+ DataFrame.SVM.RFF: instance DataFrame.Internal.Column.Columnable a => DataFrame.Model.Predict (DataFrame.SVM.RFF.RFFSVMModel a) a
+ DataFrame.SVM.RFF: instance GHC.Classes.Eq DataFrame.SVM.RFF.RFFConfig
+ DataFrame.SVM.RFF: instance GHC.Show.Show DataFrame.SVM.RFF.RFFConfig
+ DataFrame.SVM.RFF: instance GHC.Show.Show a => GHC.Show.Show (DataFrame.SVM.RFF.RFFSVMModel a)
+ DataFrame.SymbolicRegression: SCos :: UnOp
+ DataFrame.SymbolicRegression: SExp :: UnOp
+ DataFrame.SymbolicRegression: SLog :: UnOp
+ DataFrame.SymbolicRegression: SNeg :: UnOp
+ DataFrame.SymbolicRegression: SRConfig :: !Int -> !Int -> !Int -> !Int -> !Int -> !Double -> !Double -> !Double -> !Double -> ![UnOp] -> SRConfig
+ DataFrame.SymbolicRegression: SRModel :: !Expr Double -> !Double -> ![(Int, Double, Expr Double)] -> !Int -> SRModel
+ DataFrame.SymbolicRegression: SSin :: UnOp
+ DataFrame.SymbolicRegression: SSqrt :: UnOp
+ DataFrame.SymbolicRegression: [srBestMSE] :: SRModel -> !Double
+ DataFrame.SymbolicRegression: [srBest] :: SRModel -> !Expr Double
+ DataFrame.SymbolicRegression: [srCrossoverP] :: SRConfig -> !Double
+ DataFrame.SymbolicRegression: [srGenerationsRun] :: SRModel -> !Int
+ DataFrame.SymbolicRegression: [srGenerations] :: SRConfig -> !Int
+ DataFrame.SymbolicRegression: [srMaxSize] :: SRConfig -> !Int
+ DataFrame.SymbolicRegression: [srMutationP] :: SRConfig -> !Double
+ DataFrame.SymbolicRegression: [srOptimizeP] :: SRConfig -> !Double
+ DataFrame.SymbolicRegression: [srPareto] :: SRModel -> ![(Int, Double, Expr Double)]
+ DataFrame.SymbolicRegression: [srParsimony] :: SRConfig -> !Double
+ DataFrame.SymbolicRegression: [srPopSize] :: SRConfig -> !Int
+ DataFrame.SymbolicRegression: [srSeed] :: SRConfig -> !Int
+ DataFrame.SymbolicRegression: [srTournament] :: SRConfig -> !Int
+ DataFrame.SymbolicRegression: [srUnaryOps] :: SRConfig -> ![UnOp]
+ DataFrame.SymbolicRegression: data SRConfig
+ DataFrame.SymbolicRegression: data SRModel
+ DataFrame.SymbolicRegression: data UnOp
+ DataFrame.SymbolicRegression: defaultSRConfig :: SRConfig
+ DataFrame.SymbolicRegression: instance DataFrame.Model.Fit DataFrame.SymbolicRegression.SRConfig (DataFrame.Internal.Expression.Expr GHC.Types.Double) DataFrame.SymbolicRegression.SRModel
+ DataFrame.SymbolicRegression: instance DataFrame.Model.Predict DataFrame.SymbolicRegression.SRModel GHC.Types.Double
+ DataFrame.SymbolicRegression: instance GHC.Classes.Eq DataFrame.SymbolicRegression.SRConfig
+ DataFrame.SymbolicRegression: instance GHC.Show.Show DataFrame.SymbolicRegression.SRConfig
+ DataFrame.SymbolicRegression.Expr: SAdd :: BinOp
+ DataFrame.SymbolicRegression.Expr: SBin :: !BinOp -> SRExpr -> SRExpr -> SRExpr
+ DataFrame.SymbolicRegression.Expr: SConst :: !Double -> SRExpr
+ DataFrame.SymbolicRegression.Expr: SCos :: UnOp
+ DataFrame.SymbolicRegression.Expr: SDiv :: BinOp
+ DataFrame.SymbolicRegression.Expr: SExp :: UnOp
+ DataFrame.SymbolicRegression.Expr: SLog :: UnOp
+ DataFrame.SymbolicRegression.Expr: SMul :: BinOp
+ DataFrame.SymbolicRegression.Expr: SNeg :: UnOp
+ DataFrame.SymbolicRegression.Expr: SSin :: UnOp
+ DataFrame.SymbolicRegression.Expr: SSqrt :: UnOp
+ DataFrame.SymbolicRegression.Expr: SSub :: BinOp
+ DataFrame.SymbolicRegression.Expr: SUn :: !UnOp -> SRExpr -> SRExpr
+ DataFrame.SymbolicRegression.Expr: SVar :: !Int -> SRExpr
+ DataFrame.SymbolicRegression.Expr: allBinOps :: [BinOp]
+ DataFrame.SymbolicRegression.Expr: allUnOps :: [UnOp]
+ DataFrame.SymbolicRegression.Expr: constants :: SRExpr -> [Double]
+ DataFrame.SymbolicRegression.Expr: data BinOp
+ DataFrame.SymbolicRegression.Expr: data SRExpr
+ DataFrame.SymbolicRegression.Expr: data UnOp
+ DataFrame.SymbolicRegression.Expr: evalSR :: Vector (Vector Double) -> Int -> SRExpr -> Vector Double
+ DataFrame.SymbolicRegression.Expr: instance GHC.Classes.Eq DataFrame.SymbolicRegression.Expr.BinOp
+ DataFrame.SymbolicRegression.Expr: instance GHC.Classes.Eq DataFrame.SymbolicRegression.Expr.SRExpr
+ DataFrame.SymbolicRegression.Expr: instance GHC.Classes.Eq DataFrame.SymbolicRegression.Expr.UnOp
+ DataFrame.SymbolicRegression.Expr: instance GHC.Classes.Ord DataFrame.SymbolicRegression.Expr.BinOp
+ DataFrame.SymbolicRegression.Expr: instance GHC.Classes.Ord DataFrame.SymbolicRegression.Expr.SRExpr
+ DataFrame.SymbolicRegression.Expr: instance GHC.Classes.Ord DataFrame.SymbolicRegression.Expr.UnOp
+ DataFrame.SymbolicRegression.Expr: instance GHC.Enum.Bounded DataFrame.SymbolicRegression.Expr.BinOp
+ DataFrame.SymbolicRegression.Expr: instance GHC.Enum.Bounded DataFrame.SymbolicRegression.Expr.UnOp
+ DataFrame.SymbolicRegression.Expr: instance GHC.Enum.Enum DataFrame.SymbolicRegression.Expr.BinOp
+ DataFrame.SymbolicRegression.Expr: instance GHC.Enum.Enum DataFrame.SymbolicRegression.Expr.UnOp
+ DataFrame.SymbolicRegression.Expr: instance GHC.Show.Show DataFrame.SymbolicRegression.Expr.BinOp
+ DataFrame.SymbolicRegression.Expr: instance GHC.Show.Show DataFrame.SymbolicRegression.Expr.SRExpr
+ DataFrame.SymbolicRegression.Expr: instance GHC.Show.Show DataFrame.SymbolicRegression.Expr.UnOp
+ DataFrame.SymbolicRegression.Expr: setConstants :: [Double] -> SRExpr -> SRExpr
+ DataFrame.SymbolicRegression.Expr: srSize :: SRExpr -> Int
+ DataFrame.SymbolicRegression.Expr: toDataFrameExpr :: Vector Text -> SRExpr -> Expr Double
+ DataFrame.SymbolicRegression.GP: GPParams :: !Vector (Vector Double) -> !Int -> !Vector Double -> !Int -> ![UnOp] -> !Int -> !Int -> !Int -> !Int -> !Double -> !Double -> !Double -> !Double -> GPParams
+ DataFrame.SymbolicRegression.GP: [gpCrossoverP] :: GPParams -> !Double
+ DataFrame.SymbolicRegression.GP: [gpFeats] :: GPParams -> !Vector (Vector Double)
+ DataFrame.SymbolicRegression.GP: [gpGenerations] :: GPParams -> !Int
+ DataFrame.SymbolicRegression.GP: [gpMaxSize] :: GPParams -> !Int
+ DataFrame.SymbolicRegression.GP: [gpMutationP] :: GPParams -> !Double
+ DataFrame.SymbolicRegression.GP: [gpNVars] :: GPParams -> !Int
+ DataFrame.SymbolicRegression.GP: [gpN] :: GPParams -> !Int
+ DataFrame.SymbolicRegression.GP: [gpOptimizeP] :: GPParams -> !Double
+ DataFrame.SymbolicRegression.GP: [gpParsimony] :: GPParams -> !Double
+ DataFrame.SymbolicRegression.GP: [gpPopSize] :: GPParams -> !Int
+ DataFrame.SymbolicRegression.GP: [gpTarget] :: GPParams -> !Vector Double
+ DataFrame.SymbolicRegression.GP: [gpTournament] :: GPParams -> !Int
+ DataFrame.SymbolicRegression.GP: [gpUnOps] :: GPParams -> ![UnOp]
+ DataFrame.SymbolicRegression.GP: data GPParams
+ DataFrame.SymbolicRegression.GP: runGP :: GPParams -> Gen -> (SRExpr, [(Int, Double, SRExpr)], Int)
+ DataFrame.SymbolicRegression.Optimize: meanSquaredError :: Vector (Vector Double) -> Int -> Vector Double -> SRExpr -> Double
+ DataFrame.SymbolicRegression.Optimize: optimizeConstants :: Vector (Vector Double) -> Int -> Vector Double -> Int -> SRExpr -> SRExpr
+ DataFrame.SymbolicRegression.Simplify: simplify :: SRExpr -> SRExpr
+ DataFrame.Synthesis: SynthesisConfig :: !Int -> !Int -> !LossFunction -> !Int -> SynthesisConfig
+ DataFrame.Synthesis: SynthesizedFeature :: !Expr Double -> !Double -> ![(Expr Double, Double)] -> SynthesizedFeature
+ DataFrame.Synthesis: [sfExpr] :: SynthesizedFeature -> !Expr Double
+ DataFrame.Synthesis: [sfFeatures] :: SynthesizedFeature -> ![(Expr Double, Double)]
+ DataFrame.Synthesis: [sfScore] :: SynthesizedFeature -> !Double
+ DataFrame.Synthesis: [synBankCap] :: SynthesisConfig -> !Int
+ DataFrame.Synthesis: [synLoss] :: SynthesisConfig -> !LossFunction
+ DataFrame.Synthesis: [synMaxSize] :: SynthesisConfig -> !Int
+ DataFrame.Synthesis: [synTopK] :: SynthesisConfig -> !Int
+ DataFrame.Synthesis: data SynthesisConfig
+ DataFrame.Synthesis: data SynthesizedFeature
+ DataFrame.Synthesis: defaultSynthesisConfig :: SynthesisConfig
+ DataFrame.Synthesis: instance DataFrame.Model.Fit DataFrame.Synthesis.SynthesisConfig (DataFrame.Internal.Expression.Expr GHC.Types.Double) DataFrame.Synthesis.SynthesizedFeature
+ DataFrame.Synthesis: instance DataFrame.Model.Predict DataFrame.Synthesis.SynthesizedFeature GHC.Types.Double
+ DataFrame.Synthesis: instance GHC.Classes.Eq DataFrame.Synthesis.LossFunction
+ DataFrame.Synthesis: instance GHC.Classes.Eq DataFrame.Synthesis.SynthesisConfig
+ DataFrame.Synthesis: instance GHC.Show.Show DataFrame.Synthesis.LossFunction
+ DataFrame.Synthesis: instance GHC.Show.Show DataFrame.Synthesis.SynthesisConfig
+ DataFrame.Synthesis: synthesizeFeatures :: SynthesisConfig -> Expr Double -> DataFrame -> SynthesizedFeature
+ DataFrame.Transform: ScalerModel :: !Vector Text -> !Vector Double -> !Vector Double -> ScalerModel
+ DataFrame.Transform: Transform :: [NamedExpr] -> Transform
+ DataFrame.Transform: [smColumns] :: ScalerModel -> !Vector Text
+ DataFrame.Transform: [smMeans] :: ScalerModel -> !Vector Double
+ DataFrame.Transform: [smStds] :: ScalerModel -> !Vector Double
+ DataFrame.Transform: [transformOutputs] :: Transform -> [NamedExpr]
+ DataFrame.Transform: applyTransform :: Transform -> DataFrame -> DataFrame
+ DataFrame.Transform: compileThrough :: Columnable a => Transform -> Expr a -> Expr a
+ DataFrame.Transform: data ScalerModel
+ DataFrame.Transform: instance GHC.Base.Monoid DataFrame.Transform.Transform
+ DataFrame.Transform: instance GHC.Base.Semigroup DataFrame.Transform.Transform
+ DataFrame.Transform: instance GHC.Classes.Eq DataFrame.Transform.ScalerModel
+ DataFrame.Transform: instance GHC.Show.Show DataFrame.Transform.ScalerModel
+ DataFrame.Transform: newtype Transform
+ DataFrame.Transform: scalerTransform :: ScalerModel -> Transform
+ DataFrame.Transform: standardScaler :: [Text] -> DataFrame -> ScalerModel
Files
- README.md +473/−0
- dataframe-learn.cabal +43/−8
- src/DataFrame/Boosting.hs +10/−0
- src/DataFrame/Boosting/AdaBoost.hs +211/−0
- src/DataFrame/Boosting/GBM.hs +185/−0
- src/DataFrame/DBSCAN.hs +119/−0
- src/DataFrame/DecisionTree/Cart.hs +90/−29
- src/DataFrame/DecisionTree/Categorical.hs +154/−54
- src/DataFrame/DecisionTree/CondVec.hs +54/−29
- src/DataFrame/DecisionTree/Fit.hs +60/−21
- src/DataFrame/DecisionTree/Linear.hs +57/−29
- src/DataFrame/DecisionTree/Model.hs +101/−0
- src/DataFrame/DecisionTree/Numeric.hs +59/−23
- src/DataFrame/DecisionTree/Pool.hs +82/−32
- src/DataFrame/DecisionTree/Predict.hs +85/−30
- src/DataFrame/DecisionTree/Prune.hs +16/−9
- src/DataFrame/DecisionTree/Regression.hs +144/−0
- src/DataFrame/DecisionTree/Tao.hs +136/−38
- src/DataFrame/DecisionTree/Types.hs +15/−10
- src/DataFrame/Featurize/Internal.hs +132/−0
- src/DataFrame/GMM.hs +310/−0
- src/DataFrame/KMeans.hs +156/−0
- src/DataFrame/LinearAlgebra.hs +122/−0
- src/DataFrame/LinearAlgebra/Eigen.hs +121/−0
- src/DataFrame/LinearAlgebra/Solve.hs +213/−0
- src/DataFrame/LinearModel.hs +11/−0
- src/DataFrame/LinearModel/Logistic.hs +105/−0
- src/DataFrame/LinearModel/Regression.hs +128/−0
- src/DataFrame/LinearSolver.hs +73/−31
- src/DataFrame/LinearSolver/Loss.hs +47/−0
- src/DataFrame/Metrics.hs +237/−0
- src/DataFrame/Metrics/Report.hs +170/−0
- src/DataFrame/Model.hs +69/−0
- src/DataFrame/ModelSelection.hs +91/−0
- src/DataFrame/PCA.hs +131/−0
- src/DataFrame/PCA/Kernel.hs +136/−0
- src/DataFrame/Random.hs +121/−0
- src/DataFrame/SVM.hs +101/−0
- src/DataFrame/SVM/RFF.hs +146/−0
- src/DataFrame/SymbolicRegression.hs +120/−0
- src/DataFrame/SymbolicRegression/Expr.hs +122/−0
- src/DataFrame/SymbolicRegression/GP.hs +207/−0
- src/DataFrame/SymbolicRegression/Optimize.hs +68/−0
- src/DataFrame/SymbolicRegression/Simplify.hs +65/−0
- src/DataFrame/Synthesis.hs +336/−448
- src/DataFrame/Transform.hs +103/−0
+ README.md view
@@ -0,0 +1,473 @@+<!--+ This README is a runnable scripths (https://github.com/DataHaskell/scripths)+ notebook. Every ```haskell block runs top-to-bottom in one shared session and+ scripths inserts each block's output beneath it as a blockquote. The+ `-- cabal: packages:` directive builds against the local working tree.+ Regenerate (from the repo root) with:++ scripths dataframe-learn/README.md -o dataframe-learn/README.md+-->++# dataframe-learn++Machine learning for [`dataframe`](https://hackage.haskell.org/package/dataframe)+where **a fitted model is a dataframe expression**. You `fit` a model and+`predict` hands you back an `Expr` over your columns — pretty-print it to read+the formula, apply it with `derive` to score a frame, fold preprocessing into it+with `compileThrough`. The model *is* the prediction, not an opaque blob, and the+scikit-learn-style record (coefficients, centroids, components, support) is right+there too for inspection. Because every prediction is the same kind of `Expr`,+preprocessing, prediction, and deployment all compose the same way — the+[design notes](#design-notes-the-categorical-account) at the end explain why.++## A linear model is a formula++`fit` returns a record (with `regCoef`/`regIntercept` for inspection) and+`predict` compiles it to an `Expr Double` you can read. The `D` import (the+public `DataFrame` umbrella, which also gives `D.col` for the expression DSL)+carries through the rest of the notebook; each later section adds the one model+module it needs:++```haskell+-- cabal: packages: .., ., ../dataframe-core, ../dataframe-operations, ../dataframe-parsing+-- cabal: build-depends: dataframe, dataframe-learn, text+-- cabal: default-extensions: OverloadedStrings, TypeApplications+-- cabal: ghc-options: -w+import qualified DataFrame as D+import DataFrame.LinearModel+import DataFrame.Model (fit, predict)++sales = D.fromNamedColumns+ [ ("x", D.fromList ([1, 2, 3, 4, 5, 6] :: [Double]))+ , ("y", D.fromList ([2 * x + 1 | x <- [1, 2, 3, 4, 5, 6]] :: [Double]))+ ]++model = fit defaultLinearConfig (D.col @Double "y") sales+putStrLn (D.prettyPrint (predict model))+```++> <!-- scripths:mime text/plain -->+> 2.0 * x + 0.9999999999999989++## A decision tree is a readable expression++The tree compiles to nested `if/then/else` over your columns — no special+viewer, it is just an expression:++```haskell+import DataFrame.DecisionTree (defaultTreeConfig)+import DataFrame.DecisionTree.Model ()++flowers = D.fromNamedColumns+ [ ("petal_length", D.fromList ([1.4, 1.3, 1.5, 1.4, 4.5, 4.7, 4.6, 4.4, 5.5, 5.8, 5.6, 5.7] :: [Double]))+ , ("petal_width", D.fromList ([0.2, 0.2, 0.1, 0.3, 1.5, 1.4, 1.6, 1.3, 2.0, 2.1, 1.9, 2.2] :: [Double]))+ , ("species", D.fromList ([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2] :: [Double]))+ ]++tree = fit defaultTreeConfig (D.col @Double "species") flowers+putStrLn (D.prettyPrint (predict tree))+```++> <!-- scripths:mime text/plain -->+> if petal_length .<=. 2.95+> then 0.0+> else if petal_length .<=. 5.1+> then 1.0+> else 2.0++## Symbolic regression discovers a formula++Genetic programming searches for an expression that fits the data, and returns+it as a dataframe `Expr` plus the accuracy/complexity Pareto front:++```haskell+import DataFrame.SymbolicRegression++curve = D.fromNamedColumns+ [ ("x", D.fromList xs)+ , ("y", D.fromList [x * x + x | x <- xs])+ ]+ where xs = [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6] :: [Double]++sr = fit+ defaultSRConfig { srSeed = 3, srGenerations = 50, srPopSize = 300, srUnaryOps = [] }+ (D.col @Double "y") curve+putStrLn (D.prettyPrint (srBest sr) ++ " (mse " ++ show (srBestMSE sr) ++ ")")+```++> <!-- scripths:mime text/plain -->+> x + x * x (mse 0.0)++## When the formula is bigger than a glance++Not every model is a one-liner. A linear model, a small tree, or a symbolic+expression you can *read*; a 40-tree gradient booster you cannot — its `predict`+is an exact sum of forty trees. Counting the characters in each printed formula+shows the gap:++```haskell+import DataFrame.Boosting++gbm = fit defaultGBConfig { gbNEstimators = 40, gbMaxDepth = 2 } (D.col @Double "y") sales+putStr (unlines+ [ "linear prediction: " ++ show (length (D.prettyPrint (predict model))) ++ " characters"+ , "GBM(40 trees): " ++ show (length (D.prettyPrint (predict gbm))) ++ " characters" ])+```++> <!-- scripths:mime text/plain -->+> linear prediction: 28 characters+> GBM(40 trees): 7151 characters++Even when it is too big to eyeball, the expression is still the whole story:+a self-contained, dependency-free artifact that scores a frame with `derive` —+no pickled blob, no runtime to ship. For the big ensembles the *interpretability*+comes from `gbFeatureImportances` and pretty-printing individual trees, not from+reading the summed formula.++## Deploy: applying an expression to a frame++Because the model is an `Expr`, deploying it is just `derive` — you add the+prediction as a new column with the ordinary dataframe API:++```haskell+D.columnNames (D.derive "prediction" (predict model) sales)+```++> <!-- scripths:mime text/plain -->+> ["x","y","prediction"]++## A model and its preprocessing compose by substitution++Preprocessing is an expression too, so a model trained in a transformed space and+the transform that produced it *compose* — and composition of expressions is+substitution of one into the other. `compileThrough` performs that composition,+folding a fitted transform into a prediction so the result is a single formula+over the raw inputs. Here we standardize `x`, fit in the scaled space, then fold+the scaler back in to recover a raw-column model:++```haskell+import DataFrame.Transform+import DataFrame.Metrics++scaler = standardScaler ["x"] sales+scaledSales = applyTransform (scalerTransform scaler) sales+scaledModel = fit defaultLinearConfig (D.col @Double "y") scaledSales++deployed = compileThrough (scalerTransform scaler) (predict scaledModel)+putStr (unlines+ [ "trained in scaled space: " ++ D.prettyPrint (predict scaledModel)+ , "folded to raw columns: " ++ D.prettyPrint deployed ])+```++> <!-- scripths:mime text/plain -->+> trained in scaled space: 3.4156502553198655 * x + 8.0+> folded to raw columns: 3.4156502553198655 * (x - 3.5) / 1.707825127659933 + 8.0++The folded expression is a function of the raw `x` alone, so it scores the+original frame with no preprocessing step at inference time — and by the+substitution lemma it computes the same result (up to floating point) as+transforming the frame and then predicting:++```haskell+evaluate rmse deployed (D.col @Double "y") sales+```++> <!-- scripths:mime text/plain -->+> 3.6259732146947156e-16++## A realistic run: pick features, split, evaluate held-out, tune++Real frames are noisy and carry columns you must not train on. Here is a noisy+linear signal with a spurious `id` column:++```haskell+realistic = D.fromNamedColumns+ [ ("id", D.fromList [fromIntegral ((i * 7919) `mod` 97) | i <- [1 .. 40 :: Int]])+ , ("x", D.fromList xs)+ , ("y", D.fromList [2 * x + 1 + noise i | (i, x) <- zip [0 :: Int ..] xs])+ ]+ where+ xs = map fromIntegral [1 .. 40 :: Int] :: [Double]+ noise i = fromIntegral ((i * 2654435761 + 12345) `mod` 1000) / 100 - 5+```++> <!-- scripths:mime text/plain -->++**Feature selection.** Supervised `fit` uses *every* non-target column as a+feature, so a naive fit drags `id` into the model. `selectFeatures` restricts to+the columns you mean (mirroring the explicit feature list the unsupervised+fitters take), which is the difference between a leaky model and a clean one:++```haskell+import DataFrame.Model (selectFeatures)++naive = fit defaultLinearConfig (D.col @Double "y") realistic+guarded = fit defaultLinearConfig (D.col @Double "y")+ (selectFeatures ["x"] (D.col @Double "y") realistic)+putStr (unlines+ [ "all columns: " ++ D.prettyPrint (predict naive)+ , "selectFeatures [\"x\"]: " ++ D.prettyPrint (predict guarded) ])+```++> <!-- scripths:mime text/plain -->+> all columns: -7.746701620642152e-3 * id + 1.9914915483217268 * x + 1.6474622984919354+> selectFeatures ["x"]: 1.9918011257035637 * x + 1.2630769230769452++**Hold-out evaluation.** `trainTestSplit` (seeded, deterministic) keeps the score+honest — evaluate on rows the model never saw, and the metrics are realistic, not+the `1e-15` of an in-sample toy:++```haskell+import DataFrame.ModelSelection++clean = selectFeatures ["x"] (D.col @Double "y") realistic+(train, test) = trainTestSplit 0.75 7 clean+heldModel = fit defaultLinearConfig (D.col @Double "y") train+putStr (unlines+ [ "held-out R^2: " ++ show (evaluate r2 (predict heldModel) (D.col @Double "y") test)+ , "held-out RMSE: " ++ show (evaluate rmse (predict heldModel) (D.col @Double "y") test) ])+```++> <!-- scripths:mime text/plain -->+> held-out R^2: 0.9671190074242891+> held-out RMSE: 3.56674709632647++**Cross-validation.** `crossValidate` is scikit-learn's `cross_val_score`: it+fits on each training fold and scores the prediction expression on the held-out+fold. You pass a `train -> Expr` closure, so it works with any model:++```haskell+cv = crossValidate 5 0 rmse (D.col @Double "y")+ (\tr -> predict (fit defaultLinearConfig (D.col @Double "y") tr))+ clean+putStrLn ("5-fold RMSE: " ++ show (sum cv / fromIntegral (length cv)))+```++> <!-- scripths:mime text/plain -->+> 5-fold RMSE: 3.0325616706245713++`gridSearch` tunes hyperparameters the same way, over a list of configs.++## Reports without hand-rolling metrics++Metrics are plain functions (`rmse`, `mse`, `r2`, `accuracy`, multiclass+`precision`/`recall`/`f1`), and `classificationReport` bundles the common numbers+with a scikit-learn-style layout (per-class precision/recall/F1/support plus+macro/weighted averages):++```haskell+import DataFrame.Metrics.Report++clf = fit defaultLogisticConfig (D.col @Double "species") flowers+putStr (show (classificationReportExpr (predict clf) (D.col @Double "species") flowers))+```++> <!-- scripths:mime text/plain -->+> class precision recall f1 support +> 0.0 1.0 1.0 1.0 4 +> 1.0 1.0 1.0 1.0 4 +> 2.0 1.0 1.0 1.0 4 +> +> accuracy = 1.0+> macro f1 = 1.0+> weighted f1 = 1.0++## Pipelines compose as a monoid++A fitted preprocessing step is a `Transform`, and transforms compose with `<>`.+`applyTransform` runs the whole pipeline; `compileThrough` folds it into a single+expression over the raw columns for export:++```haskell+import DataFrame.PCA++features = ["petal_length", "petal_width"]+scalerF = standardScaler features flowers+pca = fit (PCAConfig (NComp 2) True) (map (D.col @Double) features) flowers+pipeline = scalerTransform scalerF <> pcaTransform pca++D.columnNames (applyTransform pipeline flowers)+```++> <!-- scripths:mime text/plain -->+> ["petal_length","petal_width","species","pc1","pc2"]++## Unsupervised models are inspectable too++k-means returns `cluster_centers_`-style centroids, and per-cluster distance /+assignment expressions:++```haskell+import DataFrame.KMeans++km = fit defaultKMeansConfig { kmK = 3, kmSeed = 1 } (map (D.col @Double) features) flowers+kmCenters km+```++> <!-- scripths:mime text/plain -->+> [[1.4,0.2],[5.65,2.05],[4.55,1.4500000000000002]]++## Synthesize the feature you would have hand-engineered++`DataFrame.Synthesis` is automated feature engineering: a bottom-up enumerative+search (with observational-equivalence pruning) for a small, interpretable+expression over your columns that tracks the target. Here `y` is the interaction+`a * b`, which a linear model on the raw columns cannot capture; synthesis+discovers the term, and feeding it back as a column lifts the fit from mediocre+to exact — still a formula you can read:++```haskell+import DataFrame.Synthesis++interactions = D.fromNamedColumns+ [ ("a", D.fromList as)+ , ("b", D.fromList bs)+ , ("y", D.fromList (zipWith (*) as bs))+ ]+ where+ as = [-1, -1, 1, 1, -2, 2, -2, 2] :: [Double]+ bs = [-1, 1, -1, 1, -2, -2, 2, 2] :: [Double]++rawModel = fit defaultLinearConfig (D.col @Double "y") interactions+feature = fit defaultSynthesisConfig (D.col @Double "y") interactions+withFeat = D.derive "synth" (predict feature) interactions+fitModel =+ fit defaultLinearConfig (D.col @Double "y")+ (selectFeatures ["synth"] (D.col @Double "y") withFeat)++putStr (unlines+ [ "discovered feature: " ++ D.prettyPrint (predict feature)+ , "raw linear R^2: " ++ show (evaluate r2 (predict rawModel) (D.col @Double "y") interactions)+ , "with synth feature: " ++ show (evaluate r2 (predict fitModel) (D.col @Double "y") withFeat)+ ])+```++> <!-- scripths:mime text/plain -->+> discovered feature: a * b+> raw linear R^2: 0.0+> with synth feature: 1.0++`predict feature` is the single best expression; `sfFeatures feature` is the whole+ranked, deduplicated bank, ready to `derive` as a batch of candidate columns.++## What's in the box++| Task | Models |+|------|--------|+| Regression | OLS, ridge, lasso, elastic net, regression trees, gradient boosting, symbolic regression |+| Classification | logistic regression, linear SVC, RFF kernel SVM, decision trees, gradient boosting, AdaBoost |+| Dimensionality reduction | PCA, Nyström kernel PCA |+| Clustering | k-means, Gaussian mixtures, DBSCAN |+| Feature engineering | `DataFrame.Synthesis` (enumerative feature synthesis), symbolic regression |+| Evaluation | `DataFrame.Metrics` (metrics + `evaluate`), `DataFrame.Metrics.Report` (reports) |+| Pipelines & tuning | `DataFrame.Transform` (composable transforms), `DataFrame.ModelSelection` (`trainTestSplit`, `crossValidate`, `gridSearch`) |++Every model is a `Fit` instance, so there is one verb to train — `fit cfg input+df` — and every model with an honest out-of-sample prediction is a `Predict`+instance, so one verb to compile it — `predict model`. Auxiliary outputs+(`gbProbaExpr`, `logisticProbExprs`, `kmeansDistanceExprs`, `pcaTransform`, …)+keep descriptive names; transductive models like DBSCAN deliberately have no+`Predict` instance. Fits that use randomness take a `seed` in their config, so+results are reproducible across Linux, macOS, and Windows. Pure Haskell — the+only extra dependency beyond the dataframe packages is `random`.++## Design notes: the categorical account++The two verbs live in `DataFrame.Model`:++```+class Fit cfg input model | cfg input -> model where+ fit :: cfg -> input -> DataFrame -> model++class Predict model r | model -> r where+ predict :: model -> Expr r+```++They are small on purpose, because the structure they hang on lives in the+expression language, not in the classes. The framing borrows from+[*Seven Sketches in Compositionality*](https://arxiv.org/abs/1803.05316)+(Fong & Spivak) and the Para/Lens account of learners+([Fong, Johnson & Spivak, *Lenses and Learners*](https://arxiv.org/abs/1903.03671);+[Cruttwell et al., *Categorical Foundations of Gradient-Based Learning*](https://arxiv.org/abs/2103.01931)).+What follows is deliberately careful about what is load-bearing and what is only+analogy.++**The row-wise fragment is a category.** Restrict to the row-wise expression+constructors — `Col`, `Lit`, `Unary`, `Binary`, `If`. Take typed column contexts+as objects and, as an arrow `Γ → Δ`, a `Δ`-tuple of such expressions over `Γ`.+Composition is simultaneous substitution (`substituteColumns`, added to+`dataframe-core` for exactly this) and the identities are the column projections+(`Col`). This is the category of contexts (the Lawvere theory) of the column+signature. The restriction is load-bearing for *both* laws, not just composition:+`Agg` and `Over` are column-level/relational, not row-wise maps, and the raw-text+column reference inside `CastWith` is opaque to substitution — so identity-by-`Col`+fails on those constructors too. They are excluded by construction (transforms+reject `Agg`/`Over`), which is why composition and identity stay well defined.++**`predict` gives every model a uniform codomain.** `Predict model r` interprets a+fitted model as an arrow in that category: `predict model :: Expr r` runs from the+model's feature context to the one-column context `{r}`, and the dependency+`model -> r` fixes the codomain object. This is *not* a functor or a denotation in+the technical sense — there is no category of models to be functorial over. The+real, useful property is uniformity: every model's prediction lands in the *same*+expression type (`Expr Double`/`Expr a`/`Expr Int`), so `derive`, the `Transform`+monoid, and `compileThrough` all apply with no per-model glue. That the compiled+`Expr` actually agrees with the fitted record's own parameters is a tested property+(`tests/Learn/Denotation.hs`), not a typeclass law — the class only knows the+symbolic half.++**`fit` is the parametrized-morphism (Para) fragment.** `fit cfg input df` chooses a+parameter — the trained record — and `predict` is the forward map applied at it.+In the Para/Lens picture of learning a learner is a *parametrized lens* carrying a+forward map plus backward update/request maps; we inhabit only the forward (Para)+part and expose no backward maps, because this interface is batch training, not+online gradient exchange. That is a complete, self-contained sub-structure, not a+half-built one — but it does mean the Lens vocabulary is motivation here, not+something the code instantiates. (The functional dependency `cfg input -> model`+fixes the parameter *type*; `fit` is the value-level map that picks the point.)++**`Transform` is a monoid of derived-column lists.** `Transform`'s `<>` keeps the+earlier step's outputs and rewrites the later step's column references through them+by substitution; `mempty` is the empty list. These are context-*extending* maps+(`applyTransform` adds columns), so this is an ordinary algebraic monoid — a monoid+*is* a one-object category (Seven Sketches ch. 3) — not the endomorphism monoid of a+fixed object. Associativity and identity hold for the row-wise fragment **provided+output names do not collide**: the implementation merges output maps with+`Data.Map.fromList`, which keeps the last binding on a clash, so reusing a column+name across steps is the one way to break the law.++**Composition is the point.** `compileThrough t (predict m)` realizes the composite+`predict m ∘ t` (read right-to-left: first `t`, then `predict m`) by substituting+`t`'s definitions into `predict m`. By the substitution lemma it denotes the same+function as transforming the frame and then predicting — equal results up to+floating point, not syntactically identical expressions. That is exactly the+"compose by substitution" example above, and it is why a model trained in a+transformed space deploys as one formula over the raw columns.++**What deliberately has no `predict` — two different reasons.** DBSCAN is+transductive: every clustering *fit* depends on the whole training set, but what+distinguishes the models is whether the *fitted* model induces an out-of-sample+rule. k-means (nearest centroid) and GMM (max posterior) do, so they have honest+`predict` arrows; DBSCAN's density-reachability assignment has no per-row rule, so+we give it no `Predict` instance rather than a fake `Maybe` or a throwing stub.+PCA and kernel PCA are the *opposite* case: they *are* arrows, but multi-output+feature maps with no privileged label column, so their canonical interface is a+`Transform` (`pcaTransform`/`pcaExprs`), not a one-column `predict`.++**A note on classifiers.** A multiclass `predict` is a genuine arrow into the label+object, but it compiles arg-max to a nested-`If` cascade (`argMaxExpr`), quadratic+in the number of classes — so for a 5-class model `prettyPrint (predict m)` is an+If-tree, not a tidy formula. The "model is a readable formula" aesthetic is honest+for affine and tree models; for classifiers and clusterers the value is that the+arrow exists and composes, not that it is short.++**An aside.** A linear or affine model's prediction is a signal-flow graph — a+weighted sum of inputs. `affineExpr` builds the arrow in the prop of affine *maps*+(the single-valued sub-prop of Seven Sketches ch. 5's signal-flow calculus of+affine relations), and dropping zero-weight terms is diagram simplification —+deleting a zero-gain wire.++(The "instance is a functor `C → Set`" slogan from Spivak's functorial data model,+Seven Sketches ch. 3, is sometimes invoked for dataframes; a single flat table is+the degenerate case — a schema with no foreign-key morphisms — so it is an analogy+here, not a structure we use.)
dataframe-learn.cabal view
@@ -1,12 +1,14 @@ cabal-version: 2.4 name: dataframe-learn-version: 1.0.2.0--synopsis: Decision trees and feature synthesis for the dataframe ecosystem.+version: 1.1.0.0+synopsis: Interpretable, expression-returning machine learning for the dataframe ecosystem. description:- @DataFrame.DecisionTree@ — decision-tree training on DataFrames.- @DataFrame.Synthesis@ — feature synthesis. Built on top of- @dataframe-operations@.+ A small scikit-learn-style ML library where every model returns both an+ inspectable record and dataframe @Expr@ value(s): linear/ridge/lasso/+ elastic-net and logistic regression, linear and RFF-kernel SVMs, decision+ trees, gradient boosting and AdaBoost, PCA and Nyström kernel PCA, k-means,+ Gaussian mixtures, DBSCAN, and symbolic regression — plus cross-validation+ and grid search. Pure Haskell, built on @dataframe-operations@. bug-reports: https://github.com/mchav/dataframe/issues license: MIT@@ -16,6 +18,7 @@ copyright: (c) 2024-2026 Michael Chavinda category: Data tested-with: GHC ==9.4.8 || ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.2+extra-doc-files: README.md common warnings ghc-options:@@ -41,12 +44,44 @@ DataFrame.DecisionTree.Tao DataFrame.DecisionTree.Fit DataFrame.LinearSolver+ DataFrame.LinearSolver.Loss+ DataFrame.LinearAlgebra+ DataFrame.LinearAlgebra.Solve+ DataFrame.LinearAlgebra.Eigen+ DataFrame.Random+ DataFrame.Featurize.Internal+ DataFrame.Model+ DataFrame.LinearModel+ DataFrame.LinearModel.Regression+ DataFrame.LinearModel.Logistic+ DataFrame.SVM+ DataFrame.DecisionTree.Regression+ DataFrame.DecisionTree.Model+ DataFrame.PCA+ DataFrame.PCA.Kernel+ DataFrame.SVM.RFF+ DataFrame.KMeans+ DataFrame.Transform+ DataFrame.Boosting+ DataFrame.Boosting.GBM+ DataFrame.Boosting.AdaBoost+ DataFrame.GMM+ DataFrame.DBSCAN+ DataFrame.Metrics+ DataFrame.Metrics.Report+ DataFrame.ModelSelection+ DataFrame.SymbolicRegression+ DataFrame.SymbolicRegression.Expr+ DataFrame.SymbolicRegression.Simplify+ DataFrame.SymbolicRegression.Optimize+ DataFrame.SymbolicRegression.GP DataFrame.Synthesis build-depends: base >= 4 && < 5, containers >= 0.6.7 && < 0.9, parallel ^>= 3.2,- dataframe-core ^>= 1.0,- dataframe-operations ^>= 1.1,+ random >= 1.2 && < 2,+ dataframe-core ^>= 1.1,+ dataframe-operations ^>= 1.1.1, text >= 2.0 && < 3, vector ^>= 0.13, vector-algorithms ^>= 0.9
+ src/DataFrame/Boosting.hs view
@@ -0,0 +1,10 @@+{- | Tree ensembles: gradient boosting (the recommended default) and AdaBoost+(SAMME). Re-exports the focused submodules.+-}+module DataFrame.Boosting (+ module DataFrame.Boosting.GBM,+ module DataFrame.Boosting.AdaBoost,+) where++import DataFrame.Boosting.AdaBoost+import DataFrame.Boosting.GBM
+ src/DataFrame/Boosting/AdaBoost.hs view
@@ -0,0 +1,211 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE UndecidableInstances #-}++{- | AdaBoost (SAMME) over short, sample-weighted classification trees. The+weighted-Gini stump fitter here is self-contained (it reuses the CART feature+encoding but not the unweighted CART recursion), so the existing decision-tree+path is untouched. 'predict' is the arg-max of weighted votes.+-}+module DataFrame.Boosting.AdaBoost (+ AdaBoostConfig (..),+ defaultAdaBoostConfig,+ AdaBoostModel (..),+) where++import Data.List (sort)+import Data.Maybe (fromMaybe, maybeToList)+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.DecisionTree.Cart (+ CartFeature (..),+ cartFeatures,+ sortIndicesByValue,+ )+import DataFrame.DecisionTree.Fit (treeToExpr)+import DataFrame.DecisionTree.Types (Tree (..))+import DataFrame.Featurize.Internal (argMaxExpr, targetValues)+import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (Columnable, TypedColumn (..), toVector)+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr (..))+import DataFrame.Internal.Interpreter (interpret)+import DataFrame.Model (Fit (..), Predict (..))+import DataFrame.Operators ((.*.), (.+.), (.==.))++data AdaBoostConfig = AdaBoostConfig+ { abNEstimators :: !Int+ , abMaxDepth :: !Int+ }+ deriving (Eq, Show)++defaultAdaBoostConfig :: AdaBoostConfig+defaultAdaBoostConfig = AdaBoostConfig{abNEstimators = 50, abMaxDepth = 1}++-- | A fitted SAMME model: per-stage weights and stumps over the class set.+data AdaBoostModel a = AdaBoostModel+ { abAlphas :: !(VU.Vector Double)+ , abStumps :: !(V.Vector (Tree a))+ , abClasses :: !(V.Vector a)+ }+ deriving (Show)++instance (Columnable a, Ord a) => Fit AdaBoostConfig (Expr a) (AdaBoostModel a) where+ fit = fitAdaBoost++instance (Columnable a, Ord a) => Predict (AdaBoostModel a) a where+ predict = adaBoostExpr++-- | Fit an AdaBoost-SAMME classifier.+fitAdaBoost ::+ (Columnable a, Ord a) =>+ AdaBoostConfig -> Expr a -> DataFrame -> AdaBoostModel a+fitAdaBoost cfg target@(Col name) df =+ AdaBoostModel+ (VU.fromList (reverse alphas))+ (V.fromList (reverse stumps))+ classesV+ where+ feats = V.fromList (cartFeatures name df)+ ys = targetValues target df+ n = V.length ys+ classes = sort (foldr dedup [] (V.toList ys))+ dedup x acc = if x `elem` acc then acc else x : acc+ classesV = V.fromList classes+ kClasses = length classes+ codes = VU.generate n (\i -> classIndex (ys V.! i))+ classIndex v = length (takeWhile (< v) classes)+ (alphas, stumps) = boost 0 (VU.replicate n (1 / fromIntegral (max 1 n))) [] []+ boost !m w as ts+ | m >= abNEstimators cfg = (as, ts)+ | otherwise =+ let stump = fitWeightedTree (abMaxDepth cfg) feats classesV codes kClasses w+ pred = predictCodes df classesV stump+ wrong :: VU.Vector Int+ wrong = VU.generate n (\i -> if pred VU.! i /= codes VU.! i then 1 else 0)+ err = clamp (VU.sum (VU.zipWith (*) w (VU.map fromIntegral wrong)) / VU.sum w)+ alpha = log ((1 - err) / err) + log (fromIntegral (max 1 (kClasses - 1)))+ w' = normalize (VU.zipWith (\wi e -> wi * exp (alpha * fromIntegral e)) w wrong)+ in if err <= 0 || err >= 1 - 1 / fromIntegral kClasses+ then (alpha : as, stump : ts)+ else boost (m + 1) w' (alpha : as) (stump : ts)+ 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)++predictCodes ::+ forall a.+ (Columnable a, Ord a) =>+ DataFrame -> V.Vector a -> Tree a -> VU.Vector Int+predictCodes df classesV stump =+ VU.fromList (map toCode preds)+ where+ 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)+ toCode v = fromMaybe 0 (V.findIndex (== v) classesV)++-- | A depth-bounded weighted classification tree (weighted Gini splits).+fitWeightedTree ::+ (Columnable a) =>+ Int ->+ V.Vector CartFeature ->+ V.Vector a ->+ VU.Vector Int ->+ Int ->+ VU.Vector Double ->+ Tree a+fitWeightedTree maxDepth feats classesV codes kClasses weights =+ go 0 (VU.enumFromN 0 (VU.length codes))+ where+ go depth idxs+ | depth >= maxDepth || VU.length idxs < 2 || isPure idxs =+ Leaf (classesV V.! majority idxs)+ | otherwise = case bestSplit idxs of+ Nothing -> Leaf (classesV V.! majority idxs)+ Just (fj, thr) ->+ let vals = cfValues (feats V.! fj)+ (l, r) = VU.partition (\i -> vals VU.! i <= thr) idxs+ in if VU.null l || VU.null r+ then Leaf (classesV V.! majority idxs)+ else+ Branch+ (cfPred (feats V.! fj) thr)+ (go (depth + 1) l)+ (go (depth + 1) r)+ classWeights idxs =+ VU.accumulate+ (+)+ (VU.replicate kClasses 0)+ (VU.map (\i -> (codes VU.! i, weights VU.! i)) idxs)+ majority idxs = VU.maxIndex (classWeights idxs)+ isPure idxs = VU.length (VU.filter (> 0) (classWeights idxs)) <= 1+ bestSplit idxs =+ let cands =+ [ (score, fj, thr)+ | fj <- [0 .. V.length feats - 1]+ , (thr, score) <- featureSplits idxs fj+ ]+ in case cands of+ [] -> Nothing+ _ -> let (_, fj, thr) = minimum3 cands in Just (fj, thr)+ featureSplits idxs fj =+ let vals = cfValues (feats V.! fj)+ member =+ VU.replicate (VU.length codes) False+ VU.// [(i, True) | i <- VU.toList idxs]+ sorted = VU.filter (member VU.!) (sortIndicesByValue vals)+ in sweep vals sorted (classWeights idxs)+ sweep vals sorted totW = go0 0 (VU.replicate kClasses 0) Nothing+ where+ m = VU.length sorted+ totWsum = VU.sum totW+ go0 !k leftW best+ | k >= m - 1 = maybeToList best+ | otherwise =+ let i = sorted VU.! k+ leftW' = leftW VU.// [(codes VU.! i, leftW VU.! (codes VU.! i) + weights VU.! i)]+ vCur = vals VU.! i+ vNext = vals VU.! (sorted VU.! (k + 1))+ wl = VU.sum leftW'+ wr = totWsum - wl+ score = wl * gini leftW' + wr * gini (VU.zipWith (-) totW leftW')+ valid = vCur /= vNext && wl > 0 && wr > 0+ best' =+ if valid && maybe True (\(_, s) -> score < s) best+ then Just ((vCur + vNext) / 2, score)+ else best+ in go0 (k + 1) leftW' best'++gini :: VU.Vector Double -> Double+gini cw =+ let total = VU.sum cw+ in if total == 0+ then 0+ else 1 - VU.sum (VU.map (\c -> (c / total) ^ (2 :: Int)) cw)++minimum3 :: (Ord a) => [(a, b, c)] -> (a, b, c)+minimum3 = foldr1 (\x@(a, _, _) y@(b, _, _) -> if a <= b then x else y)++{- | Compile to an arg-max-of-weighted-votes expression over the class set:+@argmax_c Σ_m αₘ·[stumpₘ = c]@.+-}+adaBoostExpr :: (Columnable a, Ord a) => AdaBoostModel a -> Expr a+adaBoostExpr m = argMaxExpr (zip classes scores)+ where+ classes = V.toList (abClasses m)+ stumpExprs = map treeToExpr (V.toList (abStumps m))+ alphas = VU.toList (abAlphas m)+ scores =+ [ foldr (.+.) (F.lit 0) (zipWith (vote c) alphas stumpExprs)+ | c <- classes+ ]+ vote c a se = F.lit a .*. indicator (se .==. F.lit c)+ indicator cond = F.ifThenElse cond (F.lit 1.0) (F.lit 0.0)
+ src/DataFrame/Boosting/GBM.hs view
@@ -0,0 +1,185 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Gradient boosting of regression trees (Friedman). Trees are fitted to the+negative gradient of the loss each round and accumulated with a shrinkage+factor; squared error gives regression, logistic deviance gives binary+classification. 'predict' is the additive score; 'gbProbaExpr' /+'gbDecisionExpr' give the classification probability / decision.+-}+module DataFrame.Boosting.GBM (+ GBLoss (..),+ GBConfig (..),+ defaultGBConfig,+ GBModel (..),+ gbExprAtStage,+ gbProbaExpr,+ gbDecisionExpr,+) where++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.DecisionTree.Cart (cartFeatures)+import DataFrame.DecisionTree.Fit (treeToExpr)+import DataFrame.DecisionTree.Regression (RegTreeConfig (..), fitRegTreeOn)+import DataFrame.DecisionTree.Types (Tree)+import DataFrame.Featurize.Internal (targetDoubles)+import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (TypedColumn (..), toVector)+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr (..), getColumns)+import DataFrame.Internal.Interpreter (interpret)+import DataFrame.Model (Fit (..), Predict (..))+import DataFrame.Operators ((.*.), (.+.), (.>.))++-- | The boosting loss.+data GBLoss = SquaredError | LogisticDeviance+ deriving (Eq, Show)++data GBConfig = GBConfig+ { gbLoss :: !GBLoss+ , gbNEstimators :: !Int+ , gbLearningRate :: !Double+ , gbMaxDepth :: !Int+ , gbSeed :: !Int+ }+ deriving (Eq, Show)++defaultGBConfig :: GBConfig+defaultGBConfig =+ GBConfig+ { gbLoss = SquaredError+ , gbNEstimators = 100+ , gbLearningRate = 0.1+ , gbMaxDepth = 3+ , gbSeed = 0+ }++{- | A fitted gradient-boosting model. 'gbInit' is the constant initial score+(mean, or log-odds for classification); 'gbTrees' are the staged regression+trees.+-}+data GBModel = GBModel+ { gbInit :: !Double+ , gbTrees :: !(V.Vector (Tree Double))+ , gbRate :: !Double+ , gbModelLoss :: !GBLoss+ , gbTrainScore :: !(VU.Vector Double)+ , gbFeatureUsage :: !(M.Map T.Text Int)+ }+ deriving (Show)++instance Fit GBConfig (Expr Double) GBModel where+ fit = fitGBM++instance Predict GBModel Double where+ predict = gbExpr++-- | Fit a gradient-boosting ensemble predicting @target@ from the other columns.+fitGBM :: GBConfig -> Expr Double -> DataFrame -> GBModel+fitGBM cfg target@(Col name) df =+ GBModel+ f0+ (V.fromList (reverse trees))+ lr+ (gbLoss cfg)+ (VU.fromList (reverse scores))+ usage+ where+ feats = V.fromList (cartFeatures name df)+ y = targetDoubles target df+ n = VU.length y+ lr = gbLearningRate cfg+ rtCfg =+ RegTreeConfig+ { rtMaxDepth = gbMaxDepth cfg+ , rtMinSamplesSplit = 2+ , rtMinLeafSize = 1+ , rtMinImpurityDecrease = 0.0+ }+ f0 = case gbLoss cfg of+ SquaredError -> VU.sum y / fromIntegral (max 1 n)+ LogisticDeviance ->+ let p = clamp01 (VU.sum y / fromIntegral (max 1 n))+ in log (p / (1 - p))+ (trees, scores, usage) = boost 0 (VU.replicate n f0) [] [] M.empty+ 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+ 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)++negGradient ::+ GBLoss -> VU.Vector Double -> VU.Vector Double -> VU.Vector Double+negGradient SquaredError y f = VU.zipWith (-) y f+negGradient LogisticDeviance y f =+ VU.zipWith (\yi fi -> yi - sigmoid fi) y f++lossValue :: GBLoss -> VU.Vector Double -> VU.Vector Double -> Double+lossValue SquaredError y f =+ VU.sum (VU.zipWith (\yi fi -> (yi - fi) ^ (2 :: Int)) y f)+ / fromIntegral (max 1 (VU.length y))+lossValue LogisticDeviance y f =+ VU.sum+ ( VU.zipWith+ ( \yi fi -> let p = clamp01 (sigmoid fi) in negate (yi * log p + (1 - yi) * log (1 - p))+ )+ y+ f+ )+ / fromIntegral (max 1 (VU.length y))++sigmoid :: Double -> Double+sigmoid z+ | z >= 0 = 1 / (1 + exp (-z))+ | otherwise = let e = exp z in e / (1 + e)++clamp01 :: Double -> Double+clamp01 p = max 1e-12 (min (1 - 1e-12) p)++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)++treeColumns :: Tree Double -> [T.Text]+treeColumns = getColumns . treeToExpr++-- | The full additive prediction expression: @f0 + lr · Σ treeᵢ@.+gbExpr :: GBModel -> Expr Double+gbExpr m = stageExpr (V.length (gbTrees m)) m++-- | The prediction expression using only the first @k@ trees (staged predict).+gbExprAtStage :: Int -> GBModel -> Maybe (Expr Double)+gbExprAtStage k m+ | k < 0 || k > V.length (gbTrees m) = Nothing+ | otherwise = Just (stageExpr k m)++stageExpr :: Int -> GBModel -> Expr Double+stageExpr k m =+ foldr ((.+.) . scaled) (F.lit (gbInit m)) (take k (V.toList (gbTrees m)))+ where+ scaled t = F.lit (gbRate m) .*. treeToExpr t++-- | Probability expression for classification: @sigmoid(score)@.+gbProbaExpr :: GBModel -> Expr Double+gbProbaExpr m = F.lit 1 / (F.lit 1 + exp (negate (gbExpr m)))++-- | Decision expression for classification: positive class when score > 0.+gbDecisionExpr :: GBModel -> Expr Bool+gbDecisionExpr m = gbExpr m .>. F.lit 0
+ src/DataFrame/DBSCAN.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Density-based clustering (DBSCAN). Brute-force @O(n²)@ region queries, no+spatial index — suitable for the in-memory scales this library targets. DBSCAN+is transductive: it has a 'Fit' instance but deliberately no 'Predict' instance+(there is no honest single prediction expression). 'dbscanSurrogateExpr' fits an+interpretable decision-tree surrogate on the cluster labels instead.+-}+module DataFrame.DBSCAN (+ DBSCANConfig (..),+ defaultDBSCANConfig,+ DBSCANModel (..),+ dbscanSurrogateExpr,+) where++import Control.Monad.ST (runST)+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import qualified Data.Vector.Unboxed.Mutable as VUM++import DataFrame.DecisionTree.Fit (fitDecisionTree)+import DataFrame.DecisionTree.Types (TreeConfig)+import DataFrame.Featurize.Internal (+ Features (..),+ columnExprName,+ extractFeatures,+ materializeColumn,+ )+import qualified DataFrame.Functions as F+import qualified DataFrame.Internal.Column as DI+import DataFrame.Internal.DataFrame (DataFrame, fromNamedColumns)+import DataFrame.Internal.Expression (Expr)+import DataFrame.LinearAlgebra (epsNeighbors)+import DataFrame.Model (Fit (..))++data DBSCANConfig = DBSCANConfig+ { dbEps :: !Double+ , dbMinSamples :: !Int+ }+ deriving (Eq, Show)++defaultDBSCANConfig :: DBSCANConfig+defaultDBSCANConfig = DBSCANConfig{dbEps = 0.5, dbMinSamples = 5}++{- | A fitted DBSCAN labelling. 'dbLabels' uses @-1@ for noise (sklearn's+@labels_@); 'dbCoreSampleIndices' are the core points.+-}+data DBSCANModel = DBSCANModel+ { dbLabels :: !(VU.Vector Int)+ , dbCoreSampleIndices :: !(VU.Vector Int)+ , dbNClusters :: !Int+ }+ deriving (Eq, Show)++instance Fit DBSCANConfig [Expr Double] DBSCANModel where+ fit = fitDBSCAN++-- | Cluster the feature columns with DBSCAN.+fitDBSCAN :: DBSCANConfig -> [Expr Double] -> DataFrame -> DBSCANModel+fitDBSCAN cfg features df =+ DBSCANModel labels coreIdx nClusters+ where+ Features _ _ rows n _ = extractFeatures features df+ nbrs = V.generate n (epsNeighbors (dbEps cfg) rows)+ isCore i = VU.length (nbrs V.! i) + 1 >= dbMinSamples cfg+ coreIdx = VU.fromList [i | i <- [0 .. n - 1], isCore i]+ labels = clusterLabels n nbrs isCore+ nClusters = if VU.null labels then 0 else 1 + maximum (-1 : VU.toList labels)++clusterLabels ::+ Int -> V.Vector (VU.Vector Int) -> (Int -> Bool) -> VU.Vector Int+clusterLabels n nbrs isCore = runST $ do+ lab <- VUM.replicate n (-2)+ let seedLoop c i+ | i >= n = pure ()+ | otherwise = do+ li <- VUM.read lab i+ if li /= -2+ then seedLoop c (i + 1)+ else+ if not (isCore i)+ then VUM.write lab i (-1) >> seedLoop c (i + 1)+ else do+ VUM.write lab i c+ expand lab c (VU.toList (nbrs V.! i))+ seedLoop (c + 1) (i + 1)+ expand _ _ [] = pure ()+ expand lab c (q : qs) = do+ lq <- VUM.read lab q+ if lq == -1+ then VUM.write lab q c >> expand lab c qs+ else+ if lq /= -2+ then expand lab c qs+ else do+ VUM.write lab q c+ let extra = if isCore q then VU.toList (nbrs V.! q) else []+ expand lab c (extra ++ qs)+ seedLoop 0 0+ VU.freeze lab++{- | Fit a decision-tree surrogate on the DBSCAN labels so new rows can be+assigned an (approximate) cluster. Noise (@-1@) is its own class.+-}+dbscanSurrogateExpr ::+ TreeConfig -> [Expr Double] -> DBSCANModel -> DataFrame -> Expr Int+dbscanSurrogateExpr cfg features model df =+ fitDecisionTree cfg (F.col @Int clusterCol) augmented+ where+ clusterCol = "__cluster__"+ cols = map (\e -> (columnExprName e, materializeColumn df e)) features+ augmented =+ fromNamedColumns $+ [(n, DI.fromList (VU.toList v)) | (n, v) <- cols]+ ++ [(clusterCol, DI.fromList (VU.toList (dbLabels model)))]
src/DataFrame/DecisionTree/Cart.hs view
@@ -4,10 +4,11 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} --- | sklearn-faithful CART initializer used to seed TAO. One-hot encodes--- categoricals, splits on exact (unsmoothed) Gini over midpoint thresholds--- (@<=@ routes left), and emits a @Tree@ predicting identically to--- @DecisionTreeClassifier(criterion='gini')@ on continuous features.+{- | sklearn-faithful CART initializer used to seed TAO. One-hot encodes+categoricals, splits on exact (unsmoothed) Gini over midpoint thresholds+(@<=@ routes left), and emits a @Tree@ predicting identically to+@DecisionTreeClassifier(criterion='gini')@ on continuous features.+-} module DataFrame.DecisionTree.Cart ( CartFeature (..), CartNode (..),@@ -39,8 +40,9 @@ import qualified Data.Vector.Unboxed as VU import Type.Reflection (typeRep) --- | A one-hot feature column: per-row Double values plus the sklearn LEFT--- predicate (@x <= threshold@) over the ORIGINAL DataFrame.+{- | A one-hot feature column: per-row Double values plus the sklearn LEFT+predicate (@x <= threshold@) over the ORIGINAL DataFrame.+-} data CartFeature = CartFeature { cfValues :: !(VU.Vector Double) , cfPred :: !(Double -> Expr Bool)@@ -59,8 +61,9 @@ , ctxMinLeaf :: !Int } --- | Indices @0..n-1@ stably sorted by their value (ascending), ties keeping--- ascending index. In-place unboxed merge sort — no boxed-list allocation.+{- | Indices @0..n-1@ stably sorted by their value (ascending), ties keeping+ascending index. In-place unboxed merge sort — no boxed-list allocation.+-} sortIndicesByValue :: VU.Vector Double -> VU.Vector Int sortIndicesByValue vs = VU.create $ do@@ -68,7 +71,8 @@ VA.sortBy (compare `on` (vs VU.!)) mv pure mv -buildCartTree :: forall a. (Columnable a, Ord a) => TreeConfig -> T.Text -> DataFrame -> Tree a+buildCartTree ::+ forall a. (Columnable a, Ord a) => TreeConfig -> T.Text -> DataFrame -> Tree a buildCartTree cfg target df = cartToTree feats classes (buildCartNode ctx 0 (VU.enumFromN 0 nAll) featSorted) where@@ -109,21 +113,35 @@ classCounts :: CartCtx -> VU.Vector Int -> VU.Vector Int classCounts ctx idxs =- VU.accumulate (+) (VU.replicate (ctxNClasses ctx) 0) (VU.map (\i -> (ctxCodes ctx VU.! i, 1)) idxs)+ VU.accumulate+ (+)+ (VU.replicate (ctxNClasses ctx) 0)+ (VU.map (\i -> (ctxCodes ctx VU.! i, 1)) idxs) isPure :: VU.Vector Int -> Bool isPure counts = VU.length (VU.filter (> 0) counts) <= 1 -buildCartNode :: CartCtx -> Int -> VU.Vector Int -> V.Vector (VU.Vector Int) -> CartNode+buildCartNode ::+ CartCtx -> Int -> VU.Vector Int -> V.Vector (VU.Vector Int) -> CartNode buildCartNode ctx depth idxs sortedByFeat | VU.length idxs < 2 || depth >= ctxMaxDepth ctx || isPure counts = leaf- | otherwise = maybe leaf (splitNode ctx depth idxs sortedByFeat) (bestSplit ctx sortedByFeat counts n)+ | otherwise =+ maybe+ leaf+ (splitNode ctx depth idxs sortedByFeat)+ (bestSplit ctx sortedByFeat counts n) where n = VU.length idxs counts = classCounts ctx idxs leaf = CLeaf (VU.maxIndex counts) -splitNode :: CartCtx -> Int -> VU.Vector Int -> V.Vector (VU.Vector Int) -> (Int, Double) -> CartNode+splitNode ::+ CartCtx ->+ Int ->+ VU.Vector Int ->+ V.Vector (VU.Vector Int) ->+ (Int, Double) ->+ CartNode splitNode ctx depth idxs sortedByFeat (fj, thr) = CSplit fj thr (rec leftIdx leftSorted) (rec rightIdx rightSorted) where@@ -134,9 +152,15 @@ rightSorted = V.map (VU.filter (\i -> vals VU.! i > thr)) sortedByFeat rec = buildCartNode ctx (depth + 1) --- | Minimum weighted-child-Gini @(feature, threshold)@; the first feature wins--- ties; 'Nothing' when no feature has a leaf-size-respecting threshold.-bestSplit :: CartCtx -> V.Vector (VU.Vector Int) -> VU.Vector Int -> Int -> Maybe (Int, Double)+{- | Minimum weighted-child-Gini @(feature, threshold)@; the first feature wins+ties; 'Nothing' when no feature has a leaf-size-respecting threshold.+-}+bestSplit ::+ CartCtx ->+ V.Vector (VU.Vector Int) ->+ VU.Vector Int ->+ Int ->+ Maybe (Int, Double) bestSplit ctx sortedByFeat counts n = fmap (\(_, j, t) -> (j, t)) (foldl' consider Nothing [0 .. ctxNFeats ctx - 1]) where@@ -145,8 +169,9 @@ Just (g, thr) | maybe True (\(gB, _, _) -> g < gB) acc -> Just (g, fj, thr) _ -> acc --- | Accumulator while sweeping a feature's sorted rows: best @(gini, thr)@ so--- far, per-class left counts, rows moved left, and the previous value seen.+{- | Accumulator while sweeping a feature's sorted rows: best @(gini, thr)@ so+far, per-class left counts, rows moved left, and the previous value seen.+-} data Sweep = Sweep { swBest :: !(Maybe (Double, Double)) , swLeft :: ![Int]@@ -154,9 +179,20 @@ , swPrev :: !Double } -sweepFeature :: CartCtx -> [Int] -> VU.Vector Int -> CartFeature -> Int -> Maybe (Double, Double)+sweepFeature ::+ CartCtx ->+ [Int] ->+ VU.Vector Int ->+ CartFeature ->+ Int ->+ Maybe (Double, Double) sweepFeature ctx total si feat n =- swBest (foldl' step (Sweep Nothing (replicate (ctxNClasses ctx) 0) 0 (0 / 0)) [0 .. VU.length si - 1])+ swBest+ ( foldl'+ step+ (Sweep Nothing (replicate (ctxNClasses ctx) 0) 0 (0 / 0))+ [0 .. VU.length si - 1]+ ) where vals = cfValues feat step s k = advance ctx total n (vals VU.! i) (ctxCodes ctx VU.! i) s@@ -165,24 +201,35 @@ advance :: CartCtx -> [Int] -> Int -> Double -> Int -> Sweep -> Sweep advance ctx total n v c s =- Sweep (considerThreshold ctx total n v s) (bumpClass c (swLeft s)) (swMoved s + 1) v+ Sweep+ (considerThreshold ctx total n v s)+ (bumpClass c (swLeft s))+ (swMoved s + 1)+ v -considerThreshold :: CartCtx -> [Int] -> Int -> Double -> Sweep -> Maybe (Double, Double)+considerThreshold ::+ CartCtx -> [Int] -> Int -> Double -> Sweep -> Maybe (Double, Double) considerThreshold ctx total n v s | swMoved s >= ctxMinLeaf ctx , n - swMoved s >= ctxMinLeaf ctx , v > swPrev s + 1e-7 =- keepBetter (swBest s) (weightedGini total (swLeft s) (swMoved s) n) ((swPrev s + v) / 2)+ keepBetter+ (swBest s)+ (weightedGini total (swLeft s) (swMoved s) n)+ ((swPrev s + v) / 2) | otherwise = swBest s -keepBetter :: Maybe (Double, Double) -> Double -> Double -> Maybe (Double, Double)+keepBetter ::+ Maybe (Double, Double) -> Double -> Double -> Maybe (Double, Double) keepBetter best g thr = case best of Just (wb, _) | wb <= g -> best _ -> Just (g, thr) weightedGini :: [Int] -> [Int] -> Int -> Int -> Double weightedGini total leftAcc nl n =- (fromIntegral nl * giniImpurity leftAcc nl + fromIntegral nr * giniImpurity rightAcc nr)+ ( fromIntegral nl * giniImpurity leftAcc nl+ + fromIntegral nr * giniImpurity rightAcc nr+ ) / fromIntegral n where nr = n - nl@@ -204,22 +251,31 @@ featuresOfColumn df c = case unsafeGetColumn c df of UnboxedColumn _ (v :: VU.Vector b) -> numericFeature @b c v BoxedColumn _ (v :: V.Vector b) -> oneHotFeatures @b (nRows df) c v+ pt@(PackedText _ _) -> case materializePacked pt of+ BoxedColumn _ (v :: V.Vector b) -> oneHotFeatures @b (nRows df) c v+ _ -> [] -numericFeature :: forall b. (Columnable b, VU.Unbox b) => T.Text -> VU.Vector b -> [CartFeature]+numericFeature ::+ forall b. (Columnable b, VU.Unbox b) => T.Text -> VU.Vector b -> [CartFeature] numericFeature c v = case testEquality (typeRep @b) (typeRep @Double) of Just Refl -> [CartFeature v (\t -> F.col @Double c .<=. F.lit t)] Nothing -> case sIntegral @b of- STrue -> [CartFeature (VU.map fromIntegral v) (\t -> F.toDouble (F.col @b c) .<=. F.lit t)]+ STrue ->+ [ CartFeature (VU.map fromIntegral v) (\t -> F.toDouble (F.col @b c) .<=. F.lit t)+ ] SFalse -> [] -oneHotFeatures :: forall b. (Columnable b) => Int -> T.Text -> V.Vector b -> [CartFeature]+oneHotFeatures ::+ forall b. (Columnable b) => Int -> T.Text -> V.Vector b -> [CartFeature] oneHotFeatures nAll c v = case testEquality (typeRep @b) (typeRep @T.Text) of Just Refl -> [oneHot nAll c v cat | cat <- Set.toList (Set.fromList (V.toList v))] Nothing -> [] oneHot :: Int -> T.Text -> V.Vector T.Text -> T.Text -> CartFeature oneHot nAll c v cat =- CartFeature (VU.generate nAll (\i -> if v V.! i == cat then 1 else 0)) (const (F.col @T.Text c ./=. F.lit cat))+ CartFeature+ (VU.generate nAll (\i -> if v V.! i == cat then 1 else 0))+ (const (F.col @T.Text c ./=. F.lit cat)) -- | Target column as string labels (matches pandas @y.astype(str)@). cartTargetLabels :: T.Text -> DataFrame -> V.Vector T.Text@@ -228,3 +284,8 @@ Just Refl -> v Nothing -> V.map (T.pack . show) v UnboxedColumn _ (v :: VU.Vector b) -> V.map (T.pack . show) (V.convert v)+ pt@(PackedText _ _) -> case materializePacked pt of+ BoxedColumn _ (v :: V.Vector b) -> case testEquality (typeRep @b) (typeRep @T.Text) of+ Just Refl -> v+ Nothing -> V.map (T.pack . show) v+ _ -> V.empty
src/DataFrame/DecisionTree/Categorical.hs view
@@ -5,10 +5,11 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} --- | Categorical split candidates: Breiman prefixes for binary targets,--- subset/singleton enumeration otherwise, and cross-column equality. Each--- value-list yields an OR-of-equalities condition (as an expression or a--- directly-read membership truth vector).+{- | Categorical split candidates: Breiman prefixes for binary targets,+subset/singleton enumeration otherwise, and cross-column equality. Each+value-list yields an OR-of-equalities condition (as an expression or a+directly-read membership truth vector).+-} module DataFrame.DecisionTree.Categorical ( TargetInfo (..), mkTargetInfo,@@ -29,7 +30,12 @@ ) where import DataFrame.DecisionTree.CondVec (CondVec (..), materializeCondVec)-import DataFrame.DecisionTree.Types (ColumnOrdering, SynthConfig (..), TreeConfig (..), withOrdFrom)+import DataFrame.DecisionTree.Types (+ ColumnOrdering,+ SynthConfig (..),+ TreeConfig (..),+ withOrdFrom,+ ) import DataFrame.Internal.Column import DataFrame.Internal.DataFrame (DataFrame, columnNames, unsafeGetColumn) import DataFrame.Internal.Expression (Expr (..))@@ -53,19 +59,25 @@ validBoxedValues :: Bitmap -> V.Vector a -> V.Vector a validBoxedValues bm = V.ifilter (\i _ -> bitmapTestBit bm i) --- | Target-column summary driving the categorical generator: binary vs--- multi-class, the deterministic positive class, and the raw label vector.+{- | Target-column summary driving the categorical generator: binary vs+multi-class, the deterministic positive class, and the raw label vector.+-} data TargetInfo target = TargetInfo { tiIsBinary :: !Bool , tiPositiveClass :: !(Maybe target) , tiValues :: !(V.Vector target) } --- | Compute 'TargetInfo' once per fit. The positive class for binary targets--- is the lexicographically-first distinct value, for deterministic pools.-mkTargetInfo :: forall target. (Columnable target, Ord target) => T.Text -> DataFrame -> Maybe (TargetInfo target)+{- | Compute 'TargetInfo' once per fit. The positive class for binary targets+is the lexicographically-first distinct value, for deterministic pools.+-}+mkTargetInfo ::+ forall target.+ (Columnable target, Ord target) =>+ T.Text -> DataFrame -> Maybe (TargetInfo target) mkTargetInfo target df = case interpret @target df (Col target) of- Right (TColumn column) -> either (const Nothing) (Just . targetInfoFromValues) (toVector @target column)+ Right (TColumn column) ->+ either (const Nothing) (Just . targetInfoFromValues) (toVector @target column) _ -> Nothing targetInfoFromValues :: (Ord target) => V.Vector target -> TargetInfo target@@ -77,8 +89,9 @@ (p : _) | isBinary -> Just p _ -> Nothing --- | Distinct values, capped: @Right vs@ (sorted) under the cap, else @Left@--- the count-so-far so the caller routes to the high-cardinality path.+{- | Distinct values, capped: @Right vs@ (sorted) under the cap, else @Left@+the count-so-far so the caller routes to the high-cardinality path.+-} distinctValuesUpTo :: (Ord a) => Int -> V.Vector a -> Either Int [a] distinctValuesUpTo cap values = go Set.empty 0 where@@ -88,8 +101,9 @@ | Set.size s > cap = Left (Set.size s) | otherwise = go (Set.insert (V.unsafeIndex values i) s) (i + 1) --- | OR-of-equalities for a value-list, shared by the expression and--- truth-vector discrete paths so they stay byte-identical.+{- | OR-of-equalities for a value-list, shared by the expression and+truth-vector discrete paths so they stay byte-identical.+-} orEqs :: (a -> Expr Bool) -> [a] -> Expr Bool orEqs eqLit = foldr1 (.||.) . map eqLit @@ -106,17 +120,28 @@ singletonLists :: [a] -> [[a]] singletonLists = map (: []) -breimanPrefixSplits :: (Ord a, Ord target) => target -> V.Vector a -> V.Vector target -> [a] -> (a -> Expr Bool) -> [Expr Bool]+breimanPrefixSplits ::+ (Ord a, Ord target) =>+ target ->+ V.Vector a ->+ V.Vector target ->+ [a] ->+ (a -> Expr Bool) ->+ [Expr Bool] breimanPrefixSplits pc values targetVals distinctVals eqLit = map (orEqs eqLit) (breimanPrefixLists pc values targetVals distinctVals) --- | Breiman's binary-target split set: sort levels by Laplace-smoothed--- positive rate, then take every contiguous non-trivial prefix.-breimanPrefixLists :: (Ord a, Ord target) => target -> V.Vector a -> V.Vector target -> [a] -> [[a]]+{- | Breiman's binary-target split set: sort levels by Laplace-smoothed+positive rate, then take every contiguous non-trivial prefix.+-}+breimanPrefixLists ::+ (Ord a, Ord target) => target -> V.Vector a -> V.Vector target -> [a] -> [[a]] breimanPrefixLists pc values targetVals distinctVals = nonTrivialPrefixes (sortByRate (levelCounts pc values targetVals) distinctVals) -levelCounts :: (Ord a, Eq target) => target -> V.Vector a -> V.Vector target -> M.Map a (Int, Int)+levelCounts ::+ (Ord a, Eq target) =>+ target -> V.Vector a -> V.Vector target -> M.Map a (Int, Int) levelCounts pc values targetVals = V.ifoldl' add M.empty values where add acc i v = M.insertWith plus v (indicator (V.unsafeIndex targetVals i == pc), 1) acc@@ -132,17 +157,21 @@ sortByRate counts = sortBy (compare `on` (\v -> (laplaceRate counts v, v))) nonTrivialPrefixes :: [a] -> [[a]]-nonTrivialPrefixes = tail . init . inits+nonTrivialPrefixes = drop 1 . init . inits --- | Value-lists a categorical column contributes; shared by the expression and--- truth-vector paths so both enumerate identical candidates in the same order.-catValueLists :: (Ord a, Ord target) => Bool -> Maybe target -> V.Vector target -> Int -> V.Vector a -> [[a]]+{- | Value-lists a categorical column contributes; shared by the expression and+truth-vector paths so both enumerate identical candidates in the same order.+-}+catValueLists ::+ (Ord a, Ord target) =>+ Bool -> Maybe target -> V.Vector target -> Int -> V.Vector a -> [[a]] catValueLists isBinary posClass targetVals subsetCap values | V.null values = [] | isBinary, Just pc <- posClass = binaryLists pc targetVals values | otherwise = multiclassLists subsetCap values -binaryLists :: (Ord a, Ord target) => target -> V.Vector target -> V.Vector a -> [[a]]+binaryLists ::+ (Ord a, Ord target) => target -> V.Vector target -> V.Vector a -> [[a]] binaryLists pc targetVals values | length distinct < 2 = [] | otherwise = breimanPrefixLists pc values targetVals distinct@@ -158,15 +187,17 @@ ascDistinct :: (Ord a) => V.Vector a -> [a] ascDistinct = Set.toAscList . Set.fromList . V.toList --- | Truth vector of @col ∈ values@ read directly from the column; equal to--- interpreting @orEqs (== v) values@ because the values are distinct.+{- | Truth vector of @col ∈ values@ read directly from the column; equal to+interpreting @orEqs (== v) values@ because the values are distinct.+-} membershipVec :: (Ord a) => V.Vector a -> [a] -> VU.Vector Bool membershipVec colVals vs = let !s = Set.fromList vs in VU.generate (V.length colVals) (\i -> Set.member (colVals `V.unsafeIndex` i) s) --- | Per-fit categorical generation context bundling the target summary and--- the column-ordering registry.+{- | Per-fit categorical generation context bundling the target summary and+the column-ordering registry.+-} data CatCtx target = CatCtx { ccBinary :: !Bool , ccPos :: !(Maybe target)@@ -177,7 +208,12 @@ catCtx :: TargetInfo target -> TreeConfig -> CatCtx target catCtx ti cfg =- CatCtx (tiIsBinary ti) (tiPositiveClass ti) (tiValues ti) (maxCategoricalSubsetCardinality (synthConfig cfg)) (columnOrdering cfg)+ CatCtx+ (tiIsBinary ti)+ (tiPositiveClass ti)+ (tiValues ti)+ (maxCategoricalSubsetCardinality (synthConfig cfg))+ (columnOrdering cfg) catValueListsFor :: (Ord a, Ord target) => CatCtx target -> V.Vector a -> [[a]] catValueListsFor ctx = catValueLists (ccBinary ctx) (ccPos ctx) (ccTargets ctx) (ccSubsetCap ctx)@@ -190,26 +226,54 @@ STrue -> True SFalse -> False --- | All equality-based candidate splits from non-numeric columns: per-column--- categorical conditions plus cross-column equality/order conditions.-discreteConditions :: forall target. (Columnable target, Ord target) => TargetInfo target -> TreeConfig -> DataFrame -> [Expr Bool]+{- | All equality-based candidate splits from non-numeric columns: per-column+categorical conditions plus cross-column equality/order conditions.+-}+discreteConditions ::+ forall target.+ (Columnable target, Ord target) =>+ TargetInfo target -> TreeConfig -> DataFrame -> [Expr Bool] discreteConditions targetInfo cfg df =- concatMap (columnConds (catCtx targetInfo cfg) df) (columnNames df) ++ crossColumnConds cfg df+ concatMap (columnConds (catCtx targetInfo cfg) df) (columnNames df)+ ++ crossColumnConds cfg df -columnConds :: (Columnable target, Ord target) => CatCtx target -> DataFrame -> T.Text -> [Expr Bool]+columnConds ::+ (Columnable target, Ord target) =>+ CatCtx target -> DataFrame -> T.Text -> [Expr Bool] columnConds ctx df colName = case unsafeGetColumn colName df of BoxedColumn Nothing (column :: V.Vector a) -> nonNullColConds ctx colName column BoxedColumn (Just bm) (column :: V.Vector a) -> nullableColConds ctx colName bm column UnboxedColumn _ (_ :: VU.Vector a) -> []+ pt@(PackedText _ _) -> case materializePacked pt of+ BoxedColumn Nothing (column :: V.Vector a) -> nonNullColConds ctx colName column+ BoxedColumn (Just bm) (column :: V.Vector a) -> nullableColConds ctx colName bm column+ _ -> [] -nonNullColConds :: forall a target. (Columnable a, Ord target) => CatCtx target -> T.Text -> V.Vector a -> [Expr Bool]+nonNullColConds ::+ forall a target.+ (Columnable a, Ord target) =>+ CatCtx target -> T.Text -> V.Vector a -> [Expr Bool] nonNullColConds ctx colName column =- fromMaybe [] (withOrdFrom @a (ccOrds ctx) (map (orEqs (eqExprFor @a colName)) (catValueListsFor ctx column)))+ fromMaybe+ []+ ( withOrdFrom @a+ (ccOrds ctx)+ (map (orEqs (eqExprFor @a colName)) (catValueListsFor ctx column))+ ) -nullableColConds :: forall a target. (Columnable a, Ord target) => CatCtx target -> T.Text -> Bitmap -> V.Vector a -> [Expr Bool]+nullableColConds ::+ forall a target.+ (Columnable a, Ord target) =>+ CatCtx target -> T.Text -> Bitmap -> V.Vector a -> [Expr Bool] nullableColConds ctx colName bm column | isNumericKind @a || V.null valid = []- | otherwise = fromMaybe [] (withOrdFrom @a (ccOrds ctx) (map (orEqs (eqJustFor @a colName)) (catValueListsFor ctx valid)))+ | otherwise =+ fromMaybe+ []+ ( withOrdFrom @a+ (ccOrds ctx)+ (map (orEqs (eqJustFor @a colName)) (catValueListsFor ctx valid))+ ) where valid = validBoxedValues bm column @@ -225,32 +289,50 @@ allowedPairs :: TreeConfig -> DataFrame -> [(T.Text, T.Text)] allowedPairs cfg df =- [(l, r) | l <- columnNames df, r <- columnNames df, l /= r, not (isDisallowedPair cfg l r)]+ [ (l, r)+ | l <- columnNames df+ , r <- columnNames df+ , l /= r+ , not (isDisallowedPair cfg l r)+ ] isDisallowedPair :: TreeConfig -> T.Text -> T.Text -> Bool isDisallowedPair cfg l r =- any (\(l', r') -> sort [l', r'] == sort [l, r]) (disallowedCombinations (synthConfig cfg))+ any+ (\(l', r') -> sort [l', r'] == sort [l, r])+ (disallowedCombinations (synthConfig cfg)) pairConds :: ColumnOrdering -> DataFrame -> (T.Text, T.Text) -> [Expr Bool]-pairConds ords df (l, r) = case (unsafeGetColumn l df, unsafeGetColumn r df) of+pairConds ords df (l, r) = case ( materializePacked (unsafeGetColumn l df)+ , materializePacked (unsafeGetColumn r df)+ ) of (BoxedColumn Nothing (_ :: V.Vector a), BoxedColumn Nothing (_ :: V.Vector b)) -> strictPairConds @a @b l r (BoxedColumn (Just _) (_ :: V.Vector a), BoxedColumn (Just _) (_ :: V.Vector b)) -> nullablePairConds @a @b ords l r _ -> [] -strictPairConds :: forall a b. (Columnable a, Columnable b) => T.Text -> T.Text -> [Expr Bool]+strictPairConds ::+ forall a b. (Columnable a, Columnable b) => T.Text -> T.Text -> [Expr Bool] strictPairConds l r = case testEquality (typeRep @a) (typeRep @b) of Just Refl -> [Col @a l .==. Col @a r] Nothing -> [] -nullablePairConds :: forall a b. (Columnable a, Columnable b) => ColumnOrdering -> T.Text -> T.Text -> [Expr Bool]+nullablePairConds ::+ forall a b.+ (Columnable a, Columnable b) =>+ ColumnOrdering -> T.Text -> T.Text -> [Expr Bool] nullablePairConds ords l r = case testEquality (typeRep @a) (typeRep @b) of Nothing -> [] Just Refl -> nullableEqOrLe @a ords l r -nullableEqOrLe :: forall a. (Columnable a) => ColumnOrdering -> T.Text -> T.Text -> [Expr Bool]+nullableEqOrLe ::+ forall a. (Columnable a) => ColumnOrdering -> T.Text -> T.Text -> [Expr Bool] nullableEqOrLe ords l r | isTextType @a = eqOnly- | otherwise = maybe eqOnly (++ eqOnly) (withOrdFrom @a ords [Col @(Maybe a) l .<=. Col @(Maybe a) r])+ | otherwise =+ maybe+ eqOnly+ (++ eqOnly)+ (withOrdFrom @a ords [Col @(Maybe a) l .<=. Col @(Maybe a) r]) where eqOnly = [Col @(Maybe a) l .==. Col @(Maybe a) r] @@ -259,23 +341,41 @@ Just Refl -> True Nothing -> False --- | 'discreteConditions' materialized with shared per-column reads: the--- non-nullable categorical path builds truth vectors directly from one read--- per column; nullable and cross-column fall back to interpret.-discreteCondVecs :: forall target. (Columnable target, Ord target) => TargetInfo target -> TreeConfig -> DataFrame -> [CondVec]+{- | 'discreteConditions' materialized with shared per-column reads: the+non-nullable categorical path builds truth vectors directly from one read+per column; nullable and cross-column fall back to interpret.+-}+discreteCondVecs ::+ forall target.+ (Columnable target, Ord target) =>+ TargetInfo target -> TreeConfig -> DataFrame -> [CondVec] discreteCondVecs targetInfo cfg df = concatMap (columnCondVecs (catCtx targetInfo cfg) df) (columnNames df) ++ mapMaybe (materializeCondVec df) (crossColumnConds cfg df) -columnCondVecs :: (Columnable target, Ord target) => CatCtx target -> DataFrame -> T.Text -> [CondVec]+columnCondVecs ::+ (Columnable target, Ord target) =>+ CatCtx target -> DataFrame -> T.Text -> [CondVec] columnCondVecs ctx df colName = case unsafeGetColumn colName df of BoxedColumn Nothing (column :: V.Vector a) -> nonNullColCondVecs ctx colName column BoxedColumn (Just bm) (column :: V.Vector a) -> mapMaybe (materializeCondVec df) (nullableColConds ctx colName bm column) UnboxedColumn _ (_ :: VU.Vector a) -> []+ pt@(PackedText _ _) -> case materializePacked pt of+ BoxedColumn Nothing (column :: V.Vector a) -> nonNullColCondVecs ctx colName column+ BoxedColumn (Just bm) (column :: V.Vector a) -> mapMaybe (materializeCondVec df) (nullableColConds ctx colName bm column)+ _ -> [] -nonNullColCondVecs :: forall a target. (Columnable a, Ord target) => CatCtx target -> T.Text -> V.Vector a -> [CondVec]+nonNullColCondVecs ::+ forall a target.+ (Columnable a, Ord target) => CatCtx target -> T.Text -> V.Vector a -> [CondVec] nonNullColCondVecs ctx colName column =- fromMaybe [] (withOrdFrom @a (ccOrds ctx) (map (membershipCondVec colName column) (catValueListsFor ctx column)))+ fromMaybe+ []+ ( withOrdFrom @a+ (ccOrds ctx)+ (map (membershipCondVec colName column) (catValueListsFor ctx column))+ ) -membershipCondVec :: forall a. (Columnable a, Ord a) => T.Text -> V.Vector a -> [a] -> CondVec+membershipCondVec ::+ forall a. (Columnable a, Ord a) => T.Text -> V.Vector a -> [a] -> CondVec membershipCondVec colName column vs = CondVec (orEqs (eqExprFor @a colName) vs) (membershipVec column vs)
src/DataFrame/DecisionTree/CondVec.hs view
@@ -3,9 +3,10 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} --- | Cached condition truth vectors and the per-fit cache keyed by structural--- form. A condition's truth over a fixed DataFrame is invariant for a whole--- fit, so it is materialized once and reused.+{- | Cached condition truth vectors and the per-fit cache keyed by structural+form. A condition's truth over a fixed DataFrame is invariant for a whole+fit, so it is materialized once and reused.+-} module DataFrame.DecisionTree.CondVec ( CondVec (..), materializeCondVec,@@ -25,7 +26,12 @@ import qualified DataFrame.Functions as F import DataFrame.Internal.Column (TypedColumn (..), toVector) import DataFrame.Internal.DataFrame (DataFrame)-import DataFrame.Internal.Expression (BinaryOp (binaryName), Expr (..), eqExpr, normalize)+import DataFrame.Internal.Expression (+ BinaryOp (binaryName),+ Expr (..),+ eqExpr,+ normalize,+ ) import DataFrame.Internal.Interpreter (interpret) import qualified Data.Map.Strict as M@@ -42,8 +48,9 @@ , cvVec :: !(VU.Vector Bool) } --- | Interpret a condition once over the DataFrame; 'Nothing' on a--- type/interpret failure so the candidate is silently dropped.+{- | Interpret a condition once over the DataFrame; 'Nothing' on a+type/interpret failure so the candidate is silently dropped.+-} materializeCondVec :: DataFrame -> Expr Bool -> Maybe CondVec materializeCondVec df cond = case interpret @Bool df cond of Left _ -> Nothing@@ -52,13 +59,15 @@ eitherToMaybe :: Either e a -> Maybe a eitherToMaybe = either (const Nothing) Just --- | Full-DataFrame truth vectors keyed by structural form, read-only once--- built. Seeded for free from the candidate pool plus the initial tree so the--- predict/loss passes index a vector instead of re-interpreting per node.+{- | Full-DataFrame truth vectors keyed by structural form, read-only once+built. Seeded for free from the candidate pool plus the initial tree so the+predict/loss passes index a vector instead of re-interpreting per node.+-} type CondCache = M.Map T.Text (VU.Vector Bool) --- | Structural key matching the candidate-dedup key, so a tree branch whose--- condition came from the pool hits the cache (equal keys ⟹ equal vector).+{- | Structural key matching the candidate-dedup key, so a tree branch whose+condition came from the pool hits the cache (equal keys ⟹ equal vector).+-} condCacheKey :: Expr Bool -> T.Text condCacheKey = T.pack . show . normalize @@ -66,8 +75,9 @@ condCacheFromVecs :: [CondVec] -> CondCache condCacheFromVecs cvs = M.fromList [(condCacheKey (cvExpr cv), cvVec cv) | cv <- cvs] --- | Add a tree's branch-condition vectors to a cache (one interpret per--- distinct, not-yet-cached condition).+{- | Add a tree's branch-condition vectors to a cache (one interpret per+distinct, not-yet-cached condition).+-} addTreeCondsToCache :: DataFrame -> Tree a -> CondCache -> CondCache addTreeCondsToCache df = go where@@ -77,12 +87,14 @@ insertCond :: DataFrame -> Expr Bool -> CondCache -> CondCache insertCond df cond c | M.member k c = c- | otherwise = maybe c (\cv -> M.insert k (cvVec cv) c) (materializeCondVec df cond)+ | otherwise =+ maybe c (\cv -> M.insert k (cvVec cv) c) (materializeCondVec df cond) where k = condCacheKey cond --- | A condition's truth vector: a cache hit, else interpret over the--- DataFrame. 'Nothing' mirrors the interpret-failure fallback (route left).+{- | A condition's truth vector: a cache hit, else interpret over the+DataFrame. 'Nothing' mirrors the interpret-failure fallback (route left).+-} lookupCondVec :: CondCache -> DataFrame -> Expr Bool -> Maybe (VU.Vector Bool) lookupCondVec cache df cond = case M.lookup (condCacheKey cond) cache of hit@(Just _) -> hit@@ -98,8 +110,9 @@ where misrouted cp = (boolVals VU.! cpIndex cp) /= (cpCorrectDir cp == GoLeft) --- | A same-column same-direction Double threshold comparison, with a rebuild--- function to swap in a new threshold.+{- | A same-column same-direction Double threshold comparison, with a rebuild+function to swap in a new threshold.+-} data ThreshCmp = ThreshCmp { tcCol :: !T.Text , tcName :: !T.Text@@ -109,7 +122,9 @@ asDoubleThreshold :: Expr Bool -> Maybe ThreshCmp asDoubleThreshold (Binary op (Col c :: Expr cc) (Lit (t :: tt))) =- case (testEquality (typeRep @cc) (typeRep @Double), testEquality (typeRep @tt) (typeRep @Double)) of+ case ( testEquality (typeRep @cc) (typeRep @Double)+ , testEquality (typeRep @tt) (typeRep @Double)+ ) of (Just Refl, Just Refl) -> Just (ThreshCmp c (binaryName op) t (Binary op (Col c) . Lit)) _ -> Nothing asDoubleThreshold _ = Nothing@@ -117,8 +132,9 @@ directionalNames :: [T.Text] directionalNames = ["lt", "leq", "gt", "geq"] --- | Tighter (AND) or looser (OR) of two same-direction thresholds: @<@/@<=@--- are left-half-spaces (AND = min), @>@/@>=@ are right-half-spaces (AND = max).+{- | Tighter (AND) or looser (OR) of two same-direction thresholds: @<@/@<=@+are left-half-spaces (AND = min), @>@/@>=@ are right-half-spaces (AND = max).+-} chooseThreshold :: Bool -> T.Text -> Double -> Double -> Double chooseThreshold isAnd name t1 t2 | leftDir = if isAnd then min t1 t2 else max t1 t2@@ -126,8 +142,9 @@ where leftDir = name == "lt" || name == "leq" --- | Collapse two same-column same-direction strict-Double comparisons into one--- comparison (the @True@ argument selects AND, @False@ OR); 'Nothing' otherwise.+{- | Collapse two same-column same-direction strict-Double comparisons into one+comparison (the @True@ argument selects AND, @False@ OR); 'Nothing' otherwise.+-} consolidateThreshold :: Bool -> Expr Bool -> Expr Bool -> Maybe (Expr Bool) consolidateThreshold isAnd ea eb = do a <- asDoubleThreshold ea@@ -136,20 +153,28 @@ then Just (tcRebuild a (chooseThreshold isAnd (tcName a) (tcThr a) (tcThr b))) else Nothing --- | AND-combine two cached conditions: idempotence and threshold consolidation--- first, else the generic @F.and@; the vector is always the elementwise AND.+{- | AND-combine two cached conditions: idempotence and threshold consolidation+first, else the generic @F.and@; the vector is always the elementwise AND.+-} combineAndVec :: CondVec -> CondVec -> CondVec combineAndVec a b | eqExpr (cvExpr a) (cvExpr b) = a | otherwise = CondVec expr (VU.zipWith (&&) (cvVec a) (cvVec b)) where- expr = fromMaybe (F.and (cvExpr a) (cvExpr b)) (consolidateThreshold True (cvExpr a) (cvExpr b))+ expr =+ fromMaybe+ (F.and (cvExpr a) (cvExpr b))+ (consolidateThreshold True (cvExpr a) (cvExpr b)) --- | OR-combine two cached conditions (see 'combineAndVec'; AND/OR direction--- differs in 'consolidateThreshold').+{- | OR-combine two cached conditions (see 'combineAndVec'; AND/OR direction+differs in 'consolidateThreshold').+-} combineOrVec :: CondVec -> CondVec -> CondVec combineOrVec a b | eqExpr (cvExpr a) (cvExpr b) = a | otherwise = CondVec expr (VU.zipWith (||) (cvVec a) (cvVec b)) where- expr = fromMaybe (F.or (cvExpr a) (cvExpr b)) (consolidateThreshold False (cvExpr a) (cvExpr b))+ expr =+ fromMaybe+ (F.or (cvExpr a) (cvExpr b))+ (consolidateThreshold False (cvExpr a) (cvExpr b))
src/DataFrame/DecisionTree/Fit.hs view
@@ -3,9 +3,10 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} --- | Top-level fitting: assemble the candidate pool, seed from CART, run TAO,--- and convert the result to an expression. Also the probability-tree variant--- ('fitProbTree') that annotates leaves with class distributions.+{- | Top-level fitting: assemble the candidate pool, seed from CART, run TAO,+and convert the result to an expression. Also the probability-tree variant+('fitProbTree') that annotates leaves with class distributions.+-} module DataFrame.DecisionTree.Fit ( treeToExpr, fitDecisionTree,@@ -24,7 +25,12 @@ ) where import DataFrame.DecisionTree.Cart (buildCartTree)-import DataFrame.DecisionTree.Categorical (TargetInfo (..), discreteConditions, discreteCondVecs, mkTargetInfo)+import DataFrame.DecisionTree.Categorical (+ TargetInfo (..),+ discreteCondVecs,+ discreteConditions,+ mkTargetInfo,+ ) import DataFrame.DecisionTree.CondVec (CondVec) import DataFrame.DecisionTree.Numeric (numericCondVecs, numericConditions) import DataFrame.DecisionTree.Pool (dedupCVByExpr, nubByExpr)@@ -54,9 +60,11 @@ treeToExpr (Branch cond left right) = F.ifThenElse cond (treeToExpr left) (treeToExpr right) -- | Fit a TAO decision tree (CART-seeded) and return it as an expression.-fitDecisionTree :: forall a. (Columnable a, Ord a) => TreeConfig -> Expr a -> DataFrame -> Expr a+fitDecisionTree ::+ forall a. (Columnable a, Ord a) => TreeConfig -> Expr a -> DataFrame -> Expr a fitDecisionTree cfg (Col target) df =- pruneExpr (treeToExpr (taoOptimizeCV @a cfg target condVecs df indices initialTree))+ pruneExpr+ (treeToExpr (taoOptimizeCV @a cfg target condVecs df indices initialTree)) where condVecs = candidatePool @a cfg target df initialTree = buildCartTree @a cfg target df@@ -64,18 +72,24 @@ fitDecisionTree _ expr _ = error ("Cannot create tree for compound expression: " ++ show expr) -- | The deduplicated numeric + discrete candidate pool for a target column.-candidatePool :: forall a. (Columnable a, Ord a) => TreeConfig -> T.Text -> DataFrame -> [CondVec]+candidatePool ::+ forall a.+ (Columnable a, Ord a) => TreeConfig -> T.Text -> DataFrame -> [CondVec] candidatePool cfg target df = dedupCVByExpr (numericCVs ++ discreteCVs) where dfNoTarget = exclude [target] df numericCVs = numericCondVecs cfg dfNoTarget df discreteCVs = discreteCondVecs (targetInfoOrEmpty @a target df) cfg dfNoTarget -targetInfoOrEmpty :: forall a. (Columnable a, Ord a) => T.Text -> DataFrame -> TargetInfo a+targetInfoOrEmpty ::+ forall a. (Columnable a, Ord a) => T.Text -> DataFrame -> TargetInfo a targetInfoOrEmpty target df = fromMaybe (TargetInfo False Nothing V.empty) (mkTargetInfo @a target df) -- | Fit a tree at a given depth from a raw condition list (CART + TAO + prune).-buildTree :: forall a. (Columnable a, Ord a) => TreeConfig -> Int -> T.Text -> [Expr Bool] -> DataFrame -> Expr a+buildTree ::+ forall a.+ (Columnable a, Ord a) =>+ TreeConfig -> Int -> T.Text -> [Expr Bool] -> DataFrame -> Expr a buildTree cfg depth target conds df = pruneExpr (treeToExpr (taoOptimize @a cfg target conds df indices tree)) where@@ -89,7 +103,8 @@ partitionDataFrame cond df = (filterWhere cond df, filterWhere (F.not cond) df) -- | Laplace-smoothed Gini impurity of the target distribution.-calculateGini :: forall a. (Columnable a, Ord a) => T.Text -> DataFrame -> Double+calculateGini ::+ forall a. (Columnable a, Ord a) => T.Text -> DataFrame -> Double calculateGini target df | n == 0 = 0 | otherwise = 1 - sum (map (^ (2 :: Int)) probs)@@ -106,7 +121,8 @@ where counts = getCounts @a target df -getCounts :: forall a. (Columnable a, Ord a) => T.Text -> DataFrame -> M.Map a Int+getCounts ::+ forall a. (Columnable a, Ord a) => T.Text -> DataFrame -> M.Map a Int getCounts target df = case interpret @a df (Col target) of Left e -> throw e Right (TColumn column) -> case toVector @a column of@@ -131,7 +147,9 @@ type ProbTree a = Tree (M.Map a Double) -- | Normalised class probabilities over a subset of training rows.-probsFromIndices :: forall a. (Columnable a, Ord a) => T.Text -> DataFrame -> V.Vector Int -> M.Map a Double+probsFromIndices ::+ forall a.+ (Columnable a, Ord a) => T.Text -> DataFrame -> V.Vector Int -> M.Map a Double probsFromIndices target df indices = case interpret @a df (Col target) of Right (TColumn column) -> either (const M.empty) (normaliseCounts indices) (toVector @a column) _ -> M.empty@@ -139,30 +157,51 @@ normaliseCounts :: (Ord a) => V.Vector Int -> V.Vector a -> M.Map a Double normaliseCounts indices vals = M.map (\c -> fromIntegral c / total) counts where- counts = V.foldl' (\acc i -> M.insertWith (+) (vals V.! i) (1 :: Int) acc) M.empty indices+ counts =+ V.foldl'+ (\acc i -> M.insertWith (+) (vals V.! i) (1 :: Int) acc)+ M.empty+ indices total = fromIntegral (V.length indices) :: Double --- | Re-label a fitted tree's leaves with class distributions, routing the--- training data through the (unchanged) split conditions.-buildProbTree :: forall a. (Columnable a, Ord a) => Tree a -> T.Text -> DataFrame -> V.Vector Int -> ProbTree a+{- | Re-label a fitted tree's leaves with class distributions, routing the+training data through the (unchanged) split conditions.+-}+buildProbTree ::+ forall a.+ (Columnable a, Ord a) =>+ Tree a -> T.Text -> DataFrame -> V.Vector Int -> ProbTree a buildProbTree (Leaf _) target df indices = Leaf (probsFromIndices @a target df indices) buildProbTree (Branch cond left right) target df indices =- Branch cond (buildProbTree @a left target df l) (buildProbTree @a right target df r)+ Branch+ cond+ (buildProbTree @a left target df l)+ (buildProbTree @a right target df r) where (l, r) = partitionIndices cond df indices -- | Fit a TAO tree and return one probability expression per class.-fitProbTree :: forall a. (Columnable a, Ord a) => TreeConfig -> Expr a -> DataFrame -> M.Map a (Expr Double)+fitProbTree ::+ forall a.+ (Columnable a, Ord a) =>+ TreeConfig -> Expr a -> DataFrame -> M.Map a (Expr Double) fitProbTree cfg (Col target) df = probExprs (buildProbTree @a pruned target df indices) where- conds = nubByExpr (numericConditions cfg dfNoTarget ++ discreteConditions (targetInfoOrEmpty @a target df) cfg dfNoTarget)+ conds =+ nubByExpr+ ( numericConditions cfg dfNoTarget+ ++ discreteConditions (targetInfoOrEmpty @a target df) cfg dfNoTarget+ ) dfNoTarget = exclude [target] df indices = V.enumFromN 0 (nRows df)- pruned = pruneDead (taoOptimize @a cfg target conds df indices (buildCartTree @a cfg target df))+ 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) -- | Convert a 'ProbTree' into one @Expr Double@ per class.-probExprs :: forall a. (Columnable a, Ord a) => ProbTree a -> M.Map a (Expr Double)+probExprs ::+ forall a. (Columnable a, Ord a) => ProbTree a -> M.Map a (Expr Double) probExprs tree = M.fromList [(c, classExpr c tree) | c <- nub (allClasses tree)] allClasses :: ProbTree a -> [a]
src/DataFrame/DecisionTree/Linear.hs view
@@ -1,9 +1,10 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} --- | Oblique split candidates: fit an L1-regularised logistic hyperplane to the--- care points (class-balanced) and convert it to a boolean condition, rejecting--- all-zero and degenerate (single-side) hyperplanes.+{- | Oblique split candidates: fit an L1-regularised logistic hyperplane to the+care points (class-balanced) and convert it to a boolean condition, rejecting+all-zero and degenerate (single-side) hyperplanes.+-} module DataFrame.DecisionTree.Linear ( bestLinearCandidate, fitLinearCandidate,@@ -15,7 +16,11 @@ ) where import DataFrame.DecisionTree.Numeric (NumExpr (..), numericCols)-import DataFrame.DecisionTree.Types (CarePoint (..), Direction (..), TreeConfig (..))+import DataFrame.DecisionTree.Types (+ CarePoint (..),+ Direction (..),+ TreeConfig (..),+ ) import DataFrame.Internal.Column (TypedColumn (..), toVector) import DataFrame.Internal.DataFrame (DataFrame) import DataFrame.Internal.Expression (Expr, getColumns)@@ -27,18 +32,22 @@ import qualified Data.Vector as V 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.-bestLinearCandidate :: TreeConfig -> DataFrame -> [CarePoint] -> Maybe (Expr Bool)+{- | Best oblique candidate, or 'Nothing' when the linear path is disabled or+there are too few care points to fit on.+-}+bestLinearCandidate ::+ TreeConfig -> DataFrame -> [CarePoint] -> Maybe (Expr Bool) bestLinearCandidate cfg df carePoints | not (useLinearSolver cfg) = Nothing | length carePoints < minCarePointsForLinear cfg = Nothing | otherwise = fitLinearCandidate cfg 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)+{- | 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@@ -46,7 +55,8 @@ materializedFeatures :: DataFrame -> [CarePoint] -> [(T.Text, VU.Vector Double)] materializedFeatures df carePoints = mapMaybe (materializeFeatureForCare df carePoints) (numericCols df) -linearFromFeatures :: TreeConfig -> [CarePoint] -> [(T.Text, VU.Vector Double)] -> Maybe (Expr Bool)+linearFromFeatures ::+ TreeConfig -> [CarePoint] -> [(T.Text, VU.Vector Double)] -> Maybe (Expr Bool) linearFromFeatures cfg carePoints mats | VU.all (== 0) weights = Nothing | degenerateHyperplane rows weights (LS.lmIntercept model) = Nothing@@ -54,14 +64,20 @@ where rows = careRowsFromFeatures (length carePoints) mats labels = careLabels carePoints- model = LS.fitL1Logistic (solverConfigFor cfg labels) rows labels (V.fromList (map fst mats))+ model =+ LS.fitL1Logistic+ (solverConfigFor cfg labels)+ rows+ labels+ (V.fromList (map fst mats)) weights = LS.lmWeights model solverConfigFor :: TreeConfig -> VU.Vector Double -> LS.SolverConfig solverConfigFor cfg labels = (linearSolverConfig cfg){LS.scSampleWeights = classBalancedWeights labels} --- | Class-balanced sklearn-form weights @w_i = N / (2 · N_class)@ (mean 1), or--- 'Nothing' in the degenerate one-class case (uniform weighting).+{- | Class-balanced sklearn-form weights @w_i = N / (2 · N_class)@ (mean 1), or+'Nothing' in the degenerate one-class case (uniform weighting).+-} classBalancedWeights :: VU.Vector Double -> Maybe (VU.Vector Double) classBalancedWeights labels | nPos > 0 && nNeg > 0 = Just (VU.generate nCare weightAt)@@ -74,18 +90,25 @@ | VU.unsafeIndex labels i > 0 = fromIntegral nCare / (2 * fromIntegral nPos) | otherwise = fromIntegral nCare / (2 * fromIntegral nNeg) --- | A hyperplane is degenerate when every care row scores on the same side of--- zero (equivalent to an invalid split, caught upstream).-degenerateHyperplane :: V.Vector (VU.Vector Double) -> VU.Vector Double -> Double -> Bool+{- | A hyperplane is degenerate when every care row scores on the same side of+zero (equivalent to an invalid split, caught upstream).+-}+degenerateHyperplane ::+ V.Vector (VU.Vector Double) -> VU.Vector Double -> Double -> Bool degenerateHyperplane rows weights bias = nCare > 0 && (VU.minimum scores > 0 || VU.maximum scores < 0) where nCare = V.length rows- scores = VU.generate nCare (\i -> VU.sum (VU.zipWith (*) weights (V.unsafeIndex rows i)) + bias)+ scores =+ VU.generate+ nCare+ (\i -> VU.sum (VU.zipWith (*) weights (V.unsafeIndex rows i)) + bias) --- | Per-care-point feature rows from materialized columns (each of length--- @nCare@, so indexing is in range).-careRowsFromFeatures :: Int -> [(T.Text, VU.Vector Double)] -> V.Vector (VU.Vector Double)+{- | Per-care-point feature rows from materialized columns (each of length+@nCare@, so indexing is in range).+-}+careRowsFromFeatures ::+ Int -> [(T.Text, VU.Vector Double)] -> V.Vector (VU.Vector Double) careRowsFromFeatures nCare mats = V.generate nCare (\i -> VU.generate nFeat (\j -> snd (matsVec V.! j) VU.! i)) where@@ -94,7 +117,8 @@ -- | Solver labels: @+1@ when 'GoLeft' is correct, @-1@ otherwise. careLabels :: [CarePoint] -> VU.Vector Double-careLabels carePoints = VU.fromList [if cpCorrectDir cp == GoLeft then 1.0 else -1.0 | cp <- carePoints]+careLabels carePoints =+ VU.fromList [if cpCorrectDir cp == GoLeft then 1.0 else -1.0 | cp <- carePoints] -- | First column referenced by an expression, or a placeholder when none. featName :: Expr b -> T.Text@@ -102,8 +126,9 @@ (c : _) -> c [] -> "<feat>" --- | Replace missing values with the mean of present ones; 'Nothing' when--- nothing is present so the caller can drop the feature.+{- | Replace missing values with the mean of present ones; 'Nothing' when+nothing is present so the caller can drop the feature.+-} imputeMean :: [Maybe Double] -> Maybe (VU.Vector Double) imputeMean careRaw = case catMaybes careRaw of [] -> Nothing@@ -116,14 +141,17 @@ Right (TColumn column) -> either (const Nothing) Just (toVector @Double column) _ -> Nothing -interpretMaybeDoubleVals :: DataFrame -> Expr (Maybe Double) -> Maybe (V.Vector (Maybe Double))+interpretMaybeDoubleVals ::+ DataFrame -> Expr (Maybe Double) -> Maybe (V.Vector (Maybe Double)) interpretMaybeDoubleVals df expr = case interpret @(Maybe Double) df expr of Right (TColumn column) -> either (const Nothing) Just (toVector @(Maybe Double) column) _ -> Nothing --- | Materialize a 'NumExpr' over the care rows; 'Nothing' on interpret failure--- or (nullable) when no care point has a present value, else mean-imputed.-materializeFeatureForCare :: DataFrame -> [CarePoint] -> NumExpr -> Maybe (T.Text, VU.Vector Double)+{- | Materialize a 'NumExpr' over the care rows; 'Nothing' on interpret failure+or (nullable) when no care point has a present value, else mean-imputed.+-}+materializeFeatureForCare ::+ DataFrame -> [CarePoint] -> NumExpr -> Maybe (T.Text, VU.Vector Double) materializeFeatureForCare df carePoints (NDouble expr) = do vals <- interpretDoubleVals df expr Just (featName expr, VU.fromList [vals V.! cpIndex cp | cp <- carePoints])
+ src/DataFrame/DecisionTree/Model.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE UndecidableInstances #-}++{- | sklearn-style standalone tree estimators returning inspectable records+(depth, leaf count, per-feature split usage). 'fit' trains a classifier (from a+'TreeConfig') or a regressor (from a 'RegTreeConfig'); 'predict' is the compiled+tree expression, and the record exposes the raw 'Tree' too. The bare+'DataFrame.DecisionTree.Fit.fitDecisionTree' remains for callers that only want+the classifier @Expr@.+-}+module DataFrame.DecisionTree.Model (+ DecisionTreeClassifier (..),+ DecisionTreeRegressor (..),+) where++import qualified Data.Map.Strict as M+import qualified Data.Text as T++import qualified Data.Vector as V++import DataFrame.DecisionTree.Cart (cartFeatures)+import DataFrame.DecisionTree.Fit (fitDecisionTree, treeToExpr)+import DataFrame.DecisionTree.Regression (RegTreeConfig, fitRegTreeOn)+import DataFrame.DecisionTree.Types (Tree (..), TreeConfig)+import DataFrame.Featurize.Internal (targetDoubles)+import DataFrame.Internal.Column (Columnable)+import DataFrame.Internal.Expression (Expr (..), getColumns)+import DataFrame.Model (Fit (..), Predict (..))++-- | A fitted classification tree with structural diagnostics.+data DecisionTreeClassifier a = DecisionTreeClassifier+ { dtcExpr :: !(Expr a)+ , dtcDepth :: !Int+ , dtcNLeaves :: !Int+ , dtcFeatureUsage :: !(M.Map T.Text Int)+ }+ deriving (Show)++-- | A fitted regression tree with structural diagnostics.+data DecisionTreeRegressor = DecisionTreeRegressor+ { dtrTree :: !(Tree Double)+ , dtrExpr :: !(Expr Double)+ , dtrDepth :: !Int+ , dtrNLeaves :: !Int+ , dtrFeatureUsage :: !(M.Map T.Text Int)+ }+ deriving (Show)++instance (Columnable a, Ord a) => Fit TreeConfig (Expr a) (DecisionTreeClassifier a) where+ fit cfg target df =+ DecisionTreeClassifier+ e+ (exprDepth e)+ (exprLeaves e)+ (usageCounts (exprUsage e))+ where+ e = fitDecisionTree cfg target df++instance Predict (DecisionTreeClassifier a) a where+ predict = dtcExpr++instance Fit RegTreeConfig (Expr Double) DecisionTreeRegressor where+ fit cfg target df =+ DecisionTreeRegressor+ t+ e+ (exprDepth e)+ (exprLeaves e)+ (usageCounts (exprUsage e))+ where+ t = case target of+ Col name ->+ fitRegTreeOn+ cfg+ (V.fromList (cartFeatures name df))+ (targetDoubles target df)+ Nothing+ _ ->+ error+ ("fit @DecisionTreeRegressor: target must be a column, got " ++ show target)+ e = treeToExpr t++instance Predict DecisionTreeRegressor Double where+ predict = dtrExpr++usageCounts :: [T.Text] -> M.Map T.Text Int+usageCounts = foldr (\c -> M.insertWith (+) c 1) M.empty++exprUsage :: Expr a -> [T.Text]+exprUsage (If c t e) = getColumns c ++ exprUsage t ++ exprUsage e+exprUsage _ = []++exprLeaves :: Expr a -> Int+exprLeaves (If _ t e) = exprLeaves t + exprLeaves e+exprLeaves _ = 1++exprDepth :: Expr a -> Int+exprDepth (If _ t e) = 1 + max (exprDepth t) (exprDepth e)+exprDepth _ = 0
src/DataFrame/DecisionTree/Numeric.hs view
@@ -5,10 +5,11 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} --- | Numeric split candidates: per-column Double expressions, arithmetic--- expansion, and threshold conditions. 'numericCondVecs' materializes the--- pool with a single interpret per distinct expression, deriving every--- threshold/operator truth vector by direct comparison.+{- | Numeric split candidates: per-column Double expressions, arithmetic+expansion, and threshold conditions. 'numericCondVecs' materializes the+pool with a single interpret per distinct expression, deriving every+threshold/operator truth vector by direct comparison.+-} module DataFrame.DecisionTree.Numeric ( NumExpr (..), numExprCols,@@ -67,11 +68,29 @@ combineNumExprs (NDouble e1) (NDouble e2) = map NDouble [e1 .+ e2, e1 .- e2, e1 .* e2, safeDivD e1 e2] combineNumExprs (NDouble e1) (NMaybeDouble e2) =- map NMaybeDouble [e1 .+ e2, e1 .- e2, e1 .* e2, safeDivMaybe (F.fromMaybe False (e2 ./= F.lit (0 :: Double))) (e1 ./ e2)]+ map+ NMaybeDouble+ [ e1 .+ e2+ , e1 .- e2+ , e1 .* e2+ , safeDivMaybe (F.fromMaybe False (e2 ./= F.lit (0 :: Double))) (e1 ./ e2)+ ] combineNumExprs (NMaybeDouble e1) (NDouble e2) =- map NMaybeDouble [e1 .+ e2, e1 .- e2, e1 .* e2, safeDivMaybe (e2 ./= F.lit (0 :: Double)) (e1 ./ e2)]+ map+ NMaybeDouble+ [ e1 .+ e2+ , e1 .- e2+ , e1 .* e2+ , safeDivMaybe (e2 ./= F.lit (0 :: Double)) (e1 ./ e2)+ ] combineNumExprs (NMaybeDouble e1) (NMaybeDouble e2) =- map NMaybeDouble [e1 .+ e2, e1 .- e2, e1 .* e2, safeDivMaybe (F.fromMaybe False (e2 ./= F.lit (0 :: Double))) (e1 ./ e2)]+ map+ NMaybeDouble+ [ e1 .+ e2+ , e1 .- e2+ , e1 .* e2+ , safeDivMaybe (F.fromMaybe False (e2 ./= F.lit (0 :: Double))) (e1 ./ e2)+ ] numericConditions :: TreeConfig -> DataFrame -> [Expr Bool] numericConditions = generateNumericConds@@ -92,10 +111,14 @@ condsFromExpr :: NumExpr -> Double -> [Expr Bool] condsFromExpr (NDouble e) t = [e .<= F.lit t, e .>= F.lit t, e .< F.lit t, e .> F.lit t]-condsFromExpr (NMaybeDouble e) t = map (F.fromMaybe False) [e .<= F.lit t, e .>= F.lit t, e .< F.lit t, e .> F.lit t]+condsFromExpr (NMaybeDouble e) t =+ map+ (F.fromMaybe False)+ [e .<= F.lit t, e .>= F.lit t, e .< F.lit t, e .> F.lit t] --- | Percentile thresholds for a value list: sort once, index each percentile.--- Shared by 'generateNumericConds' and 'numericCondVecs' for identical results.+{- | Percentile thresholds for a value list: sort once, index each percentile.+Shared by 'generateNumericConds' and 'numericCondVecs' for identical results.+-} percentilesOf :: [Int] -> [Double] -> [Double] percentilesOf ps valsList | n == 0 = []@@ -109,15 +132,17 @@ Right (TColumn column) -> either (const Nothing) Just (toVector @Double column) _ -> Nothing -interpretMaybeDoubleCol :: DataFrame -> Expr (Maybe Double) -> Maybe (V.Vector (Maybe Double))+interpretMaybeDoubleCol ::+ DataFrame -> Expr (Maybe Double) -> Maybe (V.Vector (Maybe Double)) interpretMaybeDoubleCol df e = case interpret @(Maybe Double) df e of Right (TColumn column) -> either (const Nothing) Just (toVector @(Maybe Double) column) _ -> Nothing --- | Materialize the numeric pool with one interpret per distinct expression,--- deriving each threshold/operator truth vector by direct comparison.--- Byte-identical to materializing 'numericConditions' one at a time, but--- avoids re-interpreting each LHS per threshold and operator.+{- | Materialize the numeric pool with one interpret per distinct expression,+deriving each threshold/operator truth vector by direct comparison.+Byte-identical to materializing 'numericConditions' one at a time, but+avoids re-interpreting each LHS per threshold and operator.+-} numericCondVecs :: TreeConfig -> DataFrame -> DataFrame -> [CondVec] numericCondVecs cfg dfGen df = concatMap forExpr (numericExprsWithTerms (synthConfig cfg) dfGen) where@@ -139,12 +164,14 @@ where gen p = VU.generate n (\i -> p (vals V.! i)) -condsForMaybe :: TreeConfig -> Expr (Maybe Double) -> V.Vector (Maybe Double) -> [CondVec]+condsForMaybe ::+ TreeConfig -> Expr (Maybe Double) -> V.Vector (Maybe Double) -> [CondVec] condsForMaybe cfg e mvals = concatMap (maybeCondsAt e mvals (V.length mvals)) ts where ts = percentilesOf (percentiles cfg) (map (fromMaybe 0) (V.toList mvals)) -maybeCondsAt :: Expr (Maybe Double) -> V.Vector (Maybe Double) -> Int -> Double -> [CondVec]+maybeCondsAt ::+ Expr (Maybe Double) -> V.Vector (Maybe Double) -> Int -> Double -> [CondVec] maybeCondsAt e mvals n t = [ CondVec (F.fromMaybe False (e .<= F.lit t)) (gen (<= t)) , CondVec (F.fromMaybe False (e .>= F.lit t)) (gen (>= t))@@ -154,13 +181,15 @@ where gen p = VU.generate n (\i -> maybe False p (mvals V.! i)) --- | Arithmetic candidate expansion, generated already-deduped: each round--- combines @frontier × base@ and admits only normalized-novel candidates.--- Produces @base@ plus @maxExprDepth-1@ combination rounds.+{- | Arithmetic candidate expansion, generated already-deduped: each round+combines @frontier × base@ and admits only normalized-novel candidates.+Produces @base@ plus @maxExprDepth-1@ combination rounds.+-} numericExprsWithTerms :: SynthConfig -> DataFrame -> [NumExpr] numericExprsWithTerms cfg df | not (enableArithOps cfg) = base- | otherwise = base ++ expandRounds cfg base (max 0 (maxExprDepth cfg - 1)) base seen0+ | otherwise =+ base ++ expandRounds cfg base (max 0 (maxExprDepth cfg - 1)) base seen0 where base = numericCols df seen0 = Set.fromList (map keyNum base)@@ -177,9 +206,16 @@ roundProducts :: SynthConfig -> [NumExpr] -> [NumExpr] -> [NumExpr] roundProducts cfg frontier base =- [c | e1 <- frontier, e2 <- base, not (numExprEq e1 e2), not (isDisallowed cfg e1 e2), c <- combineNumExprs e1 e2]+ [ c+ | e1 <- frontier+ , e2 <- base+ , not (numExprEq e1 e2)+ , not (isDisallowed cfg e1 e2)+ , c <- combineNumExprs e1 e2+ ] -expandRounds :: SynthConfig -> [NumExpr] -> Int -> [NumExpr] -> Set.Set String -> [NumExpr]+expandRounds ::+ SynthConfig -> [NumExpr] -> Int -> [NumExpr] -> Set.Set String -> [NumExpr] expandRounds _ _ 0 _ _ = [] expandRounds cfg base d frontier seen | null admitted = []
src/DataFrame/DecisionTree/Pool.hs view
@@ -1,9 +1,10 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} --- | Candidate-pool scoring and boolean expansion: penalized scoring, diverse--- top-K selection, AND/OR saturation, and structural/truth-vector dedup. The--- per-node scoring scans run in parallel chunks.+{- | Candidate-pool scoring and boolean expansion: penalized scoring, diverse+top-K selection, AND/OR saturation, and structural/truth-vector dedup. The+per-node scoring scans run in parallel chunks.+-} module DataFrame.DecisionTree.Pool ( evalWithPenaltyVec, primaryColExpr,@@ -21,9 +22,25 @@ nubByExpr, ) where -import DataFrame.DecisionTree.CondVec (CondVec (..), combineAndVec, combineOrVec, countErrorsByVec)-import DataFrame.DecisionTree.Types (CarePoint, SynthConfig (..), TreeConfig (..))-import DataFrame.Internal.Expression (Expr, compareExpr, eSize, eqExpr, getColumns, normalize)+import DataFrame.DecisionTree.CondVec (+ CondVec (..),+ combineAndVec,+ combineOrVec,+ countErrorsByVec,+ )+import DataFrame.DecisionTree.Types (+ CarePoint,+ SynthConfig (..),+ TreeConfig (..),+ )+import DataFrame.Internal.Expression (+ Expr,+ compareExpr,+ eSize,+ eqExpr,+ getColumns,+ normalize,+ ) import Control.Parallel.Strategies (parListChunk, rdeepseq, using) import Data.Function (on)@@ -33,16 +50,18 @@ import qualified Data.Text as T import qualified Data.Vector.Unboxed as VU --- | Penalized score of a candidate: care-point errors plus a complexity--- penalty, tie-broken by expression size.+{- | Penalized score of a candidate: care-point errors plus a complexity+penalty, tie-broken by expression size.+-} evalWithPenaltyVec :: TreeConfig -> [CarePoint] -> CondVec -> (Int, Int) evalWithPenaltyVec cfg carePoints cv = (countErrorsByVec (cvVec cv) carePoints + penalty, sz) where sz = eSize (cvExpr cv) penalty = floor (complexityPenalty (synthConfig cfg) * fromIntegral sz) --- | First referenced column of a condition (a sentinel for literal-only ones),--- used by 'takeDiverse' to enforce per-column diversity.+{- | First referenced column of a condition (a sentinel for literal-only ones),+used by 'takeDiverse' to enforce per-column diversity.+-} primaryColExpr :: Expr Bool -> T.Text primaryColExpr e = case getColumns e of [] -> "<noncol>"@@ -51,8 +70,9 @@ primaryColCV :: CondVec -> T.Text primaryColCV = primaryColExpr . cvExpr --- | Keep the first @k@ of an already-sorted list, admitting at most @quota@ per--- primary column (@Nothing@ disables the per-column cap).+{- | Keep the first @k@ of an already-sorted list, admitting at most @quota@ per+primary column (@Nothing@ disables the per-column cap).+-} takeDiverse :: Int -> Maybe Int -> (a -> T.Text) -> [a] -> [a] takeDiverse k Nothing _ = take k takeDiverse k (Just quota) primary = go M.empty 0@@ -65,36 +85,51 @@ where !col = primary x --- | Chunk size for the parallel per-node candidate scans; tuned by an -N--- sweep, not correctness-affecting.+{- | Chunk size for the parallel per-node candidate scans; tuned by an -N+sweep, not correctness-affecting.+-} candidateParChunk :: Int candidateParChunk = 64 --- | Decorate candidates with their penalty in parallel chunks, forcing only--- the @(Int, Int)@ key so the order (hence later sorts/minima) is preserved.+{- | Decorate candidates with their penalty in parallel chunks, forcing only+the @(Int, Int)@ key so the order (hence later sorts/minima) is preserved.+-} decorate :: (CondVec -> (Int, Int)) -> [CondVec] -> [((Int, Int), CondVec)] decorate penaltyCV xs = zip (map penaltyCV xs `using` parListChunk candidateParChunk rdeepseq) xs -- | The diverse top-@expressionPairs@ valid candidates by penalty. sortedTopK :: TreeConfig -> (CondVec -> (Int, Int)) -> [CondVec] -> [CondVec] sortedTopK cfg penaltyCV validCondVecs =- map snd (takeDiverse (expressionPairs cfg) (perColumnQuota (synthConfig cfg)) (primaryColCV . snd) sorted)+ map+ snd+ ( takeDiverse+ (expressionPairs cfg)+ (perColumnQuota (synthConfig cfg))+ (primaryColCV . snd)+ sorted+ ) where sorted = sortBy (compare `on` fst) (decorate penaltyCV validCondVecs) -- | Lowest-penalty candidate after boolean saturation of the diverse top-K.-bestDiscreteCandidate :: TreeConfig -> (CondVec -> (Int, Int)) -> [CondVec] -> Maybe CondVec+bestDiscreteCandidate ::+ TreeConfig -> (CondVec -> (Int, Int)) -> [CondVec] -> Maybe CondVec bestDiscreteCandidate _ _ [] = Nothing bestDiscreteCandidate cfg penaltyCV validCondVecs =- case saturateCandidates Structural (boolExpansion (synthConfig cfg)) (sortedTopK cfg penaltyCV validCondVecs) of+ case saturateCandidates+ Structural+ (boolExpansion (synthConfig cfg))+ (sortedTopK cfg penaltyCV validCondVecs) of [] -> Nothing xs -> Just (snd (minimumBy (compare `on` fst) (decorate penaltyCV xs))) --- | AND/OR expansion of cached conditions to depth @maxDepth@ (each--- combination is a single vector op, not an interpret).+{- | AND/OR expansion of cached conditions to depth @maxDepth@ (each+combination is a single vector op, not an interpret).+-} boolExprsVec :: [CondVec] -> [CondVec] -> Int -> Int -> [CondVec] boolExprsVec baseExprs prevExprs depth maxDepth- | depth == 0 = baseExprs ++ boolExprsVec baseExprs prevExprs (depth + 1) maxDepth+ | depth == 0 =+ baseExprs ++ boolExprsVec baseExprs prevExprs (depth + 1) maxDepth | depth >= maxDepth = [] | otherwise = combined ++ boolExprsVec baseExprs combined (depth + 1) maxDepth where@@ -103,27 +138,38 @@ data DedupMode = Structural | TruthVector deriving (Eq, Show) --- | Saturate the pool with AND/OR combinations, deduplicating structurally--- (byte-identical, first occurrence kept) or by truth vector (opt-in).+{- | Saturate the pool with AND/OR combinations, deduplicating structurally+(byte-identical, first occurrence kept) or by truth vector (opt-in).+-} saturateCandidates :: DedupMode -> Int -> [CondVec] -> [CondVec] saturateCandidates Structural maxDepth base = base' ++ go 1 base' seen0 where (base', seen0) = admitKeys Set.empty base go !depth frontier seen | depth >= maxDepth || null frontier = []- | otherwise = let (admitted, seen') = admitKeys seen (roundProducts frontier base) in admitted ++ go (depth + 1) admitted seen'+ | otherwise =+ let (admitted, seen') = admitKeys seen (roundProducts frontier base)+ in admitted ++ go (depth + 1) admitted seen' saturateCandidates TruthVector maxDepth base = M.elems (go 1 frontier0 reps0) where (reps0, frontier0) = admitVecs M.empty base go !depth frontier reps | depth >= maxDepth || null frontier = reps- | otherwise = let (reps', admitted) = admitVecs reps (roundProducts frontier base) in go (depth + 1) admitted reps'+ | otherwise =+ let (reps', admitted) = admitVecs reps (roundProducts frontier base)+ in go (depth + 1) admitted reps' --- | One combination round: @frontier × base@ via AND then OR, skipping--- self-pairs (mirrors 'boolExprsVec' for byte-identical structural output).+{- | One combination round: @frontier × base@ via AND then OR, skipping+self-pairs (mirrors 'boolExprsVec' for byte-identical structural output).+-} roundProducts :: [CondVec] -> [CondVec] -> [CondVec] roundProducts frontier base =- [c | e1 <- frontier, e2 <- base, not (eqExpr (cvExpr e1) (cvExpr e2)), c <- [combineAndVec e1 e2, combineOrVec e1 e2]]+ [ c+ | e1 <- frontier+ , e2 <- base+ , not (eqExpr (cvExpr e1) (cvExpr e2))+ , c <- [combineAndVec e1 e2, combineOrVec e1 e2]+ ] -- | Admit candidates with a not-yet-seen normalized form, preserving order. admitKeys :: Set.Set String -> [CondVec] -> ([CondVec], Set.Set String)@@ -137,9 +183,13 @@ structuralKey :: CondVec -> String structuralKey = show . normalize . cvExpr --- | Admit candidates by distinct truth vector, keeping the smallest-expression--- representative per vector.-admitVecs :: M.Map (VU.Vector Bool) CondVec -> [CondVec] -> (M.Map (VU.Vector Bool) CondVec, [CondVec])+{- | Admit candidates by distinct truth vector, keeping the smallest-expression+representative per vector.+-}+admitVecs ::+ M.Map (VU.Vector Bool) CondVec ->+ [CondVec] ->+ (M.Map (VU.Vector Bool) CondVec, [CondVec]) admitVecs = go [] where go acc reps [] = (reps, reverse acc)
src/DataFrame/DecisionTree/Predict.hs view
@@ -2,9 +2,10 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} --- | Prediction, care-point identification, node validity, and tree loss. The--- batched, cache-aware variants resolve each branch condition's truth vector--- once per call instead of once per row.+{- | Prediction, care-point identification, node validity, and tree loss. The+batched, cache-aware variants resolve each branch condition's truth vector+once per call instead of once per row.+-} module DataFrame.DecisionTree.Predict ( predictWithTree, predictManyWithTree,@@ -20,8 +21,17 @@ isValidAtNode, ) where -import DataFrame.DecisionTree.CondVec (CondCache, countErrorsByVec, lookupCondVec)-import DataFrame.DecisionTree.Types (CarePoint (..), Direction (..), Tree (..), TreeConfig (..))+import DataFrame.DecisionTree.CondVec (+ CondCache,+ countErrorsByVec,+ lookupCondVec,+ )+import DataFrame.DecisionTree.Types (+ CarePoint (..),+ Direction (..),+ Tree (..),+ TreeConfig (..),+ ) import DataFrame.Internal.Column (Columnable, TypedColumn (..), toVector) import DataFrame.Internal.DataFrame (DataFrame) import DataFrame.Internal.Expression (Expr (..))@@ -37,21 +47,24 @@ import qualified Data.Vector.Mutable as VM import qualified Data.Vector.Unboxed as VU --- | A condition's truth vector over the DataFrame, or 'Nothing' on a--- type/interpret failure (callers default such rows to the left child).+{- | A condition's truth vector over the DataFrame, or 'Nothing' on a+type/interpret failure (callers default such rows to the left child).+-} branchBool :: DataFrame -> Expr Bool -> Maybe (VU.Vector Bool) branchBool df cond = case interpret @Bool df cond of Right (TColumn column) -> either (const Nothing) Just (toVector @Bool @VU.Vector column) _ -> Nothing -- | The target column as a label vector, or 'Nothing' on failure.-interpretLabelCol :: forall a. (Columnable a) => DataFrame -> T.Text -> Maybe (V.Vector a)+interpretLabelCol ::+ forall a. (Columnable a) => DataFrame -> T.Text -> Maybe (V.Vector a) interpretLabelCol df target = case interpret @a df (Col target) of Right (TColumn column) -> either (const Nothing) Just (toVector @a column) _ -> Nothing -- | Predict the label for a single row by walking a fixed tree (@True@ → left).-predictWithTree :: forall a. (Columnable a) => T.Text -> DataFrame -> Int -> Tree a -> a+predictWithTree ::+ forall a. (Columnable a) => T.Text -> DataFrame -> Int -> Tree a -> a predictWithTree _ _ _ (Leaf v) = v predictWithTree target df idx (Branch cond left right) = predictWithTree @a target df idx (childFor cond left right idx df)@@ -61,12 +74,16 @@ Nothing -> left Just boolVals -> if boolVals VU.! idx then left else right -predictManyWithTree :: forall a. (Columnable a) => Tree a -> DataFrame -> V.Vector Int -> V.Vector a+predictManyWithTree ::+ forall a. (Columnable a) => Tree a -> DataFrame -> V.Vector Int -> V.Vector a predictManyWithTree = predictManyWithTreeCached @a M.empty --- | 'predictManyWithTree' resolving each branch condition through a 'CondCache'.--- Each condition is read at most once per call rather than once per row.-predictManyWithTreeCached :: forall a. (Columnable a) => CondCache -> Tree a -> DataFrame -> V.Vector Int -> V.Vector a+{- | 'predictManyWithTree' resolving each branch condition through a 'CondCache'.+Each condition is read at most once per call rather than once per row.+-}+predictManyWithTreeCached ::+ forall a.+ (Columnable a) => CondCache -> Tree a -> DataFrame -> V.Vector Int -> V.Vector a predictManyWithTreeCached cache tree df indices = V.create $ do mv <- VM.new (V.length indices) fill mv (V.zip (V.enumFromN 0 (V.length indices)) indices) tree@@ -78,15 +95,33 @@ Nothing -> fill mv prs left Just boolVals -> fillSplit mv (V.partition (\(_, i) -> boolVals VU.! i) prs) left right - fillSplit :: VM.MVector s a -> (V.Vector (Int, Int), V.Vector (Int, Int)) -> Tree a -> Tree a -> ST s ()+ fillSplit ::+ VM.MVector s a ->+ (V.Vector (Int, Int), V.Vector (Int, Int)) ->+ Tree a ->+ Tree a ->+ ST s () fillSplit mv (leftPrs, rightPrs) left right = fill mv leftPrs left >> fill mv rightPrs right -identifyCarePoints :: forall a. (Columnable a) => T.Text -> DataFrame -> V.Vector Int -> Tree a -> Tree a -> [CarePoint]+identifyCarePoints ::+ forall a.+ (Columnable a) =>+ T.Text -> DataFrame -> V.Vector Int -> Tree a -> Tree a -> [CarePoint] identifyCarePoints = identifyCarePointsCached @a M.empty --- | Rows the parent must route to a specific child for the (fixed) subtrees to--- classify correctly; a 'CondCache' avoids re-interpreting subtree conditions.-identifyCarePointsCached :: forall a. (Columnable a) => CondCache -> T.Text -> DataFrame -> V.Vector Int -> Tree a -> Tree a -> [CarePoint]+{- | Rows the parent must route to a specific child for the (fixed) subtrees to+classify correctly; a 'CondCache' avoids re-interpreting subtree conditions.+-}+identifyCarePointsCached ::+ forall a.+ (Columnable a) =>+ CondCache ->+ T.Text ->+ DataFrame ->+ V.Vector Int ->+ Tree a ->+ Tree a ->+ [CarePoint] identifyCarePointsCached cache target df indices leftTree rightTree = maybe [] carePoints (interpretLabelCol @a df target) where@@ -94,7 +129,9 @@ rightPreds = predictManyWithTreeCached cache rightTree df indices carePoints targetVals = V.toList (V.imapMaybe (checkPoint targetVals leftPreds rightPreds) indices) -checkPoint :: (Eq a) => V.Vector a -> V.Vector a -> V.Vector a -> Int -> Int -> Maybe CarePoint+checkPoint ::+ (Eq a) =>+ V.Vector a -> V.Vector a -> V.Vector a -> Int -> Int -> Maybe CarePoint checkPoint targetVals leftPreds rightPreds k idx = case (leftPreds V.! k == trueLabel, rightPreds V.! k == trueLabel) of (True, False) -> Just (CarePoint idx GoLeft)@@ -108,12 +145,19 @@ countCarePointErrors cond df carePoints = maybe (length carePoints) (`countErrorsByVec` carePoints) (branchBool df cond) -partitionIndices :: Expr Bool -> DataFrame -> V.Vector Int -> (V.Vector Int, V.Vector Int)+partitionIndices ::+ Expr Bool -> DataFrame -> V.Vector Int -> (V.Vector Int, V.Vector Int) partitionIndices = partitionIndicesCached M.empty --- | 'partitionIndices' resolving the condition through a 'CondCache'; a miss--- routes every index left (matching the uncached fallback).-partitionIndicesCached :: CondCache -> Expr Bool -> DataFrame -> V.Vector Int -> (V.Vector Int, V.Vector Int)+{- | 'partitionIndices' resolving the condition through a 'CondCache'; a miss+routes every index left (matching the uncached fallback).+-}+partitionIndicesCached ::+ CondCache ->+ Expr Bool ->+ DataFrame ->+ V.Vector Int ->+ (V.Vector Int, V.Vector Int) partitionIndicesCached cache cond df indices = case lookupCondVec cache df cond of Nothing -> (indices, V.empty) Just boolVals -> V.partition (boolVals VU.!) indices@@ -125,7 +169,8 @@ where (t, f) = partitionIndices c df indices -majorityValueFromIndices :: forall a. (Columnable a, Ord a) => T.Text -> DataFrame -> V.Vector Int -> a+majorityValueFromIndices ::+ forall a. (Columnable a, Ord a) => T.Text -> DataFrame -> V.Vector Int -> a majorityValueFromIndices target df indices = majorityOf (countLabels (labelColOrThrow @a df target) indices) labelColOrThrow :: forall a. (Columnable a) => DataFrame -> T.Text -> V.Vector a@@ -141,21 +186,31 @@ | M.null counts = error "Empty indices in majorityValueFromIndices" | otherwise = fst (maximumBy (compare `on` snd) (M.toList counts)) -computeTreeLoss :: forall a. (Columnable a) => T.Text -> DataFrame -> V.Vector Int -> Tree a -> Double+computeTreeLoss ::+ forall a.+ (Columnable a) => T.Text -> DataFrame -> V.Vector Int -> Tree a -> Double computeTreeLoss = computeTreeLossCached @a M.empty -- | 0/1 loss of a tree over @indices@, with a 'CondCache' for the predictions.-computeTreeLossCached :: forall a. (Columnable a) => CondCache -> T.Text -> DataFrame -> V.Vector Int -> Tree a -> Double+computeTreeLossCached ::+ forall a.+ (Columnable a) =>+ CondCache -> T.Text -> DataFrame -> V.Vector Int -> Tree a -> Double computeTreeLossCached cache target df indices tree | V.null indices = 0- | otherwise = maybe 1.0 (treeLoss cache tree df indices) (interpretLabelCol @a df target)+ | otherwise =+ maybe 1.0 (treeLoss cache tree df indices) (interpretLabelCol @a df target) -treeLoss :: (Columnable a) => CondCache -> Tree a -> DataFrame -> V.Vector Int -> V.Vector a -> Double+treeLoss ::+ (Columnable a) =>+ CondCache -> Tree a -> DataFrame -> V.Vector Int -> V.Vector a -> Double treeLoss cache tree df indices targetVals =- fromIntegral (countMismatches targetVals indices preds) / fromIntegral (V.length indices)+ fromIntegral (countMismatches targetVals indices preds)+ / fromIntegral (V.length indices) where preds = predictManyWithTreeCached cache tree df indices countMismatches :: (Eq a) => V.Vector a -> V.Vector Int -> V.Vector a -> Int countMismatches targetVals indices preds =- V.length (V.ifilter (\k _ -> targetVals V.! (indices V.! k) /= preds V.! k) preds)+ V.length+ (V.ifilter (\k _ -> targetVals V.! (indices V.! k) /= preds V.! k) preds)
src/DataFrame/DecisionTree/Prune.hs view
@@ -2,9 +2,10 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE ScopedTypeVariables #-} --- | Post-convergence simplification of a fitted tree and its expression form:--- drop branches forced by path-condition entailment, collapse identical--- siblings, and fold redundant nested conditionals.+{- | Post-convergence simplification of a fitted tree and its expression form:+drop branches forced by path-condition entailment, collapse identical+siblings, and fold redundant nested conditionals.+-} module DataFrame.DecisionTree.Prune ( pruneDead, treeEq,@@ -16,9 +17,10 @@ import DataFrame.Internal.Expression (Expr (..), eqExpr) import DataFrame.Internal.Simplify (PredFact, entails, factFalse, factTrue) --- | Drop branches whose test is forced by the path conditions reaching them,--- and collapse @Branch c t t@ to @t@. Sound for the decidable threshold subset;--- other tests are left untouched.+{- | Drop branches whose test is forced by the path conditions reaching them,+and collapse @Branch c t t@ to @t@. Sound for the decidable threshold subset;+other tests are left untouched.+-} pruneDead :: forall a. (Columnable a) => Tree a -> Tree a pruneDead = go [] where@@ -27,7 +29,11 @@ go facts (Branch cond left right) = case entails facts cond of Just True -> go facts left Just False -> go facts right- Nothing -> reconcile cond (go (addFact (factTrue cond) facts) left) (go (addFact (factFalse cond) facts) right)+ Nothing ->+ reconcile+ cond+ (go (addFact (factTrue cond) facts) left)+ (go (addFact (factFalse cond) facts) right) reconcile :: (Columnable a) => Expr Bool -> Tree a -> Tree a -> Tree a reconcile cond left right@@ -43,8 +49,9 @@ treeEq (Branch c1 l1 r1) (Branch c2 l2 r2) = eqExpr c1 c2 && treeEq l1 l2 && treeEq r1 r2 treeEq _ _ = False --- | Recursively fold @If@ expressions whose branches coincide or nest the same--- condition; leave other expressions structurally unchanged.+{- | Recursively fold @If@ expressions whose branches coincide or nest the same+condition; leave other expressions structurally unchanged.+-} pruneExpr :: forall a. (Columnable a) => Expr a -> Expr a pruneExpr (If cond t0 f0) = collapseIf cond (pruneExpr t0) (pruneExpr f0) pruneExpr (Unary op e) = Unary op (pruneExpr e)
+ src/DataFrame/DecisionTree/Regression.hs view
@@ -0,0 +1,144 @@+{-# 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)
src/DataFrame/DecisionTree/Tao.hs view
@@ -3,9 +3,10 @@ {-# 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.+{- | 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,@@ -17,7 +18,11 @@ import DataFrame.DecisionTree.CondVec import DataFrame.DecisionTree.Linear (bestLinearCandidate)-import DataFrame.DecisionTree.Pool (bestDiscreteCandidate, candidateParChunk, evalWithPenaltyVec)+import DataFrame.DecisionTree.Pool (+ bestDiscreteCandidate,+ candidateParChunk,+ evalWithPenaltyVec,+ ) import DataFrame.DecisionTree.Predict import DataFrame.DecisionTree.Prune (pruneDead) import DataFrame.DecisionTree.Types@@ -34,8 +39,9 @@ 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).+{- | The constant per-fit context threaded through the node-optimization+recursion (the cache is rebuilt each iteration).+-} data TaoEnv = TaoEnv { teCache :: !CondCache , teCfg :: !TreeConfig@@ -45,13 +51,32 @@ } -- | 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 ::+ 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+{- | 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@@ -67,32 +92,63 @@ 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 ::+ 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 ::+ 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]+ 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 ::+ 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 ::+ 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+{- | 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@@ -100,15 +156,18 @@ 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.+{- | 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+{- | 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@@ -117,7 +176,10 @@ 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 ::+ 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@@ -126,43 +188,79 @@ (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+{- | 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+ | 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+ 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+ 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.+{- | 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+ 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+ 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]+{- | 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])+ 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)
src/DataFrame/DecisionTree/Types.hs view
@@ -5,8 +5,9 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} --- | Shared types, configuration and ordering machinery for the decision-tree--- learner. Imported by every other @DataFrame.DecisionTree.*@ module.+{- | Shared types, configuration and ordering machinery for the decision-tree+learner. Imported by every other @DataFrame.DecisionTree.*@ module.+-} module DataFrame.DecisionTree.Types ( Tree (..), treeDepth,@@ -38,8 +39,9 @@ import System.IO.Unsafe (unsafePerformIO) import Type.Reflection (SomeTypeRep (..), typeRep) --- | A fitted tree: a leaf value, or an internal node testing a boolean--- expression with @True@ routing left.+{- | A fitted tree: a leaf value, or an internal node testing a boolean+expression with @True@ routing left.+-} data Tree a = Leaf !a | Branch !(Expr Bool) !(Tree a) !(Tree a)@@ -49,8 +51,9 @@ treeDepth (Leaf _) = 0 treeDepth (Branch _ l r) = 1 + max (treeDepth l) (treeDepth r) --- | A row the parent node must route to a specific child for the subtrees to--- classify it correctly (the TAO objective is the count of misroutes).+{- | A row the parent node must route to a specific child for the subtrees to+classify it correctly (the TAO objective is the count of misroutes).+-} data CarePoint = CarePoint { cpIndex :: !Int , cpCorrectDir :: !Direction@@ -121,8 +124,9 @@ , pureReplacementLinear = False } --- | Which column types support ordering for splits. Register a type with--- 'orderable' and combine with @<>@.+{- | Which column types support ordering for splits. Register a type with+'orderable' and combine with @<>@.+-} newtype ColumnOrdering = ColumnOrdering (M.Map SomeTypeRep OrdDict) instance Semigroup ColumnOrdering where@@ -164,8 +168,9 @@ data OrdDict where OrdDict :: (Columnable a, Ord a) => Proxy a -> OrdDict --- | Run @k@ with the @Ord a@ instance recovered from the ordering registry,--- or 'Nothing' when @a@ is not registered.+{- | Run @k@ with the @Ord a@ instance recovered from the ordering registry,+or 'Nothing' when @a@ is not registered.+-} withOrdFrom :: forall a r. (Columnable a) => ColumnOrdering -> ((Ord a) => r) -> Maybe r withOrdFrom (ColumnOrdering m) k = case M.lookup (SomeTypeRep (typeRep @a)) m of
+ src/DataFrame/Featurize/Internal.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Shared internal helpers used across the model fitters: turning a 'DataFrame'+plus a target/feature 'Expr' into the numeric matrices the algorithms consume,+and the common expression builders (affine score, arg-max/arg-min over named+scores) that every linear model and classifier would otherwise re-implement.+-}+module DataFrame.Featurize.Internal (+ -- * Supervised extraction+ featureNames,+ numericMatrix,+ targetDoubles,+ targetValues,++ -- * Unsupervised extraction+ Features (..),+ extractFeatures,+ columnExprName,+ materializeColumn,++ -- * Expression builders+ affineExpr,+ argMaxExpr,+ argMinExpr,+) where++import Control.Exception (throw)+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (Columnable)+import DataFrame.Internal.DataFrame (DataFrame, columnNames)+import DataFrame.Internal.Expression (Expr (..))+import DataFrame.LinearAlgebra (Matrix, transposeM)+import DataFrame.Operations.Core (columnAsDoubleVector, columnAsVector)+import DataFrame.Operators ((.&&.), (.*.), (.+.), (.<=.), (.>=.))++-- | Every column name except the supervised target's.+featureNames :: Expr a -> DataFrame -> [T.Text]+featureNames (Col target) df = filter (/= target) (columnNames df)+featureNames _ df = columnNames df++{- | The named columns as a row-major @n×d@ matrix of doubles (non-numeric+columns are coerced through 'columnAsDoubleVector'), paired with the names.+-}+numericMatrix :: [T.Text] -> DataFrame -> (V.Vector T.Text, Matrix)+numericMatrix names df = (V.fromList names, transposeM colMajor)+ where+ colMajor = V.fromList (map column names)+ column name = case columnAsDoubleVector (F.col @Double name) df of+ Right v -> v+ Left e -> throw e++-- | The target column as a vector of doubles.+targetDoubles :: Expr Double -> DataFrame -> VU.Vector Double+targetDoubles expr df = case columnAsDoubleVector expr df of+ Right v -> v+ Left e -> throw e++-- | The target column as a vector of its own type (for classifiers).+targetValues :: (Columnable a) => Expr a -> DataFrame -> V.Vector a+targetValues expr df = case columnAsVector expr df of+ Right v -> v+ Left e -> throw e++{- | The extracted feature columns of an unsupervised fit, in the shapes the+algorithms need: names, column-major vectors, the row-major matrix, and the+@(n, d)@ dimensions.+-}+data Features = Features+ { ftNames :: ![T.Text]+ , ftCols :: ![VU.Vector Double]+ , ftRows :: !Matrix+ , ftN :: !Int+ , ftD :: !Int+ }++-- | Extract the given feature columns once, in every shape the fitters use.+extractFeatures :: [Expr Double] -> DataFrame -> Features+extractFeatures features df = Features names cols rows n d+ where+ names = map columnExprName features+ cols = map (materializeColumn df) features+ n = if null cols then 0 else VU.length (head cols)+ d = length cols+ rows = V.generate n (\i -> VU.generate d (\j -> (cols !! j) VU.! i))++-- | 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)++-- | Interpret a @Col@ (or numeric) expression to a @Double@ vector.+materializeColumn :: DataFrame -> Expr Double -> VU.Vector Double+materializeColumn df e = case columnAsDoubleVector e df of+ Right v -> v+ Left err -> throw err++{- | An affine score @b + Σ wⱼ·colⱼ@ over named columns, dropping zero weights+(the shared core of linear/logistic/SVM margins).+-}+affineExpr :: Double -> [(Double, T.Text)] -> Expr Double+affineExpr b terms =+ foldr+ (.+.)+ (F.lit b)+ [F.lit w .*. (Col n :: Expr Double) | (w, n) <- terms, w /= 0]++{- | The class whose score is greatest, as a nested-@If@ expression; ties go to+the earlier class.+-}+argMaxExpr :: (Columnable a) => [(a, Expr Double)] -> Expr a+argMaxExpr = argExtreme (.>=.)++-- | The class whose score is smallest (e.g. nearest cluster by distance).+argMinExpr :: (Columnable a) => [(a, Expr Double)] -> Expr a+argMinExpr = argExtreme (.<=.)++argExtreme ::+ (Columnable a) =>+ (Expr Double -> Expr Double -> Expr Bool) -> [(a, Expr Double)] -> Expr a+argExtreme _ [] = error "argExtreme: no classes"+argExtreme _ [(c, _)] = Lit c+argExtreme cmp ((c, sc) : rest) =+ If+ (foldr ((\o acc -> cmp sc o .&&. acc) . snd) (F.lit True) rest)+ (Lit c)+ (argExtreme cmp rest)
+ src/DataFrame/GMM.hs view
@@ -0,0 +1,310 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}++{- | Gaussian mixture models fitted by EM. Full covariance by default (with a+diagonal option and an automatic fall-back when a covariance is not positive+definite), log-space responsibilities, and Cholesky-based densities for+stability. 'predict' is the hard (arg-max) component assignment; per-component+log-densities are available via 'gmmLogDensityExprs'.+-}+module DataFrame.GMM (+ CovType (..),+ GMMConfig (..),+ defaultGMMConfig,+ GMMModel (..),+ gmmLogDensityExprs,+ gmmBIC,+ gmmAIC,+) where++import Data.List (sortBy)+import qualified Data.Map.Strict as M+import Data.Ord (comparing)+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.Featurize.Internal (Features (..), argMaxExpr, extractFeatures)+import qualified DataFrame.Functions as F+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr (..))+import DataFrame.LinearAlgebra (Matrix, logSumExp)+import DataFrame.LinearAlgebra.Solve (backSubst, cholesky, forwardSubst)+import DataFrame.Model (Fit (..), Predict (..))+import DataFrame.Operators ((.*.), (.+.), (.-.))+import DataFrame.Random (mkGen, sampleIndices)++data CovType = FullCov | DiagCov+ deriving (Eq, Show)++data GMMConfig = GMMConfig+ { gmmK :: !Int+ , gmmCovType :: !CovType+ , gmmMaxIter :: !Int+ , gmmTol :: !Double+ , gmmRegCovar :: !Double+ , gmmSeed :: !Int+ }+ deriving (Eq, Show)++defaultGMMConfig :: GMMConfig+defaultGMMConfig =+ GMMConfig+ { gmmK = 2+ , gmmCovType = FullCov+ , gmmMaxIter = 100+ , gmmTol = 1.0e-3+ , gmmRegCovar = 1.0e-6+ , gmmSeed = 0+ }++-- | A fitted mixture. 'gmmCovariances' are the per-component covariance matrices.+data GMMModel = GMMModel+ { gmmWeights :: !(VU.Vector Double)+ , gmmMeans :: !(V.Vector (VU.Vector Double))+ , gmmCovariances :: !(V.Vector Matrix)+ , gmmConverged :: !Bool+ , gmmNIter :: !Int+ , gmmLogLikelihood :: !Double+ , gmmNObs :: !Int+ , gmmFeatureNames :: !(V.Vector T.Text)+ }+ deriving (Eq, Show)++instance Fit GMMConfig [Expr Double] GMMModel where+ fit = fitGMM++instance Predict GMMModel Int where+ predict = gmmAssignExpr++-- | Fit a Gaussian mixture over the given feature columns.+fitGMM :: GMMConfig -> [Expr Double] -> DataFrame -> GMMModel+fitGMM cfg features df = canonical finalModel+ where+ Features names _ rows n d = extractFeatures features df+ k = min (gmmK cfg) (max 1 n)+ reg = gmmRegCovar cfg+ (initIdx, _) = sampleIndices k n (mkGen (gmmSeed cfg))+ means0 = V.map (rows V.!) (V.convert initIdx)+ varDiag =+ VU.generate d $ \j ->+ let mu = sum [(rows V.! i) VU.! j | i <- [0 .. n - 1]] / fromIntegral (max 1 n)+ in ( sum [((rows V.! i) VU.! j - mu) ^ (2 :: Int) | i <- [0 .. n - 1]]+ / fromIntegral (max 1 n)+ )+ + reg+ cov0 = diagMatrix varDiag+ covs0 = V.replicate k cov0+ weights0 = VU.replicate k (1 / fromIntegral k)+ finalModel = em 0 weights0 means0 covs0 (-(1 / 0)) False+ em !iter weights means covs prevLL converged+ | iter >= gmmMaxIter cfg || converged =+ GMMModel weights means covs converged iter prevLL n (V.fromList names)+ | otherwise =+ let (logResp, ll) = eStep cfg rows weights means covs+ (weights', means', covs') = mStep cfg rows logResp d reg+ done = abs (ll - prevLL) < gmmTol cfg+ in em (iter + 1) weights' means' covs' ll done++-- | Per-component log-density expressions (log weight + Gaussian log pdf).+gmmLogDensityExprs :: GMMModel -> M.Map Int (Expr Double)+gmmLogDensityExprs m =+ M.fromList+ [ ( c+ , logDensityExpr+ (gmmWeights m VU.! c)+ (gmmMeans m V.! c)+ (gmmCovariances m V.! c)+ names+ )+ | c <- [0 .. V.length (gmmMeans m) - 1]+ ]+ where+ names = V.toList (gmmFeatureNames m)++-- | The hard-assignment expression: arg-max of component log-densities.+gmmAssignExpr :: GMMModel -> Expr Int+gmmAssignExpr m =+ argMaxExpr (M.toList (gmmLogDensityExprs m))++-- | Bayesian information criterion (lower is better).+gmmBIC :: GMMModel -> Double+gmmBIC m =+ negate (2 * gmmLogLikelihood m)+ + fromIntegral (nParams m) * log (fromIntegral (max 1 (gmmNObs m)))++-- | Akaike information criterion (lower is better).+gmmAIC :: GMMModel -> Double+gmmAIC m = negate (2 * gmmLogLikelihood m) + 2 * fromIntegral (nParams m)++nParams :: GMMModel -> Int+nParams m =+ let k = VU.length (gmmWeights m)+ d = if V.null (gmmMeans m) then 0 else VU.length (V.head (gmmMeans m))+ in (k - 1) + k * d + k * (d * (d + 1) `div` 2)++eStep ::+ GMMConfig ->+ Matrix ->+ VU.Vector Double ->+ V.Vector (VU.Vector Double) ->+ V.Vector Matrix ->+ (V.Vector (VU.Vector Double), Double)+eStep _ rows weights means covs = (logResp, totalLL)+ where+ k = VU.length weights+ comps =+ V.generate k $ \c ->+ ( log (max 1e-300 (weights VU.! c))+ , means V.! c+ , gaussianLogPdf (covs V.! c) (means V.! c)+ )+ perRow x =+ let lps = VU.generate k (\c -> let (lw, _, f) = comps V.! c in lw + f x)+ lse = logSumExp lps+ in (VU.map (subtract lse) lps, lse)+ results = V.map perRow rows+ logResp = V.map fst results+ totalLL = V.sum (V.map snd results)++mStep ::+ GMMConfig ->+ Matrix ->+ V.Vector (VU.Vector Double) ->+ Int ->+ Double ->+ (VU.Vector Double, V.Vector (VU.Vector Double), V.Vector Matrix)+mStep cfg rows logResp d reg = (weights, means, covs)+ where+ n = V.length rows+ k = if V.null logResp then 0 else VU.length (V.head logResp)+ resp = V.map (VU.map exp) logResp+ nk = VU.generate k (\c -> sum [resp V.! i VU.! c | i <- [0 .. n - 1]])+ weights = VU.map (/ fromIntegral (max 1 n)) nk+ means =+ V.generate k $ \c ->+ let s =+ foldr+ (VU.zipWith (+))+ (VU.replicate d 0)+ [VU.map (* (resp V.! i VU.! c)) (rows V.! i) | i <- [0 .. n - 1]]+ in VU.map (/ max 1e-12 (nk VU.! c)) s+ covs =+ V.generate k $ \c ->+ let mu = means V.! c+ acc = foldr addOuter (zeroMatrix d) [0 .. n - 1]+ addOuter i m =+ let diff = VU.zipWith (-) (rows V.! i) mu+ w = resp V.! i VU.! c+ in addScaledOuter w diff m+ scaled = scaleMatrix (1 / max 1e-12 (nk VU.! c)) acc+ regd = addDiagScalar reg scaled+ in case gmmCovType cfg of+ FullCov -> regd+ DiagCov -> diagOnly regd++gaussianLogPdf :: Matrix -> VU.Vector Double -> VU.Vector Double -> Double+gaussianLogPdf cov mu =+ case cholesky cov of+ Just l ->+ let logdet = 2 * sum [log ((l V.! i) VU.! i) | i <- [0 .. d - 1]]+ in \x ->+ let diff = VU.zipWith (-) x mu+ z = forwardSubst l diff+ quad = VU.sum (VU.map (^ (2 :: Int)) z)+ in negate 0.5 * (fromIntegral d * log (2 * pi) + logdet + quad)+ Nothing ->+ let var = VU.generate d (\i -> max 1e-12 ((cov V.! i) VU.! i))+ in \x ->+ negate 0.5+ * VU.sum+ ( VU.generate d $ \j ->+ let diff = x VU.! j - mu VU.! j+ in diff * diff / var VU.! j + log (2 * pi * var VU.! j)+ )+ where+ d = VU.length mu++logDensityExpr ::+ Double -> VU.Vector Double -> Matrix -> [T.Text] -> Expr Double+logDensityExpr weight mu cov names =+ case precisionAndLogdet cov of+ Just (prec, logdet) ->+ let constTerm =+ log (max 1e-300 weight)+ - 0.5 * (fromIntegral d * log (2 * pi) + logdet)+ quad =+ foldr (.+.) (F.lit 0) $+ [ F.lit (-(0.5 * prec V.! a VU.! b)) .*. (centered a .*. centered b)+ | a <- [0 .. d - 1]+ , b <- [0 .. d - 1]+ ]+ in F.lit constTerm .+. quad+ Nothing -> F.lit (log (max 1e-300 weight))+ where+ d = VU.length mu+ centered j = (Col (names !! j) :: Expr Double) .-. F.lit (mu VU.! j)++precisionAndLogdet :: Matrix -> Maybe (Matrix, Double)+precisionAndLogdet cov = do+ l <- cholesky cov+ let d = V.length cov+ logdet = 2 * sum [log ((l V.! i) VU.! i) | i <- [0 .. d - 1]]+ cols = [forwardThenBack l (unitVec d i) | i <- [0 .. d - 1]]+ prec =+ V.fromList+ [VU.fromList [cols !! j VU.! i | j <- [0 .. d - 1]] | i <- [0 .. d - 1]]+ pure (prec, logdet)++forwardThenBack :: Matrix -> VU.Vector Double -> VU.Vector Double+forwardThenBack l b = backSubst l (forwardSubst l b)++canonical :: GMMModel -> GMMModel+canonical m =+ let order =+ map snd $+ sortBy+ (comparing fst)+ [ (firstCoord (gmmMeans m V.! c), c)+ | c <- [0 .. V.length (gmmMeans m) - 1]+ ]+ firstCoord v = if VU.null v then 0 else VU.head v+ in m+ { gmmWeights = VU.fromList [gmmWeights m VU.! c | c <- order]+ , gmmMeans = V.fromList [gmmMeans m V.! c | c <- order]+ , gmmCovariances = V.fromList [gmmCovariances m V.! c | c <- order]+ }++diagMatrix :: VU.Vector Double -> Matrix+diagMatrix v =+ let d = VU.length v+ in V.generate d (\i -> VU.generate d (\j -> if i == j then v VU.! i else 0))++diagOnly :: Matrix -> Matrix+diagOnly m =+ let d = V.length m+ in V.generate+ d+ (\i -> VU.generate d (\j -> if i == j then (m V.! i) VU.! j else 0))++zeroMatrix :: Int -> Matrix+zeroMatrix d = V.replicate d (VU.replicate d 0)++scaleMatrix :: Double -> Matrix -> Matrix+scaleMatrix s = V.map (VU.map (* s))++addDiagScalar :: Double -> Matrix -> Matrix+addDiagScalar s = V.imap (\i row -> row VU.// [(i, row VU.! i + s)])++addScaledOuter :: Double -> VU.Vector Double -> Matrix -> Matrix+addScaledOuter w diff m =+ let d = VU.length diff+ in V.generate d $ \i ->+ VU.generate d $ \j ->+ (m V.! i) VU.! j + w * (diff VU.! i) * (diff VU.! j)++unitVec :: Int -> Int -> VU.Vector Double+unitVec d i = VU.generate d (\j -> if i == j then 1 else 0)
+ src/DataFrame/KMeans.hs view
@@ -0,0 +1,156 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}++{- | k-means clustering (Lloyd's algorithm with k-means++ seeding and multiple+restarts). 'fit' trains a 'KMeansModel' (inspectable centres); 'predict' is the+arg-min cluster assignment. Per-cluster distance features are available via+'kmeansDistanceExprs' / 'kmeansTransform'.+-}+module DataFrame.KMeans (+ KMeansConfig (..),+ defaultKMeansConfig,+ KMeansModel (..),+ kmeansDistanceExprs,+ kmeansTransform,+) where++import Data.List (minimumBy)+import Data.Ord (comparing)+import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.Featurize.Internal (Features (..), argMinExpr, extractFeatures)+import qualified DataFrame.Functions as F+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr (..), UExpr (..))+import DataFrame.LinearAlgebra (Matrix, nearestCenter, sqDist)+import DataFrame.Model (Fit (..), Predict (..))+import DataFrame.Operators ((.*.), (.+.), (.-.))+import DataFrame.Random (Gen, mkGen, nextDouble, nextIntR, splitGen)+import DataFrame.Transform (Transform (..))++data KMeansConfig = KMeansConfig+ { kmK :: !Int+ , kmNInit :: !Int+ , kmMaxIter :: !Int+ , kmTol :: !Double+ , kmSeed :: !Int+ }+ deriving (Eq, Show)++defaultKMeansConfig :: KMeansConfig+defaultKMeansConfig =+ KMeansConfig{kmK = 8, kmNInit = 10, kmMaxIter = 300, kmTol = 1.0e-4, kmSeed = 0}++-- | A fitted k-means model. 'kmCenters' are sklearn's @cluster_centers_@.+data KMeansModel = KMeansModel+ { kmCenters :: !(V.Vector (VU.Vector Double))+ , kmLabels :: !(VU.Vector Int)+ , kmInertia :: !Double+ , kmNIter :: !Int+ , kmFeatureNames :: !(V.Vector T.Text)+ }+ deriving (Eq, Show)++instance Fit KMeansConfig [Expr Double] KMeansModel where+ fit = fitKMeans++instance Predict KMeansModel Int where+ predict m = argMinExpr (zip [0 :: Int ..] (map snd (kmeansDistanceExprs m)))++-- | Fit k-means over the given feature columns.+fitKMeans :: KMeansConfig -> [Expr Double] -> DataFrame -> KMeansModel+fitKMeans cfg features df = best+ where+ Features names _ rows n _ = extractFeatures features df+ k = min (kmK cfg) (max 1 n)+ seeds = take (max 1 (kmNInit cfg)) (genSeeds (mkGen (kmSeed cfg)))+ runs = map (lloyd cfg k rows) seeds+ best =+ let (centers, labels, inertia, iters) =+ minimumBy (comparing (\(_, _, i, _) -> i)) runs+ in KMeansModel centers labels inertia iters (V.fromList names)++genSeeds :: Gen -> [Gen]+genSeeds g = let (g1, g2) = splitGen g in g1 : genSeeds g2++{- | One k-means run: k-means++ seeding then Lloyd iterations. Returns+@(centers, labels, inertia, nIter)@.+-}+lloyd ::+ KMeansConfig ->+ Int ->+ Matrix ->+ Gen ->+ (V.Vector (VU.Vector Double), VU.Vector Int, Double, Int)+lloyd cfg k rows g0+ | V.null rows = (V.empty, VU.empty, 0, 0)+ | otherwise = iterate' 0 initCenters+ where+ initCenters = kmeansPP k rows g0+ iterate' !iter centers =+ let labels = VU.generate (V.length rows) (\i -> fst (nearestCenter centers (rows V.! i)))+ newCenters = recompute centers labels+ shift = V.sum (V.zipWith sqDist centers newCenters)+ in if iter + 1 >= kmMaxIter cfg || shift <= kmTol cfg+ then (newCenters, labels, inertiaOf newCenters labels, iter + 1)+ else iterate' (iter + 1) newCenters+ recompute centers labels =+ V.generate k $ \c ->+ let members = [rows V.! i | i <- [0 .. V.length rows - 1], labels VU.! i == c]+ in if null members then centers V.! c else meanOf members+ inertiaOf centers labels =+ sum+ [ sqDist (rows V.! i) (centers V.! (labels VU.! i))+ | i <- [0 .. V.length rows - 1]+ ]++meanOf :: [VU.Vector Double] -> VU.Vector Double+meanOf vs =+ let d = VU.length (head vs)+ s = foldr (VU.zipWith (+)) (VU.replicate d 0) vs+ in VU.map (/ fromIntegral (length vs)) s++-- | k-means++ seeding: spread initial centres by squared-distance sampling.+kmeansPP :: Int -> Matrix -> Gen -> V.Vector (VU.Vector Double)+kmeansPP k rows g0 = V.fromList (reverse (pick [first] g1))+ where+ n = V.length rows+ (i0, g1) = nextIntR (0, n - 1) g0+ first = rows V.! i0+ pick chosen g+ | length chosen >= k = chosen+ | otherwise =+ let dists = VU.generate n (\i -> minimum [sqDist (rows V.! i) c | c <- chosen])+ (u, g') = nextDouble g+ idx = sampleCumulative dists (u * VU.sum dists)+ in pick (rows V.! idx : chosen) g'++sampleCumulative :: VU.Vector Double -> Double -> Int+sampleCumulative dists target = go 0 0+ where+ go i acc+ | i >= VU.length dists - 1 = VU.length dists - 1+ | acc + dists VU.! i >= target = i+ | otherwise = go (i + 1) (acc + dists VU.! i)++-- | Per-cluster squared-distance expressions, named @dist1@, @dist2@, …+kmeansDistanceExprs :: KMeansModel -> [(T.Text, Expr Double)]+kmeansDistanceExprs m =+ [ ("dist" <> T.pack (show (c + 1)), distExpr (kmCenters m V.! c))+ | c <- [0 .. V.length (kmCenters m) - 1]+ ]+ where+ names = V.toList (kmFeatureNames m)+ distExpr center =+ foldr (.+.) (F.lit 0) $+ [ let diff = (Col n :: Expr Double) .-. F.lit ci in diff .*. diff+ | (n, ci) <- zip names (VU.toList center)+ ]++-- | The per-cluster distance features as a composable fitted 'Transform'.+kmeansTransform :: KMeansModel -> Transform+kmeansTransform m = Transform [(n, UExpr e) | (n, e) <- kmeansDistanceExprs m]
+ src/DataFrame/LinearAlgebra.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE BangPatterns #-}++{- | Dependency-free dense linear algebra over row-major matrices, shared by the+models in @dataframe-learn@. Basic vector/matrix operations plus stability and+distance helpers; solvers live in "DataFrame.LinearAlgebra.Solve" and+eigenproblems in "DataFrame.LinearAlgebra.Eigen".+-}+module DataFrame.LinearAlgebra (+ Matrix,+ dot,+ axpy,+ scaleV,+ matVec,+ tMatVec,+ gram,+ transposeM,+ identityM,+ logSumExp,+ sqDist,+ nearestCenter,+ epsNeighbors,+) where++import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++{- | Row-major dense matrix: an outer boxed vector of equal-length rows. An+@n×d@ matrix has @n@ rows of length @d@.+-}+type Matrix = V.Vector (VU.Vector Double)++-- | Inner product of two equal-length vectors.+dot :: VU.Vector Double -> VU.Vector Double -> Double+dot a b = VU.sum (VU.zipWith (*) a b)+{-# INLINE dot #-}++-- | @axpy a x y = a*x + y@.+axpy :: Double -> VU.Vector Double -> VU.Vector Double -> VU.Vector Double+axpy a = VU.zipWith (\xi yi -> a * xi + yi)+{-# INLINE axpy #-}++-- | Scalar-vector product.+scaleV :: Double -> VU.Vector Double -> VU.Vector Double+scaleV a = VU.map (* a)+{-# INLINE scaleV #-}++-- | @matVec A v@ for @A@ of shape @n×d@ and @v@ of length @d@; result length @n@.+matVec :: Matrix -> VU.Vector Double -> VU.Vector Double+matVec a v = VU.convert (V.map (`dot` v) a)++-- | @tMatVec A v = Aᵀ v@ for @A@ of shape @n×d@, @v@ of length @n@; result length @d@.+tMatVec :: Matrix -> VU.Vector Double -> VU.Vector Double+tMatVec a v+ | V.null a = VU.empty+ | otherwise = V.foldl' step (VU.replicate d 0) (V.zipWith (,) vBoxed a)+ where+ d = VU.length (V.head a)+ vBoxed = V.generate (V.length a) (v VU.!)+ step !acc (vi, row) = axpy vi row acc++-- | @gram A = Aᵀ A@, the @d×d@ symmetric matrix of column inner products.+gram :: Matrix -> Matrix+gram a+ | V.null a = V.empty+ | otherwise =+ V.generate d $ \i ->+ VU.generate d $ \j ->+ V.foldl' (\ !acc row -> acc + (row VU.! i) * (row VU.! j)) 0 a+ where+ d = VU.length (V.head a)++-- | Transpose an @n×d@ matrix to @d×n@.+transposeM :: Matrix -> Matrix+transposeM a+ | V.null a = V.empty+ | otherwise = V.generate d $ \j -> VU.generate n $ \i -> (a V.! i) VU.! j+ where+ n = V.length a+ d = VU.length (V.head a)++-- | @d×d@ identity matrix.+identityM :: Int -> Matrix+identityM d = V.generate d $ \i -> VU.generate d $ \j -> if i == j then 1 else 0++-- | Numerically stable @log Σ exp xᵢ@.+logSumExp :: VU.Vector Double -> Double+logSumExp xs+ | VU.null xs = negate (1 / 0)+ | otherwise = m + log (VU.sum (VU.map (\x -> exp (x - m)) xs))+ where+ m = VU.maximum xs++-- | Squared Euclidean distance.+sqDist :: VU.Vector Double -> VU.Vector Double -> Double+sqDist a b = VU.sum (VU.zipWith (\x y -> let z = x - y in z * z) a b)+{-# INLINE sqDist #-}++-- | Index of and squared distance to the nearest centre.+nearestCenter ::+ V.Vector (VU.Vector Double) -> VU.Vector Double -> (Int, Double)+nearestCenter centers p =+ V.ifoldl'+ ( \(!bi, !bd) i c ->+ let dd = sqDist c p in if dd < bd then (i, dd) else (bi, bd)+ )+ (-1, 1 / 0)+ centers++{- | Indices @j@ (excluding @i@) within squared radius @eps²@ of row @i@, by+brute force; @O(n d)@ per query.+-}+epsNeighbors :: Double -> Matrix -> Int -> VU.Vector Int+epsNeighbors eps rows i =+ VU.fromList+ [ j+ | j <- [0 .. n - 1]+ , j /= i+ , sqDist (rows V.! i) (rows V.! j) <= eps2+ ]+ where+ n = V.length rows+ eps2 = eps * eps
+ src/DataFrame/LinearAlgebra/Eigen.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE BangPatterns #-}++{- | Symmetric eigenproblems in pure Haskell: cyclic Jacobi for full+decomposition (PCA covariance, @m×m@ kernels) and power iteration for the+dominant eigenpair (FISTA step sizes). Deterministic, sign-canonicalised output.+-}+module DataFrame.LinearAlgebra.Eigen (+ jacobiEigenSym,+ powerIterTop,+) where++import Control.Monad (forM_, when)+import Control.Monad.ST (runST)+import Data.List (sortBy)+import Data.Ord (Down (..), comparing)+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import qualified Data.Vector.Unboxed.Mutable as VUM+import DataFrame.LinearAlgebra (Matrix, dot, matVec, scaleV)++{- | Cyclic Jacobi eigendecomposition of a symmetric matrix. Eigenvalues are+returned in descending order paired with eigenvectors as rows; each eigenvector+is sign-canonicalised (largest-magnitude component positive) so the output is+unique.+-}+jacobiEigenSym :: Matrix -> (VU.Vector Double, Matrix)+jacobiEigenSym a0+ | V.null a0 = (VU.empty, V.empty)+ | otherwise = runST $ do+ a <- VUM.new (d * d)+ forM_ [0 .. d - 1] $ \i ->+ forM_ [0 .. d - 1] $ \j ->+ VUM.write a (i * d + j) ((a0 V.! i) VU.! j)+ v <- VUM.replicate (d * d) 0+ forM_ [0 .. d - 1] $ \i -> VUM.write v (i * d + i) 1+ sweep a v 0+ afrozen <- VU.freeze a+ vmat <- VU.freeze v+ let diag = VU.generate d (\i -> afrozen VU.! (i * d + i))+ vecs =+ V.generate d $ \col ->+ VU.generate d $ \row -> vmat VU.! (row * d + col)+ paired =+ sortBy+ (comparing (Down . fst))+ (zip (VU.toList diag) (V.toList vecs))+ pure+ ( VU.fromList (map fst paired)+ , V.fromList (map (canonicalSign . snd) paired)+ )+ where+ d = V.length a0+ maxSweeps = 100+ tol = 1e-12+ sweep a v s+ | s >= maxSweeps = pure ()+ | otherwise = do+ off <- offNorm a+ when (off >= tol) $ do+ forM_ [0 .. d - 2] $ \p ->+ forM_ [p + 1 .. d - 1] $ \q -> rotate a v p q+ sweep a v (s + 1)+ offNorm a = go 0 0+ where+ go i !acc+ | i >= d = pure acc+ | otherwise = do+ r <- goRow i (i + 1) acc+ go (i + 1) r+ goRow i j !acc+ | j >= d = pure acc+ | otherwise = do+ x <- VUM.read a (i * d + j)+ goRow i (j + 1) (acc + x * x)+ rotate a v p q = do+ apq <- VUM.read a (p * d + q)+ when (abs apq > 1e-300) $ do+ app <- VUM.read a (p * d + p)+ aqq <- VUM.read a (q * d + q)+ let theta = (aqq - app) / (2 * apq)+ s' = if theta == 0 then 1 else signum theta+ t = s' / (abs theta + sqrt (theta * theta + 1))+ c = 1 / sqrt (t * t + 1)+ sn = t * c+ forM_ [0 .. d - 1] $ \i -> do+ aip <- VUM.read a (i * d + p)+ aiq <- VUM.read a (i * d + q)+ VUM.write a (i * d + p) (c * aip - sn * aiq)+ VUM.write a (i * d + q) (sn * aip + c * aiq)+ forM_ [0 .. d - 1] $ \j -> do+ apj <- VUM.read a (p * d + j)+ aqj <- VUM.read a (q * d + j)+ VUM.write a (p * d + j) (c * apj - sn * aqj)+ VUM.write a (q * d + j) (sn * apj + c * aqj)+ forM_ [0 .. d - 1] $ \i -> do+ vip <- VUM.read v (i * d + p)+ viq <- VUM.read v (i * d + q)+ VUM.write v (i * d + p) (c * vip - sn * viq)+ VUM.write v (i * d + q) (sn * vip + c * viq)++canonicalSign :: VU.Vector Double -> VU.Vector Double+canonicalSign vec =+ let idx = VU.maxIndex (VU.map abs vec)+ in if vec VU.! idx < 0 then VU.map negate vec else vec++{- | Dominant eigenvalue and eigenvector of a symmetric PSD matrix via power+iteration with a deterministic all-ones start.+-}+powerIterTop :: Int -> Matrix -> (Double, VU.Vector Double)+powerIterTop iters a+ | V.null a = (0, VU.empty)+ | otherwise = go iters (normalize (VU.replicate d 1))+ where+ d = V.length a+ normalize v =+ let nrm = sqrt (dot v v) in if nrm == 0 then v else scaleV (1 / nrm) v+ go 0 v = (dot v (matVec a v), v)+ go k v =+ let av = matVec a v+ nrm = sqrt (dot av av)+ in if nrm < 1e-300 then (0, v) else go (k - 1) (scaleV (1 / nrm) av)
+ src/DataFrame/LinearAlgebra/Solve.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE BangPatterns #-}++{- | Householder QR (for ordinary least squares) and Cholesky factorisation (for+ridge normal equations and Gaussian log-densities). Pure, deterministic, no+LAPACK; sound at the @d@ ≤ low-hundreds scales this library targets.+-}+module DataFrame.LinearAlgebra.Solve (+ qrLeastSquares,+ cholesky,+ choleskySolve,+ logDetFromChol,+ forwardSubst,+ backSubst,+) where++import Control.Monad (forM_)+import Control.Monad.ST (ST, runST)+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU+import qualified Data.Vector.Unboxed.Mutable as VUM+import DataFrame.LinearAlgebra (Matrix)++{- | Solve @min ‖A x − b‖₂@ for an @n×d@ matrix @A@ (@n ≥ d@) via Householder QR.+@Left cols@ reports rank deficiency (near-zero @R@ diagonal) with the offending+column indices; @Right x@ is the least-squares solution.+-}+qrLeastSquares :: Matrix -> VU.Vector Double -> Either [Int] (VU.Vector Double)+qrLeastSquares a b+ | V.null a = Right VU.empty+ | n < d = Left [0 .. d - 1]+ | otherwise = runST $ do+ mat <- VUM.new (n * d)+ forM_ [0 .. n - 1] $ \i ->+ forM_ [0 .. d - 1] $ \j ->+ VUM.write mat (j * n + i) ((a V.! i) VU.! j)+ rhs <- VUM.new n+ forM_ [0 .. n - 1] $ \i -> VUM.write rhs i (b VU.! i)+ deficient <- householder mat rhs n d+ if not (null deficient)+ then pure (Left deficient)+ else do+ x <- VUM.new d+ backSubstQR mat rhs n d x+ Right <$> VU.freeze x+ where+ n = V.length a+ d = VU.length (V.head a)++householder ::+ VUM.STVector s Double -> VUM.STVector s Double -> Int -> Int -> ST s [Int]+householder mat rhs n d = go 0 []+ where+ tol = 1e-10+ go k acc+ | k >= d = pure (reverse acc)+ | otherwise = do+ normSq <- sumSq k+ let alphaMag = sqrt normSq+ akk <- VUM.read mat (k * n + k)+ let alpha = if akk > 0 then negate alphaMag else alphaMag+ if alphaMag < tol+ then go (k + 1) (k : acc)+ else do+ VUM.write mat (k * n + k) (akk - alpha)+ vNormSq <- sumSq k+ if vNormSq < tol * tol+ then go (k + 1) acc+ else do+ forM_ [k + 1 .. d - 1] $ \j -> reflectColumn k j vNormSq+ reflectRhs k vNormSq+ VUM.write mat (k * n + k) alpha+ go (k + 1) acc+ sumSq k = foldRows k+ where+ foldRows i+ | i >= n = pure 0+ | otherwise = do+ x <- VUM.read mat (k * n + i)+ rest <- foldRows (i + 1)+ pure (x * x + rest)+ reflectColumn k j vNormSq = do+ dotv <- dotV k j k+ let beta = 2 * dotv / vNormSq+ forM_ [k .. n - 1] $ \i -> do+ vi <- VUM.read mat (k * n + i)+ aij <- VUM.read mat (j * n + i)+ VUM.write mat (j * n + i) (aij - beta * vi)+ reflectRhs k vNormSq = do+ dotv <- dotRhs k k+ let beta = 2 * dotv / vNormSq+ forM_ [k .. n - 1] $ \i -> do+ vi <- VUM.read mat (k * n + i)+ bi <- VUM.read rhs i+ VUM.write rhs i (bi - beta * vi)+ dotV k j i+ | i >= n = pure 0+ | otherwise = do+ vi <- VUM.read mat (k * n + i)+ aij <- VUM.read mat (j * n + i)+ rest <- dotV k j (i + 1)+ pure (vi * aij + rest)+ dotRhs k i+ | i >= n = pure 0+ | otherwise = do+ vi <- VUM.read mat (k * n + i)+ bi <- VUM.read rhs i+ rest <- dotRhs k (i + 1)+ pure (vi * bi + rest)++backSubstQR ::+ VUM.STVector s Double ->+ VUM.STVector s Double ->+ Int ->+ Int ->+ VUM.STVector s Double ->+ ST s ()+backSubstQR mat rhs n d x = forM_ [d - 1, d - 2 .. 0] $ \i -> do+ bi <- VUM.read rhs i+ s <- sumAbove i (i + 1) 0+ rii <- VUM.read mat (i * n + i)+ VUM.write x i ((bi - s) / rii)+ where+ sumAbove i j !acc+ | j >= d = pure acc+ | otherwise = do+ rij <- VUM.read mat (j * n + i)+ xj <- VUM.read x j+ sumAbove i (j + 1) (acc + rij * xj)++{- | Cholesky factor @L@ (lower-triangular, @A = L Lᵀ@) of a symmetric+positive-definite matrix, or 'Nothing' if a non-positive pivot is hit.+-}+cholesky :: Matrix -> Maybe Matrix+cholesky a+ | V.null a = Just V.empty+ | otherwise = runST $ do+ l <- VUM.replicate (d * d) 0+ ok <- buildL l+ if ok then Just <$> freezeLower l else pure Nothing+ where+ d = V.length a+ buildL l = go 0+ where+ go j+ | j >= d = pure True+ | otherwise = do+ s <- sumLk l j j (j - 1) 0+ let ajj = (a V.! j) VU.! j+ diag = ajj - s+ if diag <= 0+ then pure False+ else do+ let ljj = sqrt diag+ VUM.write l (j * d + j) ljj+ forM_ [j + 1 .. d - 1] $ \i -> do+ sij <- sumLk l i j (j - 1) 0+ let aij = (a V.! i) VU.! j+ VUM.write l (i * d + j) ((aij - sij) / ljj)+ go (j + 1)+ sumLk l i j k !acc+ | k < 0 = pure acc+ | otherwise = do+ lik <- VUM.read l (i * d + k)+ ljk <- VUM.read l (j * d + k)+ sumLk l i j (k - 1) (acc + lik * ljk)+ freezeLower l = do+ frozen <- VU.freeze l+ pure $ V.generate d $ \i -> VU.slice (i * d) d frozen++-- | Solve @L y = b@ for lower-triangular @L@.+forwardSubst :: Matrix -> VU.Vector Double -> VU.Vector Double+forwardSubst l b = runST $ do+ y <- VUM.new d+ forM_ [0 .. d - 1] $ \i -> do+ let row = l V.! i+ s <- sumKnown y row i 0 0+ VUM.write y i ((b VU.! i - s) / (row VU.! i))+ VU.freeze y+ where+ d = V.length l+ sumKnown y row i j !acc+ | j >= i = pure acc+ | otherwise = do+ yj <- VUM.read y j+ sumKnown y row i (j + 1) (acc + (row VU.! j) * yj)++-- | Solve @Lᵀ x = y@ for lower-triangular @L@.+backSubst :: Matrix -> VU.Vector Double -> VU.Vector Double+backSubst l y = runST $ do+ x <- VUM.new d+ forM_ [d - 1, d - 2 .. 0] $ \i -> do+ s <- sumKnown x i (i + 1) 0+ VUM.write x i ((y VU.! i - s) / ((l V.! i) VU.! i))+ VU.freeze x+ where+ d = V.length l+ sumKnown x i j !acc+ | j >= d = pure acc+ | otherwise = do+ xj <- VUM.read x j+ sumKnown x i (j + 1) (acc + ((l V.! j) VU.! i) * xj)++{- | Solve the SPD system @A x = b@ via Cholesky; 'Nothing' when @A@ is not+positive-definite.+-}+choleskySolve :: Matrix -> VU.Vector Double -> Maybe (VU.Vector Double)+choleskySolve a b = do+ l <- cholesky a+ pure (backSubst l (forwardSubst l b))++-- | @log det A = 2 Σ log Lᵢᵢ@ from a Cholesky factor @L@.+logDetFromChol :: Matrix -> Double+logDetFromChol l = 2 * V.sum (V.imap (\i row -> log (row VU.! i)) l)
+ src/DataFrame/LinearModel.hs view
@@ -0,0 +1,11 @@+{- | Linear models for the dataframe ecosystem: regression (OLS, ridge, lasso,+elastic net) and one-vs-rest logistic classification. Re-exports the focused+submodules.+-}+module DataFrame.LinearModel (+ module DataFrame.LinearModel.Regression,+ module DataFrame.LinearModel.Logistic,+) where++import DataFrame.LinearModel.Logistic+import DataFrame.LinearModel.Regression
+ src/DataFrame/LinearModel/Logistic.hs view
@@ -0,0 +1,105 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE UndecidableInstances #-}++{- | Logistic regression: binary and one-vs-rest multiclass over the FISTA+solver. 'fit' trains a 'LogisticModel'; 'predict' is the arg-max class decision.+Per-class margins and (normalized) probabilities stay available as named+auxiliary expressions.+-}+module DataFrame.LinearModel.Logistic (+ LogisticConfig (..),+ defaultLogisticConfig,+ LogisticModel (..),+ logisticMarginExprs,+ logisticProbExprs,+) where++import Data.List (sort)+import qualified Data.Map.Strict as M+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.Featurize.Internal (+ affineExpr,+ argMaxExpr,+ featureNames,+ numericMatrix,+ targetValues,+ )+import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (Columnable)+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr)+import DataFrame.LinearSolver (+ LinearModel (..),+ SolverConfig,+ defaultSolverConfig,+ fitL1Logistic,+ )+import DataFrame.Model (Fit (..), Predict (..))++-- | Hyperparameters for logistic regression (the FISTA solver config).+newtype LogisticConfig = LogisticConfig {lgSolver :: SolverConfig}+ deriving (Eq, Show)++defaultLogisticConfig :: LogisticConfig+defaultLogisticConfig = LogisticConfig defaultSolverConfig++{- | A fitted (one-vs-rest) logistic model: parallel vectors of class labels and+their binary sub-models. 'lgModels' carries sklearn's per-class @coef_@.+-}+data LogisticModel a = LogisticModel+ { lgClasses :: !(V.Vector a)+ , lgModels :: !(V.Vector LinearModel)+ }+ deriving (Eq, Show)++instance (Columnable a, Ord a) => Fit LogisticConfig (Expr a) (LogisticModel a) where+ fit = fitLogistic++instance (Columnable a, Ord a) => Predict (LogisticModel a) a where+ predict m = argMaxExpr (labelledMargins m)++-- | Fit one-vs-rest logistic regression; the target column supplies the classes.+fitLogistic ::+ (Columnable a, Ord a) =>+ LogisticConfig -> Expr a -> DataFrame -> LogisticModel a+fitLogistic (LogisticConfig cfg) target df =+ LogisticModel (V.fromList classes) (V.fromList (map fitOne classes))+ where+ names = featureNames target df+ (nameVec, mat) = numericMatrix names df+ ys = targetValues target df+ classes = sort (foldr dedup [] (V.toList ys))+ dedup x acc = if x `elem` acc then acc else x : acc+ fitOne c =+ let labels =+ VU.generate (V.length ys) (\i -> if ys V.! i == c then 1 else -1)+ in fitL1Logistic cfg mat labels nameVec++-- | The raw margin @Expr@ for each class.+logisticMarginExprs ::+ (Columnable a, Ord a) => LogisticModel a -> M.Map a (Expr Double)+logisticMarginExprs m = M.fromList (labelledMargins m)++-- | Per-class probability expressions: @1 / (1 + exp(-margin))@.+logisticProbExprs ::+ (Columnable a, Ord a) => LogisticModel a -> M.Map a (Expr Double)+logisticProbExprs = M.map sigmoidExpr . logisticMarginExprs++labelledMargins :: LogisticModel a -> [(a, Expr Double)]+labelledMargins m =+ [ (lgClasses m V.! i, marginOf (lgModels m V.! i))+ | i <- [0 .. V.length (lgClasses m) - 1]+ ]++marginOf :: LinearModel -> Expr Double+marginOf m =+ affineExpr+ (lmIntercept m)+ (zip (VU.toList (lmWeights m)) (V.toList (lmFeatureNames m)))++sigmoidExpr :: Expr Double -> Expr Double+sigmoidExpr z = F.lit 1 / (F.lit 1 + exp (negate z))
+ src/DataFrame/LinearModel/Regression.hs view
@@ -0,0 +1,128 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}++{- | Linear regression with the standard penalties: ordinary least squares+(Householder QR), ridge (Cholesky on the regularized normal equations), and+lasso / elastic net (FISTA). 'fit' produces a 'LinearRegressor' record;+'predict' compiles it to an @Expr Double@ over the raw feature columns.+-}+module DataFrame.LinearModel.Regression (+ Penalty (..),+ LinearConfig (..),+ defaultLinearConfig,+ LinearRegressor (..),+ predictLinear,+) where++import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.Featurize.Internal (+ affineExpr,+ featureNames,+ numericMatrix,+ targetDoubles,+ )+import DataFrame.Internal.Expression (Expr)+import DataFrame.LinearAlgebra (Matrix, dot, gram, tMatVec)+import DataFrame.LinearAlgebra.Solve (choleskySolve, qrLeastSquares)+import DataFrame.LinearSolver (+ LinearModel (..),+ SolverConfig (..),+ defaultSolverConfig,+ fitProx,+ )+import DataFrame.LinearSolver.Loss (squaredLoss)+import DataFrame.Model (Fit (..), Predict (..))++-- | Regularization choice. @alpha@ is the penalty strength; @l1Ratio@ mixes L1/L2.+data Penalty+ = OLS+ | Ridge !Double+ | Lasso !Double+ | ElasticNet !Double !Double+ deriving (Eq, Show)++-- | Hyperparameters for linear regression: the penalty and the FISTA solver config.+data LinearConfig = LinearConfig+ { lcPenalty :: !Penalty+ , lcSolver :: !SolverConfig+ }+ deriving (Eq, Show)++defaultLinearConfig :: LinearConfig+defaultLinearConfig = LinearConfig{lcPenalty = OLS, lcSolver = defaultSolverConfig}++{- | A fitted linear regressor. @regCoef@ and @regIntercept@ are sklearn's+@coef_@ / @intercept_@ in raw feature space.+-}+data LinearRegressor = LinearRegressor+ { regCoef :: !(VU.Vector Double)+ , regIntercept :: !Double+ , regFeatureNames :: !(V.Vector T.Text)+ , regPenalty :: !Penalty+ }+ deriving (Eq, Show)++instance Fit LinearConfig (Expr Double) LinearRegressor where+ fit (LinearConfig penalty cfg) target df =+ case penalty of+ OLS -> closedForm (olsSolve mat y)+ Ridge alpha -> closedForm (ridgeSolve alpha mat y)+ Lasso alpha -> proxFit alpha 1.0+ ElasticNet alpha l1r -> proxFit alpha l1r+ where+ names = featureNames target df+ (nameVec, mat) = numericMatrix names df+ y = targetDoubles target df+ closedForm (coef, intercept) =+ LinearRegressor coef intercept nameVec penalty+ proxFit alpha l1r =+ let proxCfg =+ cfg{scL1Lambda = alpha * l1r, scL2Lambda = alpha * (1 - l1r)}+ m = fitProx squaredLoss proxCfg mat y nameVec+ in LinearRegressor (lmWeights m) (lmIntercept m) nameVec penalty++instance Predict LinearRegressor Double where+ predict m =+ affineExpr+ (regIntercept m)+ (zip (VU.toList (regCoef m)) (V.toList (regFeatureNames m)))++-- | OLS via QR on the intercept-augmented design matrix.+olsSolve :: Matrix -> VU.Vector Double -> (VU.Vector Double, Double)+olsSolve mat y =+ case qrLeastSquares augmented y of+ Right sol -> (VU.drop 1 sol, sol VU.! 0)+ Left _ -> ridgeSolve 1e-8 mat y+ where+ augmented = V.map (VU.cons 1) mat++{- | Ridge via Cholesky on @(XcᵀXc + αI) w = Xcᵀ yc@ over centred data; the+intercept is recovered from the column/target means.+-}+ridgeSolve :: Double -> Matrix -> VU.Vector Double -> (VU.Vector Double, Double)+ridgeSolve alpha mat y =+ case choleskySolve a rhs of+ Just w -> (w, meanY - dot w meansX)+ Nothing -> (VU.replicate d 0, meanY)+ where+ n = V.length mat+ d = if n == 0 then 0 else VU.length (V.head mat)+ meansX =+ VU.generate d $ \j ->+ sum [(mat V.! i) VU.! j | i <- [0 .. n - 1]] / fromIntegral n+ meanY = VU.sum y / fromIntegral n+ centered = V.map (\row -> VU.zipWith (-) row meansX) mat+ yc = VU.map (subtract meanY) y+ a = addDiag alpha (gram centered)+ rhs = tMatVec centered yc++-- | Add @alpha@ to the diagonal of a square matrix.+addDiag :: Double -> Matrix -> Matrix+addDiag alpha = V.imap (\i row -> row VU.// [(i, row VU.! i + alpha)])++-- | Predict the target for each row of a feature matrix.+predictLinear :: LinearRegressor -> Matrix -> VU.Vector Double+predictLinear m = VU.convert . V.map (\x -> regIntercept m + dot (regCoef m) x)
src/DataFrame/LinearSolver.hs view
@@ -1,9 +1,12 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE ScopedTypeVariables #-} -{- | L1-regularized logistic regression used as the per-node split solver in-'DataFrame.DecisionTree'. Produces a sparse oblique hyperplane that can be-compiled to an 'Expr Bool' over numeric columns.+{- | Proximal-gradient (FISTA) solver for L1/L2-regularized generalized linear+models. 'fitL1Logistic' is the binary logistic split solver used by+'DataFrame.DecisionTree'; 'fitProx' generalizes it to any 'SmoothLoss'+(squared loss for lasso/elastic-net, squared hinge for LinearSVC). Features are+standardized internally and weights de-standardized, so the model applies to+raw column values. -} module DataFrame.LinearSolver ( -- * Model@@ -13,14 +16,16 @@ SolverConfig (..), defaultSolverConfig, - -- * Solver+ -- * Solvers fitL1Logistic,+ fitProx, -- * Expr conversion modelToExpr, -- * Internals (exposed for testing) standardize,+ columnStats, softThreshold, sigmoid, dotProduct,@@ -28,6 +33,13 @@ import qualified DataFrame.Functions as F import DataFrame.Internal.Expression (Expr (..))+import DataFrame.LinearAlgebra (Matrix, gram, scaleV)+import DataFrame.LinearAlgebra.Eigen (powerIterTop)+import DataFrame.LinearSolver.Loss (+ SmoothLoss (..),+ logisticLoss,+ sigmoid,+ ) import DataFrame.Operators ((.*.), (.+.), (.>.)) import Control.Monad.ST (ST, runST)@@ -95,7 +107,44 @@ V.Vector T.Text -> LinearModel {-# INLINEABLE fitL1Logistic #-}-fitL1Logistic cfg rows labels featureNames+fitL1Logistic = runFista logisticLoss logisticLipschitz+ where+ logisticLipschitz _ keepN = fromIntegral (keepN + 1) / 4++{- | Fit any 'SmoothLoss' with the elastic-net proximal-gradient engine. The+Lipschitz constant uses the spectral norm of the standardized Gram matrix+(power iteration), which is tight for squared and squared-hinge losses where+the logistic trace bound would be far too loose.+-}+fitProx ::+ SmoothLoss ->+ SolverConfig ->+ V.Vector (VU.Vector Double) ->+ VU.Vector Double ->+ V.Vector T.Text ->+ LinearModel+fitProx loss = runFista loss specNormLipschitz+ where+ specNormLipschitz xKept _ =+ let n = V.length xKept+ gramN = V.map (scaleV (1 / fromIntegral n)) (gram xKept)+ (specNorm, _) = powerIterTop 50 gramN+ in slCurvBound loss * (specNorm + 1)++{- | Shared FISTA scaffolding: standardize, drop near-constant columns, run the+inner loop, de-standardize. @lipschitzOf@ receives the standardized kept-feature+matrix and the number of kept columns and returns the smooth-part Lipschitz+bound; the L2 penalty contribution @λ₂@ is added here.+-}+runFista ::+ SmoothLoss ->+ (Matrix -> Int -> Double) ->+ SolverConfig ->+ V.Vector (VU.Vector Double) ->+ VU.Vector Double ->+ V.Vector T.Text ->+ LinearModel+runFista loss lipschitzOf cfg rows labels featureNames | n == 0 || d == 0 = zeroModel | otherwise = let (!means, !stds, !variances) = columnStats rows@@ -106,14 +155,11 @@ let !meansKept = gatherBy keep means !stdsKept = gatherBy keep stds !xKept = V.map (standardizeRowKept keep means stds) rows- -- Elastic-Net Lipschitz: standard logistic bound- -- @(d+1)/4@ plus the L2 part's Hessian-norm- -- contribution @λ₂·I@ (operator norm @λ₂@). !lipschitz =- fromIntegral (VU.length keep + 1) / 4- + scL2Lambda cfg+ lipschitzOf xKept (VU.length keep) + scL2Lambda cfg (!wStdKept, !bStd) = fistaLoop+ loss (scL1Lambda cfg) (scL2Lambda cfg) lipschitz@@ -270,48 +316,42 @@ | v < -lambda = v + lambda | otherwise = 0 --- | Numerically stable logistic sigmoid.-sigmoid :: Double -> Double-sigmoid z- | z >= 0 = 1 / (1 + exp (-z))- | otherwise = let ez = exp z in ez / (1 + ez)- {- | Dot product of two unboxed vectors. Caller must ensure equal length; lengths are not checked. -} dotProduct :: VU.Vector Double -> VU.Vector Double -> Double dotProduct u v = VU.sum (VU.zipWith (*) u v) -{- | Gradient of the average binary logistic loss at @(w, b)@ for labels in-@{\-1,+1}@. Returns @(gradW, gradB)@.+{- | Gradient of the average loss at @(w, b)@. Returns @(gradW, gradB)@. Sample-weighted variant: when @sampleWeights@ is @Just ws@ the per-row contribution is multiplied by @ws[i]@. With weights of mean 1 (i.e. @Σ w_i = N@; the class-balanced convention used by 'fitLinearCandidate'), the @1/N@ normalisation is preserved exactly. -}-logisticGradient ::+lossGradient ::+ SmoothLoss -> Maybe (VU.Vector Double) -> V.Vector (VU.Vector Double) -> VU.Vector Double -> VU.Vector Double -> Double -> (VU.Vector Double, Double)-logisticGradient sampleWeights features labels w b = (gradW, gradB)+lossGradient loss sampleWeights features labels w b = (gradW, gradB) where !invN = 1 / fromIntegral (V.length features)- !coeffs = rowCoeffs sampleWeights features labels w b invN+ !coeffs = rowCoeffs loss sampleWeights features labels w b invN !gradW = accumulateGradW (VU.length w) features coeffs !gradB = VU.sum coeffs -{- | Per-row loss coefficient. Without sample weights:-@c_i = -y_i * sigmoid(-y_i * margin_i) / N@. With @Just ws@, each row's-contribution is additionally multiplied by @ws[i]@.+{- | Per-row loss coefficient @c_i = ℓ'(y_i, z_i) / N@ at margin+@z_i = w·x_i + b@, optionally scaled by @ws[i]@. unsafeIndex is safe: @i@ ranges over @[0,n-1]@ and @labels@ / @sampleWeights@ both have length @n@ by construction. -} rowCoeffs ::+ SmoothLoss -> Maybe (VU.Vector Double) -> V.Vector (VU.Vector Double) -> VU.Vector Double ->@@ -319,12 +359,12 @@ Double -> Double -> VU.Vector Double-rowCoeffs sampleWeights features labels w b invN =+rowCoeffs loss sampleWeights features labels w b invN = VU.generate (V.length features) $ \i -> let !yi = VU.unsafeIndex labels i !row = V.unsafeIndex features i- !margin = yi * (dotProduct w row + b)- !base = -(yi * sigmoid (-margin) * invN)+ !z = dotProduct w row + b+ !base = slGradZ loss yi z * invN in case sampleWeights of Nothing -> base Just ws -> base * VU.unsafeIndex ws i@@ -350,6 +390,7 @@ @prox(z) = softThreshold(z, λ₁/lp) / (1 + λ₂/lp)@. -} fistaLoop ::+ SmoothLoss -> Double -> Double -> Double ->@@ -361,13 +402,13 @@ VU.Vector Double -> Double -> (VU.Vector Double, Double)-fistaLoop lambda1 lambda2 lp maxIter tol sampleWeights features labels w0 b0 =+fistaLoop loss lambda1 lambda2 lp maxIter tol sampleWeights features labels w0 b0 = go 0 w0 b0 w0 b0 1.0 where !shrink = lambda1 / lp !ridgeDenom = 1 + lambda2 / lp !stepInv = 1 / lp- proxStep = fistaProxStep sampleWeights features labels shrink ridgeDenom stepInv+ proxStep = fistaProxStep loss sampleWeights features labels shrink ridgeDenom stepInv go !iter !xWPrev !xBPrev !yW !yB !t | iter >= maxIter = (xWPrev, xBPrev) | iter > 0 && delta < tol = (xW, xB)@@ -386,6 +427,7 @@ Teboulle 2009 §4). The intercept is unregularised (no L1 or L2 applied). -} fistaProxStep ::+ SmoothLoss -> Maybe (VU.Vector Double) -> V.Vector (VU.Vector Double) -> VU.Vector Double ->@@ -395,8 +437,8 @@ VU.Vector Double -> Double -> (VU.Vector Double, Double)-fistaProxStep sampleWeights features labels shrink ridgeDenom stepInv yW yB =- let (gW, gB) = logisticGradient sampleWeights features labels yW yB+fistaProxStep loss sampleWeights features labels shrink ridgeDenom stepInv yW yB =+ let (gW, gB) = lossGradient loss sampleWeights features labels yW yB !wNew = VU.zipWith (\yi gi -> softThreshold shrink (yi - gi * stepInv) / ridgeDenom)
+ src/DataFrame/LinearSolver/Loss.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE OverloadedStrings #-}++{- | Smooth losses for the proximal-gradient engine. Each carries its+derivative @∂ℓ/∂z@ at @z = w·x + b@ and a global bound on the curvature+@∂²ℓ/∂z²@ (used for the FISTA step size).+-}+module DataFrame.LinearSolver.Loss (+ SmoothLoss (..),+ sigmoid,+ logisticLoss,+ squaredLoss,+ sqHingeLoss,+) where++import qualified Data.Text as T++{- | A convex, @C¹@ per-sample loss @ℓ(y, z)@. 'slGradZ' is @∂ℓ/∂z@;+'slCurvBound' bounds @∂²ℓ/∂z²@ over all @(y, z)@.+-}+data SmoothLoss = SmoothLoss+ { slName :: !T.Text+ , slGradZ :: Double -> Double -> Double+ , slCurvBound :: !Double+ }++-- | Numerically stable logistic sigmoid.+sigmoid :: Double -> Double+sigmoid z+ | z >= 0 = 1 / (1 + exp (-z))+ | otherwise = let ez = exp z in ez / (1 + ez)++-- | Binary logistic loss for labels in @{\-1,+1}@: @ℓ = log(1 + exp(-y z))@.+logisticLoss :: SmoothLoss+logisticLoss =+ SmoothLoss "logistic" (\y z -> negate (y * sigmoid (negate (y * z)))) 0.25++-- | Squared error for regression: @ℓ = ½ (z - y)²@.+squaredLoss :: SmoothLoss+squaredLoss = SmoothLoss "squared" (flip (-)) 1.0++-- | Squared hinge for classification (LinearSVC default), labels @{\-1,+1}@.+sqHingeLoss :: SmoothLoss+sqHingeLoss =+ SmoothLoss+ "squared_hinge"+ (\y z -> let m = 1 - y * z in if m > 0 then negate (2 * y * m) else 0)+ 2.0
+ src/DataFrame/Metrics.hs view
@@ -0,0 +1,237 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Evaluation metrics for fitted models. The everyday entry point is+'evaluate', which applies a model's prediction expression and a truth column to+a frame and folds a metric — no manual @interpret@/extract plumbing. Metrics are+plain functions (@type Metric = Vector -> Vector -> Double@), so you pass @mse@+or @accuracy@ directly. Classification metrics handle multiclass via 'Average';+'classificationReport' / 'regressionReport' bundle the common numbers with a+scikit-learn-style 'Show'.+-}+module DataFrame.Metrics (+ -- * Metric type + evaluation+ Metric,+ evaluate,+ predictColumn,+ columnOf,++ -- * Regression metrics+ mse,+ rmse,+ mae,+ r2,++ -- * Classification metrics+ accuracy,+ logLoss,+ Average (..),+ precision,+ recall,+ f1,+ rocAuc,++ -- * Per-class helpers (for reports)+ classCounts,+ precOf,+ recOf,+ f1Of,+) where++import Data.Either (fromRight)+import Data.List (nub, sort, sortBy)+import Data.Ord (comparing)+import qualified Data.Text as T+import qualified Data.Vector.Unboxed as VU++import DataFrame.Internal.Column (TypedColumn (..), toVector)+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr)+import DataFrame.Internal.Interpreter (interpret)+import DataFrame.Operations.Transformations (derive)++-- | A metric maps predictions and ground truth to a scalar score.+type Metric = VU.Vector Double -> VU.Vector Double -> Double++{- | Evaluate a model's prediction expression against a truth column on a frame.++> evaluate rmse (linearExpr model) (F.col @Double "target") df+> evaluate accuracy (logisticDecisionExpr model) (F.col @Double "label") df+-}+evaluate :: Metric -> Expr Double -> Expr Double -> DataFrame -> Double+evaluate metric predExpr truthExpr df =+ metric (columnOf df predExpr) (columnOf df truthExpr)++-- | Add a model's prediction expression to a frame as a named column.+predictColumn :: T.Text -> Expr Double -> DataFrame -> DataFrame+predictColumn = derive++-- | Interpret an expression to a @Double@ vector over a frame.+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)++n2 :: VU.Vector Double -> Double+n2 = fromIntegral . VU.length++-- | Mean squared error.+mse :: Metric+mse preds truth+ | VU.null truth = 0+ | otherwise =+ VU.sum (VU.zipWith (\p t -> (p - t) ^ (2 :: Int)) preds truth) / n2 truth++-- | Root mean squared error.+rmse :: Metric+rmse preds truth = sqrt (mse preds truth)++-- | Mean absolute error.+mae :: Metric+mae preds truth+ | VU.null truth = 0+ | otherwise = VU.sum (VU.zipWith (\p t -> abs (p - t)) preds truth) / n2 truth++-- | Coefficient of determination @R²@.+r2 :: Metric+r2 preds truth+ | VU.null truth || ssTot == 0 = 0+ | otherwise = 1 - ssRes / ssTot+ where+ mean = VU.sum truth / n2 truth+ ssRes = VU.sum (VU.zipWith (\p t -> (t - p) ^ (2 :: Int)) preds truth)+ ssTot = VU.sum (VU.map (\t -> (t - mean) ^ (2 :: Int)) truth)++-- | Fraction of exact matches.+accuracy :: Metric+accuracy preds truth+ | VU.null truth = 0+ | otherwise =+ fromIntegral (VU.length (VU.filter id (VU.zipWith (==) preds truth))) / n2 truth++-- | Binary log loss; probabilities clamped away from @0@/@1@.+logLoss :: Metric+logLoss probs truth+ | VU.null truth = 0+ | otherwise =+ negate+ ( VU.sum+ ( VU.zipWith+ (\p y -> let q = clampP p in y * log q + (1 - y) * log (1 - q))+ probs+ truth+ )+ )+ / n2 truth+ where+ clampP p = max 1e-15 (min (1 - 1e-15) p)++-- | Averaging strategy for multiclass precision/recall/F1.+data Average+ = -- | one class is positive; the rest negative+ Binary Double+ | -- | unweighted mean over classes+ Macro+ | -- | pool per-class counts (equals accuracy for single-label)+ Micro+ | -- | support-weighted mean over classes+ Weighted+ deriving (Eq, Show)++-- | Per-class @(tp, fp, fn, support)@ over the class set of @truth ∪ preds@.+classCounts ::+ VU.Vector Double -> VU.Vector Double -> [(Double, (Int, Int, Int, Int))]+classCounts preds truth =+ [(c, countsFor c) | c <- classes]+ where+ classes = sort (nub (VU.toList truth ++ VU.toList preds))+ countsFor c =+ VU.foldl'+ ( \(tp, fp, fn, sup) (p, y) ->+ ( if p == c && y == c then tp + 1 else tp+ , if p == c && y /= c then fp + 1 else fp+ , if p /= c && y == c then fn + 1 else fn+ , if y == c then sup + 1 else sup+ )+ )+ (0, 0, 0, 0)+ (VU.zip preds truth)++safeDiv :: Int -> Int -> Double+safeDiv a b = if b == 0 then 0 else fromIntegral a / fromIntegral b++precOf, recOf :: (Int, Int, Int, Int) -> Double+precOf (tp, fp, _, _) = safeDiv tp (tp + fp)+recOf (tp, _, fn, _) = safeDiv tp (tp + fn)++f1Of :: (Int, Int, Int, Int) -> Double+f1Of cs =+ let p = precOf cs; r = recOf cs in if p + r == 0 then 0 else 2 * p * r / (p + r)++averaged ::+ ((Int, Int, Int, Int) -> Double) ->+ Average ->+ VU.Vector Double ->+ VU.Vector Double ->+ Double+averaged stat avg preds truth =+ case avg of+ Binary pos -> maybe 0 stat (lookup pos cc)+ Macro -> meanOf [stat c | (_, c) <- cc]+ Weighted ->+ let total = sum [sup | (_, (_, _, _, sup)) <- cc]+ in if total == 0+ then 0+ else+ sum [fromIntegral sup * stat c | (_, c@(_, _, _, sup)) <- cc]+ / fromIntegral total+ Micro ->+ let tp = sum [t | (_, (t, _, _, _)) <- cc]+ fp = sum [x | (_, (_, x, _, _)) <- cc]+ fn = sum [x | (_, (_, _, x, _)) <- cc]+ in stat (tp, fp, fn, 0)+ where+ cc = classCounts preds truth+ meanOf xs = if null xs then 0 else sum xs / fromIntegral (length xs)++-- | Precision with the given averaging.+precision :: Average -> VU.Vector Double -> VU.Vector Double -> Double+precision = averaged precOf++-- | Recall with the given averaging.+recall :: Average -> VU.Vector Double -> VU.Vector Double -> Double+recall = averaged recOf++-- | F1 with the given averaging.+f1 :: Average -> VU.Vector Double -> VU.Vector Double -> Double+f1 = averaged f1Of++{- | Binary ROC-AUC (Mann–Whitney). @scores@ are predicted probabilities, @truth@+is @0@/@1@.+-}+rocAuc :: VU.Vector Double -> VU.Vector Double -> Double+rocAuc scores truth+ | nPos == 0 || nNeg == 0 = 0.5+ | otherwise = (rankSum - nPos * (nPos + 1) / 2) / (nPos * nNeg)+ where+ ranked = rankAverages (VU.toList scores)+ pairs = zip (VU.toList truth) ranked+ rankSum = sum [r | (y, r) <- pairs, y == 1]+ nPos = fromIntegral (length (filter (== 1) (VU.toList truth)))+ nNeg = fromIntegral (VU.length truth) - nPos++-- | Average ranks (ties share the mean rank), returned in input order.+rankAverages :: [Double] -> [Double]+rankAverages xs =+ let indexed = zip [0 :: Int ..] xs+ sorted = sortBy (comparing snd) indexed+ ranked = assignRanks 1 sorted+ in map snd (sortBy (comparing fst) ranked)+ where+ assignRanks _ [] = []+ assignRanks start grp =+ let v = snd (head grp)+ (tied, rest) = span ((== v) . snd) grp+ k = length tied+ avgRank = fromIntegral (sum [start .. start + k - 1]) / fromIntegral k+ in [(i, avgRank) | (i, _) <- tied] ++ assignRanks (start + k) rest
+ src/DataFrame/Metrics/Report.hs view
@@ -0,0 +1,170 @@+{- | Bundled, pretty-printing evaluation summaries: a labelled confusion matrix+and scikit-learn-style regression / classification reports. The @*Expr@ variants+take a model's prediction expression and a truth column directly, so a full+report is a one-liner after fitting.+-}+module DataFrame.Metrics.Report (+ ConfusionMatrix (..),+ confusionMatrix,+ confusionMatrixExpr,+ RegressionReport (..),+ regressionReport,+ regressionReportExpr,+ ClassStats (..),+ ClassificationReport (..),+ classificationReport,+ classificationReportExpr,+) where++import Data.List (nub, sort, sortBy)+import Data.Ord (Down (..), comparing)+import qualified Data.Vector.Unboxed as VU++import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr)+import DataFrame.Metrics (+ Average (..),+ accuracy,+ classCounts,+ columnOf,+ f1,+ f1Of,+ mae,+ mse,+ precOf,+ r2,+ recOf,+ rmse,+ )++-- | A labelled confusion matrix: class order plus row-major @actual×predicted@.+data ConfusionMatrix = ConfusionMatrix+ { cmClasses :: ![Double]+ , cmCounts :: ![[Int]]+ }+ deriving (Eq)++-- | Confusion matrix over the class set of @truth ∪ preds@.+confusionMatrix :: VU.Vector Double -> VU.Vector Double -> ConfusionMatrix+confusionMatrix preds truth = ConfusionMatrix classes counts+ where+ classes = sort (nub (VU.toList truth ++ VU.toList preds))+ counts =+ [ [ VU.length (VU.filter id (VU.zipWith (\p t -> t == a && p == c) preds truth))+ | c <- classes+ ]+ | a <- classes+ ]++-- | Confusion matrix from a prediction expression and a truth column.+confusionMatrixExpr ::+ Expr Double -> Expr Double -> DataFrame -> ConfusionMatrix+confusionMatrixExpr predExpr truthExpr df =+ confusionMatrix (columnOf df predExpr) (columnOf df truthExpr)++instance Show ConfusionMatrix where+ show (ConfusionMatrix classes counts) =+ unlines (header : rows)+ where+ lbls = map show classes+ w = maximum (8 : map length lbls) + 2+ cell s = replicate (max 1 (w - length s)) ' ' ++ s+ header = cell "a\\p" ++ concatMap cell lbls+ rows = [cell a ++ concatMap (cell . show) row | (a, row) <- zip lbls counts]++-- | Regression metrics bundle.+data RegressionReport = RegressionReport+ { rrMSE :: !Double+ , rrRMSE :: !Double+ , rrMAE :: !Double+ , rrR2 :: !Double+ }+ deriving (Eq)++instance Show RegressionReport where+ show r =+ unlines+ [ "Regression report"+ , " mse = " ++ show (rrMSE r)+ , " rmse = " ++ show (rrRMSE r)+ , " mae = " ++ show (rrMAE r)+ , " r2 = " ++ show (rrR2 r)+ ]++-- | Regression report from prediction/truth vectors.+regressionReport :: VU.Vector Double -> VU.Vector Double -> RegressionReport+regressionReport preds truth =+ RegressionReport+ (mse preds truth)+ (rmse preds truth)+ (mae preds truth)+ (r2 preds truth)++-- | Regression report from a prediction expression and a truth column.+regressionReportExpr ::+ Expr Double -> Expr Double -> DataFrame -> RegressionReport+regressionReportExpr predExpr truthExpr df =+ regressionReport (columnOf df predExpr) (columnOf df truthExpr)++-- | Per-class precision/recall/F1/support.+data ClassStats = ClassStats+ { csPrecision :: !Double+ , csRecall :: !Double+ , csF1 :: !Double+ , csSupport :: !Int+ }+ deriving (Eq, Show)++{- | A scikit-learn-style classification report: per-class stats plus accuracy+and macro/weighted F1.+-}+data ClassificationReport = ClassificationReport+ { crPerClass :: ![(Double, ClassStats)]+ , crAccuracy :: !Double+ , crMacroF1 :: !Double+ , crWeightedF1 :: !Double+ }+ deriving (Eq)++instance Show ClassificationReport where+ show r =+ unlines $+ (pad "class" ++ pad "precision" ++ pad "recall" ++ pad "f1" ++ pad "support")+ : [ pad (show c)+ ++ pad (num (csPrecision s))+ ++ pad (num (csRecall s))+ ++ pad (num (csF1 s))+ ++ pad (show (csSupport s))+ | (c, s) <- crPerClass r+ ]+ ++ [ ""+ , "accuracy = " ++ num (crAccuracy r)+ , "macro f1 = " ++ num (crMacroF1 r)+ , "weighted f1 = " ++ num (crWeightedF1 r)+ ]+ where+ pad s = let w = 12 in s ++ replicate (max 1 (w - length s)) ' '+ num x = show (fromIntegral (round (x * 1000) :: Int) / 1000 :: Double)++-- | Classification report from prediction/truth vectors.+classificationReport ::+ VU.Vector Double -> VU.Vector Double -> ClassificationReport+classificationReport preds truth =+ ClassificationReport+ perClass+ (accuracy preds truth)+ (f1 Macro preds truth)+ (f1 Weighted preds truth)+ where+ perClass =+ sortBy (comparing (Down . csSupport . snd)) $+ [ (c, ClassStats (precOf s) (recOf s) (f1Of s) (supOf s))+ | (c, s) <- classCounts preds truth+ ]+ supOf (_, _, _, sup) = sup++-- | Classification report from a prediction expression and a truth column.+classificationReportExpr ::+ Expr Double -> Expr Double -> DataFrame -> ClassificationReport+classificationReportExpr predExpr truthExpr df =+ classificationReport (columnOf df predExpr) (columnOf df truthExpr)
+ src/DataFrame/Model.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FunctionalDependencies #-}++{- | The two verbs every model speaks. Instead of a per-model @fitX@ / @xExpr@+zoo, every estimator is an instance of these classes:++ * 'fit' trains a model from a hyperparameter config, an @input@ (the supervised+ target @Expr a@ or the unsupervised feature list @[Expr Double]@), and a frame.+ * 'predict' compiles the model's canonical prediction to an @Expr@ over the raw+ columns (regressors give @Expr Double@, classifiers @Expr a@, clusterers+ @Expr Int@). Models with no honest out-of-sample prediction (e.g. DBSCAN) simply+ have no instance — @predict@ on them is a compile error, not a fake.++Every prediction lands in the /same/ expression type, so a fitted model composes+with 'DataFrame.Operations.Transformations.derive', the 'DataFrame.Transform'+monoid, and 'DataFrame.Transform.compileThrough' with no per-model glue.++Auxiliary outputs (class probabilities, per-cluster distances, component+loadings, the @*Transform@ pipeline pieces) keep their own descriptive+functions — they are not the one canonical prediction, so they are not forced+through 'predict'.++Supervised 'fit' treats every non-target column as a feature; use+'selectFeatures' to restrict to an explicit set when the frame also carries ids,+timestamps, or a second candidate target.+-}+module DataFrame.Model (+ Fit (..),+ Predict (..),+ selectFeatures,+) where++import qualified Data.Text as T++import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr (..))+import DataFrame.Operations.Subset (select)++{- | Train a model. @cfg@ is the hyperparameter config; @input@ is the supervised+target @Expr a@ or the unsupervised feature list @[Expr Double]@. The config and+input together determine the model, so @fit cfg target df@ needs no annotation+(a classifier's label type comes from its @Expr a@ target).+-}+class Fit cfg input model | cfg input -> model where+ fit :: cfg -> input -> DataFrame -> model++{- | Compile a fitted model's canonical prediction to an expression over the raw+columns. The result type @r@ is determined by the model.+-}+class Predict model r | model -> r where+ predict :: model -> Expr r++{- | Restrict @df@ to exactly the named feature columns plus the supervised+target (when the target is a column), so a following 'fit' trains on those+features only.++Supervised 'fit' otherwise uses /every/ non-target column as a feature —+convenient on a clean frame, a leakage hazard when the frame carries ids,+timestamps, or a second candidate target. This mirrors the explicit+@[Expr Double]@ feature list the unsupervised fitters already take:++> model = fit defaultLinearConfig target (selectFeatures ["age", "income"] target df)+-}+selectFeatures :: [T.Text] -> Expr a -> DataFrame -> DataFrame+selectFeatures cols target = select (cols ++ targetCols target)+ where+ targetCols :: Expr b -> [T.Text]+ targetCols (Col n) = [n]+ targetCols _ = []
+ src/DataFrame/ModelSelection.hs view
@@ -0,0 +1,91 @@+{- | Cross-validation and grid search for hyperparameter tuning. The model+fitters have heterogeneous types, so these helpers are parameterized by a+user-supplied @train -> test -> score@ closure; the search maximizes the mean+cross-validated score (use a negated error metric to minimize). Splitting reuses+the deterministic 'kFolds' / 'randomSplit' from @dataframe-operations@.+-}+module DataFrame.ModelSelection (+ trainTestSplit,+ crossValScore,+ crossValidate,+ GridSearchResult (..),+ gridSearch,+) where++import Data.List (maximumBy)+import Data.Ord (comparing)+import System.Random (mkStdGen)++import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr)+import DataFrame.Metrics (Metric, evaluate)+import DataFrame.Operations.Merge ()+import DataFrame.Operations.Subset (kFolds, randomSplit)++{- | Split into @(train, test)@ with the given training fraction and seed+(deterministic).+-}+trainTestSplit :: Double -> Int -> DataFrame -> (DataFrame, DataFrame)+trainTestSplit trainFrac seed = randomSplit (mkStdGen seed) trainFrac++{- | Per-fold scores from k-fold cross-validation. @scoreFn train test@ fits on+the training rows and returns a score on the held-out fold.+-}+crossValScore ::+ Int -> Int -> (DataFrame -> DataFrame -> Double) -> DataFrame -> [Double]+crossValScore folds seed scoreFn df =+ [ scoreFn (combine (others i)) (fs !! i)+ | i <- [0 .. length fs - 1]+ , not (null (others i))+ ]+ where+ fs = kFolds (mkStdGen seed) folds df+ others i = [f | (j, f) <- zip [0 ..] fs, j /= i]+ combine = foldr1 (<>)++{- | scikit-learn @cross_val_score@: fit a model on each training fold and score+its prediction expression against a truth column on the held-out fold.++@fitPredict train@ fits on the training frame and returns the prediction+expression; @truth@ is the target column. Returns the per-fold metric values.++> crossValidate 5 0 rmse (F.col @Double "target")+> (\tr -> predict (fit defaultLinearConfig (F.col @Double "target") tr)) df+-}+crossValidate ::+ Int ->+ Int ->+ Metric ->+ Expr Double ->+ (DataFrame -> Expr Double) ->+ DataFrame ->+ [Double]+crossValidate folds seed metric truth fitPredict =+ crossValScore folds seed score+ where+ score train = evaluate metric (fitPredict train) truth++-- | The outcome of a grid search: the best config, its score, and all results.+data GridSearchResult c = GridSearchResult+ { gsBest :: !c+ , gsBestScore :: !Double+ , gsAll :: ![(c, Double)]+ }+ deriving (Show)++{- | Search configurations by mean cross-validated score, returning the+maximizer. @scoreFn cfg train test@ fits @cfg@ on @train@ and scores on @test@.+-}+gridSearch ::+ Int ->+ Int ->+ [c] ->+ (c -> DataFrame -> DataFrame -> Double) ->+ DataFrame ->+ GridSearchResult c+gridSearch folds seed configs scoreFn df =+ GridSearchResult bestC bestS scored+ where+ scored = [(c, mean (crossValScore folds seed (scoreFn c) df)) | c <- configs]+ (bestC, bestS) = maximumBy (comparing snd) scored+ mean xs = if null xs then -(1 / 0) else sum xs / fromIntegral (length xs)
+ src/DataFrame/PCA.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}++{- | Principal component analysis via the symmetric Jacobi eigensolver on the+covariance of the (optionally standardized) feature columns. 'fit' trains a+'PCAModel' (components + explained variance); the projection is exposed as+'pcaExprs' / 'pcaTransform' (PCA is a transformer, so it has no 'Predict').+-}+module DataFrame.PCA (+ NComponents (..),+ PCAConfig (..),+ defaultPCAConfig,+ PCAModel (..),+ pcaExprs,+ pcaTransform,+) where++import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.Featurize.Internal (Features (..), extractFeatures)+import qualified DataFrame.Functions as F+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr (..), UExpr (..))+import DataFrame.LinearAlgebra (gram)+import DataFrame.LinearAlgebra.Eigen (jacobiEigenSym)+import DataFrame.Model (Fit (..))+import DataFrame.Operators ((.*.), (.+.), (.-.))+import DataFrame.Transform (Transform (..))++-- | How many components to keep.+data NComponents = NComp !Int | VarianceCovered !Double+ deriving (Eq, Show)++data PCAConfig = PCAConfig+ { pcaNComponents :: !NComponents+ , pcaStandardize :: !Bool+ }+ deriving (Eq, Show)++defaultPCAConfig :: PCAConfig+defaultPCAConfig = PCAConfig{pcaNComponents = NComp 2, pcaStandardize = False}++{- | A fitted PCA. 'pcaComponents' are sklearn's @components_@ (row @i@ is the+@i@-th loading vector); 'pcaScale' is @Just@ the per-column std when+standardizing.+-}+data PCAModel = PCAModel+ { pcaComponents :: !(V.Vector (VU.Vector Double))+ , pcaExplainedVariance :: !(VU.Vector Double)+ , pcaExplainedVarianceRatio :: !(VU.Vector Double)+ , pcaMean :: !(VU.Vector Double)+ , pcaScale :: !(Maybe (VU.Vector Double))+ , pcaFeatureNames :: !(V.Vector T.Text)+ }+ deriving (Eq, Show)++instance Fit PCAConfig [Expr Double] PCAModel where+ fit = fitPCA++-- | Fit PCA on the given feature columns (each must be a @Col@).+fitPCA :: PCAConfig -> [Expr Double] -> DataFrame -> PCAModel+fitPCA cfg features df =+ PCAModel+ { pcaComponents = V.take k vecs+ , pcaExplainedVariance = VU.take k evar+ , pcaExplainedVarianceRatio = VU.take k ratio+ , pcaMean = means+ , pcaScale = if pcaStandardize cfg then Just scales else Nothing+ , pcaFeatureNames = V.fromList names+ }+ where+ Features names cols _ n d = extractFeatures features df+ means = VU.fromList [VU.sum c / fromIntegral (max 1 n) | c <- cols]+ scales =+ VU.fromList+ [ let mu = means VU.! j+ v = VU.sum (VU.map (\x -> (x - mu) ^ (2 :: Int)) c) / fromIntegral (max 1 n)+ s = sqrt v+ in if s < 1e-12 then 1 else s+ | (j, c) <- zip [0 ..] cols+ ]+ scaled =+ V.generate n $ \i ->+ VU.generate d $ \j ->+ let mu = means VU.! j+ s = if pcaStandardize cfg then scales VU.! j else 1+ in ((cols !! j) VU.! i - mu) / s+ denom = fromIntegral (max 1 (n - 1))+ cov = V.map (VU.map (/ denom)) (gram scaled)+ (evals, vecs) = jacobiEigenSym cov+ evar = VU.map (max 0) evals+ total = VU.sum evar+ ratio = if total == 0 then evar else VU.map (/ total) evar+ k = resolveK (pcaNComponents cfg) d ratio++-- | Per-component projection expressions, named @pc1@, @pc2@, …+pcaExprs :: PCAModel -> [(T.Text, Expr Double)]+pcaExprs m =+ [ ("pc" <> T.pack (show i), componentExpr (pcaComponents m V.! (i - 1)))+ | i <- [1 .. V.length (pcaComponents m)]+ ]+ where+ names = V.toList (pcaFeatureNames m)+ means = VU.toList (pcaMean m)+ scales = maybe (repeat 1) VU.toList (pcaScale m)+ componentExpr vec =+ foldr (.+.) (F.lit 0) $+ [ F.lit (w / s) .*. ((Col n :: Expr Double) .-. F.lit mu)+ | (w, n, mu, s) <- zip4 (VU.toList vec) names means scales+ ]++-- | The PCA projection as a composable fitted 'Transform'.+pcaTransform :: PCAModel -> Transform+pcaTransform m = Transform [(n, UExpr e) | (n, e) <- pcaExprs m]++resolveK :: NComponents -> Int -> VU.Vector Double -> Int+resolveK (NComp k) d _ = max 1 (min k d)+resolveK (VarianceCovered frac) d ratio = max 1 (min d (go 0 0 1))+ where+ go !acc !cum !i+ | i > VU.length ratio = VU.length ratio+ | cum >= frac = acc+ | otherwise = go (acc + 1) (cum + ratio VU.! (i - 1)) (i + 1)++zip4 :: [a] -> [b] -> [c] -> [d] -> [(a, b, c, d)]+zip4 (a : as) (b : bs) (c : cs) (d : ds) = (a, b, c, d) : zip4 as bs cs ds+zip4 _ _ _ _ = []
+ src/DataFrame/PCA/Kernel.hs view
@@ -0,0 +1,136 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}++{- | Kernel PCA with an RBF kernel, solved on a set of landmark points (Nyström).+Exact kernel PCA when the landmark count covers every row, a principled+approximation otherwise. 'fit' trains the model; the projection is exposed as+'kernelPCAExprs' / 'kernelPcaTransform' (a transformer, so no 'Predict').+-}+module DataFrame.PCA.Kernel (+ KernelPCAConfig (..),+ defaultKernelPCAConfig,+ KernelPCAModel (..),+ kernelPCAExprs,+ kernelPcaTransform,+) where++import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.Featurize.Internal (Features (..), extractFeatures)+import qualified DataFrame.Functions as F+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr (..), UExpr (..))+import DataFrame.LinearAlgebra (sqDist)+import DataFrame.LinearAlgebra.Eigen (jacobiEigenSym)+import DataFrame.Model (Fit (..))+import DataFrame.Operators ((.*.), (.+.), (.-.))+import DataFrame.Random (mkGen, sampleIndices)+import DataFrame.Transform (Transform (..))++data KernelPCAConfig = KernelPCAConfig+ { kpcaNComponents :: !Int+ , kpcaGamma :: !(Maybe Double)+ , kpcaNLandmarks :: !Int+ , kpcaSeed :: !Int+ }+ deriving (Eq, Show)++defaultKernelPCAConfig :: KernelPCAConfig+defaultKernelPCAConfig =+ KernelPCAConfig+ { kpcaNComponents = 2+ , kpcaGamma = Nothing+ , kpcaNLandmarks = 128+ , kpcaSeed = 0+ }++{- | A fitted kernel PCA. Each component is @Σ_l βₗ·K(x, landmarkₗ) + cᵢ@ with an+RBF kernel of bandwidth 'kpcaGammaUsed'.+-}+data KernelPCAModel = KernelPCAModel+ { kpcaLandmarks :: !(V.Vector (VU.Vector Double))+ , kpcaBetas :: !(V.Vector (VU.Vector Double))+ , kpcaConsts :: !(VU.Vector Double)+ , kpcaEigenvalues :: !(VU.Vector Double)+ , kpcaGammaUsed :: !Double+ , kpcaFeatureNames :: !(V.Vector T.Text)+ }+ deriving (Eq, Show)++instance Fit KernelPCAConfig [Expr Double] KernelPCAModel where+ fit = fitKernelPCA++-- | Fit kernel PCA over the given feature columns.+fitKernelPCA :: KernelPCAConfig -> [Expr Double] -> DataFrame -> KernelPCAModel+fitKernelPCA cfg features df =+ KernelPCAModel+ { kpcaLandmarks = landmarks+ , kpcaBetas = betas+ , kpcaConsts = consts+ , kpcaEigenvalues = VU.take k evals+ , kpcaGammaUsed = gamma+ , kpcaFeatureNames = V.fromList names+ }+ where+ Features names _ rows n d = extractFeatures features df+ m = min (max 1 (kpcaNLandmarks cfg)) n+ (idx, _) = sampleIndices m n (mkGen (kpcaSeed cfg))+ landmarks = V.map (rows V.!) (V.convert idx)+ gamma = case kpcaGamma cfg of+ Just g -> g+ Nothing -> 1 / fromIntegral (max 1 d)+ kmat =+ V.generate m $ \i ->+ VU.generate m $ \j ->+ exp (negate gamma * sqDist (landmarks V.! i) (landmarks V.! j))+ rowMean i = VU.sum (kmat V.! i) / fromIntegral m+ totalMean = sum [rowMean i | i <- [0 .. m - 1]] / fromIntegral m+ centered =+ V.generate m $ \i ->+ VU.generate m $ \j ->+ (kmat V.! i) VU.! j - rowMean i - rowMean j + totalMean+ (evals, vecs) = jacobiEigenSym centered+ k = min (kpcaNComponents cfg) m+ alphas =+ V.generate k $ \i ->+ let lam = max 1e-12 (evals VU.! i)+ in VU.map (/ sqrt lam) (vecs V.! i)+ betas =+ V.map+ (\a -> let s = VU.sum a / fromIntegral m in VU.map (subtract s) a)+ alphas+ consts =+ VU.generate k $ \i ->+ let a = alphas V.! i+ sA = VU.sum a+ in negate (sum [a VU.! l * rowMean l | l <- [0 .. m - 1]])+ + totalMean * sA++-- | Per-component projection expressions, named @kpc1@, @kpc2@, …+kernelPCAExprs :: KernelPCAModel -> [(T.Text, Expr Double)]+kernelPCAExprs m =+ [ ("kpc" <> T.pack (show (i + 1)), componentExpr i)+ | i <- [0 .. V.length (kpcaBetas m) - 1]+ ]+ where+ names = V.toList (kpcaFeatureNames m)+ gamma = kpcaGammaUsed m+ componentExpr i =+ foldr (.+.) (F.lit (kpcaConsts m VU.! i)) $+ [ F.lit (kpcaBetas m V.! i VU.! l) .*. kernelExpr (kpcaLandmarks m V.! l)+ | l <- [0 .. V.length (kpcaLandmarks m) - 1]+ ]+ kernelExpr landmark =+ exp (F.lit (negate gamma) .*. sqDistExpr landmark)+ sqDistExpr landmark =+ foldr (.+.) (F.lit 0) $+ [ let diff = (Col n :: Expr Double) .-. F.lit lj in diff .*. diff+ | (n, lj) <- zip names (VU.toList landmark)+ ]++-- | The kernel-PCA projection as a composable fitted 'Transform'.+kernelPcaTransform :: KernelPCAModel -> Transform+kernelPcaTransform m = Transform [(n, UExpr e) | (n, e) <- kernelPCAExprs m]
+ src/DataFrame/Random.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE CPP #-}++{- | Deterministic, platform-independent random sampling for the stochastic+fitters. Built on @random@'s SplitMix 'StdGen' (only 'genWord64' and a+version-bridged split are used, the stable surface across @random@ versions);+the distributions here are our own so a seeded fit is bit-reproducible on+Linux, macOS, and Windows.+-}+module DataFrame.Random (+ Gen,+ mkGen,+ splitGen,+ nextWord64,+ nextDouble,+ nextIntR,+ gaussianPair,+ gaussianVector,+ shuffleInts,+ sampleIndices,+) where++import Control.Monad (forM_)+import Control.Monad.ST (runST)+import Data.Bits (shiftR)+import qualified Data.Vector.Unboxed as VU+import qualified Data.Vector.Unboxed.Mutable as VUM+import Data.Word (Word64)+import System.Random (StdGen, genWord64, mkStdGen)+import qualified System.Random as R++-- | The pure splittable generator. A fit is a function of @(seed, data)@.+type Gen = StdGen++-- | Seed a generator from an 'Int'.+mkGen :: Int -> Gen+mkGen = mkStdGen++-- | Split into two independent generators.+splitGen :: Gen -> (Gen, Gen)+#if MIN_VERSION_random(1,3,0)+splitGen = R.splitGen+#else+splitGen = R.split+#endif++-- | Raw 64-bit draw.+nextWord64 :: Gen -> (Word64, Gen)+nextWord64 = genWord64++-- | Uniform 'Double' in @[0, 1)@ from the top 53 bits (exact mantissa).+nextDouble :: Gen -> (Double, Gen)+nextDouble g =+ let (w, g') = genWord64 g+ d = fromIntegral (w `shiftR` 11) * (1 / 9007199254740992)+ in (d, g')++{- | Uniform 'Int' in the inclusive range @[lo, hi]@ by rejection sampling+(unbiased). Returns @lo@ when @hi <= lo@.+-}+nextIntR :: (Int, Int) -> Gen -> (Int, Gen)+nextIntR (lo, hi) g+ | hi <= lo = (lo, g)+ | otherwise = loop g+ where+ range = fromIntegral (hi - lo + 1) :: Word64+ threshold = negate range `mod` range+ loop gg =+ let (w, gg') = genWord64 gg+ in if w >= threshold+ then (lo + fromIntegral (w `mod` range), gg')+ else loop gg'++{- | A pair of independent standard normals via Box-Muller, consuming exactly two+uniforms so stream offsets stay data-independent.+-}+gaussianPair :: Gen -> ((Double, Double), Gen)+gaussianPair g =+ let (u1, g1) = nextDouble g+ (u2, g2) = nextDouble g1+ u1' = if u1 <= 0 then 2.220446049250313e-16 else u1+ r = sqrt (-(2 * log u1'))+ a = 2 * pi * u2+ in ((r * cos a, r * sin a), g2)++-- | A length-@n@ vector of standard normals.+gaussianVector :: Int -> Gen -> (VU.Vector Double, Gen)+gaussianVector n g0 = go n g0 []+ where+ go k g acc+ | k <= 0 = (VU.fromList (take n (reverse acc)), g)+ | otherwise =+ let ((z0, z1), g') = gaussianPair g+ in go (k - 2) g' (z1 : z0 : acc)++{- | A uniformly random permutation of @[0 .. n-1]@ (Fisher-Yates), threading the+generator purely.+-}+shuffleInts :: Int -> Gen -> (VU.Vector Int, Gen)+shuffleInts n g0+ | n <= 1 = (VU.enumFromN 0 (max 0 n), g0)+ | otherwise =+ let (swaps, g1) = genSwaps (n - 1) g0 []+ v = runST $ do+ m <- VU.thaw (VU.enumFromN 0 n)+ forM_ swaps $ uncurry (VUM.swap m)+ VU.freeze m+ in (v, g1)+ where+ genSwaps i g acc+ | i < 1 = (reverse acc, g)+ | otherwise =+ let (j, g') = nextIntR (0, i) g+ in genSwaps (i - 1) g' ((i, j) : acc)++{- | @sampleIndices k n@ draws @k@ distinct indices from @[0 .. n-1]@ (the first+@k@ of a full shuffle); returns all @n@ when @k >= n@.+-}+sampleIndices :: Int -> Int -> Gen -> (VU.Vector Int, Gen)+sampleIndices k n g =+ let (perm, g') = shuffleInts n g+ in (VU.take (min k n) perm, g')
+ src/DataFrame/SVM.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE UndecidableInstances #-}++{- | Linear support vector classification: L2-regularized squared hinge fitted+with the FISTA engine (sklearn's LinearSVC default loss). 'fit' trains a+one-vs-rest 'LinearSVCModel'; 'predict' is the arg-max class margin. There is no+@predict_proba@, matching sklearn's LinearSVC.+-}+module DataFrame.SVM (+ LinearSVCModel (..),+ SVCConfig (..),+ defaultSVCConfig,+ svcMarginExprs,+) where++import Data.List (sort)+import qualified Data.Map.Strict as M+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.Featurize.Internal (+ affineExpr,+ argMaxExpr,+ featureNames,+ numericMatrix,+ targetValues,+ )+import DataFrame.Internal.Column (Columnable)+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr)+import DataFrame.LinearSolver (LinearModel (..), SolverConfig (..), fitProx)+import DataFrame.LinearSolver.Loss (sqHingeLoss)+import DataFrame.Model (Fit (..), Predict (..))++-- | Hyper-parameters. @svcC@ is the inverse regularization strength (sklearn @C@).+data SVCConfig = SVCConfig+ { svcC :: !Double+ , svcMaxIter :: !Int+ , svcTol :: !Double+ }+ deriving (Eq, Show)++defaultSVCConfig :: SVCConfig+defaultSVCConfig = SVCConfig{svcC = 1.0, svcMaxIter = 1000, svcTol = 1.0e-4}++-- | A fitted one-vs-rest linear SVC: class labels and their margin sub-models.+data LinearSVCModel a = LinearSVCModel+ { svcClasses :: !(V.Vector a)+ , svcModels :: !(V.Vector LinearModel)+ }+ deriving (Eq, Show)++instance (Columnable a, Ord a) => Fit SVCConfig (Expr a) (LinearSVCModel a) where+ fit = fitLinearSVC++instance (Columnable a, Ord a) => Predict (LinearSVCModel a) a where+ predict m = argMaxExpr (labelledMargins m)++-- | Fit a one-vs-rest linear SVC.+fitLinearSVC ::+ (Columnable a, Ord a) =>+ SVCConfig -> Expr a -> DataFrame -> LinearSVCModel a+fitLinearSVC cfg target df =+ LinearSVCModel (V.fromList classes) (V.fromList (map fitOne classes))+ where+ names = featureNames target df+ (nameVec, mat) = numericMatrix names df+ ys = targetValues target df+ classes = sort (foldr dedup [] (V.toList ys))+ dedup x acc = if x `elem` acc then acc else x : acc+ solverCfg =+ SolverConfig+ { scL1Lambda = 0+ , scL2Lambda = 1 / svcC cfg+ , scMaxIter = svcMaxIter cfg+ , scTol = svcTol cfg+ , scSampleWeights = Nothing+ }+ fitOne c =+ let labels =+ VU.generate (V.length ys) (\i -> if ys V.! i == c then 1 else -1)+ in fitProx sqHingeLoss solverCfg mat labels nameVec++-- | The raw margin expression for each class.+svcMarginExprs ::+ (Columnable a, Ord a) => LinearSVCModel a -> M.Map a (Expr Double)+svcMarginExprs m = M.fromList (labelledMargins m)++labelledMargins :: LinearSVCModel a -> [(a, Expr Double)]+labelledMargins m =+ [ (svcClasses m V.! i, marginOf (svcModels m V.! i))+ | i <- [0 .. V.length (svcClasses m) - 1]+ ]++marginOf :: LinearModel -> Expr Double+marginOf m =+ affineExpr+ (lmIntercept m)+ (zip (VU.toList (lmWeights m)) (V.toList (lmFeatureNames m)))
+ src/DataFrame/SVM/RFF.hs view
@@ -0,0 +1,146 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UndecidableInstances #-}++{- | Approximate RBF-kernel SVM via Random Fourier Features (Rahimi & Recht): map+each row through @z(x) = √(2/D)·cos(W x + b)@ with @W ~ N(0, 2γI)@ (seeded), then+fit a linear SVC in the random-feature space. 'predict' compiles to a closed+@Σ_r β_r·cos(…)@ expression of size @O(D·d)@, independent of the row count.+-}+module DataFrame.SVM.RFF (+ RFFConfig (..),+ defaultRFFConfig,+ RFFSVMModel (..),+) where++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.Featurize.Internal (featureNames, numericMatrix, targetValues)+import qualified DataFrame.Functions as F+import DataFrame.Internal.Column (Columnable)+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr (..))+import DataFrame.LinearAlgebra (dot)+import DataFrame.LinearSolver (LinearModel (..), SolverConfig (..), fitProx)+import DataFrame.LinearSolver.Loss (sqHingeLoss)+import DataFrame.Model (Fit (..), Predict (..))+import DataFrame.Operators ((.*.), (.+.), (.>.))+import DataFrame.Random (Gen, gaussianVector, mkGen, nextDouble)++data RFFConfig = RFFConfig+ { rffD :: !Int+ , rffGamma :: !Double+ , rffC :: !Double+ , rffMaxIter :: !Int+ , rffTol :: !Double+ , rffSeed :: !Int+ }+ deriving (Eq, Show)++defaultRFFConfig :: RFFConfig+defaultRFFConfig =+ RFFConfig+ { rffD = 100+ , rffGamma = 0.1+ , rffC = 1.0+ , rffMaxIter = 1000+ , rffTol = 1.0e-4+ , rffSeed = 0+ }++{- | A fitted RFF SVM (binary). 'rffW' / 'rffB' are the random projection;+'rffCoef' / 'rffIntercept' the linear SVC in feature space.+-}+data RFFSVMModel a = RFFSVMModel+ { rffW :: !(V.Vector (VU.Vector Double))+ , rffB :: !(VU.Vector Double)+ , rffCoef :: !(VU.Vector Double)+ , rffIntercept :: !Double+ , rffScale :: !Double+ , rffNegClass :: !a+ , rffPosClass :: !a+ , rffFeatureNames :: !(V.Vector T.Text)+ }+ deriving (Show)++instance (Columnable a, Ord a) => Fit RFFConfig (Expr a) (RFFSVMModel a) where+ fit = fitRFFSVM++instance (Columnable a) => Predict (RFFSVMModel a) a where+ predict m =+ If (margin .>. F.lit 0) (Lit (rffPosClass m)) (Lit (rffNegClass m))+ where+ names = V.toList (rffFeatureNames m)+ margin =+ foldr (.+.) (F.lit (rffIntercept m)) $+ [ F.lit (rffCoef m VU.! r * rffScale m) .*. cosTerm r+ | r <- [0 .. V.length (rffW m) - 1]+ , rffCoef m VU.! r /= 0+ ]+ cosTerm r = cos (linComb (rffW m V.! r) (rffB m VU.! r))+ linComb w b =+ foldr (.+.) (F.lit b) $+ [ F.lit (w VU.! j) .*. (Col n :: Expr Double)+ | (j, n) <- zip [0 ..] names+ ]++-- | Fit a binary RFF SVM. Targets with more than two classes are rejected.+fitRFFSVM ::+ (Columnable a, Ord a) => RFFConfig -> Expr a -> DataFrame -> RFFSVMModel a+fitRFFSVM cfg target df =+ case classes of+ [neg, pos] -> build neg pos+ _ -> error "fitRFFSVM: binary classification only (got /= 2 classes)"+ where+ names = featureNames target df+ (nameVec, mat) = numericMatrix names df+ ys = targetValues target df+ classes = sort (foldr dedup [] (V.toList ys))+ dedup x acc = if x `elem` acc then acc else x : acc+ d = if V.null mat then 0 else VU.length (V.head mat)+ bigD = max 1 (rffD cfg)+ (ws, bs) = sampleRFF bigD d (rffGamma cfg) (mkGen (rffSeed cfg))+ scale = sqrt (2 / fromIntegral bigD)+ z = V.map (featureRow ws bs scale) mat+ featNames = V.fromList ["rff" <> T.pack (show r) | r <- [0 .. bigD - 1]]+ build neg pos =+ let labels = VU.generate (V.length ys) (\i -> if ys V.! i == pos then 1 else -1)+ solverCfg =+ SolverConfig+ { scL1Lambda = 0+ , scL2Lambda = 1 / rffC cfg+ , scMaxIter = rffMaxIter cfg+ , scTol = rffTol cfg+ , scSampleWeights = Nothing+ }+ model = fitProx sqHingeLoss solverCfg z labels featNames+ in RFFSVMModel ws bs (lmWeights model) (lmIntercept model) scale neg pos nameVec++sampleRFF ::+ Int -> Int -> Double -> Gen -> (V.Vector (VU.Vector Double), VU.Vector Double)+sampleRFF bigD d gamma g0 = (V.fromList ws, VU.fromList bs)+ where+ sigma = sqrt (2 * gamma)+ (ws, g1) = goW bigD g0 []+ goW 0 g acc = (reverse acc, g)+ goW k g acc =+ let (vec, g') = gaussianVector d g+ in goW (k - 1) g' (VU.map (* sigma) vec : acc)+ bs = take bigD (goB g1)+ goB g = let (u, g') = nextDouble g in (u * 2 * pi) : goB g'++featureRow ::+ V.Vector (VU.Vector Double) ->+ VU.Vector Double ->+ Double ->+ VU.Vector Double ->+ VU.Vector Double+featureRow ws bs scale x =+ VU.generate (V.length ws) $ \r ->+ scale * cos (dot (ws V.! r) x + bs VU.! r)
+ src/DataFrame/SymbolicRegression.hs view
@@ -0,0 +1,120 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}++{- | Symbolic regression by genetic programming (modelled on the+@symbolic-regression@ library, ported dependency-light: no e-graphs, no NLOPT).+'predict' is the best discovered @Expr Double@; the search also returns the+accuracy-vs-complexity Pareto front. Deterministic given the seed.+-}+module DataFrame.SymbolicRegression (+ UnOp (..),+ SRConfig (..),+ defaultSRConfig,+ SRModel (..),+) where++import Control.Exception (throw)+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.Featurize.Internal (featureNames, targetDoubles)+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr (..))+import DataFrame.Model (Fit (..), Predict (..))+import DataFrame.Operations.Core (columnAsDoubleVector)+import DataFrame.Random (mkGen)+import DataFrame.SymbolicRegression.Expr (+ UnOp (..),+ allUnOps,+ toDataFrameExpr,+ )+import DataFrame.SymbolicRegression.GP (GPParams (..), runGP)+import DataFrame.SymbolicRegression.Simplify (simplify)++data SRConfig = SRConfig+ { srSeed :: !Int+ , srPopSize :: !Int+ , srGenerations :: !Int+ , srMaxSize :: !Int+ , srTournament :: !Int+ , srCrossoverP :: !Double+ , srMutationP :: !Double+ , srOptimizeP :: !Double+ , srParsimony :: !Double+ , srUnaryOps :: ![UnOp]+ }+ deriving (Eq, Show)++defaultSRConfig :: SRConfig+defaultSRConfig =+ SRConfig+ { srSeed = 42+ , srPopSize = 200+ , srGenerations = 40+ , srMaxSize = 25+ , srTournament = 5+ , srCrossoverP = 0.9+ , srMutationP = 0.3+ , srOptimizeP = 0.15+ , srParsimony = 1.0e-3+ , srUnaryOps = allUnOps+ }++{- | A fitted symbolic regressor. 'srBest' is the lowest-error expression;+'srPareto' is the @(complexity, mse, expr)@ frontier.+-}+data SRModel = SRModel+ { srBest :: !(Expr Double)+ , srBestMSE :: !Double+ , srPareto :: ![(Int, Double, Expr Double)]+ , srGenerationsRun :: !Int+ }++instance Fit SRConfig (Expr Double) SRModel where+ fit = fitSymbolicRegression++instance Predict SRModel Double where+ predict = srBest++-- | Search for an expression predicting @target@ from the other columns.+fitSymbolicRegression :: SRConfig -> Expr Double -> DataFrame -> SRModel+fitSymbolicRegression cfg target df =+ SRModel+ { srBest = translate best+ , srBestMSE = bestMse+ , srPareto = [(sz, mse, translate e) | (sz, mse, e) <- front]+ , srGenerationsRun = gens+ }+ where+ names = featureNames target df+ nameVec = V.fromList names+ cols = V.fromList (map (materialize df . Col) names)+ target' = targetDoubles target df+ n = VU.length target'+ params =+ GPParams+ { gpFeats = cols+ , gpN = n+ , gpTarget = target'+ , gpNVars = length names+ , gpUnOps = srUnaryOps cfg+ , gpPopSize = srPopSize cfg+ , gpGenerations = srGenerations cfg+ , gpMaxSize = srMaxSize cfg+ , gpTournament = srTournament cfg+ , gpCrossoverP = srCrossoverP cfg+ , gpMutationP = srMutationP cfg+ , gpOptimizeP = srOptimizeP cfg+ , gpParsimony = srParsimony cfg+ }+ (best, front, gens) = runGP params (mkGen (srSeed cfg))+ bestMse = case [m | (_, m, e) <- front, e == best] of+ (m : _) -> m+ [] -> 1 / 0+ translate = toDataFrameExpr nameVec . simplify++materialize :: DataFrame -> Expr Double -> VU.Vector Double+materialize df e = case columnAsDoubleVector e df of+ Right v -> v+ Left err -> throw err
+ src/DataFrame/SymbolicRegression/Expr.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE FlexibleContexts #-}++{- | The symbolic-regression expression tree: a small first-order ADT (no hegg,+no 'Fix'). Vectorized evaluation over a feature matrix and a total translation+to a dataframe 'Expr Double' (the SR result IS a dataframe expression). Division,+log, and sqrt are protected so evaluation never produces @NaN@.+-}+module DataFrame.SymbolicRegression.Expr (+ SRExpr (..),+ BinOp (..),+ UnOp (..),+ evalSR,+ toDataFrameExpr,+ srSize,+ constants,+ setConstants,+ allBinOps,+ allUnOps,+) where++import qualified Data.Text as T+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import qualified DataFrame.Functions as F+import DataFrame.Internal.Expression (Expr (..))+import DataFrame.Operators ((.*.), (.+.), (.-.), (./.))++data BinOp = SAdd | SSub | SMul | SDiv+ deriving (Eq, Ord, Show, Enum, Bounded)++data UnOp = SNeg | SSin | SCos | SExp | SLog | SSqrt+ deriving (Eq, Ord, Show, Enum, Bounded)++-- | A symbolic-regression expression over feature variables and constants.+data SRExpr+ = SVar !Int+ | SConst !Double+ | SUn !UnOp SRExpr+ | SBin !BinOp SRExpr SRExpr+ deriving (Eq, Ord, Show)++allBinOps :: [BinOp]+allBinOps = [minBound .. maxBound]++allUnOps :: [UnOp]+allUnOps = [minBound .. maxBound]++{- | Evaluate over a feature matrix given column-major (@feats ! j@ is feature+@j@ across all rows). Protected operators keep results finite.+-}+evalSR :: V.Vector (VU.Vector Double) -> Int -> SRExpr -> VU.Vector Double+evalSR feats n = go+ where+ go (SVar j)+ | j < V.length feats = feats V.! j+ | otherwise = VU.replicate n 0+ go (SConst c) = VU.replicate n c+ go (SUn op e) = VU.map (unFn op) (go e)+ go (SBin op a b) = VU.zipWith (binFn op) (go a) (go b)++binFn :: BinOp -> Double -> Double -> Double+binFn SAdd a b = a + b+binFn SSub a b = a - b+binFn SMul a b = a * b+binFn SDiv a b = if abs b < 1e-9 then 1 else a / b++unFn :: UnOp -> Double -> Double+unFn SNeg = negate+unFn SSin = sin+unFn SCos = cos+unFn SExp = exp . min 50+unFn SLog = \x -> log (abs x + 1e-9)+unFn SSqrt = sqrt . abs++-- | Translate to a dataframe expression over the named feature columns.+toDataFrameExpr :: V.Vector T.Text -> SRExpr -> Expr Double+toDataFrameExpr names = go+ where+ go (SVar j)+ | j < V.length names = Col (names V.! j)+ | otherwise = F.lit 0+ go (SConst c) = F.lit c+ go (SUn op e) = unExpr op (go e)+ go (SBin op a b) = binExpr op (go a) (go b)+ unExpr SNeg = negate+ unExpr SSin = sin+ unExpr SCos = cos+ unExpr SExp = exp+ unExpr SLog = log+ unExpr SSqrt = sqrt+ binExpr SAdd = (.+.)+ binExpr SSub = (.-.)+ binExpr SMul = (.*.)+ binExpr SDiv = (./.)++srSize :: SRExpr -> Int+srSize (SVar _) = 1+srSize (SConst _) = 1+srSize (SUn _ e) = 1 + srSize e+srSize (SBin _ a b) = 1 + srSize a + srSize b++-- | The constant values in left-to-right traversal order.+constants :: SRExpr -> [Double]+constants (SConst c) = [c]+constants (SVar _) = []+constants (SUn _ e) = constants e+constants (SBin _ a b) = constants a ++ constants b++-- | Replace the constants in traversal order; extra values are ignored.+setConstants :: [Double] -> SRExpr -> SRExpr+setConstants vals e = fst (go vals e)+ where+ go vs (SConst _) = case vs of+ (v : rest) -> (SConst v, rest)+ [] -> (SConst 0, [])+ go vs (SVar j) = (SVar j, vs)+ go vs (SUn op a) = let (a', vs') = go vs a in (SUn op a', vs')+ go vs (SBin op a b) =+ let (a', vs') = go vs a+ (b', vs'') = go vs' b+ in (SBin op a' b', vs'')
+ src/DataFrame/SymbolicRegression/GP.hs view
@@ -0,0 +1,207 @@+{- | A compact generational genetic-programming search over 'SRExpr': ramped+random initialization, tournament selection, subtree crossover and mutation,+elitism, and a complexity-keyed Pareto archive. Deterministic given the seed+(the splitmix generator from "DataFrame.Random").+-}+module DataFrame.SymbolicRegression.GP (+ GPParams (..),+ runGP,+) where++import Data.List (foldl', minimumBy, sortBy)+import qualified Data.Map.Strict as M+import Data.Ord (comparing)+import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.Random (Gen, nextDouble, nextIntR)+import DataFrame.SymbolicRegression.Expr+import DataFrame.SymbolicRegression.Optimize (+ meanSquaredError,+ optimizeConstants,+ )+import DataFrame.SymbolicRegression.Simplify (simplify)++-- | GP hyper-parameters resolved from the public config.+data GPParams = GPParams+ { gpFeats :: !(V.Vector (VU.Vector Double))+ , gpN :: !Int+ , gpTarget :: !(VU.Vector Double)+ , gpNVars :: !Int+ , gpUnOps :: ![UnOp]+ , gpPopSize :: !Int+ , gpGenerations :: !Int+ , gpMaxSize :: !Int+ , gpTournament :: !Int+ , gpCrossoverP :: !Double+ , gpMutationP :: !Double+ , gpOptimizeP :: !Double+ , gpParsimony :: !Double+ }++type Scored = (SRExpr, Double)++-- | Run the search; returns @(best, pareto front, generations run)@.+runGP :: GPParams -> Gen -> (SRExpr, [(Int, Double, SRExpr)], Int)+runGP p g0 =+ let (pop0, g1) = initPop p g0+ scored0 = map (scoreOf p) pop0+ arch0 = foldl' (archiveInsert p) M.empty scored0+ (_, finalArch, gN, _) =+ iterate' 0 scored0 arch0 g1+ best = bestOfArchive finalArch+ front =+ [ (sz, mse, e)+ | (sz, (mse, e)) <- M.toList finalArch+ ]+ in (snd3 best, sortBy (comparing fst3) front, gN)+ where+ iterate' gen pop arch g+ | gen >= gpGenerations p = (pop, arch, gen, g)+ | otherwise =+ let (pop', g') = nextGen p pop g+ arch' = foldl' (archiveInsert p) arch pop'+ in iterate' (gen + 1) pop' arch' g'+ fst3 (a, _, _) = a+ snd3 (_, b, _) = b+ bestOfArchive arch =+ case M.toList arch of+ [] -> (0 :: Int, SConst 0, 1 / 0)+ xs ->+ let (sz, (mse, e)) = minimumBy (comparing (fst . snd)) xs+ in (sz, e, mse)++scoreOf :: GPParams -> SRExpr -> Scored+scoreOf p e = (e, meanSquaredError (gpFeats p) (gpN p) (gpTarget p) e)++fitness :: GPParams -> Scored -> Double+fitness p (e, mse) = mse + gpParsimony p * fromIntegral (srSize e)++archiveInsert ::+ GPParams -> M.Map Int (Double, SRExpr) -> Scored -> M.Map Int (Double, SRExpr)+archiveInsert _ arch (e, mse)+ | isNaN mse || isInfinite mse = arch+ | otherwise =+ let key = srSize (simplify e)+ in M.insertWith better key (mse, e) arch+ where+ better newv@(m1, _) oldv@(m2, _) = if m1 < m2 then newv else oldv++initPop :: GPParams -> Gen -> ([SRExpr], Gen)+initPop p = go (gpPopSize p) []+ where+ go 0 acc g = (acc, g)+ go k acc g =+ let (depth, g1) = nextIntR (1, 4) g+ (e, g2) = randomExpr p depth g1+ in go (k - 1) (e : acc) g2++randomExpr :: GPParams -> Int -> Gen -> (SRExpr, Gen)+randomExpr p depth g+ | depth <= 1 = randomLeaf p g+ | otherwise =+ let (r, g1) = nextDouble g+ in if r < 0.3+ then randomLeaf p g1+ else+ let (isUn, g2) = nextDouble g1+ in if isUn < 0.3 && not (null (gpUnOps p))+ then+ let (oi, g3) = nextIntR (0, length (gpUnOps p) - 1) g2+ (e, g4) = randomExpr p (depth - 1) g3+ in (SUn (gpUnOps p !! oi) e, g4)+ else+ let (oi, g3) = nextIntR (0, length allBinOps - 1) g2+ (a, g4) = randomExpr p (depth - 1) g3+ (b, g5) = randomExpr p (depth - 1) g4+ in (SBin (allBinOps !! oi) a b, g5)++randomLeaf :: GPParams -> Gen -> (SRExpr, Gen)+randomLeaf p g =+ let (r, g1) = nextDouble g+ in if r < 0.6 && gpNVars p > 0+ then let (j, g2) = nextIntR (0, gpNVars p - 1) g1 in (SVar j, g2)+ else let (c, g2) = nextDouble g1 in (SConst (c * 4 - 2), g2)++nextGen :: GPParams -> [Scored] -> Gen -> ([Scored], Gen)+nextGen p pop g0 =+ let elite = minimumBy (comparing (fitness p)) pop+ (rest, g1) = go (gpPopSize p - 1) [] g0+ in (elite : rest, g1)+ where+ go 0 acc g = (acc, g)+ go k acc g =+ let (child, g') = breed p pop g+ scored = optimizeMaybe p child g'+ in go (k - 1) (fst scored : acc) (snd scored)++optimizeMaybe :: GPParams -> SRExpr -> Gen -> (Scored, Gen)+optimizeMaybe p e g =+ let (r, g1) = nextDouble g+ e' =+ if r < gpOptimizeP p+ then optimizeConstants (gpFeats p) (gpN p) (gpTarget p) 15 e+ else e+ in (scoreOf p e', g1)++breed :: GPParams -> [Scored] -> Gen -> (SRExpr, Gen)+breed p pop g0 =+ let (pa, g1) = tournament p pop g0+ (doX, g2) = nextDouble g1+ (child, g3) =+ if doX < gpCrossoverP p+ then+ let (pb, g2') = tournament p pop g2+ (c, g3') = crossover pa pb g2'+ in (c, g3')+ else (pa, g2)+ (doM, g4) = nextDouble g3+ (child', g5) =+ if doM < gpMutationP p then mutate p child g4 else (child, g4)+ capped = if srSize child' > gpMaxSize p then pa else child'+ in (simplify capped, g5)++tournament :: GPParams -> [Scored] -> Gen -> (SRExpr, Gen)+tournament p pop g0 =+ let (picks, g1) = pickN (gpTournament p) g0+ chosen = map (pop !!) picks+ in (fst (minimumBy (comparing (fitness p)) chosen), g1)+ where+ n = length pop+ pickN 0 g = ([], g)+ pickN k g =+ let (i, g') = nextIntR (0, n - 1) g+ (is, g'') = pickN (k - 1) g'+ in (i : is, g'')++crossover :: SRExpr -> SRExpr -> Gen -> (SRExpr, Gen)+crossover a b g0 =+ let (ia, g1) = nextIntR (0, srSize a - 1) g0+ (ib, g2) = nextIntR (0, srSize b - 1) g1+ sub = subtreeAt ib b+ in (replaceAt ia a sub, g2)++mutate :: GPParams -> SRExpr -> Gen -> (SRExpr, Gen)+mutate p e g0 =+ let (i, g1) = nextIntR (0, srSize e - 1) g0+ (depth, g2) = nextIntR (1, 3) g1+ (newSub, g3) = randomExpr p depth g2+ in (replaceAt i e newSub, g3)++subtreeAt :: Int -> SRExpr -> SRExpr+subtreeAt 0 e = e+subtreeAt i (SUn _ e) = subtreeAt (i - 1) e+subtreeAt i (SBin _ a b) =+ let sa = srSize a+ in if i <= sa then subtreeAt (i - 1) a else subtreeAt (i - 1 - sa) b+subtreeAt _ e = e++replaceAt :: Int -> SRExpr -> SRExpr -> SRExpr+replaceAt 0 _ new = new+replaceAt i (SUn op e) new = SUn op (replaceAt (i - 1) e new)+replaceAt i (SBin op a b) new =+ let sa = srSize a+ in if i <= sa+ then SBin op (replaceAt (i - 1) a new) b+ else SBin op a (replaceAt (i - 1 - sa) b new)+replaceAt _ e _ = e
+ src/DataFrame/SymbolicRegression/Optimize.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE BangPatterns #-}++{- | Constant optimization for symbolic-regression candidates: finite-difference+gradient descent with backtracking line search on the embedded constants. Pure+and dependency-free; effective at the one-to-few constants a tree carries, which+is where random restarts plus a quasi-Newton step would otherwise be used.+-}+module DataFrame.SymbolicRegression.Optimize (+ optimizeConstants,+ meanSquaredError,+) where++import qualified Data.Vector as V+import qualified Data.Vector.Unboxed as VU++import DataFrame.SymbolicRegression.Expr (+ SRExpr,+ constants,+ evalSR,+ setConstants,+ )++-- | Mean squared error of an expression's predictions against the target.+meanSquaredError ::+ V.Vector (VU.Vector Double) -> Int -> VU.Vector Double -> SRExpr -> Double+meanSquaredError feats n target e =+ let pred = evalSR feats n e+ diff = VU.zipWith (-) pred target+ in VU.sum (VU.map (\x -> x * x) diff) / fromIntegral (max 1 n)++-- | Refine an expression's constants to reduce MSE (no-op when constant-free).+optimizeConstants ::+ V.Vector (VU.Vector Double) ->+ Int ->+ VU.Vector Double ->+ Int ->+ SRExpr ->+ SRExpr+optimizeConstants feats n target iters expr+ | null theta0 = expr+ | otherwise = setConstants (descend iters theta0) expr+ where+ theta0 = constants expr+ eps = 1e-6+ mseAt theta = meanSquaredError feats n target (setConstants theta expr)+ descend 0 theta = theta+ descend k theta =+ let f0 = mseAt theta+ g = numGrad theta+ gn = sqrt (sum (map (\x -> x * x) g))+ in if gn < 1e-10+ then theta+ else+ let theta' = lineSearch theta g f0+ in if theta' == theta then theta else descend (k - 1) theta'+ numGrad theta =+ [ (mseAt (bump i eps theta) - mseAt (bump i (negate eps) theta)) / (2 * eps)+ | i <- [0 .. length theta - 1]+ ]+ bump i delta theta =+ [if j == i then t + delta else t | (j, t) <- zip [0 ..] theta]+ lineSearch theta g f0 = go (1.0 :: Double)+ where+ go !step+ | step < 1e-8 = theta+ | otherwise =+ let theta' = zipWith (\t gi -> t - step * gi) theta g+ in if mseAt theta' < f0 then theta' else go (step / 2)
+ src/DataFrame/SymbolicRegression/Simplify.hs view
@@ -0,0 +1,65 @@+{- | A fuel-bounded, deterministic algebraic simplifier — the dependency-light+stand-in for equality saturation. It is used as a canonical dedup key for the+Pareto archive and to tidy reported expressions; no confluence is claimed, only+that it is a total, size-non-increasing, idempotent function whose rewrites+preserve evaluation.+-}+module DataFrame.SymbolicRegression.Simplify (+ simplify,+) where++import DataFrame.SymbolicRegression.Expr (BinOp (..), SRExpr (..), UnOp (..))++-- | Simplify to a fixed point (bounded by a fuel counter).+simplify :: SRExpr -> SRExpr+simplify = go (10 :: Int)+ where+ go 0 e = e+ go fuel e =+ let e' = step e+ in if e' == e then e else go (fuel - 1) e'++step :: SRExpr -> SRExpr+step (SUn op e) = simplifyUn op (step e)+step (SBin op a b) = simplifyBin op (step a) (step b)+step e = e++simplifyUn :: UnOp -> SRExpr -> SRExpr+simplifyUn SNeg (SUn SNeg e) = e+simplifyUn op (SConst c) = SConst (foldUn op c)+simplifyUn op e = SUn op e++simplifyBin :: BinOp -> SRExpr -> SRExpr -> SRExpr+simplifyBin op (SConst a) (SConst b) = SConst (foldBin op a b)+simplifyBin SAdd a (SConst 0) = a+simplifyBin SAdd (SConst 0) b = b+simplifyBin SSub a (SConst 0) = a+simplifyBin SSub a b | a == b = SConst 0+simplifyBin SMul _ (SConst 0) = SConst 0+simplifyBin SMul (SConst 0) _ = SConst 0+simplifyBin SMul a (SConst 1) = a+simplifyBin SMul (SConst 1) b = b+simplifyBin SDiv a (SConst 1) = a+simplifyBin SDiv a b | a == b = SConst 1+simplifyBin op a b+ | commutative op && a > b = SBin op b a+ | otherwise = SBin op a b++commutative :: BinOp -> Bool+commutative SAdd = True+commutative SMul = True+commutative _ = False++foldBin :: BinOp -> Double -> Double -> Double+foldBin SAdd a b = a + b+foldBin SSub a b = a - b+foldBin SMul a b = a * b+foldBin SDiv a b = if abs b < 1e-9 then 1 else a / b++foldUn :: UnOp -> Double -> Double+foldUn SNeg = negate+foldUn SSin = sin+foldUn SCos = cos+foldUn SExp = exp . min 50+foldUn SLog = \x -> log (abs x + 1e-9)+foldUn SSqrt = sqrt . abs
src/DataFrame/Synthesis.hs view
@@ -1,483 +1,371 @@ {-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExplicitNamespaces #-}-{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE UndecidableInstances #-} -module DataFrame.Synthesis where+{- | 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. +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.++The engine:++ * enumerates programs by increasing AST size (so the first representative of any+ behaviour is the smallest — interpretability for free);+ * evaluates each candidate /incrementally/ by combining the cached result+ vectors of its subprograms (one vector op), never re-interpreting the whole+ tree;+ * keeps exactly one program per /observational-equivalence/ class — candidates+ producing the same column (up to a float tolerance) are interchangeable, so+ duplicates are dropped rather than re-explored;+ * breaks commutative symmetry (never both @a+b@ and @b+a@) and uses protected+ operators (@sqrt|x|@, @log(|x|+1)@) plus a denominator guard so domain errors+ never arise;+ * caps each size layer by fit score when it grows large (a cost-guided+ tractability bound over /distinct/ behaviours, not a lossy beam over raw+ syntax).++'fit' returns the best 'SynthesizedFeature'; 'predict' is its expression.+'synthesizeFeatures' returns the whole ranked, deduplicated feature bank — useful+as automated feature engineering feeding a downstream model.++Deferred (documented next steps, not yet implemented): skeleton enumeration with+closed-form least-squares coefficient fitting, hard-row counterexample sampling+for very large frames, and piecewise (condition-abduction) features.+-}+module DataFrame.Synthesis (+ LossFunction (..),+ SynthesisConfig (..),+ defaultSynthesisConfig,+ SynthesizedFeature (..),+ synthesizeFeatures,+) where++import Data.Bits (xor)+import Data.Either (fromRight)+import Data.List (sortBy)+import qualified Data.Map.Strict as M+import Data.Maybe (fromMaybe)+import Data.Ord (Down (..), comparing)+import qualified Data.Text as T+import qualified Data.Vector.Unboxed as VU+import Data.Word (Word64)+import GHC.Float (castDoubleToWord64)++import DataFrame.Featurize.Internal (featureNames) import qualified DataFrame.Functions as F-import DataFrame.Internal.Column-import DataFrame.Internal.DataFrame (- DataFrame (..),- columnNames,- )-import DataFrame.Internal.Expression (- Expr (..),- eSize,- eqExpr,+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (Expr (..))+import DataFrame.Internal.Statistics (+ meanSquaredError,+ mutualInformationBinned,+ percentile',+ variance', )-import DataFrame.Internal.Interpreter (interpret)-import DataFrame.Internal.Statistics+import DataFrame.Model (Fit (..), Predict (..)) import DataFrame.Operations.Core (columnAsDoubleVector)-import qualified DataFrame.Operations.Statistics as Stats-import DataFrame.Operations.Subset (exclude) -import Control.Exception (throw)-import Data.Function-import qualified Data.List as L-import qualified Data.Map as M-import Data.Maybe (listToMaybe)-import qualified Data.Text as T-import Data.Type.Equality-import qualified Data.Vector.Unboxed as VU-import DataFrame.Operators-import Debug.Trace (trace)-import Type.Reflection (typeRep)+-- | How a candidate's output column is scored against the target (higher is better).+data LossFunction+ = -- | Pearson @r²@: scale-invariant, the default for derived features.+ PearsonCorrelation+ | -- | Binned mutual information: captures nonlinear association.+ MutualInformation+ | -- | Negative mean squared error: for reproducing a target exactly.+ MeanSquaredError+ deriving (Eq, Show) -generateConditions ::- TypedColumn Double -> [Expr Bool] -> [Expr Double] -> DataFrame -> [Expr Bool]-generateConditions labels conds ps df =- let- newConds =- [ p .<= q- | p <- filter (not . isLiteral) ps- , q <- ps- , Prelude.not (eqExpr p q)- ]- ++ [ F.not p- | p <- conds- ]- expandedConds =- conds- ++ newConds- ++ [p .&& q | p <- newConds, q <- conds, Prelude.not (eqExpr p q)]- ++ [p .|| q | p <- newConds, q <- conds, Prelude.not (eqExpr p q)]- in- pickTopNBool df labels (deduplicate df expandedConds)+-- | Search hyperparameters.+data SynthesisConfig = SynthesisConfig+ { synMaxSize :: !Int+ -- ^ Largest AST (node count) to enumerate.+ , synBankCap :: !Int+ -- ^ Max observationally-distinct programs kept per size layer.+ , synLoss :: !LossFunction+ , synTopK :: !Int+ -- ^ How many ranked features to return in the bank.+ }+ deriving (Eq, Show) -generatePrograms ::- Bool ->- [Expr Bool] ->- [Expr Double] ->- [Expr Double] ->- [Expr Double] ->- [Expr Double]-generatePrograms _ _ vars' constants [] = vars' ++ constants-generatePrograms includeConds conds vars constants ps =- let- existingPrograms = ps ++ vars ++ constants- in- existingPrograms- ++ [ transform p- | p <- ps ++ vars- , Prelude.not (isConditional p)- , transform <-- [ sqrt- , abs- , log . (+ Lit 1)- , exp- , sin- , cos- , F.relu- , signum- ]- ]- ++ [ F.pow p i- | p <- existingPrograms- , Prelude.not (isConditional p)- , i <- [2 .. 6]- ]- ++ [ p + q- | (i, p) <- zip [(0 :: Int) ..] existingPrograms- , (j, q) <- zip [(0 :: Int) ..] existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , i >= j- ]- ++ [ p - q- | (i, p) <- zip [(0 :: Int) ..] existingPrograms- , (j, q) <- zip [(0 :: Int) ..] existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , i /= j- ]- ++ ( if includeConds- then- [ F.min p q- | (i, p) <- zip [(0 :: Int) ..] existingPrograms- , (j, q) <- zip [(0 :: Int) ..] existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , Prelude.not (eqExpr p q)- , i > j- ]- ++ [ F.max p q- | (i, p) <- zip [(0 :: Int) ..] existingPrograms- , (j, q) <- zip [(0 :: Int) ..] existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , Prelude.not (eqExpr p q)- , i > j- ]- ++ [ F.ifThenElse cond r s- | cond <- conds- , r <- existingPrograms- , s <- existingPrograms- , Prelude.not (isConditional r || isConditional s)- , Prelude.not (eqExpr r s)- ]- else []- )- ++ [ p * q- | (i, p) <- zip [(0 :: Int) ..] existingPrograms- , (j, q) <- zip [(0 :: Int) ..] existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , i >= j- ]- ++ [ p / q- | p <- existingPrograms- , q <- existingPrograms- , Prelude.not (isLiteral p && isLiteral q)- , Prelude.not (isConditional p || isConditional q)- , Prelude.not (eqExpr p q)- ]+defaultSynthesisConfig :: SynthesisConfig+defaultSynthesisConfig =+ SynthesisConfig+ { synMaxSize = 6+ , synBankCap = 500+ , synLoss = PearsonCorrelation+ , synTopK = 16+ } -isLiteral :: Expr a -> Bool-isLiteral (Lit _) = True-isLiteral _ = False+{- | A synthesized feature. 'sfExpr' is the best-scoring expression and 'sfFeatures'+is the ranked, observationally-distinct bank (expression and its score).+-}+data SynthesizedFeature = SynthesizedFeature+ { sfExpr :: !(Expr Double)+ , sfScore :: !Double+ , sfFeatures :: ![(Expr Double, Double)]+ } -isConditional :: Expr a -> Bool-isConditional (If{}) = True-isConditional _ = False+instance Fit SynthesisConfig (Expr Double) SynthesizedFeature where+ fit = synthesizeFeatures -deduplicate ::- forall a.- (Columnable a) =>- DataFrame ->- [Expr a] ->- [(Expr a, TypedColumn a)]-deduplicate df = go [] . L.nubBy eqExpr . L.sortBy (\e1 e2 -> compare (eSize e1) (eSize e2))+instance Predict SynthesizedFeature Double where+ predict = sfExpr++-- | A candidate's evaluated column over the example rows.+type Output = VU.Vector Double++data Prog = Prog+ { progExpr :: !(Expr Double)+ , progSize :: !Int+ , progOut :: !Output+ }++-- | Search for expressions over the non-target columns that track @target@.+synthesizeFeatures ::+ SynthesisConfig -> Expr Double -> DataFrame -> SynthesizedFeature+synthesizeFeatures cfg target df+ | null leaves || VU.null tgt = SynthesizedFeature (Lit 0) (negate (1 / 0)) []+ | otherwise = SynthesizedFeature best bestScore ranked where- go _ [] = []- go seen (x : xs)- | hasInvalid = go seen xs- | res `elem` seen = go seen xs- | otherwise = (x, res) : go (res : seen) xs- where- res = case interpret @a df x of- Left e -> throw e- Right v -> v- hasInvalid = case res of- (TColumn (UnboxedColumn _ (column :: VU.Vector b))) -> case testEquality (typeRep @Double) (typeRep @b) of- Just Refl -> VU.any (\n -> isNaN n || isInfinite n) column- Nothing -> False- _ -> False+ feats = featureNames target df+ tgt = fromRight VU.empty (columnAsDoubleVector target df)+ n = VU.length tgt+ leaves = mkLeaves df feats n+ bank = grow cfg tgt leaves+ scored =+ [ (progExpr p, progSize p, s)+ | p <- bank+ , Just s <- [scoreOf (synLoss cfg) tgt (progOut p)]+ ]+ sorted = sortBy (comparing (\(_, sz, s) -> (Down s, sz))) scored+ ranked = [(e, s) | (e, _, s) <- take (synTopK cfg) sorted]+ (best, bestScore) = case ranked of+ ((e, s) : _) -> (e, s)+ [] -> (Lit 0, negate (1 / 0)) --- | Checks if two programs generate the same outputs given all the same inputs.-equivalent :: DataFrame -> Expr Double -> Expr Double -> Bool-equivalent df p1 p2 = case (==) <$> interpret df p1 <*> interpret df p2 of- Left e -> throw e- Right v -> v+-- | Size-1 programs: numeric feature columns and a pool of constants, OE-deduped.+mkLeaves :: DataFrame -> [T.Text] -> Int -> [Prog]+mkLeaves df feats n = fst (dedupProgs M.empty candidates)+ where+ candidates =+ [ Prog (Col name) 1 o+ | name <- feats+ , Right o <- [columnAsDoubleVector (Col name :: Expr Double) df]+ ]+ ++ [Prog (Lit v) 1 (VU.replicate n v) | v <- constantPool df feats] -synthesizeFeatureExpr ::- -- | Target expression- T.Text ->- BeamConfig ->- DataFrame ->- Either String (Expr Double)-synthesizeFeatureExpr target cfg df =- let- df' = exclude [target] df- t = case interpret df (Col target) of- Left e -> throw e- Right v -> v- in- case beamSearch- df'- cfg- t- (percentiles df')- []- [] of- Nothing -> Left "No programs found"- Just p -> Right p+{- | Domain-informed constants: per-column quartiles, variance, and std, plus a few+small integers. (Duplicates collapse under observational equivalence.)+-}+constantPool :: DataFrame -> [T.Text] -> [Double]+constantPool df feats =+ [0, 1, 2, -1]+ ++ [ roundSig 2 v+ | name <- feats+ , Right c <- [columnAsDoubleVector (Col name :: Expr Double) df]+ , v <-+ [percentile' p c | p <- [1, 25, 75, 99]] ++ [variance' c, sqrt (variance' c)]+ ] -f1FromBinary :: VU.Vector Double -> VU.Vector Double -> Maybe Double-f1FromBinary trues preds =- let (!tp, !fp, !fn) =- VU.foldl' step (0 :: Int, 0 :: Int, 0 :: Int) $- VU.zip (VU.map (> 0) preds) (VU.map (> 0) trues)- in f1FromCounts tp fp fn+-- | Grow the bank one size layer at a time, keeping one program per OE class.+grow :: SynthesisConfig -> Output -> [Prog] -> [Prog]+grow cfg tgt leaves = go 2 leaves (foldr (seenInsert . progOut) M.empty leaves) where- step (!tp, !fp, !fn) (!p, !t) =- case (p, t) of- (True, True) -> (tp + 1, fp, fn)- (True, False) -> (tp, fp + 1, fn)- (False, True) -> (tp, fp, fn + 1)- (False, False) -> (tp, fp, fn)+ go size bank seen+ | size > synMaxSize cfg = bank+ | otherwise =+ let (kept, seen') = absorb cfg tgt seen (layer size bank)+ in go (size + 1) (bank ++ kept) seen' -f1FromCounts :: Int -> Int -> Int -> Maybe Double-f1FromCounts tp fp fn =- let tp' = fromIntegral tp- fp' = fromIntegral fp- fn' = fromIntegral fn- precision = if tp' + fp' == 0 then 0 else tp' / (tp' + fp')- recall = if tp' + fn' == 0 then 0 else tp' / (tp' + fn')- in if precision + recall == 0- then Nothing- else Just (2 * precision * recall / (precision + recall))+-- | All candidate programs of exactly @size@ nodes, built from smaller ones.+layer :: Int -> [Prog] -> [Prog]+layer size bank = unaries ++ pows ++ comms ++ subs ++ divs+ where+ atSize s = filter ((== s) . progSize) bank+ args1 = atSize (size - 1)+ unaries =+ [ Prog (mk e) size (VU.map f o)+ | (mk, f) <- unaryProds+ , Prog e _ o <- args1+ ]+ pows =+ [ Prog (F.pow e k) size (VU.map (^ k) o)+ | Prog e _ o <- args1+ , k <- [2 .. 6 :: Int]+ ]+ comms =+ [ Prog (mk ea eb) size (VU.zipWith f oa ob)+ | (mk, f) <- commutativeProds+ , (Prog ea _ oa, Prog eb _ ob) <- unorderedPairs size bank+ ]+ subs =+ [ Prog (ea - eb) size (VU.zipWith (-) oa ob)+ | (Prog ea _ oa, Prog eb _ ob) <- orderedPairs size bank+ ]+ divs =+ [ Prog (ea / eb) size (VU.zipWith (/) oa ob)+ | (Prog ea _ oa, Prog eb _ ob) <- orderedPairs size bank+ , VU.all ((> 1e-9) . abs) ob+ ] -fitClassifier ::- -- | Target expression- T.Text ->- -- | Depth of search (Roughly, how many terms in the final expression)- Int ->- -- | Beam size - the number of candidate expressions to consider at a time.- Int ->- DataFrame ->- Either String (Expr Int)-fitClassifier target d b df =- let- df' = exclude [target] df- t = case interpret df (Col target) of- Left e -> throw e- Right v -> v- in- case beamSearch- df'- (BeamConfig d b F1 True)- t- (percentiles df' ++ [Lit 1, Lit 0, Lit (-1)])- []- [] of- Nothing -> Left "No programs found"- Just p -> Right (F.ifThenElse (p .> (0 :: Expr Double)) 1 0)+-- | Protected unary operators: total on all reals (no NaN/domain errors).+unaryProds :: [(Expr Double -> Expr Double, Double -> Double)]+unaryProds =+ [ (sqrt . abs, sqrt . abs)+ , (abs, abs)+ , (\e -> log (abs e + 1), \x -> log (abs x + 1))+ , (exp, exp)+ , (sin, sin)+ , (cos, cos)+ , (F.relu, max 0)+ , (signum, signum)+ ] -percentiles :: DataFrame -> [Expr Double]-percentiles df =- let- doubleColumns =- map- (either throw id . ((`columnAsDoubleVector` df) . Col @Double))- (columnNames df)- in- concatMap- (\c -> map (Lit . roundTo2SigDigits . (`percentile'` c)) [1, 25, 75, 99])- doubleColumns- ++ map (Lit . roundTo2SigDigits . variance') doubleColumns- ++ map (Lit . roundTo2SigDigits . sqrt . variance') doubleColumns+-- | Commutative binary operators (enumerated over unordered operand pairs).+commutativeProds ::+ [(Expr Double -> Expr Double -> Expr Double, Double -> Double -> Double)]+commutativeProds =+ [ ((+), (+))+ , ((*), (*))+ , (F.min, min)+ , (F.max, max)+ ] -roundToSigDigits :: Int -> Double -> Double-roundToSigDigits n x- | x == 0 = 0- | otherwise =- let magnitude = floor (logBase 10 (abs x))- scale = 10 ** fromIntegral (n - 1 - magnitude)- in fromIntegral (round (x * scale) :: Int) / scale+-- | Ordered operand pairs whose sizes sum to @size-1@ (for non-commutative ops).+orderedPairs :: Int -> [Prog] -> [(Prog, Prog)]+orderedPairs size bank =+ [ (a, b)+ | sa <- [1 .. size - 2]+ , let sb = size - 1 - sa+ , sb >= 1+ , a <- atSize sa+ , b <- atSize sb+ ]+ where+ atSize s = filter ((== s) . progSize) bank -roundTo2SigDigits :: Double -> Double-roundTo2SigDigits = roundToSigDigits 2+-- | Unordered operand pairs (for commutative ops): each pair once.+unorderedPairs :: Int -> [Prog] -> [(Prog, Prog)]+unorderedPairs size bank =+ [ (a, b)+ | sa <- [1 .. size - 2]+ , let sb = size - 1 - sa+ , sb >= 1+ , sa <= sb+ , (i, a) <- zip [0 :: Int ..] (atSize sa)+ , (j, b) <- zip [0 :: Int ..] (atSize sb)+ , sa < sb || i <= j+ ]+ where+ atSize s = filter ((== s) . progSize) bank -fitRegression ::- -- | Target expression- T.Text ->- -- | Depth of search (Roughly, how many terms in the final expression)- Int ->- -- | Beam size - the number of candidate expressions to consider at a time.- Int ->- DataFrame ->- Either String (Expr Double)-fitRegression target d b df =- let- df' = exclude [target] df- targetMean = Stats.mean (Col @Double target) df- t = case interpret df (Col target) of- Left e -> throw e- Right v -> v- cfg = BeamConfig d b MeanSquaredError True- constants =- percentiles df'- ++ [Lit targetMean]- ++ [ F.pow p i- | i <- [1 .. 6]- , p <- [Lit 10, Lit 1, Lit 0.1]- ]- in- case beamSearch df' cfg t constants [] [] of- Nothing -> Left "No programs found"- Just p -> Right p+{- | Keep the valid, observationally-novel candidates of a layer, then cap by fit+score (cost-guided). Returns the kept programs and the updated OE-class set.+-}+absorb ::+ SynthesisConfig -> Output -> Seen -> [Prog] -> ([Prog], Seen)+absorb cfg tgt seen0 cands = (capLayer cfg tgt fresh, seen')+ where+ (fresh, seen') = dedupProgs seen0 cands -data LossFunction- = PearsonCorrelation- | MutualInformation- | MeanSquaredError- | F1+-- | When a layer has more distinct programs than the cap, keep the best-scoring.+capLayer :: SynthesisConfig -> Output -> [Prog] -> [Prog]+capLayer cfg tgt progs+ | length progs <= synBankCap cfg = progs+ | otherwise = take (synBankCap cfg) (sortBy (comparing (Down . rank)) progs)+ where+ rank p = fromMaybe (negate (1 / 0)) (scoreOf (synLoss cfg) tgt (progOut p)) -getLossFunction ::- LossFunction -> (VU.Vector Double -> VU.Vector Double -> Maybe Double)-getLossFunction f = case f of- MutualInformation ->- ( \l r ->- mutualInformationBinned- (Prelude.max 10 (ceiling (sqrt (fromIntegral (VU.length l) :: Double))))- l- r- )- PearsonCorrelation -> (\l r -> (^ (2 :: Int)) <$> correlation' l r)- MeanSquaredError -> (\l r -> fmap negate (meanSquaredError l r))- F1 -> f1FromBinary+-- | Fit score of an output against the target (higher is better), or @Nothing@.+scoreOf :: LossFunction -> Output -> Output -> Maybe Double+scoreOf lf tgt out+ | VU.length out /= VU.length tgt = Nothing+ | otherwise = finite $ case lf of+ PearsonCorrelation -> pearsonR2 tgt out+ MutualInformation -> mutualInformationBinned bins tgt out+ MeanSquaredError -> negate <$> meanSquaredError tgt out+ where+ bins = max 10 (ceiling (sqrt (fromIntegral (VU.length tgt) :: Double)))+ -- Belt-and-suspenders: drop any non-finite score so it cannot win the ranking.+ finite (Just s) | isNaN s || isInfinite s = Nothing+ finite ms = ms -data BeamConfig = BeamConfig- { searchDepth :: Int- , beamLength :: Int- , lossFunction :: LossFunction- , includeConditionals :: Bool- }+{- | Pearson @r²@ via the numerically stable centered two-pass formula. Returns+'Nothing' when the feature (or target) is constant, and is bounded by+Cauchy–Schwarz to @[0,1]@ — unlike the one-pass @n·Σxy − Σx·Σy@ form, which+cancels catastrophically for low-variance features and can report @r² > 1@.+-}+pearsonR2 :: Output -> Output -> Maybe Double+pearsonR2 ys xs+ | n < 2 = Nothing+ | sxx <= 0 || syy <= 0 = Nothing+ | otherwise = Just (min 1 (sxy * sxy / (sxx * syy)))+ where+ n = VU.length xs+ nf = fromIntegral n+ mx = VU.sum xs / nf+ my = VU.sum ys / nf+ sxy = VU.sum (VU.zipWith (\x y -> (x - mx) * (y - my)) xs ys)+ sxx = VU.sum (VU.map (\x -> (x - mx) * (x - mx)) xs)+ syy = VU.sum (VU.map (\y -> (y - my) * (y - my)) ys) -defaultBeamConfig :: BeamConfig-defaultBeamConfig = BeamConfig 2 100 PearsonCorrelation False+-- | An output is usable iff it is non-empty and free of NaN/±Inf.+valid :: Output -> Bool+valid o = not (VU.null o) && VU.all (\x -> not (isNaN x || isInfinite x)) o -beamSearch ::- DataFrame ->- -- | Parameters of the beam search.- BeamConfig ->- -- | Examples- TypedColumn Double ->- -- | Constants- [Expr Double] ->- -- | Conditions- [Expr Bool] ->- -- | Programs- [Expr Double] ->- Maybe (Expr Double)-beamSearch df cfg outputs constants conds programs- | searchDepth cfg == 0 = case ps of- [] -> Nothing- (x : _) -> Just x- | otherwise =- beamSearch- df- (cfg{searchDepth = searchDepth cfg - 1})- outputs- constants- conditions- (generatePrograms (includeConditionals cfg) conditions vars constants ps)+{- | Quantize an output to nine significant digits, so float noise (@x*2@ vs+@x+x@) collapses while genuinely distinct features stay apart.+-}+quantize :: Output -> Output+quantize = VU.map (\x -> if x == 0 then 0 else signum x * roundSig 9 (abs x))++{- | The observational-equivalence class set: a map from a 64-bit FNV-1a+fingerprint of the quantized output to the (usually one) quantized outputs with+that fingerprint. Bucketing on the fingerprint keeps membership cheap, and+verifying exact equality within the bucket makes a hash collision harmless — two+genuinely different columns that happen to collide are kept apart, not merged.+-}+type Seen = M.Map Int [Output]++-- | FNV-1a fingerprint of an already-quantized output's bit patterns.+fpOf :: Output -> Int+fpOf = fromIntegral . VU.foldl' step (1469598103934665603 :: Word64) where- vars = map Col names- conditions = generateConditions outputs conds (vars ++ constants) df- ps = pickTopN df outputs cfg $ deduplicate df programs- names = (map fst . L.sortBy (compare `on` snd) . M.toList . columnIndices) df+ step !h x = (h `xor` castDoubleToWord64 x) * 1099511628211 -pickTopN ::- DataFrame ->- TypedColumn Double ->- BeamConfig ->- [(Expr Double, TypedColumn a)] ->- [Expr Double]-pickTopN _ _ _ [] = []-pickTopN df (TColumn column) cfg ps =- let- l = case toVector @Double @VU.Vector column of- Left e -> throw e- Right v -> v- ordered =- Prelude.take- (beamLength cfg)- ( map fst $- L.sortBy- ( \(_, c2) (_, c1) ->- if maybe False isInfinite c1- || maybe False isInfinite c2- || maybe False isNaN c1- || maybe False isNaN c2- then LT- else compare c1 c2- )- ( map- (\(e, res) -> (e, getLossFunction (lossFunction cfg) l (asDoubleVector res)))- ps- )- )- asDoubleVector c =- let- (TColumn col') = c- in- case toVector @Double @VU.Vector col' of- Left e -> throw e- Right v -> VU.convert v- interpretDoubleVector e' =- let- (TColumn col') = case interpret df e' of- Left err -> throw err- Right v -> v- in- case toVector @Double @VU.Vector col' of- Left err -> throw err- Right v -> VU.convert v- in- trace- ( "Best loss: "- ++ show- ( getLossFunction (lossFunction cfg) l . interpretDoubleVector- <$> listToMaybe ordered- )- ++ " "- ++ (if null ordered then "empty" else show (listToMaybe ordered))- )- ordered+-- | Record an output's observational-equivalence class.+seenInsert :: Output -> Seen -> Seen+seenInsert o = M.insertWith (++) (fpOf q) [q]+ where+ q = quantize o -pickTopNBool ::- DataFrame ->- TypedColumn Double ->- [(Expr Bool, TypedColumn Bool)] ->- [Expr Bool]-pickTopNBool _ _ [] = []-pickTopNBool _df (TColumn column) ps =- let- l = case toVector @Double @VU.Vector column of- Left e -> throw e- Right v -> v- ordered =- Prelude.take- 10- ( map fst $- L.sortBy- ( \(_, c2) (_, c1) ->- if maybe False isInfinite c1- || maybe False isInfinite c2- || maybe False isNaN c1- || maybe False isNaN c2- then LT- else compare c1 c2- )- ( map- (\(e, res) -> (e, getLossFunction MutualInformation l (asDoubleVector res)))- ps- )- )- asDoubleVector c =- let- (TColumn col') = c- in- case toVector @Bool @VU.Vector col' of- Left e -> throw e- Right v -> VU.map (fromIntegral @Int @Double . fromEnum) v- in- ordered+-- | Round a positive double to @n@ significant digits.+roundSig :: Int -> Double -> Double+roundSig n x+ | x == 0 = 0+ | otherwise =+ let magnitude = floor (logBase 10 (abs x)) :: Int+ scale = 10 ** fromIntegral (n - 1 - magnitude)+ in fromIntegral (round (x * scale) :: Integer) / scale -satisfiesExamples :: DataFrame -> TypedColumn Double -> Expr Double -> Bool-satisfiesExamples df column expr =- let- result = case interpret df expr of- Left e -> throw e- Right v -> v- in- result == column+{- | Keep the first valid program of each observational-equivalence class,+preserving order; returns the kept programs and the grown class set.+-}+dedupProgs :: Seen -> [Prog] -> ([Prog], Seen)+dedupProgs = go []+ where+ go acc s [] = (reverse acc, s)+ go acc s (p : ps)+ | not (valid o) = go acc s ps+ | member = go acc s ps+ | otherwise = go (p : acc) (M.insertWith (++) fp [q] s) ps+ where+ o = progOut p+ q = quantize o+ fp = fpOf q+ member = maybe False (q `elem`) (M.lookup fp s)
+ src/DataFrame/Transform.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++{- | Fitted column transforms as a composable monoid. A 'Transform' is a list of+named output expressions; @s <> t@ means \"apply @s@, then @t@\", fusing @t@'s+references to @s@'s outputs by simultaneous substitution. 'applyTransform' runs+one against a frame; 'compileThrough' folds a transform into a model's+prediction expression so the result is a single expression over the raw inputs.++Every right-hand side must be row-wise (no aggregation/window), and within one+transform each expression reads the original frame.+-}+module DataFrame.Transform (+ Transform (..),+ applyTransform,+ compileThrough,+ ScalerModel (..),+ standardScaler,+ scalerTransform,+) where++import Control.Exception (throw)+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 qualified DataFrame.Functions as F+import DataFrame.Internal.Column (Columnable)+import DataFrame.Internal.DataFrame (DataFrame)+import DataFrame.Internal.Expression (+ Expr (..),+ NamedExpr,+ UExpr (..),+ substituteColumns,+ )+import DataFrame.Operations.Core (columnAsDoubleVector)+import DataFrame.Operations.Transformations (deriveMany)+import DataFrame.Operators ((.-.), (./.))++-- | A fitted transform: named output columns derived from the input frame.+newtype Transform = Transform {transformOutputs :: [NamedExpr]}++instance Semigroup Transform where+ Transform s <> Transform t =+ Transform (s ++ map (subst (M.fromList s)) t)+ where+ subst :: M.Map T.Text UExpr -> NamedExpr -> NamedExpr+ subst m (nm, UExpr e) = (nm, UExpr (substituteColumns m e))++instance Monoid Transform where+ mempty = Transform []++-- | Apply a transform to a frame (deriving its outputs in order).+applyTransform :: Transform -> DataFrame -> DataFrame+applyTransform (Transform os) = deriveMany os++{- | Fold a preprocessing transform into a model's prediction expression,+yielding one expression over the transform's input columns.+-}+compileThrough :: (Columnable a) => Transform -> Expr a -> Expr a+compileThrough (Transform os) = substituteColumns (M.fromList os)++-- | A fitted standardizer: per-column means and standard deviations.+data ScalerModel = ScalerModel+ { smColumns :: !(V.Vector T.Text)+ , smMeans :: !(VU.Vector Double)+ , smStds :: !(VU.Vector Double)+ }+ deriving (Eq, Show)++-- | Fit a standard scaler over the named columns.+standardScaler :: [T.Text] -> DataFrame -> ScalerModel+standardScaler names df =+ ScalerModel (V.fromList names) (VU.fromList means) (VU.fromList stds)+ where+ cols = map column names+ column n = case columnAsDoubleVector (F.col @Double n) df of+ Right v -> v+ Left e -> throw e+ means = [VU.sum c / fromIntegral (max 1 (VU.length c)) | c <- cols]+ stds =+ [ let mu = mean+ v =+ VU.sum (VU.map (\x -> (x - mu) ^ (2 :: Int)) c)+ / fromIntegral (max 1 (VU.length c))+ s = sqrt v+ in if s < 1e-12 then 1 else s+ | (mean, c) <- zip means cols+ ]++-- | The scaler as a 'Transform': @(col - μ) / σ@ per column.+scalerTransform :: ScalerModel -> Transform+scalerTransform m =+ Transform+ [ (n, UExpr ((F.col @Double n .-. F.lit mu) ./. F.lit sigma))+ | (n, mu, sigma) <-+ zip3+ (V.toList (smColumns m))+ (VU.toList (smMeans m))+ (VU.toList (smStds m))+ ]