data-sketches-0.4.0.0: src/DataSketches/Quantiles/RelativeErrorQuantile.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TypeApplications #-}
-- | Relative Error Quantiles (REQ) sketch for approximate quantile computation
-- with /relative/ error bounds.
--
-- Most quantile sketches (like KLL) have /additive/ error: the answer might
-- be off by, say, 1.3% of the full rank range no matter where you query.
-- That's fine for p50 or p90, but at p99.99 a 1.3% additive error is enormous
-- relative to the sliver of the distribution you're looking at.
--
-- REQ flips this: the error is /proportional to the distance from the nearest
-- tail/. At p99.99, the error is proportional to 0.01%, not 1.3%. This makes
-- REQ the right choice when you care specifically about extreme percentiles —
-- SLA monitoring at p99.9, tail-latency debugging, outlier detection.
--
-- For general-purpose quantiles where you don't need tail precision, prefer
-- "DataSketches.Quantiles.KLL" — it's faster and simpler.
--
-- Based on "Relative Error Streaming Quantiles" (Cormode et al., PODS 2021).
--
-- === Configuration
--
-- @k@ controls accuracy vs. space. Must be even, in @[4, 1024]@. Larger @k@
-- means tighter error bounds but more memory. @k = 12@ is a reasonable default.
--
-- 'RankAccuracy' selects which tail gets the better error guarantee:
--
-- * 'HighRanksAreAccurate' — use when you care about p99, p99.9, p99.99
-- * 'LowRanksAreAccurate' — use when you care about p1, p0.1, p0.01
--
-- === Usage
--
-- @
-- import qualified DataSketches.Quantiles.RelativeErrorQuantile as REQ
--
-- main :: IO ()
-- main = do
-- sk <- REQ.'mkReqSketch' 12 REQ.'HighRanksAreAccurate'
-- mapM_ (REQ.'insert' sk) [1..100000 :: Double]
-- p999 <- REQ.'quantile' sk 0.999
-- putStrLn $ "p99.9 = " ++ show p999
-- @
--
-- === Implementation
--
-- Backed by a C implementation (@cbits\/req.c@) behind a 'ForeignPtr'.
-- All sketch memory lives outside the GHC heap — zero GC pressure,
-- zero boxing overhead.
--
-- === Mergeability
--
-- REQ sketches are fully mergeable: two sketches built on separate data
-- partitions can be combined via 'merge' to produce a sketch equivalent
-- to having processed all data in a single stream. Both sketches must
-- share the same 'RankAccuracy' setting.
module DataSketches.Quantiles.RelativeErrorQuantile (
-- * Construction
ReqSketch
, mkReqSketch
-- ** Configuration settings
, RankAccuracy(..)
-- * Querying
, count
, null
, sum
, maximum
, minimum
, retainedItemCount
, relativeStandardError
, countWithCriterion
, probabilityMassFunction
, quantile
, quantiles
, rank
, rankLowerBound
, ranks
, rankUpperBound
, cumulativeDistributionFunction
, getK
, numLevels
-- * Updating the sketch
, merge
, insert
, insertBatch
-- * Inspection
, rankAccuracy
, isEstimationMode
, isLessThanOrEqual
, setCriterionLE
, setCriterionLT
, DoubleIsNonFiniteException(..)
) where
import Control.Exception (throw)
import Control.Monad (when)
import Control.Monad.Primitive ( PrimMonad(PrimState) )
import Data.Word ( Word32, Word64 )
import DataSketches.Quantiles.RelativeErrorQuantile.Types
( RankAccuracy(..) )
import DataSketches.Quantiles.RelativeErrorQuantile.Internal.Constants
( fixRseFactor, initNumberOfSections, relRseFactor )
import DataSketches.Quantiles.RelativeErrorQuantile.Internal.DoubleBuffer
( DoubleIsNonFiniteException(..) )
import DataSketches.Quantiles.RelativeErrorQuantile.CInternal
import qualified Data.List
import qualified Data.Vector.Storable as VS
import Prelude hiding (sum, minimum, maximum, null)
type ReqSketch = CReqSketch
mkReqSketch :: PrimMonad m
=> Word32
-> RankAccuracy
-> m (ReqSketch (PrimState m))
mkReqSketch k ra = mkCReqSketch k (raToInt ra)
where
raToInt HighRanksAreAccurate = 1
raToInt LowRanksAreAccurate = 0
insert :: PrimMonad m => ReqSketch (PrimState m) -> Double -> m ()
insert = creqInsert
{-# INLINE insert #-}
-- | Bulk-insert a storable vector of doubles. Avoids per-element FFI overhead.
insertBatch :: PrimMonad m => ReqSketch (PrimState m) -> VS.Vector Double -> m ()
insertBatch = creqInsertBatch
merge :: PrimMonad m => ReqSketch (PrimState m) -> ReqSketch (PrimState m) -> m (ReqSketch (PrimState m))
merge dst src = creqMerge dst src >> pure dst
count :: PrimMonad m => ReqSketch (PrimState m) -> m Word64
count = creqCount
{-# INLINE count #-}
null :: PrimMonad m => ReqSketch (PrimState m) -> m Bool
null = creqIsEmpty
minimum :: PrimMonad m => ReqSketch (PrimState m) -> m Double
minimum = creqMin
maximum :: PrimMonad m => ReqSketch (PrimState m) -> m Double
maximum = creqMax
sum :: PrimMonad m => ReqSketch (PrimState m) -> m Double
sum = creqSum
retainedItemCount :: PrimMonad m => ReqSketch (PrimState m) -> m Int
retainedItemCount = creqRetained
getK :: PrimMonad m => ReqSketch (PrimState m) -> m Word32
getK = creqK
rankAccuracy :: PrimMonad m => ReqSketch (PrimState m) -> m RqAcc
rankAccuracy sk = do
i <- creqRankAccuracy sk
pure $ if i == 1 then HighRanksAreAccurate else LowRanksAreAccurate
type RqAcc = RankAccuracy
numLevels :: PrimMonad m => ReqSketch (PrimState m) -> m Int
numLevels = creqNumLevels
isEstimationMode :: PrimMonad m => ReqSketch (PrimState m) -> m Bool
isEstimationMode sk = (> 1) <$> creqNumLevels sk
isLessThanOrEqual :: PrimMonad m => ReqSketch (PrimState m) -> m Bool
isLessThanOrEqual sk = (== 1) <$> creqCriterion sk
setCriterionLE :: PrimMonad m => ReqSketch (PrimState m) -> m ()
setCriterionLE sk = creqSetCriterion sk 1
setCriterionLT :: PrimMonad m => ReqSketch (PrimState m) -> m ()
setCriterionLT sk = creqSetCriterion sk 0
countWithCriterion :: PrimMonad m => ReqSketch (PrimState m) -> Double -> m Word64
countWithCriterion sk value = do
when (isNaN value || isInfinite value) $ throw (DoubleIsNonFiniteException value)
creqCountWithCriterion sk value
rank :: PrimMonad m => ReqSketch (PrimState m) -> Double -> m Double
rank sk value = do
when (isNaN value || isInfinite value) $ throw (DoubleIsNonFiniteException value)
creqRank sk value
ranks :: PrimMonad m => ReqSketch (PrimState m) -> [Double] -> m [Double]
ranks sk = mapM (rank sk)
quantile :: PrimMonad m => ReqSketch (PrimState m) -> Double -> m Double
quantile sk normRank = do
empty <- null sk
if empty
then pure (0/0)
else do
when (normRank < 0 || normRank > 1.0) $
error $ "Normalized rank must be in the range [0.0, 1.0]: " ++ show normRank
creqQuantile sk normRank
quantiles :: PrimMonad m => ReqSketch (PrimState m) -> [Double] -> m [Double]
quantiles sk = mapM (quantile sk)
rankLowerBound :: PrimMonad m => ReqSketch (PrimState m) -> Double -> Int -> m Double
rankLowerBound sk r numStdDev = do
numLevels <- creqNumLevels sk
k_ <- fromIntegral <$> getK sk
total <- count sk
ra <- rankAccuracy sk
pure $ getRankLB k_ numLevels r numStdDev (ra == HighRanksAreAccurate) total
rankUpperBound :: PrimMonad m => ReqSketch (PrimState m) -> Double -> Int -> m Double
rankUpperBound sk r numStdDev = do
numLevels <- creqNumLevels sk
k_ <- fromIntegral <$> getK sk
total <- count sk
ra <- rankAccuracy sk
pure $ getRankUB k_ numLevels r numStdDev (ra == HighRanksAreAccurate) total
relativeStandardError :: Int -> Double -> RankAccuracy -> Word64 -> Double
relativeStandardError k_ rank_ hra = getRankUB k_ 2 rank_ 1 (hra == HighRanksAreAccurate)
cumulativeDistributionFunction
:: PrimMonad m => ReqSketch (PrimState m) -> [Double] -> m (Maybe [Double])
cumulativeDistributionFunction sk splitPoints = do
validateSplits splitPoints
empty <- null sk
if empty
then pure Nothing
else do
rs <- mapM (rank sk) splitPoints
pure $ Just (rs ++ [1.0])
probabilityMassFunction :: PrimMonad m => ReqSketch (PrimState m) -> [Double] -> m [Double]
probabilityMassFunction sk splitPoints = do
empty <- null sk
if empty
then pure []
else do
rs <- mapM (rank sk) splitPoints
let cdf = rs ++ [1.0]
pmf = zipWith (-) cdf (0.0 : cdf)
pure pmf
-- Private helpers
validateSplits :: Monad m => [Double] -> m ()
validateSplits splits = do
when (Data.List.null splits) $
error "splits must not be empty"
case filter (\x -> isInfinite x || isNaN x) splits of
(badValue:_) -> throw (DoubleIsNonFiniteException badValue)
[] -> pure ()
getRankLB :: Int -> Int -> Double -> Int -> Bool -> Word64 -> Double
getRankLB k_ levels r numStdDev hra totalN_ =
if exactRank k_ levels r hra totalN_ then r
else max lbRel lbFix
where
relative = relRseFactor / fromIntegral k_ * (if hra then 1.0 - r else r)
fixed = fixRseFactor / fromIntegral k_
lbRel = r - fromIntegral numStdDev * relative
lbFix = r - fromIntegral numStdDev * fixed
getRankUB :: Int -> Int -> Double -> Int -> Bool -> Word64 -> Double
getRankUB k_ levels r numStdDev hra totalN_ =
if exactRank k_ levels r hra totalN_ then r
else min ubRel ubFix
where
relative = relRseFactor / fromIntegral k_ * (if hra then 1.0 - r else r)
fixed = fixRseFactor / fromIntegral k_
ubRel = r + fromIntegral numStdDev * relative
ubFix = r + fromIntegral numStdDev * fixed
exactRank :: Int -> Int -> Double -> Bool -> Word64 -> Bool
exactRank k_ levels r hra totalN_ =
(levels == 1 || fromIntegral totalN_ <= baseCap)
|| (hra && r >= 1.0 - exactRankThresh || not hra && r <= exactRankThresh)
where
baseCap = k_ * initNumberOfSections
exactRankThresh :: Double
exactRankThresh = fromIntegral baseCap / fromIntegral totalN_