{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE BangPatterns, DeriveDataTypeable, RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
module Gauge.Analysis
( Outliers(..)
, OutlierEffect(..)
, OutlierVariance(..)
, Report
, SampleAnalysis(..)
, analyseSample
, scale
, analyseBenchmark
, analyseMean
, countOutliers
, classifyOutliers
, noteOutliers
, outlierVariance
, regress
, benchmark'
, benchmarkWith'
) where
import Control.Applicative
import Control.Arrow (second)
import Control.DeepSeq (NFData(rnf))
import Control.Monad (forM_, when)
import Gauge.Benchmark (Benchmark (..), BenchmarkAnalysis(..), Benchmarkable, runBenchmark)
import Gauge.IO.Printf (note, printError, prolix, rewindClearLine)
import Gauge.Main.Options (defaultConfig, Config(..), Verbosity (..),
DisplayMode (..))
import Gauge.Measurement (Measured(measTime), secs, rescale, measureKeys,
measureAccessors_, validateAccessors, renderNames)
import Gauge.Monad (Gauge, askConfig, gaugeIO, Crit(..), askCrit, withConfig)
import Gauge.Format
import qualified Gauge.CSV as CSV
import Data.Data (Data, Typeable)
import Data.Int (Int64)
import Data.IORef (IORef, readIORef, writeIORef)
import Gauge.ListMap (Map)
import Data.Maybe (fromJust, isJust)
import Data.Traversable
import GHC.Generics (Generic)
import Statistics.Function (sort)
import Statistics.Quantile (weightedAvg, Sorted(..))
import Statistics.Regression (bootstrapRegress, olsRegress)
import Statistics.Resampling (Estimator(..),resample)
import Statistics.Sample (mean)
import Statistics.Sample.KernelDensity (kde)
import Statistics.Types (Sample, Estimate(..),ConfInt(..),confidenceInterval
,cl95,confidenceLevel)
import System.Random.MWC (GenIO, createSystemRandom)
import Text.Printf (printf)
import qualified Gauge.ListMap as Map
import qualified Data.Vector as V
import qualified Data.Vector.Generic as G
import qualified Data.Vector.Unboxed as U
import qualified Statistics.Resampling.Bootstrap as B
import qualified Statistics.Types as B
import qualified Statistics.Types as St
import Prelude hiding (sequence, mapM)
data Outliers = Outliers {
samplesSeen :: !Int64
, lowSevere :: !Int64
, lowMild :: !Int64
, highMild :: !Int64
, highSevere :: !Int64
} deriving (Eq, Show, Typeable, Data, Generic)
instance NFData Outliers
data OutlierEffect = Unaffected
| Slight
| Moderate
| Severe
deriving (Eq, Ord, Show, Typeable, Data, Generic)
instance NFData OutlierEffect
outliersEmpty :: Outliers
outliersEmpty = Outliers 0 0 0 0 0
addOutliers :: Outliers -> Outliers -> Outliers
addOutliers (Outliers s a b c d) (Outliers t w x y z) =
Outliers (s+t) (a+w) (b+x) (c+y) (d+z)
{-# INLINE addOutliers #-}
data OutlierVariance = OutlierVariance {
ovEffect :: OutlierEffect
, ovDesc :: String
, ovFraction :: Double
} deriving (Eq, Show, Typeable, Data, Generic)
instance NFData OutlierVariance where
rnf OutlierVariance{..} = rnf ovEffect `seq` rnf ovDesc `seq` rnf ovFraction
data Regression = Regression {
regResponder :: String
, regCoeffs :: Map String (St.Estimate St.ConfInt Double)
, regRSquare :: St.Estimate St.ConfInt Double
} deriving (Eq, Show, Typeable, Generic)
instance NFData Regression where
rnf Regression{..} =
rnf regResponder `seq` rnf regCoeffs `seq` rnf regRSquare
data SampleAnalysis = SampleAnalysis {
anRegress :: [Regression]
, anMean :: St.Estimate St.ConfInt Double
, anStdDev :: St.Estimate St.ConfInt Double
, anOutlierVar :: OutlierVariance
} deriving (Eq, Show, Typeable, Generic)
instance NFData SampleAnalysis where
rnf SampleAnalysis{..} =
rnf anRegress `seq` rnf anMean `seq`
rnf anStdDev `seq` rnf anOutlierVar
data KDE = KDE {
kdeType :: String
, kdeValues :: U.Vector Double
, kdePDF :: U.Vector Double
} deriving (Eq, Show, Typeable, Data, Generic)
instance NFData KDE where
rnf KDE{..} = rnf kdeType `seq` rnf kdeValues `seq` rnf kdePDF
data Report = Report {
reportName :: String
, reportKeys :: [String]
, reportMeasured :: V.Vector Measured
, reportAnalysis :: SampleAnalysis
, reportOutliers :: Outliers
, reportKDEs :: [KDE]
} deriving (Eq, Show, Typeable, Generic)
instance NFData Report where
rnf Report{..} =
rnf reportName `seq` rnf reportKeys `seq`
rnf reportMeasured `seq` rnf reportAnalysis `seq` rnf reportOutliers `seq`
rnf reportKDEs
classifyOutliers :: Sample -> Outliers
classifyOutliers sa = U.foldl' ((. outlier) . addOutliers) outliersEmpty ssa
where outlier e = Outliers {
samplesSeen = 1
, lowSevere = if e <= loS && e < hiM then 1 else 0
, lowMild = if e > loS && e <= loM then 1 else 0
, highMild = if e >= hiM && e < hiS then 1 else 0
, highSevere = if e >= hiS && e > loM then 1 else 0
}
!loS = q1 - (iqr * 3)
!loM = q1 - (iqr * 1.5)
!hiM = q3 + (iqr * 1.5)
!hiS = q3 + (iqr * 3)
q1 = weightedAvg 1 4 (Sorted ssa)
q3 = weightedAvg 3 4 (Sorted ssa)
ssa = sort sa
iqr = q3 - q1
outlierVariance
:: B.Estimate B.ConfInt Double
-> B.Estimate B.ConfInt Double
-> Double
-> OutlierVariance
outlierVariance µ σ a = OutlierVariance effect desc varOutMin
where
( effect, desc ) | varOutMin < 0.01 = (Unaffected, "no")
| varOutMin < 0.1 = (Slight, "slight")
| varOutMin < 0.5 = (Moderate, "moderate")
| otherwise = (Severe, "severe")
varOutMin = (minBy varOut 1 (minBy cMax 0 µgMin)) / σb2
varOut c = (ac / a) * (σb2 - ac * σg2) where ac = a - c
σb = B.estPoint σ
µa = B.estPoint µ / a
µgMin = µa / 2
σg = min (µgMin / 4) (σb / sqrt a)
σg2 = σg * σg
σb2 = σb * σb
minBy f q r = min (f q) (f r)
cMax x = fromIntegral (floor (-2 * k0 / (k1 + sqrt det)) :: Int)
where
k1 = σb2 - a * σg2 + ad
k0 = -a * ad
ad = a * d
d = k * k where k = µa - x
det = k1 * k1 - 4 * σg2 * k0
countOutliers :: Outliers -> Int64
countOutliers (Outliers _ a b c d) = a + b + c + d
{-# INLINE countOutliers #-}
analyseMean :: Sample
-> Int
-> Gauge Double
analyseMean a iters = do
let µ = mean a
_ <- note "mean is %s (%d iterations)\n" (secs µ) iters
noteOutliers . classifyOutliers $ a
return µ
scale :: Double
-> SampleAnalysis -> SampleAnalysis
scale f s@SampleAnalysis{..} = s {
anMean = B.scale f anMean
, anStdDev = B.scale f anStdDev
}
getGen :: Gauge GenIO
getGen = memoise gen createSystemRandom
getMeasurement :: (U.Unbox a) => (Measured -> a) -> V.Vector Measured -> U.Vector a
getMeasurement f v = U.convert . V.map f $ v
memoise :: (Crit -> IORef (Maybe a)) -> IO a -> Gauge a
memoise ref generate = do
r <- ref <$> askCrit
gaugeIO $ do
mv <- readIORef r
case mv of
Just rv -> return rv
Nothing -> do
rv <- generate
writeIORef r (Just rv)
return rv
toCL :: Maybe Double -> B.CL Double
toCL a =
case a of
Nothing -> B.cl95
Just x -> B.mkCL x
analyseSample :: String
-> V.Vector Measured
-> Gauge (Either String Report)
analyseSample name meas = do
Config{..} <- askConfig
let ests = [Mean,StdDev]
stime = getMeasurement (measTime . rescale) $ meas
n = G.length meas
_ <- prolix "bootstrapping with %d samples\n" n
gen <- getGen
ers <- (sequence <$>) . mapM (\(ps,r) -> regress gen ps r meas) $ ((["iters"],"time"):regressions)
case ers of
Left err -> pure $ Left err
Right rs -> do
resamps <- gaugeIO $ resample gen ests resamples stime
let [estMean,estStdDev] = B.bootstrapBCA (toCL confInterval) stime
resamps
ov = outlierVariance estMean estStdDev (fromIntegral n)
an = SampleAnalysis
{ anRegress = rs
, anMean = estMean
, anStdDev = estStdDev
, anOutlierVar = ov
}
return $ Right $ Report
{ reportName = name
, reportKeys = measureKeys
, reportMeasured = meas
, reportAnalysis = an
, reportOutliers = classifyOutliers stime
, reportKDEs = [uncurry (KDE "time") (kde 128 stime)]
}
regress :: GenIO
-> [String]
-> String
-> V.Vector Measured
-> Gauge (Either String Regression)
regress gen predNames respName meas
| G.null meas = pure $ Left "no measurements"
| otherwise = case validateAccessors predNames respName of
Left err -> pure $ Left err
Right accs -> do
let unmeasured = [n | (n, Nothing) <- map (second ($ G.head meas)) accs]
if not (null unmeasured)
then pure $ Left $ "no data available for " ++ renderNames unmeasured
else do
let (r:ps) = map ((`getMeasurement` meas) . (fromJust .) . snd) accs
Config{..} <- askConfig
(coeffs,r2) <- gaugeIO $
bootstrapRegress gen resamples (toCL confInterval)
olsRegress ps r
pure $ Right $ Regression
{ regResponder = respName
, regCoeffs = Map.fromList (zip (predNames ++ ["y"]) (G.toList coeffs))
, regRSquare = r2
}
noteOutliers :: Outliers -> Gauge ()
noteOutliers o = do
let frac n = (100::Double) * fromIntegral n / fromIntegral (samplesSeen o)
check :: Int64 -> Double -> String -> Gauge ()
check k t d = when (frac k > t) $
note " %d (%.1g%%) %s\n" k (frac k) d
outCount = countOutliers o
when (outCount > 0) $ do
_ <- note "found %d outliers among %d samples (%.1g%%)\n"
outCount (samplesSeen o) (frac outCount)
check (lowSevere o) 0 "low severe"
check (lowMild o) 1 "low mild"
check (highMild o) 1 "high mild"
check (highSevere o) 0 "high severe"
analyseBenchmark :: String -> V.Vector Measured -> Gauge Report
analyseBenchmark desc meas = do
Config{..} <- askConfig
_ <- prolix "%sanalysing with %d resamples\n" rewindClearLine resamples
erp <- analyseSample desc meas
case erp of
Left err -> printError "*** Error: %s\n" err
Right rpt@Report{..} -> do
let SampleAnalysis{..} = reportAnalysis
OutlierVariance{..} = anOutlierVar
wibble = printOverallEffect ovEffect
(builtin, others) = splitAt 1 anRegress
gaugeIO $ CSV.write csvFile $ CSV.Row
[ CSV.string desc
, CSV.float (estPoint anMean)
, CSV.float (fst $ confidenceInterval anMean)
, CSV.float (snd $ confidenceInterval anMean)
, CSV.float (estPoint anStdDev)
, CSV.float (fst $ confidenceInterval anStdDev)
, CSV.float (snd $ confidenceInterval anStdDev)
]
case displayMode of
StatsTable -> do
_ <- note "%sbenchmarked %s%s%s\n" rewindClearLine green desc reset
let r2 n = printf "%.3f R\178" n
forM_ builtin $ \Regression{..} ->
case Map.lookup "iters" regCoeffs of
Nothing -> return ()
Just t -> bs secs "time" t >> bs r2 "" regRSquare
bs secs "mean" anMean
bs secs "std dev" anStdDev
forM_ others $ \Regression{..} -> do
_ <- bs r2 (regResponder ++ ":") regRSquare
forM_ (Map.toList regCoeffs) $ \(prd,val) ->
bs (printf "%.3g") (" " ++ prd) val
when (verbosity == Verbose || (ovEffect > Slight && verbosity > Quiet)) $ do
when (verbosity == Verbose) $ noteOutliers reportOutliers
_ <- note "variance introduced by outliers: %d%% (%s)\n"
(round (ovFraction * 100) :: Int) wibble
return ()
_ <- traverse
(\(k, (a, s, _)) -> reportStat Verbose a s k)
measureAccessors_
_ <- note "\n"
pure ()
Condensed -> do
_ <- note "%s%s%-40s%s " rewindClearLine green desc reset
bsSmall secs "mean" anMean
bsSmall secs "( +-" anStdDev
_ <- note ")\n"
pure ()
return rpt
where bs :: (Double -> String) -> String -> Estimate ConfInt Double -> Gauge ()
bs f metric e@Estimate{..} =
note "%s%-20s%s %s%-10s%s (%s .. %s%s)\n" yellow metric reset
red (f estPoint) reset
(f $ fst $ confidenceInterval e) (f $ snd $ confidenceInterval e)
(let cl = confIntCL estError
str | cl == cl95 = ""
| otherwise = printf ", ci %.3f" (confidenceLevel cl)
in str
)
bsSmall :: (Double -> String) -> String -> Estimate ConfInt Double -> Gauge ()
bsSmall f metric Estimate{..} =
note "%s %s%-10s%s" metric red (f estPoint) reset
reportStat :: Verbosity
-> (Measured -> Maybe Double)
-> (Double -> String)
-> String -> Gauge ()
reportStat lvl accessor sh msg = do
let v0 = V.map (accessor . rescale) meas
when (V.all isJust v0) $ do
let v = V.map fromJust v0
total = V.sum v
len = V.length v
avg = total / (fromIntegral len)
when (verbosity >= lvl && avg > 0.0) $ do
note "%-20s %-10s (%s .. %s)\n" msg (sh avg)
(sh (V.minimum v)) (sh (V.maximum v))
printOverallEffect :: OutlierEffect -> String
printOverallEffect Unaffected = "unaffected"
printOverallEffect Slight = "slightly inflated"
printOverallEffect Moderate = "moderately inflated"
printOverallEffect Severe = "severely inflated"
benchmarkWith' :: Config -> Benchmarkable -> IO ()
benchmarkWith' cfg bm = withConfig cfg $
runBenchmark (const True) (Benchmark "function" bm) (BenchmarkNormal analyseBenchmark)
benchmark' :: Benchmarkable -> IO ()
benchmark' = benchmarkWith' defaultConfig