packages feed

dataframe-learn-2.5.0.0: src/DataFrame/Metrics.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

-- | Evaluation metrics for fitted models.
module DataFrame.Metrics (
    -- * Metric type + evaluation
    Metric,
    evaluate,
    predictColumn,
    columnOf,

    -- * Regression metrics
    mse,
    rmse,
    mae,
    r2,

    -- * Classification metrics
    accuracy,
    logLoss,
    Average (..),
    precision,
    recall,
    f1,
    rocAuc,

    -- * Per-class helpers (for reports)
    classCounts,
    precOf,
    recOf,
    f1Of,
) where

import Control.Exception (throw)
import Data.Either (fromRight)
import Data.List (nub, sort)
import Data.Ord (comparing)
import qualified Data.Text as T
import qualified Data.Vector.Algorithms.Intro as VA
import qualified Data.Vector.Unboxed as VU

import DataFrame.Errors (DataFrameException (..))
import DataFrame.Internal.Column (TypedColumn (..), toVector)
import DataFrame.Internal.DataFrame (DataFrame)
import DataFrame.Internal.Expression (Expr)
import DataFrame.Internal.Interpreter (interpret)
import DataFrame.Operations.Transformations (derive)

-- | A metric maps predictions and ground truth to a scalar score.
type Metric = VU.Vector Double -> VU.Vector Double -> Double

-- | Evaluate a model's prediction expression against a truth column.
evaluate :: Metric -> Expr Double -> Expr Double -> DataFrame -> Double
evaluate metric predExpr truthExpr df =
    metric (columnOf df predExpr) (columnOf df truthExpr)

-- | Add a model's prediction expression to a frame as a named column.
predictColumn :: T.Text -> Expr Double -> DataFrame -> DataFrame
predictColumn = derive

-- | Interpret an expression to a @Double@ vector over a frame.
columnOf :: DataFrame -> Expr Double -> VU.Vector Double
columnOf df e = case interpret @Double df e of
    Right (TColumn c) -> fromRight VU.empty (toVector @Double @VU.Vector c)
    Left err -> throw err

-- | Number of compared pairs, the shorter of the two vectors' lengths.
nCompared :: VU.Vector Double -> VU.Vector Double -> Double
nCompared preds truth = fromIntegral (min (VU.length preds) (VU.length truth))

-- | Mean squared error.
mse :: Metric
mse preds truth
    | n == 0 = throw (EmptyDataSetException "mse")
    | otherwise =
        VU.sum (VU.zipWith (\p t -> (p - t) ^ (2 :: Int)) preds truth) / n
  where
    n = nCompared preds truth

-- | Root mean squared error.
rmse :: Metric
rmse preds truth = sqrt (mse preds truth)

-- | Mean absolute error.
mae :: Metric
mae preds truth
    | n == 0 = throw (EmptyDataSetException "mae")
    | otherwise = VU.sum (VU.zipWith (\p t -> abs (p - t)) preds truth) / n
  where
    n = nCompared preds truth

-- | Coefficient of determination @R²@.
r2 :: Metric
r2 preds truth
    | n == 0 = throw (EmptyDataSetException "r2")
    | ssTot == 0 = 0
    | otherwise = 1 - ssRes / ssTot
  where
    n = nCompared preds truth
    truth' = VU.take (min (VU.length preds) (VU.length truth)) truth
    mean = VU.sum truth' / n
    ssRes = VU.sum (VU.zipWith (\p t -> (t - p) ^ (2 :: Int)) preds truth')
    ssTot = VU.sum (VU.map (\t -> (t - mean) ^ (2 :: Int)) truth')

-- | Fraction of exact matches.
accuracy :: Metric
accuracy preds truth
    | n == 0 = throw (EmptyDataSetException "accuracy")
    | otherwise =
        fromIntegral (VU.length (VU.filter id (VU.zipWith (==) preds truth))) / n
  where
    n = nCompared preds truth

-- | Binary log loss; probabilities clamped away from @0@/@1@.
logLoss :: Metric
logLoss probs truth
    | n == 0 = throw (EmptyDataSetException "logLoss")
    | otherwise =
        negate
            ( VU.sum
                ( VU.zipWith
                    (\p y -> let q = clampP p in y * log q + (1 - y) * log (1 - q))
                    probs
                    truth
                )
            )
            / n
  where
    n = nCompared probs truth
    clampP p = max 1e-15 (min (1 - 1e-15) p)

-- | Averaging strategy for multiclass precision/recall/F1.
data Average
    = -- | one class is positive; the rest negative
      Binary Double
    | -- | unweighted mean over classes
      Macro
    | -- | pool per-class counts (equals accuracy for single-label)
      Micro
    | -- | support-weighted mean over classes
      Weighted
    deriving (Eq, Show)

-- | Per-class @(tp, fp, fn, support)@ over the class set of @truth ∪ preds@.
classCounts ::
    VU.Vector Double -> VU.Vector Double -> [(Double, (Int, Int, Int, Int))]
classCounts preds truth =
    [(c, countsFor c) | c <- classes]
  where
    classes = sort (nub (VU.toList truth ++ VU.toList preds))
    countsFor c =
        VU.foldl'
            ( \(tp, fp, fn, sup) (p, y) ->
                ( if p == c && y == c then tp + 1 else tp
                , if p == c && y /= c then fp + 1 else fp
                , if p /= c && y == c then fn + 1 else fn
                , if y == c then sup + 1 else sup
                )
            )
            (0, 0, 0, 0)
            (VU.zip preds truth)

safeDiv :: Int -> Int -> Double
safeDiv a b = if b == 0 then 0 else fromIntegral a / fromIntegral b

precOf, recOf :: (Int, Int, Int, Int) -> Double
precOf (tp, fp, _, _) = safeDiv tp (tp + fp)
recOf (tp, _, fn, _) = safeDiv tp (tp + fn)

f1Of :: (Int, Int, Int, Int) -> Double
f1Of cs =
    let p = precOf cs; r = recOf cs in if p + r == 0 then 0 else 2 * p * r / (p + r)

averaged ::
    ((Int, Int, Int, Int) -> Double) ->
    Average ->
    VU.Vector Double ->
    VU.Vector Double ->
    Double
averaged stat avg preds truth =
    case avg of
        Binary pos -> maybe 0 stat (lookup pos cc)
        Macro -> meanOf [stat c | (_, c) <- cc]
        Weighted ->
            let total = sum [sup | (_, (_, _, _, sup)) <- cc]
             in if total == 0
                    then 0
                    else
                        sum [fromIntegral sup * stat c | (_, c@(_, _, _, sup)) <- cc]
                            / fromIntegral total
        Micro ->
            let tp = sum [t | (_, (t, _, _, _)) <- cc]
                fp = sum [x | (_, (_, x, _, _)) <- cc]
                fn = sum [x | (_, (_, _, x, _)) <- cc]
             in stat (tp, fp, fn, 0)
  where
    cc = classCounts preds truth
    meanOf xs = if null xs then 0 else sum xs / fromIntegral (length xs)

-- | Precision with the given averaging.
precision :: Average -> VU.Vector Double -> VU.Vector Double -> Double
precision = averaged precOf

-- | Recall with the given averaging.
recall :: Average -> VU.Vector Double -> VU.Vector Double -> Double
recall = averaged recOf

-- | F1 with the given averaging.
f1 :: Average -> VU.Vector Double -> VU.Vector Double -> Double
f1 = averaged f1Of

-- | Binary ROC-AUC (Mann-Whitney), tied scores sharing their average rank.
rocAuc :: VU.Vector Double -> VU.Vector Double -> Double
rocAuc scores truth
    | nPos == 0 || nNeg == 0 = 0.5
    | otherwise = (rankSum - nPos * (nPos + 1) / 2) / (nPos * nNeg)
  where
    n = VU.length scores
    sorted = VU.modify (VA.sortBy (comparing fst)) (VU.zip scores truth)
    nPos = fromIntegral (VU.length (VU.filter (== 1) truth))
    nNeg = fromIntegral n - nPos
    rankSum = go 0 0
    go !start !acc
        | start >= n = acc
        | otherwise =
            let v = fst (VU.unsafeIndex sorted start)
                end = maybe n (+ start) (VU.findIndex ((/= v) . fst) (VU.drop start sorted))
                rank = fromIntegral (start + 1 + end) / 2
                positives = VU.length (VU.filter ((== 1) . snd) (VU.slice start (end - start) sorted))
             in go end (acc + rank * fromIntegral positives)