dataframe-learn-2.5.0.0: src/DataFrame/ModelSelection.hs
-- | Cross-validation and grid search for hyperparameter tuning.
module DataFrame.ModelSelection (
crossValScore,
crossValidate,
stratifiedFoldIds,
outOfFold,
GridSearchResult (..),
gridSearch,
) where
import Data.List (maximumBy, sortOn)
import Data.Ord (comparing)
import qualified Data.Set as S
import qualified Data.Vector.Unboxed as VU
import System.Random (mkStdGen, randoms)
import DataFrame.Internal.DataFrame (DataFrame)
import DataFrame.Internal.Expression (Expr)
import DataFrame.Metrics (Metric, evaluate)
import DataFrame.Operations.Merge ()
import DataFrame.Operations.Subset (kFolds, rowsAtIndices)
-- | Per-fold scores from k-fold cross-validation.
crossValScore ::
Int -> Int -> (DataFrame -> DataFrame -> Double) -> DataFrame -> [Double]
crossValScore folds seed scoreFn df =
[ scoreFn (combine (others i)) (fs !! i)
| i <- [0 .. length fs - 1]
, not (null (others i))
]
where
fs = kFolds (mkStdGen seed) folds df
others i = [f | (j, f) <- zip [0 ..] fs, j /= i]
combine = foldr1 (<>)
-- | Fit on each training fold and score predictions on the held-out fold.
crossValidate ::
Int ->
Int ->
Metric ->
Expr Double ->
(DataFrame -> Expr Double) ->
DataFrame ->
[Double]
crossValidate folds seed metric truth fitPredict =
crossValScore folds seed score
where
score train = evaluate metric (fitPredict train) truth
-- | A class-balanced fold number in [0, k) for every row.
stratifiedFoldIds :: Int -> Int -> VU.Vector Double -> VU.Vector Int
stratifiedFoldIds k seed labels = VU.update (VU.replicate n 0) (VU.fromList assigned)
where
n = VU.length labels
shuffled = map snd (sortOn fst (zip (randoms (mkStdGen seed) :: [Double]) [0 .. n - 1]))
assigned =
concat
[ zip [i | i <- shuffled, labels VU.! i == c] (cycle [0 .. k - 1])
| c <- S.toList (S.fromList (VU.toList labels))
]
-- | Out-of-fold scores: each row is scored by a model not trained on it.
outOfFold ::
VU.Vector Int ->
(DataFrame -> DataFrame -> VU.Vector Double) ->
DataFrame ->
VU.Vector Double
outOfFold folds fitScore df =
VU.update
(VU.replicate (VU.length folds) 0)
(VU.concat (map score [0 .. k - 1]))
where
k = if VU.null folds then 0 else VU.maximum folds + 1
score f =
let test = VU.findIndices (== f) folds
train = VU.findIndices (/= f) folds
in VU.zip test (fitScore (rowsAtIndices train df) (rowsAtIndices test df))
-- | The outcome of a grid search: the best config, its score, and all results.
data GridSearchResult c = GridSearchResult
{ gsBest :: !c
, gsBestScore :: !Double
, gsAll :: ![(c, Double)]
}
deriving (Show)
-- | Search configurations by mean cross-validated score, returning the best.
gridSearch ::
Int ->
Int ->
[c] ->
(c -> DataFrame -> DataFrame -> Double) ->
DataFrame ->
GridSearchResult c
gridSearch folds seed configs scoreFn df =
GridSearchResult bestC bestS scored
where
scored = [(c, mean (crossValScore folds seed (scoreFn c) df)) | c <- configs]
(bestC, bestS) = maximumBy (comparing snd) scored
mean xs = if null xs then -(1 / 0) else sum xs / fromIntegral (length xs)