packages feed

dataframe-operations-2.3.0.0: src/DataFrame/Operations/Transformations.hs

{-# LANGUAGE ConstrainedClassMethods #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE UndecidableSuperClasses #-}

module DataFrame.Operations.Transformations (
    -- * Apply
    apply,
    safeApply,
    applyMany,
    applyInt,
    applyDouble,
    applyWhere,
    applyAtIndex,

    -- * Derive
    derive,
    deriveWithExpr,
    deriveMany,

    -- * Impute
    ImputeOp (..),
    impute,
    imputeCore,
) where

import qualified Data.List as L
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Vector as V

import Control.Exception (throw)
import Data.Maybe
import DataFrame.Errors (DataFrameException (..), TypeErrorContext (..))
import DataFrame.Internal.Column (
    Column,
    Columnable,
    TypedColumn (..),
    columnTypeString,
    hasMissing,
    ifoldrColumn,
    imapColumn,
    mapColumn,
 )
import DataFrame.Internal.DataFrame (DataFrame (..), getColumn, insertColumn)
import DataFrame.Internal.Expression
import DataFrame.Internal.Interpreter
import DataFrame.Internal.Nullable (BaseType)
import DataFrame.Operations.Core
import GHC.TypeLits (ErrorMessage (..), TypeError)
import Type.Reflection (typeRep)

-- | O(k) Apply a function to a given column in a dataframe.
apply ::
    forall b c.
    (Columnable b, Columnable c) =>
    -- | function to apply
    (b -> c) ->
    -- | Column name
    T.Text ->
    -- | DataFrame to apply operation to
    DataFrame ->
    DataFrame
apply f columnName d = case safeApply f columnName d of
    Left (TypeMismatchException context) ->
        throw $ TypeMismatchException (context{callingFunctionName = Just "apply"})
    Left exception -> throw exception
    Right df -> df

-- | O(k) Safe version of the apply function. Returns (instead of throwing) the error.
safeApply ::
    forall b c.
    (Columnable b, Columnable c) =>
    -- | function to apply
    (b -> c) ->
    -- | Column name
    T.Text ->
    -- | DataFrame to apply operation to
    DataFrame ->
    Either DataFrameException DataFrame
safeApply f columnName d = case getColumn columnName d of
    Nothing ->
        Left $ ColumnsNotFoundException [columnName] "apply" (M.keys $ columnIndices d)
    Just column -> do
        column' <- mapColumn f column
        pure $ insertColumn columnName column' d

{- | O(k) Apply a function to an expression in a dataframe and
add the result into `alias` column.
-}
derive :: forall a. (Columnable a) => T.Text -> Expr a -> DataFrame -> DataFrame
derive name expr df = case interpret @a df (normalize expr) of
    Left e -> throw e
    Right (TColumn value) ->
        (insertColumn name value df)
            { derivingExpressions = M.insert name (UExpr expr) (derivingExpressions df)
            }

{- | O(k) Apply a function to an expression in a dataframe and
add the result into `alias` column but

==== __Examples__

>>> (z, df') = deriveWithExpr "z" (F.col @Int "x" + F.col "y") df
>>> filterWhere (z .>= 50)
-}
deriveWithExpr ::
    forall a. (Columnable a) => T.Text -> Expr a -> DataFrame -> (Expr a, DataFrame)
deriveWithExpr name expr df = case interpret @a df (normalize expr) of
    Left e -> throw e
    Right (TColumn value) ->
        ( Col name
        , (insertColumn name value df)
            { derivingExpressions = M.insert name (UExpr expr) (derivingExpressions df)
            }
        )

deriveMany :: [NamedExpr] -> DataFrame -> DataFrame
deriveMany exprs df =
    let
        f (name, UExpr (expr :: Expr a)) d =
            case interpret @a df expr of
                Left e -> throw e
                Right (TColumn value) -> insertColumn name value d
     in
        fold f exprs df

-- | O(k * n) Apply a function to given column names in a dataframe.
applyMany ::
    (Columnable b, Columnable c) =>
    (b -> c) ->
    [T.Text] ->
    DataFrame ->
    DataFrame
applyMany f names df = L.foldl' (flip (apply f)) df names

-- | O(k) Convenience function that applies to an int column.
applyInt ::
    (Columnable b) =>
    -- | function to apply
    (Int -> b) ->
    -- | Column name
    T.Text ->
    -- | DataFrame to apply operation to
    DataFrame ->
    DataFrame
applyInt = apply

-- | O(k) Convenience function that applies to an double column.
applyDouble ::
    (Columnable b) =>
    -- | function to apply
    (Double -> b) ->
    -- | Column name
    T.Text ->
    -- | DataFrame to apply operation to
    DataFrame ->
    DataFrame
applyDouble = apply

{- | O(k * n) Apply a function to a column only if there is another column
value that matches the given criterion.

> applyWhere (<20) "Age" (const "Gen-Z") "Generation" df
-}
applyWhere ::
    forall a b.
    (Columnable a, Columnable b) =>
    -- | Filter condition
    (a -> Bool) ->
    -- | Criterion Column
    T.Text ->
    -- | function to apply
    (b -> b) ->
    -- | Column name
    T.Text ->
    -- | DataFrame to apply operation to
    DataFrame ->
    DataFrame
applyWhere condition filterColumnName f columnName df = case getColumn filterColumnName df of
    Nothing ->
        throw $
            ColumnsNotFoundException
                [filterColumnName]
                "applyWhere"
                (M.keys $ columnIndices df)
    Just column -> case ifoldrColumn
        (\i val acc -> if condition val then V.cons i acc else acc)
        V.empty
        column of
        Left e -> throw e
        Right indexes ->
            if V.null indexes
                then df
                else L.foldl' (\d i -> applyAtIndex i f columnName d) df indexes

-- | O(k) Apply a function to the column at a given index.
applyAtIndex ::
    forall a.
    (Columnable a) =>
    -- | Index
    Int ->
    -- | function to apply
    (a -> a) ->
    -- | Column name
    T.Text ->
    -- | DataFrame to apply operation to
    DataFrame ->
    DataFrame
applyAtIndex i f columnName df = case getColumn columnName df of
    Nothing ->
        throw $
            ColumnsNotFoundException [columnName] "applyAtIndex" (M.keys $ columnIndices df)
    Just column -> case imapColumn (\index value -> if index == i then f value else value) column of
        Left e -> throw e
        Right column' -> insertColumn columnName column' df

{- | Core impute implementation for nullable columns. A column with no null
bitmap cannot hold a missing value, so imputing it is a mistake, not a no-op.
-}
imputeCore ::
    forall b.
    (Columnable b) =>
    Expr (Maybe b) ->
    b ->
    DataFrame ->
    DataFrame
imputeCore (Col columnName) value df = case getColumn columnName df of
    Nothing ->
        throw $
            ColumnsNotFoundException [columnName] "impute" (M.keys $ columnIndices df)
    Just col | hasMissing col -> case safeApply (fromMaybe value) columnName df of
        Left (TypeMismatchException context) -> throw $ TypeMismatchException (context{callingFunctionName = Just "impute"})
        Left exception -> throw exception
        Right res -> res
    Just col ->
        throw
            (nonNullableColumnError columnName ("Maybe " ++ show (typeRep @b)) col)
imputeCore expr _ _ = throw (NonColumnReferenceException (T.pack (show expr)))

{- | 'impute' was handed a column that carries no null bitmap, so there is
nothing it could replace.
-}
nonNullableColumnError :: T.Text -> String -> Column -> DataFrameException
nonNullableColumnError columnName wanted col =
    TypeMismatchException @() @()
        MkTypeErrorContext
            { userType = Left wanted
            , expectedType = Left (columnTypeString col)
            , errorColumnName = Just (T.unpack columnName)
            , callingFunctionName = Just "impute"
            }

class (Columnable a) => ImputeOp a where
    runImpute :: Expr a -> BaseType a -> DataFrame -> DataFrame
    runImputeWith ::
        (Columnable (BaseType a)) =>
        (Expr (BaseType a) -> Expr (BaseType a)) ->
        Expr a ->
        DataFrame ->
        DataFrame

instance
    {-# OVERLAPPABLE #-}
    ( Columnable a
    , TypeError
        ( 'Text "impute needs a nullable column, but was given Expr "
            ':<>: 'ShowType a
            ':$$: 'Text "Try F.col @(Maybe "
            ':<>: 'ShowType a
            ':<>: 'Text ") instead."
        )
    ) =>
    ImputeOp a
    where
    runImpute = errorWithoutStackTrace "impute: unreachable"
    runImputeWith = errorWithoutStackTrace "impute: unreachable"

{- | Replace all instances of `Nothing` in a column with the given value.
Throws when the column carries no nulls to replace.
-}
impute ::
    forall a.
    (ImputeOp a) =>
    Expr a ->
    BaseType a ->
    DataFrame ->
    DataFrame
impute = runImpute