packages feed

porcupine-core-0.1.0.0: examples/example-radon/ExampleRadon.hs

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DeriveAnyClass        #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE OverloadedLabels      #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE TupleSections         #-}
{-# LANGUAGE Arrows #-}
{-# OPTIONS_GHC -Wwarn -Wno-missing-signatures -Wno-name-shadowing #-}

-- This example is loosely based on the series of blog posts by Thomas Wiecki
-- https://twiecki.io/blog/2014/03/17/bayesian-glms-3/ .

import           Control.Monad
import           Data.Aeson
import qualified Data.Csv as Csv
import           Data.DocRecord
import qualified Data.Text                     as T
import qualified Data.Vector                   as V
import Data.Functor
import           GHC.Generics
import           Porcupine
import           Prelude                       hiding (id, (.))
import qualified Control.Foldl as L
import           Graphics.Vega.VegaLite        as VL
import Control.Monad.Bayes.Class
import Control.Monad.Bayes.Sampler
import Control.Monad.Bayes.Weighted
import Control.Monad.Bayes.Traced
import Numeric.Log

import Plotting  -- In the same folder


data RadonObservation = RadonObservation
  { state :: !T.Text
  , county :: !T.Text
  , basement :: !T.Text
  , log_radon :: !Double }
  deriving (Generic, FromJSON, ToJSON
           ,Csv.FromNamedRecord, Csv.ToNamedRecord, Csv.DefaultOrdered)

-- | We want to read each RadonObservation as a set of Records. This supports
-- reading from CSV files with headers and from JSON files. The Vector cannot
-- directly be read from the CSV, as we would not known whether the columns are
-- positional or nominal. This is why we use the 'Records' wrapper here (for
-- nominal columns). This requires our datatype to instantiate
-- Csv.From/ToNamedRecord
radonObsSerials :: BidirSerials (V.Vector RadonObservation)
radonObsSerials = dimap Records fromRecords $  -- We wrap/unwrap the Records
  someBidirSerial (CSVSerial "csv" True ',')
  <>
  someBidirSerial JSONSerial

radonObsFile :: DataSource (V.Vector RadonObservation)
radonObsFile = dataSource ["data", "radon"] radonObsSerials

filteredCsvFile :: DataSink (V.Vector RadonObservation)
filteredCsvFile = dataSink ["debug", "radon-filtered"] radonObsSerials

vegaliteSerials :: PureSerials VegaLite
vegaliteSerials =
  lmap VL.toHtml (somePureSerial $ PlainTextSerial $ Just "html")
  <> lmap VL.fromVL (somePureSerial JSONSerial)

writeViz name = writeData (dataSink ["viz", name] vegaliteSerials)

data Summary = Summary { numRows :: Int
                       , uniqueStates :: [T.Text]
                       , numUniqueCounties :: Int }
  deriving (Show)

foldSummary :: L.Fold RadonObservation Summary
foldSummary = Summary <$> L.length
                      <*> L.premap state L.nub
                      <*> (L.premap county L.nub <&> length)

data ModelParams = ModelParams
  { rateWithB :: Double -- ^ ratio of houses with and without basement
  , radonWithB :: Double -- ^ radon level in houses with basement
  , radonWithoutB :: Double -- ^ radon level in houses without basement
  , noiseWithB :: Double -- ^ variation around radonWithB
  , noiseWithoutB :: Double -- ^ variation around radonWithoutB
  } deriving (Eq, Show, Generic, ToJSON, FromJSON)

priorModel :: MonadSample m => m ModelParams
priorModel =
    ModelParams <$> uniform 0 1
                <*> uniform 0 10
                <*> uniform 0 10
                <*> uniform 0 10
                <*> uniform 0 10

likelihood :: ModelParams -> (Bool, Double) -> Log Double
likelihood params (hasBasement, radonObserved) = 
    case hasBasement of
        True -> let radonModel = radonWithB params
                    noiseModel = noiseWithB params
                    rate = realToFrac $ rateWithB params
                in rate * normalPdf radonModel noiseModel radonObserved
        False -> let radonModel = radonWithoutB params
                     noiseModel = noiseWithoutB params
                     rate = realToFrac $ 1 - rateWithB params
                in rate * normalPdf radonModel noiseModel radonObserved

model :: MonadInfer m => [(Bool, Double)] -> m ModelParams
model observations = do
    params <- priorModel
    mapM_ (score . likelihood params) observations
    return params

posteriorForward :: MonadSample m => m ModelParams -> m (Bool, Double)
posteriorForward model = do
    params <- model
    hasBasement <- bernoulli (rateWithB params)
    value <- case hasBasement of
        True -> normal (radonWithB params) (noiseWithB params)
        False -> normal (radonWithoutB params) (noiseWithoutB params)
    return (hasBasement, value)

sampleFlatLinRegModel :: (LogThrow m) => PTask m () ()
sampleFlatLinRegModel = proc () -> do
  radonObs <- loadData radonObsFile -< ()
  writeData filteredCsvFile -< radonObs
  let (summary,xs,ys) = flip L.fold radonObs $
        (,,) <$> foldSummary
             <*> L.premap ((== "Y") . basement) L.list
             <*> L.premap log_radon L.list
      xLbl = "has basement"
      yLbl = "log radon"
  logInfo -< show summary
  
  vizSize <- getOption ["viz", "options"]
             (docField @"vizSize" (400,400) "Width & height of visualisations") -< ()
  writeViz "1" -< plot vizSize
                       (S $ scatter2 xLbl yLbl (-3,5))
                       (Cols [(xLbl, VL.Booleans xs)
                             ,(yLbl, VL.Numbers ys)])
  nsamples <- getOption ["sampling", "options"]
              (docField @"nsamples" 5000 "Number of samples to draw") -< ()
  samples <- ioTask -<
    sampleIOfixed $ prior $ mh nsamples $ model (zip xs ys)
  writeViz "2" -< plot vizSize
                       (H [[density2DPlot "radonWithB" "radonWithoutB" (0,2) (0,2)]
                          ,[density2DPlot "noiseWithB" "noiseWithoutB" (0,2) (0,2)]])
                       (J samples)

  samples <- ioTask -<
    sampleIOfixed $ prior $ mh nsamples $ posteriorForward $ model (zip xs ys)
  let (xModel, yModel) = unzip samples
  writeViz "3" -< plot vizSize
    (S $ scatter2 xLbl yLbl (-3,5))
    (Cols [(xLbl, VL.Booleans xModel)
          ,(yLbl, VL.Numbers yModel)])
  

runIn topdir = runPipelineTask
  (FullConfig "example-radon"  -- Name of the executable (for --help)
              "example-radon.yaml" -- Default config file path
              topdir -- Default root directory for mappings
              ())
  (baseContexts "")
  sampleFlatLinRegModel ()

main :: IO ()
main = runIn "examples/example-radon"