hasklepias 0.13.0 → 0.13.1
raw patch · 12 files changed
+208/−73 lines, 12 files
Files
- ChangeLog.md +6/−0
- exampleApp/Main.hs +5/−3
- examples/ExampleCohort1.hs +53/−35
- hasklepias.cabal +2/−1
- src/FeatureCompose.hs +17/−11
- src/FeatureCompose/Aeson.hs +6/−7
- src/FeatureCompose/Attributes.hs +50/−4
- src/FeatureCompose/Featureable.hs +37/−0
- src/Hasklepias.hs +3/−0
- src/Hasklepias/Cohort/Index.hs +10/−7
- src/Hasklepias/ReexportsUnsafe.hs +3/−1
- test/FeatureCompose/AesonSpec.hs +16/−4
ChangeLog.md view
@@ -1,5 +1,11 @@ # Changelog for hasklepias +## 0.13.1++* Adds the `Featureable` type, which allows users to put features into a heterogenous list. A drawback of `Featureable` is that two `Featurable` values cannot be tested for equality, so testing will need to occur before a `Feature n d` is packed into a `Featureable` (by the `packFeature` function) or after the `Featureable` is encoded to JSON. See `examples/ExampleCohort1.hs` for example usage.+* Adds a `"type"` field to JSON output `Feature`s, which is a string representing the type `d` in a `Feature n d`. E.g., for a `Feature "x" Bool`, the result would be : `{..., "type": "Bool", ...}`.+* Adds the `Role` and `Purpose` types, which now are included as part of the `Attributes` type. The `Role`s mostly align with `stype`'s `valid_roles`, with the exception that `"identifier"`, `"index"` and `"censoring"` are not included and `"other"` corresponds to `Unspecified`.+ ## 0.13.0 * Adds a rudimentary `Attributes` type and `HasAttributes` typeclass for adding attributes to `Feature`s. This interfaces will likely change in the future, but for now users have the ability to add information like labels to a `Feature`. In fact, `Feature`s which are encoded to JSON are *required* to have attributes.
exampleApp/Main.hs view
@@ -39,7 +39,7 @@ critTrue = define $ pure Include instance HasAttributes "dummy" Bool where- getAttributes _ = MkAttributes "Text" "Text" "Text"+ getAttributes _ = emptyAttributes {------------------------------------------------------------------------------- Cohort Specifications and evaluation@@ -53,13 +53,15 @@ featEvs = featureEvents events -- | Define the shape of features for a cohort-type CohortFeatures = ( Feature "dummy" Bool )+type CohortFeatures = [Featureable]+ -- (Feature "dummy" Bool ) + -- | Make a function that runs the features for a calendar index makeFeatureRunner :: Events Day -> CohortFeatures-makeFeatureRunner events = eval featureDummy ef+makeFeatureRunner events = [packFeature ( eval featureDummy ef )] where ef = featureEvents events -- | Make a cohort specification for each calendar time
examples/ExampleCohort1.hs view
@@ -261,22 +261,45 @@ diabetes = twoOutOneIn ["is_diabetes_outpatient"] ["is_diabetes_inpatient"] instance HasAttributes "diabetes" Bool where- getAttributes _ = MkAttributes + getAttributes _ = basicAttributes "Has Diabetes" "Has Diabetes within baseline"- "Has at least 1 event during the baseline interval has any of the 'cpts1' concepts \- \ OR there are at least 2 event that have 'cpts2' concepts which have at least 7 days \- \ between them during the baseline interval"+ [Covariate]+ ["baseline"] + ckd :: BoolFeatDef "ckd" ckd = twoOutOneIn ["is_ckd_outpatient"] ["is_ckd_inpatient"] +instance HasAttributes "ckd" Bool where+ getAttributes _ = basicAttributes+ "Has ckd"+ "Has CKD within baseline"+ [Covariate]+ ["baseline"]+ ppi :: BoolFeatDef "ppi" ppi = medHx ["is_ppi"] +instance HasAttributes "ppi" Bool where+ getAttributes _ = basicAttributes + "Has ppi"+ "Has PPI within baseline"+ [Covariate]+ ["baseline"]+ glucocorticoids :: BoolFeatDef "glucocorticoids" glucocorticoids = medHx ["is_glucocorticoids"] +instance HasAttributes "glucocorticoids" Bool where+ getAttributes _ = basicAttributes + "Has glucocorticoids"+ "Has glucocorticoids within baseline"+ [Covariate]+ ["baseline"]++-- instance HasAttributes "" (*)+ {------------------------------------------------------------------------------- Cohort Specifications and evaluation -------------------------------------------------------------------------------}@@ -303,38 +326,29 @@ featInd = featureIndex index featEvs = featureEvents events --- | Define the shape of features for a cohort-type ExampleFeatures =- ( Feature "calendarIndex" (Index Interval Day)- , BoolFeat "diabetes"- , BoolFeat "ckd"- , BoolFeat "ppi"- , BoolFeat "glucocorticoids"- )- -- | Make a function that runs the features for a calendar index makeFeatureRunner :: Index Interval Day -> Events Day- -> ExampleFeatures-makeFeatureRunner index events = (- idx- , eval diabetes (idx, ef)- , eval ckd (idx, ef)- , eval ppi (idx, ef)- , eval glucocorticoids (idx, ef)- )+ -> [Featureable] +makeFeatureRunner index events = [+ packFeature idx+ , packFeature $ eval diabetes (idx, ef)+ , packFeature $ eval ckd (idx, ef)+ , packFeature $ eval ppi (idx, ef)+ , packFeature $ eval glucocorticoids (idx, ef)+ ] where idx = featureIndex index ef = featureEvents events -- | Make a cohort specification for each calendar time-cohortSpecs :: [CohortSpec (Events Day) ExampleFeatures]+cohortSpecs :: [CohortSpec (Events Day) [Featureable]] cohortSpecs = map (\x -> specifyCohort (makeCriteriaRunner x) (makeFeatureRunner x)) indices -- | A function that evaluates all the calendar cohorts for a population-evalCohorts :: Population (Events Day) -> [Cohort ExampleFeatures]+evalCohorts :: Population (Events Day) -> [Cohort [Featureable]] evalCohorts pop = map (`evalCohort` pop) cohortSpecs {-------------------------------------------------------------------------------@@ -376,19 +390,21 @@ makeExpectedCovariate :: (KnownSymbol name) => FeatureData Bool -> Feature name Bool makeExpectedCovariate = makeFeature +instance HasAttributes "calendarIndex" (Index Interval Day) where+ makeExpectedFeatures :: FeatureData (Index Interval Day) -> (FeatureData Bool, FeatureData Bool, FeatureData Bool, FeatureData Bool)- -> ExampleFeatures+ -> [Featureable] makeExpectedFeatures i (b1, b2, b3, b4) =- ( makeFeature i :: Feature "calendarIndex" (Index Interval Day)- , makeExpectedCovariate b1- , makeExpectedCovariate b2- , makeExpectedCovariate b3- , makeExpectedCovariate b4- )+ [ packFeature (makeFeature i :: Feature "calendarIndex" (Index Interval Day))+ , packFeature ( makeExpectedCovariate b1 :: Feature "diabetes" Bool )+ , packFeature ( makeExpectedCovariate b2 :: Feature "ckd" Bool )+ , packFeature ( makeExpectedCovariate b3 :: Feature "ppi" Bool )+ , packFeature ( makeExpectedCovariate b4 :: Feature "glucocorticoids" Bool )+ ] -expectedFeatures1 :: [ExampleFeatures]+expectedFeatures1 :: [[Featureable ]] expectedFeatures1 = map (uncurry makeExpectedFeatures) [ (pure $ makeIndex $ beginerval 1 (fromGregorian 2017 4 1),@@ -399,13 +415,13 @@ (pure True, pure False, pure True, pure False)) ] -expectedObsUnita :: [ObsUnit ExampleFeatures]+expectedObsUnita :: [ObsUnit [Featureable]] expectedObsUnita = zipWith (curry MkObsUnit) (replicate 5 "a") expectedFeatures1 -makeExpectedCohort :: AttritionInfo -> [ObsUnit ExampleFeatures] -> Cohort ExampleFeatures+makeExpectedCohort :: AttritionInfo -> [ObsUnit [Featureable]] -> Cohort [Featureable] makeExpectedCohort a x = MkCohort (Just a, x) -expectedCohorts :: [Cohort ExampleFeatures]+expectedCohorts :: [Cohort [Featureable]] expectedCohorts = zipWith (curry MkCohort)@@ -424,5 +440,7 @@ exampleCohort1tests :: TestTree exampleCohort1tests = testGroup "Unit tests for calendar cohorts" [ testCase "expected Features for testData1" $- evalCohorts testPop @?= expectedCohorts+ -- Featureable cannot be tested for equality directly, hence encoding to + -- JSON bytestring and testing that for equality+ encode (evalCohorts testPop) @?= encode expectedCohorts ]
hasklepias.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: hasklepias-version: 0.13.0+version: 0.13.1 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@@ -32,6 +32,7 @@ FeatureCompose FeatureCompose.Aeson FeatureCompose.Attributes+ FeatureCompose.Featureable FeatureEvents Hasklepias Hasklepias.Aeson
src/FeatureCompose.hs view
@@ -9,9 +9,9 @@ -} {-# OPTIONS_HADDOCK hide #-} +{-# LANGUAGE Safe #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE Safe #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE DataKinds #-}@@ -21,6 +21,9 @@ {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE InstanceSigs #-} module FeatureCompose( -- * Features and FeatureData@@ -55,9 +58,9 @@ , (=<<), join, liftM, liftM2, liftM3) import safe Data.Either ( Either(..) ) import safe Data.Eq ( Eq(..) )-import safe Data.Foldable ( Foldable(foldr) )-import safe Data.Function ( ($), (.) )-import safe Data.List ( (++) )+import safe Data.Foldable ( Foldable(foldr), fold )+import safe Data.Function ( ($), (.), id )+import safe Data.List ( (++), transpose, concat ) import safe Data.Proxy ( Proxy(..) ) import safe Data.Text ( Text, pack ) import safe Data.Traversable ( Traversable(..) )@@ -87,7 +90,7 @@ -} {- tag::featureData[] -}-newtype FeatureData d = MkFeatureData { +newtype FeatureData d = MkFeatureData { getFeatureData :: Either MissingReason d -- ^ Unwrap FeatureData. } {- end::featureData[] -}@@ -336,7 +339,7 @@ -> args -- ^ a tuple of arguments to the @'Definition'@ -> return -instance Eval (FeatureData b -> FeatureData a) +instance Eval (FeatureData b -> FeatureData a) (FeatureData b) (FeatureData a) where eval (D1 f) x = fmap f x eval (D1A f) x = x >>= f@@ -350,7 +353,7 @@ MkFeatureData (Right r) -> r -instance Eval (FeatureData c -> FeatureData b -> FeatureData a) +instance Eval (FeatureData c -> FeatureData b -> FeatureData a) (FeatureData c, FeatureData b) (FeatureData a) where eval (D2 f) (x, y) = liftA2 f x y eval (D2A f) (x, y) = join (liftA2 f x y)@@ -381,8 +384,11 @@ {- | Initializes @Feature@ @Attributes@ to empty strings -} -class HasAttributes n a where- getAttributes :: Feature n a -> Attributes+-- class HasAttributes n a where+-- getAttributes :: Feature n a -> Attributes+-- getAttributes _ = MkAttributes "" "" "" --- instance HasAttributes name d where--- getAttributes x = MkAttributes "" "" ""+-- instance (KnownSymbol n) => HasAttributes n a where+ -- getAttributes :: Feature n a -> Attributes+ -- getAttributes _ = MkAttributes "" "" ""+
src/FeatureCompose/Aeson.hs view
@@ -25,6 +25,7 @@ import FeatureCompose.Attributes import Data.Aeson ( object, KeyValue((.=)), ToJSON(toJSON) ) import Data.Proxy ( Proxy(Proxy) )+import Data.Typeable (typeRep, Typeable) instance (ToJSON a, Ord a, Show a)=> ToJSON (Interval a) where toJSON x = object ["begin" .= begin x, "end" .= end x]@@ -36,14 +37,12 @@ (Left l) -> toJSON l (Right r) -> toJSON r +instance ToJSON Role where+instance ToJSON Purpose where instance ToJSON Attributes where --- instance (KnownSymbol n, ToJSON d) => ToJSON (Feature n d) where--- toJSON x = object [ "name" .= show (symbolVal (Proxy @n))--- -- , "attrs" .= toJSON (getAttributes x)--- , "data" .= toJSON (getFData x) ]--instance (KnownSymbol n, ToJSON d, HasAttributes n d) => ToJSON (Feature n d) where- toJSON x = object [ "name" .= show (symbolVal (Proxy @n))+instance (Typeable d, KnownSymbol n, ToJSON d, HasAttributes n d) => ToJSON (Feature n d) where+ toJSON x = object [ "name" .= symbolVal (Proxy @n) , "attrs" .= toJSON (getAttributes x)+ , "type" .= toJSON (show $ typeRep (Proxy @d)) , "data" .= toJSON (getFData x) ]
src/FeatureCompose/Attributes.hs view
@@ -12,17 +12,43 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE MultiParamTypeClasses #-} module FeatureCompose.Attributes(- Attributes(..)- -- , HasAttributes(..)+ Attributes(..)+ , Role(..)+ , Purpose(..)+ , HasAttributes(..)+ , emptyAttributes+ , basicAttributes+ , emptyPurpose ) where import safe Data.Eq ( Eq )+import safe Data.Ord ( Ord )+import safe Data.Set ( Set, empty, fromList ) import safe Data.Text ( Text ) import safe GHC.Show ( Show ) import safe GHC.Generics ( Generic )+import safe GHC.TypeLits ( KnownSymbol ) +{- | A type to identify a feature's role -}+data Role =+ Outcome+ | Covariate+ | Exposure+ | Competing+ | Weight+ | Intermediate+ | Unspecified+ deriving (Eq, Ord, Show, Generic)++{- | A type to identify a feature's purpose -}+data Purpose = MkPurpose {+ getRole :: Set Role + , getTags :: Set Text+ } deriving (Eq, Show, Generic)+ {- | A data type for holding attritbutes of Features. This type and the @HasAttributes@ are likely to change in future versions.@@ -31,9 +57,29 @@ getShortLabel :: Text , getLongLabel :: Text , getDerivation :: Text+ , getPurpose :: Purpose } deriving (Eq, Show, Generic) --- class HasAttributes a where--- getAttributes :: a -> Attributes+{- | Just empty purpose -}+emptyPurpose :: Purpose+emptyPurpose = MkPurpose empty empty +{- | Just empty attributes -}+emptyAttributes :: Attributes+emptyAttributes = MkAttributes "" "" "" emptyPurpose +{- | Create attributes with just short label, long label, roles, and tags. -}+basicAttributes :: + Text -- ^ short label+ -> Text -- ^ long label+ -> [Role] -- ^ purpose roles+ -> [Text] -- ^ purpose tags+ -> Attributes+basicAttributes sl ll rls tgs = + MkAttributes sl ll "" + (MkPurpose (fromList rls) (fromList tgs))+++class (KnownSymbol n) => HasAttributes n a where+ getAttributes :: f n a -> Attributes+ getAttributes _ = emptyAttributes
+ src/FeatureCompose/Featureable.hs view
@@ -0,0 +1,37 @@+{-|+Module : Functions for defining attributes Feature data+Description : Defines attributes instances for Features.+Copyright : (c) NoviSci, Inc 2020+License : BSD3+Maintainer : bsaul@novisci.com+-}++{-# LANGUAGE ExistentialQuantification #-}++module FeatureCompose.Featureable (+ Featureable(..)+ , packFeature+) where++import FeatureCompose ( HasAttributes, Feature )+import FeatureCompose.Aeson ()+import FeatureCompose.Attributes ()+import GHC.TypeLits ( KnownSymbol )+import Data.Aeson ( ToJSON(toJSON) )+import Data.Typeable ( Typeable )++{- | Existential type to hold features -}+data Featureable = forall d . (Show d, ToJSON d) => MkFeatureable d++{- | Pack a feature into a Featurable -}+packFeature ::+ (KnownSymbol n, Show d, ToJSON d, Typeable d, HasAttributes n d) =>+ Feature n d -> Featureable+packFeature = MkFeatureable++instance Show Featureable where+ show (MkFeatureable x) = show x++instance ToJSON Featureable where+ toJSON (MkFeatureable x) = toJSON x+
src/Hasklepias.hs view
@@ -25,6 +25,8 @@ , module FeatureCompose.Attributes -- ** Writing Features to JSON , module FeatureCompose.Aeson+ -- ** Exporting Features+ , module FeatureCompose.Featureable -- * Utilities for defining Features from Events {- |@@ -64,6 +66,7 @@ import FeatureCompose import FeatureCompose.Aeson import FeatureCompose.Attributes+import FeatureCompose.Featureable import FeatureEvents import Hasklepias.Reexports import Hasklepias.ReexportsUnsafe
src/Hasklepias/Cohort/Index.hs view
@@ -8,7 +8,8 @@ {-# OPTIONS_HADDOCK hide #-} {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE Safe #-}+{-# LANGUAGE DeriveGeneric #-}+-- {-# LANGUAGE Safe #-} module Hasklepias.Cohort.Index( Index@@ -16,21 +17,23 @@ , getIndex ) where -import safe GHC.Show ( Show )-import safe Data.Eq ( Eq )-import safe IntervalAlgebra ( Intervallic )+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 (Intervallic i a) => Index i a = MkIndex { +newtype Index i a = MkIndex { getIndex :: i a -- ^ Unwrap an @Index@- } deriving (Eq, Show)+ } 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/ReexportsUnsafe.hs view
@@ -11,11 +11,13 @@ module Hasklepias.ReexportsUnsafe ( -- * Re-exports- module GHC.IO+ module Data.Aeson+ , module GHC.IO , module Test.Tasty , module Test.Tasty.HUnit ) where +import Data.Aeson (encode) import GHC.IO ( IO(..) ) -- import GHC.Types ( Any )
test/FeatureCompose/AesonSpec.hs view
@@ -35,9 +35,14 @@ dummy = pure True instance HasAttributes "dummy" Bool where- getAttributes x = MkAttributes "some Label" "longer label..." "a description"+ getAttributes x = MkAttributes "some Label" "longer label..." "a description" emptyPurpose +dummy2 :: Feature "dummy2" Bool+dummy2 = pure True +instance HasAttributes "dummy2" Bool where+ getAttributes x = emptyAttributes + spec :: Spec spec = do it "an Int event is parsed correctly" $@@ -45,9 +50,16 @@ it "dummy encodes correctly" $ encode dummy `shouldBe` - "{\"data\":true,\"name\":\"\\\"dummy\\\"\",\- \\"attrs\":{\"getDerivation\":\"a description\",\+ "{\"data\":true,\"name\":\"dummy\",\"type\":\"Bool\",\+ \\"attrs\":{\"getPurpose\":{\"getTags\":[],\"getRole\":[]},\+ \\"getDerivation\":\"a description\",\ \\"getLongLabel\":\"longer label...\",\ \\"getShortLabel\":\"some Label\"}}" -+ it "dummy2 encodes correctly" $+ encode dummy2 `shouldBe` + "{\"data\":true,\"name\":\"dummy2\",\"type\":\"Bool\",\+ \\"attrs\":{\"getPurpose\":{\"getTags\":[],\"getRole\":[]},\+ \\"getDerivation\":\"\",\+ \\"getLongLabel\":\"\",\+ \\"getShortLabel\":\"\"}}"