Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion dataframe-core/src-internal/DataFrame/Internal/AggKernel.hs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ varScatter takeSqrt g nGroups v = runST $ do
| otherwise = do
c <- VUM.unsafeRead cnt k
mm <- VUM.unsafeRead m2 k
let var = if c < 2 then 0 else mm / fromIntegral (c - 1)
-- NaN at n = 1, matching computeVariance
let var = if c < 2 then 0 / 0 else mm / fromIntegral (c - 1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm. @daikonradish does it make sense to stick a NaN here or fail instead. I think the right thing is to make it optional but the ergonomics of dealing with Maybe make me question putting it in the happy path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the same code the groupBy interpreter path uses... so Ig throwing there means one singleton group kills the whole aggregation (and the kernels would have to match)? NaN keeps the singleton visible without killing the query, and matches pandas/numpy (sample variance of n=1 -> NaN). Can simply switch all three to throw... (if u want it to fail loud)

VUM.unsafeWrite out k (if takeSqrt then sqrt var else var)
fin (k + 1)
fin 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ varPar takeSqrt vis offs nGroups v caps bounds = do
| otherwise = do
c <- VUM.unsafeRead cnt k
mm <- VUM.unsafeRead m2 k
let var = if c < 2 then 0 else mm / fromIntegral (c - 1)
-- NaN at n = 1, matching computeVariance
let var = if c < 2 then 0 / 0 else mm / fromIntegral (c - 1)
VUM.unsafeWrite out k (if takeSqrt then sqrt var else var)
fin (k + 1)
fin 0
Expand Down
22 changes: 22 additions & 0 deletions dataframe-core/src-internal/DataFrame/Internal/Column.hs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,28 @@ columnBitmap (UnboxedColumn bm _) = bm
columnBitmap (PackedText bm _) = bm
columnBitmap (MergedColumn _ _) = Nothing

{- | Drops the null values in a nullable column: those slots hold a sentinel,
not a value. Identity on columns without a bitmap. 'VG.ifilter' inspects only
the index, so boxed error thunks at null slots are never forced.
-}
dropNulls :: Column -> Column
dropNulls (BoxedColumn (Just bm) xs) =
BoxedColumn Nothing (VG.ifilter (\i _ -> bitmapTestBit bm i) xs)
dropNulls (UnboxedColumn (Just bm) xs) =
UnboxedColumn Nothing (VG.ifilter (\i _ -> bitmapTestBit bm i) xs)
dropNulls c@(PackedText (Just _) _) = dropNulls (materializePacked c)
dropNulls c = c
{-# INLINE dropNulls #-}

{- | 'dropNulls', unless the view type @a@ is @Maybe@-headed: a @Maybe@-typed
view encodes the nulls as values, so the column passes through untouched.
-}
dropNullsExceptMaybe :: forall a. (Typeable a) => Column -> Column

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is confusing. Why doesn't drop nulls handle this case. That would be a good thing to put in the comment. Let me read the calm site.

dropNullsExceptMaybe c = case typeRep @a of
App m _ | Just HRefl <- eqTypeRep m (typeRep @Maybe) -> c
_ -> dropNulls c
{-# INLINE dropNullsExceptMaybe #-}

{- | Decode a 'PackedText' into a @BoxedColumn Text@ (bit-identical to
materializing at freeze). Identity on every other column.
-}
Expand Down
43 changes: 29 additions & 14 deletions dataframe-learn/src/DataFrame/Metrics.hs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

Expand Down Expand Up @@ -45,6 +46,7 @@ import Data.Ord (comparing)
import qualified Data.Text as T
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)
Expand Down Expand Up @@ -73,15 +75,20 @@ columnOf df e = case interpret @Double df e of
Right (TColumn c) -> fromRight VU.empty (toVector @Double @VU.Vector c)
Left err -> throw err

n2 :: VU.Vector Double -> Double
n2 = fromIntegral . VU.length
{- | Compared pairs: 'VU.zipWith' truncates to the shorter vector, so every
mean below divides by this, never by the length of 'truth' alone.
-}
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
| VU.null truth = 0
| n == 0 = throw (EmptyDataSetException "mse")
| otherwise =
VU.sum (VU.zipWith (\p t -> (p - t) ^ (2 :: Int)) preds truth) / n2 truth
VU.sum (VU.zipWith (\p t -> (p - t) ^ (2 :: Int)) preds truth) / n
where
n = nCompared preds truth

-- | Root mean squared error.
rmse :: Metric
Expand All @@ -90,30 +97,37 @@ rmse preds truth = sqrt (mse preds truth)
-- | Mean absolute error.
mae :: Metric
mae preds truth
| VU.null truth = 0
| otherwise = VU.sum (VU.zipWith (\p t -> abs (p - t)) preds truth) / n2 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
| VU.null truth || ssTot == 0 = 0
| n == 0 = throw (EmptyDataSetException "r2")
| ssTot == 0 = 0
| otherwise = 1 - ssRes / ssTot
where
mean = VU.sum truth / n2 truth
ssRes = VU.sum (VU.zipWith (\p t -> (t - p) ^ (2 :: Int)) preds truth)
ssTot = VU.sum (VU.map (\t -> (t - mean) ^ (2 :: Int)) truth)
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
| VU.null truth = 0
| n == 0 = throw (EmptyDataSetException "accuracy")
| otherwise =
fromIntegral (VU.length (VU.filter id (VU.zipWith (==) preds truth))) / n2 truth
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
| VU.null truth = 0
| n == 0 = throw (EmptyDataSetException "logLoss")
| otherwise =
negate
( VU.sum
Expand All @@ -123,8 +137,9 @@ logLoss probs truth
truth
)
)
/ n2 truth
/ n
where
n = nCompared probs truth
clampP p = max 1e-15 (min (1 - 1e-15) p)

-- | Averaging strategy for multiclass precision/recall/F1.
Expand Down
35 changes: 28 additions & 7 deletions dataframe-learn/tests-internal/Learn/EdgeCases.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import DataFrame.LinearModel
import DataFrame.LinearSolver (sigmoid)
import DataFrame.PCA

import DataFrame.Internal.Statistics (correlation', variance')
import DataFrame.Internal.Statistics (correlation', meanSquaredError, variance')

import Test.HUnit

Expand Down Expand Up @@ -124,14 +124,13 @@ testVarianceConstant = TestCase $ do
let v = variance' (VU.replicate 100 (7.0 :: Double))
assertEqual "variance of constant column is 0" 0 v

{- Variance of fewer than two samples is defined to be 0 (computeVariance guard),
not NaN from a /0. -}
{- Sample variance of one observation is undefined: NaN, so a singleton
group can never look as tight as a genuinely constant column. -}
testVarianceSingleton :: Test
testVarianceSingleton = TestCase $ do
assertEqual
"variance of one sample is 0"
0
(variance' (VU.fromList [3.5 :: Double]))
assertBool
"variance of one sample is NaN"
(isNaN (variance' (VU.fromList [3.5 :: Double])))

{- Correlation of a perfectly linear pair is exactly +1 (and -1 reversed),
computed stably. y = 2x+1 over a spread of x. -}
Expand Down Expand Up @@ -169,6 +168,27 @@ testCorrelationTooFew = TestCase $ do
Nothing
(correlation' (VU.fromList [1]) (VU.fromList [2]))

{- meanSquaredError refuses length mismatches and empty inputs rather than
averaging over terms it never summed (or indexing out of bounds). -}
testMeanSquaredErrorGuards :: Test
testMeanSquaredErrorGuards = TestCase $ do
assertEqual
"mse of mismatched lengths is Nothing"
Nothing
(meanSquaredError (VU.fromList [0, 0, 0, 0]) (VU.fromList [2, 2]))
assertEqual
"mse with the longer prediction does not index out of bounds"
Nothing
(meanSquaredError (VU.fromList [1]) (VU.fromList [1, 2, 3]))
assertEqual
"mse of empty inputs is Nothing"
Nothing
(meanSquaredError VU.empty VU.empty)
assertEqual
"mse of equal-length inputs is the plain mean"
(Just 4.0)
(meanSquaredError (VU.fromList [0, 0]) (VU.fromList [2, 2]))

-- ===========================================================================
-- Category 8: stability inside the model expr layer
-- ===========================================================================
Expand Down Expand Up @@ -425,6 +445,7 @@ tests =
, testCorrelationPerfect
, testCorrelationConstantColumnIsNaN
, testCorrelationTooFew
, testMeanSquaredErrorGuards
, testLogisticProbsExtremeFeatures
, testOLSOneRow
, testLogisticSingleClass
Expand Down
19 changes: 12 additions & 7 deletions dataframe-operations/src-internal/DataFrame/Internal/Statistics.hs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ varianceStep (VarAcc !n !meanVal !m2) !x =

computeVariance :: VarAcc -> Double
computeVariance (VarAcc !n _ !m2)
| n < 2 = 0 -- or error "variance of <2 samples"
| n == 0 = throw $ EmptyDataSetException "variance"
-- undefined at n = 1: NaN, not 0
| n < 2 = 0 / 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Variance' is also what the groupBy path calls (variance = Agg (CollectAgg "variance" variance') in Functions.hs), so a throw here kinda takes down a whole aggregation when any group is a singleton, and the scatter kernels would need to throw too to stay in sync. It's left as NaN for now, same question as the kernel thread.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Maybe to make this PR easier to reason about please add specific examples or behaviour changes in the PR description. Sort of like how a front end engineer adds screenshots to their PRs. A clear before and after of failure modes and their new fixes - they could even be copies of the test examples but they should be something like:

Before:

$ ./scripts/repl.sh
ghci> :script dataframe.ghci
dataframe> df <- D.readCsv "./data/housing.csv"
dataframe> -- the operation

Then after would paste a similar thing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added before/after examples to the description.

| otherwise = m2 / fromIntegral (n - 1)
{-# INLINE computeVariance #-}

Expand Down Expand Up @@ -106,7 +108,7 @@ skewnessStep (SkewAcc !n !meanVal !m2 !m3) !x' =
computeSkewness :: SkewAcc -> Double
computeSkewness (SkewAcc n _ m2 m3)
| n < 3 = 0 -- or error "skewness of <3 samples"
| otherwise = (sqrt (fromIntegral n - 1) * m3) / sqrt (m2 ^ (3 :: Int))
| otherwise = (sqrt (fromIntegral n) * m3) / sqrt (m2 ^ (3 :: Int))
{-# INLINE computeSkewness #-}

skewness' :: (VU.Unbox a, Real a, Num a) => VU.Vector a -> Double
Expand Down Expand Up @@ -202,11 +204,14 @@ interQuartileRange' samp =
{-# INLINE interQuartileRange' #-}

meanSquaredError :: VU.Vector Double -> VU.Vector Double -> Maybe Double
meanSquaredError target prediction =
let
squareDiff = VU.ifoldl' (\sq i e -> (e - target VU.! i) ^ (2 :: Int) + sq) 0 prediction
in
Just $ squareDiff / fromIntegral (max (VU.length target) (VU.length prediction))
meanSquaredError target prediction
| VU.length target /= VU.length prediction = Nothing

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great.

| VU.null target = Nothing
| otherwise =
Just
( VU.sum (VU.zipWith (\t p -> (p - t) ^ (2 :: Int)) target prediction)
/ fromIntegral (VU.length target)
)
{-# INLINE meanSquaredError #-}

mutualInformationBinned ::
Expand Down
Loading
Loading