hasklepias 0.14.0 → 0.15.0
raw patch · 33 files changed
+1839/−1109 lines, 33 filesdep ~interval-algebra
Dependency ranges changed: interval-algebra
Files
- ChangeLog.md +9/−0
- examples/ExampleFeatures4.hs +571/−0
- examples/Main.hs +5/−2
- hasklepias.cabal +16/−14
- src/Cohort.hs +35/−0
- src/Cohort/Core.hs +167/−0
- src/Cohort/Criteria.hs +144/−0
- src/Cohort/Index.hs +39/−0
- src/Cohort/Input.hs +97/−0
- src/Cohort/Output.hs +120/−0
- src/FeatureEvents.hs +0/−249
- src/Features/Compose.hs +17/−1
- src/Hasklepias.hs +7/−4
- src/Hasklepias/Cohort.hs +0/−35
- src/Hasklepias/Cohort/Core.hs +0/−167
- src/Hasklepias/Cohort/Criteria.hs +0/−144
- src/Hasklepias/Cohort/Index.hs +0/−39
- src/Hasklepias/Cohort/Input.hs +0/−97
- src/Hasklepias/Cohort/Output.hs +0/−120
- src/Hasklepias/FeatureEvents.hs +269/−0
- src/Hasklepias/MakeApp.hs +1/−1
- src/Hasklepias/Misc.hs +89/−0
- src/Hasklepias/Reexports.hs +27/−9
- test/Cohort/CoreSpec.hs +72/−0
- test/Cohort/CriteriaSpec.hs +48/−0
- test/Cohort/InputSpec.hs +72/−0
- test/EventDataSpec.hs +1/−1
- test/FeatureEventsSpec.hs +0/−32
- test/Features/OutputSpec.hs +1/−1
- test/Hasklepias/Cohort/CoreSpec.hs +0/−72
- test/Hasklepias/Cohort/CriteriaSpec.hs +0/−48
- test/Hasklepias/Cohort/InputSpec.hs +0/−73
- test/Hasklepias/FeatureEventsSpec.hs +32/−0
ChangeLog.md view
@@ -1,5 +1,14 @@ # Changelog for hasklepias +## 0.15.0++* Adds `Occurrence` type which is a simply a pair a reason and an `EventTime`. That is, an `Occurrence` captures what and when something occurred. Adds the `CensoredOccurrence` type which is similar to an `Occurrence`, except that the reason is of type `CensoringReason cr or`, where `data CensoringReason cr or = AdminCensor | C cr | O or`. The time of a `CensoredOccurrence` is a `MaybeCensored (EventTime a)` (not simply can `EventTime`). See the examples in `examples/ExampleFeatures4` for usage.+* A number of the utility functions in the `Hasklepias.FeatureEvents` module are generalized to operate on data structures other than lists of `Event`s.+* Exports type synonyms `F n a` and `Def d` for `Feature n a` and `Definiton d` to save a bit of typing.+* Adds `Hasklepias.Misc` module as a location to collect miscellaneous types and functions for the time-being, until better locations are found or created.+* Adds `examples/ExampleFeatures4.hs` which is an extensive example of assessing exposure protocols and censored outcomes.+* Moves the `Cohort` module to the top-level. Moves the `FeatureEvents` module within the `Hasklepias` module.+ ## 0.14.0 * Adds the ability to modify the output shape of cohorts. The `makeCohortApp` now takes a "`shape`" function as an argument of type `Cohort d -> CohortShape shape`. Currently, two functions of this type are provided: `rowWise` and `colWise`. The `rowWise` functions presents the output feature data in a row-wise format where each subject's data is its own array; whereas `colWise` presents the feature data where all the data of a given feature are in a single array.
+ examples/ExampleFeatures4.hs view
@@ -0,0 +1,571 @@+{-|+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 $ getIndex 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` ???
examples/Main.hs view
@@ -6,6 +6,7 @@ import ExampleFeatures1 ( exampleFeatures1Spec ) import ExampleFeatures2 ( exampleFeatures2Spec ) import ExampleFeatures3 ( exampleFeatures3Spec ) +import ExampleFeatures4 ( exampleFeatures4Spec ) import ExampleCohort1 ( exampleCohort1tests ) import Test.Tasty import Test.Tasty.Hspec ( testSpec )@@ -18,11 +19,13 @@ main = do spec1 <- testSpec "spec1" exampleFeatures1Spec spec2 <- testSpec "spec2" exampleFeatures2Spec- spec3 <- testSpec "spec3" exampleFeatures3Spec + spec3 <- testSpec "spec3" exampleFeatures3Spec+ spec4 <- testSpec "spec4" exampleFeatures4Spec defaultMain (testGroup "tests" [ spec1 , spec2- , spec3 + , spec3+ , spec4 , testGroup "Tests" [exampleCohort1tests] ])
hasklepias.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: hasklepias-version: 0.14.0+version: 0.15.0 description: Please see the README on GitHub at <https://github.com/novisci/asclepias#readme> homepage: https://github.com/novisci/asclepias/#readme bug-reports: https://github.com/novisci/asclepias/issues@@ -36,17 +36,18 @@ Features.Featureable Features.Output Features.Featureset- FeatureEvents+ Cohort+ Cohort.Core+ Cohort.Input+ Cohort.Output+ Cohort.Criteria+ Cohort.Index Hasklepias+ Hasklepias.FeatureEvents Hasklepias.MakeApp+ Hasklepias.Misc Hasklepias.Reexports Hasklepias.ReexportsUnsafe- Hasklepias.Cohort- Hasklepias.Cohort.Core- Hasklepias.Cohort.Input- Hasklepias.Cohort.Output- Hasklepias.Cohort.Criteria- Hasklepias.Cohort.Index Stype Stype.Aeson Stype.Numeric@@ -71,7 +72,7 @@ , co-log == 0.4.0.1 , flow == 1.0.22 , ghc-prim == 0.6.1- , interval-algebra == 0.9.0+ , interval-algebra == 0.10.0 , lens == 5.0.1 , lens-aeson == 1.1.1 , mtl == 2.2.2@@ -101,10 +102,10 @@ FeaturesSpec Features.OutputSpec Features.FeaturesetSpec- Hasklepias.Cohort.InputSpec- Hasklepias.Cohort.CoreSpec- Hasklepias.Cohort.CriteriaSpec- FeatureEventsSpec+ Cohort.InputSpec+ Cohort.CoreSpec+ Cohort.CriteriaSpec+ Hasklepias.FeatureEventsSpec Stype.AesonSpec Stype.Numeric.CensoredSpec Paths_hasklepias@@ -121,7 +122,7 @@ , flow == 1.0.22 , hasklepias , hspec- , interval-algebra == 0.9.0+ , interval-algebra == 0.10.0 , lens == 5.0.1 , text >=1.2.3 , time >=1.11@@ -138,6 +139,7 @@ ExampleFeatures1 ExampleFeatures2 ExampleFeatures3+ ExampleFeatures4 ExampleCohort1 hs-source-dirs: examples
+ src/Cohort.hs view
@@ -0,0 +1,35 @@+{-|+Module : Hasklepias Cohorts+Description : Defines the Cohort type and associated methods+Copyright : (c) NoviSci, Inc 2020+License : BSD3+Maintainer : bsaul@novisci.com+-}+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE NoImplicitPrelude #-}++module Cohort(++ -- * Defining Cohorts+ module Cohort.Core++ -- ** Criteria+ , module Cohort.Criteria+ -- ** Index+ , module Cohort.Index++ -- * Cohort I/O+ -- ** Input+ , module Cohort.Input + -- ** Output+ , module Cohort.Output+++) where+++import Cohort.Core+import Cohort.Index+import Cohort.Criteria+import Cohort.Input+import Cohort.Output
+ src/Cohort/Core.hs view
@@ -0,0 +1,167 @@+{-|+Module : Hasklepias Cohorts+Description : Defines the Cohort type and associated methods+Copyright : (c) NoviSci, Inc 2020+License : BSD3+Maintainer : bsaul@novisci.com+-}+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE LambdaCase #-}+-- {-# LANGUAGE Safe #-}++module Cohort.Core(+ Subject(..)+ , ID+ , Population(..)+ , ObsUnit(..)+ , CohortData(..)+ , Cohort(..)+ , CohortSpec+ , AttritionInfo(..)+ , specifyCohort+ , makeObsUnitFeatures+ , evalCohort+ , getCohortIDs+ , getCohortData+ , getAttritionInfo+) where++import GHC.Num ( Num((+)), Natural )+import Data.Aeson ( FromJSON, ToJSON, ToJSONKey )+import Data.Bool ( Bool )+import Data.Eq ( Eq )+import Data.Foldable ( Foldable(length) )+import Data.Function ( ($) )+import Data.Functor ( Functor(fmap) )+import Data.Maybe ( Maybe(..), catMaybes )+import Data.List ( zipWith, replicate )+import Data.List.NonEmpty ( NonEmpty(..), zip, fromList, nonEmpty )+import Data.Map.Strict as Map ( toList, fromListWith )+import Data.Text ( Text )+import GHC.Generics ( Generic )+import GHC.Show ( Show(..) )+import Cohort.Index ( makeIndex, Index(..) )+import Cohort.Criteria++-- | A subject identifier. Currently, simply @Text@.+type ID = Text++-- | A subject is just a pair of @ID@ and data.+newtype Subject d = MkSubject (ID, d)+ deriving (Eq, Show, Generic)++instance Functor Subject where+ fmap f (MkSubject (id, x)) = MkSubject (id, f x)++instance (FromJSON d) => FromJSON (Subject d) where++-- | A population is a list of @'Subject'@s+newtype Population d = MkPopulation [Subject d] + deriving (Eq, Show, Generic)++instance Functor Population where+ fmap f (MkPopulation x) = MkPopulation (fmap (fmap f) x)++instance (FromJSON d) => FromJSON (Population d) where++-- | An observational unit is what a subject may be transformed into.+data ObsUnit d = MkObsUnit {+ obsID :: ID+ , obsData :: d }+ deriving (Eq, Show, Generic)++-- | A container for CohortData+newtype CohortData d = MkCohortData { getObsData :: [ObsUnit d] }+ deriving (Eq, Show, Generic)++-- | A cohort is a list of observational units along with @'AttritionInfo'@ +-- regarding the number of subjects excluded by the @'Criteria'@. +newtype Cohort d = MkCohort (Maybe AttritionInfo, CohortData d)+ deriving (Eq, Show, Generic)++-- | Gets the attrition info from a cohort+getAttritionInfo :: Cohort d -> Maybe AttritionInfo+getAttritionInfo (MkCohort (x, _)) = x++-- | Unpacks a @'Population'@ to a list of subjects.+getPopulation :: Population d -> [Subject d]+getPopulation (MkPopulation x) = x++-- | Gets the data out of a @'Subject'@.+getSubjectData :: Subject d -> d+getSubjectData (MkSubject (_, x)) = x++-- | Tranforms a @'Subject'@ into a @'ObsUnit'@.+makeObsUnitFeatures :: (d1 -> d0) -> Subject d1 -> ObsUnit d0+makeObsUnitFeatures f (MkSubject (id, dat)) = MkObsUnit id (f dat)++-- | A cohort specification consist of two functions: one that transforms a subject's+-- input data into a @'Criteria'@ and another that transforms a subject's input data+-- into the desired return type.+data CohortSpec d1 d0 = MkCohortSpec+ { runCriteria:: d1 -> Criteria+ -- (Feature (Index i a))+ , runFeatures:: d1 -> d0 }++-- | Creates a @'CohortSpec'@.+specifyCohort :: (d1 -> Criteria) -> (d1 -> d0) -> CohortSpec d1 d0+specifyCohort = MkCohortSpec++-- | Evaluates the @'runCriteria'@ of a @'CohortSpec'@ on a @'Population'@ to +-- return a list of @Subject Criteria@ (one per subject in the population). +evalCriteria :: CohortSpec d1 d0 -> Population d1 -> [Subject Criteria]+evalCriteria (MkCohortSpec runCrit _) (MkPopulation pop) = fmap (fmap runCrit) pop++-- | Convert a list of @Subject Criteria@ into a list of @Subject CohortStatus@+evalCohortStatus :: [Subject Criteria] -> [Subject CohortStatus]+evalCohortStatus = fmap (fmap checkCohortStatus)++-- | Runs the input function which transforms a subject into an observational unit. +-- If the subeject is excluded, the result is @Nothing@; otherwise it is @Just@ +-- an observational unit.+evalSubjectCohort :: (d1 -> d0) -> Subject CohortStatus -> Subject d1 -> Maybe (ObsUnit d0)+evalSubjectCohort f (MkSubject (id, status)) subjData =+ case status of+ Included -> Just $ makeObsUnitFeatures f subjData+ ExcludedBy _ -> Nothing++-- | A type which collects the counts of subjects included or excluded.+newtype AttritionInfo = MkAttritionInfo (NonEmpty (CohortStatus, Natural))+ deriving (Eq, Show, Generic)++-- | Initializes @AttritionInfo@ from a @'Criteria'@.+initAttritionInfo :: Criteria -> AttritionInfo+initAttritionInfo x =+ MkAttritionInfo $ zip (initStatusInfo x) + (0 :| replicate (length (getCriteria x)) 0)++-- | Creates an @'AttritionInfo'@ from a list of @Subject CohortStatus@. The result+-- is @Nothing@ if the input list is empty.+measureAttrition :: [Subject CohortStatus] -> Maybe AttritionInfo+measureAttrition l = fmap MkAttritionInfo $ nonEmpty $ Map.toList $+ Map.fromListWith (+) $ fmap (\x -> (getSubjectData x, 1)) l++-- | The internal function to evaluate a @'CohortSpec'@ on a @'Population'@. +evalUnits :: CohortSpec d1 d0 -> Population d1 -> (Maybe AttritionInfo, CohortData d0)+evalUnits spec pop =+ ( measureAttrition statuses+ , MkCohortData $ catMaybes $ zipWith (evalSubjectCohort (runFeatures spec))+ statuses+ (getPopulation pop))+ where crits = evalCriteria spec pop+ statuses = evalCohortStatus crits++-- | Evaluates a @'CohortSpec'@ on a @'Population'@.+evalCohort :: CohortSpec d1 d0 -> Population d1 -> Cohort d0+evalCohort s p = MkCohort $ evalUnits s p++-- | Get IDs from a cohort.+getCohortIDs :: Cohort d -> [ID]+getCohortIDs (MkCohort (_, dat)) = fmap obsID ( getObsData dat )++-- | Get data from a cohort.+getCohortData :: Cohort d -> [d]+getCohortData (MkCohort (_, dat)) = fmap obsData ( getObsData dat )
+ src/Cohort/Criteria.hs view
@@ -0,0 +1,144 @@+{-|+Module : Cohort Criteria+Description : Defines the Criteria and related types and functions+Copyright : (c) NoviSci, Inc 2020+License : BSD3+Maintainer : bsaul@novisci.com+-}+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE Safe #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TupleSections #-}++module Cohort.Criteria(+ Criterion+ , Criteria(..)+ , Status(..)+ , CohortStatus(..)+ , criterion+ , criteria+ , excludeIf+ , includeIf+ , initStatusInfo+ , checkCohortStatus+) where++import safe GHC.Generics ( Generic )+import safe GHC.Num ( Num((+)), Natural )+import safe GHC.Show ( Show(show) )+import safe GHC.TypeLits ( KnownSymbol, symbolVal )+import safe Control.Applicative ( Applicative(pure) )+import safe Control.Monad ( Functor(..) )+import safe Data.Bifunctor ( Bifunctor(second) )+import safe Data.Bool ( Bool(..), otherwise, not, (&&) )+import safe Data.Either ( either )+import safe Data.Eq ( Eq(..) )+import safe Data.Function ( ($), (.), const, id )+import safe qualified Data.List.NonEmpty as NE+ ( NonEmpty, zip, fromList, toList, map )+import safe Data.List ( find, (++) )+import safe Data.Maybe ( Maybe(..), maybe )+import safe Data.Ord ( Ord(..), Ordering(..) )+import safe Data.Semigroup ( Semigroup((<>)) )+import safe Data.Tuple ( fst, snd )+import safe Data.Text ( Text, pack )+import safe Features.Compose ( getFeatureData+ , Feature+ , nameFeature+ , FeatureN(..) )++-- | Defines the return type for @'Criterion'@ indicating whether to include or +-- exclude a subject.+data Status = Include | Exclude deriving (Eq, Show)++-- | Defines subject's diposition in a cohort either included or which criterion+-- they were excluded by. See @'checkCohortStatus'@ for evaluating a @'Criteria'@+-- to determine CohortStatus.+data CohortStatus =+ Included | ExcludedBy (Natural, Text)+ deriving (Eq, Show, Generic)++-- Defines an ordering to put @Included@ last in a container of @'CohortStatus'@.+-- The @'ExcludedBy'@ are ordered by their number value.+instance Ord CohortStatus where+ compare Included Included = EQ+ compare Included (ExcludedBy _) = GT+ compare (ExcludedBy _) Included = LT+ compare (ExcludedBy (i, _)) (ExcludedBy (j, _)) = compare i j++-- | Helper to convert a @Bool@ to a @'Status'@+-- +-- >>> includeIf True+-- >>> includeIf False+-- Include+-- Exclude+includeIf :: Bool -> Status+includeIf True = Include+includeIf False = Exclude++-- | Helper to convert a @Bool@ to a @'Status'@+-- +-- >>> excludeIf True+-- >>> excludeIf False+-- Exclude+-- Include+excludeIf :: Bool -> Status+excludeIf True = Exclude+excludeIf False = Include++-- | A type that is simply a @'FeatureN Status'@, that is, a feature that +-- identifies whether to @'Include'@ or @'Exclude'@ a subject.+newtype Criterion = MkCriterion ( FeatureN Status ) deriving (Eq, Show)++-- | Converts a @'Feature'@ to a @'Criterion'@.+criterion :: (KnownSymbol n) => Feature n Status -> Criterion+criterion x = MkCriterion (nameFeature x)++-- | A nonempty collection of @'Criterion'@ paired with a @Natural@ number.+newtype Criteria = MkCriteria {+ getCriteria :: NE.NonEmpty (Natural, Criterion)+ } deriving (Eq, Show)++-- | Constructs a @'Criteria'@ from a @'NE.NonEmpty'@ collection of @'Criterion'@.+criteria :: NE.NonEmpty Criterion -> Criteria+criteria l = MkCriteria $ NE.zip (NE.fromList [1..]) l++-- | Unpacks a @'Criterion'@ into a (Text, Status) pair where the text is the+-- name of the criterion and its @Status@ is the value of the status in the +-- @'Criterion'@. In the case, that the value of the @'FeatureData'@ within the +-- @'Criterion'@ is @Left@, the status is set to @'Exclude'@. +getStatus :: Criterion -> (Text, Status)+getStatus (MkCriterion x) =+ either (const (nm, Exclude)) (nm,) ((getFeatureData . getDataN) x)+ where nm = getNameN x++-- | Converts a subject's @'Criteria'@ into a @'NE.NonEmpty'@ triple of +-- (order of criterion, name of criterion, status)+getStatuses ::+ Criteria -> NE.NonEmpty (Natural, Text, Status)+getStatuses (MkCriteria x) =+ fmap (\c -> (fst c, (fst.getStatus.snd) c, (snd.getStatus.snd) c)) x++-- | An internal function used to @'Data.List.find'@ excluded statuses. Used in+-- 'checkCohortStatus'.+findExclude ::+ Criteria -> Maybe (Natural, Text, Status)+findExclude x = find (\(_, _, z) -> z == Exclude) (getStatuses x)++-- | Converts a subject's @'Criteria'@ to a @'CohortStatus'@. The status is set+-- to @'Included'@ if none of the @'Criterion'@ have a status of @'Exclude'@.+checkCohortStatus ::+ Criteria -> CohortStatus+checkCohortStatus x =+ maybe Included (\(i, n, _) -> ExcludedBy (i, n)) (findExclude x)++-- | Utility to get the name of a @'Criterion'@.+getCriterionName :: Criterion -> Text+getCriterionName (MkCriterion x) = getNameN x++-- | Initializes a container of @'CohortStatus'@ from a @'Criteria'@. This can be used+-- to collect generate all the possible Exclusion/Inclusion reasons. +initStatusInfo :: Criteria -> NE.NonEmpty CohortStatus+initStatusInfo (MkCriteria z) =+ NE.map (ExcludedBy . Data.Bifunctor.second getCriterionName) z <> pure Included
+ src/Cohort/Index.hs view
@@ -0,0 +1,39 @@+{-|+Module : Cohort Index +Description : Defines the Index and related types and functions+Copyright : (c) NoviSci, Inc 2020+License : BSD3+Maintainer : bsaul@novisci.com+-}++{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE DeriveGeneric #-}+-- {-# LANGUAGE Safe #-}++module Cohort.Index(+ Index+ , makeIndex+ , getIndex+) where++import GHC.Show ( Show )+import GHC.Generics+import Data.Eq ( Eq )+import IntervalAlgebra ( Intervallic )+import Data.Aeson++{-|+An @Index@ is a wrapper for an @Intervallic@ used to indicate that a particular+interval is considered an index interval to which other intervals will be compared.+-}++newtype Index i a = MkIndex { + getIndex :: i a -- ^ Unwrap an @Index@+ } deriving (Eq, Show, Generic)++-- | Creates a new @'Index'@.+makeIndex :: Intervallic i a => i a -> Index i a+makeIndex = MkIndex++instance (Intervallic i a, ToJSON (i a)) => ToJSON (Index i a)
+ src/Cohort/Input.hs view
@@ -0,0 +1,97 @@+{-|+Module : Functions for Parsing Hasklepias populations +Description : Defines FromJSON instances for Hasklepias populations .+Copyright : (c) NoviSci, Inc 2020+License : BSD3+Maintainer : bsaul@novisci.com+-}+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE TupleSections #-}++module Cohort.Input(+ parsePopulationLines+ , parsePopulationIntLines+ , parsePopulationDayLines+ , ParseError(..)+) where++import Control.Applicative ( Applicative((<*>)), (<$>) )+import Data.Aeson ( FromJSON(..)+ , ToJSON(..)+ , eitherDecode+ , Value(Array))+import qualified Data.ByteString.Lazy as B ( fromStrict+ , toStrict+ , ByteString)+import qualified Data.ByteString.Char8 as C ( lines )+import Prelude (+ String)+import Data.Bifunctor ( Bifunctor(first) )+import Data.Either ( Either(..)+ , partitionEithers )+import Data.Eq ( Eq )+import Data.Function ( ($), id ) +import Data.Functor ( Functor(fmap) )+import Data.List ( sort, (++), zipWith )+import qualified Data.Map.Strict as M ( toList, fromListWith)+import Data.Ord ( Ord )+import Data.Text (Text, pack)+import Data.Time.Calendar ( Day )+import Data.Vector ( (!) )+import EventData ( Events, event, Event )+import EventData.Aeson ()+import Cohort.Core ( Population(..)+ , ID+ , Subject(MkSubject) )+import GHC.Int ( Int )+import GHC.Num ( Natural )+import GHC.Show ( Show )+import IntervalAlgebra ( IntervalSizeable )+++newtype SubjectEvent a = MkSubjectEvent (ID, Event a)++subjectEvent :: ID -> Event a -> SubjectEvent a+subjectEvent x y = MkSubjectEvent (x, y)++instance (FromJSON a, Show a, IntervalSizeable a b) => FromJSON (SubjectEvent a) where+ parseJSON (Array v) = subjectEvent <$>+ parseJSON (v ! 0) <*> (event <$> parseJSON (v ! 5) <*> parseJSON (Array v))++mapIntoPop :: (Ord a) => [SubjectEvent a] -> Population (Events a)+mapIntoPop l = MkPopulation $+ fmap (\(id, es) -> MkSubject (id, sort es)) -- TODO: is there a way to avoid the sort?+ (M.toList $ M.fromListWith (++) + (fmap (\(MkSubjectEvent (id, e)) -> (id, [e])) l ))++decodeIntoSubj :: (FromJSON a, Show a, IntervalSizeable a b) => + B.ByteString -> Either Text (SubjectEvent a)+decodeIntoSubj x = first pack $ eitherDecode x ++-- | Contains the line number and error message.+newtype ParseError = MkParseError (Natural, Text) deriving (Eq, Show)++-- | Parse @Event Int@ from json lines.+parseSubjectLines ::+ (FromJSON a, Show a, IntervalSizeable a b) =>+ B.ByteString -> ( [ParseError], [SubjectEvent a] )+parseSubjectLines l =+ partitionEithers $ zipWith+ (\x i -> first (\t -> MkParseError (i,t)) (decodeIntoSubj $ B.fromStrict x) )+ (C.lines $ B.toStrict l)+ [1..]++-- | Parse @Event Int@ from json lines.+parsePopulationLines :: (FromJSON a, Show a, IntervalSizeable a b) => + B.ByteString -> ([ParseError], Population (Events a))+parsePopulationLines x = fmap mapIntoPop (parseSubjectLines x)++-- | Parse @Event Int@ from json lines.+parsePopulationIntLines :: B.ByteString -> ([ParseError], Population (Events Int))+parsePopulationIntLines x = fmap mapIntoPop (parseSubjectLines x)++-- | Parse @Event Day@ from json lines.+parsePopulationDayLines :: B.ByteString -> ([ParseError], Population (Events Day))+parsePopulationDayLines x = fmap mapIntoPop (parseSubjectLines x)
+ src/Cohort/Output.hs view
@@ -0,0 +1,120 @@+{-|+Module : Hasklepias Cohorts+Description : Defines the options for outputting a cohort+Copyright : (c) NoviSci, Inc 2020+License : BSD3+Maintainer : bsaul@novisci.com+-}+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE DeriveAnyClass #-}++module Cohort.Output(+ CohortShape+ , ShapeCohort(..)+ , toJSONCohortShape+) where++import Data.Aeson ( ToJSON(..)+ , Value+ , object+ , (.=) )+import Data.Function ( (.) )+import Data.Functor ( Functor(fmap) )+import Data.List.NonEmpty as NE ( NonEmpty(..)+ , head+ , fromList+ , zip )+import Data.Tuple ( uncurry )+import GHC.Generics ( Generic )+import GHC.Types ( Type )+import GHC.Show ( Show )+import Cohort.Core ( AttritionInfo,+ Cohort,+ ObsUnit,+ ID,+ CohortData,+ getCohortData,+ getCohortIDs )+import Cohort.Criteria ( CohortStatus )+import Features.Featureset ( FeaturesetList(MkFeaturesetList)+ , Featureset+ , getFeatureset+ , getFeaturesetList+ , tpose )+import Features.Output ( ShapeOutput(dataOnly, nameAttr)+ , OutputShape )++instance (ToJSON d) => ToJSON (ObsUnit d) where+instance (ToJSON d) => ToJSON (CohortData d) where+instance (ToJSON d) => ToJSON (Cohort d) where+instance ToJSON CohortStatus where+instance ToJSON AttritionInfo where++-- | A type used to determine the output shape of a Cohort.+data CohortShape d where+ ColumnWise :: (Show a, ToJSON a) => a -> CohortShape ColumnWise+ RowWise :: (Show a, ToJSON a) => a -> CohortShape RowWise++deriving instance Show d => Show (CohortShape d)++-- | Maps CohortShape into an Aeson Value. +-- TODO: implement Generic and ToJSON instance of CohortShape directly.+toJSONCohortShape :: CohortShape shape -> Value+toJSONCohortShape (ColumnWise x) = toJSON x+toJSONCohortShape (RowWise x) = toJSON x++class ShapeCohort d where+ colWise :: Cohort d -> CohortShape ColumnWise+ rowWise :: Cohort d -> CohortShape RowWise++instance ShapeCohort Featureset where+ colWise x = ColumnWise (shapeColumnWise x)+ rowWise x = RowWise (shapeRowWise x)++data ColumnWise = MkColumnWise {+ colAttributes :: NonEmpty (OutputShape Type)+ , ids :: [ID]+ , colData :: NonEmpty (NonEmpty (OutputShape Type))+ } deriving ( Show, Generic )++instance ToJSON ColumnWise where+ toJSON x = object [ "attributes" .= colAttributes x+ , "ids" .= ids x+ , "data" .= colData x ]++newtype IDRow = MkIDRow (ID, NonEmpty (OutputShape Type))+ deriving ( Show, Generic )++instance ToJSON IDRow where+ toJSON (MkIDRow x) = object [ uncurry (.=) x]++data RowWise = MkRowWise {+ attributes :: NonEmpty (OutputShape Type)+ , rowData :: NonEmpty IDRow+ } deriving ( Show, Generic )++instance ToJSON RowWise where+ toJSON x = object [ "attributes" .= attributes x+ , "data" .= rowData x ]++shapeColumnWise :: Cohort Featureset -> ColumnWise+shapeColumnWise x = MkColumnWise+ (fmap (nameAttr . NE.head . getFeatureset) z)+ (getCohortIDs x)+ (fmap (fmap dataOnly . getFeatureset) z)+ -- TODO: don't use fromList; do something more principled+ where z = getFeaturesetList (tpose (MkFeaturesetList (NE.fromList (getCohortData x))))++shapeRowWise :: Cohort Featureset -> RowWise+shapeRowWise x = MkRowWise+ (fmap (nameAttr . NE.head . getFeatureset) z)+ (fmap MkIDRow (zip ids (fmap (fmap dataOnly . getFeatureset) z)))+ -- TODO: don't use fromList; do something more principled+ where z = NE.fromList (getCohortData x)+ ids = fromList (getCohortIDs x)
− src/FeatureEvents.hs
@@ -1,249 +0,0 @@-{-|-Module : Functions for composing features from events -Description : Functions for composing features. -Copyright : (c) NoviSci, Inc 2020-License : BSD3-Maintainer : bsaul@novisci.com--Provides functions used in defining @'Features.Feature'@ from -@'EventData.Event'@s.--}-{-# OPTIONS_HADDOCK hide #-}-{-# LANGUAGE NoImplicitPrelude #-}--module FeatureEvents(- -- ** Container predicates- isNotEmpty- , atleastNofX- , anyGapsWithinAtLeastDuration- , allGapsWithinLessThanDuration-- -- ** Finding occurrences of concepts- , nthConceptOccurrence- , firstConceptOccurrence-- -- ** Reshaping containers- , allPairs- , splitByConcepts-- -- ** Create filters- , makeConceptsFilter- , makePairedFilter-- -- ** Functions for working with Event Domains- , viewBirthYears- , previewBirthYear-- -- ** Function for manipulating intervals- , lookback- , lookahead-- -- ** Misc functions- , computeAgeAt-) where---import IntervalAlgebra ( Intervallic- , IntervalSizeable(..)- , ComparativePredicateOf1- , ComparativePredicateOf2- , Interval- , IntervalCombinable- , begin- , end- , beginerval- , enderval )-import IntervalAlgebra.PairedInterval ( PairedInterval, getPairData )-import IntervalAlgebra.IntervalUtilities ( durations, gapsWithin )-import EventData ( Events- , Event- , ConceptEvent- , ctxt )-import EventData.Context ( Concept- , Concepts- , Context- , HasConcept( hasConcepts )- , facts )-import EventData.Context.Domain ( Domain- , demo- , info- , _Demographics )-import Safe ( headMay, lastMay )-import Control.Applicative ( Applicative(liftA2) )-import Control.Monad ( Functor(fmap), (=<<) )-import Control.Lens ( preview, (^.) )-import Data.Bool ( Bool(..), (&&), not, (||) )-import Data.Either ( either )-import Data.Eq ( Eq )-import Data.Foldable ( Foldable(length, null), all, any )-import Data.Function ( (.), ($), const )-import Data.Functor ( Functor(fmap) )-import Data.Int ( Int )-import Data.Maybe ( Maybe(..), maybe, mapMaybe )-import Data.Monoid ( Monoid )-import Data.Ord ( Ord(..) )-import Data.Time.Calendar ( Day, Year, diffDays )-import Data.Text ( Text )-import Data.Text.Read ( rational )-import Data.Tuple ( fst )-import Witherable ( filter, Filterable, Witherable )-import GHC.Num ( Integer, fromInteger )-import GHC.Real ( RealFrac(floor), (/) )---- | Is the input list empty? -isNotEmpty :: [a] -> Bool-isNotEmpty = not.null---- | Filter 'Events' to those that have any of the provided concepts.-makeConceptsFilter ::- [Text] -- ^ the list of concepts by which to filter - -> Events a- -> Events a-makeConceptsFilter cpts = filter (`hasConcepts` cpts)---- | Filter 'Events' to a single @'Maybe' 'Event'@, based on a provided function,--- with the provided concepts. For example, see 'firstConceptOccurrence' and--- 'lastConceptOccurrence'.-nthConceptOccurrence ::- (Events a -> Maybe (Event a)) -- ^ function used to select a single event- -> [Text]- -> Events a- -> Maybe (Event a)-nthConceptOccurrence f c = f.makeConceptsFilter c---- | Finds the *first* occurrence of an 'Event' with at least one of the concepts.--- Assumes the input 'Events' list is appropriately sorted.-firstConceptOccurrence ::- [Text]- -> Events a- -> Maybe (Event a)-firstConceptOccurrence = nthConceptOccurrence headMay---- | Finds the *last* occurrence of an 'Event' with at least one of the concepts.--- Assumes the input 'Events' list is appropriately sorted.-lastConceptOccurrence ::- [Text]- -> Events a- -> Maybe (Event a)-lastConceptOccurrence = nthConceptOccurrence lastMay---- | Does 'Events' have at least @n@ events with any of the Concept in @x@.-atleastNofX ::- Int -- ^ n- -> [Text] -- ^ x- -> Events a -> Bool-atleastNofX n x es = length (makeConceptsFilter x es) >= n---- | Takes a predicate of intervals and a predicate on the data part of a --- paired interval to create a single predicate such that both input--- predicates should hold.-makePairPredicate :: Ord a =>- ComparativePredicateOf2 (i0 a) ((PairedInterval b) a)- -> i0 a- -> (b -> Bool)- -> (PairedInterval b a -> Bool)-makePairPredicate pi i pd x = pi i x && pd (getPairData x)---- | -makePairedFilter :: Ord a =>- ComparativePredicateOf2 (i0 a) ((PairedInterval b) a)- -> i0 a- -> (b -> Bool)- -> [PairedInterval b a]- -> [PairedInterval b a]-makePairedFilter fi i fc = filter (makePairPredicate fi i fc)---- | Generate all pair-wise combinations from two lists.-allPairs :: [a] -> [b] -> [(a, b)]-allPairs = liftA2 (,)---- | Split an @Events a@ into a pair of @Events a@. The first element contains--- events have any of the concepts in the first argument, similarly for the--- second element.-splitByConcepts :: [Text]- -> [Text]- -> Events a- -> (Events a, Events a)-splitByConcepts c1 c2 es = ( filter (`hasConcepts` c1) es- , filter (`hasConcepts` c2) es)----- | Create a predicate function that checks whether within a provided spanning--- interval, are there (e.g. any, all) gaps of (e.g. <, <=, >=, >) a specified--- duration among the input intervals?-makeGapsWithinPredicate ::- ( Monoid (t (Interval a))- , Monoid (t (Maybe (Interval a)))- , Applicative t- , Witherable t- , IntervalSizeable a b- , IntervalCombinable i0 a- , IntervalCombinable i1 a) =>- ((b -> Bool) -> t b -> Bool)- -> (b -> b -> Bool)- -> (b -> i0 a -> t (i1 a) -> Bool)-makeGapsWithinPredicate f op gapDuration interval l =- maybe False (f (`op` gapDuration) . durations) (gapsWithin interval l)---- | Within a provided spanning interval, are there any gaps of at least the--- specified duration among the input intervals?-anyGapsWithinAtLeastDuration ::- (IntervalSizeable a b, IntervalCombinable i0 a, IntervalCombinable i1 a) =>- b -- ^ duration of gap- -> i0 a -- ^ within this interval- -> [i1 a]- -> Bool-anyGapsWithinAtLeastDuration = makeGapsWithinPredicate any (>=)---- | Within a provided spanning interval, are all gaps less than the specified--- duration among the input intervals?------ >>> allGapsWithinLessThanDuration 30 (beginerval 100 (0::Int)) [beginerval 5 (-1), beginerval 99 10]--- True-allGapsWithinLessThanDuration ::- (IntervalSizeable a b, IntervalCombinable i0 a, IntervalCombinable i1 a) =>- b -- ^ duration of gap- -> i0 a -- ^ within this interval- -> [i1 a]- -> Bool-allGapsWithinLessThanDuration = makeGapsWithinPredicate all (<)---- | Utility for reading text into a maybe integer-intMayMap :: Text -> Maybe Integer -- TODO: this is ridiculous-intMayMap x = fmap floor (either (const Nothing) (Just . fst) (Data.Text.Read.rational x))---- | Preview birth year from a domain-previewBirthYear :: Domain -> Maybe Year-previewBirthYear dmn = intMayMap =<< ((^.demo.info) =<< preview _Demographics dmn)---- | Returns a (possibly emtpy) list of birth years from a set of events-viewBirthYears :: Events a -> [Year]-viewBirthYears = mapMaybe (\e -> previewBirthYear =<< Just (ctxt e^.facts ))---- | Compute the "age" in years between two calendar days. The difference between--- the days is rounded down.-computeAgeAt :: Day -> Day -> Integer-computeAgeAt bd at = floor (fromInteger (diffDays at bd) / 365.25)---- | Creates a new @Interval@ of a provided lookback duration ending at the --- 'begin' of the input interval.------ >>> lookback 4 (beginerval 10 (1 :: Int))--- (-3, 1)-lookback :: (Intervallic i a, IntervalSizeable a b) =>- b -- ^ lookback duration- -> i a- -> Interval a-lookback d x = enderval d (begin x)---- | Creates a new @Interval@ of a provided lookahead duration beginning at the --- 'end' of the input interval.------ >>> lookahead 4 (beginerval 1 (1 :: Int))--- (2, 6)-lookahead :: (Intervallic i a, IntervalSizeable a b) =>- b -- ^ lookahead duration- -> i a- -> Interval a-lookahead d x = beginerval d (end x)-
src/Features/Compose.hs view
@@ -55,7 +55,7 @@ import safe Control.Applicative ( Applicative(..) , liftA3, (<$>) ) import safe Control.Monad ( Functor(..), Monad(..)- , (=<<), join, liftM, liftM2, liftM3)+ , (=<<), join, liftM, liftM2, liftM3, liftM4) import safe Data.Either ( Either(..) ) import safe Data.Eq ( Eq(..) ) import safe Data.Foldable ( Foldable(foldr), fold )@@ -258,6 +258,8 @@ D2A :: (c -> b -> f0 a) -> Definition (f2 c -> f1 b -> f0 a) D3 :: (d -> c -> b -> a) -> Definition (f3 d -> f2 c -> f1 b -> f0 a) D3A :: (d -> c -> b -> f0 a) -> Definition (f3 d -> f2 c -> f1 b -> f0 a)+ D4 :: (e -> d -> c -> b -> a) -> Definition (f4 e -> f3 d -> f2 c -> f1 b -> f0 a) + D4A :: (e -> d -> c -> b -> f0 a) -> Definition (f4 e -> f3 d -> f2 c -> f1 b -> f0 a) {- | Define (and @'DefineA@) provide a means to create new @'Definition'@s via @'define'@ (@'defineA'@). The @'define'@ function takes a single function input @@ -299,19 +301,24 @@ instance Define (b -> a) (FeatureData b -> FeatureData a) where define = D1 instance Define (c -> b -> a) (FeatureData c -> FeatureData b -> FeatureData a) where define = D2 instance Define (d -> c -> b -> a) (FeatureData d -> FeatureData c -> FeatureData b -> FeatureData a) where define = D3+instance Define (e -> d -> c -> b -> a) (FeatureData e -> FeatureData d -> FeatureData c -> FeatureData b -> FeatureData a) where define = D4 instance DefineA (b -> FeatureData a) (FeatureData b -> FeatureData a) where defineA = D1A instance DefineA (c -> b -> FeatureData a) (FeatureData c -> FeatureData b -> FeatureData a) where defineA = D2A instance DefineA (d -> c -> b -> FeatureData a) (FeatureData d -> FeatureData c -> FeatureData b -> FeatureData a) where defineA = D3A+instance DefineA (e -> d -> c -> b -> FeatureData a) (FeatureData e -> FeatureData d -> FeatureData c -> FeatureData b -> FeatureData a) where defineA = D4A instance Define (b -> a) (Feature n1 b -> Feature n0 a) where define = D1 instance Define (c -> b -> a) (Feature n2 c -> Feature n1 b -> Feature n0 a) where define = D2 instance Define (d -> c -> b -> a) (Feature n3 d -> Feature n2 c -> Feature n1 b -> Feature n0 a) where define = D3+instance Define (e -> d -> c -> b -> a) (Feature n4 e -> Feature n3 d -> Feature n2 c -> Feature n1 b -> Feature n0 a) where define = D4 instance DefineA (b -> Feature n0 a) (Feature n1 b -> Feature n0 a) where defineA = D1A instance DefineA (c -> b -> Feature n0 a) (Feature n2 c -> Feature n1 b -> Feature n0 a) where defineA = D2A instance DefineA (d -> c -> b -> Feature n0 a) (Feature n3 d -> Feature n2 c -> Feature n1 b -> Feature n0 a) where defineA = D3A+instance DefineA (e -> d -> c -> b -> Feature n0 a) (Feature n4 e -> Feature n3 d -> Feature n2 c -> Feature n1 b -> Feature n0 a) where defineA = D4A + {- | Evaluate a @Definition@. Note that (currently), the second argument of 'eval' is a *tuple* of inputs. For example, @@ -379,6 +386,15 @@ eval (D3 f) (MkFeature x, MkFeature y, MkFeature z) = MkFeature $ liftA3 f x y z eval (D3A f) (MkFeature x, MkFeature y, MkFeature z) = case liftA3 f x y z of+ MkFeatureData (Left l) -> MkFeature $ MkFeatureData (Left l)+ MkFeatureData (Right r) -> r++instance Eval (Feature n4 e -> Feature n3 d -> Feature n2 c -> Feature n1 b -> Feature n0 a)+ (Feature n4 e, Feature n3 d, Feature n2 c, Feature n1 b) (Feature n0 a)+ where+ eval (D4 f) (MkFeature v, MkFeature x, MkFeature y, MkFeature z) = MkFeature $ liftM4 f v x y z+ eval (D4A f) (MkFeature v, MkFeature x, MkFeature y, MkFeature z) =+ case liftM4 f v x y z of MkFeatureData (Left l) -> MkFeature $ MkFeatureData (Left l) MkFeatureData (Right r) -> r
src/Hasklepias.hs view
@@ -23,10 +23,11 @@ can be found on [hackage](https://hackage.haskell.org/package/interval-algebra). -}- , module FeatureEvents+ , module Hasklepias.FeatureEvents+ , module Hasklepias.Misc -- * Specifying and building cohorts- , module Hasklepias.Cohort+ , module Cohort -- ** Creating an executable cohort application , module Hasklepias.MakeApp@@ -47,10 +48,12 @@ import Features -import FeatureEvents+import Cohort++import Hasklepias.FeatureEvents+import Hasklepias.Misc import Hasklepias.Reexports import Hasklepias.ReexportsUnsafe-import Hasklepias.Cohort import Hasklepias.MakeApp import Stype
− src/Hasklepias/Cohort.hs
@@ -1,35 +0,0 @@-{-|-Module : Hasklepias Cohorts-Description : Defines the Cohort type and associated methods-Copyright : (c) NoviSci, Inc 2020-License : BSD3-Maintainer : bsaul@novisci.com--}-{-# OPTIONS_HADDOCK hide #-}-{-# LANGUAGE NoImplicitPrelude #-}--module Hasklepias.Cohort(-- -- * Defining Cohorts- module Hasklepias.Cohort.Core-- -- ** Criteria- , module Hasklepias.Cohort.Criteria- -- ** Index- , module Hasklepias.Cohort.Index-- -- * Cohort I/O- -- ** Input- , module Hasklepias.Cohort.Input - -- ** Output- , module Hasklepias.Cohort.Output---) where---import Hasklepias.Cohort.Core-import Hasklepias.Cohort.Index-import Hasklepias.Cohort.Criteria-import Hasklepias.Cohort.Input-import Hasklepias.Cohort.Output
− src/Hasklepias/Cohort/Core.hs
@@ -1,167 +0,0 @@-{-|-Module : Hasklepias Cohorts-Description : Defines the Cohort type and associated methods-Copyright : (c) NoviSci, Inc 2020-License : BSD3-Maintainer : bsaul@novisci.com--}-{-# OPTIONS_HADDOCK hide #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE LambdaCase #-}--- {-# LANGUAGE Safe #-}--module Hasklepias.Cohort.Core(- Subject(..)- , ID- , Population(..)- , ObsUnit(..)- , CohortData(..)- , Cohort(..)- , CohortSpec- , AttritionInfo(..)- , specifyCohort- , makeObsUnitFeatures- , evalCohort- , getCohortIDs- , getCohortData- , getAttritionInfo-) where--import GHC.Num ( Num((+)), Natural )-import Data.Aeson ( FromJSON, ToJSON, ToJSONKey )-import Data.Bool ( Bool )-import Data.Eq ( Eq )-import Data.Foldable ( Foldable(length) )-import Data.Function ( ($) )-import Data.Functor ( Functor(fmap) )-import Data.Maybe ( Maybe(..), catMaybes )-import Data.List ( zipWith, replicate )-import Data.List.NonEmpty ( NonEmpty(..), zip, fromList, nonEmpty )-import Data.Map.Strict as Map ( toList, fromListWith )-import Data.Text ( Text )-import GHC.Generics ( Generic )-import GHC.Show ( Show(..) )-import Hasklepias.Cohort.Index ( makeIndex, Index(..) )-import Hasklepias.Cohort.Criteria---- | A subject identifier. Currently, simply @Text@.-type ID = Text---- | A subject is just a pair of @ID@ and data.-newtype Subject d = MkSubject (ID, d)- deriving (Eq, Show, Generic)--instance Functor Subject where- fmap f (MkSubject (id, x)) = MkSubject (id, f x)--instance (FromJSON d) => FromJSON (Subject d) where---- | A population is a list of @'Subject'@s-newtype Population d = MkPopulation [Subject d] - deriving (Eq, Show, Generic)--instance Functor Population where- fmap f (MkPopulation x) = MkPopulation (fmap (fmap f) x)--instance (FromJSON d) => FromJSON (Population d) where---- | An observational unit is what a subject may be transformed into.-data ObsUnit d = MkObsUnit {- obsID :: ID- , obsData :: d }- deriving (Eq, Show, Generic)---- | A container for CohortData-newtype CohortData d = MkCohortData { getObsData :: [ObsUnit d] }- deriving (Eq, Show, Generic)---- | A cohort is a list of observational units along with @'AttritionInfo'@ --- regarding the number of subjects excluded by the @'Criteria'@. -newtype Cohort d = MkCohort (Maybe AttritionInfo, CohortData d)- deriving (Eq, Show, Generic)---- | Gets the attrition info from a cohort-getAttritionInfo :: Cohort d -> Maybe AttritionInfo-getAttritionInfo (MkCohort (x, _)) = x---- | Unpacks a @'Population'@ to a list of subjects.-getPopulation :: Population d -> [Subject d]-getPopulation (MkPopulation x) = x---- | Gets the data out of a @'Subject'@.-getSubjectData :: Subject d -> d-getSubjectData (MkSubject (_, x)) = x---- | Tranforms a @'Subject'@ into a @'ObsUnit'@.-makeObsUnitFeatures :: (d1 -> d0) -> Subject d1 -> ObsUnit d0-makeObsUnitFeatures f (MkSubject (id, dat)) = MkObsUnit id (f dat)---- | A cohort specification consist of two functions: one that transforms a subject's--- input data into a @'Criteria'@ and another that transforms a subject's input data--- into the desired return type.-data CohortSpec d1 d0 = MkCohortSpec- { runCriteria:: d1 -> Criteria- -- (Feature (Index i a))- , runFeatures:: d1 -> d0 }---- | Creates a @'CohortSpec'@.-specifyCohort :: (d1 -> Criteria) -> (d1 -> d0) -> CohortSpec d1 d0-specifyCohort = MkCohortSpec---- | Evaluates the @'runCriteria'@ of a @'CohortSpec'@ on a @'Population'@ to --- return a list of @Subject Criteria@ (one per subject in the population). -evalCriteria :: CohortSpec d1 d0 -> Population d1 -> [Subject Criteria]-evalCriteria (MkCohortSpec runCrit _) (MkPopulation pop) = fmap (fmap runCrit) pop---- | Convert a list of @Subject Criteria@ into a list of @Subject CohortStatus@-evalCohortStatus :: [Subject Criteria] -> [Subject CohortStatus]-evalCohortStatus = fmap (fmap checkCohortStatus)---- | Runs the input function which transforms a subject into an observational unit. --- If the subeject is excluded, the result is @Nothing@; otherwise it is @Just@ --- an observational unit.-evalSubjectCohort :: (d1 -> d0) -> Subject CohortStatus -> Subject d1 -> Maybe (ObsUnit d0)-evalSubjectCohort f (MkSubject (id, status)) subjData =- case status of- Included -> Just $ makeObsUnitFeatures f subjData- ExcludedBy _ -> Nothing---- | A type which collects the counts of subjects included or excluded.-newtype AttritionInfo = MkAttritionInfo (NonEmpty (CohortStatus, Natural))- deriving (Eq, Show, Generic)---- | Initializes @AttritionInfo@ from a @'Criteria'@.-initAttritionInfo :: Criteria -> AttritionInfo-initAttritionInfo x =- MkAttritionInfo $ zip (initStatusInfo x) - (0 :| replicate (length (getCriteria x)) 0)---- | Creates an @'AttritionInfo'@ from a list of @Subject CohortStatus@. The result--- is @Nothing@ if the input list is empty.-measureAttrition :: [Subject CohortStatus] -> Maybe AttritionInfo-measureAttrition l = fmap MkAttritionInfo $ nonEmpty $ Map.toList $- Map.fromListWith (+) $ fmap (\x -> (getSubjectData x, 1)) l---- | The internal function to evaluate a @'CohortSpec'@ on a @'Population'@. -evalUnits :: CohortSpec d1 d0 -> Population d1 -> (Maybe AttritionInfo, CohortData d0)-evalUnits spec pop =- ( measureAttrition statuses- , MkCohortData $ catMaybes $ zipWith (evalSubjectCohort (runFeatures spec))- statuses- (getPopulation pop))- where crits = evalCriteria spec pop- statuses = evalCohortStatus crits---- | Evaluates a @'CohortSpec'@ on a @'Population'@.-evalCohort :: CohortSpec d1 d0 -> Population d1 -> Cohort d0-evalCohort s p = MkCohort $ evalUnits s p---- | Get IDs from a cohort.-getCohortIDs :: Cohort d -> [ID]-getCohortIDs (MkCohort (_, dat)) = fmap obsID ( getObsData dat )---- | Get data from a cohort.-getCohortData :: Cohort d -> [d]-getCohortData (MkCohort (_, dat)) = fmap obsData ( getObsData dat )
− src/Hasklepias/Cohort/Criteria.hs
@@ -1,144 +0,0 @@-{-|-Module : Cohort Criteria-Description : Defines the Criteria and related types and functions-Copyright : (c) NoviSci, Inc 2020-License : BSD3-Maintainer : bsaul@novisci.com--}-{-# OPTIONS_HADDOCK hide #-}-{-# LANGUAGE Safe #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE TupleSections #-}--module Hasklepias.Cohort.Criteria(- Criterion- , Criteria(..)- , Status(..)- , CohortStatus(..)- , criterion- , criteria- , excludeIf- , includeIf- , initStatusInfo- , checkCohortStatus-) where--import safe GHC.Generics ( Generic )-import safe GHC.Num ( Num((+)), Natural )-import safe GHC.Show ( Show(show) )-import safe GHC.TypeLits ( KnownSymbol, symbolVal )-import safe Control.Applicative ( Applicative(pure) )-import safe Control.Monad ( Functor(..) )-import safe Data.Bifunctor ( Bifunctor(second) )-import safe Data.Bool ( Bool(..), otherwise, not, (&&) )-import safe Data.Either ( either )-import safe Data.Eq ( Eq(..) )-import safe Data.Function ( ($), (.), const, id )-import safe qualified Data.List.NonEmpty as NE- ( NonEmpty, zip, fromList, toList, map )-import safe Data.List ( find, (++) )-import safe Data.Maybe ( Maybe(..), maybe )-import safe Data.Ord ( Ord(..), Ordering(..) )-import safe Data.Semigroup ( Semigroup((<>)) )-import safe Data.Tuple ( fst, snd )-import safe Data.Text ( Text, pack )-import safe Features.Compose ( getFeatureData- , Feature- , nameFeature- , FeatureN(..) )---- | Defines the return type for @'Criterion'@ indicating whether to include or --- exclude a subject.-data Status = Include | Exclude deriving (Eq, Show)---- | Defines subject's diposition in a cohort either included or which criterion--- they were excluded by. See @'checkCohortStatus'@ for evaluating a @'Criteria'@--- to determine CohortStatus.-data CohortStatus =- Included | ExcludedBy (Natural, Text)- deriving (Eq, Show, Generic)---- Defines an ordering to put @Included@ last in a container of @'CohortStatus'@.--- The @'ExcludedBy'@ are ordered by their number value.-instance Ord CohortStatus where- compare Included Included = EQ- compare Included (ExcludedBy _) = GT- compare (ExcludedBy _) Included = LT- compare (ExcludedBy (i, _)) (ExcludedBy (j, _)) = compare i j---- | Helper to convert a @Bool@ to a @'Status'@--- --- >>> includeIf True--- >>> includeIf False--- Include--- Exclude-includeIf :: Bool -> Status-includeIf True = Include-includeIf False = Exclude---- | Helper to convert a @Bool@ to a @'Status'@--- --- >>> excludeIf True--- >>> excludeIf False--- Exclude--- Include-excludeIf :: Bool -> Status-excludeIf True = Exclude-excludeIf False = Include---- | A type that is simply a @'FeatureN Status'@, that is, a feature that --- identifies whether to @'Include'@ or @'Exclude'@ a subject.-newtype Criterion = MkCriterion ( FeatureN Status ) deriving (Eq, Show)---- | Converts a @'Feature'@ to a @'Criterion'@.-criterion :: (KnownSymbol n) => Feature n Status -> Criterion-criterion x = MkCriterion (nameFeature x)---- | A nonempty collection of @'Criterion'@ paired with a @Natural@ number.-newtype Criteria = MkCriteria {- getCriteria :: NE.NonEmpty (Natural, Criterion)- } deriving (Eq, Show)---- | Constructs a @'Criteria'@ from a @'NE.NonEmpty'@ collection of @'Criterion'@.-criteria :: NE.NonEmpty Criterion -> Criteria-criteria l = MkCriteria $ NE.zip (NE.fromList [1..]) l---- | Unpacks a @'Criterion'@ into a (Text, Status) pair where the text is the--- name of the criterion and its @Status@ is the value of the status in the --- @'Criterion'@. In the case, that the value of the @'FeatureData'@ within the --- @'Criterion'@ is @Left@, the status is set to @'Exclude'@. -getStatus :: Criterion -> (Text, Status)-getStatus (MkCriterion x) =- either (const (nm, Exclude)) (nm,) ((getFeatureData . getDataN) x)- where nm = getNameN x---- | Converts a subject's @'Criteria'@ into a @'NE.NonEmpty'@ triple of --- (order of criterion, name of criterion, status)-getStatuses ::- Criteria -> NE.NonEmpty (Natural, Text, Status)-getStatuses (MkCriteria x) =- fmap (\c -> (fst c, (fst.getStatus.snd) c, (snd.getStatus.snd) c)) x---- | An internal function used to @'Data.List.find'@ excluded statuses. Used in--- 'checkCohortStatus'.-findExclude ::- Criteria -> Maybe (Natural, Text, Status)-findExclude x = find (\(_, _, z) -> z == Exclude) (getStatuses x)---- | Converts a subject's @'Criteria'@ to a @'CohortStatus'@. The status is set--- to @'Included'@ if none of the @'Criterion'@ have a status of @'Exclude'@.-checkCohortStatus ::- Criteria -> CohortStatus-checkCohortStatus x =- maybe Included (\(i, n, _) -> ExcludedBy (i, n)) (findExclude x)---- | Utility to get the name of a @'Criterion'@.-getCriterionName :: Criterion -> Text-getCriterionName (MkCriterion x) = getNameN x---- | Initializes a container of @'CohortStatus'@ from a @'Criteria'@. This can be used--- to collect generate all the possible Exclusion/Inclusion reasons. -initStatusInfo :: Criteria -> NE.NonEmpty CohortStatus-initStatusInfo (MkCriteria z) =- NE.map (ExcludedBy . Data.Bifunctor.second getCriterionName) z <> pure Included
− src/Hasklepias/Cohort/Index.hs
@@ -1,39 +0,0 @@-{-|-Module : Cohort Index -Description : Defines the Index and related types and functions-Copyright : (c) NoviSci, Inc 2020-License : BSD3-Maintainer : bsaul@novisci.com--}--{-# OPTIONS_HADDOCK hide #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE DeriveGeneric #-}--- {-# LANGUAGE Safe #-}--module Hasklepias.Cohort.Index(- Index- , makeIndex- , getIndex-) where--import GHC.Show ( Show )-import GHC.Generics-import Data.Eq ( Eq )-import IntervalAlgebra ( Intervallic )-import Data.Aeson--{-|-An @Index@ is a wrapper for an @Intervallic@ used to indicate that a particular-interval is considered an index interval to which other intervals will be compared.--}--newtype Index i a = MkIndex { - getIndex :: i a -- ^ Unwrap an @Index@- } deriving (Eq, Show, Generic)---- | Creates a new @'Index'@.-makeIndex :: Intervallic i a => i a -> Index i a-makeIndex = MkIndex--instance (Intervallic i a, ToJSON (i a)) => ToJSON (Index i a)
− src/Hasklepias/Cohort/Input.hs
@@ -1,97 +0,0 @@-{-|-Module : Functions for Parsing Hasklepias populations -Description : Defines FromJSON instances for Hasklepias populations .-Copyright : (c) NoviSci, Inc 2020-License : BSD3-Maintainer : bsaul@novisci.com--}-{-# OPTIONS_HADDOCK hide #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE TupleSections #-}--module Hasklepias.Cohort.Input(- parsePopulationLines- , parsePopulationIntLines- , parsePopulationDayLines- , ParseError(..)-) where--import Control.Applicative ( Applicative((<*>)), (<$>) )-import Data.Aeson ( FromJSON(..)- , ToJSON(..)- , eitherDecode- , Value(Array))-import qualified Data.ByteString.Lazy as B ( fromStrict- , toStrict- , ByteString)-import qualified Data.ByteString.Char8 as C ( lines )-import Prelude (- String)-import Data.Bifunctor ( Bifunctor(first) )-import Data.Either ( Either(..)- , partitionEithers )-import Data.Eq ( Eq )-import Data.Function ( ($), id ) -import Data.Functor ( Functor(fmap) )-import Data.List ( sort, (++), zipWith )-import qualified Data.Map.Strict as M ( toList, fromListWith)-import Data.Ord ( Ord )-import Data.Text (Text, pack)-import Data.Time.Calendar ( Day )-import Data.Vector ( (!) )-import EventData ( Events, event, Event )-import EventData.Aeson ()-import Hasklepias.Cohort.Core ( Population(..)- , ID- , Subject(MkSubject) )-import GHC.Int ( Int )-import GHC.Num ( Natural )-import GHC.Show ( Show )-import IntervalAlgebra ( IntervalSizeable )---newtype SubjectEvent a = MkSubjectEvent (ID, Event a)--subjectEvent :: ID -> Event a -> SubjectEvent a-subjectEvent x y = MkSubjectEvent (x, y)--instance (FromJSON a, Show a, IntervalSizeable a b) => FromJSON (SubjectEvent a) where- parseJSON (Array v) = subjectEvent <$>- parseJSON (v ! 0) <*> (event <$> parseJSON (v ! 5) <*> parseJSON (Array v))--mapIntoPop :: (Ord a) => [SubjectEvent a] -> Population (Events a)-mapIntoPop l = MkPopulation $- fmap (\(id, es) -> MkSubject (id, sort es)) -- TODO: is there a way to avoid the sort?- (M.toList $ M.fromListWith (++) - (fmap (\(MkSubjectEvent (id, e)) -> (id, [e])) l ))--decodeIntoSubj :: (FromJSON a, Show a, IntervalSizeable a b) => - B.ByteString -> Either Text (SubjectEvent a)-decodeIntoSubj x = first pack $ eitherDecode x ---- | Contains the line number and error message.-newtype ParseError = MkParseError (Natural, Text) deriving (Eq, Show)---- | Parse @Event Int@ from json lines.-parseSubjectLines ::- (FromJSON a, Show a, IntervalSizeable a b) =>- B.ByteString -> ( [ParseError], [SubjectEvent a] )-parseSubjectLines l =- partitionEithers $ zipWith- (\x i -> first (\t -> MkParseError (i,t)) (decodeIntoSubj $ B.fromStrict x) )- (C.lines $ B.toStrict l)- [1..]---- | Parse @Event Int@ from json lines.-parsePopulationLines :: (FromJSON a, Show a, IntervalSizeable a b) => - B.ByteString -> ([ParseError], Population (Events a))-parsePopulationLines x = fmap mapIntoPop (parseSubjectLines x)---- | Parse @Event Int@ from json lines.-parsePopulationIntLines :: B.ByteString -> ([ParseError], Population (Events Int))-parsePopulationIntLines x = fmap mapIntoPop (parseSubjectLines x)---- | Parse @Event Day@ from json lines.-parsePopulationDayLines :: B.ByteString -> ([ParseError], Population (Events Day))-parsePopulationDayLines x = fmap mapIntoPop (parseSubjectLines x)
− src/Hasklepias/Cohort/Output.hs
@@ -1,120 +0,0 @@-{-|-Module : Hasklepias Cohorts-Description : Defines the options for outputting a cohort-Copyright : (c) NoviSci, Inc 2020-License : BSD3-Maintainer : bsaul@novisci.com--}-{-# OPTIONS_HADDOCK hide #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE DeriveAnyClass #-}--module Hasklepias.Cohort.Output(- CohortShape- , ShapeCohort(..)- , toJSONCohortShape-) where--import Data.Aeson ( ToJSON(..)- , Value- , object- , (.=) )-import Data.Function ( (.) )-import Data.Functor ( Functor(fmap) )-import Data.List.NonEmpty as NE ( NonEmpty(..)- , head- , fromList- , zip )-import Data.Tuple ( uncurry )-import GHC.Generics ( Generic )-import GHC.Types ( Type )-import GHC.Show ( Show )-import Hasklepias.Cohort.Core ( AttritionInfo,- Cohort,- ObsUnit,- ID,- CohortData,- getCohortData,- getCohortIDs )-import Hasklepias.Cohort.Criteria ( CohortStatus )-import Features.Featureset ( FeaturesetList(MkFeaturesetList)- , Featureset- , getFeatureset- , getFeaturesetList- , tpose )-import Features.Output ( ShapeOutput(dataOnly, nameAttr)- , OutputShape )--instance (ToJSON d) => ToJSON (ObsUnit d) where-instance (ToJSON d) => ToJSON (CohortData d) where-instance (ToJSON d) => ToJSON (Cohort d) where-instance ToJSON CohortStatus where-instance ToJSON AttritionInfo where---- | A type used to determine the output shape of a Cohort.-data CohortShape d where- ColumnWise :: (Show a, ToJSON a) => a -> CohortShape ColumnWise- RowWise :: (Show a, ToJSON a) => a -> CohortShape RowWise--deriving instance Show d => Show (CohortShape d)---- | Maps CohortShape into an Aeson Value. --- TODO: implement Generic and ToJSON instance of CohortShape directly.-toJSONCohortShape :: CohortShape shape -> Value-toJSONCohortShape (ColumnWise x) = toJSON x-toJSONCohortShape (RowWise x) = toJSON x--class ShapeCohort d where- colWise :: Cohort d -> CohortShape ColumnWise- rowWise :: Cohort d -> CohortShape RowWise--instance ShapeCohort Featureset where- colWise x = ColumnWise (shapeColumnWise x)- rowWise x = RowWise (shapeRowWise x)--data ColumnWise = MkColumnWise {- colAttributes :: NonEmpty (OutputShape Type)- , ids :: [ID]- , colData :: NonEmpty (NonEmpty (OutputShape Type))- } deriving ( Show, Generic )--instance ToJSON ColumnWise where- toJSON x = object [ "attributes" .= colAttributes x- , "ids" .= ids x- , "data" .= colData x ]--newtype IDRow = MkIDRow (ID, NonEmpty (OutputShape Type))- deriving ( Show, Generic )--instance ToJSON IDRow where- toJSON (MkIDRow x) = object [ uncurry (.=) x]--data RowWise = MkRowWise {- attributes :: NonEmpty (OutputShape Type)- , rowData :: NonEmpty IDRow- } deriving ( Show, Generic )--instance ToJSON RowWise where- toJSON x = object [ "attributes" .= attributes x- , "data" .= rowData x ]--shapeColumnWise :: Cohort Featureset -> ColumnWise-shapeColumnWise x = MkColumnWise- (fmap (nameAttr . NE.head . getFeatureset) z)- (getCohortIDs x)- (fmap (fmap dataOnly . getFeatureset) z)- -- TODO: don't use fromList; do something more principled- where z = getFeaturesetList (tpose (MkFeaturesetList (NE.fromList (getCohortData x))))--shapeRowWise :: Cohort Featureset -> RowWise-shapeRowWise x = MkRowWise- (fmap (nameAttr . NE.head . getFeatureset) z)- (fmap MkIDRow (zip ids (fmap (fmap dataOnly . getFeatureset) z)))- -- TODO: don't use fromList; do something more principled- where z = NE.fromList (getCohortData x)- ids = fromList (getCohortIDs x)
+ src/Hasklepias/FeatureEvents.hs view
@@ -0,0 +1,269 @@+{-|+Module : Functions for composing features from events +Description : Functions for composing features. +Copyright : (c) NoviSci, Inc 2020+License : BSD3+Maintainer : bsaul@novisci.com++Provides functions used in defining @'Features.Feature'@ from +@'EventData.Event'@s.+-}+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE NoImplicitPrelude #-}++module Hasklepias.FeatureEvents(+ -- ** Container predicates+ isNotEmpty+ , atleastNofX+ , anyGapsWithinAtLeastDuration+ , allGapsWithinLessThanDuration++ -- ** Finding occurrences of concepts+ , nthConceptOccurrence+ , firstConceptOccurrence++ -- ** Reshaping containers+ , allPairs+ , splitByConcepts++ -- ** Create filters+ , makeConceptsFilter+ , makePairedFilter++ -- ** Functions for working with Event Domains+ , viewBirthYears+ , previewBirthYear++ -- ** Function for manipulating intervals+ , lookback+ , lookahead++ -- ** Misc functions+ , computeAgeAt+) where+++import IntervalAlgebra ( Intervallic+ , IntervalSizeable(..)+ , ComparativePredicateOf1+ , ComparativePredicateOf2+ , Interval+ , IntervalCombinable+ , begin+ , end+ , beginerval+ , enderval )+import IntervalAlgebra.PairedInterval ( PairedInterval, getPairData )+import IntervalAlgebra.IntervalUtilities ( durations, gapsWithin )+import EventData ( Events+ , Event+ , ConceptEvent+ , ctxt )+import EventData.Context ( Concept+ , Concepts+ , Context+ , HasConcept( hasConcepts )+ , facts )+import EventData.Context.Domain ( Domain+ , demo+ , info+ , _Demographics )+import Safe ( headMay, lastMay )+import Control.Applicative ( Applicative(liftA2) )+import Control.Monad ( Functor(fmap), (=<<) )+import Control.Lens ( preview, (^.) )+import Data.Bool ( Bool(..), (&&), not, (||) )+import Data.Either ( either )+import Data.Eq ( Eq )+import Data.Foldable ( Foldable(length, null)+ , all+ , any+ , toList )+import Data.Function ( (.), ($), const )+import Data.Functor ( Functor(fmap) )+import Data.Int ( Int )+import Data.Maybe ( Maybe(..), maybe, mapMaybe )+import Data.Monoid ( Monoid )+import Data.Ord ( Ord(..) )+import Data.Time.Calendar ( Day, Year, diffDays )+import Data.Text ( Text )+import Data.Text.Read ( rational )+import Data.Tuple ( fst )+import Witherable ( filter, Filterable, Witherable )+import GHC.Num ( Integer, fromInteger )+import GHC.Real ( RealFrac(floor), (/) )++-- | Is the input list empty? +isNotEmpty :: [a] -> Bool+isNotEmpty = not.null++-- | Filter 'Events' to those that have any of the provided concepts.+makeConceptsFilter ::+ ( Filterable f ) => + [Text] -- ^ the list of concepts by which to filter + -> f (Event a)+ -> f (Event a)+makeConceptsFilter cpts = filter (`hasConcepts` cpts)++-- | Filter 'Events' to a single @'Maybe' 'Event'@, based on a provided function,+-- with the provided concepts. For example, see 'firstConceptOccurrence' and+-- 'lastConceptOccurrence'.+nthConceptOccurrence ::+ ( Filterable f ) => + (f (Event a) -> Maybe (Event a)) -- ^ function used to select a single event+ -> [Text]+ -> f (Event a)+ -> Maybe (Event a)+nthConceptOccurrence f c = f.makeConceptsFilter c++-- | Finds the *first* occurrence of an 'Event' with at least one of the concepts.+-- Assumes the input 'Events' list is appropriately sorted.+firstConceptOccurrence ::+ ( Witherable f ) => + [Text]+ -> f (Event a)+ -> Maybe (Event a)+firstConceptOccurrence = nthConceptOccurrence (headMay . toList)++-- | Finds the *last* occurrence of an 'Event' with at least one of the concepts.+-- Assumes the input 'Events' list is appropriately sorted.+lastConceptOccurrence ::+ ( Witherable f ) =>+ [Text]+ -> f (Event a)+ -> Maybe (Event a)+lastConceptOccurrence = nthConceptOccurrence (lastMay . toList)++-- | Does 'Events' have at least @n@ events with any of the Concept in @x@.+atleastNofX ::+ Int -- ^ n+ -> [Text] -- ^ x+ -> Events a -> Bool+atleastNofX n x es = length (makeConceptsFilter x es) >= n++-- | Takes a predicate of intervals and a predicate on the data part of a +-- paired interval to create a single predicate such that both input+-- predicates should hold.+makePairPredicate :: Ord a =>+ ComparativePredicateOf2 (i0 a) ((PairedInterval b) a)+ -> i0 a+ -> (b -> Bool)+ -> (PairedInterval b a -> Bool)+makePairPredicate pi i pd x = pi i x && pd (getPairData x)++-- | +makePairedFilter :: Ord a =>+ ComparativePredicateOf2 (i0 a) ((PairedInterval b) a)+ -> i0 a+ -> (b -> Bool)+ -> [PairedInterval b a]+ -> [PairedInterval b a]+makePairedFilter fi i fc = filter (makePairPredicate fi i fc)++-- | Generate all pair-wise combinations from two lists.+allPairs :: [a] -> [b] -> [(a, b)]+allPairs = liftA2 (,)++-- | Split an @Events a@ into a pair of @Events a@. The first element contains+-- events have any of the concepts in the first argument, similarly for the+-- second element.+splitByConcepts :: + ( Filterable f ) =>+ [Text]+ -> [Text]+ -> f (Event a)+ -> (f (Event a), f (Event a))+splitByConcepts c1 c2 es = ( filter (`hasConcepts` c1) es+ , filter (`hasConcepts` c2) es)++-- | Create a predicate function that checks whether within a provided spanning+-- interval, are there (e.g. any, all) gaps of (e.g. <, <=, >=, >) a specified+-- duration among the input intervals?+makeGapsWithinPredicate ::+ ( Monoid (t (Interval a))+ , Monoid (t (Maybe (Interval a)))+ , Applicative t+ , Witherable t+ , IntervalSizeable a b+ , IntervalCombinable i0 a+ , IntervalCombinable i1 a) =>+ ((b -> Bool) -> t b -> Bool)+ -> (b -> b -> Bool)+ -> (b -> i0 a -> t (i1 a) -> Bool)+makeGapsWithinPredicate f op gapDuration interval l =+ maybe False (f (`op` gapDuration) . durations) (gapsWithin interval l)++-- | Within a provided spanning interval, are there any gaps of at least the+-- specified duration among the input intervals?+anyGapsWithinAtLeastDuration ::+ ( IntervalSizeable a b+ , IntervalCombinable i0 a+ , IntervalCombinable i1 a+ , Monoid (t (Interval a))+ , Monoid (t (Maybe (Interval a)))+ , Applicative t+ , Witherable t) =>+ b -- ^ duration of gap+ -> i0 a -- ^ within this interval+ -> t (i1 a)+ -> Bool+anyGapsWithinAtLeastDuration = makeGapsWithinPredicate any (>=)++-- | Within a provided spanning interval, are all gaps less than the specified+-- duration among the input intervals?+--+-- >>> allGapsWithinLessThanDuration 30 (beginerval 100 (0::Int)) [beginerval 5 (-1), beginerval 99 10]+-- True+allGapsWithinLessThanDuration ::+ ( IntervalSizeable a b+ , IntervalCombinable i0 a+ , IntervalCombinable i1 a+ , Monoid (t (Interval a))+ , Monoid (t (Maybe (Interval a)))+ , Applicative t+ , Witherable t) =>+ b -- ^ duration of gap+ -> i0 a -- ^ within this interval+ -> t (i1 a)+ -> Bool+allGapsWithinLessThanDuration = makeGapsWithinPredicate all (<)++-- | Utility for reading text into a maybe integer+intMayMap :: Text -> Maybe Integer -- TODO: this is ridiculous+intMayMap x = fmap floor (either (const Nothing) (Just . fst) (Data.Text.Read.rational x))++-- | Preview birth year from a domain+previewBirthYear :: Domain -> Maybe Year+previewBirthYear dmn = intMayMap =<< ((^.demo.info) =<< preview _Demographics dmn)++-- | Returns a (possibly emtpy) list of birth years from a set of events+viewBirthYears :: Events a -> [Year]+viewBirthYears = mapMaybe (\e -> previewBirthYear =<< Just (ctxt e^.facts ))++-- | Compute the "age" in years between two calendar days. The difference between+-- the days is rounded down.+computeAgeAt :: Day -> Day -> Integer+computeAgeAt bd at = floor (fromInteger (diffDays at bd) / 365.25)++-- | Creates a new @Interval@ of a provided lookback duration ending at the +-- 'begin' of the input interval.+--+-- >>> lookback 4 (beginerval 10 (1 :: Int))+-- (-3, 1)+lookback :: (Intervallic i a, IntervalSizeable a b) =>+ b -- ^ lookback duration+ -> i a+ -> Interval a+lookback d x = enderval d (begin x)++-- | Creates a new @Interval@ of a provided lookahead duration beginning at the +-- 'end' of the input interval.+--+-- >>> lookahead 4 (beginerval 1 (1 :: Int))+-- (2, 6)+lookahead :: (Intervallic i a, IntervalSizeable a b) =>+ b -- ^ lookahead duration+ -> i a+ -> Interval a+lookahead d x = beginerval d (end x)+
src/Hasklepias/MakeApp.hs view
@@ -36,7 +36,7 @@ import GHC.IO ( IO ) import EventData ( Events )-import Hasklepias.Cohort+import Cohort import IntervalAlgebra ( IntervalSizeable ) import Control.Monad.IO.Class (MonadIO, liftIO)
+ src/Hasklepias/Misc.hs view
@@ -0,0 +1,89 @@+{-|+Module : Misc types and functions +Description : Misc types and functions useful in Hasklepias.+Copyright : (c) NoviSci, Inc 2020+License : BSD3+Maintainer : bsaul@novisci.com++These functions may be moved to more appropriate modules in future versions.+-}+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE DeriveGeneric #-}++module Hasklepias.Misc (+ F+ , Def+ , Occurrence(..)+ , makeOccurrence+ , getOccurrenceReason+ , getOccurrenceTime+ , CensoringReason(..)+ , OccurrenceReason(..)+ , CensoredOccurrence(..)+ , adminCensor++) where++import Features.Compose ( Feature, Definition )+import Data.Bool ( otherwise, (&&) )+import Data.Eq ( Eq(..) )+import Data.Ord ( Ord(..), Ordering(..) )+import Data.Semigroup ( Semigroup((<>)) )+import GHC.Generics ( Generic )+import GHC.Show ( Show(..) )+import Stype.Numeric.Censored ( MaybeCensored(..) )+import Stype.Numeric.Continuous ( EventTime )++-- | Type synonym for 'Feature'.+type F n a = Feature n a++-- | Type synonym for 'Definition'.+type Def d = Definition d++-- | A simple typeclass for making a type a "reason" for an event.+class (Ord a, Show a) => OccurrenceReason a where++-- | A type containing the time and when something occurred+newtype Occurrence what when = MkOccurrence ( what , EventTime when )+ deriving (Eq, Show, Generic)++-- | Create an 'Occurrence'+makeOccurrence :: (OccurrenceReason what) => + what -> EventTime b -> Occurrence what b+makeOccurrence r t = MkOccurrence (r , t)++-- | Get the reason for an 'Occurrence'.+getOccurrenceReason :: Occurrence what b -> what +getOccurrenceReason (MkOccurrence (r, t)) = r++-- | Get the time of an 'Occurrence'.+getOccurrenceTime :: Occurrence what b -> EventTime b +getOccurrenceTime (MkOccurrence (r, t)) = t++-- Define a custom ordering based on times and then reasons.+instance (OccurrenceReason r, Ord b) => Ord (Occurrence r b) where+ compare (MkOccurrence (r1, t1)) (MkOccurrence (r2, t2))+ | t1 < t2 = LT+ | t1 == t2 && r1 < r2 = LT+ | t1 == t2 && r1 == r2 = EQ+ | otherwise = GT++-- | Sum type for possible censoring and outcome reasons, including administrative+-- censoring.+data CensoringReason cr or = AdminCensor | C cr | O or+ deriving (Eq, Show, Generic)++-- | A type to represent censored 'Occurrence'.+data CensoredOccurrence censors outcomes b = MkCensoredOccurrence {+ reason :: CensoringReason censors outcomes+ , time :: MaybeCensored ( EventTime b )}+ deriving (Eq, Generic)++instance (OccurrenceReason c, OccurrenceReason o, Show b) => + Show ( CensoredOccurrence c o b ) where+ show (MkCensoredOccurrence r t) = "(" <> show t <> ", " <> show r <> ")"++-- | Creates an administratively censored occurrence.+adminCensor :: EventTime b -> CensoredOccurrence c o b+adminCensor t = MkCensoredOccurrence AdminCensor ( RightCensored t )
src/Hasklepias/Reexports.hs view
@@ -13,22 +13,26 @@ -- * Re-exports module GHC.Num+ , module GHC.Real , module GHC.Generics , module GHC.Show , module GHC.TypeLits , module Control.Monad , module Control.Applicative+ , module Data.Bifunctor , module Data.Bool , module Data.Either , module Data.Eq , module Data.Int , module Data.Maybe , module Data.Monoid+ , module Data.Foldable , module Data.Function- , module Data.Functor , module Data.List , module Data.List.NonEmpty , module Data.Ord+ , module Data.Proxy+ , module Set , module Data.Time.Calendar , module Data.Text , module Data.Tuple@@ -42,21 +46,32 @@ , module Witherable ) where -import safe GHC.Num ( Integer )+import safe GHC.Num ( Integer(..)+ , Num(..)+ , Natural(..) )+import safe GHC.Real ( Integral(..), toInteger ) import safe GHC.Generics ( Generic ) import safe GHC.Show ( Show(..) )-import safe GHC.TypeLits ( KnownSymbol )-import safe Control.Monad ( Functor(fmap), Monad(..) )+import safe GHC.TypeLits ( KnownSymbol(..)+ , symbolVal ) import safe Control.Applicative ( (<$>), Applicative(..) )+import safe Control.Monad ( Functor(..)+ , Monad(..)+ , join+ , (>>=)+ , (=<<) )+import safe Data.Bifunctor ( Bifunctor(..) ) import safe Data.Bool ( Bool(..) , (&&), not, (||) , bool , otherwise ) import safe Data.Either ( Either(..)) import safe Data.Eq ( Eq, (==))-import safe Data.Function ( (.), ($), const, id )-import safe Data.Functor ( fmap )-import safe Data.Int ( Int )+import safe Data.Foldable ( Foldable(..)+ , minimum+ , maximum )+import safe Data.Function ( (.), ($), const, id, flip )+import safe Data.Int ( Int(..) ) import safe Data.List ( all , any , map@@ -79,8 +94,11 @@ mapMaybe, maybeToList ) import safe Data.Monoid ( Monoid(..), (<>) )-import safe Data.Ord ( Ord((>=), (<), (>), (<=))+import safe Data.Ord ( Ord(..)+ , Ordering(..) , max, min )+import safe Data.Proxy ( Proxy(..) )+import safe Data.Set as Set ( Set(..), fromList, member) import safe Data.Time.Calendar ( Day, MonthOfYear, Year , CalendarDiffDays(..) , addGregorianDurationClip@@ -94,6 +112,6 @@ import safe IntervalAlgebra.IntervalUtilities import safe IntervalAlgebra.PairedInterval -import safe Witherable ( Filterable(filter) )+import safe Witherable ( Filterable(filter), Witherable ) import safe Flow ( (!>), (.>), (<!), (<.), (<|), (|>) ) import Safe ( headMay, lastMay )
+ test/Cohort/CoreSpec.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DataKinds #-}++module Cohort.CoreSpec (+ spec+ ) where++import Features+import Cohort+import Data.List.NonEmpty+import Test.Hspec ( describe, pending, shouldBe, it, Spec )++-- data Feat1++d1 :: Definition (Feature "feat" Int -> Feature "feat1" Bool)+d1 =+ defineA+ (\x ->+ if x < 0 then+ makeFeature $ featureDataL $ Other "at least 1 < 0"+ else+ pure ((x + 1) == 5)+ )+++d2 :: Definition (Feature "feat" Int -> Feature "feat2" Status)+d2 = define (\x -> includeIf (x*2 > 4))++d3 :: Definition (Feature "feat" Int -> Feature "feat3" Int)+d3 = define (+ 2)+++testSubject1 :: Subject Int+testSubject1 = MkSubject ("1", 0)+testSubject2 :: Subject Int+testSubject2 = MkSubject ("2", 54)++testPopulation :: Population Int+testPopulation = MkPopulation [testSubject1, testSubject2]++buildCriteria :: Int -> Criteria+buildCriteria dat = criteria $ pure ( criterion feat1 )+ where feat1 = eval d2 $ pure dat++type Features = (Feature "feat1" Bool, Feature "feat3" Int)+buildFeatures :: Int -> Features+buildFeatures dat =+ ( eval d1 input+ , eval d3 input+ )+ where input = pure dat++testCohort :: CohortSpec Int Features+testCohort = specifyCohort buildCriteria buildFeatures++testOut :: Cohort Features+testOut = MkCohort+ ( Just $ MkAttritionInfo $ (ExcludedBy (1, "feat2"), 1) :| [ (Included, 1) ]+ , MkCohortData [MkObsUnit "2" + ( makeFeature (featureDataR False)+ , makeFeature (featureDataR 56)) ])++-- evalCohort testCohort testPopulation+spec :: Spec+spec = do++ describe "checking d1" $+ do++ it "include f1" $+ evalCohort testCohort testPopulation `shouldBe` testOut+
+ test/Cohort/CriteriaSpec.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DataKinds #-}+module Cohort.CriteriaSpec (+ spec+ ) where++import Features+import Cohort.Criteria+import Test.Hspec ( describe, pending, shouldBe, it, Spec )+import Data.List.NonEmpty+++f1 :: Status -> Criterion +f1 s = criterion (makeFeature (featureDataR s) :: Feature "f1" Status)++f2 :: Status -> Criterion +f2 s = criterion (makeFeature (featureDataR s) :: Feature "f2" Status)++f3 :: Status -> Criterion +f3 s = criterion (makeFeature (featureDataR s) :: Feature "f3" Status)++f4 :: Criterion +f4 = criterion ( makeFeature (featureDataL $ Other "something") :: Feature "f4" Status)++spec :: Spec+spec = do++ describe "checking d1" $+ do++ it "include f1" $+ checkCohortStatus (criteria $ pure (f1 Include)) `shouldBe` Included++ it "include f1, f2, f3" $+ checkCohortStatus (criteria $ f1 Include :| [f2 Include, f3 Include] ) + `shouldBe` Included++ it "exclude on f2" $+ checkCohortStatus (criteria $ f2 Exclude :| [f3 Include]) + `shouldBe` ExcludedBy (1, "f2")++ it "exclude on f2" $+ checkCohortStatus (criteria $ f1 Include :| [f2 Exclude, f3 Include]) + `shouldBe` ExcludedBy (2, "f2")++ it "error on f4" $+ checkCohortStatus (criteria $ f1 Include :| [f2 Include, f3 Include, f4]) + `shouldBe` ExcludedBy (4, "f4")
+ test/Cohort/InputSpec.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE OverloadedStrings #-}+module Cohort.InputSpec (spec) where++import IntervalAlgebra+import EventData+import Cohort+import EventData.Context as HC+import EventData.Context.Domain+import Data.Aeson+import Data.Maybe+import Data.Time as DT+import Test.Hspec+import qualified Data.ByteString.Lazy as B++++testInputsDay1 :: B.ByteString+testInputsDay1 =+ "[\"abc\", \"2020-01-01\", \"2020-01-02\", \"Diagnosis\",\+ \[\"someThing\"],\+ \{\"domain\":\"Diagnosis\",\+ \ \"time\":{\"begin\":\"2020-01-01\",\"end\":\"2020-01-02\"}}]\n\+ \[\"abc\", \"2020-01-05\", \"2020-01-06\", \"Diagnosis\",\+ \[\"someThing\"],\+ \{\"domain\":\"Diagnosis\",\+ \ \"time\":{\"begin\":\"2020-01-05\",\"end\":\"2020-01-06\"}}]"+++testInputsDay2 :: B.ByteString+testInputsDay2 =+ "[\"def\", \"2020-01-01\", null, \"Diagnosis\",\+ \[\"someThing\"],\+ \{\"domain\":\"Diagnosis\",\+ \ \"time\":{\"begin\":\"2020-01-01\",\"end\":\"2020-01-02\"}}]\n\+ \[\"def\", \"2020-01-05\", null, \"Diagnosis\",\+ \[\"someThing\"],\+ \{\"domain\":\"Diagnosis\",\+ \ \"time\":{\"begin\":\"2020-01-05\",\"end\":\"2020-01-06\"}}]"++testInputsDayBad :: B.ByteString+testInputsDayBad =+ "[\"ghi\", \"2020-01-01\", null, \"Diagnosis\",\+ \[\"someThing\"],\+ \{\"time\":{\"begin\":\"2020-01-01\",\"end\":\"2020-01-02\"}}]\n\+ \[\"ghi\", \"2020-01-05\", null, \"Diagnosis\",\+ \[\"someThing\"],\+ \{\"domain\":\"Diagnosis\",\+ \ \"time\":{\"begin\":\"2020-01-05\",\"end\":\"2020-01-04\"}}]"++testInput = testInputsDay1 <> "\n" <> testInputsDay2+++testOutDay1 = event (beginerval 1 (fromGregorian 2020 1 1))+ (HC.context ( UnimplementedDomain () ) (packConcepts ["someThing"]))+testOutDay2 = event (beginerval 1 (fromGregorian 2020 1 5))+ (HC.context ( UnimplementedDomain () ) (packConcepts [ "someThing"]))+++testOutPop = MkPopulation [+ MkSubject ("abc", [testOutDay1, testOutDay2])+ , MkSubject ("def", [testOutDay1, testOutDay2])+ ]++spec :: Spec+spec = do+ it "a population is parsed" $+ parsePopulationDayLines testInput `shouldBe` ([], testOutPop)++ it "a population is parsed" $+ parsePopulationDayLines (testInput <> "\n" <> testInputsDayBad)`shouldBe`+ ([MkParseError (5, "Error in $: key \"domain\" not found")+ , MkParseError (6, "Error in $: 2020-01-04<2020-01-05")], testOutPop)
test/EventDataSpec.hs view
@@ -3,7 +3,7 @@ import IntervalAlgebra import IntervalAlgebra.IntervalUtilities-import FeatureEvents +import Hasklepias.FeatureEvents import EventData import EventData.Context as HC import EventData.Context.Domain ( Domain(UnimplementedDomain) )
− test/FeatureEventsSpec.hs
@@ -1,32 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE FlexibleContexts #-}-module FeatureEventsSpec (spec) where--import IntervalAlgebra-import FeatureEvents ( firstConceptOccurrence )-import EventData ( Event, event )-import EventData.Context as HC ( context, packConcepts )-import EventData.Context.Domain-import Data.Maybe-import Test.Hspec ( it, shouldBe, Spec )---- | Toy events for unit tests-evnt1 :: Event Int-evnt1 = event ( beginerval (4 :: Int) (1 :: Int) ) - ( HC.context (UnimplementedDomain ()) (packConcepts ["c1", "c2"] ))-evnt2 :: Event Int-evnt2 = event ( beginerval (4 :: Int) (2 :: Int) )- ( HC.context (UnimplementedDomain ()) (packConcepts ["c3", "c4"] ))-evnts :: [Event Int]-evnts = [evnt1, evnt2]--spec :: Spec-spec = do- it "find first occurrence of c1" $- firstConceptOccurrence ["c1"] evnts `shouldBe` Just evnt1- it "find first occurrence of c3" $- firstConceptOccurrence ["c3"] evnts `shouldBe` Just evnt2- it "find first occurrence of c5" $- firstConceptOccurrence ["c5"] evnts `shouldBe` Nothing
test/Features/OutputSpec.hs view
@@ -8,7 +8,7 @@ import IntervalAlgebra import EventData import EventData.Context-import FeatureEvents+import Hasklepias.FeatureEvents import Features import Data.Aeson (encode) import Data.Maybe
− test/Hasklepias/Cohort/CoreSpec.hs
@@ -1,72 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE DataKinds #-}--module Hasklepias.Cohort.CoreSpec (- spec- ) where--import Features-import Hasklepias.Cohort-import Data.List.NonEmpty-import Test.Hspec ( describe, pending, shouldBe, it, Spec )---- data Feat1--d1 :: Definition (Feature "feat" Int -> Feature "feat1" Bool)-d1 =- defineA- (\x ->- if x < 0 then- makeFeature $ featureDataL $ Other "at least 1 < 0"- else- pure ((x + 1) == 5)- )---d2 :: Definition (Feature "feat" Int -> Feature "feat2" Status)-d2 = define (\x -> includeIf (x*2 > 4))--d3 :: Definition (Feature "feat" Int -> Feature "feat3" Int)-d3 = define (+ 2)---testSubject1 :: Subject Int-testSubject1 = MkSubject ("1", 0)-testSubject2 :: Subject Int-testSubject2 = MkSubject ("2", 54)--testPopulation :: Population Int-testPopulation = MkPopulation [testSubject1, testSubject2]--buildCriteria :: Int -> Criteria-buildCriteria dat = criteria $ pure ( criterion feat1 )- where feat1 = eval d2 $ pure dat--type Features = (Feature "feat1" Bool, Feature "feat3" Int)-buildFeatures :: Int -> Features-buildFeatures dat =- ( eval d1 input- , eval d3 input- )- where input = pure dat--testCohort :: CohortSpec Int Features-testCohort = specifyCohort buildCriteria buildFeatures--testOut :: Cohort Features-testOut = MkCohort- ( Just $ MkAttritionInfo $ (ExcludedBy (1, "feat2"), 1) :| [ (Included, 1) ]- , MkCohortData [MkObsUnit "2" - ( makeFeature (featureDataR False)- , makeFeature (featureDataR 56)) ])---- evalCohort testCohort testPopulation-spec :: Spec-spec = do-- describe "checking d1" $- do-- it "include f1" $- evalCohort testCohort testPopulation `shouldBe` testOut-
− test/Hasklepias/Cohort/CriteriaSpec.hs
@@ -1,48 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE DataKinds #-}-module Hasklepias.Cohort.CriteriaSpec (- spec- ) where--import Features-import Hasklepias.Cohort.Criteria-import Test.Hspec ( describe, pending, shouldBe, it, Spec )-import Data.List.NonEmpty---f1 :: Status -> Criterion -f1 s = criterion (makeFeature (featureDataR s) :: Feature "f1" Status)--f2 :: Status -> Criterion -f2 s = criterion (makeFeature (featureDataR s) :: Feature "f2" Status)--f3 :: Status -> Criterion -f3 s = criterion (makeFeature (featureDataR s) :: Feature "f3" Status)--f4 :: Criterion -f4 = criterion ( makeFeature (featureDataL $ Other "something") :: Feature "f4" Status)--spec :: Spec-spec = do-- describe "checking d1" $- do-- it "include f1" $- checkCohortStatus (criteria $ pure (f1 Include)) `shouldBe` Included-- it "include f1, f2, f3" $- checkCohortStatus (criteria $ f1 Include :| [f2 Include, f3 Include] ) - `shouldBe` Included-- it "exclude on f2" $- checkCohortStatus (criteria $ f2 Exclude :| [f3 Include]) - `shouldBe` ExcludedBy (1, "f2")-- it "exclude on f2" $- checkCohortStatus (criteria $ f1 Include :| [f2 Exclude, f3 Include]) - `shouldBe` ExcludedBy (2, "f2")-- it "error on f4" $- checkCohortStatus (criteria $ f1 Include :| [f2 Include, f3 Include, f4]) - `shouldBe` ExcludedBy (4, "f4")
− test/Hasklepias/Cohort/InputSpec.hs
@@ -1,73 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module Hasklepias.Cohort.InputSpec (spec) where--import IntervalAlgebra-import EventData--- import Hasklepias.Cohort.Aeson-import Hasklepias.Cohort-import EventData.Context as HC-import EventData.Context.Domain-import Data.Aeson-import Data.Maybe-import Data.Time as DT-import Test.Hspec-import qualified Data.ByteString.Lazy as B----testInputsDay1 :: B.ByteString-testInputsDay1 =- "[\"abc\", \"2020-01-01\", \"2020-01-02\", \"Diagnosis\",\- \[\"someThing\"],\- \{\"domain\":\"Diagnosis\",\- \ \"time\":{\"begin\":\"2020-01-01\",\"end\":\"2020-01-02\"}}]\n\- \[\"abc\", \"2020-01-05\", \"2020-01-06\", \"Diagnosis\",\- \[\"someThing\"],\- \{\"domain\":\"Diagnosis\",\- \ \"time\":{\"begin\":\"2020-01-05\",\"end\":\"2020-01-06\"}}]"---testInputsDay2 :: B.ByteString-testInputsDay2 =- "[\"def\", \"2020-01-01\", null, \"Diagnosis\",\- \[\"someThing\"],\- \{\"domain\":\"Diagnosis\",\- \ \"time\":{\"begin\":\"2020-01-01\",\"end\":\"2020-01-02\"}}]\n\- \[\"def\", \"2020-01-05\", null, \"Diagnosis\",\- \[\"someThing\"],\- \{\"domain\":\"Diagnosis\",\- \ \"time\":{\"begin\":\"2020-01-05\",\"end\":\"2020-01-06\"}}]"--testInputsDayBad :: B.ByteString-testInputsDayBad =- "[\"ghi\", \"2020-01-01\", null, \"Diagnosis\",\- \[\"someThing\"],\- \{\"time\":{\"begin\":\"2020-01-01\",\"end\":\"2020-01-02\"}}]\n\- \[\"ghi\", \"2020-01-05\", null, \"Diagnosis\",\- \[\"someThing\"],\- \{\"domain\":\"Diagnosis\",\- \ \"time\":{\"begin\":\"2020-01-05\",\"end\":\"2020-01-04\"}}]"--testInput = testInputsDay1 <> "\n" <> testInputsDay2---testOutDay1 = event (beginerval 1 (fromGregorian 2020 1 1))- (HC.context ( UnimplementedDomain () ) (packConcepts ["someThing"]))-testOutDay2 = event (beginerval 1 (fromGregorian 2020 1 5))- (HC.context ( UnimplementedDomain () ) (packConcepts [ "someThing"]))---testOutPop = MkPopulation [- MkSubject ("abc", [testOutDay1, testOutDay2])- , MkSubject ("def", [testOutDay1, testOutDay2])- ]--spec :: Spec-spec = do- it "a population is parsed" $- parsePopulationDayLines testInput `shouldBe` ([], testOutPop)-- it "a population is parsed" $- parsePopulationDayLines (testInput <> "\n" <> testInputsDayBad)`shouldBe`- ([MkParseError (5, "Error in $: key \"domain\" not found")- , MkParseError (6, "Error in $: 2020-01-04<2020-01-05")], testOutPop)
+ test/Hasklepias/FeatureEventsSpec.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE FlexibleContexts #-}+module Hasklepias.FeatureEventsSpec (spec) where++import IntervalAlgebra+import Hasklepias.FeatureEvents ( firstConceptOccurrence )+import EventData ( Event, event )+import EventData.Context as HC ( context, packConcepts )+import EventData.Context.Domain+import Data.Maybe+import Test.Hspec ( it, shouldBe, Spec )++-- | Toy events for unit tests+evnt1 :: Event Int+evnt1 = event ( beginerval (4 :: Int) (1 :: Int) ) + ( HC.context (UnimplementedDomain ()) (packConcepts ["c1", "c2"] ))+evnt2 :: Event Int+evnt2 = event ( beginerval (4 :: Int) (2 :: Int) )+ ( HC.context (UnimplementedDomain ()) (packConcepts ["c3", "c4"] ))+evnts :: [Event Int]+evnts = [evnt1, evnt2]++spec :: Spec+spec = do+ it "find first occurrence of c1" $+ firstConceptOccurrence ["c1"] evnts `shouldBe` Just evnt1+ it "find first occurrence of c3" $+ firstConceptOccurrence ["c3"] evnts `shouldBe` Just evnt2+ it "find first occurrence of c5" $+ firstConceptOccurrence ["c5"] evnts `shouldBe` Nothing