packages feed

dataframe-learn-1.1.0.1: src/DataFrame/Featurize/Internal.hs

{-# 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 DataFrame.Errors (DataFrameException (..), TypeErrorContext (..))
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 (asFeatureError name e)

asFeatureError :: T.Text -> DataFrameException -> DataFrameException
asFeatureError name (TypeMismatchException ctx) =
    TypeMismatchException
        ctx
            { errorColumnName = Just (T.unpack name)
            , callingFunctionName =
                Just
                    "model fit (feature columns must be numeric Double; drop or encode non-numeric columns)"
            }
asFeatureError _ e = 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 = case cols of
        (x: _) -> VU.length x
        _      -> 0
    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)