packages feed

dataframe-operations-1.0.0.0: src/DataFrame/Typed/Operations.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module DataFrame.Typed.Operations (
    -- * Schema-preserving operations
    filterWhere,
    filter,
    filterBy,
    filterAllJust,
    filterJust,
    filterNothing,
    sortBy,
    take,
    takeLast,
    drop,
    dropLast,
    range,
    cube,
    distinct,
    sample,
    shuffle,

    -- * Schema-modifying operations
    derive,
    impute,
    select,
    exclude,
    rename,
    renameMany,
    insert,
    insertColumn,
    insertVector,
    cloneColumn,
    dropColumn,
    replaceColumn,

    -- * Metadata
    dimensions,
    nRows,
    nColumns,
    columnNames,

    -- * Vertical merge
    append,
) where

import Data.Proxy (Proxy (..))
import qualified Data.Text as T
import qualified Data.Vector as V
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
import System.Random (RandomGen)
import Prelude hiding (drop, filter, take)

import qualified DataFrame.Functions as DF
import DataFrame.Internal.Column (Columnable)
import qualified DataFrame.Internal.Column as C
import qualified DataFrame.Internal.DataFrame as D
import qualified DataFrame.Operations.Aggregation as DA
import qualified DataFrame.Operations.Core as D
import DataFrame.Operations.Merge ()
import qualified DataFrame.Operations.Permutation as D
import qualified DataFrame.Operations.Subset as D
import qualified DataFrame.Operations.Transformations as D

import DataFrame.Typed.Freeze (unsafeFreeze)
import DataFrame.Typed.Schema
import DataFrame.Typed.Types (TExpr (..), TSortOrder (..), TypedDataFrame (..))
import qualified DataFrame.Typed.Types as T

-------------------------------------------------------------------------------
-- Schema-preserving operations
-------------------------------------------------------------------------------

{- | Filter rows where a boolean expression evaluates to True.
The expression is validated against the schema at compile time.
-}
filterWhere :: TExpr cols Bool -> TypedDataFrame cols -> TypedDataFrame cols
filterWhere (TExpr expr) (TDF df) = TDF (D.filterWhere expr df)

-- | Filter rows by applying a predicate to a typed expression.
filter ::
    (Columnable a) =>
    TExpr cols a -> (a -> Bool) -> TypedDataFrame cols -> TypedDataFrame cols
filter (TExpr expr) pred' (TDF df) = TDF (D.filter expr pred' df)

-- | Filter rows by a predicate on a column expression (flipped argument order).
filterBy ::
    (Columnable a) =>
    (a -> Bool) -> TExpr cols a -> TypedDataFrame cols -> TypedDataFrame cols
filterBy pred' (TExpr expr) (TDF df) = TDF (D.filterBy pred' expr df)

{- | Keep only rows where ALL Optional columns have Just values.
Strips 'Maybe' from all column types in the result schema.

@
df :: TDF '[Column \"x\" (Maybe Double), Column \"y\" Int]
filterAllJust df :: TDF '[Column \"x\" Double, Column \"y\" Int]
@
-}
filterAllJust :: TypedDataFrame cols -> TypedDataFrame (StripAllMaybe cols)
filterAllJust (TDF df) = unsafeFreeze (D.filterAllJust df)

{- | Keep only rows where the named column has Just values.
Strips 'Maybe' from that column's type in the result schema.

@
filterJust \@\"x\" df
@
-}
filterJust ::
    forall name cols.
    ( KnownSymbol name
    , AssertPresent name cols
    ) =>
    TypedDataFrame cols -> TypedDataFrame (StripMaybeAt name cols)
filterJust (TDF df) = unsafeFreeze (D.filterJust colName df)
  where
    colName = T.pack (symbolVal (Proxy @name))

{- | Keep only rows where the named column has Nothing.
Schema is preserved (column types unchanged, just fewer rows).
-}
filterNothing ::
    forall name cols.
    ( KnownSymbol name
    , AssertPresent name cols
    ) =>
    TypedDataFrame cols -> TypedDataFrame cols
filterNothing (TDF df) = TDF (D.filterNothing colName df)
  where
    colName = T.pack (symbolVal (Proxy @name))

{- | Sort by the given typed sort orders.
Sort orders reference columns that are validated against the schema.
-}
sortBy :: [TSortOrder cols] -> TypedDataFrame cols -> TypedDataFrame cols
sortBy ords (TDF df) = TDF (D.sortBy (map toUntypedSort ords) df)
  where
    toUntypedSort :: TSortOrder cols -> D.SortOrder
    toUntypedSort (Asc (TExpr e)) = D.Asc e
    toUntypedSort (Desc (TExpr e)) = D.Desc e

-- | Take the first @n@ rows.
take :: Int -> TypedDataFrame cols -> TypedDataFrame cols
take n (TDF df) = TDF (D.take n df)

-- | Take the last @n@ rows.
takeLast :: Int -> TypedDataFrame cols -> TypedDataFrame cols
takeLast n (TDF df) = TDF (D.takeLast n df)

-- | Drop the first @n@ rows.
drop :: Int -> TypedDataFrame cols -> TypedDataFrame cols
drop n (TDF df) = TDF (D.drop n df)

-- | Drop the last @n@ rows.
dropLast :: Int -> TypedDataFrame cols -> TypedDataFrame cols
dropLast n (TDF df) = TDF (D.dropLast n df)

-- | Take rows in the given range (start, end).
range :: (Int, Int) -> TypedDataFrame cols -> TypedDataFrame cols
range r (TDF df) = TDF (D.range r df)

-- | Take a sub-cube of the DataFrame.
cube :: (Int, Int) -> TypedDataFrame cols -> TypedDataFrame cols
cube c (TDF df) = TDF (D.cube c df)

-- | Remove duplicate rows.
distinct :: TypedDataFrame cols -> TypedDataFrame cols
distinct (TDF df) = TDF (DA.distinct df)

-- | Randomly sample a fraction of rows.
sample ::
    (RandomGen g) => g -> Double -> TypedDataFrame cols -> TypedDataFrame cols
sample g frac (TDF df) = TDF (D.sample g frac df)

-- | Shuffle all rows randomly.
shuffle :: (RandomGen g) => g -> TypedDataFrame cols -> TypedDataFrame cols
shuffle g (TDF df) = TDF (D.shuffle g df)

-------------------------------------------------------------------------------
-- Schema-modifying operations
-------------------------------------------------------------------------------

{- | Derive a new column from a typed expression. The column name must NOT
already exist in the schema (enforced at compile time via 'AssertAbsent').
The expression is validated against the current schema.

@
df' = derive \@\"total\" (col \@\"price\" * col \@\"qty\") df
-- df' :: TDF (Column \"total\" Double ': originalCols)
@
-}
derive ::
    forall name a cols.
    ( KnownSymbol name
    , Columnable a
    , AssertAbsent name cols
    ) =>
    TExpr cols a ->
    TypedDataFrame cols ->
    TypedDataFrame (Snoc cols (T.Column name a))
derive (TExpr expr) (TDF df) = unsafeFreeze (D.derive colName expr df)
  where
    colName = T.pack (symbolVal (Proxy @name))

impute ::
    forall name a cols.
    ( KnownSymbol name
    , Columnable a
    , Maybe a ~ Lookup name cols
    ) =>
    a ->
    TypedDataFrame cols ->
    TypedDataFrame (Impute name cols)
impute value (TDF df) =
    unsafeFreeze
        (D.derive colName (DF.fromMaybe value (DF.col @(Maybe a) colName)) df)
  where
    colName = T.pack (symbolVal (Proxy @name))

-- | Select a subset of columns by name.
select ::
    forall (names :: [Symbol]) cols.
    (AllKnownSymbol names, AssertAllPresent names cols) =>
    TypedDataFrame cols -> TypedDataFrame (SubsetSchema names cols)
select (TDF df) = unsafeFreeze (D.select (symbolVals @names) df)

-- | Exclude columns by name.
exclude ::
    forall (names :: [Symbol]) cols.
    (AllKnownSymbol names) =>
    TypedDataFrame cols -> TypedDataFrame (ExcludeSchema names cols)
exclude (TDF df) = unsafeFreeze (D.exclude (symbolVals @names) df)

-- | Rename a column.
rename ::
    forall old new cols.
    (KnownSymbol old, KnownSymbol new) =>
    TypedDataFrame cols -> TypedDataFrame (RenameInSchema old new cols)
rename (TDF df) = unsafeFreeze (D.rename oldName newName df)
  where
    oldName = T.pack (symbolVal (Proxy @old))
    newName = T.pack (symbolVal (Proxy @new))

-- | Rename multiple columns from a type-level list of pairs.
renameMany ::
    forall (pairs :: [(Symbol, Symbol)]) cols.
    (AllKnownPairs pairs) =>
    TypedDataFrame cols -> TypedDataFrame (RenameManyInSchema pairs cols)
renameMany (TDF df) = unsafeFreeze (foldRenames (pairVals @pairs) df)
  where
    foldRenames [] df' = df'
    foldRenames ((old, new) : rest) df' = foldRenames rest (D.rename old new df')

-- | Insert a new column from a Foldable container.
insert ::
    forall name a cols t.
    ( KnownSymbol name
    , Columnable a
    , Foldable t
    , AssertAbsent name cols
    ) =>
    t a -> TypedDataFrame cols -> TypedDataFrame (T.Column name a ': cols)
insert xs (TDF df) = unsafeFreeze (D.insert colName xs df)
  where
    colName = T.pack (symbolVal (Proxy @name))

-- | Insert a raw 'Column' value.
insertColumn ::
    forall name a cols.
    ( KnownSymbol name
    , Columnable a
    , AssertAbsent name cols
    ) =>
    C.Column -> TypedDataFrame cols -> TypedDataFrame (T.Column name a ': cols)
insertColumn col (TDF df) = unsafeFreeze (D.insertColumn colName col df)
  where
    colName = T.pack (symbolVal (Proxy @name))

-- | Insert a boxed 'Vector'.
insertVector ::
    forall name a cols.
    ( KnownSymbol name
    , Columnable a
    , AssertAbsent name cols
    ) =>
    V.Vector a -> TypedDataFrame cols -> TypedDataFrame (T.Column name a ': cols)
insertVector vec (TDF df) = unsafeFreeze (D.insertVector colName vec df)
  where
    colName = T.pack (symbolVal (Proxy @name))

-- | Clone an existing column under a new name.
cloneColumn ::
    forall old new cols.
    ( KnownSymbol old
    , KnownSymbol new
    , AssertPresent old cols
    , AssertAbsent new cols
    ) =>
    TypedDataFrame cols -> TypedDataFrame (T.Column new (Lookup old cols) ': cols)
cloneColumn (TDF df) = unsafeFreeze (D.cloneColumn oldName newName df)
  where
    oldName = T.pack (symbolVal (Proxy @old))
    newName = T.pack (symbolVal (Proxy @new))

-- | Drop a column by name.
dropColumn ::
    forall name cols.
    ( KnownSymbol name
    , AssertPresent name cols
    ) =>
    TypedDataFrame cols -> TypedDataFrame (RemoveColumn name cols)
dropColumn (TDF df) = unsafeFreeze (D.exclude [colName] df)
  where
    colName = T.pack (symbolVal (Proxy @name))

{- | Replace an existing column with new values derived from a typed expression.
The column must already exist and the new type must match.
-}
replaceColumn ::
    forall name a cols.
    ( KnownSymbol name
    , Columnable a
    , a ~ SafeLookup name cols
    , AssertPresent name cols
    ) =>
    TExpr cols a -> TypedDataFrame cols -> TypedDataFrame cols
replaceColumn (TExpr expr) (TDF df) = unsafeFreeze (D.derive colName expr df)
  where
    colName = T.pack (symbolVal (Proxy @name))

-- | Vertically merge two DataFrames with the same schema.
append :: TypedDataFrame cols -> TypedDataFrame cols -> TypedDataFrame cols
append (TDF a) (TDF b) = TDF (a <> b)

-------------------------------------------------------------------------------
-- Metadata (pass-through)
-------------------------------------------------------------------------------

dimensions :: TypedDataFrame cols -> (Int, Int)
dimensions (TDF df) = D.dimensions df

nRows :: TypedDataFrame cols -> Int
nRows (TDF df) = D.nRows df

nColumns :: TypedDataFrame cols -> Int
nColumns (TDF df) = D.nColumns df

columnNames :: TypedDataFrame cols -> [T.Text]
columnNames (TDF df) = D.columnNames df

-------------------------------------------------------------------------------
-- Internal helpers
-------------------------------------------------------------------------------

-- | Helper class for extracting [(Text, Text)] from type-level pairs.
class AllKnownPairs (pairs :: [(Symbol, Symbol)]) where
    pairVals :: [(T.Text, T.Text)]

instance AllKnownPairs '[] where
    pairVals = []

instance
    (KnownSymbol a, KnownSymbol b, AllKnownPairs rest) =>
    AllKnownPairs ('(a, b) ': rest)
    where
    pairVals =
        ( T.pack (symbolVal (Proxy @a))
        , T.pack (symbolVal (Proxy @b))
        )
            : pairVals @rest