packages feed

hasklepias-0.16.0: examples/ExampleFeatures4.hs

{-|
Module      : ExampleFeatures4
Description : Demostrates how to define an outcome monitoring treatment regimes
              over time.
Copyright   : (c) NoviSci, Inc 2020
License     : BSD3
Maintainer  : bsaul@novisci.com
-}

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeApplications #-}

module ExampleFeatures4(
    exampleFeatures4Spec
) where

import Hasklepias
import ExampleEvents ( exampleEvents4 )
import Test.Hspec ( shouldBe, it, Spec, xcontext, describe, pending )

{-
  Example Data and utilities to create such
-}

type EventData a b = (b, a, Text)

t1 :: (a, b, c) -> a
t1 (x , _ , _) = x
t2 :: (a, b, c) -> b
t2 (_ , x , _) = x
t3 :: (a, b, c) -> c
t3 (_ , _ , x) = x

toEvent :: (IntervalSizeable a b, Show a, Integral b) => 
  EventData a b-> Event a
toEvent x = event (beginerval (t1 x) (t2 x))
    (context (UnimplementedDomain ()) (packConcepts [t3 x]))

toEvents :: (Show a, IntervalSizeable a b, Integral b) =>
  [EventData a b] -> Events a
toEvents = sort.map toEvent

sapExample1 :: Events Day
sapExample1 = toEvents [
    (1, fromGregorian 2017 1 1, "index")
  , (1, fromGregorian 2017 3 1, "pcsk")
 ]

sapExample2 :: Events Day
sapExample2 = toEvents [
    (1, fromGregorian 2017 1 1,   "index")
  , (1, fromGregorian 2017 3 1, "wellness")
 ]

p1Events :: Events Int
p1Events = toEvents [
    (1, 1,   "index")
  , (1, 7 + 15, "pcsk")
 ]

p2Events :: Events Int
p2Events = toEvents [
    (1, 1,   "index")
  , (1, 7 + 60, "pcsk")
 ]

p3Events :: Events Int
p3Events = toEvents [
    (1, 1,   "index")
  , (1, 7 + 120, "pcsk")
 ]

p4Events :: Events Int
p4Events = toEvents [
    (1, 1,   "index")
  , (1, 7 + 240, "pcsk")
 ]

p5Events :: Events Int
p5Events = toEvents [
    (1, 1,   "index")
 ]

{-
  Types used for features
-}

data CensorReason =
  -- The order matters here in that if two censoring events occur on the same
  -- day then the reason for censoring will be chosen based on the following 
  -- ordering.
    Death
  | Disenrollment
  | Discontinuation
  | Noncompliance
  | EndOfData
  deriving (Eq, Show, Ord)

instance OccurrenceReason CensorReason

data OutcomeReason =
    Wellness
  | Accident
  -- etc
  deriving (Eq, Show, Ord)

instance OccurrenceReason OutcomeReason

data NegOutcomes b = MkNegOutcome {
    g1 :: CensoredOccurrence CensorReason OutcomeReason b
  , g2 :: CensoredOccurrence CensorReason OutcomeReason b
  , g3 :: CensoredOccurrence CensorReason OutcomeReason b
  , g4 :: CensoredOccurrence CensorReason OutcomeReason b
  , g5 :: CensoredOccurrence CensorReason OutcomeReason b
 } deriving (Eq)

instance (Show b) => Show ( NegOutcomes b ) where
  show (MkNegOutcome x1 x2 x3 x4 x5) =
    "\n g1: " ++ show x1 ++
    "\n g2: " ++ show x2 ++
    "\n g3: " ++ show x3 ++
    "\n g4: " ++ show x4 ++
    "\n g5: " ++ show x5 ++
    "\n"

data ProtocolStatus a =
    Compliant
  | NonCompliant (EventTime a)
  deriving (Eq, Show)

data Protocols a = MkProtocols {
    noInit  :: ProtocolStatus a
  , init30  :: ProtocolStatus a
  , init90  :: ProtocolStatus a
  , init180 :: ProtocolStatus a
  , init365 :: ProtocolStatus a
 } deriving (Eq)

instance (Show a) => Show (Protocols a) where
  show (MkProtocols x1 x2 x3 x4 x5) =
    "\n " ++ show x1 ++
    "\n " ++ show x2 ++
    "\n " ++ show x3 ++
    "\n " ++ show x4 ++
    "\n " ++ show x5 ++
    "\n"

{-
  Helper functions
-}

-- | Duration of follow up in days
followupDuration :: Integral b => b
followupDuration = 365

-- | Duration to not observe events after the begin of index.
washoutDuration :: Integral b => b
washoutDuration = 7

-- | Creates an interval *starting 7 days after the index* and 
--   ending 'followupDuration' days later.
makeFollowupInterval :: ( Integral b
                        , Intervallic i a
                        , IntervalSizeable a b) => 
    b -> Index i a -> Interval a
makeFollowupInterval dur index =  
  beginerval dur (add washoutDuration (begin index))

-- | Creates an interval *starting 7 days after the index* and 
--   ending 'followupDuration' days later.
followupInterval :: (Integral b, IntervalSizeable a b) => 
    Index Interval a -> Interval a
followupInterval = makeFollowupInterval 365

{-
  Functions for defining the study's exposure protocol(s)
-}

protocol :: ( Intervallic i0 a
            , Intervallic i1 a
            , Intervallic i2 a
            , IntervalSizeable a b
            , Filterable container) =>
       (Index i2 a -> i0 a)
    -- ^ Function that maps an index interval to interval during which protocol is evaluated
    -> (i0 a -> container (i1 a) -> ProtocolStatus b)
    -- ^ Function that maps data to a @ProtocolStatus@.
    -> Index i2 a
    -> container (i1 a)
    -> ProtocolStatus b
protocol g f i dat = f (g i) ( filterConcur (g i) dat )

compliantIfNone :: (  IntervalSizeable a b
                    , Intervallic i0 a
                    , Intervallic i1 a
                    , Witherable container ) =>
        i0 a
    -> container (i1 a)
    -> ProtocolStatus b
compliantIfNone i x
  | null x     = Compliant
  | otherwise  = NonCompliant (mkEventTime (fmap (`diff` begin i) (end <$> headMay (toList x))))

compliantIfSome :: ( IntervalSizeable a b
                    , Intervallic i0 a
                    , Intervallic i1 a
                    , Witherable container) =>
       i0 a
    -> container (i1 a)
    -> ProtocolStatus b
compliantIfSome i x
  | null x     = NonCompliant (mkEventTime (Just $ diff (end i) (begin i)))
  | otherwise  = Compliant

protocolNoInit :: ( Integral b
                  , IntervalSizeable a b
                  , Intervallic i0 a
                  , Intervallic i1 a
                  , Witherable container) =>
       Index i0 a
    -> container (i1 a)
    -> ProtocolStatus b
protocolNoInit = protocol (makeFollowupInterval 365) compliantIfNone

protocols :: (  Integral b
              , IntervalSizeable a b
              , Intervallic i0 a
              , Intervallic i1 a
              , Witherable container) =>
       Index i0 a
    -> container (i1 a)
    -> Protocols b
protocols i e = MkProtocols
  ( protocol (makeFollowupInterval 365) compliantIfNone i e)
  ( protocol (makeFollowupInterval 30 ) compliantIfSome i e)
  ( protocol (makeFollowupInterval 90 ) compliantIfSome i e)
  ( protocol (makeFollowupInterval 180) compliantIfSome i e)
  ( protocol (makeFollowupInterval 365) compliantIfSome i e)

-- adminCensor :: (Integral b) => EventTime b -> CensoredOccurrence c o b
-- adminCensor t = MkCensoredOccurrence AdminCensor ( RightCensored t )

compliantOutcome :: (Integral b) => 
     EventTime b
  -> Occurrence OutcomeReason b
  -> Occurrence CensorReason b
  -> CensoredOccurrence CensorReason OutcomeReason b
compliantOutcome
      adminTime
      (MkOccurrence  (oreason, otime))
      (MkOccurrence  (creason, ctime))
  | all (adminTime <) [otime, ctime]   = adminCensor adminTime
  | all (otime <=)    [ctime]          = MkCensoredOccurrence (O oreason) (Uncensored otime)
  | otherwise                          = MkCensoredOccurrence (C creason) (RightCensored ctime)

nonCompliantOutcome :: (Integral b) => 
     EventTime b
  -> EventTime b
  -> Occurrence OutcomeReason b
  -> Occurrence CensorReason b
  -> CensoredOccurrence CensorReason OutcomeReason b
nonCompliantOutcome
      etime
      adminTime
      (MkOccurrence  (oreason, otime))
      (MkOccurrence  (creason, ctime))
  | all (adminTime <) [otime, ctime, etime] = adminCensor adminTime
  | all (otime <=)    [ctime, etime]        = MkCensoredOccurrence (O oreason) (Uncensored otime)
  | etime <= ctime                          = MkCensoredOccurrence (C Noncompliance) (RightCensored etime)
  | otherwise                               = MkCensoredOccurrence (C creason) (RightCensored ctime)

decideOutcome :: (Integral b) =>
     EventTime b      -- ^ admin censoring time
  -> ProtocolStatus b -- ^ pcsk
  -> Occurrence OutcomeReason b    -- ^ time of outcome
  -> Occurrence CensorReason b    -- ^ time of censoring (other than noncompliance)
  -> CensoredOccurrence CensorReason OutcomeReason b
decideOutcome  adminTime exposure outcomeTime censorTime =
  case exposure of
    Compliant      -> compliantOutcome adminTime outcomeTime censorTime
    NonCompliant t -> nonCompliantOutcome t adminTime outcomeTime censorTime

{-
   Features needed to evaluate censoring and outcome events
-}
index :: (Ord a)=> 
  Def (F "events" (Events a) -> F "index" (Index Interval a))
index = defineA (
       makeConceptsFilter ["index"]
    .> intervals
    .> headMay
    .> \case
          Nothing -> makeFeature $ featureDataL ( Other "no index" )
          Just x  -> pure $ makeIndex x
      )

flupEvents :: (Integral b, IntervalSizeable a b) => 
    Def (
     F "index" (Index Interval a)
  -> F "events" (Events a)
  -> F "allFollowupEvents" (Events b))
flupEvents = define (\index es ->
      es
      |> filterConcur ( followupInterval index)
      |> fmap ( diffFromBegin ( followupInterval index ) )
    )

{-
   Censoring Events
-}

death :: Integral b => Def (
     F "allFollowupEvents" (Events b)
  -> F "death" (EventTime b))
death = define (mkEventTime . fmap begin . firstConceptOccurrence ["death"])

disenrollment :: (Integral b, IntervalSizeable a b) =>
  Def (
     F "index" (Index Interval a)
     -- using all events rather than just follow-up events because enrollment
     -- intervals need to be combined first
  -> F "events" (Events a)
  -> F "disenrollment" (EventTime b))
disenrollment = define (\i events ->
  events
  |> makeConceptsFilter ["enrollment"]
  -- combine any concurring enrollment intervals
  |> combineIntervals
  -- find gaps between any enrollment intervals (as well as bounds of followup )
  |> gapsWithin (followupInterval i)
  -- get the first gap longer than 30 days (if it exists)
  |> \x -> (headMay . filter (\x -> duration x > 30)) =<< x
  -- Shift endpoints of intervals so that end of follow up is reference point
  |> fmap (diffFromBegin (followupInterval i))
  -- take the end of this gap as the time of disenrollment
  |> fmap end
  |> mkEventTime )

-- | A collector feature for all censors (except noncompliance)
censorTime :: (Integral b) => Def (
     F "death" (EventTime b)
  -> F "disenrollment" (EventTime b)
  -- etc
  -> F "censortime" (Occurrence CensorReason b))
censorTime = define (
  \dth disrl  ->
      minimum [ makeOccurrence Death dth
              , makeOccurrence Disenrollment disrl
              -- etc
              ]
   )

{- 
  Exposure Definitions
-}

pcskEvents :: Def (
     F "events" (Events a)
  -> F "pcskEvents" (Events a))
pcskEvents = define ( makeConceptsFilter ["pcsk"] )

pcskProtocols :: (Integral b, IntervalSizeable a b) => 
  Def (
     F "index" (Index Interval a)
  -> F "pcskEvents" (Events a)
  -> F "pcskProtocols" (Protocols b) )
pcskProtocols = define protocols

{-
  Outcome definitions
-}

makeg :: (Integral b, IntervalSizeable a b) => 
      b
    -> Index Interval a
    -> ProtocolStatus b
    -> Occurrence OutcomeReason b
    -> Occurrence CensorReason b
    -> CensoredOccurrence CensorReason OutcomeReason b
makeg dur i = decideOutcome 
    (mkEventTime $ Just $ duration (makeFollowupInterval dur i))

makeNegOutcomes :: (Integral b, IntervalSizeable a b) => 
      Index Interval a
   -> Protocols b
   -> Occurrence CensorReason b 
   -> Occurrence OutcomeReason b
   -> NegOutcomes b
makeNegOutcomes i (MkProtocols p1 p2 p3 p4 p5) c o = MkNegOutcome
    (makeg 365 i p1 o c)
    (makeg 30  i p2 o c)
    (makeg 90  i p3 o c)
    (makeg 180 i p4 o c)
    (makeg 365 i p5 o c)

type OutcomeFeature name a b = 
     F "index" (Index Interval a)
  -> F "allFollowupEvents" (Events b)
  -> F "pcskProtocols" (Protocols b)
  -> F "censortime" (Occurrence CensorReason b)
  -> F name (NegOutcomes b)

makeOutcomeDefinition :: 
  ( KnownSymbol name
  , Integral b
  , IntervalSizeable  a b) =>
    [Text]
  -> OutcomeReason
  -> Def (   F "index" (Index Interval a)
          -> F "allFollowupEvents" (Events b)
          -> F "pcskProtocols" (Protocols b)
          -> F "censortime" ( Occurrence CensorReason b) 
          -> F name ( NegOutcomes b))
makeOutcomeDefinition cpt oreason  = define (
    \index events protocols censor  ->
      events
      |> firstConceptOccurrence cpt
      |> \x -> makeOccurrence oreason (mkEventTime (fmap begin x))
      |> makeNegOutcomes index protocols censor
  )

o1 :: (Integral b, IntervalSizeable  a b ) => 
  Def ( OutcomeFeature "wellness" a b)
o1 = makeOutcomeDefinition ["wellness"] Wellness

o2 :: (Integral b, IntervalSizeable  a b ) => 
  Def ( OutcomeFeature "accident" a b)
o2 = makeOutcomeDefinition ["accident"] Accident 

{- 
   Tests of protocols
-}

testProtocols :: (Integral b, IntervalSizeable a b ) => 
  [Event a] -> Feature "pcskProtocols" (Protocols b)
testProtocols input = eval pcskProtocols (idx,  pcev)
  where evs   = pure input
        idx   = eval index evs
        pcev  = eval pcskEvents evs

p1Protocols :: Feature "pcskProtocols" (Protocols Int)
p1Protocols = pure $ MkProtocols
    ( NonCompliant (mkEventTime (Just 15)) )
      Compliant
      Compliant
      Compliant
      Compliant

p2Protocols :: Feature "pcskProtocols" (Protocols Int)
p2Protocols = pure $ MkProtocols
    ( NonCompliant (mkEventTime (Just 60)) )
    ( NonCompliant (mkEventTime (Just 30)) )
      Compliant
      Compliant
      Compliant

p3Protocols :: Feature "pcskProtocols" (Protocols Int)
p3Protocols = pure $ MkProtocols
    ( NonCompliant (mkEventTime (Just 120)) )
    ( NonCompliant (mkEventTime (Just 30)) )
    ( NonCompliant (mkEventTime (Just 90)) )
      Compliant
      Compliant

p4Protocols :: Feature "pcskProtocols" (Protocols Int)
p4Protocols = pure $ MkProtocols
    ( NonCompliant (mkEventTime (Just 240)) )
    ( NonCompliant (mkEventTime (Just 30)) )
    ( NonCompliant (mkEventTime (Just 90)) )
    ( NonCompliant (mkEventTime (Just 180)) )
      Compliant

p5Protocols :: Feature "pcskProtocols" (Protocols Int)
p5Protocols = pure $ MkProtocols
      Compliant
    ( NonCompliant (mkEventTime (Just 30)) )
    ( NonCompliant (mkEventTime (Just 90)) )
    ( NonCompliant (mkEventTime (Just 180)) )
    ( NonCompliant (mkEventTime (Just 365)) )

{- 
   Tests of outcomes
-}

testOutcomes :: ( Integral b, IntervalSizeable  a b ) => 
      [Event a]
   -> (Feature "wellness" ( NegOutcomes b ), Feature "accident" (NegOutcomes b) )
testOutcomes input = (
      eval o1 (idx, flevs, prot, ctime) 
    , eval o2 (idx, flevs, prot, ctime) )
  where evs   = pure input
        idx   = eval index evs
        flevs = eval flupEvents (idx, evs)
        pcev  = eval pcskEvents evs
        dth   = eval death flevs
        disen = eval disenrollment (idx, evs)
        prot  = eval pcskProtocols (idx, pcev)
        ctime = eval censorTime (dth, disen)

p1Outcomes :: (Integral b) => 
  ( Feature "wellness" ( NegOutcomes b)
  , Feature "accident" ( NegOutcomes b)) 
p1Outcomes = (
    pure $ MkNegOutcome 
      (MkCensoredOccurrence (C Noncompliance) (RightCensored (mkEventTime (Just 15))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 30))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 90))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 180))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 365))))
  , pure $ MkNegOutcome 
      (MkCensoredOccurrence (C Noncompliance) (RightCensored (mkEventTime (Just 15))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 30))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 90))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 180))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 365)))) 
 )


p1Outcomes' :: (Integral b) => 
  ( Feature "wellness" (NegOutcomes b)
  , Feature "accident" (NegOutcomes b)) 
p1Outcomes' = (
    pure $ MkNegOutcome 
      (MkCensoredOccurrence (C Noncompliance) (RightCensored (mkEventTime (Just 15))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 30))))
      (MkCensoredOccurrence (O Wellness)      (Uncensored (mkEventTime (Just 51))))
      (MkCensoredOccurrence (O Wellness)      (Uncensored (mkEventTime (Just 51))))
      (MkCensoredOccurrence (O Wellness)      (Uncensored (mkEventTime (Just 51))))
  , pure $ MkNegOutcome 
      (MkCensoredOccurrence (C Noncompliance) (RightCensored (mkEventTime (Just 15))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 30))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 90))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 180))))
      (MkCensoredOccurrence  AdminCensor      (RightCensored (mkEventTime (Just 365)))) 
 )

{-
  Test specs
-}

exampleFeatures4Spec :: Spec
exampleFeatures4Spec = do
    describe "tests of exposure protocols" $
      do
        it "p1" $
          testProtocols p1Events `shouldBe` p1Protocols
        it "p2" $
          testProtocols p2Events `shouldBe` p2Protocols
        it "p3" $
          testProtocols p3Events `shouldBe` p3Protocols
        it "p4" $
          testProtocols p4Events `shouldBe` p4Protocols
        it "p5" $
          testProtocols p5Events `shouldBe` p5Protocols

    describe "tests of outcomes" $
      do
        it "p1" $
          testOutcomes p1Events `shouldBe` p1Outcomes
        it "p1'" $
          testOutcomes (sort p1Events <> [toEvent (1, 59, "wellness")] ) 
            `shouldBe` p1Outcomes'

    -- describe "SAP examples" $ 
    --   do
    --     it "sap example 1" pending
        -- testOutcomes sapExample1 `shouldBe` ???