packages feed

dataframe-0.3.1.2: src/DataFrame/Functions.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}

module DataFrame.Functions where

import DataFrame.Internal.Column
import DataFrame.Internal.DataFrame (DataFrame (..), unsafeGetColumn)
import DataFrame.Internal.Expression (Expr (..), UExpr (..), eSize, interpret)
import DataFrame.Internal.Statistics
import DataFrame.Operations.Subset (exclude)

import Control.Monad
import qualified Data.Char as Char
import Data.Function
import qualified Data.List as L
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Data.Vector.Generic as VG
import qualified Data.Vector.Unboxed as VU
import Debug.Trace ( traceShow )
import Language.Haskell.TH
import qualified Language.Haskell.TH.Syntax as TH
import Type.Reflection (typeRep)
import Prelude hiding (sum, minimum, maximum)
import Data.Type.Equality

name :: (Show a) => Expr a -> T.Text
name (Col n) = n
name other = error $ "You must call `name` on a column reference. Not the expression: " ++ show other

col :: (Columnable a) => T.Text -> Expr a
col = Col

as :: (Columnable a) => Expr a -> T.Text -> (T.Text, UExpr)
as expr name = (name, Wrap expr)

ifThenElse :: (Columnable a) => Expr Bool -> Expr a -> Expr a -> Expr a
ifThenElse = If

lit :: (Columnable a) => a -> Expr a
lit = Lit

lift :: (Columnable a, Columnable b) => (a -> b) -> Expr a -> Expr b
lift = UnaryOp "udf"

lift2 :: (Columnable c, Columnable b, Columnable a) => (c -> b -> a) -> Expr c -> Expr b -> Expr a
lift2 = BinaryOp "udf"

(==) :: (Columnable a, Eq a) => Expr a -> Expr a -> Expr Bool
(==) = BinaryOp "eq" (Prelude.==)

(<) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool
(<) = BinaryOp "lt" (Prelude.<)

(>) :: (Columnable a, Ord a) => Expr a -> Expr a -> Expr Bool
(>) = BinaryOp "gt" (Prelude.>)

(<=) :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool
(<=) = BinaryOp "leq" (Prelude.<=)

(>=) :: (Columnable a, Ord a, Eq a) => Expr a -> Expr a -> Expr Bool
(>=) = BinaryOp "geq" (Prelude.>=)

and :: Expr Bool -> Expr Bool -> Expr Bool
and = BinaryOp "and" (&&)

or :: Expr Bool -> Expr Bool -> Expr Bool
or = BinaryOp "or" (||)

not :: Expr Bool -> Expr Bool
not = UnaryOp "not" Prelude.not

count :: (Columnable a) => Expr a -> Expr Int
count expr = AggFold expr "foldUdf" 0 (\acc _ -> acc + 1)

minimum :: (Columnable a, Ord a) => Expr a -> Expr a
minimum expr = AggReduce expr "minimum" min

maximum :: (Columnable a, Ord a) => Expr a -> Expr a
maximum expr = AggReduce expr "maximum" max

sum :: forall a. (Columnable a, Num a, VU.Unbox a) => Expr a -> Expr a
sum expr = AggNumericVector expr "sum" VG.sum

mean :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double
mean expr = AggNumericVector expr "mean" mean'

median :: Expr Double -> Expr Double
median expr = AggNumericVector expr "mean" median'

percentile :: Int -> Expr Double -> Expr Double
percentile n expr = AggNumericVector expr (T.pack $ "percentile " ++ show n) ((VU.! 0) . quantiles' (VU.fromList [n]) 100)

standardDeviation :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double
standardDeviation expr = AggNumericVector expr "stddev" (sqrt . variance')

zScore :: Expr Double -> Expr Double
zScore c = (c - mean c) / standardDeviation c

reduce :: forall a b. (Columnable a, Columnable b) => Expr b -> a -> (a -> b -> a) -> Expr a
reduce expr = AggFold expr "foldUdf"

generatePrograms :: [Expr Double] -> [Expr Double] -> [Expr Double]
generatePrograms vars existingPrograms =
    existingPrograms ++
    [ transform p
    | p <- existingPrograms
    , transform <- [zScore, abs, log . (+ Lit 1), exp]
    ] ++
    [ p + q
    | (i, p) <- zip [0..] existingPrograms
    , (j, q) <- zip [0..] existingPrograms
    , i Prelude.>= j
    ] ++
    [ ifThenElse (p DataFrame.Functions.>= percentile n p) p q
    | p <- existingPrograms
    , q <- existingPrograms ++ [lit 1, lit 0, lit (-1)]
    , n <- [1, 25, 50, 75, 99]
    ] ++
    [ p - q
    | p <- existingPrograms
    , q <- existingPrograms
    ] ++
    [ p * q
    | (i, p) <- zip [0..] existingPrograms
    , (j, q) <- zip [0..] existingPrograms
    , i Prelude.>= j
    ] ++
    [ p / q
    | p <- existingPrograms
    , q <- existingPrograms
    ] ++
    [ t p
    | v <- vars
    , p <- existingPrograms
    , t <- transformsWithVar v
    ]
  where
    transformsWithVar v =
        [ (v +)
        , (* v)
        , (/ v)
        , (v /)
        , \v' -> v' - v
        , (v -)
        ]

-- | Deduplicate programs pick the least smallest one by size.
deduplicate :: DataFrame
            -> [Expr Double]
            -> [Expr Double]
deduplicate df = go S.empty . L.sortBy (\e1 e2 -> compare (eSize e1) (eSize e2))
  where
    go _ [] = []
    go seen (x : xs)
        | hasInvalid = go seen xs
        | S.member res seen = go seen xs
        | otherwise = x : go (S.insert res seen) xs
            where
                res = interpret df x
                infinity = read @Double "Infinity"
                hasInvalid = case res of
                    (TColumn (UnboxedColumn (col :: VU.Vector a))) -> case testEquality (typeRep @Double) (typeRep @a) of
                                    Just Refl -> VU.any (\n -> isNaN n || isInfinite n) col
                                    Nothing -> False
                    _ -> False

-- | Checks if two programs generate the same outputs given all the same inputs.
equivalent :: DataFrame -> Expr Double -> Expr Double -> Bool
equivalent df p1 p2 = interpret df p1 Prelude.== interpret df p2

synthesizeFeatureExpr ::
    -- | 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)
synthesizeFeatureExpr target d b df = let
        df' = exclude [target] df
        names = (map fst . L.sortBy (compare `on` snd) . M.toList . columnIndices) df'
        variables = map col names
    in case beamSearch df' b (interpret df (Col target)) variables d of
            Nothing -> Left "No programs found"
            Just p -> Right p

beamSearch ::
    DataFrame ->
    -- | Beam size
    Int ->
    -- | Examples
    TypedColumn Double ->
    -- | Programs
    [Expr Double] ->
    -- | Search depth
    Int ->
    Maybe (Expr Double)
beamSearch df b outputs programs d
    | d Prelude.== 0 = case ps of
        []    -> Nothing
        (x:_) -> Just x
    | otherwise =
        case findFirst ps of
            Just p -> Just p
            Nothing -> beamSearch df b outputs (generatePrograms (map col names) ps) (d - 1)
  where
    ps = pickTopN df outputs b $ deduplicate df programs
    names = (map fst . L.sortBy (compare `on` snd) . M.toList . columnIndices) df
    findFirst [] = Nothing
    findFirst (p : ps')
        | satisfiesExamples df outputs p = Just p
        | otherwise = findFirst ps'

pickTopN :: DataFrame
         -> TypedColumn Double
         -> Int
         -> [Expr Double]
         -> [Expr Double]
pickTopN df (TColumn col) n ps = let
        l = VU.convert (toVector @Double col)
        ordered = take n (L.sortBy (\e e' -> let
                c1 = abs <$> correlation' l (asDoubleVector e')
                c2 = abs <$> correlation' l (asDoubleVector e)
            in if maybe False isInfinite c1 || maybe False isInfinite c2 then LT else compare c1 c2) ps)
        asDoubleVector e = let
                (TColumn col') = interpret df e
            in VU.convert (toVector @Double col')
    in ordered

satisfiesExamples :: DataFrame -> TypedColumn Double -> Expr Double -> Bool
satisfiesExamples df col expr = let
        result = interpret df expr
    in result Prelude.== col

-- See Section 2.4 of the Haskell Report https://www.haskell.org/definition/haskell2010.pdf
isReservedId :: T.Text -> Bool
isReservedId t = case t of
    "case" -> True
    "class" -> True
    "data" -> True
    "default" -> True
    "deriving" -> True
    "do" -> True
    "else" -> True
    "foreign" -> True
    "if" -> True
    "import" -> True
    "in" -> True
    "infix" -> True
    "infixl" -> True
    "infixr" -> True
    "instance" -> True
    "let" -> True
    "module" -> True
    "newtype" -> True
    "of" -> True
    "then" -> True
    "type" -> True
    "where" -> True
    _ -> False

isVarId :: T.Text -> Bool
isVarId t = case T.uncons t of
    -- We might want to check  c == '_' || Char.isLower c
    -- since the haskell report considers '_' a lowercase character
    -- However, to prevent an edge case where a user may have a
    -- "Name" and an "_Name_" in the same scope, wherein we'd end up
    -- with duplicate "_Name_"s, we eschew the check for '_' here.
    Just (c, _) -> Char.isLower c && Char.isAlpha c
    Nothing -> False

isHaskellIdentifier :: T.Text -> Bool
isHaskellIdentifier t = Prelude.not (isVarId t) || isReservedId t

sanitize :: T.Text -> T.Text
sanitize t
    | isValid = t
    | isHaskellIdentifier t' = "_" <> t' <> "_"
    | otherwise = t'
  where
    isValid =
        Prelude.not (isHaskellIdentifier t)
            && isVarId t
            && T.all Char.isAlphaNum t
    t' = T.map replaceInvalidCharacters . T.filter (Prelude.not . parentheses) $ t
    replaceInvalidCharacters c
        | Char.isUpper c = Char.toLower c
        | Char.isSpace c = '_'
        | Char.isPunctuation c = '_' -- '-' will also become a '_'
        | Char.isSymbol c = '_'
        | Char.isAlphaNum c = c -- Blanket condition
        | otherwise = '_' -- If we're unsure we'll default to an underscore
    parentheses c = case c of
        '(' -> True
        ')' -> True
        '{' -> True
        '}' -> True
        '[' -> True
        ']' -> True
        _ -> False

typeFromString :: [String] -> Q Type
typeFromString [] = fail "No type specified"
typeFromString [t] = do
    maybeType <- lookupTypeName t
    case maybeType of
        Just name -> return (ConT name)
        Nothing -> fail $ "Unsupported type: " ++ t
typeFromString [tycon, t1] = do
    outer <- typeFromString [tycon]
    inner <- typeFromString [t1]
    return (AppT outer inner)
typeFromString [tycon, t1, t2] = do
    outer <- typeFromString [tycon]
    lhs <- typeFromString [t1]
    rhs <- typeFromString [t2]
    return (AppT (AppT outer lhs) rhs)
typeFromString s = fail $ "Unsupported type: " ++ unwords s

declareColumns :: DataFrame -> DecsQ
declareColumns df =
    let
        names = (map fst . L.sortBy (compare `on` snd) . M.toList . columnIndices) df
        types = map (columnTypeString . (`unsafeGetColumn` df)) names
        specs = zipWith (\name type_ -> (name, sanitize name, type_)) names types
     in
        fmap concat $ forM specs $ \(raw, nm, tyStr) -> do
            ty <- typeFromString (words tyStr)
            traceShow (nm <> " :: Expr " <> T.pack tyStr) (pure ())
            let n = mkName (T.unpack nm)
            sig <- sigD n [t|Expr $(pure ty)|]
            val <- valD (varP n) (normalB [|col $(TH.lift raw)|]) []
            pure [sig, val]