packages feed

dataframe-learn-2.0.0.0: src/DataFrame/SymbolicRegression.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

{- | 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 (
    module DataFrame.Model,
    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
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) where
    type ModelOf SRConfig (Expr Double) = SRModel
    fit = fitSymbolicRegression

instance Predict SRModel where
    type Prediction SRModel = Expr Double
    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