packages feed

ideas-statistics-1.0: src/Domain/Hypothesis/Rules.hs

-----------------------------------------------------------------------------
-- Copyright 2020, Ideas project team. This file is distributed under the
-- terms of the Apache License 2.0. For more information, see the files
-- "LICENSE.txt" and "NOTICE.txt", which are included in the distribution.
-----------------------------------------------------------------------------
module Domain.Hypothesis.Rules
   ( addAlphaRule, addDfRule, addDfBetweenWithin
   , addH0FromHARule, addH0FromHAEqualSignRule, addHARule
   , addHypothesesRule, addHypothesesChiSquaredRule
   , addObservedTotals, addExpectedFrequencies
   , addConclusionPValueRule, addRejectionRule
   , addTestFormulaRule, addTestValueRule
   , chooseTTestPairedRule, chooseTTestRule, chooseTTestTwoRule
   , chooseZTestRule, chooseRPearsonRule, chooseAnovaRule, chooseChiSquaredRule
   , computePValueTTest, computePValueZTest
   , criticalConclusionRule
   , hypothesesConclusionCriticalRule, hypothesesConclusionPValueRule
   , addStandardErrorSigma, addStandardErrorSD
   , determineSided
   , lookupTValueRule, lookupZValueRule, lookupRValueRule, lookupFValueRule
   , lookupChiValueRule
     -------------
   , inferSidedness, inferTestChoice, inferTestChoices, inferRejectionCritical
   , inferConclusionCritical, inferConclusionPValue, inferDf, inferVar
   , inferTestFormula, inferCriticalZWith, inferCriticalTWith
   , inferCriticalRWith, inferCriticalFWith, inferCriticalChiWith
   , inferTestValue, inferDfBetweenWithin, chiSquaredDf, chiSquaredTestValue
   , computeTotals, computeExpectedFrequencies, getTable
   ) where

import Control.Monad
import Data.List
import Data.Maybe
import Domain.Hypothesis.Common
import Domain.Math.Data.Relation
import Domain.Math.Expr hiding ((.*.), (./.), (^))
import Domain.Math.Numeric.Views
import Domain.Statistics.ComponentSet
import Domain.Statistics.Views
import Ideas.Common.Library

----------------------------------------------------------
-- Rules for determining the confidence level

addAlphaRule :: Rule ComponentSet
addAlphaRule =
  describe "Rule for adding the alpha component" .
  makeRule "component.alpha" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs = do
      guard (derived cs `doesNotContain` SignificanceLevel)
      let alpha = case get SignificanceLevel (initials cs) of
                     Just (CExpr a) -> a
                     _ -> toExpr pickAlpha
      return $ append SignificanceLevel (CExpr alpha) cs

----------------------------------------------------------
-- Rules for constructing the hypotheses

determineSided :: Rule ComponentSet
determineSided =
  describe "Rule for determine one-/two-sided testing" .
  makeRule "component.sided" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs = do
       guard (cs `doesNotContain` Sidedness)
       sided <- 
          case getTestType TestChoice (initials cs) of
             Just Anova -> return RightSided
             _ -> do
                ha <- getRelation AlternativeHypothesis cs
                return (sidedFromHA ha)
       return $ append Sidedness (CChoice $ Sided sided) cs

addHypothesesRule :: Rule ComponentSet
addHypothesesRule = 
   describe "Add null hypothesis and alternative hypothesis, in one step" $
   makeRule "component.hypotheses" f
 where
   f :: ComponentSet -> [ComponentSet]
   f = applyAll $ (addH0FromHARule ./. addH0FromHAEqualSignRule) .*. addHARule

addH0FromHARule :: Rule ComponentSet
addH0FromHARule =
  describe "Rule for adding the H0 component based on HA" .
  makeRule "component.h0-from-ha" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs = do
      guard (cs `doesNotContain` NullHypothesis)
      ha <- getRelation AlternativeHypothesis cs
      return . append NullHypothesis
        (CRelation $ h0FromHA ha) $ cs

addH0FromHAEqualSignRule :: Rule ComponentSet
addH0FromHAEqualSignRule =
  describe "Rule for adding the H0 component based on HA; use equal sign (by convention)" .
  makeRule "component.h0-from-ha-eq" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs = do
      guard (cs `doesNotContain` NullHypothesis)
      ha <- getRelation AlternativeHypothesis cs   
      return . append NullHypothesis
        (CRelation $ h0FromHAEqualSign ha) $ cs

addHARule :: Rule ComponentSet
addHARule =
  describe "Rule for adding the HA component" .
  makeRule "component.ha" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs = do
       guard (derived cs `doesNotContain` AlternativeHypothesis)
       ha <- get AlternativeHypothesis (initials cs) 
       return $ append AlternativeHypothesis ha cs

----------------------------------------------------------
-- Rules for determining the properties of the data

chooseTTestRule :: Rule ComponentSet
chooseTTestRule =
  describe "Rule for choosing the type of test" .
  makeRule "component.test.t-test" $
     addTestChoice TTestOne

chooseTTestTwoRule :: Rule ComponentSet
chooseTTestTwoRule =
  describe "Rule for choosing the type of test" .
  makeRule "component.test.t-test-two" $
     addTestChoice TTestTwo

chooseTTestPairedRule :: Rule ComponentSet
chooseTTestPairedRule =
  describe "Rule for choosing the type of test" .
  makeRule "component.test.t-test-paired" $
     addTestChoice TTestPaired

-- | If the standard deviation of the population is known then the z-test is
-- /always/ chosen, thus a t-test is never chosen in that case. When this
-- information is unknown, then an estimation of the standard deviation of the
-- population has to be made based on the sample, in that case a t-test is
-- chosen.
--
-- When the sample size becomes large enough* it is also possible to choose a
-- z-test because the z-distribution looks like the t-distribution for large
-- sample sizes.
--
-- NOTE*: The threshold for what `large' means may vary. For now this threshold
--        is fixed at 100.
--
-- Consisely: If PopulationSdev known: ZTest
--            else if SampleSize big enough: ZTest or TTest
--            else TTest
chooseZTestRule :: Rule ComponentSet
chooseZTestRule =
  describe "Rule for choosing the type of test" .
  makeRule "component.test.z-test" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs =
      let largeThreshold = 100
      in do
        guard (derived cs `doesNotContain` TestChoice)
        n <- match naturalView $ fromMaybe 0 (getExpr SampleSize cs)
        if cs `contains` PopulationSdev || n >= largeThreshold 
           then return (append TestChoice (CChoice $ TestType ZTest) cs)
           else do
              -- This branch is similar to the previous rule body in revision 10549
              let tests = validTests cs
              guard (ZTest `elem` tests)
              return $ append TestChoice (CChoice $ TestType ZTest) cs

addTestFormulaRule :: Rule ComponentSet
addTestFormulaRule =
  describe "Rule for adding the test formula" .
  makeRule "component.test-formula" $ f
  where
    f :: ComponentSet -> [ComponentSet]
    f cs = do
      guard (derived cs `doesNotContain` TestFormula)
      rel <- inferTestFormula cs
      return $ append TestFormula (CRelation rel) cs

addTestValueRule :: Rule ComponentSet
addTestValueRule =
  describe "Rule for adding the test value (from the formula)" .
  makeRule "component.test-value" $ f
  where
    f :: ComponentSet -> [ComponentSet]
    f cs = do
      guard (derived cs `doesNotContain` TestValue)
      tv <- inferTestValue cs
      return $ append TestValue (CRelation tv) cs

inferTestValue :: MonadPlus m => ComponentSet -> m (Relation Expr)
inferTestValue cs =
   case getRelation TestValue (initials cs) of
      Just initialTestValue -> 
         return initialTestValue
      _ | inferTestChoice cs == Just ChiSquared -> 
         chiSquaredTestValue cs
      _ -> do
         new <- msum (map return $ applyAll addTestFormulaRule cs)
         let cs' = substitute new
         var  <- leftHandSide  <$> getRelation TestFormula new
         expr <- rightHandSide <$> getRelation TestFormula cs'
         val  <- matchM doubleView expr
         return $ var .==. toExpr val

----------------------------------------------------------
-- Rules for performing a T-Test

addDfRule :: Rule ComponentSet
addDfRule =
  describe "Rule for adding the degrees of freedom component" .
  makeRule "component.df" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs = do
      guard (derived cs `doesNotContain` Df)
      case get Df (initials cs) of
         Just initialDf -> 
            return $ append Df initialDf cs
         _ | inferTestChoice cs == Just ChiSquared -> do
            df <- chiSquaredDf cs
            return $ append Df (CExpr (toExpr df)) cs
         _ -> do 
            test <- inferTestChoice cs
            df   <- degreesOfFreedomFromTest test (chooseTypeOfTest cs)
            val  <- matchM doubleView (getSubstitution cs |-> df)
            return $ append Df (CExpr (toExpr val)) cs

lookupTValueRule :: Rule ComponentSet
lookupTValueRule =
  describe "Rule for looking up a t-value" .
  makeRule "component.critical.t-value" $ 
     inferCriticalTWith $ \sided alpha df -> 
        [computeCriticalT sided alpha df]

-- shared function for computing t-value and r-value
inferCriticalGenericWith :: (TestType -> Bool) -> Expr -> (Sided -> Double -> Double -> [Double]) 
                         -> ComponentSet -> [ComponentSet]
inferCriticalGenericWith forTestType var compute cs = do
   guard (cs `doesNotContain` Critical)
   guard (derived cs `contains` AlternativeHypothesis)
   guard (any forTestType (inferTestChoices cs))
   cs'   <- matchM substitutedView cs
   alpha <- matchM doubleView <=< getExpr SignificanceLevel $ cs   
   let cs'' = case inferDf cs' of 
                 Just df -> substitute (append Df (CExpr df) cs')
                 Nothing -> cs'
   df <- matchM doubleView =<< getExpr Df cs''
   sided <- inferSidedness cs
   value <- compute sided alpha df
   return $ append Critical (CRelation $ var .==. fromDouble value) cs

inferCriticalTWith :: (Sided -> Double -> Double -> [Double]) -> ComponentSet -> [ComponentSet]
inferCriticalTWith = inferCriticalGenericWith isTTest (Var "tcrit")

computePValueTTest :: Rule ComponentSet
computePValueTTest =
  describe "Rule for computing the p-value for a t-test" .
  makeRule "component.p-value.t-test" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs = do
      guard (cs `doesNotContain` PValue)
      guard (derived cs `contains` AlternativeHypothesis)
      guard (maybe False isTTest (inferTestChoice cs))
      let cs'   = substitute cs
      testStatistic <- match doubleView =<< fmap rightHandSide (getRelation TestValue cs')

      let cs'' = case inferDf cs' of 
                    Just df -> substitute (append Df (CExpr df) cs')
                    Nothing -> cs'
      df <- matchM doubleView =<< getExpr Df cs''
      sided         <- inferSidedness cs
      let value = computePValueT sided testStatistic df
      return $ append PValue (CExpr $ fromDouble value) cs

----------------------------------------------------------
-- Rules for performing a Z-Test

lookupZValueRule :: Rule ComponentSet
lookupZValueRule =
  describe "Rule for looking up a z-value" .
  makeRule "component.critical.z-value" $ 
     inferCriticalZWith $ \sided alpha -> 
        [computeCriticalZ sided alpha]

inferCriticalZWith :: (Sided -> Double -> [Double]) -> ComponentSet -> [ComponentSet]
inferCriticalZWith compute cs = do
  guard (cs `doesNotContain` Critical)
  guard (derived cs `contains` AlternativeHypothesis)
  guard (maybe False (ZTest ==) (inferTestChoice cs))
  alpha <- matchM doubleView <=< getExpr SignificanceLevel $ cs
  sided <- inferSidedness cs
  value <- compute sided alpha
  return $ append Critical (CRelation $ Var "zcrit" .==. fromDouble value) cs

computePValueZTest :: Rule ComponentSet
computePValueZTest =
  describe "Rule for computing the p-value for a z-test" .
  makeRule "component.p-value.z-test" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs = do
      guard (cs `doesNotContain` PValue)
      guard (maybe False (ZTest ==) (inferTestChoice cs))
      guard (derived cs `contains` AlternativeHypothesis)
      let cs'   = substitute cs
      -- TestStatistic was renamed to TestFormula, which is now a CRelation
      testStatistic <- match doubleView =<< fmap rightHandSide (getRelation TestValue cs')
      sided <- inferSidedness cs
      let value = computePValueZ sided testStatistic
      return $ append PValue (CExpr $ fromDouble value) cs

----------------------------------------------------------
-- Rules for performing R-Pearson

lookupRValueRule :: Rule ComponentSet
lookupRValueRule =
  describe "Rule for looking up a r-value" .
  makeRule "component.critical.r-value" $ 
     inferCriticalRWith $ \sided alpha df -> 
        [computeCriticalR sided alpha df]

chooseRPearsonRule :: Rule ComponentSet
chooseRPearsonRule =
  describe "Rule for choosing the type of test" .
  makeRule "component.test.r-pearson" $
     addTestChoice RPearson

addTestChoice :: TestType -> ComponentSet -> Maybe ComponentSet
addTestChoice testType cs = do
  guard (derived cs `doesNotContain` TestChoice)
  let tests = validTests cs
  guard (testType `elem` tests)
  return $ append TestChoice (CChoice $ TestType testType) cs

inferCriticalRWith :: (Sided -> Double -> Double -> [Double]) -> ComponentSet -> [ComponentSet]
inferCriticalRWith = inferCriticalGenericWith (== RPearson) (Var "rcrit")

----------------------------------------------------------
-- Rules for performing Anova

lookupFValueRule :: Rule ComponentSet
lookupFValueRule =
  describe "Rule for looking up a F-value" .
  makeRule "component.critical.f-value" $ inferCriticalFWith computeCriticalF

chooseAnovaRule :: Rule ComponentSet
chooseAnovaRule =
  describe "Rule for choosing the type of test" .
  makeRule "component.test.anova" $
     addTestChoice Anova

addDfBetweenWithin :: Rule ComponentSet
addDfBetweenWithin = describe "Add df between and within (for Anova)" $
  makeRule "component.df-anova" f
 where
   f :: ComponentSet -> Maybe ComponentSet
   f cs = do
      guard (derived cs `doesNotContain` DfBetween)
      guard (derived cs `doesNotContain` DfWithin)
      (between, within) <- inferDfBetweenWithin cs
      return $ append DfBetween (CExpr $ toExpr between) $ 
               append DfWithin  (CExpr $ toExpr within)  cs

inferDfBetweenWithin :: Monad m => ComponentSet -> m (Double, Double)
inferDfBetweenWithin cs = do
   n     <- matchM doubleView <=< getExpr SampleSize $ cs
   let nrOfGroups = 2 -- always 2, for now
       dfBetween  = nrOfGroups - 1
       dfWithin   = n - nrOfGroups
   return (dfBetween, dfWithin)

inferCriticalFWith :: (Double -> Double -> Double -> [Double]) -> ComponentSet -> [ComponentSet]
inferCriticalFWith compute cs = do
  guard (cs `doesNotContain` Critical)
  guard (derived cs `contains` AlternativeHypothesis)
  guard (maybe False (Anova ==) (inferTestChoice cs))
  alpha <- matchM doubleView <=< getExpr SignificanceLevel $ cs
  (dfBetween, dfWithin) <- inferDfBetweenWithin cs
  value <- compute dfBetween dfWithin alpha
  return $ append Critical (CRelation $ Var "Fcrit" .==. fromDouble value) cs

----------------------------------------------------------
-- Rules for performing Chi-Squared

chooseChiSquaredRule :: Rule ComponentSet
chooseChiSquaredRule = describe "Rule for choosing the type of test" .
  makeRule "component.test.chi-squared" $
     addTestChoice ChiSquared

addHypothesesChiSquaredRule :: Rule ComponentSet
addHypothesesChiSquaredRule = describe "Add hypotheses (null and alternative) for chi-squared" $
   makeRule "component.hypotheses-chi-squared" f
 where
   f :: ComponentSet -> Maybe ComponentSet
   f cs = do
      guard (derived cs `doesNotContain` NullHypothesis)
      guard (derived cs `doesNotContain` AlternativeHypothesis)
      guard (maybe False (ChiSquared ==) (inferTestChoice cs))
      return $ append NullHypothesis (CExpr independent) 
             $ append AlternativeHypothesis (CExpr dependent) cs

addObservedTotals :: Rule ComponentSet
addObservedTotals = describe "Add totals (rows and columns) for observed frequencies" $ 
   makeRule "component.observed-totals" f
 where
   f :: ComponentSet -> Maybe ComponentSet
   f cs = do  
      guard (derived cs `doesNotContain` ObservedColumnTotals)
      guard (derived cs `doesNotContain` ObservedRowTotals)
      guard (derived cs `doesNotContain` ObservedTotal)
      table <- getTable ObservedFrequencies cs
      let (rowTotals, columnTotals, total) = computeTotals table
      return $ append ObservedRowTotals (CExpr $ toExpr rowTotals) 
             $ append ObservedColumnTotals (CExpr $ toExpr columnTotals) 
             $ append ObservedTotal (CExpr $ toExpr total) cs

type ChiSquaredTotals = ([Int], [Int], Int)

computeTotals :: [[Int]] -> ChiSquaredTotals
computeTotals table = 
   let rowTotals    = map sum table
       columnTotals = map sum (transpose table)
       total        = sum rowTotals
   in (rowTotals, columnTotals, total)

computeExpectedFrequencies :: ChiSquaredTotals -> [[Double]]
computeExpectedFrequencies (rowTotals, columnTotals, total) = table
 where
   xss = map (replicate (length columnTotals)) rowTotals
   yss = replicate (length rowTotals) columnTotals
   table = zipWith (zipWith f) xss yss
   f x y = fromIntegral (x*y) / fromIntegral total

addExpectedFrequencies :: Rule ComponentSet
addExpectedFrequencies = describe "Add expected frequencies" $ 
   makeRule "component.expected-frequencies" f
 where
   f :: ComponentSet -> Maybe ComponentSet
   f cs = do
      guard (derived cs `doesNotContain` ExpectedFrequencies)
      observed <- getTable ObservedFrequencies cs
      let totals   = computeTotals observed
          expected = computeExpectedFrequencies totals
      return $ append ExpectedFrequencies (CExpr $ toExpr expected) cs
   
lookupChiValueRule :: Rule ComponentSet
lookupChiValueRule = describe "Rule for looking up a chi^2-value" .
  makeRule "component.critical.chi-value" $ 
     inferCriticalChiWith $ \sided alpha df -> 
        computeCriticalChi sided alpha df

inferCriticalChiWith :: (Sided -> Double -> Double -> [Double]) -> ComponentSet -> [ComponentSet]
inferCriticalChiWith = inferCriticalGenericWith (== ChiSquared) (Var "chicrit")

chiSquaredTestValue :: MonadPlus m => ComponentSet -> m (Relation Expr)
chiSquaredTestValue cs = do 
   observed <- getTable ObservedFrequencies cs
   expected <- getDoubleTable ExpectedFrequencies cs
   let table = zipWith (zipWith f) observed expected
       f o e = (fromIntegral o-e)^(2 :: Int) / e
       value = sum (map sum table)
   return (chiSquared .==. toExpr value)
   
chiSquaredDf :: MonadPlus m => ComponentSet -> m Int
chiSquaredDf cs = do
   observed <- getTable ObservedFrequencies cs
   guard (not $ null observed)
   let r = length observed
       c = length $ head observed
   return ((r-1)*(c-1))
   
getTable :: MonadPlus m => ComponentId -> ComponentSet -> m [[Int]]
getTable n cs = getExpr n cs >>= fromExpr

getDoubleTable :: MonadPlus m => ComponentId -> ComponentSet -> m [[Double]]
getDoubleTable n cs = getExpr n cs >>= fromExpr

----------------------------------------------------------
-- Rules for constructing the rejection region

-- lookup the Sidedness component; if not present, try to infer (using the rule)
inferSidedness :: MonadPlus m => ComponentSet -> m Sided
inferSidedness cs = 
   getSided Sidedness cs `mplus` do
      testType <- inferTestChoice cs
      guard (testType `elem` [Anova, ChiSquared])
      return RightSided
    `mplus` do
      cs' <- applyM determineSided cs
      getSided Sidedness cs'

inferVar :: MonadPlus m => ComponentSet -> m Expr
inferVar cs = do 
   testType <- inferTestChoice cs
   return (varForTestType testType)

varForTestType :: TestType -> Expr
varForTestType testType = 
   case testType of 
      ZTest      -> Var "z"
      RPearson   -> Var "r"
      Anova      -> Var "F"
      ChiSquared -> chiSquared
      _          -> Var "t"

inferTestChoice :: MonadPlus m => ComponentSet -> m TestType
inferTestChoice cs =
   case validTests cs of
      hd:_ -> return hd
      _    -> fail "no valid test choice"

inferTestChoices :: ComponentSet -> [TestType]
inferTestChoices cs = 
   case getTestType TestChoice cs of
      Just test -> [test]
      Nothing   -> validTests cs

inferTestFormula :: ComponentSet -> [Relation Expr]
inferTestFormula cs = 
   case getRelation TestFormula cs of
      Just formula -> [formula]
      Nothing -> do
         testType <- inferTestChoices cs
         let var  = varForTestType testType
         let test = chooseTypeOfTest cs
         t <- testFormulaFromTest testType test
         return (var .==. t)

inferRejectionCritical :: MonadPlus m => ComponentSet -> m (Relation Expr)
inferRejectionCritical cs = 
   getRelation RejectionCritical cs `mplus` do
      cs' <- applyM addRejectionRule cs
      getRelation RejectionCritical cs'

inferConclusionCritical :: MonadPlus m => ComponentSet -> m Bool
inferConclusionCritical cs = 
   getConclusion ConclusionCritical cs `mplus` do
      cs' <- applyM criticalConclusionRule cs
      getConclusion ConclusionCritical cs'

inferConclusionPValue :: MonadPlus m => ComponentSet -> m (Relation Expr)
inferConclusionPValue cs = 
   getRelation ConclusionPValue cs `mplus` do
      cs' <- applyM addConclusionPValueRule cs
      getRelation ConclusionPValue cs'

inferDf :: MonadPlus m => ComponentSet -> m Expr
inferDf cs =
   getExpr Df cs `mplus` do
      cs' <- applyM addDfRule cs
      getExpr Df cs'

addRejectionRule :: Rule ComponentSet
addRejectionRule =
  describe "Rule for constructing the rejection critical component" .
  makeRule "component.rejection.critical" $ f
  where
    f :: ComponentSet -> [ComponentSet]
    f cs = do
      guard (cs `doesNotContain` RejectionCritical)
      guard (derived cs `contains` AlternativeHypothesis)
      let cs' = substitute cs
      sided <- inferSidedness cs'
      testType <- inferTestChoices cs
      let rel = case testType of 
                   ZTest      -> sidedRelation sided (Var "z") (Var "zcrit")
                   RPearson   -> sidedRelation sided (Var "r") (Var "rcrit")
                   Anova      -> sidedRelation sided (Var "F") (Var "Fcrit")
                   ChiSquared -> sidedRelation sided chiSquared (Var "chicrit")
                   _          -> sidedRelation sided (Var "t") (Var "tcrit")
      return . append RejectionCritical
        (CRelation rel) $ cs

addConclusionPValueRule :: Rule ComponentSet
addConclusionPValueRule =
  describe "Rule for constructing the conclusion p-value component" .
  makeRule "component.conclusion.p-value" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs = do
      guard (cs `doesNotContain` ConclusionPValue)
      pv    <- matchM doubleView =<< getExpr PValue cs 
      alpha <- matchM doubleView =<< getExpr SignificanceLevel cs
      let relType = if pv > alpha then GreaterThan else LessThanOrEqualTo
      return . append ConclusionPValue
       (CRelation $ makeType relType (Var "p") (Var "alpha")) $ cs

----------------------------------------------------------
-- Rules for making a conclusion

criticalConclusionRule :: Rule ComponentSet
criticalConclusionRule =
  describe ("Rule for creating a conclusion based on a critical value " ++
  "and test statistic") .
  makeRule "component.critical-conclusion" $ f
  where
    f :: ComponentSet -> Maybe ComponentSet
    f cs = do
      rej <- inferRejectionCritical cs
      -- to do: rejection critical is added to component set only to get the substituted relation
      let cs' = substitute (append RejectionCritical (CRelation rej) cs)
      rejection <- getRelation RejectionCritical cs'
      guard (cs `doesNotContain` ConclusionCritical)
      lhs       <- match doubleView $ leftHandSide  rejection
      rhs       <- match doubleView $ rightHandSide rejection
      let result = eval (relationType rejection) lhs rhs
      return $ append ConclusionCritical (CChoice $ Conclusion result) cs

hypothesesConclusionCriticalRule :: Rule ComponentSet
hypothesesConclusionCriticalRule = 
   describe "derive the hypotheses conclusion from the critical conclusion" $
   makeRule "component.hypotheses-conclusion-critical" f 
 where
   f :: ComponentSet -> Maybe ComponentSet
   f cs = do
      guard (cs `doesNotContain` ConclusionHypotheses)
      concl <- inferConclusionCritical cs
      let rejhyp = if concl then RejectH0 else DontRejectH0
      return $ append ConclusionHypotheses (CChoice (RejectionHypotheses rejhyp)) cs

hypothesesConclusionPValueRule :: Rule ComponentSet
hypothesesConclusionPValueRule = 
   describe "derive the hypotheses conclusion from the p-value" $
   makeRule "component.hypotheses-conclusion-pvalue" f
 where
   f :: ComponentSet -> Maybe ComponentSet
   f cs = do
      guard (cs `doesNotContain` ConclusionHypotheses)
      rel <- inferConclusionPValue cs
      let rejhyp = if relationType rel == LessThanOrEqualTo then RejectH0 else DontRejectH0
      return $ append ConclusionHypotheses (CChoice (RejectionHypotheses rejhyp)) cs

addStandardErrorSigma :: Rule ComponentSet
addStandardErrorSigma = 
   describe "derive standard error from population standard deviation and sample size" $ 
   makeRule "component.standard-error-sigma" f 
 where
   f :: ComponentSet -> Maybe ComponentSet
   f cs = do
      guard (cs `doesNotContain` StandardError)
      n   <- matchM doubleView =<< getExpr SampleSize cs
      psdev <- matchM doubleView =<< getRhsExpr PopulationSdev cs
      let se = Var "sigmaM" .==. toExpr (psdev / sqrt n)
      return $ append StandardError (CRelation se) cs
      
addStandardErrorSD :: Rule ComponentSet
addStandardErrorSD = 
   describe "derive standard error from sample standard deviation and sample size" $ 
   makeRule "component.standard-error-sd" f 
 where
   f :: ComponentSet -> Maybe ComponentSet
   f cs = do
      guard (cs `doesNotContain` StandardError)
      n   <- matchM doubleView =<< getExpr SampleSize cs
      sdev <- matchM doubleView =<< getRhsExpr SampleSdev cs
      let se = Var "SEM" .==. toExpr (sdev / sqrt n)
      return $ append StandardError (CRelation se) cs