1 {-# LANGUAGE ScopedTypeVariables #-}
2 -- | This module implements the composition of a Majority Judgment
3 -- from a tree of Majority Judgments: for the same question,
4 -- the same choices, the same judges and the same grades.
5 -- In that tree, a parent judgment is formed by the aggregation of its children judgments,
6 -- where a child judgment contributes only for a percentage of the parent judgment.
7 module Majority.Section where
9 import Control.Applicative (Applicative(..), Alternative(..))
11 import Data.Either (Either(..))
12 import Data.Eq (Eq(..))
13 import Data.Foldable (Foldable(..), any)
14 import Data.Function (($), (.))
15 import Data.Functor ((<$>), (<$))
16 import Data.Hashable (Hashable(..))
17 import Data.Maybe (Maybe(..), isNothing, maybe, fromMaybe)
18 import Data.Ord (Ord(..))
19 import Data.Traversable (Traversable(..))
20 import Data.Tree as Tree
21 import Prelude (Num(..), Fractional(..), toRational)
22 import Text.Show (Show(..))
23 import qualified Data.HashMap.Strict as HM
24 import qualified Data.HashSet as HS
25 import qualified Data.List as List
26 import qualified Data.Map.Strict as Map
31 -- | An opinion of a 'judge' about a 'choice' at a specific section 'Tree.Node'.
34 { sectionShare :: Maybe Share
35 -- ^ A 'Share' within the parent 'Tree.Node'
36 -- (defaulting to a 'Share' computed as the remaining 'Share' to reach 1
37 -- divided by the number of defaulted 'Share's).
38 , sectionGrade :: Maybe grade
39 -- ^ A 'grade' attributed to the current 'Tree.Node'
40 -- (defaulting to the 'grade' set on an ancestor 'Tree.Node' if any,
41 -- or the |judge|'s default grade).
44 -- ** Type 'SectionByJudge'
45 type SectionByJudge judge grade = HM.HashMap judge (Section grade)
47 -- ** Type 'SectionNode'
48 -- | Node value of a 'Tree' holding a 'Section', per 'judge', per 'choice'.
49 data SectionNode choice judge grade
51 { sectionNodeShare :: Maybe Share
52 -- ^ A default 'sectionShare' for judges not specifying their own.
53 , sectionByJudgeByChoice :: HM.HashMap choice (SectionByJudge judge grade)
56 -- * Type 'ErrorSection'
57 data ErrorSection choice judge grade
58 = ErrorSection_unknown_choices (HS.HashSet choice)
59 -- ^ When some 'choice's are not known.
60 | ErrorSection_unknown_judges (HM.HashMap choice (HS.HashSet judge))
61 -- ^ When some 'judge's are not known.
62 | ErrorSection_invalid_shares (HM.HashMap choice (HM.HashMap judge [Share]))
63 -- ^ When at least one of the 'Share's is not positive, or when their sum is not 1.
66 -- | @'opinionsBySection' cs js ss@ computes the 'Opinions' of the given 'Judges' @js@ about the given 'choice's @cs@,
67 -- from the 'grade' (specified or omitted) attributed to 'choice's
68 -- and the 'Share's (specified or omitted) attributed to 'Tree.Node'
69 -- in given 'Tree' @ss@.
71 forall choice judge grade.
79 Tree (SectionNode choice judge grade) ->
80 Either (ErrorSection choice judge grade)
81 (Tree (OpinionsByChoice choice judge grade))
82 opinionsBySection cs js = go ((singleGrade <$> js) <$ HS.toMap cs)
84 go :: HM.HashMap choice (HM.HashMap judge (Distribution grade)) ->
85 Tree (SectionNode choice judge grade) ->
86 Either (ErrorSection choice judge grade)
87 (Tree (OpinionsByChoice choice judge grade))
88 go defaultDistJC (Tree.Node (SectionNode _sectionNodeShare currOpinJC) childOpinJCS) =
89 -- From current 'Tree.Node''s value.
90 let currDistJC :: HM.HashMap choice (HM.HashMap judge (Distribution grade)) =
91 -- Collect the 'Distribution' of current 'Tree.Node',
92 -- and insert default 'Distribution'
93 -- for each unspecified 'judge'
94 -- of each (specified or unspecified) 'choice'.
96 HM.mapWithKey (\choice ->
97 let defaultDistJ = defaultDistJC HM.!choice in
98 HM.mapWithKey (\judge ->
99 maybe (defaultDistJ HM.!judge) singleGrade .
103 HM.unionWith HM.union
107 -- From children 'Tree.Node's.
108 let maybeChildShareSJC :: HM.HashMap choice (HM.HashMap judge [Maybe Share]) =
109 -- Collect the (specified or explicitely (with 'Nothing') unspecified) 'Share's by section,
110 -- and insert all unspecified 'Share's when a 'choice' or a 'judge' is unspecified.
111 foldr (\(Tree.Node SectionNode{sectionNodeShare, sectionByJudgeByChoice} _) ->
112 let defaultChildShareSJC = ([sectionNodeShare] <$ js) <$ defaultDistJC in
113 let specifiedChildShareSJC =
114 (<$> sectionByJudgeByChoice) $
115 (pure . (<|> sectionNodeShare) . sectionShare <$>) in
116 -- Fusion specified 'choice's into accum.
117 HM.unionWith (HM.unionWith (List.++)) $
118 -- Add default 'Share' for this 'Tree.Node',
119 -- for each unspecified 'judge' of specified and unspecified 'choice'.
120 HM.unionWith HM.union
121 specifiedChildShareSJC
122 defaultChildShareSJC)
126 let childShareSJC :: HM.HashMap choice (HM.HashMap judge [Share]) =
127 -- Replace unspecified shares of each child 'Tree.Node'
128 -- by an even default: the total remaining 'Share'
129 -- divided by the number of unspecified 'Share's.
130 (<$> maybeChildShareSJC) $ \maybeShareSJ ->
131 (<$> maybeShareSJ) $ \maybeShareS ->
132 let specifiedShare = sum $ fromMaybe 0 <$> maybeShareS in
133 let unspecifiedShares = toRational $ List.length $ List.filter isNothing maybeShareS in
134 let defaultShare = (1 - specifiedShare) / unspecifiedShares in
135 fromMaybe defaultShare <$> maybeShareS
138 -- Test for unknown choices.
139 _ | unknownChoices <- currOpinJC`HM.difference`defaultDistJC
140 , not $ null unknownChoices ->
141 Left $ ErrorSection_unknown_choices $
142 HS.fromMap $ (() <$) $ unknownChoices
143 -- Test for unknown judges.
144 _ | unknownJudgesC <- HM.filter (not . null) $
145 HM.intersectionWith HM.difference
148 , not $ null unknownJudgesC ->
149 Left $ ErrorSection_unknown_judges $
150 HS.fromMap . (() <$) <$> unknownJudgesC
151 -- Handle no child 'Tree.Node':
152 -- current 'Distribution' is computed from current 'Tree.Node''s value ('currOpinJC')
153 -- and inherited default 'Distribution' ('defaultDistJC').
154 [] -> Right $ Tree.Node currDistJC []
155 -- Test for invalid shares.
156 _ | invalidSharesJC <-
157 HM.filter (not . null) $
158 HM.filter (\ss -> any (< 0) ss || sum ss /= 1)
160 , not $ null invalidSharesJC ->
161 Left $ ErrorSection_invalid_shares invalidSharesJC
162 -- Handle children 'Tree.Node's:
163 -- current 'Opinions' is computed from the 'Opinions' of the children 'Tree.Node's.
165 distJCS :: [Tree (HM.HashMap choice (HM.HashMap judge (Distribution grade)))] <-
166 traverse (go $ currDistJC) childOpinJCS
167 -- 'grade's set at current 'Tree.Node' ('currDistJC')
168 -- become the new default 'grade's ('defaultDistJC')
169 -- within its children 'Tree.Node's.
170 let distSJC :: HM.HashMap choice (HM.HashMap judge [Distribution grade]) =
171 -- Collect the 'Distribution's by section.
173 let newDistSJC = (pure <$>) <$> rootLabel distJC in
174 HM.unionWith (HM.unionWith (List.++)) newDistSJC)
177 let distJC :: HM.HashMap choice (HM.HashMap judge (Distribution grade)) =
178 -- Compute the current 'Distribution' by scaling (share *) and merging (+)
179 -- the children 'Distribution's.
180 HM.mapWithKey (\choice ->
181 let childShareSJ = childShareSJC HM.!choice in
182 HM.mapWithKey (\judge ->
183 let childShareS = childShareSJ HM.!judge in
186 (\share dist -> (share *) <$> dist)
189 Right $ Tree.Node distJC distJCS