packages feed

dataframe-learn-1.1.0.0: src/DataFrame/Transform.hs

{-# 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))
        ]