dataframe-operations-2.6.0.0: src/DataFrame/Operations/Aggregation/Run.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- | Execute a recognised aggregation plan, producing one result column.
module DataFrame.Operations.Aggregation.Run (
runPlan,
runMomentPlan,
runMedianVarFused,
) where
import qualified Data.Text as T
import qualified Data.Vector.Algorithms.Intro as VA
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import Control.Concurrent (getNumCapabilities)
import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))
import DataFrame.Internal.Aggregation.Kernel.Dense (
denseMaxMinusMin,
denseReduce,
)
import DataFrame.Internal.Aggregation.Kernel.Distinct (countDistinctGroups)
import DataFrame.Internal.Aggregation.Kernel.Moments (
Moments (..),
momentScatterPar,
momentStreamPar,
)
import DataFrame.Internal.Aggregation.Kernel.Scatter (
maxMinusMinScatterPar,
scatterReducePar,
streamGroupCap,
)
import DataFrame.Internal.Aggregation.Plan (
AggPlan (..),
MomentPlan (..),
)
import DataFrame.Internal.Aggregation.Reduction (
Reduction (..),
cleanDoubleVector,
)
import DataFrame.Internal.Column (Column (..), fromUnboxedVector)
import DataFrame.Internal.Control.Concurrent (parallelBounds_)
import DataFrame.Internal.DataFrame (GroupedDataFrame (..), getColumn)
import System.IO.Unsafe (unsafePerformIO)
import Type.Reflection (typeRep)
-- | Group-domain size at or below which the dense kernel is chosen.
denseThreshold :: Int
denseThreshold = 262144
runPlan :: GroupedDataFrame -> VU.Vector Int -> Int -> AggPlan -> Column
runPlan gdf rtg nGroups plan = case plan of
PlanScatter red name -> scatterColumn red name
PlanMaxMinusMin a b ->
-- min/max are order-independent, so the fused kernels are exact.
let ca = col a
cb = col b
direct
| nGroups <= streamGroupCap = denseMaxMinusMin rtg nGroups ca cb
| otherwise = maxMinusMinScatterPar vis offs nGroups ca cb
in case direct of
Just out -> out
Nothing -> maxMinusMin vis offs nGroups ca cb
PlanMedian name -> groupedMedian vis offs nGroups (col name)
PlanCountDistinct name -> case countDistinctGroups rtg nGroups (col name) of
Just counts -> fromUnboxedVector counts
Nothing -> error "runPlan: countDistinctGroups rejected a planned column"
where
vis = valueIndices gdf
offs = offsets gdf
-- Dense fast path: scatter straight off the eager @rowToGroup@.
scatterColumn red name =
let c = col name
dense
| nGroups <= denseThreshold = denseReduce red rtg nGroups c
| otherwise = Nothing
in case dense of
Just out -> out
Nothing -> case scatterReducePar red vis offs nGroups c of
Just out -> out
Nothing -> error "runPlan: scatterReducePar rejected a planned column"
col name = case getColumn name (fullDataframe gdf) of
Just c -> c
Nothing -> error ("runPlan: planned column missing: " ++ T.unpack name)
-- | Run a recognised moment plan as one fused scatter over two columns.
runMomentPlan ::
GroupedDataFrame -> Int -> MomentPlan -> Maybe [(T.Text, Column)]
runMomentPlan gdf nGroups mp = do
let cx = col (mpColX mp)
cy = col (mpColY mp)
-- Preferred: the streaming kernel, one fused pass over rowToGroup.
streamed = momentStreamPar (rowToGroup gdf) nGroups cx cy
ms <- case streamed of
Just m -> Just m
Nothing -> momentScatterPar vis offs nGroups cx cy
pure
[ (mpNName mp, mN ms)
, (mpSxName mp, mSx ms)
, (mpSyName mp, mSy ms)
, (mpSxxName mp, mSxx ms)
, (mpSyyName mp, mSyy ms)
, (mpSxyName mp, mSxy ms)
]
where
vis = valueIndices gdf
offs = offsets gdf
col name = case getColumn name (fullDataframe gdf) of
Just c -> c
Nothing -> error ("runMomentPlan: planned column missing: " ++ T.unpack name)
-- | @max a - min b@ over the small per-group arrays.
maxMinusMin ::
VU.Vector Int -> VU.Vector Int -> Int -> Column -> Column -> Column
maxMinusMin vis offs nGroups ca cb =
case (ca, cb) of
( UnboxedColumn Nothing (_ :: VU.Vector x)
, UnboxedColumn Nothing (_ :: VU.Vector y)
)
| Just Refl <- testEquality (typeRep @x) (typeRep @Int)
, Just Refl <- testEquality (typeRep @y) (typeRep @Int) ->
let mx = scatterExtremaInt RMax vis offs nGroups ca
mn = scatterExtremaInt RMin vis offs nGroups cb
in fromUnboxedVector (VU.zipWith (-) mx mn)
_ ->
let mx = scatterExtremaDbl RMax vis offs nGroups ca
mn = scatterExtremaDbl RMin vis offs nGroups cb
in fromUnboxedVector (VU.zipWith (-) mx mn)
scatterExtremaInt ::
Reduction -> VU.Vector Int -> VU.Vector Int -> Int -> Column -> VU.Vector Int
scatterExtremaInt red vis offs nGroups c = case scatterReducePar red vis offs nGroups c of
Just (UnboxedColumn _ (v :: VU.Vector a))
| Just Refl <- testEquality (typeRep @a) (typeRep @Int) -> v
_ -> error "scatterExtremaInt"
scatterExtremaDbl ::
Reduction -> VU.Vector Int -> VU.Vector Int -> Int -> Column -> VU.Vector Double
scatterExtremaDbl red vis offs nGroups c =
case scatterReducePar red vis offs nGroups c of
Just (UnboxedColumn _ (v :: VU.Vector a))
| Just Refl <- testEquality (typeRep @a) (typeRep @Double) -> v
| Just Refl <- testEquality (typeRep @a) (typeRep @Int) -> VU.map fromIntegral v
_ -> error "scatterExtremaDbl"
-- | Fused grouped median and variance family over the same column.
runMedianVarFused ::
GroupedDataFrame -> Int -> Column -> Maybe (Column, Column, Column)
runMedianVarFused gdf nGroups c = do
vals <- cleanDoubleVector c
let (med, var) =
medianVarByGroup (valueIndices gdf) (offsets gdf) nGroups vals
pure
( fromUnboxedVector med
, fromUnboxedVector var
, fromUnboxedVector (VU.map sqrt var)
)
medianVarByGroup ::
VU.Vector Int ->
VU.Vector Int ->
Int ->
VU.Vector Double ->
(VU.Vector Double, VU.Vector Double)
medianVarByGroup vis offs nGroups vals = unsafePerformIO $ do
let !n = VU.length vis
buf <- VUM.new (max 1 n)
medOut <- VUM.new (max 1 nGroups)
varOut <- VUM.new (max 1 nGroups)
caps <- getNumCapabilities
let !bounds = groupRangeBounds offs nGroups caps
parallelBounds_ caps bounds $ \gs ge ->
let grp !g
| g >= ge = pure ()
| otherwise = do
let !s = VU.unsafeIndex offs g
!e = VU.unsafeIndex offs (g + 1)
!len = e - s
fill !pos
| pos >= e = pure ()
| otherwise = do
VUM.unsafeWrite buf pos (VU.unsafeIndex vals (VU.unsafeIndex vis pos))
fill (pos + 1)
fill s
-- Welford over the gathered slice (still in row order).
let welford !pos !c !mu !mm
| pos >= e =
pure (if c < 2 then 0 else mm / fromIntegral (c - 1))
| otherwise = do
x <- VUM.unsafeRead buf pos
let !c' = c + 1
!delta = x - mu
!mu' = mu + delta / fromIntegral c'
!mm' = mm + delta * (x - mu')
welford (pos + 1) c' mu' mm'
var <- welford s (0 :: Int) 0 0
VUM.unsafeWrite varOut g var
-- Median selection, as in 'medianByGroup'.
let slice = VUM.unsafeSlice s len buf
!mid = len `div` 2
VA.select slice (mid + 1)
let scan !i !hi !lo
| i > mid = pure (hi, lo)
| otherwise = do
x <- VUM.unsafeRead slice i
if x > hi
then scan (i + 1) x hi
else scan (i + 1) hi (max lo x)
(hi, lo) <- scan 0 (negate (1 / 0)) (negate (1 / 0))
let med = if odd len then hi else (hi + lo) / 2
VUM.unsafeWrite medOut g med
grp (g + 1)
in grp gs
med <- VU.unsafeFreeze (VUM.unsafeSlice 0 nGroups medOut)
var <- VU.unsafeFreeze (VUM.unsafeSlice 0 nGroups varOut)
pure (med, var)
{-# NOINLINE medianVarByGroup #-}
-- | Holistic per-group median over a single unboxed Int or Double column.
groupedMedian :: VU.Vector Int -> VU.Vector Int -> Int -> Column -> Column
groupedMedian vis offs nGroups c = case cleanDoubleVector c of
Nothing -> error "groupedMedian: non-numeric planned column"
Just vals -> fromUnboxedVector (medianByGroup vis offs nGroups vals)
medianByGroup ::
VU.Vector Int -> VU.Vector Int -> Int -> VU.Vector Double -> VU.Vector Double
medianByGroup vis offs nGroups vals = unsafePerformIO $ do
let !n = VU.length vis
buf <- VUM.new (max 1 n)
out <- VUM.new (max 1 nGroups)
caps <- getNumCapabilities
let !bounds = groupRangeBounds offs nGroups caps
-- Each worker sorts and takes medians of its own disjoint group range.
parallelBounds_ caps bounds $ \gs ge ->
let grp !g
| g >= ge = pure ()
| otherwise = do
let !s = VU.unsafeIndex offs g
!e = VU.unsafeIndex offs (g + 1)
!len = e - s
fill !pos
| pos >= e = pure ()
| otherwise = do
VUM.unsafeWrite buf pos (VU.unsafeIndex vals (VU.unsafeIndex vis pos))
fill (pos + 1)
fill s
let slice = VUM.unsafeSlice s len buf
!mid = len `div` 2
-- Move the least mid+1 values to the front, unordered.
VA.select slice (mid + 1)
let scan !i !hi !lo
| i > mid = pure (hi, lo)
| otherwise = do
x <- VUM.unsafeRead slice i
if x > hi
then scan (i + 1) x hi
else scan (i + 1) hi (max lo x)
(hi, lo) <- scan 0 (negate (1 / 0)) (negate (1 / 0))
let med = if odd len then hi else (hi + lo) / 2
VUM.unsafeWrite out g med
grp (g + 1)
in grp gs
VU.unsafeFreeze (VUM.unsafeSlice 0 nGroups out)
{-# NOINLINE medianByGroup #-}
-- | Split the groups into contiguous ranges balanced by row count.
groupRangeBounds :: VU.Vector Int -> Int -> Int -> VU.Vector Int
groupRangeBounds offs nGroups caps = VU.create $ do
b <- VUM.new (caps + 1)
let !nRows = VU.unsafeIndex offs nGroups
!per = max 1 ((nRows + caps - 1) `div` caps)
adv !target !gg
| gg >= nGroups = nGroups
| VU.unsafeIndex offs gg >= target = gg
| otherwise = adv target (gg + 1)
go !w !prev
| w >= caps = VUM.unsafeWrite b caps nGroups
| otherwise = do
let !target = min nRows (w * per)
!g = adv target prev
VUM.unsafeWrite b w g
go (w + 1) g
VUM.unsafeWrite b 0 0
go 1 0
pure b