Interactive Election Visualisations in Python with Altair
By: Tom Titcombe
Tags: Politics, Python, Visualization, Data Science, Data Visualization

In a recent post, I showed how we can generate hexmaps with matplotlib to visualise elections. While these maps are a great demonstration of how to convey geographic information, they are not without flaws. For example, it can be difficult to identify which constituency is which from the static image, because of the manipulations applied to constituencies to make them of equal size.

In this post we will use Altair, a declarative Python plotting library, to deliver improved, interactive versions of the hexmaps. You can follow along with this post using the notebook on my GitHub.

First, let us remind ourselves what we were left with after the last post.

Altair

Altair is a declarative statistical visualization library for Python

Essentially, this means we define our data and the output (what the chart looks like) and Altair will do all the manipulations which take us from input to output.

Compare that to packages like matplotlib, the staple of Python visualisation: for the hexmap we had to explicitly specify the coordinates, rotation, size and colour of each hexagon.

While matplotlib provides more control over the final state of graphs, implementing even basic customisation usually takes far more time, knowledge and effort than with Altair (and certainly more than people are willing to give it).

Altair is built on Vega, a JavaScript plotting library. To render the interactive plots, you need a frontend application such as Jupyter lab.

Altair can be installed via pip ( pip install -U altair vega_datasets jupyterlab ) or conda ( conda install -c conda-forge altair vega_datasets jupyterlab ).

Data

In this post we will be visualising the makeup of the UK parliament immediately after the 2017 general election. This data is freely available from the gov.uk website. This data has the constituency name, Constituency, and the name of the winning party, Party.

We have assigned hex coordinates, p and q, of constituencies using ODILeeds’ fantastic hexmap.

A Basic Map

It should be simple to generate the basic hex map in Altair due its declarative style. Let’s put that to the test.

import altair as alt alt.Chart(data)

.mark_circle()

.encode(

x="q",

y="r",

color=alt.value("lightgray"),

size=alt.value(50),

)

It took many lines (and many hours of StackOverflowing) to get the basic matplotlib map working; for Altair, it’s seven lines.

In Altair, we first create a Chart object which contains the data we wish to use. We specify what type of marks to make on the chart with mark_circle() . Then we encode the features of the mark with aspects of the data they should be represented by, in this case we set the x values to come from the data column q and the y values to come from the data column r. Additionally, we have set the size of each circle to a constant value using size=alt.value(50) .

After defining these few, simple rules, Altair does all the dirty work and produces a brilliant plot.

Granted, the colour is uninformative and dull at the moment. To remedy this, we need to provide Altair a recipe for linking the party names to a specific colour.

parties = ["Conservative", "Labour", "Lib Dem", "Green", ...]

party_colours = ["darkblue", "red", "orange", "green", ...] #etc. colours_obj = alt.Color(

"Party:N",

scale=alt.Scale(domain=parties,

range=party_colours)

) alt.Chart(data)

.mark_circle()

.encode(

x="q",

y="r",

color=colours_obj,

size=alt.value(50),

)

The alt.Color object tells Altair to get colours from the Party column in the data. However, the parties are strings and therefore cannot be interpreted as a colour; alt.Scale translates all of the parties in the domain parameter into the corresponding colour in the range parameter. This is in direct contrast to matplotlib where a colour must be defined for every object.

One of the negative properties of the matplotlib graph was that it was legend-less: I could not figure out how to produce a legend which could link the colours of the iteratively drawn hexagons with a party name. Knowing matplotlib, even if it were possible to add a label to each hexagon, the resulting legend would have an entry for every single hexagon, paying no notice of repetitions.

Perhaps unsurprisingly, legends are a trivial matter in Altair — the Color object generates one automatically.

Basic Interactivity

At this point we have generated a map which supersedes the matplotlib version. However, it still suffers from the issue of the warped geographic boundaries being difficult to identify. Unless you’re looking for a coastal constituency, the chances are you will not be able to locate it.

We could add text to each hexagon denoting the constituency name, but that will either be too difficult to read or make the plot too large to appreciate. What we need to is to take the static graph and make it interactive. Fortunately for the purposes of this blog post, Altair excels at interactivity.

alt.Chart(data)

.mark_circle()

.encode(

x="q",

y="r",

color=colours_obj,

size=alt.value(50),

tooltip=["Constituency:N"],

)

Altair has integrated support for tooltips in a single additional line of code. This is a powerful feature than can instantly elevate the usability of a graph.

Oxford: definitely not north of Twickenham

Even More Interactivity

Of course, Altair has far more interactivity to offer than tooltips. We will sample some of these additional offerings by getting the chart to highlight all constituencies of a certain party on a mouse-click. This behaviour is useful to help identify the distribution of a party throughout the land in what could otherwise be an overwhelming display of colour.

We start by creating a selection object and telling it that the important information we care about is the Party of the selection. We add this interaction selection element to the chart with add_selection .

To get changing behaviour, we need to replace the static mark parameters with conditional ones. alt.condition takes a condition, a value to display when the condition is met, and a value to display when it is not. Note that the condition in this case is the selection object. The condition is met when the selection object’s Party parameter is the same as the Party parameter of a mark Altair is attempting to display.

selector = alt.selection_single(empty='all', fields=['Party']) colours_condition = alt.condition(selector,

colours_obj,

alt.value("lightgray") alt.Chart(data)

.mark_circle()

.encode(

x="q",

y="r",

color=colours_condition,

size=alt.value(50),

tooltip=["Constituency:N"],

).add_selection(selector)

This example highlights how much pain Altair takes away from complex plotting. We don’t even have to use conditional code to get conditional behaviour.

Combining Charts

We end this post by showing how multiple charts can be displayed together. We will add a bar graph showing the total number of MPs of each party - information which is difficult to draw from the throng of circles and colour we currently have.

First, we create the bars. This is a similar process to before, except we are using mark_bars to signal to Altair that, shockingly, we wish to mark bars on the chart object. Notice also that we assign this object to a variable; we will do the same to our map chart.

The y value for the bars is the count of occurrences of each Party. In other plotting libraries, we would be required to calculate these before plotting; Altair will do this for you with the count() aggregation.

We will also add a horizontal line to the graph to show the point at which a party has a majority. Unfortunately this is an area where Altair stumbles: simply giving the line the y value of alt.value(325) * would not produce the correct result. Instead, we must add the threshold value to our data object and tell Altair to use it.

*Non-technical side-note: 325 is technically the threshold for a majority in the House of Commons, but due to “features” of our democratic system, such as the Speaker and Sinn Fein, the practical requirement is slightly lower.

df["threshold"] = 325



bars = base.mark_bar().encode(

x="Party:N",

y=alt.Y("count()", title="Number of MPs"),

color=colours_condition

)



majority = base.mark_rule(color="black", strokeDash=[1, 1]).encode(

y="threshold:Q",

size=alt.value(3)

) map | (bars + majority)

Altair chart objects can easily be combined, stacked or concatenated. The | operator horizontally stacks chart objects, & vertically stacks them, and + adds objects to the same chart.

Next Steps

In this quick tour of Altair we created powerful visualisations which would have taken far more time to produce in other libraries, if they are even possible.

Despite the ease with which Altair handles custom and complex plots, it is a woefully underutilised package. Its interactivity and hands-off approach to plotting could, and should, make it the go-to plotting library for Python users.

In a later post we will expand upon these plots to visualise how the UK’s political geography may evolve under different voting systems.

You can find the code used in this post on my GitHub.