packages feed

dataframe-core-1.0.0.0: src/DataFrame/Internal/Grouping.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE Strict #-}
{-# LANGUAGE TypeApplications #-}

module DataFrame.Internal.Grouping (
    groupBy,
    buildRowToGroup,
    changingPoints,
) where

import qualified Data.IntMap.Strict as IM
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 qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM

import Control.Exception (throw)
import Control.Monad
import Control.Monad.ST (runST)
import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))
import DataFrame.Errors
import DataFrame.Internal.Column (
    Column (..),
    bitmapTestBit,
 )
import DataFrame.Internal.DataFrame (DataFrame (..), GroupedDataFrame (..))
import DataFrame.Internal.Hash
import DataFrame.Internal.Types
import Type.Reflection (typeRep)

{- | O(k * n) groups the dataframe by the given rows aggregating the remaining rows
into vector that should be reduced later.
-}
groupBy ::
    [T.Text] ->
    DataFrame ->
    GroupedDataFrame
groupBy names df
    | any (`notElem` columnNames df) names =
        throw $
            ColumnsNotFoundException
                (names L.\\ columnNames df)
                "groupBy"
                (columnNames df)
    | nRows df == 0 =
        Grouped
            df
            names
            VU.empty
            (VU.fromList [0])
            VU.empty
    | otherwise =
        let !vis = VU.map fst valIndices
            !os = changingPoints valIndices
            !n = nRows df
         in Grouped
                df
                names
                vis
                os
                (buildRowToGroup n vis os)
  where
    indicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` names) (columnIndices df)
    -- valIndices is a vector of (rowIdx, rowHash) sorted by rowHash so that
    -- runs of equal-hash rows are adjacent. Hashes are computed with the
    -- in-tree FNV combinators from "DataFrame.Internal.Hash", and bucketed
    -- with 'Data.IntMap.Strict' to keep the dep set minimal (no hashable,
    -- no vector-algorithms).
    valIndices = runST $ do
        let n = nRows df
        mh <- VUM.replicate n fnvOffset
        let selectedCols = map (columns df V.!) indicesToGroup
        forM_ selectedCols $ \case
            UnboxedColumn _ (v :: VU.Vector a) ->
                case testEquality (typeRep @a) (typeRep @Int) of
                    Just Refl ->
                        VU.imapM_
                            ( \i x -> do
                                !h <- VUM.unsafeRead mh i
                                VUM.unsafeWrite mh i (mixInt h x)
                            )
                            v
                    Nothing ->
                        case testEquality (typeRep @a) (typeRep @Double) of
                            Just Refl ->
                                VU.imapM_
                                    ( \i d -> do
                                        !h <- VUM.unsafeRead mh i
                                        VUM.unsafeWrite mh i (mixDouble h d)
                                    )
                                    v
                            Nothing ->
                                case sIntegral @a of
                                    STrue ->
                                        VU.imapM_
                                            ( \i d -> do
                                                !h <- VUM.unsafeRead mh i
                                                VUM.unsafeWrite mh i (mixInt h (fromIntegral @a @Int d))
                                            )
                                            v
                                    SFalse ->
                                        case sFloating @a of
                                            STrue ->
                                                VU.imapM_
                                                    ( \i d -> do
                                                        !h <- VUM.unsafeRead mh i
                                                        VUM.unsafeWrite mh i (mixDouble h (realToFrac d :: Double))
                                                    )
                                                    v
                                            SFalse ->
                                                VU.imapM_
                                                    ( \i d -> do
                                                        !h <- VUM.unsafeRead mh i
                                                        VUM.unsafeWrite mh i (mixShow h d)
                                                    )
                                                    v
            BoxedColumn bm (v :: V.Vector a) ->
                case testEquality (typeRep @a) (typeRep @T.Text) of
                    Just Refl ->
                        V.imapM_
                            ( \i t -> do
                                !h <- VUM.unsafeRead mh i
                                let h' = case bm of
                                        Just bm' | not (bitmapTestBit bm' i) -> mixInt h 0 -- null sentinel
                                        _ -> mixText h t
                                VUM.unsafeWrite mh i h'
                            )
                            v
                    Nothing ->
                        V.imapM_
                            ( \i d -> do
                                !h <- VUM.unsafeRead mh i
                                let h' = case bm of
                                        Just bm' | not (bitmapTestBit bm' i) -> mixInt h 0 -- null sentinel
                                        _ -> mixShow h d
                                VUM.unsafeWrite mh i h'
                            )
                            v
        hashes <- VU.unsafeFreeze mh
        -- Bucket row indices by hash using an IntMap, then walk it in
        -- ascending key order to emit (rowIdx, hash) pairs grouped by
        -- hash. Each bucket's accumulated list is reversed so rows come
        -- out in the original row order.
        let buckets =
                VU.ifoldl'
                    (\acc i h -> IM.insertWith (++) h [i] acc)
                    IM.empty
                    hashes
            ordered =
                [ (i, h)
                | (h, is) <- IM.toAscList buckets
                , i <- reverse is
                ]
        return (VU.fromList ordered)

-- Inline accessors to avoid depending on Operations.Core

columnNames :: DataFrame -> [T.Text]
columnNames = M.keys . columnIndices

nRows :: DataFrame -> Int
nRows = fst . dataframeDimensions

{- | Build the rowToGroup lookup vector from valueIndices and offsets.
rowToGroup[i] = k means row i belongs to group k.
-}
buildRowToGroup :: Int -> VU.Vector Int -> VU.Vector Int -> VU.Vector Int
buildRowToGroup n vis os = runST $ do
    rtg <- VUM.new n
    let nGroups = VU.length os - 1
    forM_ [0 .. nGroups - 1] $ \k ->
        let s = VU.unsafeIndex os k
            e = VU.unsafeIndex os (k + 1)
         in forM_ [s .. e - 1] $ \i ->
                VUM.unsafeWrite rtg (VU.unsafeIndex vis i) k
    VU.unsafeFreeze rtg
{-# NOINLINE buildRowToGroup #-}

changingPoints :: VU.Vector (Int, Int) -> VU.Vector Int
changingPoints vs =
    VU.reverse
        (VU.fromList (VU.length vs : fst (VU.ifoldl' findChangePoints initialState vs)))
  where
    initialState = ([0], snd (VU.head vs))
    findChangePoints (!offs, !currentVal) index (_, !newVal)
        | currentVal == newVal = (offs, currentVal)
        | otherwise = (index : offs, newVal)