About 47.6 million people were registered as entitled to vote in the United Kingdom’s general election on 12 December 2019 and about 32.1 million did so. I wanted to use Haskell to view the outcome.
Results
The House of Commons Library publishes the results of the election as two comma-separated values (CSV) files, the smaller by constituency (including the identity of the winning candidate) and the larger by all candidates.
I needed only the smaller file, which provided for each constituency the number of votes for each of the 12 largest political parties (grouping other votes under ‘Other’), the number of invalid votes and the size of the electorate.
Four of the 3,320 candidates did not identify as male or female, although none of them were elected as a member of parliament (MP):
1 2 3 4 5 |
-- | Representing the gender identity of a person. data Gender = Male | Female | NonBinary deriving (Eq, Show) |
I used the type Party
to represent the party affiliations of the candidates, with the constructors defined in the same order as the parties were listed in the CSV:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
-- | Representing the political party affiliations of candidates in the United -- Kingdon's 12 December 2019 general election. data Party = Con -- ^ Conservative and Unionist Party | Lab -- ^ Labour Party (including Labour and Co-operative Party -- candidates) | LD -- ^ Liberal Democrats | Brexit -- ^ The Brexit Party | Green -- ^ Green Party | SNP -- ^ Scottish National Party (SNP) | PC -- ^ Plaid Cymru - The Party of Wales | DUP -- ^ Democratic Unionist Party - D.U.P. | SF -- ^ Sinn Fein | SDLP -- ^ SDLP (Social Democratic & Labour Party) | UUP -- ^ Ulster Unionist Party | Alliance -- ^ Alliance - Alliance Party of Northern Ireland | Other -- ^ All other candidates (including the Speaker seeking -- re-election) deriving (Eq, Ix, Ord, Read, Show) |
The result for each constituency could be a hold for a party or a gain from another party:
1 2 3 4 5 6 7 8 9 |
-- Representing results from the perspective of political parties. data PartyResult = Hold Party | Gain Party Party -- ^ Gain for the first party from the second party deriving (Eq, Show) winner :: PartyResult -> Party winner (Hold p) = p winner (Gain p _) = p |
I used an array indexed by Party
to represent valid votes by party.
1 2 |
-- | Representing valid votes by party type Votes = Array Party Int |
I used the type ConstituencyResult
to represent the results of constituencies:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
-- | Representing results for constituencies data ConstituencyResult = ConstituencyResult { declarationTime :: LocalTime -- ^ According to the British Broadcasting Corporation , mp :: Person , result :: PartyResult , electorate :: Int , votes :: Votes , invalidVotes :: Int , otherWinnerVotes :: Int -- ^ If the winning candidate was not affilated with one of the 12 largest -- parties } deriving (Eq, Show) |
I used a (strict) hash map to represent the results of the election:
1 2 3 4 5 |
-- | Representing constituencies. type Constituency = Text -- | Representing the results of the 12 December 2019 general election type GE2019 = HashMap Constituency ConstituencyResult |
The package cassava
provides a Haskell library to parse CSV files. The type of each field needs to be an instance of FromField
and the type of each record needs to be an instance of FromRecord
. For example, to parse data such as “Con hold” or “Con gain from Lab Coop”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
instance FromField PartyResult where parseField s | l == 0 = fail errMsg | otherwise = do p1 <- parseField (ws !! 0) :: Parser Party if l > 1 && (ws !! 1) == "hold" then pure (Hold p1) else if l > 3 && (ws !! 1) == "gain" && (ws !! 2) == "from" then do p2 <- parseField (ws !! 3) :: Parser Party pure (Gain p1 p2) else fail errMsg where ws = BC.split ' ' s l = length ws errMsg = "Not recognised as PartyResult: " ++ BC.unpack s |
or to parse data such as “2019-12-13 02:30:00”:
1 2 3 |
instance FromField LocalTime where parseField s = parseTimeM False defaultTimeLocale "%Y-%m-%d %H:%M:%S" (BC.unpack s) |
The hash map is made from the Vector
of records. The records must capture the constituency (the hash map key) and its result. This uses the RecordWildCards
language extension:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
-- | Representing constituencies and their results data ConstituencyAndResult = ConstituencyAndResult { constituency :: Text , constituencyResult :: ConstituencyResult } deriving (Eq, Show) instance FromRecord ConstituencyAndResult where parseRecord v = do constituency <- v .! 0 declarationTime <- v .! 7 firstName <- v .! 8 lastName <- v .! 9 gender <- v .! 10 let mp = Person{..} result <- v .! 11 electorate <- v .! 14 invalidVotes <- v .! 16 votes' <- mapM (v .!) [18 .. 30] let votes = listArray (Con, Other) votes' otherWinnerVotes <- v .! 31 let constituencyResult = ConstituencyResult{..} pure $ ConstituencyAndResult{..} mkGE2019 :: Vector ConstituencyAndResult -> GE2019 mkGE2019 = V.foldl' (\m (ConstituencyAndResult k v) -> HM.insert k v m) HM.empty |
Cartogram
UK parliamentary constituencies vary considerably by area or size of electorate. The outcome of the general election can be viewed using a cartogram where each constituency is represented by a regular hexagon of the same size – a ‘hex map’.
Versions of such hex maps are published by ODI Leeds and Ben Flanagan of ERSI. I used the latter.
The ERSI hex map is provided as an ERSI Shapefile. That specification is overwrought for a hex map. ODI Leeds defines a simple JSON (JavaScript Object Notation) format for hex maps – HexJSON.
I used Matthew Bloch’s mapshaper tool to view the ERSI Shapefile and export as a file in the GeoJSON format. With the assistance of the geojson
package (GeoJSON) and the aeson
package (JSON), I wrote a utility to read the contents of the GeoJSON file, transform the features to a HexJSON format and write the transformed data to a JSON file.
The transformation calculated the centre of each feature (specifically, the average of the vertices) and, allowing for rounding differences, the minimum horizontal and vertical distances between centres. This allowed the centres to be mapped to a hexagonal grid.
Diagrams
I used the diagrams
project to render the hex maps as a Scalable Vector Graphics (SVG) image.
The function Diagrams.TwoD.Text.text
returns a primitive text diagram but that diagram takes up no space. I needed to know the size of text in legends, so I used the SVGFonts package and the function textSVG
, which returns a Path
.
The key functions that produce a hex map are styleHexMap
and styleHexMap'
, where the former is the latter specialised to Constituency
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
styleHexMap :: HexMap p -> (Constituency -> HexStyle) -> HexMap HexStyle styleHexMap hm s = styleHexMap' hm s' where s' key _ _ = s key styleHexMap' :: HexMap p -> (Constituency -> QR -> p -> HexStyle) -> HexMap HexStyle styleHexMap' (HexMap coord hm) s = HexMap coord hm' where hm' = mapWithKey s' hm s' key (Hex qr props) = Hex qr (s key qr props) |
An example of a function that yields Constituency -> HexStyle
is:
1 2 3 4 5 6 7 |
winners :: GE2019 -> Constituency -> HexStyle winners ge2019 key = mempty # fc c # lc black # lw 0.5 where cr = fromJust $ HM.lookup key ge2019 r = result cr w = winner r c = partyColour w |