{-| 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 ExampleEvents ( exampleEvents4 ) import Hasklepias import Test.Hspec ( Spec , describe , it , pending , shouldBe , xcontext ) {- 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` ???