Ridings

In [1]:
%matplotlib inline
from matplotlib.projections.polar import PolarAxes
from matplotlib.projections import register_projection
from IPython.display import display
from elections import models as election_models
from parliaments import models as parliament_models
from proceedings import models as proceeding_models
from django_extensions.db.fields.json import JSONDict
from collections import OrderedDict
from federal_common.sources import EN, FR
from django.db.models.base import ModelBase
from IPython.display import HTML
import pandas as pd
import seaborn as sns
import matplotlib.ticker as ticker
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import math
import numpy as np
import os

def columns_reorder_left(dataframe, columns):
    unmentioned = [column for column in dataframe.columns if column not in columns]
    dataframe = dataframe[columns + unmentioned]
    return dataframe

def columns_reorder_after(dataframe, pairs):
    columns = list(dataframe.columns)
    for left, right in pairs.items():
        columns.remove(right)
        columns.insert(columns.index(left) + 1, right)
    return dataframe[columns]

dataframe_json_mapper = {
    "government_party": "Library of Parliament, History of Federal Ridings",
    "party": "Library of Parliament, History of Federal Ridings",
    "province": "Library of Parliament, Province / Territory File",
    "riding": "House of Commons, Constituencies",
    "parliamentarian": "Library of Parliament, Parliament File",
}

def get_dataframe(qs, mapping, index=None):
    mapping = OrderedDict(mapping)
    qs = qs.objects.all() if isinstance(qs, ModelBase) else qs
    dataframe = pd.DataFrame(
        {
            k: v.get(EN, {}).get(dataframe_json_mapper[k.split("__")[-2]], "") if isinstance(v, dict) else v
            for k, v in row_dict.items()
        }
        for row_dict in qs.values(*[
            key[0] if isinstance(key, tuple) else key
            for key in mapping.keys()
        ])
    )
    dataframe = columns_reorder_left(dataframe, list(mapping.keys()))
    dataframe.rename(columns=mapping, inplace=True)
    if index:
        dataframe = dataframe.set_index(index)
    dataframe = dataframe.sort_index()
    return dataframe

def select_by_index(dataframe, indexes):
    indexer = [slice(None)] * len(dataframe.index.names)
    for key, value in indexes.items():
        indexer[dataframe.index.names.index(key)] = value
    if len(indexer) > 1:
        return dataframe.loc[tuple(indexer), :]
    else:
        return dataframe.loc[tuple(indexer)]

def display_toggler(button="Click to toggle"):
    return HTML("""
        <script>
            function toggle_cell(event) {{
                $(event ? event.target : ".toggler")
                    .parents(".output_wrapper")
                    .prev()
                    .toggle(event ? undefined : false)
            }}
            $(document).ready(setTimeout(toggle_cell, 0))
        </script>
        <p><button class="toggler" onClick="javascript:toggle_cell(event)">{}</button></p>
    """.format(button))

display_toggler("Click to toggle the display the initializing scripts")
Out[1]:

In [2]:
elections = get_dataframe(election_models.GeneralElection, {
    "number": "Election",
    "date": "Date",
    "population": "Population",
    "registered": "Registered",
    "ballots_total": "Ballots (Total)",
    "parliament__seats": "Seats",
    "parliament__government_party__names": "Party",
}, ["Election"])
elections['Date'] = pd.to_datetime(elections['Date'])  # TODO: Why isn't this happening automatically?
ridings = get_dataframe(election_models.ElectionRiding.objects.filter(general_election__number=42), {
    "general_election__number": "Election",
    "general_election__date": "Election: Date",
    "riding__province__names": "Province",
    "riding__names": "Riding",
    "population": "Population",
    "registered": "Registered",
    "ballots_rejected": "Ballots (Rejected)"
}, ["Election", "Province", "Riding"])
ridings['Election: Date'] = pd.to_datetime(ridings['Election: Date'])
candidates = get_dataframe(election_models.ElectionCandidate.objects.filter(election_riding__general_election__number=42), {
    "election_riding__general_election__number": "Election",
    "election_riding__general_election__date": "Election: Date",
    "election_riding__riding__province__names": "Province",
    "election_riding__riding__names": "Riding",
    "name": "Candidate",
    "party__names": "Party",
    "party__color": "Party: Color",
    "election_riding__population": "Riding: Population",
    "election_riding__registered": "Riding: Registered",
    "ballots": "Ballots",
    "elected": "Elected",
    "acclaimed": "Acclaimed",
}, ["Election", "Province", "Riding", "Candidate"])
candidates['Election: Date'] = pd.to_datetime(candidates['Election: Date'])

# Some ridings historically had two seats (Halifax, Victoria, etc). For the purposes of
# calculating voter turnout, we need to define a fractional ballot where a voter's single
# ballot is split in half between two candidates.
ridings = ridings.join(candidates[candidates["Elected"] | candidates["Acclaimed"]].reset_index().groupby(ridings.index.names)["Candidate"].count().to_frame())
ridings = ridings.rename(columns={"Candidate": "Seats"})
candidates = candidates.reset_index().set_index(ridings.index.names).join(ridings["Seats"]).reset_index().set_index(candidates.index.names)
candidates["Ballots (Fractional)"] = candidates["Ballots"] / candidates["Seats"]
del candidates["Seats"]

parties = candidates.reset_index().groupby(["Election", "Party"]).agg({
    "Election: Date": "first",
    'Candidate': 'count',
    'Ballots (Fractional)': 'sum',
    'Elected': 'sum',
    'Acclaimed': 'sum',
    'Party: Color': 'first',
}).rename(columns={
    'Candidate': 'Candidates',
    'Ballots (Fractional)': 'Ballots',
    "Party: Color": "Color",
})
parties["Seats"] = parties["Elected"] + parties["Acclaimed"]
del parties["Elected"]
del parties["Acclaimed"]

# Augment elections with winning party data
elections = elections.reset_index().set_index(parties.index.names).join(parties, rsuffix=" (Party)").reset_index().set_index(elections.index.names).rename(columns={
    'Candidates': 'Party: Candidates',
    'Ballots': 'Party: Ballots',
    'Color': 'Party: Color',
    'Seats (Party)': "Party: Seats",
})
del elections["Election: Date"]
elections = columns_reorder_after(elections, {
    "Seats": "Party",
})

# Copy valid ballots per candidate up to the ridings level
ridings = ridings.join(candidates.reset_index().groupby(ridings.index.names)["Ballots (Fractional)"].sum().to_frame())
ridings = ridings.rename(columns={"Ballots (Fractional)": "Ballots (Valid)"})
ridings = columns_reorder_after(ridings, {"Ballots (Valid)": "Ballots (Rejected)"})

# Copy winning candidate up to the ridings level
ridings_winning_parties = candidates[candidates["Elected"] == True].reset_index().set_index(ridings.index.names)
ridings = ridings.join(ridings_winning_parties[["Candidate", "Party", "Party: Color", "Ballots"]])
ridings = ridings.rename(columns={
    "Candidate": "Elected: Name",
    "Ballots": "Elected: Ballots",
    "Party": "Elected: Party",
    "Color": "Elected: Color",
})

# Copy valid ballots per riding back down to the ridings level
candidates = candidates.reset_index().set_index(ridings.index.names).join(
    ridings.reset_index().groupby(ridings.index.names).agg({"Ballots (Valid)": "first"})
).reset_index().set_index(candidates.index.names)
candidates = columns_reorder_after(candidates, {"Riding: Registered": "Ballots (Valid)"})
candidates = candidates.rename(columns={
    "Ballots (Valid)": "Riding: Ballots (Valid)",
})

# Copy valid ballots per riding up to the elections level
elections_ballot_sums = ridings.reset_index().groupby(ridings.index.names).agg({"Ballots (Valid)": "first"}).reset_index().groupby(["Election"])["Ballots (Valid)"].sum().to_frame()
elections = elections.join(elections_ballot_sums)
elections = columns_reorder_after(elections, {"Ballots (Total)": "Ballots (Valid)"})

# Augment parties with election level data
elections["Party: Ballot %"] = elections["Party: Ballots"] / elections["Ballots (Valid)"]
elections["Party: Seat %"] = elections["Party: Seats"] / elections["Seats"]
parties["Seat %"] = parties["Seats"] / elections["Seats"]
parties["Vote %"] = parties["Ballots"] / elections["Ballots (Valid)"]
parties["Seat % - Vote %"] = parties["Seat %"] - parties["Vote %"]
parties["Seat % / Vote %"] = parties["Seat %"] / parties["Vote %"]
candidates["Ballot %"] = candidates["Ballots"] / candidates["Riding: Ballots (Valid)"]
ridings_winning_parties = candidates[candidates["Elected"] == True].reset_index().set_index(ridings.index.names)
ridings = ridings.join(ridings_winning_parties[["Ballot %"]])
ridings = ridings.rename(columns={"Ballot %": "Elected: Ballot %"})

display_toggler("Click to toggle the display the data initialization")
Out[2]:

In [10]:
from chorogrid import Colorbin, Chorogrid
from range_key_dict import RangeKeyDict

# Merge Chorogrid's riding data (square long/lat) with our own data
df = pd.read_csv('chorogrid/databases/canada_federal_ridings.csv')
ridings_prime = ridings.reset_index()
ridings_prime["Riding"] = ridings_prime["Riding"].replace( 
    ["Bow River", "Ville-Marie—Le Sud-Ouest—Île-des-Soeurs", "Burnaby North—Seymour", "Beauport—Côte-de-Beaupré—Île d'Orléans—Charlevoix", "Coquitlam—Port Coquitlam"],
    ["Bow river", "Ville-Marie—Le Sud-Ouest—Île-des-Sœurs", "Burnaby—Seymour", "Beauport—Côte-de-Beaupré—Île d’Orléans—Charlevoix", "Coquitlam-Port Coquitlam"]
)
df = df.merge(ridings_prime, left_on="federal_electoral_district", right_on="Riding")

# Determine a good colouration of the chorogrid (3 colours below 50%, 3 above)
tertiles_low = df[df["Elected: Ballot %"] < 0.5]["Elected: Ballot %"].quantile([0/3, 1/3, 2/3, 3/3]).values
tertiles_high = df[df["Elected: Ballot %"] > 0.5]["Elected: Ballot %"].quantile([0/3, 1/3, 2/3, 3/3]).values
color_range = (
    ((0.0, tertiles_low[1]), '#542788'),
    ((tertiles_low[1], tertiles_low[2]), '#998ec3'),
    ((tertiles_low[2], 0.5), '#d8daeb'),
    ((0.5, tertiles_high[1]), '#fee0b6'),
    ((tertiles_high[1], tertiles_high[2]), '#f1a340'),
    ((tertiles_high[2], 1.0), '#b35806'),
)
range_key_dict = RangeKeyDict(dict(color_range))
canada_riding_colors = [range_key_dict[x] for x in list(df["Elected: Ballot %"])]

# Generate the per-riding choropleth
cg_ridings = Chorogrid('chorogrid/databases/canada_federal_ridings.csv', list(df.district_code), canada_riding_colors, 'district_code')
cg_ridings.set_title('2015 federal election, MP win threshold', font_dict={'font-size': 19})
cg_ridings.set_legend([c[1] for c in color_range], ["{}% to {}%".format(int(c[0][0] * 100), int(c[0][1] * 100)) for c in color_range], width=5)
cg_ridings.draw_squares(spacing_dict={'cell_width': 15, 'roundedness': 0}, font_dict={'fill-opacity': 0})

# Generate the per-province borders
df_prov = pd.read_csv('chorogrid/databases/canada_provinces.csv')
canada_prov_colors = ['none' for x in range(len(df_prov.province))]
cg_prov = Chorogrid('chorogrid/databases/canada_provinces.csv', df_prov.province, canada_prov_colors, 'province')
cg_prov.draw_multisquare(font_dict={'fill-opacity': 0}, spacing_dict={'margin_bottom': 250, 'cell_width': 16, 'stroke_width': 2, 'stroke_color': '#000000'})
cg_ridings.done_and_overlay(cg_prov, show=True)
48001 48007 48012 48022 48034 48004 48008 48010 48020 48021 48005 48011 48018 48024 48026 48029 48031 48033 48003 48006 48009 48014 48015 48023 48028 48030 48002 48013 48016 48017 48019 48025 48027 48032 59009 59010 59018 59026 59027 59041 59025 59031 59037 59039 59011 59028 59034 59036 59040 59019 59030 59033 59038 59042 59003 59007 59035 59002 59008 59012 59016 59001 59021 59022 59023 59032 59004 59005 59006 59014 59017 59013 59015 59020 59024 59029 46001 46002 46007 46004 46011 46013 46014 46003 46005 46009 46012 46006 46008 46010 13003 13005 13010 13001 13006 13007 13008 13002 13004 13009 10004 10005 10003 10002 10001 10006 10007 12011 12006 12009 12003 12005 12007 12002 12004 12008 12001 12010 61001 62001 35042 35046 35091 35105 35117 35052 35083 35092 35106 35107 35112 35113 35116 35002 35008 35014 35026 35032 35040 35045 35048 35053 35103 35010 35011 35012 35017 35022 35051 35057 35080 35099 35100 35005 35009 35016 35025 35043 35047 35058 35062 35069 35111 35004 35013 35015 35029 35063 35065 35073 35082 35104 35121 35003 35030 35035 35054 35060 35072 35087 35118 35119 35120 35020 35027 35033 35034 35056 35059 35061 35070 35084 35115 35018 35023 35028 35037 35038 35049 35055 35086 35090 35021 35024 35036 35039 35041 35078 35081 35085 35093 35001 35031 35064 35068 35071 35079 35096 35101 35110 35114 35019 35044 35067 35075 35076 35077 35094 35097 35108 35109 35006 35007 35066 35074 35089 35095 35102 35050 35088 35098 11003 11004 11002 11001 24002 24027 24030 24057 24005 24013 24031 24035 24038 24029 24048 24050 24063 24033 24040 24056 24062 24065 24075 24003 24004 24012 24055 24068 24076 24078 24009 24015 24036 24052 24064 24069 24070 24074 24001 24021 24028 24032 24037 24053 24054 24058 24060 24071 24014 24019 24022 24025 24034 24039 24042 24045 24046 24049 24067 24077 24008 24011 24017 24020 24024 24043 24044 24059 24066 24010 24016 24018 24041 24051 24061 24072 24073 24006 24007 24023 24047 24026 47001 47002 47004 47010 47007 47011 47012 47003 47005 47006 47009 47008 47013 47014 60001 60% to 100% 54% to 60% 50% to 54% 45% to 50% 39% to 45% 0% to 39% 2015 federal election, MP win threshold NL PE NS NB QC ON MB SK AB BC YT NT NU