packages feed

dataframe-operations-1.1.0.1: src/DataFrame/Operations/SetOps.hs

{-# LANGUAGE ScopedTypeVariables #-}

{- |
Module      : DataFrame.Operations.SetOps
Description : Set-theoretic ("topos") row operations.

These treat a 'DataFrame' as a /set/ of rows and implement the subobject
lattice from relational algebra: 'union', 'intersect', 'difference', and
'symmetricDifference'. Every result is deduplicated, so each operation has the
schema-preserving shape @DataFrame -> DataFrame -> DataFrame@.

Row equality is the same hash-based notion used by 'distinct' (see
"DataFrame.Operations.Aggregation"), so these operations and 'distinct' agree
on what "the same row" means. Both inputs are expected to share a schema; the
typed layer ('DataFrame.Typed') enforces that statically.
-}
module DataFrame.Operations.SetOps (
    union,
    intersect,
    difference,
    symmetricDifference,
) where

import Prelude hiding (filter)

import qualified Data.Vector.Unboxed as VU

import DataFrame.Internal.DataFrame (
    DataFrame (..),
    GroupedDataFrame (..),
    columnNames,
 )
import DataFrame.Operations.Aggregation (distinct, groupBy, selectIndices)
import DataFrame.Operations.Merge ()

{- | All rows that appear in either dataframe, deduplicated.

@union a b@ is @distinct (a <> b)@: the set union of the two row sets.
-}
union :: DataFrame -> DataFrame -> DataFrame
union a b = distinct (a <> b)

{- | Rows that appear in both dataframes, deduplicated.

A row survives iff an equal row is present in each input.
-}
intersect :: DataFrame -> DataFrame -> DataFrame
intersect = setOp (&&)

{- | Rows present in the left dataframe but absent from the right, deduplicated
(the relational @EXCEPT@; the subobject complement of @a@ by @b@).
-}
difference :: DataFrame -> DataFrame -> DataFrame
difference = setOp (\inLeft inRight -> inLeft && not inRight)

{- | Rows present in exactly one of the two dataframes, deduplicated.

@symmetricDifference a b@ is @union (difference a b) (difference b a)@.
-}
symmetricDifference :: DataFrame -> DataFrame -> DataFrame
symmetricDifference a b = difference a b `union` difference b a

{- | Core engine for 'intersect' and 'difference'.

Concatenate the inputs, group by every column, and decide each group from
whether it has a member on the left side (original-row index @< nRows a@) and/or
the right side. The first member of a qualifying group is emitted as the
representative row, which keeps the result deduplicated and, because group
members come out in ascending row order, prefers the left dataframe's row.
-}
setOp :: (Bool -> Bool -> Bool) -> DataFrame -> DataFrame -> DataFrame
setOp keep a b =
    selectIndices (VU.fromList chosen) combined
  where
    leftRows = fst (dataframeDimensions a)
    combined = a <> b
    Grouped _ _ vis offs _ = groupBy (columnNames combined) combined
    nGroups = VU.length offs - 1
    chosen =
        [ VU.head members
        | k <- [0 .. nGroups - 1]
        , let s = VU.unsafeIndex offs k
              e = VU.unsafeIndex offs (k + 1)
              members = VU.slice s (e - s) vis
              inLeft = VU.any (< leftRows) members
              inRight = VU.any (>= leftRows) members
        , keep inLeft inRight
        ]