Python visualization demystified

Radar Charts in Matplotlib

A quick tutorial on radar charts in Matplotlib

A radar chart (also known as a spider or star chart) is a visualization used to display multivariate data across three or more dimensions, using a consistent scale. Not everyone is a huge fan of these charts, but I think they have their place in comparing entities across a range of dimensions in a visually appealing way.

Get our data

A radar chart is useful when trying to compare the relative weight or importance of different dimensions within one or more entities. The example we'll use here is with cars. Cars have different fuel efficiency, range, acceleration, torque, storage capacity and costs. We can use a radar chart to benchmark specific cars against each other and against the broader population. Let's start with getting our data.

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# For our sample data.
from vega_datasets import data

# Load cars dataset so we can compare cars across
# a few dimensions in the radar plot.
df = data.cars()
df.head()
Acceleration Cylinders Displacement Horsepower Miles_per_Gallon Name Origin Weight_in_lbs Year
0 12.0 8 307.0 130.0 18.0 chevrolet chevelle malibu USA 3504 1970-01-01
1 11.5 8 350.0 165.0 15.0 buick skylark 320 USA 3693 1970-01-01
2 11.0 8 318.0 150.0 18.0 plymouth satellite USA 3436 1970-01-01
3 12.0 8 304.0 150.0 16.0 amc rebel sst USA 3433 1970-01-01
4 10.5 8 302.0 140.0 17.0 ford torino USA 3449 1970-01-01

Each record is a car with its specs across a range of attributes. We need to transform those attributes into a consistent scale, so let's do a linear transformation of each to convert to a 0-100 scale.

# The attributes we want to use in our radar plot.
factors = ['Acceleration', 'Displacement', 'Horsepower',
           'Miles_per_Gallon', 'Weight_in_lbs']

# New scale should be from 0 to 100.
new_max = 100
new_min = 0
new_range = new_max - new_min

# Do a linear transformation on each variable to change value
# to [0, 100].
for factor in factors:
  max_val = df[factor].max()
  min_val = df[factor].min()
  val_range = max_val - min_val
  df[factor + '_Adj'] = df[factor].apply(
      lambda x: (((x - min_val) * new_range) / val_range) + new_min)


# Add the year to the name of the car to differentiate between
# the same model.
df['Car Model'] = df.apply(lambda row: '{} {}'.format(row.Name, row.Year.year), axis=1)

# Trim down to cols we want and rename to be nicer.
dft = df.loc[:, ['Car Model', 'Acceleration_Adj', 'Displacement_Adj',
                 'Horsepower_Adj', 'Miles_per_Gallon_Adj',
                 'Weight_in_lbs_Adj']]

dft.rename(columns={
    'Acceleration_Adj': 'Acceleration',
    'Displacement_Adj': 'Displacement',
    'Horsepower_Adj': 'Horsepower',
    'Miles_per_Gallon_Adj': 'MPG',
    'Weight_in_lbs_Adj': 'Weight'
}, inplace=True)

dft.set_index('Car Model', inplace=True)

dft.head()
Acceleration Displacement Horsepower MPG Weight
Car Model
chevrolet chevelle malibu 1970 23.809524 61.757106 45.652174 23.936170 53.614970
buick skylark 320 1970 20.833333 72.868217 64.673913 15.957447 58.973632
plymouth satellite 1970 17.857143 64.599483 56.521739 23.936170 51.686986
amc rebel sst 1970 23.809524 60.981912 56.521739 18.617021 51.601928
ford torino 1970 14.880952 60.465116 51.086957 21.276596 52.055571

We're all set with the data - we have a car in each row with five attributes, each with a value between zero and 100. Let's create some radar charts.

Building the Radar Chart

Creating a radar chart in Matplotlib is definitely not a straightforward affair, so we'll break it down into a few steps. First, let's get the base figure and our data plotted on a polar (aka circular) axis.

# Each attribute we'll plot in the radar chart.
labels = ['Acceleration', 'Displacement', 'Horsepower', 'MPG', 'Weight']

# Let's look at the 1970 Chevy Impala and plot it.
values = dft.loc['chevrolet impala 1970'].tolist()

# Number of variables we're plotting.
num_vars = len(labels)

# Split the circle into even parts and save the angles
# so we know where to put each axis.
angles = np.linspace(0, 2 * np.pi, num_vars, endpoint=False).tolist()

# The plot is a circle, so we need to "complete the loop"
# and append the start value to the end.
values += values[:1]
angles += angles[:1]

# ax = plt.subplot(polar=True)
fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True))

# Draw the outline of our data.
ax.plot(angles, values, color='red', linewidth=1)
# Fill it in.
ax.fill(angles, values, color='red', alpha=0.25)

matplotlib radar chart

It's a start but still lacking in a few ways. Some things to highlight before we move on. fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True)) is a nice (object-oriented) way to create the circular plot and figure itself, as well as set the size of the overall chart.

We create the data plot itself by sequentially calling ax.plot(), which plots the line outline, and ax.fill() which fills in the shape. The two main arguments are angles, which is a list of the angle radians between each axis emanating from the center, and values, which is a list of the data values.

You can see numerous things are wrong with the chart though - the axes don't align with the shape, there are no labels, and the grid itself seems to have two lines right around 100.

Fix the axes

We'll first fix the axes by using some methods specific to polar plots.

# Fix axis to go in the right order and start at 12 o'clock.
ax.set_theta_offset(np.pi / 2)
ax.set_theta_direction(-1)

# Draw axis lines for each angle and label.
ax.set_thetagrids(np.degrees(angles), labels)

matplotlib radar chart

Better. A few things changed here:

  • Our first axis starts right at 12 o'clock (or zero degrees)
  • Our axes are ordered clockwise, according to the list of attributes we fed in
  • We have labels for both the axes and the gridlines
  • The axes and red shape are now aligned

The axis labels though aren't perfect though; several of them overlap with the grid itself and the alignment could be better. Let's fix that.

# Go through labels and adjust alignment based on where
# it is in the circle.
for label, angle in zip(ax.get_xticklabels(), angles):
  if angle in (0, np.pi):
    label.set_horizontalalignment('center')
  elif 0 < angle < np.pi:
    label.set_horizontalalignment('left')
  else:
    label.set_horizontalalignment('right')

matplotlib radar chart

Better! Still a few subtle problems though. Let's make sure the grid goes from 0 to 100, no more, no less. Let's also move the grid labels (0, 20, ... , 100) slightly so they're centered between the first two axes.

# Ensure radar goes from 0 to 100.
ax.set_ylim(0, 100)
# You can also set gridlines manually like this:
# ax.set_rgrids([20, 40, 60, 80, 100])

# Set position of y-labels (0-100) to be in the middle
# of the first two axes.
ax.set_rlabel_position(180 / num_vars)

matplotlib radar chart

Lastly, let's change the color of the plot and add some styling changes as well as a title for the figure. So all together, that looks like:

# Each attribute we'll plot in the radar chart.
labels = ['Acceleration', 'Displacement', 'Horsepower', 'MPG', 'Weight']

# Let's look at the 1970 Chevy Impala and plot it.
car = 'chevrolet impala 1970'
values = dft.loc[car].tolist()

# Number of variables we're plotting.
num_vars = len(labels)

# Split the circle into even parts and save the angles
# so we know where to put each axis.
angles = np.linspace(0, 2 * np.pi, num_vars, endpoint=False).tolist()

# The plot is a circle, so we need to "complete the loop"
# and append the start value to the end.
values += values[:1]
angles += angles[:1]

# ax = plt.subplot(polar=True)
fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True))

# Draw the outline of our data.
ax.plot(angles, values, color='#1aaf6c', linewidth=1)
# Fill it in.
ax.fill(angles, values, color='#1aaf6c', alpha=0.25)

# Fix axis to go in the right order and start at 12 o'clock.
ax.set_theta_offset(np.pi / 2)
ax.set_theta_direction(-1)

# Draw axis lines for each angle and label.
ax.set_thetagrids(np.degrees(angles), labels)

# Go through labels and adjust alignment based on where
# it is in the circle.
for label, angle in zip(ax.get_xticklabels(), angles):
  if angle in (0, np.pi):
    label.set_horizontalalignment('center')
  elif 0 < angle < np.pi:
    label.set_horizontalalignment('left')
  else:
    label.set_horizontalalignment('right')

# Ensure radar goes from 0 to 100.
ax.set_ylim(0, 100)
# You can also set gridlines manually like this:
# ax.set_rgrids([20, 40, 60, 80, 100])

# Set position of y-labels (0-100) to be in the middle
# of the first two axes.
ax.set_rlabel_position(180 / num_vars)

# Add some custom styling.
# Change the color of the tick labels.
ax.tick_params(colors='#222222')
# Make the y-axis (0-100) labels smaller.
ax.tick_params(axis='y', labelsize=8)
# Change the color of the circular gridlines.
ax.grid(color='#AAAAAA')
# Change the color of the outermost gridline (the spine).
ax.spines['polar'].set_color('#222222')
# Change the background color inside the circle itself.
ax.set_facecolor('#FAFAFA')

# Lastly, give the chart a title and give it some
# padding above the "Acceleration" label.
ax.set_title('1970 Chevy Impala Specs', y=1.08)

matplotlib radar chart

Comparing entities

Radar charts are even more useful when comparing multiple entities. As a final example, we'll add a few more cars to the same plot. To do this, you just call ax.plot() and ax.show() for each record. We create a helper function below to make it a bit more DRY (Don't Repeat Yourself).

Note also that we add label=car_model to each ax.plot() and then call ax.legend() at the very end to add a legend to the chart as well so we can differentiate between the shapes.

# Each attribute we'll plot in the radar chart.
labels = ['Acceleration', 'Displacement', 'Horsepower', 'MPG', 'Weight']

# Number of variables we're plotting.
num_vars = len(labels)

# Split the circle into even parts and save the angles
# so we know where to put each axis.
angles = np.linspace(0, 2 * np.pi, num_vars, endpoint=False).tolist()

# The plot is a circle, so we need to "complete the loop"
# and append the start value to the end.
angles += angles[:1]

# ax = plt.subplot(polar=True)
fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True))

# Helper function to plot each car on the radar chart.
def add_to_radar(car_model, color):
  values = dft.loc[car_model].tolist()
  values += values[:1]
  ax.plot(angles, values, color=color, linewidth=1, label=car_model)
  ax.fill(angles, values, color=color, alpha=0.25)

# Add each car to the chart.
add_to_radar('chevrolet impala 1970', '#1aaf6c')
add_to_radar('peugeot 504 1979', '#429bf4')
add_to_radar('ford granada 1977', '#d42cea')

# Fix axis to go in the right order and start at 12 o'clock.
ax.set_theta_offset(np.pi / 2)
ax.set_theta_direction(-1)

# Draw axis lines for each angle and label.
ax.set_thetagrids(np.degrees(angles), labels)

# Go through labels and adjust alignment based on where
# it is in the circle.
for label, angle in zip(ax.get_xticklabels(), angles):
  if angle in (0, np.pi):
    label.set_horizontalalignment('center')
  elif 0 < angle < np.pi:
    label.set_horizontalalignment('left')
  else:
    label.set_horizontalalignment('right')

# Ensure radar goes from 0 to 100.
ax.set_ylim(0, 100)
# You can also set gridlines manually like this:
# ax.set_rgrids([20, 40, 60, 80, 100])

# Set position of y-labels (0-100) to be in the middle
# of the first two axes.
ax.set_rlabel_position(180 / num_vars)

# Add some custom styling.
# Change the color of the tick labels.
ax.tick_params(colors='#222222')
# Make the y-axis (0-100) labels smaller.
ax.tick_params(axis='y', labelsize=8)
# Change the color of the circular gridlines.
ax.grid(color='#AAAAAA')
# Change the color of the outermost gridline (the spine).
ax.spines['polar'].set_color('#222222')
# Change the background color inside the circle itself.
ax.set_facecolor('#FAFAFA')

# Add title.
ax.set_title('Comparing Cars Across Dimensions', y=1.08)

# Add a legend as well.
ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1))

matplotlib radar chart

Thanks and hopefully this is helpful to get a grasp of radar charts in Matplotlib!