packages feed

dataframe-operations-2.6.0.0: src/DataFrame/Operations/Permutation.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module DataFrame.Operations.Permutation (
    -- * Sorting
    SortOrder (..),
    sortBy,
    topBy,

    -- * Shuffling
    shuffle,
    shuffledIndices,
) where

import qualified Data.List as L
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Algorithms.Merge as VA
import qualified Data.Vector.Algorithms.Radix as Radix
import qualified Data.Vector.Generic as VG
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM

import Control.Exception (throw)
import Control.Monad (forM_, when)
import Control.Monad.ST (runST)
import Data.Bits (
    complement,
    countLeadingZeros,
    testBit,
    unsafeShiftR,
    xor,
    (.&.),
    (.|.),
 )
import Data.Int (Int16, Int32, Int64, Int8)
import Data.Type.Equality (testEquality, (:~:) (Refl))
import Data.Typeable (Typeable)
import Data.Vector.Internal.Check (HasCallStack)
import Data.Word (Word16, Word32, Word64, Word8)
import DataFrame.Errors (DataFrameException (..))
import DataFrame.Internal.Column (
    Column (..),
    Columnable,
    atIndicesStable,
    materializeMerged,
    sliceColumn,
 )
import DataFrame.Internal.Data.PackedText (packedSlice, sliceCmpBytes)
import DataFrame.Internal.DataFrame (
    DataFrame (..),
    columnNames,
    unsafeGetColumn,
 )
import DataFrame.Internal.Expression (Expr (Col), getColumns)
import DataFrame.Operations.Core (dimensions)
import DataFrame.Operations.Transformations (derive)
import GHC.Float (castDoubleToWord64)
import System.Random (Random (randomR), RandomGen)
import Type.Reflection (typeRep)

-- | Sort order taken as a parameter by the 'sortBy' function.
data SortOrder where
    Asc :: (Columnable a, Ord a) => Expr a -> SortOrder
    Desc :: (Columnable a, Ord a) => Expr a -> SortOrder

instance Eq SortOrder where
    (==) :: SortOrder -> SortOrder -> Bool
    (==) (Asc _) (Asc _) = True
    (==) (Desc _) (Desc _) = True
    (==) _ _ = False

sortOrderColumns :: SortOrder -> [T.Text]
sortOrderColumns (Asc e) = getColumns e
sortOrderColumns (Desc e) = getColumns e

mustFlipCompare :: SortOrder -> Bool
mustFlipCompare (Asc _) = True
mustFlipCompare (Desc _) = False

-- | Materialize compound sort expressions into synthetic columns.
prepareSortColumns :: [SortOrder] -> DataFrame -> ([SortOrder], DataFrame)
prepareSortColumns = go 0
  where
    go _ [] acc = ([], acc)
    go i (ord : rest) acc =
        let (ord', acc') = materializeSortOrder i ord acc
            (rest', acc'') = go (i + 1) rest acc'
         in (ord' : rest', acc'')

materializeSortOrder :: Int -> SortOrder -> DataFrame -> (SortOrder, DataFrame)
materializeSortOrder _ ord@(Asc (Col _)) df = (ord, df)
materializeSortOrder _ ord@(Desc (Col _)) df = (ord, df)
materializeSortOrder i (Asc (e :: Expr a)) df =
    let name = syntheticName i
     in (Asc (Col name :: Expr a), derive name e df)
materializeSortOrder i (Desc (e :: Expr a)) df =
    let name = syntheticName i
     in (Desc (Col name :: Expr a), derive name e df)

syntheticName :: Int -> T.Text
syntheticName i = "__sortBy_synthetic_" <> T.pack (show i) <> "__"

-- | O(k log n) Sort the dataframe by the given sort orders.
sortBy ::
    [SortOrder] ->
    DataFrame ->
    DataFrame
sortBy sortOrds df
    | not (null missing) =
        throw $
            ColumnsNotFoundException
                missing
                "sortBy"
                (columnNames df)
    | otherwise =
        let
            (sortOrds', df') = prepareSortColumns sortOrds df
            comparators = map (`sortOrderComparator` df') sortOrds'
            compositeCompare i j = mconcat [c i j | c <- comparators]
            nRows = fst (dataframeDimensions df')
            indexes = case sortOrds' of
                [ord] | Just ixs <- radixSortIndices ord df' -> ixs
                _ -> sortIndices compositeCompare nRows
         in
            df{columns = V.map (atIndicesStable indexes) (columns df)}
  where
    referenced = L.nub (concatMap sortOrderColumns sortOrds)
    missing = referenced L.\\ columnNames df

-- | The rows a 'sortBy' would place at positions @[offset, offset + count)@.
topBy :: Int -> Int -> [SortOrder] -> DataFrame -> DataFrame
topBy offset count sortOrds df
    | not (null missing) =
        throw $
            ColumnsNotFoundException
                missing
                "topBy"
                (columnNames df)
    | k >= nRows || k <= 0 =
        let sorted = sortBy sortOrds df
         in sorted
                { columns = V.map (sliceColumn offset' (k - offset')) (columns sorted)
                , dataframeDimensions = (k - offset', snd (dataframeDimensions df))
                }
    | otherwise =
        let
            (sortOrds', df') = prepareSortColumns sortOrds df
            comparators = map (`sortOrderComparator` df') sortOrds'
            -- Ties keep their original order, as in the stable full sort.
            cmp i j = mconcat [c i j | c <- comparators] <> compare i j
            leading = smallestIndices cmp k nRows
            window = VU.slice offset' (k - offset') (sortIndicesOf cmp leading)
         in
            df
                { columns = V.map (atIndicesStable window) (columns df)
                , dataframeDimensions = (VU.length window, snd (dataframeDimensions df))
                }
  where
    nRows = fst (dataframeDimensions df)
    k = min nRows (max 0 offset + max 0 count)
    offset' = min k (max 0 offset)
    referenced = L.nub (concatMap sortOrderColumns sortOrds)
    missing = referenced L.\\ columnNames df

-- | The @k@ smallest of @[0, n)@ under @cmp@, unordered (a bounded max-heap).
smallestIndices :: (Int -> Int -> Ordering) -> Int -> Int -> VU.Vector Int
smallestIndices cmp k n = runST $ do
    heap <- VUM.generate k id
    let siftDown !i = do
            let l = 2 * i + 1
                r = l + 1
            if l >= k
                then pure ()
                else do
                    vi <- VUM.unsafeRead heap i
                    vl <- VUM.unsafeRead heap l
                    (c, vc) <-
                        if r < k
                            then do
                                vr <- VUM.unsafeRead heap r
                                pure (if cmp vr vl == GT then (r, vr) else (l, vl))
                            else pure (l, vl)
                    when (cmp vc vi == GT) $ do
                        VUM.unsafeWrite heap i vc
                        VUM.unsafeWrite heap c vi
                        siftDown c
    forM_ [k `div` 2 - 1, k `div` 2 - 2 .. 0] siftDown
    forM_ [k .. n - 1] $ \i -> do
        top <- VUM.unsafeRead heap 0
        when (cmp i top == LT) $ do
            VUM.unsafeWrite heap 0 i
            siftDown 0
    VU.unsafeFreeze heap

-- | 'sortIndices' over an explicit index set.
sortIndicesOf :: (Int -> Int -> Ordering) -> VU.Vector Int -> VU.Vector Int
sortIndicesOf cmp ixs = runST $ do
    mv <- VG.thaw (VU.convert ixs :: V.Vector Int)
    VA.sortBy cmp mv
    VU.convert <$> VG.unsafeFreeze mv

-- | Build a row-index comparator from a SortOrder and a DataFrame.
sortOrderComparator :: SortOrder -> DataFrame -> Int -> Int -> Ordering
sortOrderComparator (Asc (Col name :: Expr a)) df =
    case unsafeGetColumn name df of
        BoxedColumn _ (v :: V.Vector b) -> case testEquality (typeRep @a) (typeRep @b) of
            Just Refl -> \i j -> compare (v `V.unsafeIndex` i) (v `V.unsafeIndex` j)
            Nothing -> \_ _ -> EQ
        UnboxedColumn _ (v :: VU.Vector b) -> case testEquality (typeRep @a) (typeRep @b) of
            Just Refl -> \i j -> compare (v `VU.unsafeIndex` i) (v `VU.unsafeIndex` j)
            Nothing -> \_ _ -> EQ
        PackedText _ p -> case testEquality (typeRep @a) (typeRep @T.Text) of
            Just Refl -> \i j ->
                let (ai, oi, li) = packedSlice p i
                    (aj, oj, lj) = packedSlice p j
                 in sliceCmpBytes ai oi li aj oj lj
            Nothing -> \_ _ -> EQ
        c@(MergedColumn _ _) -> case materializeMerged c of
            BoxedColumn _ (v :: V.Vector b) -> case testEquality (typeRep @a) (typeRep @b) of
                Just Refl -> \i j -> compare (v `V.unsafeIndex` i) (v `V.unsafeIndex` j)
                Nothing -> \_ _ -> EQ
            _ -> \_ _ -> EQ
sortOrderComparator (Desc (Col name :: Expr a)) df =
    case unsafeGetColumn name df of
        BoxedColumn _ (v :: V.Vector b) -> case testEquality (typeRep @a) (typeRep @b) of
            Just Refl -> \i j -> compare (v `V.unsafeIndex` j) (v `V.unsafeIndex` i)
            Nothing -> \_ _ -> EQ
        UnboxedColumn _ (v :: VU.Vector b) -> case testEquality (typeRep @a) (typeRep @b) of
            Just Refl -> \i j -> compare (v `VU.unsafeIndex` j) (v `VU.unsafeIndex` i)
            Nothing -> \_ _ -> EQ
        PackedText _ p -> case testEquality (typeRep @a) (typeRep @T.Text) of
            Just Refl -> \i j ->
                let (ai, oi, li) = packedSlice p i
                    (aj, oj, lj) = packedSlice p j
                 in sliceCmpBytes aj oj lj ai oi li
            Nothing -> \_ _ -> EQ
        c@(MergedColumn _ _) -> case materializeMerged c of
            BoxedColumn _ (v :: V.Vector b) -> case testEquality (typeRep @a) (typeRep @b) of
                Just Refl -> \i j -> compare (v `V.unsafeIndex` j) (v `V.unsafeIndex` i)
                Nothing -> \_ _ -> EQ
            _ -> \_ _ -> EQ
sortOrderComparator _ _ = error "Sorting on compound column"

-- | Sort row indices using a comparator function.

-- | Stable LSD radix sort by one clean numeric column, else 'Nothing'.
radixSortIndices :: SortOrder -> DataFrame -> Maybe (VU.Vector Int)
radixSortIndices ord df = case ord of
    Asc (Col name) -> keysOf name False
    Desc (Col name) -> keysOf name True
    _ -> Nothing
  where
    keysOf name desc = case unsafeGetColumn name df of
        UnboxedColumn Nothing (v :: VU.Vector b) -> do
            key <- orderKey @b
            -- NaN has no 'compare' order; leave it to the comparison sort.
            case testEquality (typeRep @b) (typeRep @Double) of
                Just Refl | VU.any isNaN v -> Nothing
                _ -> Just ()
            let flipIf k = if desc then complement k else k
            Just (radixByKey (VU.map (flipIf . key) v))
        _ -> Nothing

-- | An unsigned key whose order matches the type's 'compare' order.
orderKey :: forall b. (Typeable b) => Maybe (b -> Word64)
orderKey
    | Just Refl <- testEquality (typeRep @b) (typeRep @Int) = Just signed
    | Just Refl <- testEquality (typeRep @b) (typeRep @Int64) = Just signed
    | Just Refl <- testEquality (typeRep @b) (typeRep @Int32) = Just signed
    | Just Refl <- testEquality (typeRep @b) (typeRep @Int16) = Just signed
    | Just Refl <- testEquality (typeRep @b) (typeRep @Int8) = Just signed
    | Just Refl <- testEquality (typeRep @b) (typeRep @Word64) = Just fromIntegral
    | Just Refl <- testEquality (typeRep @b) (typeRep @Word32) = Just fromIntegral
    | Just Refl <- testEquality (typeRep @b) (typeRep @Word16) = Just fromIntegral
    | Just Refl <- testEquality (typeRep @b) (typeRep @Word8) = Just fromIntegral
    | Just Refl <- testEquality (typeRep @b) (typeRep @Double) = Just double
    | otherwise = Nothing
  where
    signed :: (Integral a) => a -> Word64
    signed x = fromIntegral (fromIntegral x :: Int64) `xor` 0x8000000000000000
    -- -0.0 maps to 0.0's key, as 'compare' treats them equal.
    double d =
        let w = castDoubleToWord64 (if d == 0 then 0 else d)
         in if testBit w 63 then complement w else w `xor` 0x8000000000000000

-- | Row indices ordered by key, ties in row order; skips constant bytes.
radixByKey :: VU.Vector Word64 -> VU.Vector Int
radixByKey keys
    | VU.null keys = VU.empty
    | otherwise = runST $ do
        let !k0 = VU.unsafeHead keys
            !varying = VU.foldl' (\acc k -> acc .|. (k `xor` k0)) 0 keys
            !passes = (64 - countLeadingZeros varying + 7) `div` 8
        pairs <- VU.thaw (VU.imap (\i k -> (k, i)) keys)
        Radix.sortBy
            passes
            256
            (\p (k, _) -> fromIntegral ((k `unsafeShiftR` (8 * p)) .&. 255))
            pairs
        VU.map snd <$> VU.unsafeFreeze pairs

sortIndices :: (Int -> Int -> Ordering) -> Int -> VU.Vector Int
sortIndices cmp nRows = runST $ do
    withIndexes <- VG.thaw (V.generate nRows id :: V.Vector Int)
    VA.sortBy cmp withIndexes
    sorted <- VG.unsafeFreeze withIndexes
    return (VU.convert sorted)

shuffle ::
    (RandomGen g) =>
    g ->
    DataFrame ->
    DataFrame
shuffle pureGen df =
    let
        indexes = shuffledIndices pureGen (fst (dimensions df))
     in
        df{columns = V.map (atIndicesStable indexes) (columns df)}

shuffledIndices :: (HasCallStack, RandomGen g) => g -> Int -> VU.Vector Int
shuffledIndices pureGen k
    | k < 0 = error $ "Vector index may not be a neative number: " <> show k
    | k == 0 = VU.empty
    | otherwise = shuffleVec pureGen
  where
    shuffleVec :: (RandomGen g) => g -> VU.Vector Int
    shuffleVec g = runST $ do
        vm <- VUM.generate k id
        go vm (k - 1) g
        VU.unsafeFreeze vm

    go _v i _ | i <= 0 = pure ()
    go v i gen =
        let
            (j, nextGen) = randomR (0, i) gen
         in
            VUM.swap v i j *> go v (i - 1) nextGen