packages feed

dataframe 0.3.3.9 → 0.3.4.0

raw patch · 14 files changed

+307/−248 lines, 14 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- DataFrame: Ascending :: SortOrder
- DataFrame: Descending :: SortOrder
- DataFrame.Display.Web.Plot: chartJsScript :: Text
- DataFrame.Internal.Expression: mkUnaggregatedColumn :: (Vector v a, Columnable a) => v a -> Vector Int -> Vector Int -> Vector (v a)
- DataFrame.Operations.Aggregation: hash' :: Columnable a => a -> Int
- DataFrame.Operations.Aggregation: mkGroupedColumns :: Vector Int -> DataFrame -> DataFrame -> Text -> DataFrame
- DataFrame.Operations.Aggregation: mkRowRep :: [Int] -> DataFrame -> Int -> Int
- DataFrame.Operations.Permutation: Ascending :: SortOrder
- DataFrame.Operations.Permutation: Descending :: SortOrder
+ DataFrame: Asc :: Text -> SortOrder
+ DataFrame: Desc :: Text -> SortOrder
+ DataFrame.Functions: dropFirstAndLast :: [a] -> [a]
+ DataFrame.Functions: meanMaybe :: (Columnable a, Real a) => Expr (Maybe a) -> Expr Double
+ DataFrame.Functions: optionalToDoubleVector :: Real a => Vector (Maybe a) -> Vector Double
+ DataFrame.Functions: stddevMaybe :: (Columnable a, Real a) => Expr (Maybe a) -> Expr Double
+ DataFrame.Functions: sumMaybe :: (Columnable a, Num a) => Expr (Maybe a) -> Expr a
+ DataFrame.Internal.Expression: mkAggregatedColumnUnboxed :: (Columnable a, Unbox a, Columnable b, Unbox b) => Vector a -> Vector Int -> Vector Int -> (Vector a -> b) -> Vector b
+ DataFrame.Internal.Expression: mkUnaggregatedColumnBoxed :: Columnable a => Vector a -> Vector Int -> Vector Int -> Vector (Vector a)
+ DataFrame.Internal.Expression: mkUnaggregatedColumnUnboxed :: (Columnable a, Unbox a) => Vector a -> Vector Int -> Vector Int -> Vector (Vector a)
+ DataFrame.Internal.Row: produceOrderingFromRow :: [Bool] -> Row -> Row -> Ordering
+ DataFrame.Operations.Aggregation: computeRowHashes :: [Int] -> DataFrame -> Vector Int
+ DataFrame.Operations.Permutation: Asc :: Text -> SortOrder
+ DataFrame.Operations.Permutation: Desc :: Text -> SortOrder
+ DataFrame.Operations.Permutation: getSortColumnName :: SortOrder -> Text
+ DataFrame.Operations.Permutation: mustFlipCompare :: SortOrder -> Bool
+ DataFrame.Operations.Statistics: meanMaybe :: (Columnable a, Real a) => Expr (Maybe a) -> DataFrame -> Double
+ DataFrame.Operations.Statistics: optionalToDoubleVector :: Real a => Vector (Maybe a) -> Vector Double
- DataFrame: sortBy :: SortOrder -> [Text] -> DataFrame -> DataFrame
+ DataFrame: sortBy :: [SortOrder] -> DataFrame -> DataFrame
- DataFrame.Internal.Row: mkRowRep :: DataFrame -> Set Text -> Int -> Row
+ DataFrame.Internal.Row: mkRowRep :: DataFrame -> [Text] -> Int -> Row
- DataFrame.Internal.Row: sortedIndexes' :: Bool -> Vector Row -> Vector Int
+ DataFrame.Internal.Row: sortedIndexes' :: [Bool] -> Vector Row -> Vector Int
- DataFrame.Operations.Permutation: sortBy :: SortOrder -> [Text] -> DataFrame -> DataFrame
+ DataFrame.Operations.Permutation: sortBy :: [SortOrder] -> DataFrame -> DataFrame

Files

CHANGELOG.md view
@@ -1,5 +1,11 @@ # Revision history for dataframe +## 0.3.4.0+* Fix right join - previously erased some values in the key.+* Change sort API so we can sort on different rows.+* Add meanMaybe and stddevMaybe that work on `Maybe` values.+* More efficient numeric groupby - use radix sort for indices and pre-sort when collecting.+ ## 0.3.3.9 * Fix compilation issue for ghc 9.12.* 
app/Benchmark.hs view
@@ -8,31 +8,17 @@ import qualified DataFrame.Functions as F import System.Random.Stateful +import DataFrame ((|>))+ main :: IO () main = do-    let n = 100_000_000-    g <- newIOGenM =<< newStdGen-    let range = (0 :: Double, 1 :: Double)-    startGeneration <- getCurrentTime-    ns <- VU.replicateM n (uniformRM range g)-    xs <- VU.replicateM n (uniformRM range g)-    ys <- VU.replicateM n (uniformRM range g)-    let df = D.fromUnnamedColumns (map D.fromUnboxedVector [ns, xs, ys])+    df <- D.readCsv "../db-benchmark/data/G1_2e6_1e2_0_0.csv"     print df-    endGeneration <- getCurrentTime-    let generationTime = diffUTCTime endGeneration startGeneration-    putStrLn $ "Data generation Time: " ++ show generationTime-    startCalculation <- getCurrentTime-    print $ D.mean (F.col @Double "0") df-    print $ D.variance (F.col @Double "1") df-    print $ D.correlation "1" "2" df-    endCalculation <- getCurrentTime-    let calculationTime = diffUTCTime endCalculation startCalculation-    putStrLn $ "Calculation Time: " ++ show calculationTime-    startFilter <- getCurrentTime-    print $ D.filter (F.col @Double "0") (> 0.971) df D.|> D.take 10-    endFilter <- getCurrentTime-    let filterTime = diffUTCTime endFilter startFilter-    putStrLn $ "Filter Time: " ++ show filterTime-    let totalTime = diffUTCTime endFilter startGeneration-    putStrLn $ "Total Time: " ++ show totalTime+    start <- getCurrentTime+    print $+        df+            |> D.groupBy ["id1"]+            |> D.aggregate [F.sum (F.col @Int "v1") `F.as` "v1_sum"]+    end <- getCurrentTime+    let computeTime = diffUTCTime end start+    putStrLn $ "Compute Time: " ++ show computeTime
dataframe.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               dataframe-version:            0.3.3.9+version:            0.3.4.0  synopsis: A fast, safe, and intuitive DataFrame library. 
src/DataFrame/Display/Web/Plot.hs view
@@ -29,6 +29,7 @@ import DataFrame.Internal.Expression import DataFrame.Operations.Core import qualified DataFrame.Operations.Subset as D+import Numeric (showFFloat) import System.Directory import System.Info import System.Process (@@ -40,7 +41,6 @@     std_out,     waitForProcess,  )-import Text.Printf  newtype HtmlPlot = HtmlPlot T.Text deriving (Show) @@ -73,10 +73,6 @@         , plotFile = Nothing         } -chartJsScript :: T.Text-chartJsScript =-    "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js\"></script>\n"- generateChartId :: IO T.Text generateChartId = do     gen <- newStdGen@@ -96,11 +92,9 @@         , "px;height:"         , T.pack (show height)         , "px\"></canvas>\n"-        , "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js\"></script>\n"+        , "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js\"></script>\n"         , "<script>\n"-        , "setTimeout(() => {"         , content-        , "}, 200);"         , "\n</script>\n"         ] @@ -113,13 +107,17 @@     chartId <- generateChartId     let values = extractNumericColumn colName df         (minVal, maxVal) = if null values then (0, 1) else (minimum values, maximum values)-        numBins = 30         binWidth = (maxVal - minVal) / fromIntegral numBins         bins = [minVal + fromIntegral i * binWidth | i <- [0 .. numBins - 1]]         counts = calculateHistogram values bins binWidth+        precision = max 0 $ ceiling (negate $ logBase 10 binWidth)          labels =-            T.intercalate "," ["\"" <> T.pack (printf "%.2f" b) <> "\"" | b <- bins]+            T.intercalate+                ","+                [ "\"" <> T.pack (showFFloat (Just precision) b "") <> "\""+                | b <- bins+                ]         dataPoints = T.intercalate "," [T.pack (show c) | c <- counts]          chartTitle =@@ -129,11 +127,9 @@          jsCode =             T.concat-                [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                , "var ctx = document.getElementById('"+                [ "setTimeout(function() { new Chart(\""                 , chartId-                , "').getContext('2d');\n"-                , "new Chart(ctx , {\n"+                , "\", {\n"                 , "  type: \"bar\",\n"                 , "  data: {\n"                 , "    labels: ["@@ -159,7 +155,7 @@                 , "      yAxes: [{ ticks: { beginAtZero: true } }]\n"                 , "    }\n"                 , "  }\n"-                , "});});"+                , "})}, 100);"                 ]      return $@@ -192,11 +188,9 @@          jsCode =             T.concat-                [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                , "var ctx = document.getElementById('"+                [ "setTimeout(function() { new Chart(\""                 , chartId-                , "').getContext('2d');\n"-                , "new Chart(ctx , {\n"+                , "\", {\n"                 , "  type: \"scatter\",\n"                 , "  data: {\n"                 , "    datasets: [{\n"@@ -223,7 +217,7 @@                 , "\" } }]\n"                 , "    }\n"                 , "  }\n"-                , "});});"+                , "})}, 100);"                 ]      return $@@ -287,11 +281,9 @@          jsCode =             T.concat-                [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                , "var ctx = document.getElementById('"+                [ "setTimeout(function() { new Chart(\""                 , chartId-                , "').getContext('2d');\n"-                , "new Chart(ctx , {\n"+                , "\", {\n"                 , "  type: \"scatter\",\n"                 , "  data: {\n"                 , "    datasets: [\n"@@ -311,7 +303,7 @@                 , "\" } }]\n"                 , "    }\n"                 , "  }\n"-                , "});});"+                , "})}, 100);"                 ]      return $@@ -363,11 +355,9 @@          jsCode =             T.concat-                [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                , "var ctx = document.getElementById('"+                [ "setTimeout(function() { new Chart(\""                 , chartId-                , "').getContext('2d');\n"-                , "new Chart(ctx , {\n"+                , "\", {\n"                 , "  type: \"line\",\n"                 , "  data: {\n"                 , "    labels: ["@@ -387,7 +377,7 @@                 , "\" } }]\n"                 , "    }\n"                 , "  }\n"-                , "});});"+                , "})}, 100);"                 ]      return $@@ -419,11 +409,9 @@                  jsCode =                     T.concat-                        [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                        , "var ctx = document.getElementById('"+                        [ "setTimeout(function() { new Chart(\""                         , chartId-                        , "').getContext('2d');\n"-                        , "new Chart(ctx , {\n"+                        , "\", {\n"                         , "  type: \"bar\",\n"                         , "  data: {\n"                         , "    labels: ["@@ -447,7 +435,7 @@                         , "      yAxes: [{ ticks: { beginAtZero: true } }]\n"                         , "    }\n"                         , "  }\n"-                        , "});});"+                        , "})}, 100);"                         ]             return $                 HtmlPlot $@@ -465,11 +453,8 @@                  jsCode =                     T.concat-                        [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                        , "var ctx = document.getElementById('"+                        [ "setTimeout(function() { new Chart(\""                         , chartId-                        , "').getContext('2d');\n"-                        , "new Chart(ctx , {\n"                         , "\", {\n"                         , "  type: \"bar\",\n"                         , "  data: {\n"@@ -494,7 +479,7 @@                         , "      yAxes: [{ ticks: { beginAtZero: true } }]\n"                         , "    }\n"                         , "  }\n"-                        , "});});"+                        , "})}, 100);"                         ]             return $                 HtmlPlot $@@ -519,11 +504,9 @@                  jsCode =                     T.concat-                        [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                        , "var ctx = document.getElementById('"+                        [ "setTimeout(function() { new Chart(\""                         , chartId-                        , "').getContext('2d');\n"-                        , "new Chart(ctx , {\n"+                        , "\", {\n"                         , "  type: \"pie\",\n"                         , "  data: {\n"                         , "    labels: ["@@ -543,7 +526,7 @@                         , chartTitle                         , "\" }\n"                         , "  }\n"-                        , "});});"+                        , "})}, 100);"                         ]             return $                 HtmlPlot $@@ -565,11 +548,9 @@                  jsCode =                     T.concat-                        [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                        , "var ctx = document.getElementById('"+                        [ "setTimeout(function() { new Chart(\""                         , chartId-                        , "').getContext('2d');\n"-                        , "new Chart(ctx , {\n"+                        , "\", {\n"                         , "  type: \"pie\",\n"                         , "  data: {\n"                         , "    labels: ["@@ -589,7 +570,7 @@                         , chartTitle                         , "\" }\n"                         , "  }\n"-                        , "});});"+                        , "})}, 100);"                         ]             return $                 HtmlPlot $@@ -658,11 +639,9 @@          jsCode =             T.concat-                [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                , "var ctx = document.getElementById('"+                [ "setTimeout(function() { new Chart(\""                 , chartId-                , "').getContext('2d');\n"-                , "new Chart(ctx , {\n"+                , "\", {\n"                 , "  type: \"bar\",\n"                 , "  data: {\n"                 , "    labels: ["@@ -681,7 +660,7 @@                 , "      yAxes: [{ stacked: true, ticks: { beginAtZero: true } }]\n"                 , "    }\n"                 , "  }\n"-                , "});});"+                , "})}, 100);"                 ]      return $@@ -712,11 +691,9 @@          jsCode =             T.concat-                [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                , "var ctx = document.getElementById('"+                [ "setTimeout(function() { new Chart(\""                 , chartId-                , "').getContext('2d');\n"-                , "new Chart(ctx , {\n"+                , "\", {\n"                 , "  type: \"bar\",\n"                 , "  data: {\n"                 , "    labels: ["@@ -740,7 +717,7 @@                 , "      yAxes: [{ ticks: { beginAtZero: true } }]\n"                 , "    }\n"                 , "  }\n"-                , "});});"+                , "})}, 100);"                 ]      return $@@ -773,11 +750,9 @@                  jsCode =                     T.concat-                        [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                        , "var ctx = document.getElementById('"+                        [ "setTimeout(function() { new Chart(\""                         , chartId-                        , "').getContext('2d');\n"-                        , "new Chart(ctx , {\n"+                        , "\", {\n"                         , "  type: \"bar\",\n"                         , "  data: {\n"                         , "    labels: ["@@ -803,7 +778,7 @@                         , "      yAxes: [{ ticks: { beginAtZero: true } }]\n"                         , "    }\n"                         , "  }\n"-                        , "});});"+                        , "})}, 100);"                         ]             return $                 HtmlPlot $@@ -827,11 +802,9 @@                  jsCode =                     T.concat-                        [ "requirejs(['https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js'], function (Chart) {\n"-                        , "var ctx = document.getElementById('"+                        [ "setTimeout(function() { new Chart(\""                         , chartId-                        , "').getContext('2d');\n"-                        , "new Chart(ctx , {\n"+                        , "\", {\n"                         , "  type: \"bar\",\n"                         , "  data: {\n"                         , "    labels: ["@@ -855,7 +828,7 @@                         , "      yAxes: [{ ticks: { beginAtZero: true } }]\n"                         , "    }\n"                         , "  }\n"-                        , "});});"+                        , "})}, 100);"                         ]             return $                 HtmlPlot $
src/DataFrame/Functions.hs view
@@ -34,14 +34,17 @@  import Control.Exception (throw) import Control.Monad+import Control.Monad.IO.Class import qualified Data.Char as Char import Data.Containers.ListUtils import Data.Function+import Data.Functor import qualified Data.List as L import qualified Data.Map as M-import Data.Maybe (fromMaybe, listToMaybe)+import Data.Maybe (catMaybes, fromMaybe, isJust, listToMaybe) import qualified Data.Set as S import qualified Data.Text as T+import qualified Data.Text.IO as T import Data.Time import Data.Type.Equality import qualified Data.Vector as V@@ -49,12 +52,13 @@ import qualified Data.Vector.Unboxed as VU import qualified DataFrame.Operations.Core as D import qualified DataFrame.Operations.Transformations as D-import Debug.Trace (trace, traceShow)+import Debug.Trace (trace) import Language.Haskell.TH import qualified Language.Haskell.TH.Syntax as TH import Text.Regex.TDFA import Type.Reflection (typeRep)-import Prelude hiding (maximum, minimum, sum)+import Prelude hiding (maximum, minimum)+import Prelude as P  name :: (Show a) => Expr a -> T.Text name (Col n) = n@@ -166,15 +170,28 @@ sum :: forall a. (Columnable a, Num a, VU.Unbox a) => Expr a -> Expr a sum expr = AggNumericVector expr "sum" VG.sum +sumMaybe :: forall a. (Columnable a, Num a) => Expr (Maybe a) -> Expr a+sumMaybe expr = AggVector expr "sumMaybe" (P.sum . catMaybes . V.toList)+ mean :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double mean expr = AggNumericVector expr "mean" mean' +meanMaybe :: forall a. (Columnable a, Real a) => Expr (Maybe a) -> Expr Double+meanMaybe expr = AggVector expr "meanMaybe" (mean' . optionalToDoubleVector)+ variance :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double variance expr = AggNumericVector expr "variance" variance'  median :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double median expr = AggNumericVector expr "median" median' +optionalToDoubleVector :: (Real a) => V.Vector (Maybe a) -> VU.Vector Double+optionalToDoubleVector =+    VU.fromList+        . V.foldl'+            (\acc e -> if isJust e then realToFrac (fromMaybe 0 e) : acc else acc)+            []+ percentile :: Int -> Expr Double -> Expr Double percentile n expr =     AggNumericVector@@ -185,6 +202,9 @@ stddev :: (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double stddev expr = AggNumericVector expr "stddev" (sqrt . variance') +stddevMaybe :: forall a. (Columnable a, Real a) => Expr (Maybe a) -> Expr Double+stddevMaybe expr = AggVector expr "stddevMaybe" (sqrt . variance' . optionalToDoubleVector)+ zScore :: Expr Double -> Expr Double zScore c = (c - mean c) / stddev c @@ -770,7 +790,10 @@     maybeType <- lookupTypeName t     case maybeType of         Just name -> return (ConT name)-        Nothing -> fail $ "Unsupported type: " ++ t+        Nothing ->+            if take 1 t == "["+                then typeFromString [dropFirstAndLast t] <&> AppT ListT+                else fail $ "Unsupported type: " ++ t typeFromString [tycon, t1] = do     outer <- typeFromString [tycon]     inner <- typeFromString [t1]@@ -782,6 +805,9 @@     return (AppT (AppT outer lhs) rhs) typeFromString s = fail $ "Unsupported types: " ++ unwords s +dropFirstAndLast :: [a] -> [a]+dropFirstAndLast = reverse . drop 1 . reverse . drop 1+ declareColumns :: DataFrame -> DecsQ declareColumns df =     let@@ -791,7 +817,7 @@      in         fmap concat $ forM specs $ \(raw, nm, tyStr) -> do             ty <- typeFromString (words tyStr)-            traceShow (nm <> " :: Expr " <> T.pack tyStr) (pure ())+            liftIO $ T.putStrLn (nm <> " :: Expr " <> T.pack tyStr)             let n = mkName (T.unpack nm)             sig <- sigD n [t|Expr $(pure ty)|]             val <- valD (varP n) (normalB [|col $(TH.lift raw)|]) []
src/DataFrame/Internal/Expression.hs view
@@ -421,21 +421,58 @@     = UnAggregated Column     | Aggregated (TypedColumn a) -mkUnaggregatedColumn ::-    forall v a.-    (VG.Vector v a, Columnable a) =>-    v a -> VU.Vector Int -> VU.Vector Int -> V.Vector (v a)-mkUnaggregatedColumn col os indices =-    V.generate-        (VU.length os - 1)-        ( \i ->-            VG.generate-                (os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i))-                ( \j ->-                    col `VG.unsafeIndex` (indices `VG.unsafeIndex` (j + (os `VG.unsafeIndex` i)))-                )-        )+mkUnaggregatedColumnBoxed ::+    forall a.+    (Columnable a) =>+    V.Vector a -> VU.Vector Int -> VU.Vector Int -> V.Vector (V.Vector a)+mkUnaggregatedColumnBoxed col os indices =+    let+        sorted = V.unsafeBackpermute col (V.convert indices)+        n i = os `VG.unsafeIndex` (i + 1) - (os `VG.unsafeIndex` i)+        start i = os `VG.unsafeIndex` i+     in+        V.generate+            (VU.length os - 1)+            ( \i ->+                V.unsafeSlice (start i) (n i) sorted+            ) +mkUnaggregatedColumnUnboxed ::+    forall a.+    (Columnable a, VU.Unbox a) =>+    VU.Vector a -> VU.Vector Int -> VU.Vector Int -> V.Vector (VU.Vector a)+mkUnaggregatedColumnUnboxed col os indices =+    let+        sorted = VU.unsafeBackpermute col indices+        n i = os `VU.unsafeIndex` (i + 1) - (os `VU.unsafeIndex` i)+        start i = os `VG.unsafeIndex` i+     in+        V.generate+            (VU.length os - 1)+            ( \i ->+                VU.unsafeSlice (start i) (n i) sorted+            )++mkAggregatedColumnUnboxed ::+    forall a b.+    (Columnable a, VU.Unbox a, Columnable b, VU.Unbox b) =>+    VU.Vector a ->+    VU.Vector Int ->+    VU.Vector Int ->+    (VU.Vector a -> b) ->+    VU.Vector b+mkAggregatedColumnUnboxed col os indices f =+    let+        sorted = VU.unsafeBackpermute col indices+        n i = os `VU.unsafeIndex` (i + 1) - (os `VU.unsafeIndex` i)+        start i = os `VG.unsafeIndex` i+     in+        VU.generate+            (VU.length os - 1)+            ( \i ->+                f (VU.unsafeSlice (start i) (n i) sorted)+            )+ nestedTypeException ::     forall a b. (Typeable a, Typeable b) => String -> DataFrameException nestedTypeException expression = case typeRep @a of@@ -470,9 +507,10 @@                     V.replicate (VG.length (offsets gdf) - 1) value interpretAggregation gdf@(Grouped df names indices os) (Col name) = case getColumn name df of     Nothing -> Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)-    Just (BoxedColumn col) -> Right $ UnAggregated $ fromVector $ mkUnaggregatedColumn col os indices-    Just (OptionalColumn col) -> Right $ UnAggregated $ fromVector $ mkUnaggregatedColumn col os indices-    Just (UnboxedColumn col) -> Right $ UnAggregated $ fromVector $ mkUnaggregatedColumn col os indices+    Just (BoxedColumn col) -> Right $ UnAggregated $ fromVector $ mkUnaggregatedColumnBoxed col os indices+    Just (OptionalColumn col) -> Right $ UnAggregated $ fromVector $ mkUnaggregatedColumnBoxed col os indices+    Just (UnboxedColumn col) ->+        Right $ UnAggregated $ fromVector $ mkUnaggregatedColumnUnboxed col os indices interpretAggregation gdf expression@(UnaryOp _ (f :: c -> d) expr) =     case interpretAggregation @c gdf expr of         Left (TypeMismatchException context) ->@@ -738,6 +776,23 @@                         }                     )         (Left e) -> Left e+interpretAggregation gdf@(Grouped df names indices os) expression@(AggNumericVector (Col name) op (f :: VU.Vector b -> c)) =+    case getColumn name df of+        -- TODO(mchavinda): Fix the compedium of type errors here+        -- This is mostly done help with the benchmarking.+        Nothing -> Left $ ColumnNotFoundException name "" (M.keys $ columnIndices df)+        Just (BoxedColumn col) -> error "Type mismatch."+        Just (OptionalColumn col) -> error "Type mismatch."+        Just (UnboxedColumn (col :: VU.Vector d)) -> case testEquality (typeRep @b) (typeRep @d) of+            Just Refl -> case testEquality (typeRep @c) (typeRep @a) of+                Just Refl ->+                    Right $+                        Aggregated $+                            TColumn $+                                fromUnboxedVector $+                                    mkAggregatedColumnUnboxed col os indices f+                Nothing -> error "Type mismatch"+            Nothing -> error "Type mismatch" interpretAggregation gdf@(Grouped df names indices os) expression@(AggNumericVector expr op (f :: VU.Vector b -> c)) =     case interpretAggregation @b gdf expr of         (Left (TypeMismatchException context)) ->
src/DataFrame/Internal/Row.hs view
@@ -11,7 +11,6 @@  import qualified Data.List as L import qualified Data.Map as M-import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Vector as V import qualified Data.Vector.Algorithms.Merge as VA@@ -122,10 +121,9 @@ toRowList :: DataFrame -> [Row] toRowList df =     let-        nameSet =-            S.fromList (map fst (L.sortBy (compare `on` snd) $ M.toList (columnIndices df)))+        names = map fst (L.sortBy (compare `on` snd) $ M.toList (columnIndices df))      in-        map (mkRowRep df nameSet) [0 .. (fst (dataframeDimensions df) - 1)]+        map (mkRowRep df names) [0 .. (fst (dataframeDimensions df) - 1)]  {- | Converts the dataframe to a vector of rows with only the specified columns. @@ -149,11 +147,7 @@ Vector of empty rows (one per dataframe row) -} toRowVector :: [T.Text] -> DataFrame -> V.Vector Row-toRowVector names df =-    let-        nameSet = S.fromList names-     in-        V.generate (fst (dataframeDimensions df)) (mkRowRep df nameSet)+toRowVector names df = V.generate (fst (dataframeDimensions df)) (mkRowRep df names)  mkRowFromArgs :: [T.Text] -> DataFrame -> Int -> Row mkRowFromArgs names df i = V.map get (V.fromList names)@@ -169,11 +163,14 @@         Just (UnboxedColumn column) -> toAny (column VU.! i)         Just (OptionalColumn column) -> toAny (column V.! i) -mkRowRep :: DataFrame -> S.Set T.Text -> Int -> Row-mkRowRep df names i = V.generate (S.size names) (\index -> get (names' V.! index))+-- This function will return the items in the order that is specified+-- by the user. For example, if the dataframe consists of the columns+-- "Age", "Pclass", "Name", and the user asks for ["Name", "Age"],+-- this will order the values in the order ["Mr Smith", 50]+mkRowRep :: DataFrame -> [T.Text] -> Int -> Row+mkRowRep df names i = V.generate (L.length names) (\index -> get (names' V.! index))   where-    inOrderIndexes = map fst $ L.sortBy (compare `on` snd) $ M.toList (columnIndices df)-    names' = V.fromList [n | n <- inOrderIndexes, S.member n names]+    names' = V.fromList names     throwError name =         error $             "Column "@@ -194,9 +191,16 @@         Nothing ->             throw $ ColumnNotFoundException name "mkRowRep" (M.keys $ columnIndices df) -sortedIndexes' :: Bool -> V.Vector Row -> VU.Vector Int-sortedIndexes' asc rows = runST $ do+sortedIndexes' :: [Bool] -> V.Vector Row -> VU.Vector Int+sortedIndexes' flipCompare rows = runST $ do     withIndexes <- VG.thaw (V.indexed rows)-    VA.sortBy ((if asc then compare else flip compare) `on` snd) withIndexes+    VA.sortBy (produceOrderingFromRow flipCompare `on` snd) withIndexes     sorted <- VG.unsafeFreeze withIndexes     return $ VU.generate (VG.length rows) (\i -> fst (sorted VG.! i))++produceOrderingFromRow :: [Bool] -> Row -> Row -> Ordering+produceOrderingFromRow mustFlips v1 v2 = V.foldr (<>) mempty vZipped+  where+    vFlip = V.fromList mustFlips+    vZipped =+        V.zipWith3 (\b e1 e2 -> if b then compare e1 e2 else compare e2 e1) vFlip v1 v2
src/DataFrame/Operations/Aggregation.hs view
@@ -12,7 +12,7 @@ import qualified Data.Map as M import qualified Data.Text as T import qualified Data.Vector as V-import qualified Data.Vector.Algorithms.Merge as VA+import qualified Data.Vector.Algorithms.Radix as VA import qualified Data.Vector.Generic as VG import qualified Data.Vector.Unboxed as VU @@ -23,17 +23,15 @@ import DataFrame.Errors import DataFrame.Internal.Column (     Column (..),-    Columnable,     TypedColumn (..),     atIndicesStable,-    getIndices,-    getIndicesUnboxed,  ) import DataFrame.Internal.DataFrame (DataFrame (..), GroupedDataFrame (..)) import DataFrame.Internal.Expression+import DataFrame.Internal.Types import DataFrame.Operations.Core import DataFrame.Operations.Subset-import Type.Reflection (typeOf, typeRep)+import Type.Reflection (typeRep)  {- | O(k * n) groups the dataframe by the given rows aggregating the remaining rows into vector that should be reduced later.@@ -57,11 +55,15 @@             (VU.fromList (reverse (changingPoints valueIndices)))   where     indicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` names) (columnIndices df)-    rowRepresentations = VU.generate (fst (dimensions df)) (mkRowRep indicesToGroup df)+    rowRepresentations = computeRowHashes indicesToGroup df      valueIndices = runST $ do         withIndexes <- VG.thaw $ VG.indexed rowRepresentations-        VA.sortBy (\(a, b) (a', b') -> compare b b') withIndexes+        VA.sortBy+            (VA.passes @Int 0)+            (VA.size @Int 0)+            (\p e -> VA.radix 0 (snd e))+            withIndexes         VG.unsafeFreeze withIndexes  changingPoints :: (Eq a, VU.Unbox a) => VU.Vector (Int, a) -> [Int]@@ -72,43 +74,37 @@         | currentVal == newVal = (offsets, currentVal)         | otherwise = (index : offsets, newVal) -mkRowRep :: [Int] -> DataFrame -> Int -> Int-mkRowRep groupColumnIndices df i = case h of-    [x] -> x-    xs -> hash h+computeRowHashes :: [Int] -> DataFrame -> VU.Vector Int+computeRowHashes indices df =+    L.foldl' combineCol initialHashes selectedCols   where-    h = map mkHash groupColumnIndices-    getHashedElem :: Column -> Int -> Int-    getHashedElem (BoxedColumn (c :: V.Vector a)) j = hash' @a (c V.! j)-    getHashedElem (UnboxedColumn (c :: VU.Vector a)) j = hash' @a (c VU.! j)-    getHashedElem (OptionalColumn (c :: V.Vector a)) j = hash' @a (c V.! j)-    mkHash j = getHashedElem ((V.!) (columns df) j) i+    n = fst (dimensions df)+    initialHashes = VU.replicate n 0 -{- | This hash function returns the hash when given a non numeric type but-the value when given a numeric.--}-hash' :: (Columnable a) => a -> Int-hash' value = case testEquality (typeOf value) (typeRep @Double) of-    Just Refl -> round $ value * 1000-    Nothing -> case testEquality (typeOf value) (typeRep @Int) of-        Just Refl -> value-        Nothing -> case testEquality (typeOf value) (typeRep @T.Text) of-            Just Refl -> hash value-            Nothing -> hash (show value)+    selectedCols = map (columns df V.!) indices -mkGroupedColumns ::-    VU.Vector Int -> DataFrame -> DataFrame -> T.Text -> DataFrame-mkGroupedColumns indices df acc name =-    case (V.!) (columns df) (columnIndices df M.! name) of-        BoxedColumn column ->-            let vs = indices `getIndices` column-             in insertVector name vs acc-        OptionalColumn column ->-            let vs = indices `getIndices` column-             in insertVector name vs acc-        UnboxedColumn column ->-            let vs = indices `getIndicesUnboxed` column-             in insertUnboxedVector name vs acc+    combineCol :: VU.Vector Int -> Column -> VU.Vector Int+    combineCol acc col = case col of+        UnboxedColumn (v :: VU.Vector a) -> case testEquality (typeRep @a) (typeRep @Int) of+            Just Refl -> VU.zipWith hashWithSalt acc v+            Nothing -> case testEquality (typeRep @a) (typeRep @Double) of+                Just Refl -> VU.zipWith (\h d -> hashWithSalt h (doubleToInt d)) acc v+                Nothing -> case sIntegral @a of+                    STrue -> VU.zipWith (\h d -> hashWithSalt h (fromIntegral @a @Int d)) acc v+                    SFalse -> case sFloating @a of+                        STrue -> VU.zipWith (\h d -> hashWithSalt h ((doubleToInt . realToFrac) d)) acc v+                        SFalse -> VU.zipWith (\h d -> hashWithSalt h (hash (show d))) acc v+        BoxedColumn (v :: V.Vector a) -> case testEquality (typeRep @a) (typeRep @T.Text) of+            Just Refl -> VG.convert (V.zipWith hashWithSalt (VG.convert acc) v)+            Nothing ->+                VG.convert+                    (V.zipWith (\h d -> hashWithSalt h (hash (show d))) (VG.convert acc) v)+        OptionalColumn v ->+            VG.convert+                (V.zipWith (\h d -> hashWithSalt h (hash (show d))) (VG.convert acc) v)++    doubleToInt :: Double -> Int+    doubleToInt = floor  {- | Aggregate a grouped dataframe using the expressions given. All ungrouped columns will be dropped.
src/DataFrame/Operations/Join.hs view
@@ -71,8 +71,7 @@             [c | (k, c) <- M.toList (D.columnIndices right), k `elem` cs]          rightRowRepresentations :: VU.Vector Int-        rightRowRepresentations =-            VU.generate (fst (D.dimensions right)) (D.mkRowRep rightIndicesToGroup right)+        rightRowRepresentations = D.computeRowHashes rightIndicesToGroup right          -- Build the Hash Map: Int -> Vector of Indices         -- We use ifoldr to efficiently insert (index, key) without intermediate allocations.@@ -90,8 +89,7 @@             [c | (k, c) <- M.toList (D.columnIndices left), k `elem` cs]          leftRowRepresentations :: VU.Vector Int-        leftRowRepresentations =-            VU.generate (fst (D.dimensions left)) (D.mkRowRep leftIndicesToGroup left)+        leftRowRepresentations = D.computeRowHashes leftIndicesToGroup left          -- Perform the Join         (leftIndexChunks, rightIndexChunks) =@@ -173,9 +171,9 @@ leftJoin cs right left =     let         leftIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices left)-        leftRowRepresentations = VU.generate (fst (D.dimensions left)) (D.mkRowRep leftIndicesToGroup left)+        leftRowRepresentations = D.computeRowHashes leftIndicesToGroup left         rightIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices right)-        rightRowRepresentations = VU.generate (fst (D.dimensions right)) (D.mkRowRep rightIndicesToGroup right)+        rightRowRepresentations = D.computeRowHashes rightIndicesToGroup right         rightKeyCountsAndIndices =             VU.foldr                 (\(i, v) acc -> M.insertWith (++) v [i] acc)@@ -246,66 +244,14 @@ -} rightJoin ::     [T.Text] -> DataFrame -> DataFrame -> DataFrame-rightJoin cs right left =-    let-        leftIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices left)-        leftRowRepresentations = VU.generate (fst (D.dimensions left)) (D.mkRowRep leftIndicesToGroup left)-        leftKeyCountsAndIndicesVec =-            M.map VU.fromList $-                VU.foldr-                    (\(i, v) acc -> M.insertWith (++) v [i] acc)-                    M.empty-                    (VU.indexed leftRowRepresentations)-        rightIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices right)-        rightRowRepresentations = VU.generate (fst (D.dimensions right)) (D.mkRowRep rightIndicesToGroup right)-        rightRowCount = fst (D.dimensions right)-        pairs =-            [ (maybeLeft, j)-            | j <- [0 .. rightRowCount - 1]-            , maybeLeft <--                case M.lookup (rightRowRepresentations VU.! j) leftKeyCountsAndIndicesVec of-                    Nothing -> [Nothing]-                    Just lVec -> map Just (VU.toList lVec)-            ]-        expandedLeftIndicies = VB.fromList (map fst pairs)-        expandedRightIndicies = VU.fromList (map snd pairs)-        expandedLeft =-            left-                { columns = VB.map (D.atIndicesWithNulls expandedLeftIndicies) (D.columns left)-                , dataframeDimensions =-                    (VB.length expandedLeftIndicies, snd (D.dataframeDimensions left))-                }-        expandedRight =-            right-                { columns = VB.map (D.atIndicesStable expandedRightIndicies) (D.columns right)-                , dataframeDimensions =-                    (VU.length expandedRightIndicies, snd (D.dataframeDimensions right))-                }-        leftColumns = D.columnNames left-        rightColumns = D.columnNames right-        initDf = expandedLeft-        insertIfPresent _ Nothing df = df-        insertIfPresent name (Just c) df = D.insertColumn name c df-     in-        D.fold-            ( \name df ->-                if name `elem` cs-                    then df-                    else-                        ( if name `elem` leftColumns-                            then insertIfPresent ("Right_" <> name) (D.getColumn name expandedRight) df-                            else insertIfPresent name (D.getColumn name expandedRight) df-                        )-            )-            rightColumns-            initDf+rightJoin cs left right = leftJoin cs right left  fullOuterJoin ::     [T.Text] -> DataFrame -> DataFrame -> DataFrame fullOuterJoin cs right left =     let         leftIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices left)-        leftRowRepresentations = VU.generate (fst (D.dimensions left)) (D.mkRowRep leftIndicesToGroup left)+        leftRowRepresentations = D.computeRowHashes leftIndicesToGroup left         leftKeyCountsAndIndices =             VU.foldr                 (\(i, v) acc -> M.insertWith (++) v [i] acc)@@ -313,7 +259,7 @@                 (VU.indexed leftRowRepresentations)         leftKeyCountsAndIndicesVec = M.map VU.fromList leftKeyCountsAndIndices         rightIndicesToGroup = M.elems $ M.filterWithKey (\k _ -> k `elem` cs) (D.columnIndices right)-        rightRowRepresentations = VU.generate (fst (D.dimensions right)) (D.mkRowRep rightIndicesToGroup right)+        rightRowRepresentations = D.computeRowHashes rightIndicesToGroup right         rightKeyCountsAndIndices =             VU.foldr                 (\(i, v) acc -> M.insertWith (++) v [i] acc)
src/DataFrame/Operations/Permutation.hs view
@@ -16,18 +16,28 @@ import System.Random  -- | Sort order taken as a parameter by the 'sortBy' function.-data SortOrder = Ascending | Descending deriving (Eq)+data SortOrder+    = Asc T.Text+    | Desc T.Text+    deriving (Eq) +getSortColumnName :: SortOrder -> T.Text+getSortColumnName (Asc n) = n+getSortColumnName (Desc n) = n++mustFlipCompare :: SortOrder -> Bool+mustFlipCompare (Asc _) = True+mustFlipCompare (Desc _) = False+ {- | O(k log n) Sorts the dataframe by a given row.  > sortBy Ascending ["Age"] df -} sortBy ::-    SortOrder ->-    [T.Text] ->+    [SortOrder] ->     DataFrame ->     DataFrame-sortBy order names df+sortBy sortOrds df     | any (`notElem` columnNames df) names =         throw $             ColumnNotFoundException@@ -36,9 +46,12 @@                 (columnNames df)     | otherwise =         let-            indexes = sortedIndexes' (order == Ascending) (toRowVector names df)+            indexes = sortedIndexes' mustFlips (toRowVector names df)          in             df{columns = V.map (atIndicesStable indexes) (columns df)}+  where+    names = map getSortColumnName sortOrds+    mustFlips = map mustFlipCompare sortOrds  shuffle ::     (RandomGen g) =>
src/DataFrame/Operations/Statistics.hs view
@@ -26,6 +26,7 @@ import DataFrame.Internal.DataFrame (     DataFrame (..),     columnAsUnboxedVector,+    columnAsVector,     empty,     getColumn,  )@@ -97,6 +98,15 @@         Left e -> throw e         Right xs -> mean' xs +meanMaybe ::+    forall a. (Columnable a, Real a) => Expr (Maybe a) -> DataFrame -> Double+meanMaybe (Col name) df = (mean' . optionalToDoubleVector) (columnAsVector @(Maybe a) name df)+meanMaybe expr df = case interpret @(Maybe a) df expr of+    Left e -> throw e+    Right (TColumn col) -> case toVector @(Maybe a) col of+        Left e -> throw e+        Right xs -> (mean' . optionalToDoubleVector) xs+ -- | Calculates the median of a given column as a standalone value. median ::     forall a. (Columnable a, Real a, VU.Unbox a) => Expr a -> DataFrame -> Double@@ -178,6 +188,13 @@             ColumnNotFoundException name "applyStatistic" (M.keys $ columnIndices df)     _ -> Nothing {-# INLINE _getColumnAsDouble #-}++optionalToDoubleVector :: (Real a) => V.Vector (Maybe a) -> VU.Vector Double+optionalToDoubleVector =+    VU.fromList+        . V.foldl'+            (\acc e -> if isJust e then realToFrac (fromMaybe 0 e) : acc else acc)+            []  -- | Calculates the sum of a given column as a standalone value. sum ::
tests/Operations/Aggregations.hs view
@@ -37,6 +37,7 @@             ( testData                 & D.groupBy ["test1"]                 & D.aggregate [F.count (F.col @Int "test2") `F.as` "test2"]+                & D.sortBy [D.Asc "test1"]             )         ) @@ -53,6 +54,7 @@             ( testData                 & D.groupBy ["test1"]                 & D.aggregate [F.mean (F.col @Int "test2") `F.as` "test2"]+                & D.sortBy [D.Asc "test1"]             )         ) @@ -71,6 +73,7 @@                 & D.aggregate                     [ F.mean (F.lift (fromIntegral @Int @Double) (F.col @Int "test2")) `F.as` "test2"                     ]+                & D.sortBy [D.Asc "test1"]             )         ) @@ -87,6 +90,7 @@             ( testData                 & D.groupBy ["test1"]                 & D.aggregate [F.mean (F.col @Int "test2" + F.col @Int "test2") `F.as` "test2"]+                & D.sortBy [D.Asc "test1"]             )         ) @@ -106,6 +110,7 @@                     [ F.maximum (F.lift (fromIntegral @Int @Double) (F.col @Int "test2"))                         `F.as` "test2"                     ]+                & D.sortBy [D.Asc "test1"]             )         ) @@ -123,6 +128,7 @@                 & D.groupBy ["test1"]                 & D.aggregate                     [F.maximum (F.col @Int "test2" + F.col @Int "test2") `F.as` "test2"]+                & D.sortBy [D.Asc "test1"]             )         ) 
tests/Operations/Join.hs view
@@ -33,7 +33,7 @@                 , ("B", D.fromList ["B0" :: Text, "B1", "B2"])                 ]             )-            (D.sortBy D.Ascending ["key"] (innerJoin ["key"] df1 df2))+            (D.sortBy [D.Asc "key"] (innerJoin ["key"] df1 df2))         )  testLeftJoin :: Test@@ -47,7 +47,7 @@                 , ("B", D.fromList [Just "B0", Just "B1" :: Maybe Text, Just "B2"])                 ]             )-            (D.sortBy D.Ascending ["key"] (leftJoin ["key"] df2 df1))+            (D.sortBy [D.Asc "key"] (leftJoin ["key"] df2 df1))         )  testRightJoin :: Test@@ -61,7 +61,7 @@                 , ("B", D.fromList ["B0" :: Text, "B1", "B2"])                 ]             )-            (D.sortBy D.Ascending ["key"] (rightJoin ["key"] df2 df1))+            (D.sortBy [D.Asc "key"] (rightJoin ["key"] df2 df1))         )  staffDf :: D.DataFrame@@ -108,7 +108,7 @@                     )                 ]             )-            (D.sortBy D.Ascending ["Name"] (fullOuterJoin ["Name"] studentDf staffDf))+            (D.sortBy [D.Asc "Name"] (fullOuterJoin ["Name"] studentDf staffDf))         )  tests :: [Test]
tests/Operations/Sort.hs view
@@ -23,6 +23,13 @@ testData :: D.DataFrame testData = D.fromNamedColumns values +moreTestData :: D.DataFrame+moreTestData =+    D.fromNamedColumns+        [ ("test1", DI.fromList $ replicate 10 (0 :: Int) ++ replicate 10 1)+        , ("test2", DI.fromList $ [1 :: Int .. 10] ++ [1 .. 10])+        ]+ sortByAscendingWAI :: Test sortByAscendingWAI =     TestCase@@ -33,7 +40,7 @@                 , ("test2", DI.fromList ['a' .. 'z'])                 ]             )-            (D.sortBy D.Ascending ["test1"] testData)+            (D.sortBy [D.Asc "test1"] testData)         )  sortByDescendingWAI :: Test@@ -46,16 +53,38 @@                 , ("test2", DI.fromList $ reverse ['a' .. 'z'])                 ]             )-            (D.sortBy D.Descending ["test1"] testData)+            (D.sortBy [D.Desc "test1"] testData)         ) +sortByTwoColumns :: Test+sortByTwoColumns =+    TestCase+        ( assertEqual+            "Sorting moreTestData (which is already sorted) is idempotent."+            moreTestData+            (D.sortBy [D.Asc "test1", D.Asc "test2"] moreTestData)+        )++sortByOneColumnAscOneColumnDesc :: Test+sortByOneColumnAscOneColumnDesc =+    TestCase+        ( assertEqual+            "Sorting moreTestData by Desc of test2 reverses the order of the second column."+            ( D.fromNamedColumns+                [ ("test1", DI.fromList $ replicate 10 (0 :: Int) ++ replicate 10 1)+                , ("test2", DI.fromList $ [10 :: Int, 9 .. 1] ++ [10, 9 .. 1])+                ]+            )+            (D.sortBy [D.Asc "test1", D.Desc "test2"] moreTestData)+        )+ sortByColumnDoesNotExist :: Test sortByColumnDoesNotExist =     TestCase         ( assertExpectException             "[Error Case]"             (D.columnNotFound "[\"test0\"]" "sortBy" (D.columnNames testData))-            (print $ D.sortBy D.Ascending ["test0"] testData)+            (print $ D.sortBy [D.Asc "test0"] testData)         )  tests :: [Test]@@ -63,4 +92,6 @@     [ TestLabel "sortByAscendingWAI" sortByAscendingWAI     , TestLabel "sortByDescendingWAI" sortByDescendingWAI     , TestLabel "sortByColumnDoesNotExist" sortByColumnDoesNotExist+    , TestLabel "sortByTwoColumns" sortByTwoColumns+    , TestLabel "sortByOneColumnAscOneColumnDesc" sortByOneColumnAscOneColumnDesc     ]