packages feed

dataframe-1.1.2.0: app/Synthesis.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

import Data.Char
import qualified Data.Text as T
import qualified DataFrame as D
import DataFrame.DecisionTree
import qualified DataFrame.Functions as F
import DataFrame.Operators
import qualified DataFrame.Typed as DT
import System.Random

$( DT.deriveSchemaFromCsvFileWith
    D.defaultReadOptions{D.safeRead = D.MaybeRead}
    "TrainSchema"
    "./data/titanic/train.csv"
 )
$( DT.deriveSchemaFromCsvFileWith
    D.defaultReadOptions{D.safeRead = D.MaybeRead}
    "TestSchema"
    "./data/titanic/test.csv"
 )

-- Survived is Maybe Int (safeRead = MaybeRead); prediction is Int (model output).
type RawPredSchema =
    '[DT.Column "Survived" (Maybe Int), DT.Column "prediction" Int]

prediction :: D.Expr Int
prediction = F.col @Int "prediction"

main :: IO ()
main = do
    rawTrain <- D.readCsv "./data/titanic/train.csv"
    rawTest <- D.readCsv "./data/titanic/test.csv"

    train <-
        maybe (fail "train.csv schema mismatch") pure (DT.freeze @TrainSchema rawTrain)
    test <-
        maybe (fail "test.csv schema mismatch") pure (DT.freeze @TestSchema rawTest)

    let (trainDf, validDf) =
            D.randomSplit (mkStdGen 4232) 0.7 (DT.thaw (clean train))
        testDf = DT.thaw (clean test)

        model =
            fitDecisionTree
                ( defaultTreeConfig
                    { maxTreeDepth = 5
                    , minSamplesSplit = 5
                    , minLeafSize = 3
                    , taoIterations = 100
                    , synthConfig =
                        defaultSynthConfig
                            { complexityPenalty = 0.1
                            , maxExprDepth = 3
                            , disallowedCombinations =
                                [ ("Age", "Fare")
                                , ("passenger_class", "number_of_siblings_and_spouses")
                                , ("passenger_class", "number_of_parents_and_children")
                                ]
                            }
                    }
                )
                (F.fromMaybe 0 (F.col @(Maybe Int) "Survived"))
                (trainDf |> D.exclude ["PassengerId"])

    print model

    putStrLn "Training accuracy: "
    print $ computeAccuracy (trainDf |> D.derive (F.name prediction) model)

    putStrLn "Validation accuracy: "
    print $ computeAccuracy (validDf |> D.derive (F.name prediction) model)

    D.writeCsv
        "./predictions.csv"
        ( testDf
            |> D.derive "Survived" model
            |> D.select ["PassengerId", "Survived"]
        )

clean ::
    ( DT.AssertPresent "Ticket" cols
    , DT.SafeLookup "Ticket" cols ~ Maybe T.Text
    , DT.AssertPresent "Name" cols
    , DT.SafeLookup "Name" cols ~ Maybe T.Text
    , DT.AssertPresent "Cabin" cols
    , DT.SafeLookup "Cabin" cols ~ Maybe T.Text
    ) =>
    DT.TypedDataFrame cols ->
    DT.TypedDataFrame
        ( DT.RenameManyInSchema
            '[ '("Name", "title")
             , '("Cabin", "cabin_prefix")
             , '("Pclass", "passenger_class")
             , '("SibSp", "number_of_siblings_and_spouses")
             , '("Parch", "number_of_parents_and_children")
             ]
            cols
        )
clean tdf =
    tdf
        |> DT.replaceColumn @"Ticket" (DT.nullLift (T.filter isAlpha) (DT.col @"Ticket"))
        |> DT.replaceColumn @"Name" (DT.nullLift extractTitle (DT.col @"Name"))
        |> DT.replaceColumn @"Cabin" (DT.nullLift (T.take 1) (DT.col @"Cabin"))
        |> DT.renameMany
            @'[ '("Name", "title")
              , '("Cabin", "cabin_prefix")
              , '("Pclass", "passenger_class")
              , '("SibSp", "number_of_siblings_and_spouses")
              , '("Parch", "number_of_parents_and_children")
              ]

-- | Extract title (e.g. "Mr", "Mrs") from a full Titanic passenger name.
extractTitle :: T.Text -> T.Text
extractTitle fullName =
    case filter (T.isSuffixOf ".") (T.words fullName) of
        (w : _) -> T.dropEnd 1 w
        [] -> ""

{- | Compute binary classification accuracy from a DataFrame containing
  "Survived" and "prediction" columns.
-}
computeAccuracy :: D.DataFrame -> Double
computeAccuracy df =
    let tdf =
            DT.impute @"Survived" 0 $
                DT.unsafeFreeze @RawPredSchema $
                    df |> D.select ["Survived", "prediction"]
        survived = DT.col @"Survived"
        predCol = DT.col @"prediction"
        count expr = fromIntegral (DT.nRows (DT.filterWhere expr tdf))
        tp = count ((survived DT..==. DT.lit 1) DT..&&. (predCol DT..==. DT.lit 1))
        tn = count ((survived DT..==. DT.lit 0) DT..&&. (predCol DT..==. DT.lit 0))
        fp = count ((survived DT..==. DT.lit 0) DT..&&. (predCol DT..==. DT.lit 1))
        fn = count ((survived DT..==. DT.lit 1) DT..&&. (predCol DT..==. DT.lit 0))
     in (tp + tn) / (tp + tn + fp + fn)